Compare commits
28 Commits
features/r
...
features/w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
754b84b4d7 | ||
|
|
572690e2bc | ||
|
|
303e34299a | ||
|
|
c628de9ede | ||
|
|
e73be6bbab | ||
|
|
c9fd8b02e3 | ||
|
|
e1a63064dc | ||
|
|
42db8255d1 | ||
|
|
14d45cbe90 | ||
|
|
75be11bc96 | ||
|
|
ebf29d0eed | ||
|
|
a391d27b44 | ||
|
|
753c9b8b4b | ||
|
|
744bfefa5c | ||
|
|
850fb16c8c | ||
|
|
72ba805fbb | ||
|
|
3d7b40f029 | ||
|
|
2039a9f13b | ||
|
|
bb8f12dd7a | ||
|
|
40ab1f3665 | ||
|
|
fdef757fd0 | ||
|
|
3608ee1141 | ||
|
|
7fb3d61dff | ||
|
|
e9aa11673b | ||
|
|
03a7aa5daf | ||
|
|
755f20c5c4 | ||
|
|
64772e2e90 | ||
|
|
75a4a426e0 |
13
.tx/config
13
.tx/config
@@ -115,12 +115,6 @@ source_lang = en
|
|||||||
source_file = mayan/apps/events/locale/en/LC_MESSAGES/django.po
|
source_file = mayan/apps/events/locale/en/LC_MESSAGES/django.po
|
||||||
type = PO
|
type = PO
|
||||||
|
|
||||||
[mayan-edms.file_caching-3-0]
|
|
||||||
file_filter = mayan/apps/file_caching/locale/<lang>/LC_MESSAGES/django.po
|
|
||||||
source_lang = en
|
|
||||||
source_file = mayan/apps/file_caching/locale/en/LC_MESSAGES/django.po
|
|
||||||
type = PO
|
|
||||||
|
|
||||||
[mayan-edms.file_metadata-3-0]
|
[mayan-edms.file_metadata-3-0]
|
||||||
file_filter = mayan/apps/file_metadata/locale/<lang>/LC_MESSAGES/django.po
|
file_filter = mayan/apps/file_metadata/locale/<lang>/LC_MESSAGES/django.po
|
||||||
source_lang = en
|
source_lang = en
|
||||||
@@ -228,10 +222,3 @@ file_filter = mayan/apps/user_management/locale/<lang>/LC_MESSAGES/django.po
|
|||||||
source_lang = en
|
source_lang = en
|
||||||
source_file = mayan/apps/user_management/locale/en/LC_MESSAGES/django.po
|
source_file = mayan/apps/user_management/locale/en/LC_MESSAGES/django.po
|
||||||
type = PO
|
type = PO
|
||||||
|
|
||||||
[mayan-edms.weblink-3-0]
|
|
||||||
file_filter = mayan/apps/weblinks/locale/<lang>/LC_MESSAGES/django.po
|
|
||||||
source_lang = en
|
|
||||||
source_file = mayan/apps/weblinks/locale/en/LC_MESSAGES/django.po
|
|
||||||
type = PO
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,3 +8,7 @@
|
|||||||
- Improve source column exclusion. Improve for model subclasses in partial querysets.
|
- Improve source column exclusion. Improve for model subclasses in partial querysets.
|
||||||
- Add sortable index instance label column.
|
- Add sortable index instance label column.
|
||||||
- Add rectangle drawing transformation.
|
- Add rectangle drawing transformation.
|
||||||
|
- Redactions app.
|
||||||
|
- Remove duplicated trashed document preview.
|
||||||
|
- Add label to trashed date and time document source column.
|
||||||
|
- Tag created event fix.
|
||||||
|
|||||||
822
HISTORY.rst
822
HISTORY.rst
File diff suppressed because it is too large
Load Diff
8
Makefile
8
Makefile
@@ -18,7 +18,7 @@ clean-pyc: ## Remove Python artifacts.
|
|||||||
find . -name '*.pyc' -exec rm -f {} +
|
find . -name '*.pyc' -exec rm -f {} +
|
||||||
find . -name '*.pyo' -exec rm -f {} +
|
find . -name '*.pyo' -exec rm -f {} +
|
||||||
find . -name '*~' -exec rm -f {} +
|
find . -name '*~' -exec rm -f {} +
|
||||||
find . -name '__pycache__' -exec rm -R -f {} +
|
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
|
|
||||||
@@ -234,10 +234,10 @@ generate-requirements: ## Generate all requirements files from the project deped
|
|||||||
# Dev server
|
# Dev server
|
||||||
|
|
||||||
runserver: ## Run the development server.
|
runserver: ## Run the development server.
|
||||||
./manage.py runserver --nothreading --settings=mayan.settings.development $(ADDRPORT)
|
./manage.py runserver --settings=mayan.settings.development $(ADDRPORT)
|
||||||
|
|
||||||
runserver_plus: ## Run the Django extension's development server.
|
runserver_plus: ## Run the Django extension's development server.
|
||||||
./manage.py runserver_plus --nothreading --settings=mayan.settings.development $(ADDRPORT)
|
./manage.py runserver_plus --settings=mayan.settings.development $(ADDRPORT)
|
||||||
|
|
||||||
shell_plus: ## Run the shell_plus command.
|
shell_plus: ## Run the shell_plus command.
|
||||||
./manage.py shell_plus --settings=mayan.settings.development
|
./manage.py shell_plus --settings=mayan.settings.development
|
||||||
@@ -258,7 +258,7 @@ test-with-docker-frontend: ## Launch a front end instance that uses the producti
|
|||||||
./manage.py runserver --settings=mayan.settings.staging.docker
|
./manage.py runserver --settings=mayan.settings.staging.docker
|
||||||
|
|
||||||
test-with-docker-worker: ## Launch a worker instance that uses the production-like services.
|
test-with-docker-worker: ## Launch a worker instance that uses the production-like services.
|
||||||
DJANGO_SETTINGS_MODULE=mayan.settings.staging.docker ./manage.py celery worker -A mayan -B -l INFO -O fair
|
./manage.py celery worker --settings=mayan.settings.staging.docker -B -l INFO -O fair
|
||||||
|
|
||||||
docker-mysql-on: ## Launch and initialize a MySQL Docker container.
|
docker-mysql-on: ## Launch and initialize a MySQL Docker container.
|
||||||
docker run -d --name mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=True -e MYSQL_DATABASE=mayan_edms mysql
|
docker run -d --name mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=True -e MYSQL_DATABASE=mayan_edms mysql
|
||||||
|
|||||||
72
contrib/scripts/install/development.sh
Normal file
72
contrib/scripts/install/development.sh
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
INSTALLATION_DIRECTORY=/home/vagrant/mayan-edms/
|
||||||
|
DB_NAME=mayan_edms
|
||||||
|
DB_PASSWORD=test123
|
||||||
|
|
||||||
|
cat << EOF | sudo tee -a /etc/motd.tail
|
||||||
|
**********************************sudo apt
|
||||||
|
|
||||||
|
Mayan EDMS Vagrant Development Box
|
||||||
|
|
||||||
|
**********************************
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Update sources
|
||||||
|
echo -e "\n -> Running apt-get update & upgrade \n"
|
||||||
|
sudo apt-get -qq update
|
||||||
|
sudo apt-get -y upgrade
|
||||||
|
|
||||||
|
echo -e "\n -> Installing core binaries \n"
|
||||||
|
sudo apt-get -y install git-core python-virtualenv gcc python-dev libjpeg-dev libpng-dev libtiff-dev tesseract-ocr poppler-utils libreoffice
|
||||||
|
|
||||||
|
echo -e "\n -> Cloning development branch of repository \n"
|
||||||
|
git clone /mayan-edms-repository/ $INSTALLATION_DIRECTORY
|
||||||
|
cd $INSTALLATION_DIRECTORY
|
||||||
|
git checkout development
|
||||||
|
git reset HEAD --hard
|
||||||
|
|
||||||
|
echo -e "\n -> Setting up virtual env \n"
|
||||||
|
virtualenv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
echo -e "\n -> Installing python dependencies \n"
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
echo -e "\n -> Running Mayan EDMS initial setup \n"
|
||||||
|
./manage.py initialsetup
|
||||||
|
|
||||||
|
echo -e "\n -> Installing Redis server \n"
|
||||||
|
sudo apt-get install -y redis-server
|
||||||
|
pip install redis
|
||||||
|
|
||||||
|
echo -e "\n -> Installing testing software \n"
|
||||||
|
pip install coverage
|
||||||
|
|
||||||
|
echo -e "\n -> Installing MySQL \n"
|
||||||
|
sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password password '$DB_PASSWORD
|
||||||
|
sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password '$DB_PASSWORD
|
||||||
|
sudo apt-get install -y mysql-server libmysqlclient-dev
|
||||||
|
# Create a passwordless root and travis users
|
||||||
|
mysql -u root -p$DB_PASSWORD -e "SET PASSWORD = PASSWORD('');"
|
||||||
|
mysql -u root -e "CREATE USER 'travis'@'localhost' IDENTIFIED BY '';GRANT ALL PRIVILEGES ON * . * TO 'travis'@'localhost';FLUSH PRIVILEGES;"
|
||||||
|
mysql -u travis -e "CREATE DATABASE $DB_NAME;"
|
||||||
|
pip install mysql-python
|
||||||
|
|
||||||
|
echo -e "\n -> Installing PostgreSQL \n"
|
||||||
|
sudo apt-get install -y postgresql postgresql-server-dev-all
|
||||||
|
sudo -u postgres psql -c 'create database mayan_edms;' -U postgres
|
||||||
|
sudo cat > /etc/postgresql/9.3/main/pg_hba.conf << EOF
|
||||||
|
local all postgres trust
|
||||||
|
|
||||||
|
# TYPE DATABASE USER ADDRESS METHOD
|
||||||
|
|
||||||
|
# "local" is for Unix domain socket connections only
|
||||||
|
local all all peer
|
||||||
|
# IPv4 local connections:
|
||||||
|
host all all 127.0.0.1/32 md5
|
||||||
|
# IPv6 local connections:
|
||||||
|
host all all ::1/128 md5
|
||||||
|
EOF
|
||||||
|
|
||||||
|
pip install -q psycopg2
|
||||||
1733
contrib/scripts/install/dialog.sh
Normal file
1733
contrib/scripts/install/dialog.sh
Normal file
File diff suppressed because it is too large
Load Diff
171
contrib/scripts/install/production.sh
Normal file
171
contrib/scripts/install/production.sh
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# ====== CONFIG ======
|
||||||
|
INSTALLATION_DIRECTORY=/usr/share/mayan-edms/
|
||||||
|
DB_NAME=mayan_edms
|
||||||
|
DB_USERNAME=mayan
|
||||||
|
DB_PASSWORD=test123
|
||||||
|
# ==== END CONFIG ====
|
||||||
|
|
||||||
|
cat << EOF | tee -a /etc/motd.tail
|
||||||
|
**********************************
|
||||||
|
|
||||||
|
Mayan EDMS Vagrant Production Box
|
||||||
|
|
||||||
|
**********************************
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo -e "\n -> Running apt-get update & upgrade \n"
|
||||||
|
apt-get -qq update
|
||||||
|
apt-get -y upgrade
|
||||||
|
|
||||||
|
echo -e "\n -> Installing core binaries \n"
|
||||||
|
apt-get install nginx supervisor redis-server postgresql libpq-dev libjpeg-dev libmagic1 libpng-dev libreoffice libtiff-dev gcc ghostscript gpgv python-dev python-virtualenv tesseract-ocr poppler-utils -y
|
||||||
|
|
||||||
|
echo -e "\n -> Setting up virtualenv \n"
|
||||||
|
rm -f ${INSTALLATION_DIRECTORY}
|
||||||
|
virtualenv ${INSTALLATION_DIRECTORY}
|
||||||
|
source ${INSTALLATION_DIRECTORY}bin/activate
|
||||||
|
|
||||||
|
echo -e "\n -> Installing Mayan EDMS from PyPI \n"
|
||||||
|
pip install mayan-edms
|
||||||
|
|
||||||
|
echo -e "\n -> Installing Python client for PostgreSQL, Redis, and uWSGI \n"
|
||||||
|
pip install psycopg2 redis uwsgi
|
||||||
|
|
||||||
|
echo -e "\n -> Creating the database for the installation \n"
|
||||||
|
echo "CREATE USER mayan WITH PASSWORD '$DB_PASSWORD';" | sudo -u postgres psql
|
||||||
|
sudo -u postgres createdb -O $DB_USERNAME $DB_NAME
|
||||||
|
|
||||||
|
echo -e "\n -> Creating the directories for the logs \n"
|
||||||
|
mkdir /var/log/mayan
|
||||||
|
|
||||||
|
echo -e "\n -> Making a convenience symlink \n"
|
||||||
|
cd ${INSTALLATION_DIRECTORY}
|
||||||
|
ln -s lib/python2.7/site-packages/mayan .
|
||||||
|
|
||||||
|
echo -e "\n -> Creating an initial settings file \n"
|
||||||
|
mayan-edms.py createsettings
|
||||||
|
|
||||||
|
echo -e "\n -> Updating the mayan/settings/local.py file \n"
|
||||||
|
cat >> mayan/settings/local.py << EOF
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
|
'NAME': '$DB_NAME',
|
||||||
|
'USER': '$DB_USERNAME',
|
||||||
|
'PASSWORD': '$DB_PASSWORD',
|
||||||
|
'HOST': 'localhost',
|
||||||
|
'PORT': '5432',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BROKER_URL = 'redis://127.0.0.1:6379/0'
|
||||||
|
CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0'
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo -e "\n -> Migrating the database or initialize the project \n"
|
||||||
|
mayan-edms.py initialsetup
|
||||||
|
|
||||||
|
echo -e "\n -> Disabling the default NGINX site \n"
|
||||||
|
rm -f /etc/nginx/sites-enabled/default
|
||||||
|
|
||||||
|
echo -e "\n -> Creating a uwsgi.ini file \n"
|
||||||
|
cat > uwsgi.ini << EOF
|
||||||
|
[uwsgi]
|
||||||
|
chdir = ${INSTALLATION_DIRECTORY}lib/python2.7/site-packages/mayan
|
||||||
|
chmod-socket = 664
|
||||||
|
chown-socket = www-data:www-data
|
||||||
|
env = DJANGO_SETTINGS_MODULE=mayan.settings.production
|
||||||
|
gid = www-data
|
||||||
|
logto = /var/log/uwsgi/%n.log
|
||||||
|
pythonpath = ${INSTALLATION_DIRECTORY}lib/python2.7/site-packages
|
||||||
|
master = True
|
||||||
|
max-requests = 5000
|
||||||
|
socket = ${INSTALLATION_DIRECTORY}uwsgi.sock
|
||||||
|
uid = www-data
|
||||||
|
vacuum = True
|
||||||
|
wsgi-file = ${INSTALLATION_DIRECTORY}lib/python2.7/site-packages/mayan/wsgi.py
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo -e "\n -> Creating the directory for the uWSGI log files \n"
|
||||||
|
mkdir -p /var/log/uwsgi
|
||||||
|
|
||||||
|
echo -e "\n -> Creating the NGINX site file for Mayan EDMS, /etc/nginx/sites-available/mayan \n"
|
||||||
|
cat > /etc/nginx/sites-available/mayan << EOF
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
include uwsgi_params;
|
||||||
|
uwsgi_pass unix:${INSTALLATION_DIRECTORY}uwsgi.sock;
|
||||||
|
|
||||||
|
client_max_body_size 30M; # Increse if your plan to upload bigger documents
|
||||||
|
proxy_read_timeout 30s; # Increase if your document uploads take more than 30 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
location /static {
|
||||||
|
alias ${INSTALLATION_DIRECTORY}mayan/media/static;
|
||||||
|
expires 1h;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /favicon.ico {
|
||||||
|
alias ${INSTALLATION_DIRECTORY}mayan/media/static/appearance/images/favicon.ico;
|
||||||
|
expires 1h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo -e "\n -> Enabling the NGINX site for Mayan EDMS \n"
|
||||||
|
ln -s /etc/nginx/sites-available/mayan /etc/nginx/sites-enabled/
|
||||||
|
|
||||||
|
echo -e "\n -> Creating the supervisor file for the uWSGI process, /etc/supervisor/conf.d/mayan-uwsgi.conf \n"
|
||||||
|
cat > /etc/supervisor/conf.d/mayan-uwsgi.conf << EOF
|
||||||
|
[program:mayan-uwsgi]
|
||||||
|
command = ${INSTALLATION_DIRECTORY}bin/uwsgi --ini ${INSTALLATION_DIRECTORY}uwsgi.ini
|
||||||
|
user = root
|
||||||
|
autostart = true
|
||||||
|
autorestart = true
|
||||||
|
redirect_stderr = true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo -e "\n -> Creating the supervisor file for the Celery worker, /etc/supervisor/conf.d/mayan-celery.conf \n"
|
||||||
|
cat > /etc/supervisor/conf.d/mayan-celery.conf << EOF
|
||||||
|
[program:mayan-worker]
|
||||||
|
command = ${INSTALLATION_DIRECTORY}bin/python ${INSTALLATION_DIRECTORY}bin/mayan-edms.py celery --settings=mayan.settings.production worker -Ofair -l ERROR
|
||||||
|
directory = ${INSTALLATION_DIRECTORY}
|
||||||
|
user = www-data
|
||||||
|
stdout_logfile = /var/log/mayan/worker-stdout.log
|
||||||
|
stderr_logfile = /var/log/mayan/worker-stderr.log
|
||||||
|
autostart = true
|
||||||
|
autorestart = true
|
||||||
|
startsecs = 10
|
||||||
|
stopwaitsecs = 10
|
||||||
|
killasgroup = true
|
||||||
|
priority = 998
|
||||||
|
|
||||||
|
[program:mayan-beat]
|
||||||
|
command = ${INSTALLATION_DIRECTORY}bin/python ${INSTALLATION_DIRECTORY}bin/mayan-edms.py celery --settings=mayan.settings.production beat -l ERROR
|
||||||
|
directory = ${INSTALLATION_DIRECTORY}
|
||||||
|
user = www-data
|
||||||
|
numprocs = 1
|
||||||
|
stdout_logfile = /var/log/mayan/beat-stdout.log
|
||||||
|
stderr_logfile = /var/log/mayan/beat-stderr.log
|
||||||
|
autostart = true
|
||||||
|
autorestart = true
|
||||||
|
startsecs = 10
|
||||||
|
stopwaitsecs = 1
|
||||||
|
killasgroup = true
|
||||||
|
priority = 998
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo -e "\n -> Collecting the static files \n"
|
||||||
|
mayan-edms.py preparestatic --noinput
|
||||||
|
|
||||||
|
echo -e "\n -> Making the installation directory readable and writable by the webserver user \n"
|
||||||
|
chown www-data:www-data ${INSTALLATION_DIRECTORY} -R
|
||||||
|
|
||||||
|
echo -e "\n -> Restarting the services \n"
|
||||||
|
/etc/init.d/nginx restart
|
||||||
|
/etc/init.d/supervisor restart
|
||||||
@@ -13,12 +13,11 @@ APP_LIST = (
|
|||||||
'checkouts', 'common', 'converter', 'dashboards', 'dependencies',
|
'checkouts', 'common', 'converter', 'dashboards', 'dependencies',
|
||||||
'django_gpg', 'document_comments', 'document_indexing',
|
'django_gpg', 'document_comments', 'document_indexing',
|
||||||
'document_parsing', 'document_signatures', 'document_states',
|
'document_parsing', 'document_signatures', 'document_states',
|
||||||
'documents', 'dynamic_search', 'events', 'file_caching',
|
'documents', 'dynamic_search', 'events', 'file_metadata', 'linking',
|
||||||
'file_metadata', 'linking', 'lock_manager', 'mailer',
|
'lock_manager', 'mayan_statistics', 'mailer', 'metadata', 'mirroring',
|
||||||
'mayan_statistics', 'metadata', 'mirroring', 'motd', 'navigation',
|
'motd', 'navigation', 'ocr', 'permissions', 'platform', 'rest_api',
|
||||||
'ocr', 'permissions', 'platform', 'rest_api', 'smart_settings',
|
'smart_settings', 'sources', 'storage', 'tags', 'task_manager',
|
||||||
'sources', 'storage', 'tags', 'task_manager', 'user_management',
|
'user_management'
|
||||||
'weblinks'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
LANGUAGE_LIST = (
|
LANGUAGE_LIST = (
|
||||||
|
|||||||
35
contrib/scripts/start_gunicorn.sh
Normal file
35
contrib/scripts/start_gunicorn.sh
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
NAME="mayan-edms"
|
||||||
|
DJANGODIR=/usr/share/mayan-edms
|
||||||
|
SOCKFILE=/var/tmp/filesystem.sock
|
||||||
|
USER=www-data
|
||||||
|
GROUP=www-data
|
||||||
|
NUM_WORKERS=3
|
||||||
|
DJANGO_SETTINGS_MODULE=mayan.settings.production
|
||||||
|
DJANGO_WSGI_MODULE=mayan.wsgi
|
||||||
|
TIMEOUT=600
|
||||||
|
|
||||||
|
echo "Starting $NAME as `whoami`"
|
||||||
|
|
||||||
|
# Activate the virtual environment
|
||||||
|
cd $DJANGODIR
|
||||||
|
source bin/activate
|
||||||
|
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
|
||||||
|
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
|
||||||
|
|
||||||
|
# Create the run directory if it doesn't exist
|
||||||
|
RUNDIR=$(dirname $SOCKFILE)
|
||||||
|
test -d $RUNDIR || mkdir -p $RUNDIR
|
||||||
|
|
||||||
|
# Start your Django Unicorn
|
||||||
|
# Programs meant to be run under supervisor should not daemonize themselves (do not use --daemon)
|
||||||
|
exec bin/gunicorn ${DJANGO_WSGI_MODULE}:application \
|
||||||
|
--name $NAME \
|
||||||
|
--workers $NUM_WORKERS \
|
||||||
|
--user=$USER --group=$GROUP \
|
||||||
|
--log-level=debug \
|
||||||
|
--bind=unix:$SOCKFILE \
|
||||||
|
--timeout=$TIMEOUT
|
||||||
|
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
# BASE_IMAGE - Bare bones image with the base packages needed to run Mayan EDMS
|
# BASE_IMAGE - Bare bones image with the base packages needed to run Mayan EDMS
|
||||||
####
|
####
|
||||||
|
|
||||||
FROM debian:10.0-slim as BASE_IMAGE
|
FROM debian:9.8-slim as BASE_IMAGE
|
||||||
|
|
||||||
LABEL maintainer="Roberto Rosario roberto.rosario@mayan-edms.com"
|
LABEL maintainer="Roberto Rosario roberto.rosario@mayan-edms.com"
|
||||||
|
|
||||||
@@ -22,7 +22,6 @@ RUN set -x \
|
|||||||
&& DEBIAN_FRONTEND=noninteractive \
|
&& DEBIAN_FRONTEND=noninteractive \
|
||||||
apt-get update \
|
apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
ca-certificates \
|
|
||||||
exiftool \
|
exiftool \
|
||||||
ghostscript \
|
ghostscript \
|
||||||
gpgv \
|
gpgv \
|
||||||
@@ -30,11 +29,11 @@ apt-get update \
|
|||||||
graphviz \
|
graphviz \
|
||||||
libfuse2 \
|
libfuse2 \
|
||||||
libmagic1 \
|
libmagic1 \
|
||||||
libmariadb3 \
|
libmariadbclient18 \
|
||||||
libreoffice \
|
libreoffice \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
python3-distutils \
|
redis-server \
|
||||||
sane-utils \
|
sane-utils \
|
||||||
sudo \
|
sudo \
|
||||||
supervisor \
|
supervisor \
|
||||||
@@ -53,20 +52,22 @@ apt-get update \
|
|||||||
&& if [ "$(uname -m)" = "armv7l" ]; then \
|
&& if [ "$(uname -m)" = "armv7l" ]; then \
|
||||||
ln -s /usr/lib/arm-linux-gnueabihf/libz.so /usr/lib/ \
|
ln -s /usr/lib/arm-linux-gnueabihf/libz.so /usr/lib/ \
|
||||||
&& ln -s /usr/lib/arm-linux-gnueabihf/libjpeg.so /usr/lib/ \
|
&& ln -s /usr/lib/arm-linux-gnueabihf/libjpeg.so /usr/lib/ \
|
||||||
; fi
|
; fi \
|
||||||
|
# Discard data when Redis runs out of memory
|
||||||
|
&& echo "maxmemory-policy allkeys-lru" >> /etc/redis/redis.conf \
|
||||||
|
# Disable saving the Redis database
|
||||||
|
echo "save \"\"" >> /etc/redis/redis.conf \
|
||||||
|
# Only provision 1 database
|
||||||
|
&& echo "databases 1" >> /etc/redis/redis.conf
|
||||||
|
|
||||||
|
|
||||||
####
|
####
|
||||||
# BUILDER_IMAGE - This image builds the Python package and is discarded afterwards
|
# BUILDER_IMAGE - This image buildS the Python package and is discarded afterwards
|
||||||
# only the build artifact is carried over to the next image.
|
|
||||||
####
|
####
|
||||||
|
|
||||||
# Reuse image
|
# Reuse image
|
||||||
FROM BASE_IMAGE as BUILDER_IMAGE
|
FROM BASE_IMAGE as BUILDER_IMAGE
|
||||||
|
|
||||||
# Python libraries caching
|
|
||||||
ARG PIP_INDEX_URL
|
|
||||||
ARG PIP_TRUSTED_HOST
|
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
# Copy the source files needed to build the Python package
|
# Copy the source files needed to build the Python package
|
||||||
@@ -95,40 +96,39 @@ apt-get install -y --no-install-recommends \
|
|||||||
libssl-dev \
|
libssl-dev \
|
||||||
g++ \
|
g++ \
|
||||||
gcc \
|
gcc \
|
||||||
python3-dev \
|
python-dev \
|
||||||
python3-venv \
|
python-virtualenv \
|
||||||
&& mkdir -p "${PROJECT_INSTALL_DIR}" \
|
&& mkdir -p "${PROJECT_INSTALL_DIR}" \
|
||||||
&& chown -R mayan:mayan "${PROJECT_INSTALL_DIR}" \
|
&& chown -R mayan:mayan "${PROJECT_INSTALL_DIR}" \
|
||||||
&& chown -R mayan:mayan /src
|
&& chown -R mayan:mayan /src
|
||||||
|
|
||||||
USER mayan
|
USER mayan
|
||||||
RUN python3 -m venv "${PROJECT_INSTALL_DIR}" \
|
RUN python -m virtualenv "${PROJECT_INSTALL_DIR}" \
|
||||||
&& . "${PROJECT_INSTALL_DIR}/bin/activate" \
|
&& . "${PROJECT_INSTALL_DIR}/bin/activate" \
|
||||||
&& pip install --no-cache-dir \
|
&& pip install --no-cache-dir --no-use-pep517 \
|
||||||
librabbitmq==2.0.0 \
|
librabbitmq==1.6.1 \
|
||||||
mysqlclient==1.4.2.post1 \
|
mysql-python==1.2.5 \
|
||||||
psycopg2==2.8.3 \
|
psycopg2==2.7.3.2 \
|
||||||
redis==3.2.1 \
|
redis==2.10.6 \
|
||||||
flower==0.9.3 \
|
|
||||||
# psutil is needed by ARM builds otherwise gevent and gunicorn fail to start
|
# psutil is needed by ARM builds otherwise gevent and gunicorn fail to start
|
||||||
&& UNAME=`uname -m` && if [ "${UNAME#*arm}" != $UNAME ]; then \
|
&& UNAME=`uname -m` && if [ "${UNAME#*arm}" != $UNAME ]; then \
|
||||||
pip install --no-cache-dir \
|
pip install --no-cache-dir --no-use-pep517 \
|
||||||
psutil==5.6.2 \
|
psutil==5.6.2 \
|
||||||
; fi \
|
; fi \
|
||||||
# Install the Python packages needed to build Mayan EDMS
|
# Install the Python packages needed to build Mayan EDMS
|
||||||
&& pip install --no-cache-dir -r /src/requirements/build.txt \
|
&& pip install --no-cache-dir --no-use-pep517 -r /src/requirements/build.txt \
|
||||||
# Build Mayan EDMS
|
# Build Mayan EDMS
|
||||||
&& python3 setup.py sdist \
|
&& python setup.py sdist \
|
||||||
# Install the built Mayan EDMS package
|
# Install the built Mayan EDMS package
|
||||||
&& pip install --no-cache-dir dist/mayan* \
|
&& pip install --no-cache-dir --no-use-pep517 dist/mayan* \
|
||||||
# Install the static content
|
# Install the static content
|
||||||
&& mayan-edms.py installdependencies \
|
&& mayan-edms.py installjavascript \
|
||||||
&& MAYAN_STATIC_ROOT=${PROJECT_INSTALL_DIR}/static mayan-edms.py preparestatic --link --noinput
|
&& MAYAN_STATIC_ROOT=${PROJECT_INSTALL_DIR}/static mayan-edms.py preparestatic --link --noinput
|
||||||
|
|
||||||
COPY --chown=mayan:mayan requirements/testing-base.txt "${PROJECT_INSTALL_DIR}"
|
COPY --chown=mayan:mayan requirements/testing-base.txt "${PROJECT_INSTALL_DIR}"
|
||||||
|
|
||||||
####
|
####
|
||||||
# Final image - BASE_IMAGE + BUILDER_IMAGE artifact (Mayan install directory)
|
# Final image - BASE_IMAGE + Mayan install directory from the builder image
|
||||||
####
|
####
|
||||||
|
|
||||||
FROM BASE_IMAGE
|
FROM BASE_IMAGE
|
||||||
@@ -144,7 +144,7 @@ VOLUME ["/var/lib/mayan"]
|
|||||||
ENTRYPOINT ["entrypoint.sh"]
|
ENTRYPOINT ["entrypoint.sh"]
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
CMD ["run_all"]
|
CMD ["mayan"]
|
||||||
|
|
||||||
RUN ${PROJECT_INSTALL_DIR}/bin/mayan-edms.py platformtemplate supervisord_docker > /etc/supervisor/conf.d/mayan.conf \
|
RUN ${PROJECT_INSTALL_DIR}/bin/mayan-edms.py platformtemplate supervisord_docker > /etc/supervisor/conf.d/mayan.conf \
|
||||||
&& apt-get clean autoclean \
|
&& apt-get clean autoclean \
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
HOST_IP = `/sbin/ip route|awk '/docker0/ { print $$9 }'`
|
APT_PROXY ?= `/sbin/ip route|awk '/docker0/ { print $$9 }'`:3142
|
||||||
|
|
||||||
APT_PROXY ?= $(HOST_IP):3142
|
|
||||||
PIP_INDEX_URL ?= http://$(HOST_IP):3141/root/pypi/+simple/
|
|
||||||
PIP_TRUSTED_HOST ?= $(HOST_IP)
|
|
||||||
|
|
||||||
IMAGE_VERSION ?= `cat docker/rootfs/version`
|
IMAGE_VERSION ?= `cat docker/rootfs/version`
|
||||||
CONSOLE_COLUMNS ?= `echo $$(tput cols)`
|
CONSOLE_COLUMNS ?= `echo $$(tput cols)`
|
||||||
CONSOLE_LINES ?= `echo $$(tput lines)`
|
CONSOLE_LINES ?= `echo $$(tput lines)`
|
||||||
@@ -12,7 +7,7 @@ docker-build: ## Build a new image locally.
|
|||||||
docker build -t mayanedms/mayanedms:$(IMAGE_VERSION) -f docker/Dockerfile .
|
docker build -t mayanedms/mayanedms:$(IMAGE_VERSION) -f docker/Dockerfile .
|
||||||
|
|
||||||
docker-build-with-proxy: ## Build a new image locally using an APT proxy as APT_PROXY.
|
docker-build-with-proxy: ## Build a new image locally using an APT proxy as APT_PROXY.
|
||||||
docker build -t mayanedms/mayanedms:$(IMAGE_VERSION) -f docker/Dockerfile --build-arg APT_PROXY=$(APT_PROXY) --build-arg PIP_INDEX_URL=$(PIP_INDEX_URL) --build-arg PIP_TRUSTED_HOST=$(PIP_TRUSTED_HOST) --build-arg HTTP_PROXY=$(HTTP_PROXY) --build-arg HTTPS_PROXY=$(HTTPS_PROXY) .
|
docker build -t mayanedms/mayanedms:$(IMAGE_VERSION) -f docker/Dockerfile --build-arg APT_PROXY=$(APT_PROXY) .
|
||||||
|
|
||||||
docker-shell: ## Launch a bash instance inside a running container. Pass the container name via DOCKER_CONTAINER.
|
docker-shell: ## Launch a bash instance inside a running container. Pass the container name via DOCKER_CONTAINER.
|
||||||
docker exec -e TERM=$(TERM) -e "COLUMNS=$(CONSOLE_COLUMNS)" -e "LINES=$(CONSOLE_LINES)" -it $(DOCKER_CONTAINER) /bin/bash
|
docker exec -e TERM=$(TERM) -e "COLUMNS=$(CONSOLE_COLUMNS)" -e "LINES=$(CONSOLE_LINES)" -it $(DOCKER_CONTAINER) /bin/bash
|
||||||
@@ -28,13 +23,3 @@ docker-test-cleanup: ## Delete the test container and the test volume.
|
|||||||
docker-test-all: ## Build and executed the test suite in a test container.
|
docker-test-all: ## Build and executed the test suite in a test container.
|
||||||
docker-test-all: docker-build-with-proxy
|
docker-test-all: docker-build-with-proxy
|
||||||
docker run --rm run-tests
|
docker run --rm run-tests
|
||||||
|
|
||||||
docker-compose-build:
|
|
||||||
docker-compose -f docker/docker-compose.yml -p mayan-edms build
|
|
||||||
|
|
||||||
docker-compose-build-with-proxy:
|
|
||||||
docker-compose -f docker/docker-compose.yml -p mayan-edms build --build-arg APT_PROXY=$(APT_PROXY) --build-arg PIP_INDEX_URL=$(PIP_INDEX_URL) --build-arg PIP_TRUSTED_HOST=$(PIP_TRUSTED_HOST) --build-arg HTTP_PROXY=$(HTTP_PROXY) --build-arg HTTPS_PROXY=$(HTTPS_PROXY)
|
|
||||||
|
|
||||||
docker-compose-up:
|
|
||||||
docker-compose -f docker/docker-compose.yml -p mayan-edms up
|
|
||||||
|
|
||||||
|
|||||||
72
docker/docker-compose-development.yml
Executable file
72
docker/docker-compose-development.yml
Executable file
@@ -0,0 +1,72 @@
|
|||||||
|
version: '2.1'
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
broker:
|
||||||
|
driver: local
|
||||||
|
app:
|
||||||
|
driver: local
|
||||||
|
db:
|
||||||
|
driver: local
|
||||||
|
results:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
services:
|
||||||
|
broker:
|
||||||
|
container_name: mayan-edms-broker
|
||||||
|
image: healthcheck/rabbitmq
|
||||||
|
environment:
|
||||||
|
RABBITMQ_DEFAULT_USER: mayan
|
||||||
|
RABBITMQ_DEFAULT_PASS: mayan
|
||||||
|
RABBITMQ_DEFAULT_VHOST: mayan
|
||||||
|
volumes:
|
||||||
|
- broker:/var/lib/rabbitmq
|
||||||
|
results:
|
||||||
|
container_name: mayan-edms-results
|
||||||
|
image: healthcheck/redis
|
||||||
|
volumes:
|
||||||
|
- results:/data
|
||||||
|
#db:
|
||||||
|
# container_name: mayan-edms-db
|
||||||
|
# image: healthcheck/mysql
|
||||||
|
# environment:
|
||||||
|
# MYSQL_DATABASE: mayan
|
||||||
|
# MYSQL_PASSWORD: mayan-password
|
||||||
|
# MYSQL_ROOT_PASSWORD: root-password
|
||||||
|
# MYSQL_USER: mayan
|
||||||
|
# volumes:
|
||||||
|
# - db:/var/lib/mysql
|
||||||
|
db:
|
||||||
|
container_name: mayan-edms-db
|
||||||
|
image: healthcheck/postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: mayan
|
||||||
|
POSTGRES_PASSWORD: mayan-password
|
||||||
|
POSTGRES_USER: mayan
|
||||||
|
volumes:
|
||||||
|
- db:/var/lib/postgresql/data
|
||||||
|
mayan-edms:
|
||||||
|
container_name: mayan-edms-app
|
||||||
|
image: mayan-edms/next
|
||||||
|
build:
|
||||||
|
context: ./
|
||||||
|
args:
|
||||||
|
- APT_PROXY=172.18.0.1:3142
|
||||||
|
depends_on:
|
||||||
|
broker:
|
||||||
|
condition: service_healthy
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
results:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
MAYAN_BROKER_URL: amqp://mayan:mayan@broker:5672/mayan
|
||||||
|
MAYAN_CELERY_RESULT_BACKEND: redis://results:6379/0
|
||||||
|
MAYAN_DATABASE_ENGINE: django.db.backends.postgresql
|
||||||
|
MAYAN_DATABASE_HOST: db
|
||||||
|
MAYAN_DATABASE_NAME: mayan
|
||||||
|
MAYAN_DATABASE_PASSWORD: mayan-password
|
||||||
|
MAYAN_DATABASE_USER: mayan
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- app:/var/lib/mayan
|
||||||
@@ -1,130 +1,58 @@
|
|||||||
version: '3.7'
|
version: '2.1'
|
||||||
|
|
||||||
networks:
|
volumes:
|
||||||
mayan-bridge:
|
broker:
|
||||||
driver: bridge
|
driver: local
|
||||||
|
app:
|
||||||
|
driver: local
|
||||||
|
db:
|
||||||
|
driver: local
|
||||||
|
results:
|
||||||
|
driver: local
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
broker:
|
||||||
build:
|
container_name: mayan-edms-broker
|
||||||
context: ..
|
image: healthcheck/rabbitmq
|
||||||
dockerfile: ./docker/Dockerfile
|
environment:
|
||||||
depends_on:
|
RABBITMQ_DEFAULT_USER: mayan
|
||||||
- postgresql
|
RABBITMQ_DEFAULT_PASS: mayan
|
||||||
- redis
|
RABBITMQ_DEFAULT_VHOST: mayan
|
||||||
# Enable to use RabbitMQ
|
|
||||||
#- rabbitmq
|
|
||||||
environment: &mayan_env
|
|
||||||
# Enable to use RabbitMQ
|
|
||||||
# MAYAN_CELERY_BROKER_URL: amqp://mayan:mayanrabbitpass@broker:5672/mayan
|
|
||||||
# Disable Redis Broker to use RabbitMQ as Broker
|
|
||||||
MAYAN_CELERY_BROKER_URL: redis://redis:6379/1
|
|
||||||
MAYAN_CELERY_RESULT_BACKEND: redis://redis:6379/0
|
|
||||||
MAYAN_DATABASES: "{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayandbpass','USER':'mayan','HOST':'postgresql'}}"
|
|
||||||
image: mayanedms/mayanedms:3.2.6
|
|
||||||
networks:
|
|
||||||
- mayan-bridge
|
|
||||||
ports:
|
|
||||||
- "80:8000"
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
volumes:
|
||||||
- /docker-volumes/mayan-edms/media:/var/lib/mayan
|
- broker:/var/lib/rabbitmq
|
||||||
|
results:
|
||||||
postgresql:
|
container_name: mayan-edms-results
|
||||||
|
image: healthcheck/redis
|
||||||
|
volumes:
|
||||||
|
- results:/data
|
||||||
|
db:
|
||||||
|
container_name: mayan-edms-db
|
||||||
|
image: healthcheck/postgres
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: mayan
|
POSTGRES_DB: mayan
|
||||||
POSTGRES_PASSWORD: mayandbpass
|
POSTGRES_PASSWORD: mayan-password
|
||||||
POSTGRES_USER: mayan
|
POSTGRES_USER: mayan
|
||||||
image: postgres:9.6
|
|
||||||
networks:
|
|
||||||
- mayan-bridge
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
volumes:
|
||||||
- /docker-volumes/mayan-edms/postgres:/var/lib/postgresql/data
|
- db:/var/lib/postgresql/data
|
||||||
|
mayan-edms:
|
||||||
redis:
|
container_name: mayan-edms-app
|
||||||
command:
|
image: mayanedms/mayanedms:latest
|
||||||
- redis-server
|
depends_on:
|
||||||
- --databases
|
broker:
|
||||||
- "2"
|
condition: service_healthy
|
||||||
- --maxmemory-policy
|
db:
|
||||||
- allkeys-lru
|
condition: service_healthy
|
||||||
- --save
|
results:
|
||||||
- ""
|
condition: service_healthy
|
||||||
image: redis:5.0
|
environment:
|
||||||
networks:
|
MAYAN_BROKER_URL: amqp://mayan:mayan@broker:5672/mayan
|
||||||
- mayan-bridge
|
MAYAN_CELERY_RESULT_BACKEND: redis://results:6379/0
|
||||||
restart: unless-stopped
|
MAYAN_DATABASE_ENGINE: django.db.backends.postgresql
|
||||||
|
MAYAN_DATABASE_HOST: db
|
||||||
# Optional services
|
MAYAN_DATABASE_NAME: mayan
|
||||||
|
MAYAN_DATABASE_PASSWORD: mayan-password
|
||||||
# celery_flower:
|
MAYAN_DATABASE_USER: mayan
|
||||||
# command:
|
ports:
|
||||||
# - run_celery
|
- "80:8000"
|
||||||
# - flower
|
volumes:
|
||||||
# depends_on:
|
- app:/var/lib/mayan
|
||||||
# - postgresql
|
|
||||||
# - redis
|
|
||||||
# # Enable to use RabbitMQ
|
|
||||||
# # - rabbitmq
|
|
||||||
# environment:
|
|
||||||
# <<: *mayan_env
|
|
||||||
# image: mayanedms/mayanedms:3.2.6
|
|
||||||
# networks:
|
|
||||||
# - mayan-bridge
|
|
||||||
# ports:
|
|
||||||
# - "5555:5555"
|
|
||||||
# restart: unless-stopped
|
|
||||||
|
|
||||||
# Enable to use RabbitMQ
|
|
||||||
# rabbitmq:
|
|
||||||
# container_name: mayan-edms-rabbitmq
|
|
||||||
# image: healthcheck/rabbitmq
|
|
||||||
# environment:
|
|
||||||
# RABBITMQ_DEFAULT_USER: mayan
|
|
||||||
# RABBITMQ_DEFAULT_PASS: mayanrabbitpass
|
|
||||||
# RABBITMQ_DEFAULT_VHOST: mayan
|
|
||||||
# networks:
|
|
||||||
# - mayan-bridge
|
|
||||||
# restart: unless-stopped
|
|
||||||
# volumes:
|
|
||||||
# - /docker-volumes/mayan-edms/rabbitmq:/var/lib/rabbitmq
|
|
||||||
|
|
||||||
# Enable to run stand alone workers
|
|
||||||
# worker_fast:
|
|
||||||
# command:
|
|
||||||
# - run_worker
|
|
||||||
# - fast
|
|
||||||
# depends_on:
|
|
||||||
# - postgresql
|
|
||||||
# - redis
|
|
||||||
# # Enable to use RabbitMQ
|
|
||||||
# # - rabbitmq
|
|
||||||
# environment:
|
|
||||||
# <<: *mayan_env
|
|
||||||
# image: mayanedms/mayanedms:3.2.6
|
|
||||||
# networks:
|
|
||||||
# - mayan-bridge
|
|
||||||
# restart: unless-stopped
|
|
||||||
# volumes:
|
|
||||||
# - /docker-volumes/mayan-edms/media:/var/lib/mayan
|
|
||||||
|
|
||||||
# Enable to run stand frontend gunicorn
|
|
||||||
# frontend:
|
|
||||||
# command:
|
|
||||||
# - run_frontend
|
|
||||||
# depends_on:
|
|
||||||
# - postgresql
|
|
||||||
# - redis
|
|
||||||
# # Enable to use RabbitMQ
|
|
||||||
# # - rabbitmq
|
|
||||||
# environment:
|
|
||||||
# <<: *mayan_env
|
|
||||||
# image: mayanedms/mayanedms:3.2.6
|
|
||||||
# networks:
|
|
||||||
# - mayan-bridge
|
|
||||||
# ports:
|
|
||||||
# - "81:8000"
|
|
||||||
# restart: unless-stopped
|
|
||||||
# volumes:
|
|
||||||
# - /docker-volumes/mayan-edms/media:/var/lib/mayan
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/sh
|
||||||
|
|
||||||
# Use bash and not sh to support argument slicing "${@:2}"
|
|
||||||
# sh defaults to dash instead of bash.
|
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
echo "mayan: starting entrypoint.sh"
|
echo "mayan: starting entrypoint.sh"
|
||||||
@@ -9,15 +6,19 @@ INSTALL_FLAG=/var/lib/mayan/system/SECRET_KEY
|
|||||||
CONCURRENCY_ARGUMENT=--concurrency=
|
CONCURRENCY_ARGUMENT=--concurrency=
|
||||||
|
|
||||||
DEFAULT_USER_UID=1000
|
DEFAULT_USER_UID=1000
|
||||||
DEFAULT_USER_GID=1000
|
DEFAULT_USER_GUID=1000
|
||||||
|
|
||||||
|
export MAYAN_DEFAULT_BROKER_URL=redis://127.0.0.1:6379/0
|
||||||
|
export MAYAN_DEFAULT_CELERY_RESULT_BACKEND=redis://127.0.0.1:6379/0
|
||||||
|
|
||||||
export MAYAN_ALLOWED_HOSTS='["*"]'
|
export MAYAN_ALLOWED_HOSTS='["*"]'
|
||||||
export MAYAN_BIN=/opt/mayan-edms/bin/mayan-edms.py
|
export MAYAN_BIN=/opt/mayan-edms/bin/mayan-edms.py
|
||||||
|
export MAYAN_BROKER_URL=${MAYAN_BROKER_URL:-${MAYAN_DEFAULT_BROKER_URL}}
|
||||||
|
export MAYAN_CELERY_RESULT_BACKEND=${MAYAN_CELERY_RESULT_BACKEND:-${MAYAN_DEFAULT_CELERY_RESULT_BACKEND}}
|
||||||
export MAYAN_INSTALL_DIR=/opt/mayan-edms
|
export MAYAN_INSTALL_DIR=/opt/mayan-edms
|
||||||
export MAYAN_PYTHON_BIN_DIR=/opt/mayan-edms/bin/
|
export MAYAN_PYTHON_BIN_DIR=/opt/mayan-edms/bin/
|
||||||
export MAYAN_MEDIA_ROOT=/var/lib/mayan
|
export MAYAN_MEDIA_ROOT=/var/lib/mayan
|
||||||
export MAYAN_SETTINGS_MODULE=${MAYAN_SETTINGS_MODULE:-mayan.settings.production}
|
export MAYAN_SETTINGS_MODULE=${MAYAN_SETTINGS_MODULE:-mayan.settings.production}
|
||||||
export DJANGO_SETTINGS_MODULE=${MAYAN_SETTINGS_MODULE}
|
|
||||||
|
|
||||||
export MAYAN_GUNICORN_BIN=${MAYAN_PYTHON_BIN_DIR}gunicorn
|
export MAYAN_GUNICORN_BIN=${MAYAN_PYTHON_BIN_DIR}gunicorn
|
||||||
export MAYAN_GUNICORN_WORKERS=${MAYAN_GUNICORN_WORKERS:-2}
|
export MAYAN_GUNICORN_WORKERS=${MAYAN_GUNICORN_WORKERS:-2}
|
||||||
@@ -25,9 +26,13 @@ export MAYAN_GUNICORN_TIMEOUT=${MAYAN_GUNICORN_TIMEOUT:-120}
|
|||||||
export MAYAN_PIP_BIN=${MAYAN_PYTHON_BIN_DIR}pip
|
export MAYAN_PIP_BIN=${MAYAN_PYTHON_BIN_DIR}pip
|
||||||
export MAYAN_STATIC_ROOT=${MAYAN_INSTALL_DIR}/static
|
export MAYAN_STATIC_ROOT=${MAYAN_INSTALL_DIR}/static
|
||||||
|
|
||||||
MAYAN_WORKER_FAST_CONCURRENCY=${MAYAN_WORKER_FAST_CONCURRENCY:-0}
|
MAYAN_WORKER_FAST_CONCURRENCY=${MAYAN_WORKER_FAST_CONCURRENCY:-1}
|
||||||
MAYAN_WORKER_MEDIUM_CONCURRENCY=${MAYAN_WORKER_MEDIUM_CONCURRENCY:-0}
|
MAYAN_WORKER_MEDIUM_CONCURRENCY=${MAYAN_WORKER_MEDIUM_CONCURRENCY:-1}
|
||||||
MAYAN_WORKER_SLOW_CONCURRENCY=${MAYAN_WORKER_SLOW_CONCURRENCY:-0}
|
MAYAN_WORKER_SLOW_CONCURRENCY=${MAYAN_WORKER_SLOW_CONCURRENCY:-1}
|
||||||
|
|
||||||
|
echo "mayan: changing uid/guid"
|
||||||
|
usermod mayan -u ${MAYAN_USER_UID:-${DEFAULT_USER_UID}}
|
||||||
|
groupmod mayan -g ${MAYAN_USER_GUID:-${DEFAULT_USER_GUID}}
|
||||||
|
|
||||||
if [ "$MAYAN_WORKER_FAST_CONCURRENCY" -eq 0 ]; then
|
if [ "$MAYAN_WORKER_FAST_CONCURRENCY" -eq 0 ]; then
|
||||||
MAYAN_WORKER_FAST_CONCURRENCY=
|
MAYAN_WORKER_FAST_CONCURRENCY=
|
||||||
@@ -50,9 +55,11 @@ else
|
|||||||
fi
|
fi
|
||||||
export MAYAN_WORKER_SLOW_CONCURRENCY
|
export MAYAN_WORKER_SLOW_CONCURRENCY
|
||||||
|
|
||||||
# Allow importing of user setting modules
|
export CELERY_ALWAYS_EAGER=False
|
||||||
export PYTHONPATH=$PYTHONPATH:$MAYAN_MEDIA_ROOT
|
export PYTHONPATH=$PYTHONPATH:$MAYAN_MEDIA_ROOT
|
||||||
|
|
||||||
|
chown mayan:mayan /var/lib/mayan -R
|
||||||
|
|
||||||
apt_get_install() {
|
apt_get_install() {
|
||||||
apt-get -q update
|
apt-get -q update
|
||||||
apt-get install -y --force-yes --no-install-recommends --auto-remove "$@"
|
apt-get install -y --force-yes --no-install-recommends --auto-remove "$@"
|
||||||
@@ -60,9 +67,9 @@ apt_get_install() {
|
|||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
}
|
}
|
||||||
|
|
||||||
initialsetup() {
|
initialize() {
|
||||||
echo "mayan: initialsetup()"
|
echo "mayan: initialize()"
|
||||||
su mayan -c "${MAYAN_BIN} initialsetup --force --no-dependencies"
|
su mayan -c "${MAYAN_BIN} initialsetup --force --no-javascript"
|
||||||
}
|
}
|
||||||
|
|
||||||
os_package_installs() {
|
os_package_installs() {
|
||||||
@@ -79,71 +86,43 @@ pip_installs() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
run_all() {
|
start() {
|
||||||
echo "mayan: start()"
|
echo "mayan: start()"
|
||||||
rm -rf /var/run/supervisor.sock
|
rm -rf /var/run/supervisor.sock
|
||||||
exec /usr/bin/supervisord -nc /etc/supervisor/supervisord.conf
|
exec /usr/bin/supervisord -nc /etc/supervisor/supervisord.conf
|
||||||
}
|
}
|
||||||
|
|
||||||
performupgrade() {
|
upgrade() {
|
||||||
echo "mayan: performupgrade()"
|
echo "mayan: upgrade()"
|
||||||
su mayan -c "${MAYAN_BIN} performupgrade --no-dependencies"
|
su mayan -c "${MAYAN_BIN} performupgrade --no-javascript"
|
||||||
}
|
|
||||||
|
|
||||||
make_ready() {
|
|
||||||
# Check if this is a new install, otherwise try to upgrade the existing
|
|
||||||
# installation on subsequent starts
|
|
||||||
if [ ! -f $INSTALL_FLAG ]; then
|
|
||||||
initialsetup
|
|
||||||
else
|
|
||||||
performupgrade
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
set_uid_guid() {
|
|
||||||
echo "mayan: changing uid/guid"
|
|
||||||
usermod mayan -u ${MAYAN_USER_UID:-${DEFAULT_USER_UID}}
|
|
||||||
groupmod mayan -g ${MAYAN_USER_GID:-${DEFAULT_USER_GID}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
os_package_installs || true
|
os_package_installs || true
|
||||||
pip_installs || true
|
pip_installs || true
|
||||||
chown mayan:mayan /var/lib/mayan -R
|
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
|
||||||
run_initialsetup)
|
mayan) # Check if this is a new install, otherwise try to upgrade the existing
|
||||||
initialsetup
|
# installation on subsequent starts
|
||||||
;;
|
if [ ! -f $INSTALL_FLAG ]; then
|
||||||
|
initialize
|
||||||
|
else
|
||||||
|
upgrade
|
||||||
|
fi
|
||||||
|
start
|
||||||
|
;;
|
||||||
|
|
||||||
run_performupgrade)
|
run-tests) # Check if this is a new install, otherwise try to upgrade the existing
|
||||||
performupgrade
|
# installation on subsequent starts
|
||||||
;;
|
if [ ! -f $INSTALL_FLAG ]; then
|
||||||
|
initialize
|
||||||
|
else
|
||||||
|
upgrade
|
||||||
|
fi
|
||||||
|
run-tests.sh
|
||||||
|
;;
|
||||||
|
|
||||||
run_all)
|
*) su mayan -c "$@";
|
||||||
make_ready
|
;;
|
||||||
run_all
|
|
||||||
;;
|
|
||||||
|
|
||||||
run_celery)
|
|
||||||
run_celery.sh "${@:2}"
|
|
||||||
;;
|
|
||||||
|
|
||||||
run_frontend)
|
|
||||||
run_frontend.sh
|
|
||||||
;;
|
|
||||||
|
|
||||||
run_tests)
|
|
||||||
make_ready
|
|
||||||
run_tests.sh
|
|
||||||
;;
|
|
||||||
|
|
||||||
run_worker)
|
|
||||||
run_worker.sh "${@:2}"
|
|
||||||
;;
|
|
||||||
|
|
||||||
*)
|
|
||||||
su mayan -c "$@"
|
|
||||||
;;
|
|
||||||
|
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Use -A and not --app. Both are the same but behave differently
|
|
||||||
# -A can be located before the command while --app cannot.
|
|
||||||
su mayan -c "${MAYAN_PYTHON_BIN_DIR}celery -A mayan $@"
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
MAYAN_GUNICORN_MAX_REQUESTS=${MAYAN_GUNICORN_MAX_REQUESTS:-500}
|
|
||||||
MAYAN_GUNICORN_MAX_REQUESTS_JITTERS=${MAYAN_GUNICORN_MAX_REQUESTS_JITTERS:-50}
|
|
||||||
MAYAN_GUNICORN_WORKER_CLASS=${MAYAN_GUNICORN_WORKER_CLASS:-sync}
|
|
||||||
|
|
||||||
su mayan -c "${MAYAN_PYTHON_BIN_DIR}gunicorn -w ${MAYAN_GUNICORN_WORKERS} mayan.wsgi --max-requests ${MAYAN_GUNICORN_MAX_REQUESTS} --max-requests-jitter ${MAYAN_GUNICORN_MAX_REQUESTS_JITTERS} --worker-class ${MAYAN_GUNICORN_WORKER_CLASS} --bind 0.0.0.0:8000 --timeout ${MAYAN_GUNICORN_TIMEOUT}"
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
QUEUE_LIST=`MAYAN_WORKER_NAME=$1 su mayan -c "${MAYAN_PYTHON_BIN_DIR}mayan-edms.py platformtemplate worker_queues"`
|
|
||||||
|
|
||||||
# Use -A and not --app. Both are the same but behave differently
|
|
||||||
# -A can be located before the command while --app cannot.
|
|
||||||
# Pass ${@:2} to allow overriding the defaults arguments
|
|
||||||
su mayan -c "${MAYAN_PYTHON_BIN_DIR}celery -A mayan worker -Ofair -l ERROR -Q $QUEUE_LIST ${@:2}"
|
|
||||||
@@ -1 +1 @@
|
|||||||
3.2.6
|
3.2.5
|
||||||
|
|||||||
@@ -9,32 +9,24 @@ volumes:
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
db:
|
db:
|
||||||
|
image: postgres
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: mayan
|
POSTGRES_DB: mayan
|
||||||
POSTGRES_PASSWORD: mayandbpass
|
POSTGRES_PASSWORD: mayan-password
|
||||||
POSTGRES_USER: mayan
|
POSTGRES_USER: mayan
|
||||||
image: postgres
|
|
||||||
volumes:
|
volumes:
|
||||||
- db:/var/lib/postgresql/data
|
- db:/var/lib/postgresql/data
|
||||||
|
|
||||||
app:
|
app:
|
||||||
environment:
|
|
||||||
MAYAN_CELERY_BROKER_URL: redis://redis:6379/1
|
|
||||||
MAYAN_CELERY_RESULT_BACKEND: redis://redis:6379/0
|
|
||||||
MAYAN_DATABASES: "{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayandbpass','USER':'mayan','HOST':'db'}}"
|
|
||||||
image: mayanedms/mayanedms:latest
|
image: mayanedms/mayanedms:latest
|
||||||
ports:
|
ports:
|
||||||
- 80:8000
|
- 80:8000
|
||||||
|
environment:
|
||||||
|
MAYAN_DATABASE_ENGINE: django.db.backends.postgresql
|
||||||
|
MAYAN_DATABASE_HOST: db
|
||||||
|
MAYAN_DATABASE_NAME: mayan
|
||||||
|
MAYAN_DATABASE_PASSWORD: mayan-password
|
||||||
|
MAYAN_DATABASE_USER: mayan
|
||||||
|
MAYAN_DATABASE_CONN_MAX_AGE: 0
|
||||||
volumes:
|
volumes:
|
||||||
- app:/var/lib/mayan
|
- app:/var/lib/mayan
|
||||||
|
|
||||||
redis:
|
|
||||||
command:
|
|
||||||
- redis-server
|
|
||||||
- --databases
|
|
||||||
- "2"
|
|
||||||
- --maxmemory-policy
|
|
||||||
- allkeys-lru
|
|
||||||
- --save
|
|
||||||
- ""
|
|
||||||
image: redis:5.0
|
|
||||||
|
|||||||
@@ -127,8 +127,9 @@ For another setup that offers more performance and scalability refer to the
|
|||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
sudo -u mayan MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \
|
sudo -u mayan MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
|
||||||
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
|
||||||
|
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
||||||
/opt/mayan-edms/bin/mayan-edms.py initialsetup
|
/opt/mayan-edms/bin/mayan-edms.py initialsetup
|
||||||
|
|
||||||
|
|
||||||
@@ -147,8 +148,9 @@ For another setup that offers more performance and scalability refer to the
|
|||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
::
|
::
|
||||||
|
|
||||||
sudo mayan MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \
|
sudo MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
|
||||||
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
|
||||||
|
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
||||||
/opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf
|
/opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf
|
||||||
|
|
||||||
|
|
||||||
@@ -220,11 +222,11 @@ of a restart or power failure. The Gunicorn workers are increased to 3.
|
|||||||
---------------------------------------------------------------------
|
---------------------------------------------------------------------
|
||||||
Replace (paying attention to the comma at the end)::
|
Replace (paying attention to the comma at the end)::
|
||||||
|
|
||||||
MAYAN_CELERY_BROKER_URL="redis://127.0.0.1:6379/0",
|
MAYAN_BROKER_URL="redis://127.0.0.1:6379/0",
|
||||||
|
|
||||||
with::
|
with::
|
||||||
|
|
||||||
MAYAN_CELERY_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
|
MAYAN_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
|
||||||
|
|
||||||
increase the number of Gunicorn workers to 3 in the line (``-w 2`` section)::
|
increase the number of Gunicorn workers to 3 in the line (``-w 2`` section)::
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,12 @@ Finally create and run a Mayan EDMS container::
|
|||||||
--name mayan-edms \
|
--name mayan-edms \
|
||||||
--restart=always \
|
--restart=always \
|
||||||
-p 80:8000 \
|
-p 80:8000 \
|
||||||
-e MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'172.17.0.1'}}" \
|
-e MAYAN_DATABASE_ENGINE=django.db.backends.postgresql \
|
||||||
|
-e MAYAN_DATABASE_HOST=172.17.0.1 \
|
||||||
|
-e MAYAN_DATABASE_NAME=mayan \
|
||||||
|
-e MAYAN_DATABASE_PASSWORD=mayanuserpass \
|
||||||
|
-e MAYAN_DATABASE_USER=mayan \
|
||||||
|
-e MAYAN_DATABASE_CONN_MAX_AGE=0 \
|
||||||
-v /docker-volumes/mayan-edms/media:/var/lib/mayan \
|
-v /docker-volumes/mayan-edms/media:/var/lib/mayan \
|
||||||
mayanedms/mayanedms:<version>
|
mayanedms/mayanedms:<version>
|
||||||
|
|
||||||
@@ -103,7 +108,12 @@ instead of the IP address of the Docker host (``172.17.0.1``)::
|
|||||||
--network=mayan \
|
--network=mayan \
|
||||||
--restart=always \
|
--restart=always \
|
||||||
-p 80:8000 \
|
-p 80:8000 \
|
||||||
-e MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'mayan-edms-postgres'}}" \
|
-e MAYAN_DATABASE_ENGINE=django.db.backends.postgresql \
|
||||||
|
-e MAYAN_DATABASE_HOST=mayan-edms-postgres \
|
||||||
|
-e MAYAN_DATABASE_NAME=mayan \
|
||||||
|
-e MAYAN_DATABASE_PASSWORD=mayanuserpass \
|
||||||
|
-e MAYAN_DATABASE_USER=mayan \
|
||||||
|
-e MAYAN_DATABASE_CONN_MAX_AGE=0 \
|
||||||
-v /docker-volumes/mayan-edms/media:/var/lib/mayan \
|
-v /docker-volumes/mayan-edms/media:/var/lib/mayan \
|
||||||
mayanedms/mayanedms:<version>
|
mayanedms/mayanedms:<version>
|
||||||
|
|
||||||
@@ -127,14 +137,102 @@ To start the container again::
|
|||||||
Environment Variables
|
Environment Variables
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
The common set of settings can also be modified via environment variables when
|
The Mayan EDMS image can be configure via environment variables.
|
||||||
using the Docker image. In addition to the common set of settings, some Docker
|
|
||||||
image specific environment variables are available.
|
``MAYAN_DATABASE_ENGINE``
|
||||||
|
|
||||||
|
Defaults to ``None``. This environment variable configures the database
|
||||||
|
backend to use. If left unset, SQLite will be used. The database backends
|
||||||
|
supported by this Docker image are:
|
||||||
|
|
||||||
|
- ``'django.db.backends.postgresql'``
|
||||||
|
- ``'django.db.backends.mysql'``
|
||||||
|
- ``'django.db.backends.sqlite3'``
|
||||||
|
|
||||||
|
When using the SQLite backend, the database file will be saved in the Docker
|
||||||
|
volume. The SQLite database as used by Mayan EDMS is meant only for development
|
||||||
|
or testing, never use it in production.
|
||||||
|
|
||||||
|
``MAYAN_DATABASE_NAME``
|
||||||
|
|
||||||
|
Defaults to 'mayan'. This optional environment variable can be used to define
|
||||||
|
the database name that Mayan EDMS will connect to. For more information read
|
||||||
|
the pertinent Django documentation page:
|
||||||
|
:django-docs:`Connecting to the database <ref/databases/#connecting-to-the-database>`
|
||||||
|
|
||||||
|
``MAYAN_DATABASE_USER``
|
||||||
|
|
||||||
|
Defaults to 'mayan'. This optional environment variable is used to set the
|
||||||
|
username that will be used to connect to the database. For more information
|
||||||
|
read the pertinent Django documentation page:
|
||||||
|
:django-docs:`Settings, USER <ref/settings/#user>`
|
||||||
|
|
||||||
|
``MAYAN_DATABASE_PASSWORD``
|
||||||
|
|
||||||
|
Defaults to ''. This optional environment variable is used to set the
|
||||||
|
password that will be used to connect to the database. For more information
|
||||||
|
read the pertinent Django documentation page:
|
||||||
|
:django-docs:`Settings, PASSWORD <ref/settings/#password>`
|
||||||
|
|
||||||
|
``MAYAN_DATABASE_HOST``
|
||||||
|
|
||||||
|
Defaults to `None`. This optional environment variable is used to set the
|
||||||
|
hostname that will be used to connect to the database. This can be the
|
||||||
|
hostname of another container or an IP address. For more information read
|
||||||
|
the pertinent Django documentation page:
|
||||||
|
:django-docs:`Settings, HOST <ref/settings/#host>`
|
||||||
|
|
||||||
|
``MAYAN_DATABASE_PORT``
|
||||||
|
|
||||||
|
Defaults to `None`. This optional environment variable is used to set the
|
||||||
|
port number to use when connecting to the database. An empty string means
|
||||||
|
the default port. Not used with SQLite. For more information read the
|
||||||
|
pertinent Django documentation page:
|
||||||
|
:django-docs:`Settings, PORT <ref/settings/#port>`
|
||||||
|
|
||||||
|
``MAYAN_BROKER_URL``
|
||||||
|
|
||||||
|
This optional environment variable determines the broker that Celery will use
|
||||||
|
to relay task messages between the frontend code and the background workers.
|
||||||
|
For more information read the pertinent Celery Kombu documentation page: `Broker URL`_
|
||||||
|
|
||||||
|
.. _Broker URL: http://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls
|
||||||
|
|
||||||
|
This Docker image supports using Redis and RabbitMQ as brokers.
|
||||||
|
|
||||||
|
Caveat: If the `MAYAN_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment
|
||||||
|
variables are specified, the built-in Redis server inside the container will
|
||||||
|
be disabled.
|
||||||
|
|
||||||
|
``MAYAN_CELERY_RESULT_BACKEND``
|
||||||
|
|
||||||
|
This optional environment variable determines the results backend that Celery
|
||||||
|
will use to relay result messages from the background workers to the frontend
|
||||||
|
code. For more information read the pertinent Celery Kombu documentation page:
|
||||||
|
`Task result backend settings`_
|
||||||
|
|
||||||
|
.. _Task result backend settings: http://docs.celeryproject.org/en/3.1/configuration.html#celery-result-backend
|
||||||
|
|
||||||
|
This Docker image supports using Redis and RabbitMQ as result backends.
|
||||||
|
|
||||||
|
Caveat: If the `MAYAN_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment
|
||||||
|
variables are specified, the built-in Redis server inside the container will
|
||||||
|
be disabled.
|
||||||
|
|
||||||
``MAYAN_SETTINGS_MODULE``
|
``MAYAN_SETTINGS_MODULE``
|
||||||
|
|
||||||
Optional. Allows loading an alternate settings file.
|
Optional. Allows loading an alternate settings file.
|
||||||
|
|
||||||
|
``MAYAN_DATABASE_CONN_MAX_AGE``
|
||||||
|
|
||||||
|
Amount in seconds to keep a database connection alive. Allow reuse of database
|
||||||
|
connections. For more information read the pertinent Django documentation
|
||||||
|
page: :django-docs:`Settings, CONN_MAX_AGE <ref/settings/#conn-max-age>`
|
||||||
|
According to new information Gunicorn's microthreads don't share connections
|
||||||
|
and will exhaust the available Postgres connections available if a number
|
||||||
|
other than 0 is used. Reference: https://serverfault.com/questions/635100/django-conn-max-age-persists-connections-but-doesnt-reuse-them-with-postgresq
|
||||||
|
and https://github.com/benoitc/gunicorn/issues/996
|
||||||
|
|
||||||
``MAYAN_GUNICORN_WORKERS``
|
``MAYAN_GUNICORN_WORKERS``
|
||||||
|
|
||||||
Optional. This environment variable controls the number of frontend workers
|
Optional. This environment variable controls the number of frontend workers
|
||||||
@@ -171,21 +269,12 @@ number of CPUs detected).
|
|||||||
Optional. Changes the UID of the ``mayan`` user internal to the Docker
|
Optional. Changes the UID of the ``mayan`` user internal to the Docker
|
||||||
container. Defaults to 1000.
|
container. Defaults to 1000.
|
||||||
|
|
||||||
``MAYAN_USER_GID``
|
``MAYAN_USER_GUID``
|
||||||
|
|
||||||
Optional. Changes the GID of the ``mayan`` user internal to the Docker
|
Optional. Changes the GUID of the ``mayan`` user internal to the Docker
|
||||||
container. Defaults to 1000.
|
container. Defaults to 1000.
|
||||||
|
|
||||||
|
|
||||||
Included drivers
|
|
||||||
----------------
|
|
||||||
|
|
||||||
The Docker image supports using Redis and RabbitMQ as result backends. For
|
|
||||||
databases, the image includes support for PostgreSQL and MySQL/MariaDB.
|
|
||||||
Support for additional brokers or databases may be added using the
|
|
||||||
``MAYAN_APT_INSTALL`` environment variable.
|
|
||||||
|
|
||||||
|
|
||||||
.. _docker-accessing-outside-data:
|
.. _docker-accessing-outside-data:
|
||||||
|
|
||||||
Accessing outside data
|
Accessing outside data
|
||||||
@@ -353,7 +442,6 @@ These are:
|
|||||||
|
|
||||||
Nightly images
|
Nightly images
|
||||||
==============
|
==============
|
||||||
|
|
||||||
The continuous integration pipeline used for testing development builds also
|
The continuous integration pipeline used for testing development builds also
|
||||||
produces a resulting Docker image. These are build automatically and their
|
produces a resulting Docker image. These are build automatically and their
|
||||||
stability is not guaranteed. They should never be used in production.
|
stability is not guaranteed. They should never be used in production.
|
||||||
|
|||||||
@@ -94,11 +94,11 @@ For the Docker image, launch a separate RabbitMQ container
|
|||||||
|
|
||||||
docker run -d --name mayan-edms-rabbitmq -e RABBITMQ_DEFAULT_USER=mayan -e RABBITMQ_DEFAULT_PASS=mayanrabbitmqpassword -e RABBITMQ_DEFAULT_VHOST=mayan rabbitmq:3
|
docker run -d --name mayan-edms-rabbitmq -e RABBITMQ_DEFAULT_USER=mayan -e RABBITMQ_DEFAULT_PASS=mayanrabbitmqpassword -e RABBITMQ_DEFAULT_VHOST=mayan rabbitmq:3
|
||||||
|
|
||||||
Pass the MAYAN_CELERY_BROKER_URL environment variable (https://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls)
|
Pass the MAYAN_BROKER_URL environment variable (https://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls)
|
||||||
to the Mayan EDMS container so that it uses the RabbitMQ container the
|
to the Mayan EDMS container so that it uses the RabbitMQ container the
|
||||||
message broker::
|
message broker::
|
||||||
|
|
||||||
-e MAYAN_CELERY_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
|
-e MAYAN_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
|
||||||
|
|
||||||
When tasks finish, they leave behind a return status or the result of a
|
When tasks finish, they leave behind a return status or the result of a
|
||||||
calculation, these are stored for a while so that whoever requested the
|
calculation, these are stored for a while so that whoever requested the
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ The current document sources supported are:
|
|||||||
- IMAP email - Same as the ``POP3`` email source but for email accounts using
|
- IMAP email - Same as the ``POP3`` email source but for email accounts using
|
||||||
the ``IMAP`` protocol.
|
the ``IMAP`` protocol.
|
||||||
- Watch folder - A filesystem folder that is scanned periodically for files.
|
- Watch folder - A filesystem folder that is scanned periodically for files.
|
||||||
Any file in the watch folder is automatically uploaded. When the upload for a
|
Any file in the watch folder is automatically uploaded.
|
||||||
file is completed, the file is removed from source folder.
|
|
||||||
- Staging folder - Folder where networked attached scanned can save image
|
- Staging folder - Folder where networked attached scanned can save image
|
||||||
files. The files in these staging folders are scanned and a preview is
|
files. The files in these staging folders are scanned and a preview is
|
||||||
generated to help the process of upload. Staging folders and Watch folders
|
generated to help the process of upload. Staging folders and Watch folders
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
Version 3.2.6
|
|
||||||
=============
|
|
||||||
|
|
||||||
Released: July 10, 2019
|
|
||||||
|
|
||||||
|
|
||||||
Changes
|
|
||||||
-------
|
|
||||||
|
|
||||||
- Remove the smart settings app * import. Following MERC 0005.
|
|
||||||
- Encode settings YAML before hashing. Avoids unicode issues with Python 3.
|
|
||||||
- Fix document icon used in the workflow runtime links.
|
|
||||||
- Add trashed date time label.
|
|
||||||
- Fix thumbnail generation issue. GitLab issue #637.
|
|
||||||
Thanks to Giacomo Cariello (@giacomocariello) for the report
|
|
||||||
and the merge request fixing the issue.
|
|
||||||
|
|
||||||
Removals
|
|
||||||
--------
|
|
||||||
|
|
||||||
- None
|
|
||||||
|
|
||||||
|
|
||||||
Upgrading from a previous version
|
|
||||||
---------------------------------
|
|
||||||
|
|
||||||
If installed via Python's PIP
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Remove deprecated requirements::
|
|
||||||
|
|
||||||
sudo -u mayan curl https://gitlab.com/mayan-edms/mayan-edms/raw/master/removals.txt -o /tmp/removals.txt && sudo -u mayan /opt/mayan-edms/bin/pip uninstall -y -r /tmp/removals.txt
|
|
||||||
|
|
||||||
Type in the console::
|
|
||||||
|
|
||||||
sudo -u mayan /opt/mayan-edms/bin/pip install mayan-edms==3.2.6
|
|
||||||
|
|
||||||
the requirements will also be updated automatically.
|
|
||||||
|
|
||||||
|
|
||||||
Using Git
|
|
||||||
^^^^^^^^^
|
|
||||||
|
|
||||||
If you installed Mayan EDMS by cloning the Git repository issue the commands::
|
|
||||||
|
|
||||||
$ git reset --hard HEAD
|
|
||||||
$ git pull
|
|
||||||
|
|
||||||
otherwise download the compressed archived and uncompress it overriding the
|
|
||||||
existing installation.
|
|
||||||
|
|
||||||
Remove deprecated requirements::
|
|
||||||
|
|
||||||
$ pip uninstall -y -r removals.txt
|
|
||||||
|
|
||||||
Next upgrade/add the new requirements::
|
|
||||||
|
|
||||||
$ pip install --upgrade -r requirements.txt
|
|
||||||
|
|
||||||
|
|
||||||
Common steps
|
|
||||||
^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Perform these steps after updating the code from either step above.
|
|
||||||
|
|
||||||
Make a backup of your supervisord file::
|
|
||||||
|
|
||||||
sudo cp /etc/supervisor/conf.d/mayan.conf /etc/supervisor/conf.d/mayan.conf.bck
|
|
||||||
|
|
||||||
Update the supervisord configuration file. Replace the environment
|
|
||||||
variables values show here with your respective settings. This step will refresh
|
|
||||||
the supervisord configuration file with the new queues and the latest
|
|
||||||
recommended layout::
|
|
||||||
|
|
||||||
sudo MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
|
|
||||||
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
|
|
||||||
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
|
||||||
/opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf
|
|
||||||
|
|
||||||
Edit the supervisord configuration file and update any setting the template
|
|
||||||
generator missed::
|
|
||||||
|
|
||||||
sudo vi /etc/supervisor/conf.d/mayan.conf
|
|
||||||
|
|
||||||
Migrate existing database schema with::
|
|
||||||
|
|
||||||
sudo -u mayan MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
|
|
||||||
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
|
|
||||||
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
|
||||||
/opt/mayan-edms/bin/mayan-edms.py performupgrade
|
|
||||||
|
|
||||||
Add new static media::
|
|
||||||
|
|
||||||
sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
|
||||||
/opt/mayan-edms/bin/mayan-edms.py preparestatic --noinput
|
|
||||||
|
|
||||||
The upgrade procedure is now complete.
|
|
||||||
|
|
||||||
|
|
||||||
Backward incompatible changes
|
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
- None
|
|
||||||
|
|
||||||
|
|
||||||
Bugs fixed or issues closed
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
- :gitlab-issue:`637` Thumbnail generation bug
|
|
||||||
|
|
||||||
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/
|
|
||||||
@@ -8,104 +8,11 @@ Changes
|
|||||||
-------
|
-------
|
||||||
|
|
||||||
- Add support for icon shadows.
|
- Add support for icon shadows.
|
||||||
- Add icons and no-result template to the object error log view and
|
|
||||||
links.
|
|
||||||
- Use Select2 widget for the document type selection form.
|
|
||||||
- Backport the vertical main menu update. This update splits the previous
|
|
||||||
main menu into a new menu in the same location as the previous one
|
|
||||||
now called the top bar, and a new vertical main menu on the left side.
|
|
||||||
The vertical menu remain open even when clicking on items and upon
|
|
||||||
a browser refresh will also restore its state to match the selected
|
|
||||||
view.
|
|
||||||
- Backport workflow preview refactor. GitLab issue #532.
|
|
||||||
- Add support for source column inheritance.
|
|
||||||
- Add support for source column exclusion.
|
|
||||||
- Backport workflow context support.
|
|
||||||
- Backport workflow transitions field support.
|
|
||||||
- Backport workflow email action.
|
|
||||||
- Backport individual index rebuild support.
|
|
||||||
- Rename the installjavascript command to installdependencies.
|
|
||||||
- Remove database conversion command.
|
|
||||||
- Remove support for quoted configuration entries. Support unquoted,
|
|
||||||
nested dictionaries in the configuration. Requires manual
|
|
||||||
update of existing config.yml files.
|
|
||||||
- Support user specified locations for the configuration file with the
|
|
||||||
CONFIGURATION_FILEPATH (MAYAN_CONFIGURATION_FILEPATH environment variable), and
|
|
||||||
CONFIGURATION_LAST_GOOD_FILEPATH
|
|
||||||
(MAYAN_CONFIGURATION_LAST_GOOD_FILEPATH environment variable) settings.
|
|
||||||
- Move bootstrapped settings code to their own module in the smart_settings apps.
|
|
||||||
- Remove individual database configuration options. All database configuration
|
|
||||||
is now done using MAYAN_DATABASES to mirror Django way of doing database setup.
|
|
||||||
- Added support for YAML encoded environment variables to the platform
|
|
||||||
templates apps.
|
|
||||||
- Move YAML code to its own module. Code now resides in common.serialization
|
|
||||||
in the form of two new functions: yaml_load and yaml_dump.
|
|
||||||
- Move Django and Celery settings. Django settings now reside in the smart
|
|
||||||
settings app. Celery settings now reside in the task manager app.
|
|
||||||
- Backport FakeStorageSubclass from versions/next. Placeholder class to allow
|
|
||||||
serializing the real storage subclass to support migrations.
|
|
||||||
Used by all configurable storages.
|
|
||||||
- Support checking in and out multiple documents.
|
|
||||||
- Remove encapsulate helper.
|
|
||||||
- Add support for menu inheritance.
|
|
||||||
- Emphasize source column labels.
|
|
||||||
- Backport file cache manager app.
|
|
||||||
- Convert document image cache to use file cache manager app.
|
|
||||||
Add setting DOCUMENTS_CACHE_MAXIMUM_SIZE defaults to 500 MB.
|
|
||||||
- Update Celery to version 4.3.0. Settings changed:
|
|
||||||
MAYAN_BROKER_URL to MAYAN_CELERY_BROKER_URL,
|
|
||||||
MAYAN_CELERY_ALWAYS_EAGER to MAYAN_CELERY_TASK_ALWAYS_EAGER.
|
|
||||||
- Replace djcelery and replace it with django-celery-beat.
|
|
||||||
- Update Celery to version 4.3.0 with 55e9b2263cbdb9b449361412fd18d8ee0a442dd3
|
|
||||||
from versions/next, code from GitLab issue #594 and GitLab merge request !55.
|
|
||||||
Thanks to Jakob Haufe (@sur5r) and Jesaja Everling (@jeverling)
|
|
||||||
for much of the research and code updates.
|
|
||||||
- Support wildcard MIME type associations for the file metadata drivers.
|
|
||||||
- Rename MAYAN_GUID to MAYAN_GID
|
|
||||||
- Update Gunicorn to use sync workers.
|
|
||||||
- Include devpi-server as a development dependency.
|
|
||||||
- Update default Docker stack file.
|
|
||||||
- Remove Redis from the Docker image.
|
|
||||||
- Add Celery flower to the Docker image.
|
|
||||||
- Allow PIP proxying to the Docker image during build.
|
|
||||||
- Default Celery worker concurrency to 0 (auto).
|
|
||||||
- Set DJANGO_SETTINGS_MODULE environment variable to make it
|
|
||||||
available to sub processes.
|
|
||||||
- Add entrypoint commands to run single workers, single gunicorn
|
|
||||||
or single celery commands like "flower".
|
|
||||||
- Add platform template to return queues for a worker.
|
|
||||||
- Remove task inspection from task manager app.
|
|
||||||
- Move pagination navigation inside the toolbar.
|
|
||||||
- Remove document image clear link and view.
|
|
||||||
This is now handled by the file caching app.
|
|
||||||
- Add web links app.
|
|
||||||
- Add support to display column help text
|
|
||||||
as a tooltip.
|
|
||||||
- Update numeric dashboard widget to display
|
|
||||||
thousand commas.
|
|
||||||
- Add support for disabling document pages.
|
|
||||||
|
|
||||||
Removals
|
Removals
|
||||||
--------
|
--------
|
||||||
|
|
||||||
- Database conversion. Reason for removal. The database conversions support
|
- None
|
||||||
provided by this feature (SQLite to PostgreSQL) was being confused with
|
|
||||||
database migrations and upgrades.
|
|
||||||
|
|
||||||
Database upgrades are the responsibility of the app and the framework.
|
|
||||||
Database conversions however are not the responsibility of the app (Mayan),
|
|
||||||
they are the responsibility of the framework.
|
|
||||||
|
|
||||||
Database conversion is outside the scope of what Mayan does but we added
|
|
||||||
the code, management command, instructions and testing setup to provide
|
|
||||||
this to our users until the framework (Django) decided to add this
|
|
||||||
themselves (like they did with migrations).
|
|
||||||
|
|
||||||
Continued confusion about the purpose of the feature and confusion about
|
|
||||||
how errors with this feature were a reflexion of the code quality of
|
|
||||||
Mayannecessitated the removal of the database conversion feature.
|
|
||||||
|
|
||||||
- Django environ
|
|
||||||
|
|
||||||
|
|
||||||
Upgrading from a previous version
|
Upgrading from a previous version
|
||||||
@@ -116,11 +23,11 @@ If installed via Python's PIP
|
|||||||
|
|
||||||
Remove deprecated requirements::
|
Remove deprecated requirements::
|
||||||
|
|
||||||
sudo -u mayan curl https://gitlab.com/mayan-edms/mayan-edms/raw/master/removals.txt -o /tmp/removals.txt && sudo -u mayan /opt/mayan-edms/bin/pip uninstall -y -r /tmp/removals.txt
|
$ curl https://gitlab.com/mayan-edms/mayan-edms/raw/master/removals.txt | pip uninstall -r /dev/stdin
|
||||||
|
|
||||||
Type in the console::
|
Type in the console::
|
||||||
|
|
||||||
/opt/mayan-edms/bin/pip install mayan-edms==3.3
|
$ pip install mayan-edms==3.3
|
||||||
|
|
||||||
the requirements will also be updated automatically.
|
the requirements will also be updated automatically.
|
||||||
|
|
||||||
@@ -130,19 +37,19 @@ Using Git
|
|||||||
|
|
||||||
If you installed Mayan EDMS by cloning the Git repository issue the commands::
|
If you installed Mayan EDMS by cloning the Git repository issue the commands::
|
||||||
|
|
||||||
git reset --hard HEAD
|
$ git reset --hard HEAD
|
||||||
git pull
|
$ git pull
|
||||||
|
|
||||||
otherwise download the compressed archived and uncompress it overriding the
|
otherwise download the compressed archived and uncompress it overriding the
|
||||||
existing installation.
|
existing installation.
|
||||||
|
|
||||||
Remove deprecated requirements::
|
Remove deprecated requirements::
|
||||||
|
|
||||||
pip uninstall -y -r removals.txt
|
$ pip uninstall -y -r removals.txt
|
||||||
|
|
||||||
Next upgrade/add the new requirements::
|
Next upgrade/add the new requirements::
|
||||||
|
|
||||||
pip install --upgrade -r requirements.txt
|
$ pip install --upgrade -r requirements.txt
|
||||||
|
|
||||||
|
|
||||||
Common steps
|
Common steps
|
||||||
@@ -159,8 +66,9 @@ variables values show here with your respective settings. This step will refresh
|
|||||||
the supervisord configuration file with the new queues and the latest
|
the supervisord configuration file with the new queues and the latest
|
||||||
recommended layout::
|
recommended layout::
|
||||||
|
|
||||||
sudo MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \
|
sudo MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
|
||||||
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
|
||||||
|
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
||||||
/opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf
|
/opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf
|
||||||
|
|
||||||
Edit the supervisord configuration file and update any setting the template
|
Edit the supervisord configuration file and update any setting the template
|
||||||
@@ -170,11 +78,11 @@ generator missed::
|
|||||||
|
|
||||||
Migrate existing database schema with::
|
Migrate existing database schema with::
|
||||||
|
|
||||||
sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media /opt/mayan-edms/bin/mayan-edms.py performupgrade
|
$ mayan-edms.py performupgrade
|
||||||
|
|
||||||
Add new static media::
|
Add new static media::
|
||||||
|
|
||||||
sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media /opt/mayan-edms/bin/mayan-edms.py preparestatic --noinput
|
$ mayan-edms.py preparestatic --noinput
|
||||||
|
|
||||||
The upgrade procedure is now complete.
|
The upgrade procedure is now complete.
|
||||||
|
|
||||||
@@ -182,26 +90,12 @@ The upgrade procedure is now complete.
|
|||||||
Backward incompatible changes
|
Backward incompatible changes
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
- Update quoted settings to be unquoted:
|
- None
|
||||||
|
|
||||||
- COMMON_SHARED_STORAGE_ARGUMENTS
|
|
||||||
- CONVERTER_GRAPHICS_BACKEND_ARGUMENTS
|
|
||||||
- DOCUMENTS_CACHE_STORAGE_BACKEND_ARGUMENTS
|
|
||||||
- DOCUMENTS_STORAGE_BACKEND_ARGUMENTS
|
|
||||||
- FILE_METADATA_DRIVERS_ARGUMENTS
|
|
||||||
- SIGNATURES_STORAGE_BACKEND_ARGUMENTS
|
|
||||||
|
|
||||||
|
|
||||||
Bugs fixed or issues closed
|
Bugs fixed or issues closed
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
- :gitlab-issue:`526` RuntimeWarning: Never call result.get() within a task!
|
- :gitlab-issue:`XX`
|
||||||
- :gitlab-issue:`532` Workflow preview isn't updated right after transitions are modified
|
|
||||||
- :gitlab-issue:`540` hint-outdated/update documentation
|
|
||||||
- :gitlab-issue:`594` 3.2b1: Unable to install/run under Python 3.5/3.6/3.7
|
|
||||||
- :gitlab-issue:`634` Failing docker entrypoint when using secret config
|
|
||||||
- :gitlab-issue:`635` Build a docker image for Python3
|
|
||||||
- :gitlab-issue:`644` Update sane-utils package in docker image.
|
|
||||||
|
|
||||||
|
|
||||||
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/
|
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ versions of the documentation contain the release notes for any later releases.
|
|||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
3.3
|
3.3
|
||||||
3.2.6
|
|
||||||
3.2.5
|
3.2.5
|
||||||
3.2.4
|
3.2.4
|
||||||
3.2.3
|
3.2.3
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
__title__ = 'Mayan EDMS'
|
__title__ = 'Mayan EDMS'
|
||||||
__version__ = '3.2.6'
|
__version__ = '3.2.5'
|
||||||
__build__ = 0x030206
|
__build__ = 0x030205
|
||||||
__build_string__ = 'v3.2.6-68-gab601f9180_Wed Jul 17 04:30:11 2019 -0400'
|
__build_string__ = 'v3.2.5_Fri Jul 5 16:39:17 2019 -0400'
|
||||||
__django_version__ = '1.11'
|
__django_version__ = '1.11'
|
||||||
__author__ = 'Roberto Rosario'
|
__author__ = 'Roberto Rosario'
|
||||||
__author_email__ = 'roberto.rosario@mayan-edms.com'
|
__author_email__ = 'roberto.rosario@mayan-edms.com'
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ logger = logging.getLogger(__name__)
|
|||||||
class ModelPermission(object):
|
class ModelPermission(object):
|
||||||
_functions = {}
|
_functions = {}
|
||||||
_inheritances = {}
|
_inheritances = {}
|
||||||
_manager_names = {}
|
|
||||||
_registry = {}
|
_registry = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -98,24 +97,6 @@ class ModelPermission(object):
|
|||||||
def get_inheritance(cls, model):
|
def get_inheritance(cls, model):
|
||||||
return cls._inheritances[model]
|
return cls._inheritances[model]
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_manager(cls, model):
|
|
||||||
try:
|
|
||||||
manager_name = cls.get_manager_name(model=model)
|
|
||||||
except KeyError:
|
|
||||||
manager_name = None
|
|
||||||
|
|
||||||
if manager_name:
|
|
||||||
manager = getattr(model, manager_name)
|
|
||||||
else:
|
|
||||||
manager = model._meta.default_manager
|
|
||||||
|
|
||||||
return manager
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_manager_name(cls, model):
|
|
||||||
return cls._manager_names[model]
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def register_function(cls, model, function):
|
def register_function(cls, model, function):
|
||||||
cls._functions[model] = function
|
cls._functions[model] = function
|
||||||
@@ -123,7 +104,3 @@ class ModelPermission(object):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def register_inheritance(cls, model, related):
|
def register_inheritance(cls, model, related):
|
||||||
cls._inheritances[model] = related
|
cls._inheritances[model] = related
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def register_manager(cls, model, manager_name):
|
|
||||||
cls._manager_names[model] = manager_name
|
|
||||||
|
|||||||
@@ -200,26 +200,28 @@ class AccessControlListManager(models.Manager):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def check_access(self, obj, permissions, user):
|
def check_access(self, obj, permissions, user, manager=None):
|
||||||
# Allow specific managers for models that have more than one
|
# Allow specific managers for models that have more than one
|
||||||
# for example the Document model when checking for access for a trashed
|
# for example the Document model when checking for access for a trashed
|
||||||
# document.
|
# document.
|
||||||
|
|
||||||
meta = getattr(obj, '_meta', None)
|
if manager:
|
||||||
|
|
||||||
if not meta:
|
|
||||||
logger.debug(
|
|
||||||
ugettext(
|
|
||||||
'Object "%s" is not a model and cannot be checked for '
|
|
||||||
'access.'
|
|
||||||
) % force_text(obj)
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
manager = ModelPermission.get_manager(model=obj._meta.model)
|
|
||||||
source_queryset = manager.all()
|
source_queryset = manager.all()
|
||||||
|
else:
|
||||||
|
meta = getattr(obj, '_meta', None)
|
||||||
|
|
||||||
restricted_queryset = manager.none()
|
if not meta:
|
||||||
|
logger.debug(
|
||||||
|
ugettext(
|
||||||
|
'Object "%s" is not a model and cannot be checked for '
|
||||||
|
'access.'
|
||||||
|
) % force_text(obj)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
source_queryset = obj._meta.default_manager.all()
|
||||||
|
|
||||||
|
restricted_queryset = obj._meta.default_manager.none()
|
||||||
for permission in permissions:
|
for permission in permissions:
|
||||||
# Default relationship betweens permissions is OR
|
# Default relationship betweens permissions is OR
|
||||||
# TODO: Add support for AND relationship
|
# TODO: Add support for AND relationship
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from mayan.apps.smart_settings.classes import Namespace
|
from mayan.apps.smart_settings import Namespace
|
||||||
|
|
||||||
from .literals import DEFAULT_MAXIMUM_TITLE_LENGTH
|
from .literals import DEFAULT_MAXIMUM_TITLE_LENGTH
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
padding-top: 60px;
|
padding-top: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
@@ -98,10 +98,14 @@ hr {
|
|||||||
min-height: 120px;
|
min-height: 120px;
|
||||||
padding-bottom: 1px;
|
padding-bottom: 1px;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
text-shadow: 1px 1px 3px rgba(0, 0, 0, 1);
|
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-block .fa {
|
||||||
|
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
.radio ul li {
|
.radio ul li {
|
||||||
list-style-type:none;
|
list-style-type:none;
|
||||||
}
|
}
|
||||||
@@ -111,10 +115,14 @@ a i {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-widget {
|
.dashboard-widget {
|
||||||
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.7);
|
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-widget .panel-heading i {
|
||||||
|
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard-widget-icon {
|
.dashboard-widget-icon {
|
||||||
font-size: 200%;
|
font-size: 200%;
|
||||||
}
|
}
|
||||||
@@ -208,22 +216,6 @@ a i {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.source-column-label {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-highlighted {
|
|
||||||
box-shadow: 0px 0px 3px #18bc9c, 10px 10px 20px #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-highlighted:hover {
|
|
||||||
box-shadow: 0px 0px 3px #18bc9c, 10px 10px 20px #000000, 0px 0px 8px #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-item:not(.panel-highlighted):hover {
|
|
||||||
box-shadow: 0px 0px 8px #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Content */
|
/* Content */
|
||||||
@media (min-width:1200px) {
|
@media (min-width:1200px) {
|
||||||
.container-fluid {
|
.container-fluid {
|
||||||
@@ -253,6 +245,14 @@ a i {
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thin_border {
|
||||||
|
border: 1px solid black;
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.thin_border-thumbnail {
|
.thin_border-thumbnail {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -262,18 +262,10 @@ a i {
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Must go after .thin_border-thumbnail */
|
|
||||||
.thin_border {
|
|
||||||
border: 1px solid black;
|
|
||||||
display: inline;
|
|
||||||
margin-left: 0px;
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ajax-spinner {
|
#ajax-spinner {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 16px;
|
top: 12px;
|
||||||
left: 10px;
|
right: 10px;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
width: 25px;
|
width: 25px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
@@ -339,7 +331,7 @@ a i {
|
|||||||
.main {
|
.main {
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
margin-left: 210px;
|
/*margin-left: 210px;*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,139 +413,3 @@ a i {
|
|||||||
.btn-list {
|
.btn-list {
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Top navigation
|
|
||||||
* Hide default border to remove 1px line.
|
|
||||||
*/
|
|
||||||
.navbar-fixed-top {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* menu_main */
|
|
||||||
/* Hide for mobile, show later */
|
|
||||||
|
|
||||||
#menu-main {
|
|
||||||
display: none;
|
|
||||||
background-color: #2c3e50;
|
|
||||||
border-right: 1px solid #18bc9c;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-top: 10px;
|
|
||||||
position: fixed;
|
|
||||||
top: 51px;
|
|
||||||
width: 210px;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
#menu-main {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand {
|
|
||||||
text-align: center;
|
|
||||||
width: 210px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.main .page-header {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand {
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-fluid {
|
|
||||||
margin-right: 0px;
|
|
||||||
margin-left: 0px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#accordion-sidebar a {
|
|
||||||
padding: 10px 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#accordion-sidebar a[aria-expanded="true"] {
|
|
||||||
background: #1a242f;
|
|
||||||
}
|
|
||||||
|
|
||||||
#accordion-sidebar .panel {
|
|
||||||
border: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#accordion-sidebar a {
|
|
||||||
text-decoration: none;
|
|
||||||
outline: none;
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#accordion-sidebar .panel-heading {
|
|
||||||
background-color: #2c3e50;
|
|
||||||
color: white;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#accordion-sidebar .panel-heading:hover {
|
|
||||||
background-color: #517394;
|
|
||||||
}
|
|
||||||
|
|
||||||
#accordion-sidebar > .panel > div > .panel-body > ul > li > a:hover {
|
|
||||||
background-color: #517394;
|
|
||||||
}
|
|
||||||
|
|
||||||
#accordion-sidebar > .panel > div > .panel-body > ul > li.active {
|
|
||||||
background: #1a242f;
|
|
||||||
}
|
|
||||||
|
|
||||||
#accordion-sidebar .panel-title {
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#accordion-sidebar .panel-body {
|
|
||||||
font-size: 13px;
|
|
||||||
border: 0px;
|
|
||||||
background-color: #2c3e50;
|
|
||||||
padding-top: 5px;
|
|
||||||
padding-left: 20px;
|
|
||||||
padding-right: 0px;
|
|
||||||
padding-bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#accordion-sidebar .panel-body li {
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#accordion-sidebar .panel-body a {
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-fixed-top {
|
|
||||||
box-shadow: 0px 3px 3px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
box-shadow: 1px 1px 2px rgba(0, 0, 0, .3);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
padding-left: 12px;
|
|
||||||
padding-right: 15px;
|
|
||||||
padding-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#body-plain {
|
|
||||||
padding-top: 0px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ var MayanAppClass = MayanApp;
|
|||||||
|
|
||||||
var partialNavigation = new PartialNavigation({
|
var partialNavigation = new PartialNavigation({
|
||||||
initialURL: initialURL,
|
initialURL: initialURL,
|
||||||
disabledAnchorClasses: [
|
disabledAnchorClasses: ['disabled'],
|
||||||
'btn-multi-item-action', 'disabled', 'pagination-disabled'
|
|
||||||
],
|
|
||||||
excludeAnchorClasses: ['fancybox', 'new_window', 'non-ajax'],
|
excludeAnchorClasses: ['fancybox', 'new_window', 'non-ajax'],
|
||||||
|
formBeforeSerializeCallbacks: [MayanApp.MultiObjectFormProcess],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,47 +17,30 @@ class MayanApp {
|
|||||||
|
|
||||||
// Class methods and variables
|
// Class methods and variables
|
||||||
|
|
||||||
static countChecked() {
|
static MultiObjectFormProcess ($form, options) {
|
||||||
var checkCount = $('.check-all-slave:checked').length;
|
/*
|
||||||
|
* ajaxForm callback to add the external item checkboxes to the
|
||||||
|
* submitted form
|
||||||
|
*/
|
||||||
|
|
||||||
if (checkCount) {
|
if ($form.hasClass('form-multi-object-action')) {
|
||||||
$('#multi-item-title').hide();
|
// Turn form data into an object
|
||||||
$('#multi-item-actions').show();
|
var formArray = $form.serializeArray().reduce(function (obj, item) {
|
||||||
} else {
|
obj[item.name] = item.value;
|
||||||
$('#multi-item-title').show();
|
return obj;
|
||||||
$('#multi-item-actions').hide();
|
}, {});
|
||||||
|
|
||||||
|
// Add all checked checkboxes to the form data
|
||||||
|
$('.form-multi-object-action-checkbox:checked').each(function() {
|
||||||
|
var $this = $(this);
|
||||||
|
formArray[$this.attr('name')] = $this.attr('value');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the form data as the data to send
|
||||||
|
options.data = formArray;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static setupMultiItemActions () {
|
|
||||||
$('body').on('change', '.check-all-slave', function () {
|
|
||||||
MayanApp.countChecked();
|
|
||||||
});
|
|
||||||
|
|
||||||
$('body').on('click', '.btn-multi-item-action', function (event) {
|
|
||||||
var id_list = [];
|
|
||||||
$('.check-all-slave:checked').each(function (index, value) {
|
|
||||||
//Split the name (ie:"pk_200") and extract only the ID
|
|
||||||
id_list.push(value.name.split('_')[1]);
|
|
||||||
});
|
|
||||||
event.preventDefault();
|
|
||||||
partialNavigation.setLocation(
|
|
||||||
$(this).attr('href') + '?id_list=' + id_list.join(',')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static setupNavBarState () {
|
|
||||||
$('body').on('click', '.a-main-menu-accordion-link', function (event) {
|
|
||||||
console.log('ad');
|
|
||||||
$('.a-main-menu-accordion-link').each(function (index, value) {
|
|
||||||
$(this).parent().removeClass('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
$(this).parent().addClass('active');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static updateNavbarState () {
|
static updateNavbarState () {
|
||||||
var uri = new URI(window.location.hash);
|
var uri = new URI(window.location.hash);
|
||||||
var uriFragment = uri.fragment();
|
var uriFragment = uri.fragment();
|
||||||
@@ -172,18 +155,16 @@ class MayanApp {
|
|||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
this.setupAJAXSpinner();
|
this.setupAJAXSpinner();
|
||||||
|
this.setupAutoSubmit();
|
||||||
this.setupFormHotkeys();
|
this.setupFormHotkeys();
|
||||||
this.setupFullHeightResizing();
|
this.setupFullHeightResizing();
|
||||||
this.setupItemsSelector();
|
this.setupItemsSelector();
|
||||||
MayanApp.setupMultiItemActions();
|
|
||||||
this.setupNavbarCollapse();
|
this.setupNavbarCollapse();
|
||||||
MayanApp.setupNavBarState();
|
|
||||||
this.setupNewWindowAnchor();
|
this.setupNewWindowAnchor();
|
||||||
$.each(this.ajaxMenusOptions, function(index, value) {
|
$.each(this.ajaxMenusOptions, function(index, value) {
|
||||||
value.app = self;
|
value.app = self;
|
||||||
app.doRefreshAJAXMenu(value);
|
app.doRefreshAJAXMenu(value);
|
||||||
});
|
});
|
||||||
this.setupPanelSelection();
|
|
||||||
partialNavigation.initialize();
|
partialNavigation.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +188,14 @@ class MayanApp {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupAutoSubmit () {
|
||||||
|
$('body').on('change', '.select-auto-submit', function () {
|
||||||
|
if ($(this).val()) {
|
||||||
|
$(this.form).trigger('submit');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setupFormHotkeys () {
|
setupFormHotkeys () {
|
||||||
$('body').on('keypress', '.form-hotkey-enter', function (e) {
|
$('body').on('keypress', '.form-hotkey-enter', function (e) {
|
||||||
if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) {
|
if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) {
|
||||||
@@ -237,22 +226,9 @@ class MayanApp {
|
|||||||
app.lastChecked = null;
|
app.lastChecked = null;
|
||||||
|
|
||||||
$('body').on('click', '.check-all', function (event) {
|
$('body').on('click', '.check-all', function (event) {
|
||||||
var $this = $(this);
|
|
||||||
var checked = $(event.target).prop('checked');
|
var checked = $(event.target).prop('checked');
|
||||||
var $checkBoxes = $('.check-all-slave');
|
var $checkBoxes = $('.check-all-slave');
|
||||||
|
|
||||||
if (checked === undefined) {
|
|
||||||
checked = $this.data('checked');
|
|
||||||
checked = !checked;
|
|
||||||
$this.data('checked', checked);
|
|
||||||
|
|
||||||
if (checked) {
|
|
||||||
$this.find('[data-fa-i2svg]').addClass($this.data('icon-checked')).removeClass($this.data('icon-unchecked'));
|
|
||||||
} else {
|
|
||||||
$this.find('[data-fa-i2svg]').addClass($this.data('icon-unchecked')).removeClass($this.data('icon-checked'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$checkBoxes.prop('checked', checked);
|
$checkBoxes.prop('checked', checked);
|
||||||
$checkBoxes.trigger('change');
|
$checkBoxes.trigger('change');
|
||||||
});
|
});
|
||||||
@@ -298,58 +274,6 @@ class MayanApp {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setupPanelSelection () {
|
|
||||||
var app = this;
|
|
||||||
|
|
||||||
// Setup panel highlighting on check
|
|
||||||
$('body').on('change', '.check-all-slave', function (event) {
|
|
||||||
var checked = $(event.target).prop('checked');
|
|
||||||
if (checked) {
|
|
||||||
$(this).closest('.panel-item').addClass('panel-highlighted');
|
|
||||||
} else {
|
|
||||||
$(this).closest('.panel-item').removeClass('panel-highlighted');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('body').on('click', '.panel-item', function (event) {
|
|
||||||
var $this = $(this);
|
|
||||||
var targetSrc = $(event.target).prop('src');
|
|
||||||
var targetHref = $(event.target).prop('href');
|
|
||||||
var targetIsButton = event.target.tagName === 'BUTTON';
|
|
||||||
var lastChecked = null;
|
|
||||||
|
|
||||||
if ((targetSrc === undefined) && (targetHref === undefined) && (targetIsButton === false)) {
|
|
||||||
var $checkbox = $this.find('.check-all-slave');
|
|
||||||
var checked = $checkbox.prop('checked');
|
|
||||||
|
|
||||||
if (checked) {
|
|
||||||
$checkbox.prop('checked', '');
|
|
||||||
$checkbox.trigger('change');
|
|
||||||
} else {
|
|
||||||
$checkbox.prop('checked', 'checked');
|
|
||||||
$checkbox.trigger('change');
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!app.lastChecked) {
|
|
||||||
app.lastChecked = $checkbox;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.shiftKey) {
|
|
||||||
var $checkBoxes = $('.check-all-slave');
|
|
||||||
|
|
||||||
var start = $checkBoxes.index($checkbox);
|
|
||||||
var end = $checkBoxes.index(app.lastChecked);
|
|
||||||
|
|
||||||
$checkBoxes.slice(
|
|
||||||
Math.min(start, end), Math.max(start, end) + 1
|
|
||||||
).prop('checked', app.lastChecked.prop('checked')).trigger('change');
|
|
||||||
}
|
|
||||||
app.lastChecked = $checkbox;
|
|
||||||
window.getSelection().removeAllRanges();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setupScrollView () {
|
setupScrollView () {
|
||||||
$('.scrollable').scrollview();
|
$('.scrollable').scrollview();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,9 +136,6 @@
|
|||||||
},
|
},
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
];
|
];
|
||||||
$(function () {
|
|
||||||
$('[data-toggle="tooltip"]').tooltip();
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
{% block javascript %}{% endblock %}
|
{% block javascript %}{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body id="body-plain">
|
<body>
|
||||||
{% block content_plain %}{% endblock %}
|
{% block content_plain %}{% endblock %}
|
||||||
|
|
||||||
<script src="{% static 'appearance/node_modules/jquery/dist/jquery.min.js' %}" type="text/javascript"></script>
|
<script src="{% static 'appearance/node_modules/jquery/dist/jquery.min.js' %}" type="text/javascript"></script>
|
||||||
|
|||||||
@@ -11,9 +11,41 @@
|
|||||||
{% include 'appearance/no_results.html' %}
|
{% include 'appearance/no_results.html' %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% include "appearance/list_header.html" %}
|
<h4>
|
||||||
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
|
{% if page_obj %}
|
||||||
|
{% if page_obj.paginator.num_pages != 1 %}
|
||||||
|
{% blocktrans with page_obj.start_index as start and page_obj.end_index as end and page_obj.paginator.object_list|length as total and page_obj.number as page_number and page_obj.paginator.num_pages as total_pages %}Total ({{ start }} - {{ end }} out of {{ total }}) (Page {{ page_number }} of {{ total_pages }}){% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% blocktrans with page_obj.paginator.object_list|length as total %}Total: {{ total }}{% endblocktrans %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% blocktrans with object_list|length as total %}Total: {{ total }}{% endblocktrans %}
|
||||||
|
{% endif %}
|
||||||
|
</h4>
|
||||||
|
<hr>
|
||||||
<div class="well center-block">
|
<div class="well center-block">
|
||||||
|
<div class="clearfix">
|
||||||
|
<div class="pull-right">
|
||||||
|
<form action="{% url 'common:multi_object_action_view' %}" class="form-multi-object-action" method="get">
|
||||||
|
{% if object_list %}
|
||||||
|
{% if not hide_multi_item_actions %}
|
||||||
|
{% get_multi_item_links_form object_list %}
|
||||||
|
{% endif %}
|
||||||
|
{% if multi_item_actions %}
|
||||||
|
<fieldset style="margin-top: -10px;">
|
||||||
|
<input class="check-all" type="checkbox"/>
|
||||||
|
{{ multi_item_form }}
|
||||||
|
</fieldset>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if object_list %}
|
||||||
|
<hr style="border-bottom: 1px solid lightgrey;">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="row row-items">
|
<div class="row row-items">
|
||||||
{% for object in object_list %}
|
{% for object in object_list %}
|
||||||
<div class="{{ column_class|default:'col-xs-12 col-sm-4 col-md-3 col-lg-2' }}">
|
<div class="{{ column_class|default:'col-xs-12 col-sm-4 col-md-3 col-lg-2' }}">
|
||||||
@@ -21,9 +53,9 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label for="id_indexes_0" style="cursor: auto;">
|
<label for="id_indexes_0">
|
||||||
{% if links_multi_menus_results %}
|
{% if multi_item_actions %}
|
||||||
<input class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" style="cursor: pointer;" type="checkbox" />
|
<input class="form-multi-object-action-checkbox check-all-slave checkbox" type="checkbox" name="pk_{{ object.pk }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<span style="color: white; word-break: break-all; overflow-wrap: break-word;">
|
<span style="color: white; word-break: break-all; overflow-wrap: break-word;">
|
||||||
@@ -36,7 +68,12 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% navigation_get_source_columns source=object only_identifier=True as source_column %}
|
{% navigation_get_source_columns source=object only_identifier=True as source_column %}
|
||||||
{% navigation_source_column_resolve column=source_column as column_value %}
|
{% navigation_source_column_resolve column=source_column as column_value %}
|
||||||
{{ column_value }}
|
|
||||||
|
{% if source_column.is_attribute_absolute_url or source_column.is_object_absolute_url %}
|
||||||
|
<a href="{% navigation_source_column_get_absolute_url source_column=source_column obj=object %}">{{ column_value }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ column_value }}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -45,10 +82,11 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
|
||||||
{% if not hide_columns %}
|
{% if not hide_columns %}
|
||||||
{% navigation_get_source_columns source=object exclude_identifier=True as source_columns %}
|
{% navigation_get_source_columns source=object exclude_identifier=True as source_columns %}
|
||||||
{% for column in source_columns %}
|
{% for column in source_columns %}
|
||||||
<div class="text-center" style="">{% navigation_source_column_resolve column=column as column_value %}{% if column_value != '' %}{% if column.include_label %}<span class="source-column-label">{{ column.label }}</span>: {% endif %}{{ column_value }}{% endif %}</div>
|
<div class="text-center" style="">{% navigation_source_column_resolve column=column as column_value %}{% if column_value != '' %}{% if column.include_label %}{{ column.label }}: {% endif %}{{ column_value }}{% endif %}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -98,6 +136,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% include 'pagination/pagination.html' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% load appearance_tags %}
|
|
||||||
{% load common_tags %}
|
{% load common_tags %}
|
||||||
{% load navigation_tags %}
|
{% load navigation_tags %}
|
||||||
|
|
||||||
@@ -12,16 +11,44 @@
|
|||||||
{% include 'appearance/no_results.html' %}
|
{% include 'appearance/no_results.html' %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% include "appearance/list_header.html" %}
|
<h4>
|
||||||
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
|
{% if page_obj %}
|
||||||
|
{% if page_obj.paginator.num_pages != 1 %}
|
||||||
|
{% blocktrans with page_obj.start_index as start and page_obj.end_index as end and page_obj.paginator.object_list|length as total and page_obj.number as page_number and page_obj.paginator.num_pages as total_pages %}Total ({{ start }} - {{ end }} out of {{ total }}) (Page {{ page_number }} of {{ total_pages }}){% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% blocktrans with page_obj.paginator.object_list|length as total %}Total: {{ total }}{% endblocktrans %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% blocktrans with object_list|length as total %}Total: {{ total }}{% endblocktrans %}
|
||||||
|
{% endif %}
|
||||||
|
</h4>
|
||||||
|
<hr>
|
||||||
|
|
||||||
<div class="well center-block">
|
<div class="well center-block">
|
||||||
|
<div class="clearfix">
|
||||||
|
<div class="pull-right">
|
||||||
|
<form action="{% url 'common:multi_object_action_view' %}" class="form-multi-object-action" method="get">
|
||||||
|
{% if object_list %}
|
||||||
|
{% if not hide_multi_item_actions %}
|
||||||
|
{% get_multi_item_links_form object_list %}
|
||||||
|
{% endif %}
|
||||||
|
{% if multi_item_actions %}
|
||||||
|
<fieldset style="margin-top: -10px; margin-bottom: 10px;">
|
||||||
|
{{ multi_item_form }}
|
||||||
|
</fieldset>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-condensed table-striped">
|
<table class="table table-condensed table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
{% if not hide_header %}
|
{% if not hide_header %}
|
||||||
<tr>
|
<tr>
|
||||||
{% if links_multi_menus_results %}
|
{% if multi_item_actions %}
|
||||||
<th class="first"></th>
|
<th class="first"><input class="checkbox check-all" type="checkbox" /></th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not hide_object %}
|
{% if not hide_object %}
|
||||||
@@ -31,40 +58,30 @@
|
|||||||
{% if source_column %}
|
{% if source_column %}
|
||||||
<th>
|
<th>
|
||||||
{% if source_column.is_sortable %}
|
{% if source_column.is_sortable %}
|
||||||
<a href="{% navigation_get_sort_field_querystring column=source_column %}">{{ source_column.label }}</a>
|
<a href="{% navigation_get_sort_field_querystring column=source_column %}">{{ source_column.label }}
|
||||||
{% if source_column.get_sort_field == sort_field %}
|
{% if source_column.get_sort_field == sort_field %}
|
||||||
{% if icon_sort %}{{ icon_sort.render }}{% endif %}
|
{% if icon_sort %}{{ icon_sort.render }}{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ source_column.label }}
|
{{ source_column.label }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if source_column.help_text %}
|
|
||||||
<span data-toggle="tooltip" data-placement="bottom" title="{{ source_column.help_text }}">
|
|
||||||
{% get_icon icon_path='mayan.apps.navigation.icons.icon_source_column_help_text' %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</th>
|
</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not hide_columns %}
|
{% if not hide_columns %}
|
||||||
{% navigation_get_source_columns source=object_list exclude_identifier=True as source_columns %}
|
{% navigation_get_source_columns source=object_list exclude_identifier=True as source_columns %}
|
||||||
{% for source_column in source_columns %}
|
{% for column in source_columns %}
|
||||||
<th>
|
<th>
|
||||||
{% if source_column.is_sortable %}
|
{% if column.is_sortable %}
|
||||||
<a href="{% navigation_get_sort_field_querystring column=source_column %}">{{ source_column.label }}</a>
|
<a href="{% navigation_get_sort_field_querystring column=column %}">{{ column.label }}
|
||||||
{% if source_column.get_sort_field == sort_field %}
|
{% if column.get_sort_field == sort_field %}
|
||||||
{% if icon_sort %}{{ icon_sort.render }}{% endif %}
|
{% if icon_sort %}{{ icon_sort.render }}{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ source_column.label }}
|
{{ column.label }}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if source_column.help_text %}
|
|
||||||
<span data-toggle="tooltip" data-placement="bottom" title="{{ source_column.help_text }}">
|
|
||||||
{% get_icon icon_path='mayan.apps.navigation.icons.icon_source_column_help_text' %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</th>
|
</th>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -82,9 +99,9 @@
|
|||||||
{% for object in object_list %}
|
{% for object in object_list %}
|
||||||
<tr>
|
<tr>
|
||||||
|
|
||||||
{% if links_multi_menus_results %}
|
{% if multi_item_actions %}
|
||||||
<td>
|
<td>
|
||||||
<input class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" type="checkbox" value="" />
|
<input type="checkbox" class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" value="" />
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -95,7 +112,11 @@
|
|||||||
{% navigation_source_column_resolve column=source_column as column_value %}
|
{% navigation_source_column_resolve column=source_column as column_value %}
|
||||||
{% if column_value %}
|
{% if column_value %}
|
||||||
<td>
|
<td>
|
||||||
{{ column_value }}
|
{% if source_column.is_attribute_absolute_url or source_column.is_object_absolute_url %}
|
||||||
|
<a href="{% navigation_source_column_get_absolute_url source_column=source_column obj=object %}">{{ column_value }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ column_value }}
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -149,6 +170,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{% include 'pagination/pagination.html' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
{% load i18n %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% load common_tags %}
|
|
||||||
{% load navigation_tags %}
|
|
||||||
|
|
||||||
{% if object_list %}
|
|
||||||
<h4>
|
|
||||||
{% if page_obj %}
|
|
||||||
{% if page_obj.paginator.num_pages != 1 %}
|
|
||||||
{% blocktrans with page_obj.start_index as start and page_obj.end_index as end and page_obj.paginator.object_list|length as total and page_obj.number as page_number and page_obj.paginator.num_pages as total_pages %}Total ({{ start }} - {{ end }} out of {{ total }}) (Page {{ page_number }} of {{ total_pages }}){% endblocktrans %}
|
|
||||||
{% else %}
|
|
||||||
{% blocktrans with page_obj.paginator.object_list|length as total %}Total: {{ total }}{% endblocktrans %}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
{% blocktrans with object_list|length as total %}Total: {{ total }}{% endblocktrans %}
|
|
||||||
{% endif %}
|
|
||||||
</h4>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
{% if not hide_multi_item_actions %}
|
|
||||||
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="clearfix">
|
|
||||||
{% include 'appearance/list_toolbar.html' %}
|
|
||||||
</div>
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% load common_tags %}
|
|
||||||
{% load navigation_tags %}
|
|
||||||
|
|
||||||
{% if is_paginated or links_multi_menus_results %}
|
|
||||||
<div class="well center-block toolbar">
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if links_multi_menus_results %}
|
|
||||||
<div class="pull-left">
|
|
||||||
<div class="btn-toolbar" role="toolbar" style="margin-right: 10px;">
|
|
||||||
<div class="btn-group">
|
|
||||||
<a class="btn btn-default btn-sm check-all" data-checked=false data-icon-checked="fa fa-check-square" data-icon-unchecked="far fa-square" title="{% trans 'Select/Deselect all' %}">
|
|
||||||
<i class="far fa-square"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if is_paginated %}
|
|
||||||
<div class="pull-left">
|
|
||||||
<div class="btn-toolbar" role="toolbar">
|
|
||||||
<div class="btn-group">
|
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<a class="btn btn-default btn-sm" href="?{{ page_obj.previous_page_number.querystring }}">‹‹</a>
|
|
||||||
{% else %}
|
|
||||||
<a class="btn btn-default btn-sm disabled" href="#">‹‹</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% for page in page_obj.pages %}
|
|
||||||
|
|
||||||
{% if page %}
|
|
||||||
|
|
||||||
{% ifequal page page_obj.number %}
|
|
||||||
<a class="active btn btn-default btn-sm pagination-disabled" href="#">{{ page }}</a>
|
|
||||||
{% else %}
|
|
||||||
<a class="btn btn-default btn-sm" href="?{{ page.querystring }}">{{ page }}</a>
|
|
||||||
{% endifequal %}
|
|
||||||
{% else %}
|
|
||||||
<a class="btn btn-default btn-sm disabled" href="#">...</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
{% if page_obj.has_next %}
|
|
||||||
<a class="btn btn-default btn-sm" href="?{{ page_obj.next_page_number.querystring }}">››</a>
|
|
||||||
{% else %}
|
|
||||||
<a class="btn btn-default btn-sm disabled" href="#">››</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if links_multi_menus_results %}
|
|
||||||
<p class="pull-right" id="multi-item-title" style="line-height: 16px; padding-top: 8px;">{% trans 'Select items to activate bulk actions. Use Shift + click to select many.' %}</p>
|
|
||||||
|
|
||||||
<div class="pull-right btn-group" id="multi-item-actions" style="display: none;">
|
|
||||||
<button aria-expanded="true" class="btn btn-danger btn-sm dropdown-toggle" data-toggle="dropdown" type="button">
|
|
||||||
{% trans 'Bulk actions' %}
|
|
||||||
<span class="caret"></span>
|
|
||||||
<span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu" role="menu">
|
|
||||||
{% for multi_item_menu_results in links_multi_menus_results %}
|
|
||||||
{% for link_group in multi_item_menu_results.link_groups %}
|
|
||||||
{% with link_group.links as object_navigation_links %}
|
|
||||||
{% with 'true' as as_li %}
|
|
||||||
{% with 'true' as hide_active_anchor %}
|
|
||||||
{% with 'btn-sm btn-multi-item-action' as link_classes %}
|
|
||||||
{% include 'navigation/generic_navigation.html' %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
{% if not forloop.last and link_group %}
|
|
||||||
<li class="divider"></li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if is_paginated or links_multi_menus_results %}
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
@@ -3,11 +3,10 @@
|
|||||||
{% load navigation_tags %}
|
{% load navigation_tags %}
|
||||||
{% load smart_settings_tags %}
|
{% load smart_settings_tags %}
|
||||||
|
|
||||||
{% spaceless %}
|
|
||||||
<nav class="navbar navbar-default navbar-fixed-top">
|
<nav class="navbar navbar-default navbar-fixed-top">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="navbar-header">
|
<div class="navbar-header">
|
||||||
<button aria-expanded="false" aria-controls="navbar" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" type="button">
|
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
|
||||||
<span class="sr-only">{% trans 'Toggle navigation' %}</span>
|
<span class="sr-only">{% trans 'Toggle navigation' %}</span>
|
||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
@@ -15,10 +14,9 @@
|
|||||||
</button>
|
</button>
|
||||||
<a class="navbar-brand" href="{% url home_view %}">{% smart_setting 'COMMON_PROJECT_TITLE' %}</a>
|
<a class="navbar-brand" href="{% url home_view %}">{% smart_setting 'COMMON_PROJECT_TITLE' %}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="navbar" class="navbar-collapse collapse">
|
<div id="navbar" class="navbar-collapse collapse">
|
||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav">
|
||||||
{% navigation_resolve_menu name='topbar' as topbar_menus_results %}
|
{% navigation_resolve_menu name='main' as topbar_menus_results %}
|
||||||
{% for tobpar_menu_result in topbar_menus_results %}
|
{% for tobpar_menu_result in topbar_menus_results %}
|
||||||
{% for link_group in tobpar_menu_result.link_groups %}
|
{% for link_group in tobpar_menu_result.link_groups %}
|
||||||
{% for link in link_group.links %}
|
{% for link in link_group.links %}
|
||||||
@@ -36,8 +34,24 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{% get_menu_links name='main' as menu_links %}
|
||||||
|
{% for link_set in menu_links %}
|
||||||
|
{% for link in link_set %}
|
||||||
|
{% with 'true' as as_li %}
|
||||||
|
{% with 'true' as hide_active_anchor %}
|
||||||
|
{% with 'active' as li_class_active %}
|
||||||
|
{% with 'first' as li_class_first %}
|
||||||
|
{% with ' ' as link_classes %}
|
||||||
|
{% include 'navigation/generic_subnavigation.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
{% endspaceless %}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% load navigation_tags %}
|
|
||||||
{% load smart_settings_tags %}
|
|
||||||
|
|
||||||
{% load common_tags %}
|
|
||||||
{% load navigation_tags %}
|
|
||||||
|
|
||||||
{% spaceless %}
|
|
||||||
<div class="panel-group" id="accordion-sidebar" role="tablist" aria-multiselectable="true">
|
|
||||||
{% navigation_resolve_menu name='main' as main_menus_results %}
|
|
||||||
{% for main_menu_results in main_menus_results %}
|
|
||||||
{% for link_group in main_menu_results.link_groups %}
|
|
||||||
{% for link in link_group.links %}
|
|
||||||
{% with 'active' as li_class_active %}
|
|
||||||
{% with ' ' as link_classes %}
|
|
||||||
{% if link|get_type == "<class 'mayan.apps.navigation.classes.Menu'>" %}
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading" role="tab" id="headingOne">
|
|
||||||
<h4 class="panel-title">
|
|
||||||
<a class="non-ajax collapsed" role="button" data-toggle="collapse" data-parent="#accordion-sidebar" href="#accordion-body-{{ forloop.counter }}" aria-expanded="false" aria-controls="collapseOne">
|
|
||||||
<div class="pull-left">
|
|
||||||
{% if link.icon %}
|
|
||||||
<i class="hidden-xs hidden-sm hidden-md {{ link.icon }}"></i>
|
|
||||||
{% endif %}
|
|
||||||
{% if link.icon_class %}{{ link.icon_class.render }}{% endif %}
|
|
||||||
{{ link.label }}
|
|
||||||
</div>
|
|
||||||
<div class="accordion-indicator pull-right"><span class="caret"></span></div>
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
</a>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div id="accordion-body-{{ forloop.counter }}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingOne">
|
|
||||||
<div class="panel-body">
|
|
||||||
<ul class="list-unstyled">
|
|
||||||
{% navigation_resolve_menu name=link.name as sub_menus_results %}
|
|
||||||
{% for sub_menu_results in sub_menus_results %}
|
|
||||||
{% for link_group in sub_menu_results.link_groups %}
|
|
||||||
{% with '' as link_class_active %}
|
|
||||||
{% with 'a-main-menu-accordion-link' as link_classes %}
|
|
||||||
{% with 'true' as as_li %}
|
|
||||||
{% with link_group.links as object_navigation_links %}
|
|
||||||
{% include 'navigation/generic_navigation.html' %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading" role="tab" id="headingOne">
|
|
||||||
<h4 class="panel-title">
|
|
||||||
{% include 'navigation/generic_link_instance.html' %}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endspaceless %}
|
|
||||||
@@ -32,11 +32,8 @@
|
|||||||
{% if appearance_type == 'plain' %}
|
{% if appearance_type == 'plain' %}
|
||||||
{% block content_plain %}{% endblock %}
|
{% block content_plain %}{% endblock %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div id="menu-topbar">
|
|
||||||
{% include 'appearance/menu_topbar.html' %}
|
|
||||||
</div>
|
|
||||||
<div id="menu-main">
|
<div id="menu-main">
|
||||||
{% include 'appearance/menu_main.html' %}
|
{% include 'appearance/main_menu.html' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="row zero-margin">
|
<div class="row zero-margin">
|
||||||
@@ -104,18 +101,11 @@
|
|||||||
var app = new MayanApp({
|
var app = new MayanApp({
|
||||||
ajaxMenusOptions: [
|
ajaxMenusOptions: [
|
||||||
{
|
{
|
||||||
callback: MayanApp.updateNavbarState,
|
|
||||||
interval: 5000,
|
interval: 5000,
|
||||||
menuSelector: '#menu-main',
|
menuSelector: '#menu-main',
|
||||||
name: 'menu_main',
|
name: 'menu_main',
|
||||||
url: '{% url "rest_api:template-detail" "menu_main" %}'
|
url: '{% url "rest_api:template-detail" "menu_main" %}'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
interval: 5000,
|
|
||||||
menuSelector: '#menu-topbar',
|
|
||||||
name: 'menu_topbar',
|
|
||||||
url: '{% url "rest_api:template-detail" "menu_topbar" %}'
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{% if page %}
|
{% if page %}
|
||||||
|
|
||||||
{% ifequal page page_obj.number %}
|
{% ifequal page page_obj.number %}
|
||||||
<li class="active"><a class="pagination-disabled" href="#">{{ page }}</a></li>
|
<li class="active"><a href="#">{{ page }}</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a href="?{{ page.querystring }}">{{ page }}</a></li>
|
<li><a href="?{{ page.querystring }}">{{ page }}</a></li>
|
||||||
{% endifequal %}
|
{% endifequal %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from mayan.apps.smart_settings.classes import Namespace
|
from mayan.apps.smart_settings import Namespace
|
||||||
|
|
||||||
from .literals import DEFAULT_LOGIN_METHOD, DEFAULT_MAXIMUM_SESSION_LENGTH
|
from .literals import DEFAULT_LOGIN_METHOD, DEFAULT_MAXIMUM_SESSION_LENGTH
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
{% motd %}
|
{% motd %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-sm-10 col-sm-offset-1 col-md-10 col-md-offset-1 col-lg-6 col-lg-offset-3">
|
<div class="col-xs-10 col-xs-offset-1 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-4 col-lg-offset-4">
|
||||||
<div class="panel panel-primary">
|
<div class="panel panel-primary">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title"> </h3>
|
<h3 class="panel-title"> </h3>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from django.utils.http import urlunquote_plus
|
|||||||
from mayan.apps.common.tests import GenericViewTestCase
|
from mayan.apps.common.tests import GenericViewTestCase
|
||||||
from mayan.apps.smart_settings.classes import Namespace
|
from mayan.apps.smart_settings.classes import Namespace
|
||||||
from mayan.apps.user_management.permissions import permission_user_edit
|
from mayan.apps.user_management.permissions import permission_user_edit
|
||||||
|
from mayan.apps.user_management.tests.mixins import UserTestMixin
|
||||||
from mayan.apps.user_management.tests.literals import TEST_USER_PASSWORD_EDITED
|
from mayan.apps.user_management.tests.literals import TEST_USER_PASSWORD_EDITED
|
||||||
|
|
||||||
from ..settings import setting_maximum_session_length
|
from ..settings import setting_maximum_session_length
|
||||||
@@ -261,7 +262,7 @@ class UserLoginTestCase(GenericViewTestCase):
|
|||||||
self.assertEqual(response.redirect_chain, [(TEST_REDIRECT_URL, 302)])
|
self.assertEqual(response.redirect_chain, [(TEST_REDIRECT_URL, 302)])
|
||||||
|
|
||||||
|
|
||||||
class UserViewTestCase(UserPasswordViewTestMixin, GenericViewTestCase):
|
class UserViewTestCase(UserTestMixin, UserPasswordViewTestMixin, GenericViewTestCase):
|
||||||
def test_user_set_password_view_no_access(self):
|
def test_user_set_password_view_no_access(self):
|
||||||
self._create_test_user()
|
self._create_test_user()
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from mayan.apps.smart_settings.classes import Namespace
|
from mayan.apps.smart_settings import Namespace
|
||||||
|
|
||||||
from .literals import DEFAULT_EMAIL, DEFAULT_PASSWORD, DEFAULT_USERNAME
|
from .literals import DEFAULT_EMAIL, DEFAULT_PASSWORD, DEFAULT_USERNAME
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{% if autoadmin_properties.account %}
|
{% if autoadmin_properties.account %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-sm-10 col-sm-offset-1 col-md-10 col-md-offset-1 col-lg-6 col-lg-offset-3">
|
<div class="col-xs-10 col-xs-offset-1 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-4 col-lg-offset-4">
|
||||||
<br>
|
<br>
|
||||||
<div class="panel panel-primary">
|
<div class="panel panel-primary">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.11.22 on 2019-07-29 02:36
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('cabinets', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='cabinet',
|
|
||||||
name='label',
|
|
||||||
field=models.CharField(help_text='A short text used to identify the cabinet.', max_length=128, verbose_name='Label'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -32,10 +32,7 @@ class Cabinet(MPTTModel):
|
|||||||
blank=True, db_index=True, null=True, on_delete=models.CASCADE,
|
blank=True, db_index=True, null=True, on_delete=models.CASCADE,
|
||||||
related_name='children', to='self'
|
related_name='children', to='self'
|
||||||
)
|
)
|
||||||
label = models.CharField(
|
label = models.CharField(max_length=128, verbose_name=_('Label'))
|
||||||
help_text=_('A short text used to identify the cabinet.'),
|
|
||||||
max_length=128, verbose_name=_('Label')
|
|
||||||
)
|
|
||||||
documents = models.ManyToManyField(
|
documents = models.ManyToManyField(
|
||||||
blank=True, related_name='cabinets', to=Document,
|
blank=True, related_name='cabinets', to=Document,
|
||||||
verbose_name=_('Documents')
|
verbose_name=_('Documents')
|
||||||
|
|||||||
@@ -12,62 +12,55 @@ from .views import (
|
|||||||
CabinetDeleteView, CabinetDetailView, CabinetEditView, CabinetListView,
|
CabinetDeleteView, CabinetDetailView, CabinetEditView, CabinetListView,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns_cabinets = [
|
urlpatterns = [
|
||||||
url(
|
url(
|
||||||
regex=r'^cabinets/$', view=CabinetListView.as_view(), name='cabinet_list'
|
regex=r'^list/$', view=CabinetListView.as_view(), name='cabinet_list'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^cabinets/create/$', view=CabinetCreateView.as_view(),
|
regex=r'^(?P<pk>\d+)/child/add/$', view=CabinetChildAddView.as_view(),
|
||||||
name='cabinet_create'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
regex=r'^cabinets/(?P<pk>\d+)/children/add/$', view=CabinetChildAddView.as_view(),
|
|
||||||
name='cabinet_child_add'
|
name='cabinet_child_add'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^cabinets/(?P<pk>\d+)/delete/$', view=CabinetDeleteView.as_view(),
|
regex=r'^create/$', view=CabinetCreateView.as_view(),
|
||||||
name='cabinet_delete'
|
name='cabinet_create'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^cabinets/(?P<pk>\d+)/edit/$', view=CabinetEditView.as_view(),
|
regex=r'^(?P<pk>\d+)/edit/$', view=CabinetEditView.as_view(),
|
||||||
name='cabinet_edit'
|
name='cabinet_edit'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^cabinets/(?P<pk>\d+)/$', view=CabinetDetailView.as_view(),
|
regex=r'^(?P<pk>\d+)/delete/$', view=CabinetDeleteView.as_view(),
|
||||||
|
name='cabinet_delete'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
regex=r'^(?P<pk>\d+)/$', view=CabinetDetailView.as_view(),
|
||||||
name='cabinet_view'
|
name='cabinet_view'
|
||||||
),
|
),
|
||||||
]
|
|
||||||
|
|
||||||
urlpatterns_documents_cabinets = [
|
|
||||||
url(
|
url(
|
||||||
regex=r'^documents/(?P<pk>\d+)/cabinets/add/$',
|
regex=r'^document/(?P<pk>\d+)/cabinet/add/$',
|
||||||
view=DocumentAddToCabinetView.as_view(), name='document_cabinet_add'
|
view=DocumentAddToCabinetView.as_view(), name='document_cabinet_add'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^documents/multiple/cabinets/add/$',
|
regex=r'^document/multiple/cabinet/add/$',
|
||||||
view=DocumentAddToCabinetView.as_view(),
|
view=DocumentAddToCabinetView.as_view(),
|
||||||
name='document_multiple_cabinet_add'
|
name='document_multiple_cabinet_add'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^documents/(?P<pk>\d+)/cabinets/remove/$',
|
regex=r'^document/(?P<pk>\d+)/cabinet/remove/$',
|
||||||
view=DocumentRemoveFromCabinetView.as_view(),
|
view=DocumentRemoveFromCabinetView.as_view(),
|
||||||
name='document_cabinet_remove'
|
name='document_cabinet_remove'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^documents/multiple/cabinets/remove/$',
|
regex=r'^document/multiple/cabinet/remove/$',
|
||||||
view=DocumentRemoveFromCabinetView.as_view(),
|
view=DocumentRemoveFromCabinetView.as_view(),
|
||||||
name='multiple_document_cabinet_remove'
|
name='multiple_document_cabinet_remove'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^documents/(?P<pk>\d+)/cabinets/$',
|
regex=r'^document/(?P<pk>\d+)/cabinet/list/$',
|
||||||
view=DocumentCabinetListView.as_view(), name='document_cabinet_list'
|
view=DocumentCabinetListView.as_view(), name='document_cabinet_list'
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = []
|
|
||||||
urlpatterns.extend(urlpatterns_cabinets)
|
|
||||||
urlpatterns.extend(urlpatterns_documents_cabinets)
|
|
||||||
|
|
||||||
api_urls = [
|
api_urls = [
|
||||||
url(
|
url(
|
||||||
regex=r'^cabinets/(?P<pk>[0-9]+)/documents/(?P<document_pk>[0-9]+)/$',
|
regex=r'^cabinets/(?P<pk>[0-9]+)/documents/(?P<document_pk>[0-9]+)/$',
|
||||||
|
|||||||
@@ -6,12 +6,9 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
|
|
||||||
from mayan.apps.acls.classes import ModelPermission
|
from mayan.apps.acls.classes import ModelPermission
|
||||||
from mayan.apps.common.apps import MayanAppConfig
|
from mayan.apps.common.apps import MayanAppConfig
|
||||||
from mayan.apps.common.menus import (
|
from mayan.apps.common.menus import menu_facet, menu_main, menu_secondary
|
||||||
menu_facet, menu_main, menu_multi_item, menu_secondary
|
|
||||||
)
|
|
||||||
from mayan.apps.dashboards.dashboards import dashboard_main
|
from mayan.apps.dashboards.dashboards import dashboard_main
|
||||||
from mayan.apps.events.classes import ModelEventType
|
from mayan.apps.events.classes import ModelEventType
|
||||||
from mayan.apps.navigation.classes import SourceColumn
|
|
||||||
|
|
||||||
from .dashboard_widgets import DashboardWidgetTotalCheckouts
|
from .dashboard_widgets import DashboardWidgetTotalCheckouts
|
||||||
from .events import (
|
from .events import (
|
||||||
@@ -20,9 +17,8 @@ from .events import (
|
|||||||
)
|
)
|
||||||
from .handlers import handler_check_new_version_creation
|
from .handlers import handler_check_new_version_creation
|
||||||
from .links import (
|
from .links import (
|
||||||
link_check_in_document, link_check_in_document_multiple,
|
link_check_in_document, link_check_out_document, link_check_out_info,
|
||||||
link_check_out_document, link_check_out_document_multiple,
|
link_check_out_list
|
||||||
link_check_out_info, link_check_out_list
|
|
||||||
)
|
)
|
||||||
from .methods import (
|
from .methods import (
|
||||||
method_check_in, method_get_check_out_info, method_get_check_out_state,
|
method_check_in, method_get_check_out_info, method_get_check_out_state,
|
||||||
@@ -47,8 +43,6 @@ class CheckoutsApp(MayanAppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
super(CheckoutsApp, self).ready()
|
super(CheckoutsApp, self).ready()
|
||||||
|
|
||||||
CheckedOutDocument = self.get_model(model_name='CheckedOutDocument')
|
|
||||||
DocumentCheckout = self.get_model(model_name='DocumentCheckout')
|
|
||||||
Document = apps.get_model(
|
Document = apps.get_model(
|
||||||
app_label='documents', model_name='Document'
|
app_label='documents', model_name='Document'
|
||||||
)
|
)
|
||||||
@@ -82,22 +76,6 @@ class CheckoutsApp(MayanAppConfig):
|
|||||||
permission_document_check_out_detail_view
|
permission_document_check_out_detail_view
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
ModelPermission.register_inheritance(
|
|
||||||
model=DocumentCheckout, related='document'
|
|
||||||
)
|
|
||||||
|
|
||||||
SourceColumn(
|
|
||||||
attribute='get_user_display', include_label=True, order=99,
|
|
||||||
source=CheckedOutDocument
|
|
||||||
)
|
|
||||||
SourceColumn(
|
|
||||||
attribute='get_checkout_datetime', include_label=True, order=99,
|
|
||||||
source=CheckedOutDocument
|
|
||||||
)
|
|
||||||
SourceColumn(
|
|
||||||
attribute='get_checkout_expiration', include_label=True, order=99,
|
|
||||||
source=CheckedOutDocument
|
|
||||||
)
|
|
||||||
|
|
||||||
dashboard_main.add_widget(
|
dashboard_main.add_widget(
|
||||||
widget=DashboardWidgetTotalCheckouts, order=-1
|
widget=DashboardWidgetTotalCheckouts, order=-1
|
||||||
@@ -107,22 +85,6 @@ class CheckoutsApp(MayanAppConfig):
|
|||||||
links=(link_check_out_info,), sources=(Document,)
|
links=(link_check_out_info,), sources=(Document,)
|
||||||
)
|
)
|
||||||
menu_main.bind_links(links=(link_check_out_list,), position=98)
|
menu_main.bind_links(links=(link_check_out_list,), position=98)
|
||||||
menu_multi_item.bind_links(
|
|
||||||
links=(
|
|
||||||
link_check_in_document_multiple,
|
|
||||||
), sources=(CheckedOutDocument,)
|
|
||||||
)
|
|
||||||
menu_multi_item.bind_links(
|
|
||||||
links=(
|
|
||||||
link_check_in_document_multiple,
|
|
||||||
link_check_out_document_multiple,
|
|
||||||
), sources=(Document,)
|
|
||||||
)
|
|
||||||
menu_multi_item.unbind_links(
|
|
||||||
links=(
|
|
||||||
link_check_out_document_multiple,
|
|
||||||
), sources=(CheckedOutDocument,)
|
|
||||||
)
|
|
||||||
menu_secondary.bind_links(
|
menu_secondary.bind_links(
|
||||||
links=(link_check_out_document, link_check_in_document),
|
links=(link_check_out_document, link_check_in_document),
|
||||||
sources=(
|
sources=(
|
||||||
|
|||||||
@@ -38,26 +38,16 @@ link_check_out_document = Link(
|
|||||||
args='object.pk', condition=is_not_checked_out,
|
args='object.pk', condition=is_not_checked_out,
|
||||||
icon_class=icon_check_out_document,
|
icon_class=icon_check_out_document,
|
||||||
permissions=(permission_document_check_out,),
|
permissions=(permission_document_check_out,),
|
||||||
text=_('Check out document'), view='checkouts:check_out_document'
|
text=_('Check out document'), view='checkouts:check_out_document',
|
||||||
)
|
|
||||||
link_check_out_document_multiple = Link(
|
|
||||||
icon_class=icon_check_out_document,
|
|
||||||
permissions=(permission_document_check_out,), text=_('Check out'),
|
|
||||||
view='checkouts:check_out_document_multiple'
|
|
||||||
)
|
)
|
||||||
link_check_in_document = Link(
|
link_check_in_document = Link(
|
||||||
args='object.pk', icon_class=icon_check_in_document,
|
args='object.pk', icon_class=icon_check_in_document,
|
||||||
condition=is_checked_out, permissions=(
|
condition=is_checked_out, permissions=(
|
||||||
permission_document_check_in, permission_document_check_in_override
|
permission_document_check_in, permission_document_check_in_override
|
||||||
), text=_('Check in document'), view='checkouts:check_in_document'
|
), text=_('Check in document'), view='checkouts:check_in_document',
|
||||||
)
|
|
||||||
link_check_in_document_multiple = Link(
|
|
||||||
icon_class=icon_check_in_document,
|
|
||||||
permissions=(permission_document_check_in,), text=_('Check in'),
|
|
||||||
view='checkouts:check_in_document_multiple'
|
|
||||||
)
|
)
|
||||||
link_check_out_info = Link(
|
link_check_out_info = Link(
|
||||||
args='resolved_object.pk', icon_class=icon_check_out_info, permissions=(
|
args='resolved_object.pk', icon_class=icon_check_out_info, permissions=(
|
||||||
permission_document_check_out_detail_view,
|
permission_document_check_out_detail_view,
|
||||||
), text=_('Check in/out'), view='checkouts:check_out_info'
|
), text=_('Check in/out'), view='checkouts:check_out_info',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from django.apps import apps
|
|||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
from mayan.apps.acls.models import AccessControlList
|
|
||||||
from mayan.apps.documents.models import Document
|
from mayan.apps.documents.models import Document
|
||||||
|
|
||||||
from .events import (
|
from .events import (
|
||||||
@@ -15,53 +14,10 @@ from .events import (
|
|||||||
)
|
)
|
||||||
from .exceptions import DocumentNotCheckedOut
|
from .exceptions import DocumentNotCheckedOut
|
||||||
from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN
|
from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN
|
||||||
from .permissions import (
|
|
||||||
permission_document_check_in, permission_document_check_in_override
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DocumentCheckoutBusinessLogicManager(models.Manager):
|
|
||||||
def check_in_document(self, document, user=None):
|
|
||||||
queryset = document._meta.default_manager.filter(pk=document.pk)
|
|
||||||
return self.check_in_documents(queryset=queryset, user=user)
|
|
||||||
|
|
||||||
def check_in_documents(self, queryset, user=None):
|
|
||||||
if user:
|
|
||||||
user_document_checkouts = AccessControlList.objects.restrict_queryset(
|
|
||||||
permission=permission_document_check_in,
|
|
||||||
queryset=self.filter(user_id=user.pk, document__in=queryset),
|
|
||||||
user=user
|
|
||||||
)
|
|
||||||
|
|
||||||
others_document_checkouts = AccessControlList.objects.restrict_queryset(
|
|
||||||
permission=permission_document_check_in_override,
|
|
||||||
queryset=self.exclude(user_id=user.pk, document__in=queryset),
|
|
||||||
user=user
|
|
||||||
)
|
|
||||||
|
|
||||||
with transaction.atomic():
|
|
||||||
if user:
|
|
||||||
for checkout in user_document_checkouts:
|
|
||||||
event_document_check_in.commit(
|
|
||||||
actor=user, target=checkout.document
|
|
||||||
)
|
|
||||||
checkout.delete()
|
|
||||||
|
|
||||||
for checkout in others_document_checkouts:
|
|
||||||
event_document_forceful_check_in.commit(
|
|
||||||
actor=user, target=checkout.document
|
|
||||||
)
|
|
||||||
checkout.delete()
|
|
||||||
else:
|
|
||||||
for checkout in self.filter(document__in=queryset):
|
|
||||||
event_document_auto_check_in.commit(
|
|
||||||
target=checkout.document
|
|
||||||
)
|
|
||||||
checkout.delete()
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentCheckoutManager(models.Manager):
|
class DocumentCheckoutManager(models.Manager):
|
||||||
def are_document_new_versions_allowed(self, document, user=None):
|
def are_document_new_versions_allowed(self, document, user=None):
|
||||||
try:
|
try:
|
||||||
@@ -71,6 +27,25 @@ class DocumentCheckoutManager(models.Manager):
|
|||||||
else:
|
else:
|
||||||
return not check_out_info.block_new_version
|
return not check_out_info.block_new_version
|
||||||
|
|
||||||
|
def check_in_document(self, document, user=None):
|
||||||
|
try:
|
||||||
|
document_check_out = self.model.objects.get(document=document)
|
||||||
|
except self.model.DoesNotExist:
|
||||||
|
raise DocumentNotCheckedOut
|
||||||
|
else:
|
||||||
|
with transaction.atomic():
|
||||||
|
if user:
|
||||||
|
if self.get_check_out_info(document=document).user != user:
|
||||||
|
event_document_forceful_check_in.commit(
|
||||||
|
actor=user, target=document
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
event_document_check_in.commit(actor=user, target=document)
|
||||||
|
else:
|
||||||
|
event_document_auto_check_in.commit(target=document)
|
||||||
|
|
||||||
|
document_check_out.delete()
|
||||||
|
|
||||||
def check_in_expired_check_outs(self):
|
def check_in_expired_check_outs(self):
|
||||||
for document in self.expired_check_outs():
|
for document in self.expired_check_outs():
|
||||||
document.check_in()
|
document.check_in()
|
||||||
@@ -82,11 +57,7 @@ class DocumentCheckoutManager(models.Manager):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def checked_out_documents(self):
|
def checked_out_documents(self):
|
||||||
CheckedOutDocument = apps.get_model(
|
return Document.objects.filter(
|
||||||
app_label='checkouts', model_name='CheckedOutDocument'
|
|
||||||
)
|
|
||||||
|
|
||||||
return CheckedOutDocument.objects.filter(
|
|
||||||
pk__in=self.model.objects.values('document__id')
|
pk__in=self.model.objects.values('document__id')
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -103,11 +74,7 @@ class DocumentCheckoutManager(models.Manager):
|
|||||||
return STATE_CHECKED_IN
|
return STATE_CHECKED_IN
|
||||||
|
|
||||||
def expired_check_outs(self):
|
def expired_check_outs(self):
|
||||||
CheckedOutDocument = apps.get_model(
|
expired_list = Document.objects.filter(
|
||||||
app_label='checkouts', model_name='CheckedOutDocument'
|
|
||||||
)
|
|
||||||
|
|
||||||
expired_list = CheckedOutDocument.objects.filter(
|
|
||||||
pk__in=self.model.objects.filter(
|
pk__in=self.model.objects.filter(
|
||||||
expiration_datetime__lte=now()
|
expiration_datetime__lte=now()
|
||||||
).values_list('document__pk', flat=True)
|
).values_list('document__pk', flat=True)
|
||||||
@@ -116,6 +83,9 @@ class DocumentCheckoutManager(models.Manager):
|
|||||||
return expired_list
|
return expired_list
|
||||||
|
|
||||||
def get_by_natural_key(self, document_natural_key):
|
def get_by_natural_key(self, document_natural_key):
|
||||||
|
Document = apps.get_model(
|
||||||
|
app_label='documents', model_name='Document'
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
document = Document.objects.get_by_natural_key(document_natural_key)
|
document = Document.objects.get_by_natural_key(document_natural_key)
|
||||||
except Document.DoesNotExist:
|
except Document.DoesNotExist:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ def method_check_in(self, user=None):
|
|||||||
app_label='checkouts', model_name='DocumentCheckout'
|
app_label='checkouts', model_name='DocumentCheckout'
|
||||||
)
|
)
|
||||||
|
|
||||||
return DocumentCheckout.business_logic.check_in_document(
|
return DocumentCheckout.objects.check_in_document(
|
||||||
document=self, user=user
|
document=self, user=user
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.11.20 on 2019-07-25 04:52
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('documents', '0050_auto_20190725_0451'),
|
|
||||||
('checkouts', '0007_auto_20180310_1715'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='CheckedOutDocument',
|
|
||||||
fields=[
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'proxy': True,
|
|
||||||
'indexes': [],
|
|
||||||
},
|
|
||||||
bases=('documents.document',),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -14,10 +14,7 @@ from mayan.apps.documents.models import Document
|
|||||||
|
|
||||||
from .events import event_document_check_out
|
from .events import event_document_check_out
|
||||||
from .exceptions import DocumentAlreadyCheckedOut
|
from .exceptions import DocumentAlreadyCheckedOut
|
||||||
from .managers import (
|
from .managers import DocumentCheckoutManager, NewVersionBlockManager
|
||||||
DocumentCheckoutBusinessLogicManager, DocumentCheckoutManager,
|
|
||||||
NewVersionBlockManager
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -52,7 +49,6 @@ class DocumentCheckout(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
objects = DocumentCheckoutManager()
|
objects = DocumentCheckoutManager()
|
||||||
business_logic = DocumentCheckoutBusinessLogicManager()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('pk',)
|
ordering = ('pk',)
|
||||||
@@ -85,13 +81,13 @@ class DocumentCheckout(models.Model):
|
|||||||
natural_key.dependencies = ['documents.Document']
|
natural_key.dependencies = ['documents.Document']
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
is_new = not self.pk
|
new_checkout = not self.pk
|
||||||
if not is_new or self.document.is_checked_out():
|
if not new_checkout or self.document.is_checked_out():
|
||||||
raise DocumentAlreadyCheckedOut
|
raise DocumentAlreadyCheckedOut
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
result = super(DocumentCheckout, self).save(*args, **kwargs)
|
result = super(DocumentCheckout, self).save(*args, **kwargs)
|
||||||
if is_new:
|
if new_checkout:
|
||||||
event_document_check_out.commit(
|
event_document_check_out.commit(
|
||||||
actor=self.user, target=self.document
|
actor=self.user, target=self.document
|
||||||
)
|
)
|
||||||
@@ -123,24 +119,3 @@ class NewVersionBlock(models.Model):
|
|||||||
def natural_key(self):
|
def natural_key(self):
|
||||||
return self.document.natural_key()
|
return self.document.natural_key()
|
||||||
natural_key.dependencies = ['documents.Document']
|
natural_key.dependencies = ['documents.Document']
|
||||||
|
|
||||||
|
|
||||||
class CheckedOutDocument(Document):
|
|
||||||
class Meta:
|
|
||||||
proxy = True
|
|
||||||
|
|
||||||
def get_user_display(self):
|
|
||||||
check_out_info = self.get_check_out_info()
|
|
||||||
return check_out_info.user.get_full_name() or check_out_info.user
|
|
||||||
|
|
||||||
get_user_display.short_description = _('User')
|
|
||||||
|
|
||||||
def get_checkout_datetime(self):
|
|
||||||
return self.get_check_out_info().checkout_datetime
|
|
||||||
|
|
||||||
get_checkout_datetime.short_description = _('Checkout time and date')
|
|
||||||
|
|
||||||
def get_checkout_expiration(self):
|
|
||||||
return self.get_check_out_info().expiration_datetime
|
|
||||||
|
|
||||||
get_checkout_expiration.short_description = _('Checkout expiration')
|
|
||||||
|
|||||||
@@ -4,19 +4,13 @@ import datetime
|
|||||||
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS
|
|
||||||
from mayan.apps.common.tests.utils import as_id_list
|
|
||||||
|
|
||||||
from ..models import DocumentCheckout
|
from ..models import DocumentCheckout
|
||||||
|
|
||||||
|
|
||||||
class DocumentCheckoutTestMixin(object):
|
class DocumentCheckoutTestMixin(object):
|
||||||
_test_document_check_out_seconds = 0.1
|
_test_document_check_out_seconds = 0.1
|
||||||
|
|
||||||
def _check_out_test_document(self, document=None, user=None):
|
def _check_out_test_document(self, user=None):
|
||||||
if not document:
|
|
||||||
document = self.test_document
|
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
user = self._test_case_user
|
user = self._test_case_user
|
||||||
|
|
||||||
@@ -25,61 +19,7 @@ class DocumentCheckoutTestMixin(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.test_check_out = DocumentCheckout.objects.check_out_document(
|
self.test_check_out = DocumentCheckout.objects.check_out_document(
|
||||||
block_new_version=True, document=document,
|
block_new_version=True, document=self.test_document,
|
||||||
expiration_datetime=self._check_out_expiration_datetime,
|
expiration_datetime=self._check_out_expiration_datetime,
|
||||||
user=user
|
user=user
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DocumentCheckoutViewTestMixin(object):
|
|
||||||
def _request_test_document_check_in_get_view(self):
|
|
||||||
return self.get(
|
|
||||||
viewname='checkouts:check_in_document', kwargs={
|
|
||||||
'pk': self.test_document.pk
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def _request_test_document_check_in_post_view(self):
|
|
||||||
return self.post(
|
|
||||||
viewname='checkouts:check_in_document', kwargs={
|
|
||||||
'pk': self.test_document.pk
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def _request_test_document_multiple_check_in_post_view(self):
|
|
||||||
return self.post(
|
|
||||||
viewname='checkouts:check_in_document_multiple', data={
|
|
||||||
'id_list': as_id_list(items=self.test_documents)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def _request_test_document_check_out_view(self):
|
|
||||||
return self.post(
|
|
||||||
viewname='checkouts:check_out_document', kwargs={
|
|
||||||
'pk': self.test_document.pk
|
|
||||||
}, data={
|
|
||||||
'block_new_version': True,
|
|
||||||
'expiration_datetime_0': TIME_DELTA_UNIT_DAYS,
|
|
||||||
'expiration_datetime_1': 2
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def _request_test_document_multiple_check_out_post_view(self):
|
|
||||||
return self.post(
|
|
||||||
viewname='checkouts:check_out_document_multiple', data={
|
|
||||||
'block_new_version': True,
|
|
||||||
'expiration_datetime_0': TIME_DELTA_UNIT_DAYS,
|
|
||||||
'expiration_datetime_1': 2,
|
|
||||||
'id_list': as_id_list(items=self.test_documents)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def _request_test_document_check_out_detail_view(self):
|
|
||||||
return self.get(
|
|
||||||
viewname='checkouts:check_out_info', kwargs={
|
|
||||||
'pk': self.test_document.pk
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def _request_test_document_check_out_list_view(self):
|
|
||||||
return self.get(viewname='checkouts:check_out_list')
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class CheckoutsAPITestCase(DocumentCheckoutTestMixin, DocumentTestMixin, BaseAPI
|
|||||||
force_text(self.test_document.uuid)
|
force_text(self.test_document.uuid)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _request_test_document_check_out_view(self):
|
def _request_document_checkout_view(self):
|
||||||
return self.post(
|
return self.post(
|
||||||
viewname='rest_api:checkout-document-list', data={
|
viewname='rest_api:checkout-document-list', data={
|
||||||
'document_pk': self.test_document.pk,
|
'document_pk': self.test_document.pk,
|
||||||
@@ -74,7 +74,7 @@ class CheckoutsAPITestCase(DocumentCheckoutTestMixin, DocumentTestMixin, BaseAPI
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_document_checkout_no_access(self):
|
def test_document_checkout_no_access(self):
|
||||||
response = self._request_test_document_check_out_view()
|
response = self._request_document_checkout_view()
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
self.assertEqual(DocumentCheckout.objects.count(), 0)
|
self.assertEqual(DocumentCheckout.objects.count(), 0)
|
||||||
@@ -82,7 +82,7 @@ class CheckoutsAPITestCase(DocumentCheckoutTestMixin, DocumentTestMixin, BaseAPI
|
|||||||
def test_document_checkout_with_access(self):
|
def test_document_checkout_with_access(self):
|
||||||
self.grant_access(permission=permission_document_check_out, obj=self.test_document)
|
self.grant_access(permission=permission_document_check_out, obj=self.test_document)
|
||||||
|
|
||||||
response = self._request_test_document_check_out_view()
|
response = self._request_document_checkout_view()
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ from mayan.apps.documents.tests import GenericDocumentTestCase, DocumentTestMixi
|
|||||||
from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH
|
from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH
|
||||||
|
|
||||||
from ..exceptions import (
|
from ..exceptions import (
|
||||||
DocumentAlreadyCheckedOut, NewDocumentVersionNotAllowed
|
DocumentAlreadyCheckedOut, DocumentNotCheckedOut,
|
||||||
|
NewDocumentVersionNotAllowed
|
||||||
)
|
)
|
||||||
from ..models import DocumentCheckout, NewVersionBlock
|
from ..models import DocumentCheckout, NewVersionBlock
|
||||||
|
|
||||||
@@ -48,6 +49,10 @@ class DocumentCheckoutTestCase(DocumentCheckoutTestMixin, GenericDocumentTestCas
|
|||||||
block_new_version=True
|
block_new_version=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_checkin_without_checkout(self):
|
||||||
|
with self.assertRaises(DocumentNotCheckedOut):
|
||||||
|
self.test_document.check_in()
|
||||||
|
|
||||||
def test_auto_check_in(self):
|
def test_auto_check_in(self):
|
||||||
self._check_out_test_document()
|
self._check_out_test_document()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS
|
||||||
from mayan.apps.documents.permissions import permission_document_view
|
from mayan.apps.documents.permissions import permission_document_view
|
||||||
from mayan.apps.documents.tests import GenericDocumentViewTestCase
|
from mayan.apps.documents.tests import GenericDocumentViewTestCase
|
||||||
from mayan.apps.sources.links import link_document_version_upload
|
from mayan.apps.sources.links import link_document_version_upload
|
||||||
@@ -11,53 +12,64 @@ from ..permissions import (
|
|||||||
permission_document_check_out, permission_document_check_out_detail_view
|
permission_document_check_out, permission_document_check_out_detail_view
|
||||||
)
|
)
|
||||||
|
|
||||||
from .mixins import DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin
|
from .mixins import DocumentCheckoutTestMixin
|
||||||
|
|
||||||
|
|
||||||
class DocumentCheckoutViewTestCase(
|
class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentViewTestCase):
|
||||||
DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin,
|
def _request_document_check_in_get_view(self):
|
||||||
GenericDocumentViewTestCase
|
return self.get(
|
||||||
):
|
viewname='checkouts:check_in_document', kwargs={
|
||||||
def test_document_check_in_get_view_no_permission(self):
|
'pk': self.test_document.pk
|
||||||
self._check_out_test_document()
|
}
|
||||||
|
|
||||||
response = self._request_test_document_check_in_get_view()
|
|
||||||
self.assertNotContains(
|
|
||||||
response=response, text=self.test_document.label, status_code=404
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(self.test_document.is_checked_out())
|
def test_check_in_document_get_view_no_permission(self):
|
||||||
|
|
||||||
def test_document_check_in_get_view_with_access(self):
|
|
||||||
self._check_out_test_document()
|
self._check_out_test_document()
|
||||||
|
|
||||||
self.grant_access(
|
response = self._request_document_check_in_get_view()
|
||||||
obj=self.test_document, permission=permission_document_check_in
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self._request_test_document_check_in_get_view()
|
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response=response, text=self.test_document.label, status_code=200
|
response=response, text=self.test_document.label, status_code=200
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(self.test_document.is_checked_out())
|
self.assertTrue(self.test_document.is_checked_out())
|
||||||
|
|
||||||
def test_document_check_in_post_view_no_permission(self):
|
def test_check_in_document_get_view_with_access(self):
|
||||||
self._check_out_test_document()
|
|
||||||
|
|
||||||
response = self._request_test_document_check_in_post_view()
|
|
||||||
self.assertEqual(response.status_code, 404)
|
|
||||||
|
|
||||||
self.assertTrue(self.test_document.is_checked_out())
|
|
||||||
|
|
||||||
def test_document_check_in_post_view_with_access(self):
|
|
||||||
self._check_out_test_document()
|
self._check_out_test_document()
|
||||||
|
|
||||||
self.grant_access(
|
self.grant_access(
|
||||||
obj=self.test_document, permission=permission_document_check_in
|
obj=self.test_document, permission=permission_document_check_in
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self._request_test_document_check_in_post_view()
|
response = self._request_document_check_in_get_view()
|
||||||
|
self.assertContains(
|
||||||
|
response=response, text=self.test_document.label, status_code=200
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(self.test_document.is_checked_out())
|
||||||
|
|
||||||
|
def _request_document_check_in_post_view(self):
|
||||||
|
return self.post(
|
||||||
|
viewname='checkouts:check_in_document', kwargs={
|
||||||
|
'pk': self.test_document.pk
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_check_in_document_post_view_no_permission(self):
|
||||||
|
self._check_out_test_document()
|
||||||
|
|
||||||
|
response = self._request_document_check_in_post_view()
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
self.assertTrue(self.test_document.is_checked_out())
|
||||||
|
|
||||||
|
def test_check_in_document_post_view_with_access(self):
|
||||||
|
self._check_out_test_document()
|
||||||
|
|
||||||
|
self.grant_access(
|
||||||
|
obj=self.test_document, permission=permission_document_check_in
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self._request_document_check_in_post_view()
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
self.assertFalse(self.test_document.is_checked_out())
|
self.assertFalse(self.test_document.is_checked_out())
|
||||||
@@ -67,93 +79,24 @@ class DocumentCheckoutViewTestCase(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_document_multiple_check_in_post_view_no_permission(self):
|
def _request_document_checkout_view(self):
|
||||||
# Upload second document
|
return self.post(
|
||||||
self.upload_document()
|
viewname='checkouts:check_out_document', kwargs={
|
||||||
|
'pk': self.test_document.pk
|
||||||
self._check_out_test_document(document=self.test_documents[0])
|
}, data={
|
||||||
self._check_out_test_document(document=self.test_documents[1])
|
'expiration_datetime_0': 2,
|
||||||
|
'expiration_datetime_1': TIME_DELTA_UNIT_DAYS,
|
||||||
response = self._request_test_document_multiple_check_in_post_view()
|
'block_new_version': True
|
||||||
self.assertEqual(response.status_code, 404)
|
}
|
||||||
|
|
||||||
self.assertTrue(self.test_documents[0].is_checked_out())
|
|
||||||
self.assertTrue(self.test_documents[1].is_checked_out())
|
|
||||||
self.assertTrue(
|
|
||||||
DocumentCheckout.objects.is_checked_out(
|
|
||||||
document=self.test_documents[0]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
DocumentCheckout.objects.is_checked_out(
|
|
||||||
document=self.test_documents[1]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_document_multiple_check_in_post_view_with_document_0_access(self):
|
def test_check_out_document_view_no_permission(self):
|
||||||
# Upload second document
|
response = self._request_document_checkout_view()
|
||||||
self.upload_document()
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
self._check_out_test_document(document=self.test_documents[0])
|
|
||||||
self._check_out_test_document(document=self.test_documents[1])
|
|
||||||
|
|
||||||
self.grant_access(
|
|
||||||
obj=self.test_documents[0], permission=permission_document_check_in
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self._request_test_document_multiple_check_in_post_view()
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
|
|
||||||
self.assertFalse(self.test_documents[0].is_checked_out())
|
|
||||||
self.assertTrue(self.test_documents[1].is_checked_out())
|
|
||||||
self.assertFalse(
|
|
||||||
DocumentCheckout.objects.is_checked_out(
|
|
||||||
document=self.test_documents[0]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
DocumentCheckout.objects.is_checked_out(
|
|
||||||
document=self.test_documents[1]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_document_multiple_check_in_post_view_with_access(self):
|
|
||||||
# Upload second document
|
|
||||||
self.upload_document()
|
|
||||||
|
|
||||||
self._check_out_test_document(document=self.test_documents[0])
|
|
||||||
self._check_out_test_document(document=self.test_documents[1])
|
|
||||||
|
|
||||||
self.grant_access(
|
|
||||||
obj=self.test_documents[0], permission=permission_document_check_in
|
|
||||||
)
|
|
||||||
self.grant_access(
|
|
||||||
obj=self.test_documents[1], permission=permission_document_check_in
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self._request_test_document_multiple_check_in_post_view()
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
|
|
||||||
self.assertFalse(self.test_documents[0].is_checked_out())
|
|
||||||
self.assertFalse(self.test_documents[1].is_checked_out())
|
|
||||||
self.assertFalse(
|
|
||||||
DocumentCheckout.objects.is_checked_out(
|
|
||||||
document=self.test_documents[0]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertFalse(
|
|
||||||
DocumentCheckout.objects.is_checked_out(
|
|
||||||
document=self.test_documents[1]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_document_check_out_view_no_permission(self):
|
|
||||||
response = self._request_test_document_check_out_view()
|
|
||||||
self.assertEqual(response.status_code, 404)
|
|
||||||
|
|
||||||
self.assertFalse(self.test_document.is_checked_out())
|
self.assertFalse(self.test_document.is_checked_out())
|
||||||
|
|
||||||
def test_document_check_out_view_with_access(self):
|
def test_check_out_document_view_with_access(self):
|
||||||
self.grant_access(
|
self.grant_access(
|
||||||
obj=self.test_document, permission=permission_document_check_out
|
obj=self.test_document, permission=permission_document_check_out
|
||||||
)
|
)
|
||||||
@@ -162,117 +105,28 @@ class DocumentCheckoutViewTestCase(
|
|||||||
permission=permission_document_check_out_detail_view
|
permission=permission_document_check_out_detail_view
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self._request_test_document_check_out_view()
|
response = self._request_document_checkout_view()
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
self.assertTrue(self.test_document.is_checked_out())
|
self.assertTrue(self.test_document.is_checked_out())
|
||||||
|
|
||||||
def test_document_multiple_check_out_post_view_no_permission(self):
|
def _request_check_out_detail_view(self):
|
||||||
# Upload second document
|
return self.get(
|
||||||
self.upload_document()
|
viewname='checkouts:check_out_info', kwargs={
|
||||||
|
'pk': self.test_document.pk
|
||||||
self.grant_access(
|
}
|
||||||
obj=self.test_documents[0],
|
|
||||||
permission=permission_document_check_out_detail_view
|
|
||||||
)
|
|
||||||
self.grant_access(
|
|
||||||
obj=self.test_documents[1],
|
|
||||||
permission=permission_document_check_out_detail_view
|
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self._request_test_document_multiple_check_out_post_view()
|
def test_checkout_detail_view_no_permission(self):
|
||||||
self.assertEqual(response.status_code, 404)
|
|
||||||
|
|
||||||
self.assertFalse(self.test_documents[0].is_checked_out())
|
|
||||||
self.assertFalse(self.test_documents[1].is_checked_out())
|
|
||||||
self.assertFalse(
|
|
||||||
DocumentCheckout.objects.is_checked_out(
|
|
||||||
document=self.test_documents[0]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertFalse(
|
|
||||||
DocumentCheckout.objects.is_checked_out(
|
|
||||||
document=self.test_documents[1]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_document_multiple_check_out_post_view_with_document_0_access(self):
|
|
||||||
# Upload second document
|
|
||||||
self.upload_document()
|
|
||||||
|
|
||||||
self.grant_access(
|
|
||||||
obj=self.test_documents[0], permission=permission_document_check_out
|
|
||||||
)
|
|
||||||
self.grant_access(
|
|
||||||
obj=self.test_documents[0],
|
|
||||||
permission=permission_document_check_out_detail_view
|
|
||||||
)
|
|
||||||
self.grant_access(
|
|
||||||
obj=self.test_documents[1],
|
|
||||||
permission=permission_document_check_out_detail_view
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self._request_test_document_multiple_check_out_post_view()
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
|
|
||||||
self.assertTrue(self.test_documents[0].is_checked_out())
|
|
||||||
self.assertFalse(self.test_documents[1].is_checked_out())
|
|
||||||
self.assertTrue(
|
|
||||||
DocumentCheckout.objects.is_checked_out(
|
|
||||||
document=self.test_documents[0]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertFalse(
|
|
||||||
DocumentCheckout.objects.is_checked_out(
|
|
||||||
document=self.test_documents[1]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_document_multiple_check_out_post_view_with_access(self):
|
|
||||||
# Upload second document
|
|
||||||
self.upload_document()
|
|
||||||
|
|
||||||
self.grant_access(
|
|
||||||
obj=self.test_documents[0], permission=permission_document_check_out
|
|
||||||
)
|
|
||||||
self.grant_access(
|
|
||||||
obj=self.test_documents[1], permission=permission_document_check_out
|
|
||||||
)
|
|
||||||
self.grant_access(
|
|
||||||
obj=self.test_documents[0],
|
|
||||||
permission=permission_document_check_out_detail_view
|
|
||||||
)
|
|
||||||
self.grant_access(
|
|
||||||
obj=self.test_documents[1],
|
|
||||||
permission=permission_document_check_out_detail_view
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self._request_test_document_multiple_check_out_post_view()
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
|
|
||||||
self.assertTrue(self.test_documents[0].is_checked_out())
|
|
||||||
self.assertTrue(self.test_documents[1].is_checked_out())
|
|
||||||
self.assertTrue(
|
|
||||||
DocumentCheckout.objects.is_checked_out(
|
|
||||||
document=self.test_documents[0]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
DocumentCheckout.objects.is_checked_out(
|
|
||||||
document=self.test_documents[1]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_document_check_out_detail_view_no_permission(self):
|
|
||||||
self._check_out_test_document()
|
self._check_out_test_document()
|
||||||
|
|
||||||
response = self._request_test_document_check_out_detail_view()
|
response = self._request_check_out_detail_view()
|
||||||
|
|
||||||
self.assertNotContains(
|
self.assertNotContains(
|
||||||
response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=404
|
response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=404
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_document_check_out_detail_view_with_access(self):
|
def test_checkout_detail_view_with_access(self):
|
||||||
self._check_out_test_document()
|
self._check_out_test_document()
|
||||||
|
|
||||||
self.grant_access(
|
self.grant_access(
|
||||||
@@ -280,12 +134,15 @@ class DocumentCheckoutViewTestCase(
|
|||||||
permission=permission_document_check_out_detail_view
|
permission=permission_document_check_out_detail_view
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self._request_test_document_check_out_detail_view()
|
response = self._request_check_out_detail_view()
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=200
|
response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=200
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_document_checkout_list_view_no_permission(self):
|
def _request_check_out_list_view(self):
|
||||||
|
return self.get(viewname='checkouts:check_out_list')
|
||||||
|
|
||||||
|
def test_checkout_list_view_no_permission(self):
|
||||||
self._check_out_test_document()
|
self._check_out_test_document()
|
||||||
|
|
||||||
self.grant_access(
|
self.grant_access(
|
||||||
@@ -293,12 +150,12 @@ class DocumentCheckoutViewTestCase(
|
|||||||
permission=permission_document_view
|
permission=permission_document_view
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self._request_test_document_check_out_list_view()
|
response = self._request_check_out_list_view()
|
||||||
self.assertNotContains(
|
self.assertNotContains(
|
||||||
response=response, text=self.test_document.label, status_code=200
|
response=response, text=self.test_document.label, status_code=200
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_document_checkout_list_view_with_access(self):
|
def test_checkout_list_view_with_access(self):
|
||||||
self._check_out_test_document()
|
self._check_out_test_document()
|
||||||
|
|
||||||
self.grant_access(
|
self.grant_access(
|
||||||
@@ -310,54 +167,12 @@ class DocumentCheckoutViewTestCase(
|
|||||||
permission=permission_document_view
|
permission=permission_document_view
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self._request_test_document_check_out_list_view()
|
response = self._request_check_out_list_view()
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response=response, text=self.test_document.label, status_code=200
|
response=response, text=self.test_document.label, status_code=200
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_document_check_in_forcefull_view_no_permission(self):
|
def test_document_new_version_after_check_out(self):
|
||||||
# Gitlab issue #237
|
|
||||||
# Forcefully checking in a document by a user without adequate
|
|
||||||
# permissions throws out an error
|
|
||||||
|
|
||||||
self._create_test_user()
|
|
||||||
self._check_out_test_document(user=self.test_user)
|
|
||||||
|
|
||||||
self.grant_access(
|
|
||||||
obj=self.test_document, permission=permission_document_check_in
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.post(
|
|
||||||
viewname='checkouts:check_in_document', kwargs={
|
|
||||||
'pk': self.test_document.pk
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
self.assertTrue(self.test_document.is_checked_out())
|
|
||||||
|
|
||||||
def test_document_check_in_forcefull_view_with_access(self):
|
|
||||||
self._create_test_user()
|
|
||||||
self._check_out_test_document(user=self.test_user)
|
|
||||||
|
|
||||||
self.grant_access(
|
|
||||||
obj=self.test_document,
|
|
||||||
permission=permission_document_check_in_override
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.post(
|
|
||||||
viewname='checkouts:check_in_document', kwargs={
|
|
||||||
'pk': self.test_document.pk
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
self.assertFalse(self.test_document.is_checked_out())
|
|
||||||
|
|
||||||
|
|
||||||
class NewVersionBlockViewTestCase(
|
|
||||||
DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin,
|
|
||||||
GenericDocumentViewTestCase):
|
|
||||||
|
|
||||||
def test_document_check_out_new_version(self):
|
|
||||||
"""
|
"""
|
||||||
Gitlab issue #231
|
Gitlab issue #231
|
||||||
User shown option to upload new version of a document even though it
|
User shown option to upload new version of a document even though it
|
||||||
@@ -390,8 +205,49 @@ class NewVersionBlockViewTestCase(
|
|||||||
|
|
||||||
# Needed by the url view resolver
|
# Needed by the url view resolver
|
||||||
response.context.current_app = None
|
response.context.current_app = None
|
||||||
resolved_link = link_document_version_upload.resolve(
|
resolved_link = link_document_version_upload.resolve(context=response.context)
|
||||||
context=response.context
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(resolved_link, None)
|
self.assertEqual(resolved_link, None)
|
||||||
|
|
||||||
|
def test_forcefull_check_in_document_view_no_permission(self):
|
||||||
|
# Gitlab issue #237
|
||||||
|
# Forcefully checking in a document by a user without adequate
|
||||||
|
# permissions throws out an error
|
||||||
|
|
||||||
|
self._create_test_case_superuser()
|
||||||
|
self._check_out_test_document(user=self._test_case_superuser)
|
||||||
|
|
||||||
|
self.grant_access(
|
||||||
|
obj=self.test_document, permission=permission_document_check_in
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
viewname='checkouts:check_in_document', kwargs={
|
||||||
|
'pk': self.test_document.pk
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertContains(
|
||||||
|
response=response, text='Insufficient permissions', status_code=403
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(self.test_document.is_checked_out())
|
||||||
|
|
||||||
|
def test_forcefull_check_in_document_view_with_permission(self):
|
||||||
|
self._create_test_case_superuser()
|
||||||
|
self._check_out_test_document(user=self._test_case_superuser)
|
||||||
|
|
||||||
|
self.grant_access(
|
||||||
|
obj=self.test_document, permission=permission_document_check_in
|
||||||
|
)
|
||||||
|
self.grant_access(
|
||||||
|
obj=self.test_document, permission=permission_document_check_in_override
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
viewname='checkouts:check_in_document', kwargs={
|
||||||
|
'pk': self.test_document.pk
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
self.assertFalse(self.test_document.is_checked_out())
|
||||||
|
|||||||
@@ -4,34 +4,25 @@ from django.conf.urls import url
|
|||||||
|
|
||||||
from .api_views import APICheckedoutDocumentListView, APICheckedoutDocumentView
|
from .api_views import APICheckedoutDocumentListView, APICheckedoutDocumentView
|
||||||
from .views import (
|
from .views import (
|
||||||
DocumentCheckinView, DocumentCheckoutDetailView, DocumentCheckoutView,
|
CheckoutDocumentView, CheckoutDetailView, CheckoutListView,
|
||||||
DocumentCheckoutListView
|
DocumentCheckinView
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(
|
url(
|
||||||
regex=r'^documents/$', view=DocumentCheckoutListView.as_view(),
|
regex=r'^list/$', view=CheckoutListView.as_view(), name='check_out_list'
|
||||||
name='check_out_list'
|
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^documents/(?P<pk>\d+)/check_in/$', view=DocumentCheckinView.as_view(),
|
regex=r'^(?P<pk>\d+)/check/out/$', view=CheckoutDocumentView.as_view(),
|
||||||
name='check_in_document'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
regex=r'^documents/multiple/check_in/$',
|
|
||||||
name='check_in_document_multiple', view=DocumentCheckinView.as_view()
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
regex=r'^documents/(?P<pk>\d+)/check_out/$', view=DocumentCheckoutView.as_view(),
|
|
||||||
name='check_out_document'
|
name='check_out_document'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^documents/multiple/check_out/$',
|
regex=r'^(?P<pk>\d+)/check/in/$', view=DocumentCheckinView.as_view(),
|
||||||
name='check_out_document_multiple', view=DocumentCheckoutView.as_view()
|
name='check_in_document'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^documents/(?P<pk>\d+)/checkout/info/$',
|
regex=r'^(?P<pk>\d+)/check/info/$', view=CheckoutDetailView.as_view(),
|
||||||
view=DocumentCheckoutDetailView.as_view(), name='check_out_info'
|
name='check_out_info'
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import ugettext_lazy as _, ungettext
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from mayan.apps.acls.models import AccessControlList
|
from mayan.apps.acls.models import AccessControlList
|
||||||
from mayan.apps.common.generics import (
|
from mayan.apps.common.generics import (
|
||||||
MultipleObjectConfirmActionView, MultipleObjectFormActionView,
|
ConfirmView, SingleObjectCreateView, SingleObjectDetailView
|
||||||
SingleObjectDetailView
|
|
||||||
)
|
)
|
||||||
|
from mayan.apps.common.utils import encapsulate
|
||||||
from mayan.apps.documents.models import Document
|
from mayan.apps.documents.models import Document
|
||||||
from mayan.apps.documents.views import DocumentListView
|
from mayan.apps.documents.views import DocumentListView
|
||||||
|
|
||||||
|
from .exceptions import DocumentAlreadyCheckedOut, DocumentNotCheckedOut
|
||||||
from .forms import DocumentCheckoutForm, DocumentCheckoutDefailForm
|
from .forms import DocumentCheckoutForm, DocumentCheckoutDefailForm
|
||||||
from .icons import icon_check_out_info
|
from .icons import icon_check_out_info
|
||||||
from .models import DocumentCheckout
|
from .models import DocumentCheckout
|
||||||
@@ -20,124 +24,159 @@ from .permissions import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DocumentCheckinView(MultipleObjectConfirmActionView):
|
class DocumentCheckinView(ConfirmView):
|
||||||
error_message = 'Unable to check in document "%(instance)s". %(exception)s'
|
|
||||||
model = Document
|
|
||||||
pk_url_kwarg = 'pk'
|
|
||||||
success_message_singular = '%(count)d document checked in.'
|
|
||||||
success_message_plural = '%(count)d documents checked in.'
|
|
||||||
|
|
||||||
def get_extra_context(self):
|
def get_extra_context(self):
|
||||||
queryset = self.get_object_list()
|
document = self.get_object()
|
||||||
|
|
||||||
result = {
|
context = {
|
||||||
'title': ungettext(
|
'object': document,
|
||||||
singular='Check in %(count)d document',
|
|
||||||
plural='Check in %(count)d documents',
|
|
||||||
number=queryset.count()
|
|
||||||
) % {
|
|
||||||
'count': queryset.count(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if queryset.count() == 1:
|
if document.get_check_out_info().user != self.request.user:
|
||||||
result.update(
|
context['title'] = _(
|
||||||
{
|
'You didn\'t originally checked out this document. '
|
||||||
'object': queryset.first(),
|
'Forcefully check in the document: %s?'
|
||||||
'title': _(
|
) % document
|
||||||
'Check in document: %s'
|
else:
|
||||||
) % queryset.first()
|
context['title'] = _('Check in the document: %s?') % document
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
return context
|
||||||
|
|
||||||
def get_post_object_action_url(self):
|
def get_object(self):
|
||||||
if self.action_count == 1:
|
return get_object_or_404(klass=Document, pk=self.kwargs['pk'])
|
||||||
return reverse(
|
|
||||||
viewname='checkouts:document_checkout_info',
|
def get_post_action_redirect(self):
|
||||||
kwargs={'pk': self.action_id_list[0]}
|
return reverse(
|
||||||
|
viewname='checkouts:check_out_info', kwargs={
|
||||||
|
'pk': self.get_object().pk
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def view_action(self):
|
||||||
|
document = self.get_object()
|
||||||
|
|
||||||
|
if document.get_check_out_info().user == self.request.user:
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
obj=document, permissions=(permission_document_check_in,),
|
||||||
|
user=self.request.user
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
super(DocumentCheckinView, self).get_post_action_redirect()
|
AccessControlList.objects.check_access(
|
||||||
|
obj=document,
|
||||||
|
permissions=(permission_document_check_in_override,),
|
||||||
|
user=self.request.user
|
||||||
|
)
|
||||||
|
|
||||||
def get_source_queryset(self):
|
try:
|
||||||
# object_permission is None to disable restricting queryset mixin
|
document.check_in(user=self.request.user)
|
||||||
# and restrict the queryset ourselves from two permissions
|
except DocumentNotCheckedOut:
|
||||||
|
messages.error(
|
||||||
|
message=_('Document has not been checked out.'),
|
||||||
|
request=self.request
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messages.success(
|
||||||
|
message=_(
|
||||||
|
'Document "%s" checked in successfully.'
|
||||||
|
) % document, request=self.request
|
||||||
|
)
|
||||||
|
|
||||||
source_queryset = super(DocumentCheckinView, self).get_source_queryset()
|
|
||||||
|
|
||||||
check_in_queryset = AccessControlList.objects.restrict_queryset(
|
class CheckoutDocumentView(SingleObjectCreateView):
|
||||||
permission=permission_document_check_in, queryset=source_queryset,
|
form_class = DocumentCheckoutForm
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.document = get_object_or_404(klass=Document, pk=self.kwargs['pk'])
|
||||||
|
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
obj=self.document, permissions=(permission_document_check_out,),
|
||||||
|
user=request.user
|
||||||
|
)
|
||||||
|
|
||||||
|
return super(
|
||||||
|
CheckoutDocumentView, self
|
||||||
|
).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
try:
|
||||||
|
instance = form.save(commit=False)
|
||||||
|
instance.user = self.request.user
|
||||||
|
instance.document = self.document
|
||||||
|
instance.save()
|
||||||
|
except DocumentAlreadyCheckedOut:
|
||||||
|
messages.error(
|
||||||
|
message=_('Document already checked out.'),
|
||||||
|
request=self.request
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messages.success(
|
||||||
|
message=_(
|
||||||
|
'Document "%s" checked out successfully.'
|
||||||
|
) % self.document, request=self.request
|
||||||
|
)
|
||||||
|
|
||||||
|
return HttpResponseRedirect(redirect_to=self.get_success_url())
|
||||||
|
|
||||||
|
def get_extra_context(self):
|
||||||
|
return {
|
||||||
|
'object': self.document,
|
||||||
|
'title': _('Check out document: %s') % self.document
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_post_action_redirect(self):
|
||||||
|
return reverse(
|
||||||
|
viewname='checkouts:check_out_info', kwargs={
|
||||||
|
'pk': self.document.pk
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CheckoutListView(DocumentListView):
|
||||||
|
def get_document_queryset(self):
|
||||||
|
return AccessControlList.objects.restrict_queryset(
|
||||||
|
permission=permission_document_check_out_detail_view,
|
||||||
|
queryset=DocumentCheckout.objects.checked_out_documents(),
|
||||||
user=self.request.user
|
user=self.request.user
|
||||||
)
|
)
|
||||||
|
|
||||||
check_in_override_queryset = AccessControlList.objects.restrict_queryset(
|
|
||||||
permission=permission_document_check_in_override,
|
|
||||||
queryset=source_queryset, user=self.request.user
|
|
||||||
)
|
|
||||||
|
|
||||||
return check_in_queryset | check_in_override_queryset
|
|
||||||
|
|
||||||
def object_action(self, form, instance):
|
|
||||||
DocumentCheckout.business_logic.check_in_document(
|
|
||||||
document=instance, user=self.request.user
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentCheckoutView(MultipleObjectFormActionView):
|
|
||||||
error_message = 'Unable to checkout document "%(instance)s". %(exception)s'
|
|
||||||
form_class = DocumentCheckoutForm
|
|
||||||
model = Document
|
|
||||||
object_permission = permission_document_check_out
|
|
||||||
pk_url_kwarg = 'pk'
|
|
||||||
success_message_singular = '%(count)d document checked out.'
|
|
||||||
success_message_plural = '%(count)d documents checked out.'
|
|
||||||
|
|
||||||
def get_extra_context(self):
|
def get_extra_context(self):
|
||||||
queryset = self.get_object_list()
|
context = super(CheckoutListView, self).get_extra_context()
|
||||||
|
context.update(
|
||||||
result = {
|
{
|
||||||
'title': ungettext(
|
'extra_columns': (
|
||||||
singular='Checkout %(count)d document',
|
{
|
||||||
plural='Checkout %(count)d documents',
|
'name': _('User'),
|
||||||
number=queryset.count()
|
'attribute': encapsulate(
|
||||||
) % {
|
lambda document: document.get_check_out_info().user.get_full_name() or document.get_check_out_info().user
|
||||||
'count': queryset.count(),
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': _('Checkout time and date'),
|
||||||
|
'attribute': encapsulate(
|
||||||
|
lambda document: document.get_check_out_info().checkout_datetime
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': _('Checkout expiration'),
|
||||||
|
'attribute': encapsulate(
|
||||||
|
lambda document: document.get_check_out_info().expiration_datetime
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
'no_results_icon': icon_check_out_info,
|
||||||
|
'no_results_text': _(
|
||||||
|
'Checking out a document blocks certain document '
|
||||||
|
'operations for a predetermined amount of '
|
||||||
|
'time.'
|
||||||
|
),
|
||||||
|
'no_results_title': _('No documents have been checked out'),
|
||||||
|
'title': _('Documents checked out'),
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if queryset.count() == 1:
|
|
||||||
result.update(
|
|
||||||
{
|
|
||||||
'object': queryset.first(),
|
|
||||||
'title': _(
|
|
||||||
'Check out document: %s'
|
|
||||||
) % queryset.first()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_post_object_action_url(self):
|
|
||||||
if self.action_count == 1:
|
|
||||||
return reverse(
|
|
||||||
viewname='checkouts:document_checkout_info',
|
|
||||||
kwargs={'pk': self.action_id_list[0]}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
super(DocumentCheckoutView, self).get_post_action_redirect()
|
|
||||||
|
|
||||||
def object_action(self, form, instance):
|
|
||||||
DocumentCheckout.objects.check_out_document(
|
|
||||||
block_new_version=form.cleaned_data['block_new_version'],
|
|
||||||
document=instance,
|
|
||||||
expiration_datetime=form.cleaned_data['expiration_datetime'],
|
|
||||||
user=self.request.user,
|
|
||||||
)
|
)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class DocumentCheckoutDetailView(SingleObjectDetailView):
|
class CheckoutDetailView(SingleObjectDetailView):
|
||||||
form_class = DocumentCheckoutDefailForm
|
form_class = DocumentCheckoutDefailForm
|
||||||
model = Document
|
model = Document
|
||||||
object_permission = permission_document_check_out_detail_view
|
object_permission = permission_document_check_out_detail_view
|
||||||
@@ -149,27 +188,3 @@ class DocumentCheckoutDetailView(SingleObjectDetailView):
|
|||||||
'Check out details for document: %s'
|
'Check out details for document: %s'
|
||||||
) % self.object
|
) % self.object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DocumentCheckoutListView(DocumentListView):
|
|
||||||
def get_document_queryset(self):
|
|
||||||
return AccessControlList.objects.restrict_queryset(
|
|
||||||
permission=permission_document_check_out_detail_view,
|
|
||||||
queryset=DocumentCheckout.objects.checked_out_documents(),
|
|
||||||
user=self.request.user
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_extra_context(self):
|
|
||||||
context = super(DocumentCheckoutListView, self).get_extra_context()
|
|
||||||
context.update(
|
|
||||||
{
|
|
||||||
'no_results_icon': icon_check_out_info,
|
|
||||||
'no_results_text': _(
|
|
||||||
'Checking out a document, blocks certain operations '
|
|
||||||
'for a predetermined amount of time.'
|
|
||||||
),
|
|
||||||
'no_results_title': _('No documents have been checked out'),
|
|
||||||
'title': _('Checked out documents'),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return context
|
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ class SplitTimeDeltaWidget(forms.widgets.MultiWidget):
|
|||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
def value_from_datadict(self, querydict, files, name):
|
def value_from_datadict(self, querydict, files, name):
|
||||||
unit = querydict.get('{}_0'.format(name))
|
unit = querydict.get('{}_1'.format(name))
|
||||||
period = querydict.get('{}_1'.format(name))
|
period = querydict.get('{}_0'.format(name))
|
||||||
|
|
||||||
if not unit or not period:
|
if not unit or not period:
|
||||||
return now()
|
return now()
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ from .links import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .literals import MESSAGE_SQLITE_WARNING
|
from .literals import MESSAGE_SQLITE_WARNING
|
||||||
from .menus import menu_about, menu_secondary, menu_topbar, menu_user
|
from .menus import (
|
||||||
|
menu_about, menu_main, menu_secondary, menu_user
|
||||||
|
)
|
||||||
from .settings import (
|
from .settings import (
|
||||||
setting_auto_logging, setting_production_error_log_path,
|
setting_auto_logging, setting_production_error_log_path,
|
||||||
setting_production_error_logging
|
setting_production_error_logging
|
||||||
@@ -95,10 +97,7 @@ class CommonApp(MayanAppConfig):
|
|||||||
)
|
)
|
||||||
|
|
||||||
Template(
|
Template(
|
||||||
name='menu_main', template_name='appearance/menu_main.html'
|
name='menu_main', template_name='appearance/main_menu.html'
|
||||||
)
|
|
||||||
Template(
|
|
||||||
name='menu_topbar', template_name='appearance/menu_topbar.html'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
menu_user.bind_links(
|
menu_user.bind_links(
|
||||||
@@ -113,7 +112,7 @@ class CommonApp(MayanAppConfig):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
menu_topbar.bind_links(links=(menu_about, menu_user,), position=99)
|
menu_main.bind_links(links=(menu_about, menu_user,), position=99)
|
||||||
menu_secondary.bind_links(
|
menu_secondary.bind_links(
|
||||||
links=(link_object_error_list_clear,), sources=(
|
links=(link_object_error_list_clear,), sources=(
|
||||||
'common:object_error_list',
|
'common:object_error_list',
|
||||||
|
|||||||
@@ -61,9 +61,102 @@ PythonDependency(
|
|||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
''', module=__name__, name='PyYAML', version_string='==5.1.1'
|
''', module=__name__, name='PyYAML', version_string='==5.1.1'
|
||||||
)
|
)
|
||||||
|
PythonDependency(
|
||||||
|
copyright_text='''
|
||||||
|
Copyright (c) 2015 Ask Solem & contributors. All rights reserved.
|
||||||
|
Copyright (c) 2012-2014 GoPivotal, Inc. All rights reserved.
|
||||||
|
Copyright (c) 2009, 2010, 2011, 2012 Ask Solem, and individual contributors. All rights reserved.
|
||||||
|
|
||||||
|
Celery is licensed under The BSD License (3 Clause, also known as
|
||||||
|
the new BSD license). The license is an OSI approved Open Source
|
||||||
|
license and is GPL-compatible(1).
|
||||||
|
|
||||||
|
The license text can also be found here:
|
||||||
|
http://www.opensource.org/licenses/BSD-3-Clause
|
||||||
|
|
||||||
|
License
|
||||||
|
=======
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of Ask Solem, nor the
|
||||||
|
names of its contributors may be used to endorse or promote products
|
||||||
|
derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Ask Solem OR CONTRIBUTORS
|
||||||
|
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
Documentation License
|
||||||
|
=====================
|
||||||
|
|
||||||
|
The documentation portion of Celery (the rendered contents of the
|
||||||
|
"docs" directory of a software distribution or checkout) is supplied
|
||||||
|
under the Creative Commons Attribution-Noncommercial-Share Alike 3.0
|
||||||
|
United States License as described by
|
||||||
|
http://creativecommons.org/licenses/by-nc-sa/3.0/us/
|
||||||
|
|
||||||
|
Footnotes
|
||||||
|
=========
|
||||||
|
(1) A GPL-compatible license makes it possible to
|
||||||
|
combine Celery with other software that is released
|
||||||
|
under the GPL, it does not mean that we're distributing
|
||||||
|
Celery under the GPL license. The BSD license, unlike the GPL,
|
||||||
|
let you distribute a modified version without making your
|
||||||
|
changes open source.
|
||||||
|
''', module=__name__, name='celery', version_string='==3.1.24'
|
||||||
|
)
|
||||||
|
PythonDependency(
|
||||||
|
copyright_text='''
|
||||||
|
Copyright (c) 2012-2013 GoPivotal, Inc. All Rights Reserved.
|
||||||
|
Copyright (c) 2009-2012 Ask Solem. All Rights Reserved.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
Neither the name of Ask Solem nor the names of its contributors may be used
|
||||||
|
to endorse or promote products derived from this software without specific
|
||||||
|
prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
|
||||||
|
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
''', module=__name__, name='django-celery', version_string='==3.2.1'
|
||||||
|
)
|
||||||
PythonDependency(
|
PythonDependency(
|
||||||
module=__name__, name='django-downloadview', version_string='==1.9'
|
module=__name__, name='django-downloadview', version_string='==1.9'
|
||||||
)
|
)
|
||||||
|
PythonDependency(
|
||||||
|
module=__name__, name='django-environ', version_string='==0.4.5'
|
||||||
|
)
|
||||||
PythonDependency(
|
PythonDependency(
|
||||||
module=__name__, name='django-formtools', version_string='==2.1'
|
module=__name__, name='django-formtools', version_string='==2.1'
|
||||||
)
|
)
|
||||||
@@ -290,10 +383,6 @@ PythonDependency(
|
|||||||
module=__name__, environment=environment_development, name='Werkzeug',
|
module=__name__, environment=environment_development, name='Werkzeug',
|
||||||
version_string='==0.15.4'
|
version_string='==0.15.4'
|
||||||
)
|
)
|
||||||
PythonDependency(
|
|
||||||
module=__name__, environment=environment_development, name='devpi-server',
|
|
||||||
version_string='==5.0.0'
|
|
||||||
)
|
|
||||||
PythonDependency(
|
PythonDependency(
|
||||||
environment=environment_development, module=__name__,
|
environment=environment_development, module=__name__,
|
||||||
name='django-debug-toolbar', version_string='==1.11'
|
name='django-debug-toolbar', version_string='==1.11'
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.http import QueryDict
|
from django.http import QueryDict
|
||||||
from django.utils.encoding import force_bytes
|
from django.utils.encoding import force_bytes
|
||||||
from django.utils.six import PY3
|
|
||||||
|
|
||||||
|
|
||||||
class URL(object):
|
class URL(object):
|
||||||
@@ -21,7 +20,9 @@ class URL(object):
|
|||||||
|
|
||||||
def to_string(self):
|
def to_string(self):
|
||||||
if self._args.keys():
|
if self._args.keys():
|
||||||
query = '?{}'.format(self._args.urlencode())
|
query = force_bytes(
|
||||||
|
'?{}'.format(self._args.urlencode())
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
query = ''
|
query = ''
|
||||||
|
|
||||||
@@ -30,9 +31,6 @@ class URL(object):
|
|||||||
else:
|
else:
|
||||||
path = ''
|
path = ''
|
||||||
|
|
||||||
result = '{}{}'.format(path, query)
|
result = force_bytes('{}{}'.format(path, query))
|
||||||
|
|
||||||
if PY3:
|
return result
|
||||||
return result
|
|
||||||
else:
|
|
||||||
return force_bytes(result)
|
|
||||||
|
|||||||
@@ -41,9 +41,6 @@ icon_object_errors = Icon(
|
|||||||
icon_object_error_list = Icon(
|
icon_object_error_list = Icon(
|
||||||
driver_name='fontawesome', symbol='exclamation-triangle'
|
driver_name='fontawesome', symbol='exclamation-triangle'
|
||||||
)
|
)
|
||||||
icon_object_error_list_clear = Icon(
|
|
||||||
driver_name='fontawesome', symbol='times'
|
|
||||||
)
|
|
||||||
icon_ok = Icon(
|
icon_ok = Icon(
|
||||||
driver_name='fontawesome', symbol='check'
|
driver_name='fontawesome', symbol='check'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -50,13 +50,12 @@ link_documentation = Link(
|
|||||||
text=_('Documentation'), url='https://docs.mayan-edms.com'
|
text=_('Documentation'), url='https://docs.mayan-edms.com'
|
||||||
)
|
)
|
||||||
link_object_error_list = Link(
|
link_object_error_list = Link(
|
||||||
icon_class_path='mayan.apps.common.icons.icon_object_error_list',
|
|
||||||
kwargs=get_kwargs_factory('resolved_object'),
|
kwargs=get_kwargs_factory('resolved_object'),
|
||||||
|
icon_class_path='mayan.apps.common.icons.icon_object_error_list',
|
||||||
permissions=(permission_error_log_view,), text=_('Errors'),
|
permissions=(permission_error_log_view,), text=_('Errors'),
|
||||||
view='common:object_error_list',
|
view='common:object_error_list',
|
||||||
)
|
)
|
||||||
link_object_error_list_clear = Link(
|
link_object_error_list_clear = Link(
|
||||||
icon_class_path='mayan.apps.common.icons.icon_object_error_list_clear',
|
|
||||||
kwargs=get_kwargs_factory('resolved_object'),
|
kwargs=get_kwargs_factory('resolved_object'),
|
||||||
permissions=(permission_error_log_view,), text=_('Clear all'),
|
permissions=(permission_error_log_view,), text=_('Clear all'),
|
||||||
view='common:object_error_list_clear',
|
view='common:object_error_list_clear',
|
||||||
|
|||||||
107
mayan/apps/common/management/commands/convertdb.py
Normal file
107
mayan/apps/common/management/commands/convertdb.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from pathlib2 import Path
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core import management
|
||||||
|
from django.core.management.base import CommandError
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from mayan.apps.documents.models import DocumentType
|
||||||
|
from mayan.apps.storage.utils import fs_cleanup
|
||||||
|
|
||||||
|
from ...literals import MESSAGE_DEPRECATION_WARNING
|
||||||
|
from ...warnings import DeprecationWarning
|
||||||
|
|
||||||
|
CONVERTDB_FOLDER = 'convertdb'
|
||||||
|
CONVERTDB_OUTPUT_FILENAME = 'migrate.json'
|
||||||
|
|
||||||
|
|
||||||
|
class Command(management.BaseCommand):
|
||||||
|
help = 'Convert from a database backend to another one.'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
warnings.warn(
|
||||||
|
category=DeprecationWarning,
|
||||||
|
message=force_text(MESSAGE_DEPRECATION_WARNING)
|
||||||
|
)
|
||||||
|
|
||||||
|
super(Command, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'args', metavar='app_label[.ModelName]', nargs='*',
|
||||||
|
help=_(
|
||||||
|
'Restricts dumped data to the specified app_label or '
|
||||||
|
'app_label.ModelName.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--from', action='store', default='default', dest='from',
|
||||||
|
help=_(
|
||||||
|
'The database from which data will be exported. If omitted '
|
||||||
|
'the database named "default" will be used.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--to', action='store', default='default', dest='to',
|
||||||
|
help=_(
|
||||||
|
'The database to which data will be imported. If omitted '
|
||||||
|
'the database named "default" will be used.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--force', action='store_true', dest='force',
|
||||||
|
help=_(
|
||||||
|
'Force the conversion of the database even if the receiving '
|
||||||
|
'database is not empty.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *app_labels, **options):
|
||||||
|
# Create the media/convertdb folder
|
||||||
|
convertdb_folder_path = force_text(
|
||||||
|
Path(
|
||||||
|
settings.MEDIA_ROOT, CONVERTDB_FOLDER
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(convertdb_folder_path)
|
||||||
|
except OSError as exception:
|
||||||
|
if exception.errno == errno.EEXIST:
|
||||||
|
pass
|
||||||
|
|
||||||
|
convertdb_file_path = force_text(
|
||||||
|
Path(
|
||||||
|
convertdb_folder_path, CONVERTDB_OUTPUT_FILENAME
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
management.call_command(command_name='purgeperiodictasks')
|
||||||
|
|
||||||
|
management.call_command(
|
||||||
|
'dumpdata', *app_labels, all=True,
|
||||||
|
database=options['from'], natural_primary=True,
|
||||||
|
natural_foreign=True, output=convertdb_file_path,
|
||||||
|
interactive=False, format='json'
|
||||||
|
)
|
||||||
|
|
||||||
|
if DocumentType.objects.using(options['to']).count() and not options['force']:
|
||||||
|
fs_cleanup(convertdb_file_path)
|
||||||
|
raise CommandError(
|
||||||
|
'There is existing data in the database that will be '
|
||||||
|
'used for the import. If you proceed with the conversion '
|
||||||
|
'you might lose data. Please check your settings.'
|
||||||
|
)
|
||||||
|
|
||||||
|
management.call_command(
|
||||||
|
'loaddata', convertdb_file_path, database=options['to'], interactive=False,
|
||||||
|
verbosity=3
|
||||||
|
)
|
||||||
|
fs_cleanup(convertdb_file_path)
|
||||||
@@ -28,8 +28,8 @@ class Command(management.BaseCommand):
|
|||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--no-dependencies', action='store_true', dest='no_dependencies',
|
'--no-javascript', action='store_true', dest='no_javascript',
|
||||||
help='Don\'t download dependencies.',
|
help='Don\'t download the JavaScript dependencies.',
|
||||||
)
|
)
|
||||||
|
|
||||||
def initialize_system(self, force=False):
|
def initialize_system(self, force=False):
|
||||||
@@ -88,9 +88,9 @@ class Command(management.BaseCommand):
|
|||||||
self.initialize_system(force=options.get('force', False))
|
self.initialize_system(force=options.get('force', False))
|
||||||
pre_initial_setup.send(sender=self)
|
pre_initial_setup.send(sender=self)
|
||||||
|
|
||||||
if not options.get('no_dependencies', False):
|
if not options.get('no_javascript', False):
|
||||||
management.call_command(
|
management.call_command(
|
||||||
command_name='installdependencies', interactive=False
|
command_name='installjavascript', interactive=False
|
||||||
)
|
)
|
||||||
|
|
||||||
management.call_command(
|
management.call_command(
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
SETTING_FILE_TEMPLATE = '''
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from .base import *
|
||||||
|
|
||||||
|
SECRET_KEY = '{0}'
|
||||||
|
|
||||||
|
'''
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ class Command(management.BaseCommand):
|
|||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--no-dependencies', action='store_true', dest='no_dependencies',
|
'--no-javascript', action='store_true', dest='no_javascript',
|
||||||
help='Don\'t download dependencies.',
|
help='Don\'t download the JavaScript dependencies.',
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
@@ -25,9 +25,9 @@ class Command(management.BaseCommand):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not options.get('no_dependencies', False):
|
if not options.get('no_javascript', False):
|
||||||
management.call_command(
|
management.call_command(
|
||||||
command_name='installdependencies', interactive=False
|
command_name='installjavascript', interactive=False
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.core import management
|
from django.core import management
|
||||||
|
|
||||||
from django_celery_beat.models import IntervalSchedule, PeriodicTask
|
from djcelery.models import IntervalSchedule, PeriodicTask
|
||||||
|
|
||||||
|
|
||||||
class Command(management.BaseCommand):
|
class Command(management.BaseCommand):
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ menu_object = Menu(label=_('Actions'), name='object')
|
|||||||
menu_secondary = Menu(label=_('Secondary'), name='secondary')
|
menu_secondary = Menu(label=_('Secondary'), name='secondary')
|
||||||
menu_setup = Menu(name='setup')
|
menu_setup = Menu(name='setup')
|
||||||
menu_tools = Menu(name='tools')
|
menu_tools = Menu(name='tools')
|
||||||
menu_topbar = Menu(name='topbar')
|
|
||||||
menu_user = Menu(
|
menu_user = Menu(
|
||||||
icon_class=icon_menu_user, name='user', label=_('User')
|
icon_class=icon_menu_user, name='user', label=_('User')
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
|
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
|
||||||
from django.http import Http404, HttpResponseRedirect
|
from django.http import Http404, HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
@@ -9,7 +8,6 @@ from django.urls import reverse
|
|||||||
from django.utils.translation import ungettext, ugettext_lazy as _
|
from django.utils.translation import ungettext, ugettext_lazy as _
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
|
|
||||||
from mayan.apps.acls.classes import ModelPermission
|
|
||||||
from mayan.apps.acls.models import AccessControlList
|
from mayan.apps.acls.models import AccessControlList
|
||||||
from mayan.apps.permissions import Permission
|
from mayan.apps.permissions import Permission
|
||||||
|
|
||||||
@@ -19,28 +17,6 @@ from .literals import PK_LIST_SEPARATOR
|
|||||||
from .settings import setting_home_view
|
from .settings import setting_home_view
|
||||||
|
|
||||||
|
|
||||||
class ContentTypeViewMixin(object):
|
|
||||||
"""
|
|
||||||
This mixin makes it easier for views to retrieve a content type from
|
|
||||||
the URL pattern.
|
|
||||||
"""
|
|
||||||
content_type_url_kw_args = {
|
|
||||||
'app_label': 'app_label',
|
|
||||||
'model_name': 'model'
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_content_type(self):
|
|
||||||
return get_object_or_404(
|
|
||||||
klass=ContentType,
|
|
||||||
app_label=self.kwargs[
|
|
||||||
self.content_type_url_kw_args['app_label']
|
|
||||||
],
|
|
||||||
model=self.kwargs[
|
|
||||||
self.content_type_url_kw_args['model_name']
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DeleteExtraDataMixin(object):
|
class DeleteExtraDataMixin(object):
|
||||||
"""
|
"""
|
||||||
Mixin to populate the extra data needed for delete views
|
Mixin to populate the extra data needed for delete views
|
||||||
@@ -127,15 +103,7 @@ class ExternalObjectMixin(object):
|
|||||||
'get_external_object_queryset() method.'
|
'get_external_object_queryset() method.'
|
||||||
)
|
)
|
||||||
|
|
||||||
queryset = self.external_object_queryset
|
return self.external_object_queryset or self.external_object_class.objects.all()
|
||||||
|
|
||||||
if not queryset:
|
|
||||||
manager = ModelPermission.get_manager(
|
|
||||||
model=self.external_object_class
|
|
||||||
)
|
|
||||||
queryset = manager.all()
|
|
||||||
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def get_external_object_queryset_filtered(self):
|
def get_external_object_queryset_filtered(self):
|
||||||
queryset = self.get_external_object_queryset()
|
queryset = self.get_external_object_queryset()
|
||||||
@@ -150,20 +118,6 @@ class ExternalObjectMixin(object):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class ExternalContentTypeObjectMixin(ContentTypeViewMixin, ExternalObjectMixin):
|
|
||||||
"""
|
|
||||||
Mixin to retrieve an external object by content type from the URL pattern.
|
|
||||||
"""
|
|
||||||
external_object_pk_url_kwarg = 'object_id'
|
|
||||||
|
|
||||||
def get_external_object_queryset(self):
|
|
||||||
content_type = self.get_content_type()
|
|
||||||
self.external_object_class = content_type.model_class()
|
|
||||||
return super(
|
|
||||||
ExternalContentTypeObjectMixin, self
|
|
||||||
).get_external_object_queryset()
|
|
||||||
|
|
||||||
|
|
||||||
class FormExtraKwargsMixin(object):
|
class FormExtraKwargsMixin(object):
|
||||||
"""
|
"""
|
||||||
Mixin that allows a view to pass extra keyword arguments to forms
|
Mixin that allows a view to pass extra keyword arguments to forms
|
||||||
@@ -296,9 +250,9 @@ class ObjectActionMixin(object):
|
|||||||
|
|
||||||
def get_success_message(self, count):
|
def get_success_message(self, count):
|
||||||
return ungettext(
|
return ungettext(
|
||||||
singular=self.success_message,
|
self.success_message,
|
||||||
plural=self.success_message_plural,
|
self.success_message_plural,
|
||||||
number=count
|
count
|
||||||
) % {
|
) % {
|
||||||
'count': count,
|
'count': count,
|
||||||
}
|
}
|
||||||
@@ -317,15 +271,14 @@ class ObjectActionMixin(object):
|
|||||||
pass
|
pass
|
||||||
except ActionError:
|
except ActionError:
|
||||||
messages.error(
|
messages.error(
|
||||||
message=self.error_message % {'instance': instance},
|
self.request, self.error_message % {'instance': instance}
|
||||||
request=self.request
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.action_count += 1
|
self.action_count += 1
|
||||||
|
|
||||||
messages.success(
|
messages.success(
|
||||||
message=self.get_success_message(count=self.action_count),
|
self.request,
|
||||||
request=self.request
|
self.get_success_message(count=self.action_count)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
try:
|
|
||||||
from yaml import CSafeLoader as SafeLoader, CSafeDumper as SafeDumper
|
|
||||||
except ImportError:
|
|
||||||
from yaml import SafeLoader, SafeDumper
|
|
||||||
|
|
||||||
|
|
||||||
def yaml_dump(*args, **kwargs):
|
|
||||||
defaults = {'Dumper': SafeDumper}
|
|
||||||
defaults.update(kwargs)
|
|
||||||
|
|
||||||
return yaml.dump(*args, **defaults)
|
|
||||||
|
|
||||||
|
|
||||||
def yaml_load(*args, **kwargs):
|
|
||||||
defaults = {'Loader': SafeLoader}
|
|
||||||
defaults.update(kwargs)
|
|
||||||
|
|
||||||
return yaml.load(*args, **defaults)
|
|
||||||
@@ -6,10 +6,11 @@ from django.conf import settings
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
import mayan
|
import mayan
|
||||||
from mayan.apps.smart_settings.classes import Namespace
|
from mayan.apps.smart_settings import Namespace
|
||||||
|
|
||||||
from .literals import DEFAULT_COMMON_HOME_VIEW
|
from .literals import DEFAULT_COMMON_HOME_VIEW
|
||||||
|
|
||||||
|
|
||||||
namespace = Namespace(label=_('Common'), name='common')
|
namespace = Namespace(label=_('Common'), name='common')
|
||||||
|
|
||||||
setting_auto_logging = namespace.add_setting(
|
setting_auto_logging = namespace.add_setting(
|
||||||
@@ -94,5 +95,322 @@ setting_shared_storage = namespace.add_setting(
|
|||||||
)
|
)
|
||||||
setting_shared_storage_arguments = namespace.add_setting(
|
setting_shared_storage_arguments = namespace.add_setting(
|
||||||
global_name='COMMON_SHARED_STORAGE_ARGUMENTS',
|
global_name='COMMON_SHARED_STORAGE_ARGUMENTS',
|
||||||
default={'location': os.path.join(settings.MEDIA_ROOT, 'shared_files')}
|
default='{{location: {}}}'.format(
|
||||||
|
os.path.join(settings.MEDIA_ROOT, 'shared_files')
|
||||||
|
), quoted=True
|
||||||
|
)
|
||||||
|
|
||||||
|
namespace = Namespace(label=_('Django'), name='django')
|
||||||
|
|
||||||
|
setting_django_allowed_hosts = namespace.add_setting(
|
||||||
|
global_name='ALLOWED_HOSTS', default=settings.ALLOWED_HOSTS,
|
||||||
|
help_text=_(
|
||||||
|
'A list of strings representing the host/domain names that this site '
|
||||||
|
'can serve. This is a security measure to prevent HTTP Host header '
|
||||||
|
'attacks, which are possible even under many seemingly-safe web '
|
||||||
|
'server configurations. Values in this list can be '
|
||||||
|
'fully qualified names (e.g. \'www.example.com\'), in which case '
|
||||||
|
'they will be matched against the request\'s Host header exactly '
|
||||||
|
'(case-insensitive, not including port). A value beginning with a '
|
||||||
|
'period can be used as a subdomain wildcard: \'.example.com\' will '
|
||||||
|
'match example.com, www.example.com, and any other subdomain of '
|
||||||
|
'example.com. A value of \'*\' will match anything; in this case you '
|
||||||
|
'are responsible to provide your own validation of the Host header '
|
||||||
|
'(perhaps in a middleware; if so this middleware must be listed '
|
||||||
|
'first in MIDDLEWARE).'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_append_slash = namespace.add_setting(
|
||||||
|
global_name='APPEND_SLASH', default=settings.APPEND_SLASH,
|
||||||
|
help_text=_(
|
||||||
|
'When set to True, if the request URL does not match any of the '
|
||||||
|
'patterns in the URLconf and it doesn\'t end in a slash, an HTTP '
|
||||||
|
'redirect is issued to the same URL with a slash appended. Note '
|
||||||
|
'that the redirect may cause any data submitted in a POST request '
|
||||||
|
'to be lost. The APPEND_SLASH setting is only used if '
|
||||||
|
'CommonMiddleware is installed (see Middleware). See also '
|
||||||
|
'PREPEND_WWW.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setting_django_auth_password_validators = namespace.add_setting(
|
||||||
|
global_name='AUTH_PASSWORD_VALIDATORS',
|
||||||
|
default=settings.AUTH_PASSWORD_VALIDATORS,
|
||||||
|
help_text=_(
|
||||||
|
'The list of validators that are used to check the strength of '
|
||||||
|
'user\'s passwords.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setting_django_databases = namespace.add_setting(
|
||||||
|
global_name='DATABASES', default=settings.DATABASES,
|
||||||
|
help_text=_(
|
||||||
|
'A dictionary containing the settings for all databases to be used '
|
||||||
|
'with Django. It is a nested dictionary whose contents map a '
|
||||||
|
'database alias to a dictionary containing the options for an '
|
||||||
|
'individual database. The DATABASES setting must configure a '
|
||||||
|
'default database; any number of additional databases may also '
|
||||||
|
'be specified.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_data_upload_max_memory_size = namespace.add_setting(
|
||||||
|
global_name='DATA_UPLOAD_MAX_MEMORY_SIZE',
|
||||||
|
default=settings.DATA_UPLOAD_MAX_MEMORY_SIZE,
|
||||||
|
help_text=_(
|
||||||
|
'Default: 2621440 (i.e. 2.5 MB). The maximum size in bytes that a '
|
||||||
|
'request body may be before a SuspiciousOperation '
|
||||||
|
'(RequestDataTooBig) is raised. The check is done when accessing '
|
||||||
|
'request.body or request.POST and is calculated against the total '
|
||||||
|
'request size excluding any file upload data. You can set this to '
|
||||||
|
'None to disable the check. Applications that are expected to '
|
||||||
|
'receive unusually large form posts should tune this setting. The '
|
||||||
|
'amount of request data is correlated to the amount of memory '
|
||||||
|
'needed to process the request and populate the GET and POST '
|
||||||
|
'dictionaries. Large requests could be used as a '
|
||||||
|
'denial-of-service attack vector if left unchecked. Since web '
|
||||||
|
'servers don\'t typically perform deep request inspection, it\'s '
|
||||||
|
'not possible to perform a similar check at that level. See also '
|
||||||
|
'FILE_UPLOAD_MAX_MEMORY_SIZE.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_default_from_email = namespace.add_setting(
|
||||||
|
global_name='DEFAULT_FROM_EMAIL',
|
||||||
|
default=settings.DEFAULT_FROM_EMAIL,
|
||||||
|
help_text=_(
|
||||||
|
'Default: \'webmaster@localhost\' '
|
||||||
|
'Default email address to use for various automated correspondence '
|
||||||
|
'from the site manager(s). This doesn\'t include error messages sent '
|
||||||
|
'to ADMINS and MANAGERS; for that, see SERVER_EMAIL.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_disallowed_user_agents = namespace.add_setting(
|
||||||
|
global_name='DISALLOWED_USER_AGENTS',
|
||||||
|
default=settings.DISALLOWED_USER_AGENTS,
|
||||||
|
help_text=_(
|
||||||
|
'Default: [] (Empty list). List of compiled regular expression '
|
||||||
|
'objects representing User-Agent strings that are not allowed to '
|
||||||
|
'visit any page, systemwide. Use this for bad robots/crawlers. '
|
||||||
|
'This is only used if CommonMiddleware is installed '
|
||||||
|
'(see Middleware).'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_email_backend = namespace.add_setting(
|
||||||
|
global_name='EMAIL_BACKEND',
|
||||||
|
default=settings.EMAIL_BACKEND,
|
||||||
|
help_text=_(
|
||||||
|
'Default: \'django.core.mail.backends.smtp.EmailBackend\'. The '
|
||||||
|
'backend to use for sending emails.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_email_host = namespace.add_setting(
|
||||||
|
global_name='EMAIL_HOST',
|
||||||
|
default=settings.EMAIL_HOST,
|
||||||
|
help_text=_(
|
||||||
|
'Default: \'localhost\'. The host to use for sending email.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_email_host_password = namespace.add_setting(
|
||||||
|
global_name='EMAIL_HOST_PASSWORD',
|
||||||
|
default=settings.EMAIL_HOST_PASSWORD,
|
||||||
|
help_text=_(
|
||||||
|
'Default: \'\' (Empty string). Password to use for the SMTP '
|
||||||
|
'server defined in EMAIL_HOST. This setting is used in '
|
||||||
|
'conjunction with EMAIL_HOST_USER when authenticating to the '
|
||||||
|
'SMTP server. If either of these settings is empty, '
|
||||||
|
'Django won\'t attempt authentication.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_email_host_user = namespace.add_setting(
|
||||||
|
global_name='EMAIL_HOST_USER',
|
||||||
|
default=settings.EMAIL_HOST_USER,
|
||||||
|
help_text=_(
|
||||||
|
'Default: \'\' (Empty string). Username to use for the SMTP '
|
||||||
|
'server defined in EMAIL_HOST. If empty, Django won\'t attempt '
|
||||||
|
'authentication.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_email_port = namespace.add_setting(
|
||||||
|
global_name='EMAIL_PORT',
|
||||||
|
default=settings.EMAIL_PORT,
|
||||||
|
help_text=_(
|
||||||
|
'Default: 25. Port to use for the SMTP server defined in EMAIL_HOST.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_email_timeout = namespace.add_setting(
|
||||||
|
global_name='EMAIL_TIMEOUT',
|
||||||
|
default=settings.EMAIL_TIMEOUT,
|
||||||
|
help_text=_(
|
||||||
|
'Default: None. Specifies a timeout in seconds for blocking '
|
||||||
|
'operations like the connection attempt.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_email_user_tls = namespace.add_setting(
|
||||||
|
global_name='EMAIL_USE_TLS',
|
||||||
|
default=settings.EMAIL_USE_TLS,
|
||||||
|
help_text=_(
|
||||||
|
'Default: False. Whether to use a TLS (secure) connection when '
|
||||||
|
'talking to the SMTP server. This is used for explicit TLS '
|
||||||
|
'connections, generally on port 587. If you are experiencing '
|
||||||
|
'hanging connections, see the implicit TLS setting EMAIL_USE_SSL.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_email_user_ssl = namespace.add_setting(
|
||||||
|
global_name='EMAIL_USE_SSL',
|
||||||
|
default=settings.EMAIL_USE_SSL,
|
||||||
|
help_text=_(
|
||||||
|
'Default: False. Whether to use an implicit TLS (secure) connection '
|
||||||
|
'when talking to the SMTP server. In most email documentation this '
|
||||||
|
'type of TLS connection is referred to as SSL. It is generally used '
|
||||||
|
'on port 465. If you are experiencing problems, see the explicit '
|
||||||
|
'TLS setting EMAIL_USE_TLS. Note that EMAIL_USE_TLS/EMAIL_USE_SSL '
|
||||||
|
'are mutually exclusive, so only set one of those settings to True.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_file_upload_max_memory_size = namespace.add_setting(
|
||||||
|
global_name='FILE_UPLOAD_MAX_MEMORY_SIZE',
|
||||||
|
default=settings.FILE_UPLOAD_MAX_MEMORY_SIZE,
|
||||||
|
help_text=_(
|
||||||
|
'Default: 2621440 (i.e. 2.5 MB). The maximum size (in bytes) '
|
||||||
|
'that an upload will be before it gets streamed to the file '
|
||||||
|
'system. See Managing files for details. See also '
|
||||||
|
'DATA_UPLOAD_MAX_MEMORY_SIZE.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_login_url = namespace.add_setting(
|
||||||
|
global_name='LOGIN_URL',
|
||||||
|
default=settings.LOGIN_URL,
|
||||||
|
help_text=_(
|
||||||
|
'Default: \'/accounts/login/\' The URL where requests are '
|
||||||
|
'redirected for login, especially when using the login_required() '
|
||||||
|
'decorator. This setting also accepts named URL patterns which '
|
||||||
|
'can be used to reduce configuration duplication since you '
|
||||||
|
'don\'t have to define the URL in two places (settings '
|
||||||
|
'and URLconf).'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setting_django_login_redirect_url = namespace.add_setting(
|
||||||
|
global_name='LOGIN_REDIRECT_URL',
|
||||||
|
default=settings.LOGIN_REDIRECT_URL,
|
||||||
|
help_text=_(
|
||||||
|
'Default: \'/accounts/profile/\' The URL where requests are '
|
||||||
|
'redirected after login when the contrib.auth.login view gets no '
|
||||||
|
'next parameter. This is used by the login_required() decorator, '
|
||||||
|
'for example. This setting also accepts named URL patterns which '
|
||||||
|
'can be used to reduce configuration duplication since you don\'t '
|
||||||
|
'have to define the URL in two places (settings and URLconf).'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_logout_redirect_url = namespace.add_setting(
|
||||||
|
global_name='LOGOUT_REDIRECT_URL',
|
||||||
|
default=settings.LOGOUT_REDIRECT_URL,
|
||||||
|
help_text=_(
|
||||||
|
'Default: None. The URL where requests are redirected after a user '
|
||||||
|
'logs out using LogoutView (if the view doesn\'t get a next_page '
|
||||||
|
'argument). If None, no redirect will be performed and the logout '
|
||||||
|
'view will be rendered. This setting also accepts named URL '
|
||||||
|
'patterns which can be used to reduce configuration duplication '
|
||||||
|
'since you don\'t have to define the URL in two places (settings '
|
||||||
|
'and URLconf).'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setting_django_internal_ips = namespace.add_setting(
|
||||||
|
global_name='INTERNAL_IPS',
|
||||||
|
default=settings.INTERNAL_IPS,
|
||||||
|
help_text=_(
|
||||||
|
'A list of IP addresses, as strings, that: Allow the debug() '
|
||||||
|
'context processor to add some variables to the template context. '
|
||||||
|
'Can use the admindocs bookmarklets even if not logged in as a '
|
||||||
|
'staff user. Are marked as "internal" (as opposed to "EXTERNAL") '
|
||||||
|
'in AdminEmailHandler emails.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_languages = namespace.add_setting(
|
||||||
|
global_name='LANGUAGES',
|
||||||
|
default=settings.LANGUAGES,
|
||||||
|
help_text=_(
|
||||||
|
'A list of all available languages. The list is a list of '
|
||||||
|
'two-tuples in the format (language code, language name) '
|
||||||
|
'for example, (\'ja\', \'Japanese\'). This specifies which '
|
||||||
|
'languages are available for language selection. '
|
||||||
|
'Generally, the default value should suffice. Only set this '
|
||||||
|
'setting if you want to restrict language selection to a '
|
||||||
|
'subset of the Django-provided languages. '
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_language_code = namespace.add_setting(
|
||||||
|
global_name='LANGUAGE_CODE',
|
||||||
|
default=settings.LANGUAGE_CODE,
|
||||||
|
help_text=_(
|
||||||
|
'A string representing the language code for this installation. '
|
||||||
|
'This should be in standard language ID format. For example, U.S. '
|
||||||
|
'English is "en-us". It serves two purposes: If the locale '
|
||||||
|
'middleware isn\'t in use, it decides which translation is served '
|
||||||
|
'to all users. If the locale middleware is active, it provides a '
|
||||||
|
'fallback language in case the user\'s preferred language can\'t '
|
||||||
|
'be determined or is not supported by the website. It also provides '
|
||||||
|
'the fallback translation when a translation for a given literal '
|
||||||
|
'doesn\'t exist for the user\'s preferred language.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_static_url = namespace.add_setting(
|
||||||
|
global_name='STATIC_URL',
|
||||||
|
default=settings.STATIC_URL,
|
||||||
|
help_text=_(
|
||||||
|
'URL to use when referring to static files located in STATIC_ROOT. '
|
||||||
|
'Example: "/static/" or "http://static.example.com/" '
|
||||||
|
'If not None, this will be used as the base path for asset '
|
||||||
|
'definitions (the Media class) and the staticfiles app. '
|
||||||
|
'It must end in a slash if set to a non-empty value.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_staticfiles_storage = namespace.add_setting(
|
||||||
|
global_name='STATICFILES_STORAGE',
|
||||||
|
default=settings.STATICFILES_STORAGE,
|
||||||
|
help_text=_(
|
||||||
|
'The file storage engine to use when collecting static files with '
|
||||||
|
'the collectstatic management command. A ready-to-use instance of '
|
||||||
|
'the storage backend defined in this setting can be found at '
|
||||||
|
'django.contrib.staticfiles.storage.staticfiles_storage.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_time_zone = namespace.add_setting(
|
||||||
|
global_name='TIME_ZONE',
|
||||||
|
default=settings.TIME_ZONE,
|
||||||
|
help_text=_(
|
||||||
|
'A string representing the time zone for this installation. '
|
||||||
|
'Note that this isn\'t necessarily the time zone of the server. '
|
||||||
|
'For example, one server may serve multiple Django-powered sites, '
|
||||||
|
'each with a separate time zone setting.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_django_wsgi_application = namespace.add_setting(
|
||||||
|
global_name='WSGI_APPLICATION',
|
||||||
|
default=settings.WSGI_APPLICATION,
|
||||||
|
help_text=_(
|
||||||
|
'The full Python path of the WSGI application object that Django\'s '
|
||||||
|
'built-in servers (e.g. runserver) will use. The django-admin '
|
||||||
|
'startproject management command will create a simple wsgi.py '
|
||||||
|
'file with an application callable in it, and point this setting '
|
||||||
|
'to that application.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
namespace = Namespace(label=_('Celery'), name='celery')
|
||||||
|
|
||||||
|
setting_celery_broker_url = namespace.add_setting(
|
||||||
|
global_name='BROKER_URL', default=settings.BROKER_URL,
|
||||||
|
help_text=_(
|
||||||
|
'Default: "amqp://". Default broker URL. This must be a URL in '
|
||||||
|
'the form of: transport://userid:password@hostname:port/virtual_host '
|
||||||
|
'Only the scheme part (transport://) is required, the rest is '
|
||||||
|
'optional, and defaults to the specific transports default values.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setting_celery_result_backend = namespace.add_setting(
|
||||||
|
global_name='CELERY_RESULT_BACKEND',
|
||||||
|
default=settings.CELERY_RESULT_BACKEND,
|
||||||
|
help_text=_(
|
||||||
|
'Default: No result backend enabled by default. The backend used '
|
||||||
|
'to store task results (tombstones). Refer to '
|
||||||
|
'http://docs.celeryproject.org/en/v4.1.0/userguide/configuration.'
|
||||||
|
'html#result-backend'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from mayan.apps.storage.utils import get_storage_subclass
|
import yaml
|
||||||
|
|
||||||
|
try:
|
||||||
|
from yaml import CSafeLoader as SafeLoader
|
||||||
|
except ImportError:
|
||||||
|
from yaml import SafeLoader
|
||||||
|
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
|
|
||||||
from .settings import (
|
from .settings import (
|
||||||
setting_shared_storage, setting_shared_storage_arguments
|
setting_shared_storage, setting_shared_storage_arguments
|
||||||
)
|
)
|
||||||
|
|
||||||
storage_sharedupload = get_storage_subclass(
|
storage_sharedupload = import_string(
|
||||||
dotted_path=setting_shared_storage.value
|
dotted_path=setting_shared_storage.value
|
||||||
)(**setting_shared_storage_arguments.value)
|
)(
|
||||||
|
**yaml.load(
|
||||||
|
stream=setting_shared_storage_arguments.value or '{}',
|
||||||
|
Loader=SafeLoader
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from django_downloadview import assert_download_response
|
|||||||
from mayan.apps.acls.tests.mixins import ACLTestCaseMixin
|
from mayan.apps.acls.tests.mixins import ACLTestCaseMixin
|
||||||
from mayan.apps.permissions.classes import Permission
|
from mayan.apps.permissions.classes import Permission
|
||||||
from mayan.apps.smart_settings.classes import Namespace
|
from mayan.apps.smart_settings.classes import Namespace
|
||||||
from mayan.apps.user_management.tests.mixins import UserTestMixin
|
|
||||||
|
|
||||||
from .mixins import (
|
from .mixins import (
|
||||||
ClientMethodsTestCaseMixin, ConnectionsCheckTestCaseMixin,
|
ClientMethodsTestCaseMixin, ConnectionsCheckTestCaseMixin,
|
||||||
@@ -22,7 +21,7 @@ class BaseTestCase(
|
|||||||
SilenceLoggerTestCaseMixin, ConnectionsCheckTestCaseMixin,
|
SilenceLoggerTestCaseMixin, ConnectionsCheckTestCaseMixin,
|
||||||
RandomPrimaryKeyModelMonkeyPatchMixin, ACLTestCaseMixin,
|
RandomPrimaryKeyModelMonkeyPatchMixin, ACLTestCaseMixin,
|
||||||
ModelTestCaseMixin, OpenFileCheckTestCaseMixin,
|
ModelTestCaseMixin, OpenFileCheckTestCaseMixin,
|
||||||
TempfileCheckTestCasekMixin, UserTestMixin, TestCase
|
TempfileCheckTestCasekMixin, TestCase
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
This is the most basic test case class any test in the project should use.
|
This is the most basic test case class any test in the project should use.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from django.contrib.auth import get_user_model
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from mayan.apps.acls.classes import ModelPermission
|
from mayan.apps.acls.classes import ModelPermission
|
||||||
|
from mayan.apps.user_management.tests.mixins import UserTestMixin
|
||||||
|
|
||||||
from ..models import ErrorLogEntry
|
from ..models import ErrorLogEntry
|
||||||
from ..permissions_runtime import permission_error_log_view
|
from ..permissions_runtime import permission_error_log_view
|
||||||
@@ -12,7 +13,7 @@ from .base import GenericViewTestCase
|
|||||||
from .literals import TEST_ERROR_LOG_ENTRY_RESULT
|
from .literals import TEST_ERROR_LOG_ENTRY_RESULT
|
||||||
|
|
||||||
|
|
||||||
class CommonViewTestCase(GenericViewTestCase):
|
class CommonViewTestCase(UserTestMixin, GenericViewTestCase):
|
||||||
def _request_about_view(self):
|
def _request_about_view(self):
|
||||||
return self.get(viewname='common:about_view')
|
return self.get(viewname='common:about_view')
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from django.utils.encoding import force_text
|
|
||||||
|
|
||||||
|
|
||||||
class NullFile(object):
|
class NullFile(object):
|
||||||
def write(self, string):
|
def write(self, string):
|
||||||
@@ -17,9 +13,3 @@ def mute_stdout():
|
|||||||
sys.stdout = NullFile()
|
sys.stdout = NullFile()
|
||||||
yield
|
yield
|
||||||
sys.stdout = stdout_old
|
sys.stdout = stdout_old
|
||||||
|
|
||||||
|
|
||||||
def as_id_list(items):
|
|
||||||
return ','.join(
|
|
||||||
[force_text(item.pk) for item in items]
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.views.i18n import JavaScriptCatalog
|
from django.views.i18n import JavaScriptCatalog, set_language
|
||||||
|
|
||||||
from .api_views import (
|
from .api_views import (
|
||||||
APIContentTypeList, APITemplateDetailView, APITemplateListView
|
APIContentTypeList, APITemplateDetailView, APITemplateListView
|
||||||
@@ -10,10 +10,30 @@ from .views import (
|
|||||||
AboutView, CurrentUserLocaleProfileDetailsView,
|
AboutView, CurrentUserLocaleProfileDetailsView,
|
||||||
CurrentUserLocaleProfileEditView, FaviconRedirectView, HomeView,
|
CurrentUserLocaleProfileEditView, FaviconRedirectView, HomeView,
|
||||||
LicenseView, ObjectErrorLogEntryListClearView, ObjectErrorLogEntryListView,
|
LicenseView, ObjectErrorLogEntryListClearView, ObjectErrorLogEntryListView,
|
||||||
RootView, SetupListView, ToolsListView
|
RootView, SetupListView, ToolsListView, multi_object_action_view
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns_error_logs = [
|
urlpatterns = [
|
||||||
|
url(regex=r'^$', view=RootView.as_view(), name='root'),
|
||||||
|
url(regex=r'^home/$', view=HomeView.as_view(), name='home'),
|
||||||
|
url(regex=r'^about/$', view=AboutView.as_view(), name='about_view'),
|
||||||
|
url(regex=r'^license/$', view=LicenseView.as_view(), name='license_view'),
|
||||||
|
url(
|
||||||
|
regex=r'^object/multiple/action/$', view=multi_object_action_view,
|
||||||
|
name='multi_object_action_view'
|
||||||
|
),
|
||||||
|
url(regex=r'^setup/$', view=SetupListView.as_view(), name='setup_list'),
|
||||||
|
url(regex=r'^tools/$', view=ToolsListView.as_view(), name='tools_list'),
|
||||||
|
url(
|
||||||
|
regex=r'^user/locale/$',
|
||||||
|
view=CurrentUserLocaleProfileDetailsView.as_view(),
|
||||||
|
name='current_user_locale_profile_details'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
regex=r'^user/locale/edit/$',
|
||||||
|
view=CurrentUserLocaleProfileEditView.as_view(),
|
||||||
|
name='current_user_locale_profile_edit'
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/errors/$',
|
regex=r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/errors/$',
|
||||||
view=ObjectErrorLogEntryListView.as_view(), name='object_error_list'
|
view=ObjectErrorLogEntryListView.as_view(), name='object_error_list'
|
||||||
@@ -25,20 +45,7 @@ urlpatterns_error_logs = [
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns_user_locale = [
|
urlpatterns += [
|
||||||
url(
|
|
||||||
regex=r'^user/locale/$',
|
|
||||||
view=CurrentUserLocaleProfileDetailsView.as_view(),
|
|
||||||
name='current_user_locale_profile_details'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
regex=r'^user/locale/edit/$',
|
|
||||||
view=CurrentUserLocaleProfileEditView.as_view(),
|
|
||||||
name='current_user_locale_profile_edit'
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
urlpatterns_misc = [
|
|
||||||
url(
|
url(
|
||||||
regex=r'^favicon\.ico$', view=FaviconRedirectView.as_view()
|
regex=r'^favicon\.ico$', view=FaviconRedirectView.as_view()
|
||||||
),
|
),
|
||||||
@@ -46,21 +53,11 @@ urlpatterns_misc = [
|
|||||||
regex=r'^jsi18n/(?P<packages>\S+?)/$', view=JavaScriptCatalog.as_view(),
|
regex=r'^jsi18n/(?P<packages>\S+?)/$', view=JavaScriptCatalog.as_view(),
|
||||||
name='javascript_catalog'
|
name='javascript_catalog'
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
regex=r'^set_language/$', view=set_language, name='set_language'
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
url(regex=r'^$', view=RootView.as_view(), name='root'),
|
|
||||||
url(regex=r'^home/$', view=HomeView.as_view(), name='home'),
|
|
||||||
url(regex=r'^about/$', view=AboutView.as_view(), name='about_view'),
|
|
||||||
url(regex=r'^license/$', view=LicenseView.as_view(), name='license_view'),
|
|
||||||
url(regex=r'^setup/$', view=SetupListView.as_view(), name='setup_list'),
|
|
||||||
url(regex=r'^tools/$', view=ToolsListView.as_view(), name='tools_list'),
|
|
||||||
]
|
|
||||||
|
|
||||||
urlpatterns.extend(urlpatterns_error_logs)
|
|
||||||
urlpatterns.extend(urlpatterns_misc)
|
|
||||||
urlpatterns.extend(urlpatterns_user_locale)
|
|
||||||
|
|
||||||
api_urls = [
|
api_urls = [
|
||||||
url(
|
url(
|
||||||
regex=r'^content_types/$', view=APIContentTypeList.as_view(),
|
regex=r'^content_types/$', view=APIContentTypeList.as_view(),
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ def check_for_sqlite():
|
|||||||
return settings.DATABASES['default']['ENGINE'] == DJANGO_SQLITE_BACKEND and settings.DEBUG is False
|
return settings.DATABASES['default']['ENGINE'] == DJANGO_SQLITE_BACKEND and settings.DEBUG is False
|
||||||
|
|
||||||
|
|
||||||
|
def encapsulate(function):
|
||||||
|
# Workaround Django ticket 15791
|
||||||
|
# Changeset 16045
|
||||||
|
# http://stackoverflow.com/questions/6861601/
|
||||||
|
# cannot-resolve-callable-context-variable/6955045#6955045
|
||||||
|
return lambda: function
|
||||||
|
|
||||||
|
|
||||||
def get_related_field(model, related_field_name):
|
def get_related_field(model, related_field_name):
|
||||||
try:
|
try:
|
||||||
local_field_name, remaining_field_path = related_field_name.split(
|
local_field_name, remaining_field_path = related_field_name.split(
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from json import dumps
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import timezone, translation
|
from django.utils import timezone, translation
|
||||||
|
from django.utils.http import urlencode
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
@@ -216,3 +220,67 @@ class ToolsListView(SimpleView):
|
|||||||
'These modules are used to do system maintenance.'
|
'These modules are used to do system maintenance.'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def multi_object_action_view(request):
|
||||||
|
"""
|
||||||
|
Proxy view called first when using a multi object action, which
|
||||||
|
then redirects to the appropriate specialized view
|
||||||
|
"""
|
||||||
|
next = request.POST.get(
|
||||||
|
'next', request.GET.get(
|
||||||
|
'next', request.META.get(
|
||||||
|
'HTTP_REFERER', reverse(setting_home_view.value)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
action = request.GET.get('action', None)
|
||||||
|
id_list = ','.join(
|
||||||
|
[key[3:] for key in request.GET.keys() if key.startswith('pk_')]
|
||||||
|
)
|
||||||
|
items_property_list = [
|
||||||
|
(key[11:]) for key in request.GET.keys() if key.startswith('properties_')
|
||||||
|
]
|
||||||
|
|
||||||
|
if not action:
|
||||||
|
messages.error(
|
||||||
|
message=_('No action selected.'), request=request
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(
|
||||||
|
redirect_to=request.META.get(
|
||||||
|
'HTTP_REFERER', reverse(setting_home_view.value)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not id_list and not items_property_list:
|
||||||
|
messages.error(
|
||||||
|
message=_('Must select at least one item.'),
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(
|
||||||
|
redirect_to=request.META.get(
|
||||||
|
'HTTP_REFERER', reverse(setting_home_view.value)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Separate redirects to keep backwards compatibility with older
|
||||||
|
# functions that don't expect a properties_list parameter
|
||||||
|
if items_property_list:
|
||||||
|
return HttpResponseRedirect(
|
||||||
|
redirect_to='%s?%s' % (
|
||||||
|
action,
|
||||||
|
urlencode(
|
||||||
|
{
|
||||||
|
'items_property_list': dumps(items_property_list),
|
||||||
|
'next': next
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return HttpResponseRedirect(
|
||||||
|
redirect_to='%s?%s' % (
|
||||||
|
action, urlencode({'id_list': id_list, 'next': next})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import shutil
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
import PyPDF2
|
import PyPDF2
|
||||||
import sh
|
import sh
|
||||||
|
import yaml
|
||||||
|
try:
|
||||||
|
from yaml import CSafeLoader as SafeLoader
|
||||||
|
except ImportError:
|
||||||
|
from yaml import SafeLoader
|
||||||
|
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
@@ -15,14 +20,16 @@ from mayan.apps.storage.utils import NamedTemporaryFile
|
|||||||
|
|
||||||
from ..classes import ConverterBase
|
from ..classes import ConverterBase
|
||||||
from ..exceptions import PageCountError
|
from ..exceptions import PageCountError
|
||||||
from ..settings import setting_graphics_backend_arguments
|
from ..settings import setting_graphics_backend_config
|
||||||
|
|
||||||
from ..literals import (
|
from ..literals import (
|
||||||
DEFAULT_PDFTOPPM_DPI, DEFAULT_PDFTOPPM_FORMAT, DEFAULT_PDFTOPPM_PATH,
|
DEFAULT_PDFTOPPM_DPI, DEFAULT_PDFTOPPM_FORMAT, DEFAULT_PDFTOPPM_PATH,
|
||||||
DEFAULT_PDFINFO_PATH
|
DEFAULT_PDFINFO_PATH
|
||||||
)
|
)
|
||||||
|
|
||||||
pdftoppm_path = setting_graphics_backend_arguments.value.get(
|
pdftoppm_path = yaml.load(
|
||||||
|
stream=setting_graphics_backend_config.value, Loader=SafeLoader
|
||||||
|
).get(
|
||||||
'pdftoppm_path', DEFAULT_PDFTOPPM_PATH
|
'pdftoppm_path', DEFAULT_PDFTOPPM_PATH
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,20 +39,26 @@ except sh.CommandNotFound:
|
|||||||
pdftoppm = None
|
pdftoppm = None
|
||||||
else:
|
else:
|
||||||
pdftoppm_format = '-{}'.format(
|
pdftoppm_format = '-{}'.format(
|
||||||
setting_graphics_backend_arguments.value.get(
|
yaml.load(
|
||||||
|
stream=setting_graphics_backend_config.value, Loader=SafeLoader
|
||||||
|
).get(
|
||||||
'pdftoppm_format', DEFAULT_PDFTOPPM_FORMAT
|
'pdftoppm_format', DEFAULT_PDFTOPPM_FORMAT
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
pdftoppm_dpi = format(
|
pdftoppm_dpi = format(
|
||||||
setting_graphics_backend_arguments.value.get(
|
yaml.load(
|
||||||
|
stream=setting_graphics_backend_config.value, Loader=SafeLoader
|
||||||
|
).get(
|
||||||
'pdftoppm_dpi', DEFAULT_PDFTOPPM_DPI
|
'pdftoppm_dpi', DEFAULT_PDFTOPPM_DPI
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
pdftoppm = pdftoppm.bake(pdftoppm_format, '-r', pdftoppm_dpi)
|
pdftoppm = pdftoppm.bake(pdftoppm_format, '-r', pdftoppm_dpi)
|
||||||
|
|
||||||
pdfinfo_path = setting_graphics_backend_arguments.value.get(
|
pdfinfo_path = yaml.load(
|
||||||
|
stream=setting_graphics_backend_config.value, Loader=SafeLoader
|
||||||
|
).get(
|
||||||
'pdfinfo_path', DEFAULT_PDFINFO_PATH
|
'pdfinfo_path', DEFAULT_PDFINFO_PATH
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ import shutil
|
|||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import sh
|
import sh
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
try:
|
||||||
|
from yaml import CSafeLoader as SafeLoader
|
||||||
|
except ImportError:
|
||||||
|
from yaml import SafeLoader
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
@@ -21,13 +27,15 @@ from .literals import (
|
|||||||
CONVERTER_OFFICE_FILE_MIMETYPES, DEFAULT_LIBREOFFICE_PATH,
|
CONVERTER_OFFICE_FILE_MIMETYPES, DEFAULT_LIBREOFFICE_PATH,
|
||||||
DEFAULT_PAGE_NUMBER, DEFAULT_PILLOW_FORMAT
|
DEFAULT_PAGE_NUMBER, DEFAULT_PILLOW_FORMAT
|
||||||
)
|
)
|
||||||
from .settings import setting_graphics_backend_arguments
|
from .settings import setting_graphics_backend_config
|
||||||
|
|
||||||
libreoffice_path = setting_graphics_backend_arguments.value.get(
|
|
||||||
'libreoffice_path', DEFAULT_LIBREOFFICE_PATH
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
BACKEND_CONFIG = yaml.load(
|
||||||
|
stream=setting_graphics_backend_config.value, Loader=SafeLoader
|
||||||
|
)
|
||||||
|
libreoffice_path = BACKEND_CONFIG.get(
|
||||||
|
'libreoffice_path', DEFAULT_LIBREOFFICE_PATH
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConverterBase(object):
|
class ConverterBase(object):
|
||||||
@@ -54,7 +62,9 @@ class ConverterBase(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def get_page(self, output_format=None):
|
def get_page(self, output_format=None):
|
||||||
output_format = output_format or setting_graphics_backend_arguments.value.get(
|
output_format = output_format or yaml.load(
|
||||||
|
stream=setting_graphics_backend_config.value, Loader=SafeLoader
|
||||||
|
).get(
|
||||||
'pillow_format', DEFAULT_PILLOW_FORMAT
|
'pillow_format', DEFAULT_PILLOW_FORMAT
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -146,7 +156,7 @@ class ConverterBase(object):
|
|||||||
logger.error('Exception launching Libre Office; %s', exception)
|
logger.error('Exception launching Libre Office; %s', exception)
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
fs_cleanup(filename=libreoffice_home_directory)
|
fs_cleanup(libreoffice_home_directory)
|
||||||
|
|
||||||
# LibreOffice return a PDF file with the same name as the input
|
# LibreOffice return a PDF file with the same name as the input
|
||||||
# provided but with the .pdf extension.
|
# provided but with the .pdf extension.
|
||||||
@@ -180,7 +190,7 @@ class ConverterBase(object):
|
|||||||
shutil.copyfileobj(
|
shutil.copyfileobj(
|
||||||
fsrc=converted_file_object, fdst=temporary_converted_file_object
|
fsrc=converted_file_object, fdst=temporary_converted_file_object
|
||||||
)
|
)
|
||||||
fs_cleanup(filename=converted_file_path)
|
fs_cleanup(converted_file_path)
|
||||||
temporary_converted_file_object.seek(0)
|
temporary_converted_file_object.seek(0)
|
||||||
return temporary_converted_file_object
|
return temporary_converted_file_object
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
try:
|
||||||
|
from yaml import CSafeLoader as SafeLoader
|
||||||
|
except ImportError:
|
||||||
|
from yaml import SafeLoader
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from mayan.apps.common.serialization import yaml_load
|
|
||||||
|
|
||||||
from .models import Transformation
|
from .models import Transformation
|
||||||
|
|
||||||
|
|
||||||
@@ -18,7 +21,7 @@ class TransformationForm(forms.ModelForm):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
try:
|
try:
|
||||||
yaml_load(stream=self.cleaned_data['arguments'])
|
yaml.load(stream=self.cleaned_data['arguments'], Loader=SafeLoader)
|
||||||
except yaml.YAMLError:
|
except yaml.YAMLError:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_(
|
_(
|
||||||
|
|||||||
@@ -2,11 +2,16 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
try:
|
||||||
|
from yaml import CSafeLoader as SafeLoader, CSafeDumper as SafeDumper
|
||||||
|
except ImportError:
|
||||||
|
from yaml import SafeLoader, SafeDumper
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
|
|
||||||
from mayan.apps.common.serialization import yaml_dump, yaml_load
|
|
||||||
|
|
||||||
from .transformations import BaseTransformation
|
from .transformations import BaseTransformation
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -18,8 +23,8 @@ class TransformationManager(models.Manager):
|
|||||||
|
|
||||||
self.create(
|
self.create(
|
||||||
content_type=content_type, object_id=obj.pk,
|
content_type=content_type, object_id=obj.pk,
|
||||||
name=transformation.name, arguments=yaml_dump(
|
name=transformation.name, arguments=yaml.dump(
|
||||||
data=arguments
|
data=arguments, Dumper=SafeDumper
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -91,8 +96,9 @@ class TransformationManager(models.Manager):
|
|||||||
# Some transformations don't require arguments
|
# Some transformations don't require arguments
|
||||||
# return an empty dictionary as ** doesn't allow None
|
# return an empty dictionary as ** doesn't allow None
|
||||||
if transformation.arguments:
|
if transformation.arguments:
|
||||||
kwargs = yaml_load(
|
kwargs = yaml.load(
|
||||||
stream=transformation.arguments,
|
stream=transformation.arguments,
|
||||||
|
Loader=SafeLoader
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from mayan.apps.smart_settings.classes import Namespace
|
from mayan.apps.smart_settings import Namespace
|
||||||
|
|
||||||
from .literals import (
|
from .literals import (
|
||||||
DEFAULT_LIBREOFFICE_PATH, DEFAULT_PDFTOPPM_DPI, DEFAULT_PDFTOPPM_FORMAT,
|
DEFAULT_LIBREOFFICE_PATH, DEFAULT_PDFTOPPM_DPI, DEFAULT_PDFTOPPM_FORMAT,
|
||||||
@@ -16,15 +16,22 @@ setting_graphics_backend = namespace.add_setting(
|
|||||||
help_text=_('Graphics conversion backend to use.'),
|
help_text=_('Graphics conversion backend to use.'),
|
||||||
global_name='CONVERTER_GRAPHICS_BACKEND',
|
global_name='CONVERTER_GRAPHICS_BACKEND',
|
||||||
)
|
)
|
||||||
setting_graphics_backend_arguments = namespace.add_setting(
|
setting_graphics_backend_config = namespace.add_setting(
|
||||||
default={
|
default='''
|
||||||
'libreoffice_path': DEFAULT_LIBREOFFICE_PATH,
|
{{
|
||||||
'pdftoppm_dpi': DEFAULT_PDFTOPPM_DPI,
|
libreoffice_path: {},
|
||||||
'pdftoppm_format': DEFAULT_PDFTOPPM_FORMAT,
|
pdftoppm_dpi: {},
|
||||||
'pdftoppm_path': DEFAULT_PDFTOPPM_PATH,
|
pdftoppm_format: {},
|
||||||
'pdfinfo_path': DEFAULT_PDFINFO_PATH,
|
pdftoppm_path: {},
|
||||||
'pillow_format': DEFAULT_PILLOW_FORMAT,
|
pdfinfo_path: {},
|
||||||
}, help_text=_(
|
pillow_format: {}
|
||||||
|
|
||||||
|
}}
|
||||||
|
'''.replace('\n', '').format(
|
||||||
|
DEFAULT_LIBREOFFICE_PATH, DEFAULT_PDFTOPPM_DPI,
|
||||||
|
DEFAULT_PDFTOPPM_FORMAT, DEFAULT_PDFTOPPM_PATH, DEFAULT_PDFINFO_PATH,
|
||||||
|
DEFAULT_PILLOW_FORMAT
|
||||||
|
), help_text=_(
|
||||||
'Configuration options for the graphics conversion backend.'
|
'Configuration options for the graphics conversion backend.'
|
||||||
), global_name='CONVERTER_GRAPHICS_BACKEND_ARGUMENTS'
|
), global_name='CONVERTER_GRAPHICS_BACKEND_CONFIG', quoted=True
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ class TransformationTestCase(GenericDocumentTestCase):
|
|||||||
arguments={'top': '10'}
|
arguments={'top': '10'}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(document_page.generate_image())
|
self.assertTrue(document_page.generate_image().startswith('page'))
|
||||||
|
|
||||||
def test_crop_transformation_invalid_arguments(self):
|
def test_crop_transformation_invalid_arguments(self):
|
||||||
self._silence_logger(name='mayan.apps.converter.managers')
|
self._silence_logger(name='mayan.apps.converter.managers')
|
||||||
@@ -132,7 +132,8 @@ class TransformationTestCase(GenericDocumentTestCase):
|
|||||||
obj=document_page, transformation=TransformationCrop,
|
obj=document_page, transformation=TransformationCrop,
|
||||||
arguments={'top': 'x', 'left': '-'}
|
arguments={'top': 'x', 'left': '-'}
|
||||||
)
|
)
|
||||||
self.assertTrue(document_page.generate_image())
|
|
||||||
|
self.assertTrue(document_page.generate_image().startswith('page'))
|
||||||
|
|
||||||
def test_crop_transformation_non_valid_range_arguments(self):
|
def test_crop_transformation_non_valid_range_arguments(self):
|
||||||
self._silence_logger(name='mayan.apps.converter.managers')
|
self._silence_logger(name='mayan.apps.converter.managers')
|
||||||
@@ -144,7 +145,7 @@ class TransformationTestCase(GenericDocumentTestCase):
|
|||||||
arguments={'top': '-1000', 'bottom': '100000000'}
|
arguments={'top': '-1000', 'bottom': '100000000'}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(document_page.generate_image())
|
self.assertTrue(document_page.generate_image().startswith('page'))
|
||||||
|
|
||||||
def test_crop_transformation_overlapping_ranges_arguments(self):
|
def test_crop_transformation_overlapping_ranges_arguments(self):
|
||||||
self._silence_logger(name='mayan.apps.converter.managers')
|
self._silence_logger(name='mayan.apps.converter.managers')
|
||||||
@@ -161,7 +162,7 @@ class TransformationTestCase(GenericDocumentTestCase):
|
|||||||
arguments={'left': '1000', 'right': '10000'}
|
arguments={'left': '1000', 'right': '10000'}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(document_page.generate_image())
|
self.assertTrue(document_page.generate_image().startswith('page'))
|
||||||
|
|
||||||
def test_lineart_transformations(self):
|
def test_lineart_transformations(self):
|
||||||
document_page = self.test_document.pages.first()
|
document_page = self.test_document.pages.first()
|
||||||
@@ -171,7 +172,7 @@ class TransformationTestCase(GenericDocumentTestCase):
|
|||||||
arguments={}
|
arguments={}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(document_page.generate_image())
|
self.assertTrue(document_page.generate_image().startswith('page'))
|
||||||
|
|
||||||
def test_rotate_transformations(self):
|
def test_rotate_transformations(self):
|
||||||
document_page = self.test_document.pages.first()
|
document_page = self.test_document.pages.first()
|
||||||
@@ -181,18 +182,18 @@ class TransformationTestCase(GenericDocumentTestCase):
|
|||||||
arguments={}
|
arguments={}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(document_page.generate_image())
|
self.assertTrue(document_page.generate_image().startswith('page'))
|
||||||
|
|
||||||
Transformation.objects.add_to_object(
|
Transformation.objects.add_to_object(
|
||||||
obj=document_page, transformation=TransformationRotate180,
|
obj=document_page, transformation=TransformationRotate180,
|
||||||
arguments={}
|
arguments={}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(document_page.generate_image())
|
self.assertTrue(document_page.generate_image().startswith('page'))
|
||||||
|
|
||||||
Transformation.objects.add_to_object(
|
Transformation.objects.add_to_object(
|
||||||
obj=document_page, transformation=TransformationRotate270,
|
obj=document_page, transformation=TransformationRotate270,
|
||||||
arguments={}
|
arguments={}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(document_page.generate_image())
|
self.assertTrue(document_page.generate_image().startswith('page'))
|
||||||
|
|||||||
@@ -308,6 +308,12 @@ class TransformationDrawRectanglePercent(BaseTransformation):
|
|||||||
if bottom > 100:
|
if bottom > 100:
|
||||||
bottom = 100
|
bottom = 100
|
||||||
|
|
||||||
|
#if left > right:
|
||||||
|
# left, right = right, left
|
||||||
|
|
||||||
|
#if top > bottom:
|
||||||
|
# top, bottom = bottom, top
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'left: %f, top: %f, right: %f, bottom: %f', left, top, right,
|
'left: %f, top: %f, right: %f, bottom: %f', left, top, right,
|
||||||
bottom
|
bottom
|
||||||
@@ -519,9 +525,7 @@ class TransformationZoom(BaseTransformation):
|
|||||||
|
|
||||||
BaseTransformation.register(transformation=TransformationCrop)
|
BaseTransformation.register(transformation=TransformationCrop)
|
||||||
BaseTransformation.register(transformation=TransformationDrawRectangle)
|
BaseTransformation.register(transformation=TransformationDrawRectangle)
|
||||||
BaseTransformation.register(
|
BaseTransformation.register(transformation=TransformationDrawRectanglePercent)
|
||||||
transformation=TransformationDrawRectanglePercent
|
|
||||||
)
|
|
||||||
BaseTransformation.register(transformation=TransformationFlip)
|
BaseTransformation.register(transformation=TransformationFlip)
|
||||||
BaseTransformation.register(transformation=TransformationGaussianBlur)
|
BaseTransformation.register(transformation=TransformationGaussianBlur)
|
||||||
BaseTransformation.register(transformation=TransformationLineArt)
|
BaseTransformation.register(transformation=TransformationLineArt)
|
||||||
|
|||||||
@@ -9,19 +9,19 @@ from .views import (
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(
|
url(
|
||||||
regex=r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/transformations/$',
|
regex=r'^create_for/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/$',
|
||||||
view=TransformationListView.as_view(), name='transformation_list'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
regex=r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/transformations/create/$',
|
|
||||||
view=TransformationCreateView.as_view(), name='transformation_create'
|
view=TransformationCreateView.as_view(), name='transformation_create'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^transformations/(?P<pk>\d+)/delete/$', view=TransformationDeleteView.as_view(),
|
regex=r'^list_for/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/$',
|
||||||
|
view=TransformationListView.as_view(), name='transformation_list'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
regex=r'^delete/(?P<pk>\d+)/$', view=TransformationDeleteView.as_view(),
|
||||||
name='transformation_delete'
|
name='transformation_delete'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^transformations/(?P<pk>\d+)/edit/$', view=TransformationEditView.as_view(),
|
regex=r'^edit/(?P<pk>\d+)/$', view=TransformationEditView.as_view(),
|
||||||
name='transformation_edit'
|
name='transformation_edit'
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
try:
|
||||||
|
from yaml import CSafeLoader as SafeLoader
|
||||||
|
except ImportError:
|
||||||
|
from yaml import SafeLoader
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.deconstruct import deconstructible
|
from django.utils.deconstruct import deconstructible
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from mayan.apps.common.serialization import yaml_load
|
|
||||||
|
|
||||||
|
|
||||||
@deconstructible
|
@deconstructible
|
||||||
class YAMLValidator(object):
|
class YAMLValidator(object):
|
||||||
@@ -17,7 +20,7 @@ class YAMLValidator(object):
|
|||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
try:
|
try:
|
||||||
yaml_load(stream=value)
|
yaml.load(stream=value, Loader=SafeLoader)
|
||||||
except yaml.error.YAMLError:
|
except yaml.error.YAMLError:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('Enter a valid YAML value.'),
|
_('Enter a valid YAML value.'),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib.humanize.templatetags.humanize import intcomma
|
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
|
|
||||||
|
|
||||||
@@ -86,8 +85,7 @@ class DashboardWidgetNumeric(BaseDashboardWidget):
|
|||||||
|
|
||||||
def get_context(self):
|
def get_context(self):
|
||||||
return {
|
return {
|
||||||
'count': intcomma(value=self.count),
|
'count': self.count,
|
||||||
'count_raw': self.count,
|
|
||||||
'icon_class': self.icon_class,
|
'icon_class': self.icon_class,
|
||||||
'label': self.label,
|
'label': self.label,
|
||||||
'link': self.link,
|
'link': self.link,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% load appearance_tags %}
|
|
||||||
|
|
||||||
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-3 match-height">
|
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-3 match-height">
|
||||||
<div class="panel panel-secondary dashboard-widget">
|
<div class="panel panel-secondary dashboard-widget">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
@@ -11,7 +9,7 @@
|
|||||||
<i class="dashboard-widget-icon {{ icon }}"></i>
|
<i class="dashboard-widget-icon {{ icon }}"></i>
|
||||||
{% elif icon_class %}
|
{% elif icon_class %}
|
||||||
<div class="dashboard-widget-icon">
|
<div class="dashboard-widget-icon">
|
||||||
{% appearance_icon_render icon_class enable_shadow=True %}
|
{{ icon_class.render }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user