Compare commits

..

28 Commits

Author SHA1 Message Date
Roberto Rosario
754b84b4d7 Merge branch 'versions/minor' into features/workflow_context 2019-07-05 21:38:00 -04:00
Roberto Rosario
572690e2bc Finish workflow context implementation
Improve workflow instance detail view.
Add workflow transition field widget support.
Fix workflow transition field required support.
Update tests.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-01 09:55:58 -04:00
Roberto Rosario
303e34299a Add a JSON and YAML validator to the common app
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-01 09:41:45 -04:00
Roberto Rosario
c628de9ede Improve appearance of the object error list view
Add icon to the object error list link.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-01 09:41:06 -04:00
Roberto Rosario
e73be6bbab Don't error out if the settings are set to blank
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-01 01:12:31 -04:00
Roberto Rosario
c9fd8b02e3 Add field type selection
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-01 01:12:02 -04:00
Roberto Rosario
e1a63064dc Proof of concept of the workflow instance context
Add support for workflow instance JSON context.
Add support for two step workflow transition.
Add support for dynamic form creation for transition execution.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-30 09:51:22 -04:00
Roberto Rosario
42db8255d1 Merge branch 'versions/minor' into features/workflow_context
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-29 20:35:25 -04:00
Roberto Rosario
14d45cbe90 Use polylines for the edge splines
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 15:48:44 -04:00
Roberto Rosario
75be11bc96 Hightlight initial state
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 15:35:33 -04:00
Roberto Rosario
ebf29d0eed Add actions to workflow preview
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 15:35:27 -04:00
Roberto Rosario
a391d27b44 Add transition form comment help text
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 14:33:37 -04:00
Roberto Rosario
753c9b8b4b Merge branch 'versions/minor' into features/workflow_context 2019-06-28 14:08:58 -04:00
Roberto Rosario
744bfefa5c Add workflow email action template support
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 12:10:31 -04:00
Roberto Rosario
850fb16c8c Add automatic execution test
Add test for automatic email action execution on document upload.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 11:51:21 -04:00
Roberto Rosario
72ba805fbb Add test case database connection check
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 11:35:58 -04:00
Roberto Rosario
3d7b40f029 Add email action tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 09:54:48 -04:00
Roberto Rosario
2039a9f13b Merge branch 'clients/bc' into features/workflow_email_action 2019-06-27 08:45:27 -04:00
Roberto Rosario
bb8f12dd7a Update CHANGES file
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 08:40:43 -04:00
Roberto Rosario
40ab1f3665 [FIX] Remove tag create document registration
Make no sense to have the tag create event register to existing tags.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 08:39:48 -04:00
Roberto Rosario
fdef757fd0 Add redactions app JavaScript dependencies
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 08:22:53 -04:00
Roberto Rosario
3608ee1141 Remove included cropper.js files
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 08:17:50 -04:00
Roberto Rosario
7fb3d61dff [Fix] Change to relative imports
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 07:03:55 -04:00
Roberto Rosario
e9aa11673b Initial commit of the workflow mail action
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 07:03:31 -04:00
Roberto Rosario
03a7aa5daf Add missing migrations
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 15:04:30 -04:00
Roberto Rosario
755f20c5c4 Fix importer logging
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 14:20:00 -04:00
Roberto Rosario
64772e2e90 Update changes file
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 14:18:29 -04:00
Roberto Rosario
75a4a426e0 Remove duplicated trashed document preview
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 14:18:11 -04:00
480 changed files with 6774 additions and 29753 deletions

View File

@@ -115,12 +115,6 @@ source_lang = en
source_file = mayan/apps/events/locale/en/LC_MESSAGES/django.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]
file_filter = mayan/apps/file_metadata/locale/<lang>/LC_MESSAGES/django.po
source_lang = en
@@ -228,10 +222,3 @@ file_filter = mayan/apps/user_management/locale/<lang>/LC_MESSAGES/django.po
source_lang = en
source_file = mayan/apps/user_management/locale/en/LC_MESSAGES/django.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

View File

@@ -8,3 +8,7 @@
- Improve source column exclusion. Improve for model subclasses in partial querysets.
- Add sortable index instance label column.
- 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.

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@ clean-pyc: ## Remove Python artifacts.
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -R -f {} +
# Testing
@@ -234,10 +234,10 @@ generate-requirements: ## Generate all requirements files from the project deped
# Dev 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.
./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.
./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
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 run -d --name mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=True -e MYSQL_DATABASE=mayan_edms mysql

View 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

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -13,12 +13,11 @@ APP_LIST = (
'checkouts', 'common', 'converter', 'dashboards', 'dependencies',
'django_gpg', 'document_comments', 'document_indexing',
'document_parsing', 'document_signatures', 'document_states',
'documents', 'dynamic_search', 'events', 'file_caching',
'file_metadata', 'linking', 'lock_manager', 'mailer',
'mayan_statistics', 'metadata', 'mirroring', 'motd', 'navigation',
'ocr', 'permissions', 'platform', 'rest_api', 'smart_settings',
'sources', 'storage', 'tags', 'task_manager', 'user_management',
'weblinks'
'documents', 'dynamic_search', 'events', 'file_metadata', 'linking',
'lock_manager', 'mayan_statistics', 'mailer', 'metadata', 'mirroring',
'motd', 'navigation', 'ocr', 'permissions', 'platform', 'rest_api',
'smart_settings', 'sources', 'storage', 'tags', 'task_manager',
'user_management'
)
LANGUAGE_LIST = (

View 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

View File

@@ -4,7 +4,7 @@
# 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"
@@ -22,7 +22,6 @@ RUN set -x \
&& DEBIAN_FRONTEND=noninteractive \
apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
exiftool \
ghostscript \
gpgv \
@@ -30,11 +29,11 @@ apt-get update \
graphviz \
libfuse2 \
libmagic1 \
libmariadb3 \
libmariadbclient18 \
libreoffice \
libpq5 \
poppler-utils \
python3-distutils \
redis-server \
sane-utils \
sudo \
supervisor \
@@ -53,20 +52,22 @@ apt-get update \
&& if [ "$(uname -m)" = "armv7l" ]; then \
ln -s /usr/lib/arm-linux-gnueabihf/libz.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
# only the build artifact is carried over to the next image.
# BUILDER_IMAGE - This image buildS the Python package and is discarded afterwards
####
# Reuse image
FROM BASE_IMAGE as BUILDER_IMAGE
# Python libraries caching
ARG PIP_INDEX_URL
ARG PIP_TRUSTED_HOST
WORKDIR /src
# Copy the source files needed to build the Python package
@@ -95,40 +96,39 @@ apt-get install -y --no-install-recommends \
libssl-dev \
g++ \
gcc \
python3-dev \
python3-venv \
python-dev \
python-virtualenv \
&& mkdir -p "${PROJECT_INSTALL_DIR}" \
&& chown -R mayan:mayan "${PROJECT_INSTALL_DIR}" \
&& chown -R mayan:mayan /src
USER mayan
RUN python3 -m venv "${PROJECT_INSTALL_DIR}" \
RUN python -m virtualenv "${PROJECT_INSTALL_DIR}" \
&& . "${PROJECT_INSTALL_DIR}/bin/activate" \
&& pip install --no-cache-dir \
librabbitmq==2.0.0 \
mysqlclient==1.4.2.post1 \
psycopg2==2.8.3 \
redis==3.2.1 \
flower==0.9.3 \
&& pip install --no-cache-dir --no-use-pep517 \
librabbitmq==1.6.1 \
mysql-python==1.2.5 \
psycopg2==2.7.3.2 \
redis==2.10.6 \
# psutil is needed by ARM builds otherwise gevent and gunicorn fail to start
&& 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 \
; fi \
# 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
&& python3 setup.py sdist \
&& python setup.py sdist \
# 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
&& mayan-edms.py installdependencies \
&& mayan-edms.py installjavascript \
&& MAYAN_STATIC_ROOT=${PROJECT_INSTALL_DIR}/static mayan-edms.py preparestatic --link --noinput
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
@@ -144,7 +144,7 @@ VOLUME ["/var/lib/mayan"]
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 8000
CMD ["run_all"]
CMD ["mayan"]
RUN ${PROJECT_INSTALL_DIR}/bin/mayan-edms.py platformtemplate supervisord_docker > /etc/supervisor/conf.d/mayan.conf \
&& apt-get clean autoclean \

View File

@@ -1,9 +1,4 @@
HOST_IP = `/sbin/ip route|awk '/docker0/ { print $$9 }'`
APT_PROXY ?= $(HOST_IP):3142
PIP_INDEX_URL ?= http://$(HOST_IP):3141/root/pypi/+simple/
PIP_TRUSTED_HOST ?= $(HOST_IP)
APT_PROXY ?= `/sbin/ip route|awk '/docker0/ { print $$9 }'`:3142
IMAGE_VERSION ?= `cat docker/rootfs/version`
CONSOLE_COLUMNS ?= `echo $$(tput cols)`
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-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 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: docker-build-with-proxy
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

View 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

View File

@@ -1,130 +1,58 @@
version: '3.7'
version: '2.1'
networks:
mayan-bridge:
driver: bridge
volumes:
broker:
driver: local
app:
driver: local
db:
driver: local
results:
driver: local
services:
app:
build:
context: ..
dockerfile: ./docker/Dockerfile
depends_on:
- postgresql
- redis
# 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
broker:
container_name: mayan-edms-broker
image: healthcheck/rabbitmq
environment:
RABBITMQ_DEFAULT_USER: mayan
RABBITMQ_DEFAULT_PASS: mayan
RABBITMQ_DEFAULT_VHOST: mayan
volumes:
- /docker-volumes/mayan-edms/media:/var/lib/mayan
postgresql:
- broker:/var/lib/rabbitmq
results:
container_name: mayan-edms-results
image: healthcheck/redis
volumes:
- results:/data
db:
container_name: mayan-edms-db
image: healthcheck/postgres
environment:
POSTGRES_DB: mayan
POSTGRES_PASSWORD: mayandbpass
POSTGRES_PASSWORD: mayan-password
POSTGRES_USER: mayan
image: postgres:9.6
networks:
- mayan-bridge
restart: unless-stopped
volumes:
- /docker-volumes/mayan-edms/postgres:/var/lib/postgresql/data
redis:
command:
- redis-server
- --databases
- "2"
- --maxmemory-policy
- allkeys-lru
- --save
- ""
image: redis:5.0
networks:
- mayan-bridge
restart: unless-stopped
# Optional services
# celery_flower:
# command:
# - run_celery
# - flower
# depends_on:
# - 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
- db:/var/lib/postgresql/data
mayan-edms:
container_name: mayan-edms-app
image: mayanedms/mayanedms:latest
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:8000"
volumes:
- app:/var/lib/mayan

View File

@@ -1,7 +1,4 @@
#!/bin/bash
# Use bash and not sh to support argument slicing "${@:2}"
# sh defaults to dash instead of bash.
#!/bin/sh
set -e
echo "mayan: starting entrypoint.sh"
@@ -9,15 +6,19 @@ INSTALL_FLAG=/var/lib/mayan/system/SECRET_KEY
CONCURRENCY_ARGUMENT=--concurrency=
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_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_PYTHON_BIN_DIR=/opt/mayan-edms/bin/
export MAYAN_MEDIA_ROOT=/var/lib/mayan
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_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_STATIC_ROOT=${MAYAN_INSTALL_DIR}/static
MAYAN_WORKER_FAST_CONCURRENCY=${MAYAN_WORKER_FAST_CONCURRENCY:-0}
MAYAN_WORKER_MEDIUM_CONCURRENCY=${MAYAN_WORKER_MEDIUM_CONCURRENCY:-0}
MAYAN_WORKER_SLOW_CONCURRENCY=${MAYAN_WORKER_SLOW_CONCURRENCY:-0}
MAYAN_WORKER_FAST_CONCURRENCY=${MAYAN_WORKER_FAST_CONCURRENCY:-1}
MAYAN_WORKER_MEDIUM_CONCURRENCY=${MAYAN_WORKER_MEDIUM_CONCURRENCY:-1}
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
MAYAN_WORKER_FAST_CONCURRENCY=
@@ -50,9 +55,11 @@ else
fi
export MAYAN_WORKER_SLOW_CONCURRENCY
# Allow importing of user setting modules
export CELERY_ALWAYS_EAGER=False
export PYTHONPATH=$PYTHONPATH:$MAYAN_MEDIA_ROOT
chown mayan:mayan /var/lib/mayan -R
apt_get_install() {
apt-get -q update
apt-get install -y --force-yes --no-install-recommends --auto-remove "$@"
@@ -60,9 +67,9 @@ apt_get_install() {
rm -rf /var/lib/apt/lists/*
}
initialsetup() {
echo "mayan: initialsetup()"
su mayan -c "${MAYAN_BIN} initialsetup --force --no-dependencies"
initialize() {
echo "mayan: initialize()"
su mayan -c "${MAYAN_BIN} initialsetup --force --no-javascript"
}
os_package_installs() {
@@ -79,71 +86,43 @@ pip_installs() {
fi
}
run_all() {
start() {
echo "mayan: start()"
rm -rf /var/run/supervisor.sock
exec /usr/bin/supervisord -nc /etc/supervisor/supervisord.conf
}
performupgrade() {
echo "mayan: performupgrade()"
su mayan -c "${MAYAN_BIN} performupgrade --no-dependencies"
}
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}}
upgrade() {
echo "mayan: upgrade()"
su mayan -c "${MAYAN_BIN} performupgrade --no-javascript"
}
os_package_installs || true
pip_installs || true
chown mayan:mayan /var/lib/mayan -R
case "$1" in
run_initialsetup)
initialsetup
;;
mayan) # Check if this is a new install, otherwise try to upgrade the existing
# installation on subsequent starts
if [ ! -f $INSTALL_FLAG ]; then
initialize
else
upgrade
fi
start
;;
run_performupgrade)
performupgrade
;;
run-tests) # Check if this is a new install, otherwise try to upgrade the existing
# installation on subsequent starts
if [ ! -f $INSTALL_FLAG ]; then
initialize
else
upgrade
fi
run-tests.sh
;;
run_all)
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 "$@"
;;
*) su mayan -c "$@";
;;
esac

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
3.2.6
3.2.5

View File

@@ -9,32 +9,24 @@ volumes:
services:
db:
image: postgres
environment:
POSTGRES_DB: mayan
POSTGRES_PASSWORD: mayandbpass
POSTGRES_PASSWORD: mayan-password
POSTGRES_USER: mayan
image: postgres
volumes:
- db:/var/lib/postgresql/data
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
ports:
- 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:
- app:/var/lib/mayan
redis:
command:
- redis-server
- --databases
- "2"
- --maxmemory-policy
- allkeys-lru
- --save
- ""
image: redis:5.0

View File

@@ -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'}}" \
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
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 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'}}" \
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
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
@@ -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)::
MAYAN_CELERY_BROKER_URL="redis://127.0.0.1:6379/0",
MAYAN_BROKER_URL="redis://127.0.0.1:6379/0",
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)::

View File

@@ -49,7 +49,12 @@ Finally create and run a Mayan EDMS container::
--name mayan-edms \
--restart=always \
-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 \
mayanedms/mayanedms:<version>
@@ -103,7 +108,12 @@ instead of the IP address of the Docker host (``172.17.0.1``)::
--network=mayan \
--restart=always \
-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 \
mayanedms/mayanedms:<version>
@@ -127,14 +137,102 @@ To start the container again::
Environment Variables
---------------------
The common set of settings can also be modified via environment variables when
using the Docker image. In addition to the common set of settings, some Docker
image specific environment variables are available.
The Mayan EDMS image can be configure via environment variables.
``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``
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``
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
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.
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:
Accessing outside data
@@ -353,7 +442,6 @@ These are:
Nightly images
==============
The continuous integration pipeline used for testing development builds also
produces a resulting Docker image. These are build automatically and their
stability is not guaranteed. They should never be used in production.

View File

@@ -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
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
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
calculation, these are stored for a while so that whoever requested the

View File

@@ -15,8 +15,7 @@ The current document sources supported are:
- IMAP email - Same as the ``POP3`` email source but for email accounts using
the ``IMAP`` protocol.
- 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
file is completed, the file is removed from source folder.
Any file in the watch folder is automatically uploaded.
- Staging folder - Folder where networked attached scanned can save image
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

View File

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

View File

@@ -8,104 +8,11 @@ Changes
-------
- 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
--------
- Database conversion. Reason for removal. The database conversions support
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
- None
Upgrading from a previous version
@@ -116,11 +23,11 @@ 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
$ curl https://gitlab.com/mayan-edms/mayan-edms/raw/master/removals.txt | pip uninstall -r /dev/stdin
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.
@@ -130,19 +37,19 @@ Using Git
If you installed Mayan EDMS by cloning the Git repository issue the commands::
git reset --hard HEAD
git pull
$ 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
$ pip uninstall -y -r removals.txt
Next upgrade/add the new requirements::
pip install --upgrade -r requirements.txt
$ pip install --upgrade -r requirements.txt
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
recommended layout::
sudo MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
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
@@ -170,11 +78,11 @@ generator missed::
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::
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.
@@ -182,26 +90,12 @@ The upgrade procedure is now complete.
Backward incompatible changes
-----------------------------
- Update quoted settings to be unquoted:
- 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
- None
Bugs fixed or issues closed
---------------------------
- :gitlab-issue:`526` RuntimeWarning: Never call result.get() within a task!
- :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.
- :gitlab-issue:`XX`
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/

View File

@@ -21,7 +21,6 @@ versions of the documentation contain the release notes for any later releases.
:maxdepth: 1
3.3
3.2.6
3.2.5
3.2.4
3.2.3

View File

@@ -1,9 +1,9 @@
from __future__ import unicode_literals
__title__ = 'Mayan EDMS'
__version__ = '3.2.6'
__build__ = 0x030206
__build_string__ = 'v3.2.6-68-gab601f9180_Wed Jul 17 04:30:11 2019 -0400'
__version__ = '3.2.5'
__build__ = 0x030205
__build_string__ = 'v3.2.5_Fri Jul 5 16:39:17 2019 -0400'
__django_version__ = '1.11'
__author__ = 'Roberto Rosario'
__author_email__ = 'roberto.rosario@mayan-edms.com'

View File

@@ -12,7 +12,6 @@ logger = logging.getLogger(__name__)
class ModelPermission(object):
_functions = {}
_inheritances = {}
_manager_names = {}
_registry = {}
@classmethod
@@ -98,24 +97,6 @@ class ModelPermission(object):
def get_inheritance(cls, 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
def register_function(cls, model, function):
cls._functions[model] = function
@@ -123,7 +104,3 @@ class ModelPermission(object):
@classmethod
def register_inheritance(cls, model, related):
cls._inheritances[model] = related
@classmethod
def register_manager(cls, model, manager_name):
cls._manager_names[model] = manager_name

View File

@@ -200,26 +200,28 @@ class AccessControlListManager(models.Manager):
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
# for example the Document model when checking for access for a trashed
# document.
meta = getattr(obj, '_meta', 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:
manager = ModelPermission.get_manager(model=obj._meta.model)
if manager:
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:
# Default relationship betweens permissions is OR
# TODO: Add support for AND relationship

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
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

View File

@@ -12,7 +12,7 @@
}
body {
padding-top: 60px;
padding-top: 70px;
}
.navbar-brand {
@@ -98,10 +98,14 @@ hr {
min-height: 120px;
padding-bottom: 1px;
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;
}
.btn-block .fa {
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
}
.radio ul li {
list-style-type:none;
}
@@ -111,10 +115,14 @@ a i {
}
.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;
}
.dashboard-widget .panel-heading i {
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
}
.dashboard-widget-icon {
font-size: 200%;
}
@@ -208,22 +216,6 @@ a i {
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 */
@media (min-width:1200px) {
.container-fluid {
@@ -253,6 +245,14 @@ a i {
margin: auto;
}
.thin_border {
border: 1px solid black;
display: block;
margin-left: auto;
margin-right: auto;
}
.thin_border-thumbnail {
display: block;
max-width: 100%;
@@ -262,18 +262,10 @@ a i {
margin: auto;
}
/* Must go after .thin_border-thumbnail */
.thin_border {
border: 1px solid black;
display: inline;
margin-left: 0px;
margin-right: 0px;
}
#ajax-spinner {
position: fixed;
top: 16px;
left: 10px;
top: 12px;
right: 10px;
z-index: 9999;
width: 25px;
height: 25px;
@@ -339,7 +331,7 @@ a i {
.main {
padding-right: 0px;
padding-left: 0px;
margin-left: 210px;
/*margin-left: 210px;*/
}
}
@@ -421,139 +413,3 @@ a i {
.btn-list {
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;
}

View File

@@ -6,8 +6,7 @@ var MayanAppClass = MayanApp;
var partialNavigation = new PartialNavigation({
initialURL: initialURL,
disabledAnchorClasses: [
'btn-multi-item-action', 'disabled', 'pagination-disabled'
],
disabledAnchorClasses: ['disabled'],
excludeAnchorClasses: ['fancybox', 'new_window', 'non-ajax'],
formBeforeSerializeCallbacks: [MayanApp.MultiObjectFormProcess],
});

View File

@@ -17,47 +17,30 @@ class MayanApp {
// Class methods and variables
static countChecked() {
var checkCount = $('.check-all-slave:checked').length;
static MultiObjectFormProcess ($form, options) {
/*
* ajaxForm callback to add the external item checkboxes to the
* submitted form
*/
if (checkCount) {
$('#multi-item-title').hide();
$('#multi-item-actions').show();
} else {
$('#multi-item-title').show();
$('#multi-item-actions').hide();
if ($form.hasClass('form-multi-object-action')) {
// Turn form data into an object
var formArray = $form.serializeArray().reduce(function (obj, item) {
obj[item.name] = item.value;
return obj;
}, {});
// 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 () {
var uri = new URI(window.location.hash);
var uriFragment = uri.fragment();
@@ -172,18 +155,16 @@ class MayanApp {
var self = this;
this.setupAJAXSpinner();
this.setupAutoSubmit();
this.setupFormHotkeys();
this.setupFullHeightResizing();
this.setupItemsSelector();
MayanApp.setupMultiItemActions();
this.setupNavbarCollapse();
MayanApp.setupNavBarState();
this.setupNewWindowAnchor();
$.each(this.ajaxMenusOptions, function(index, value) {
value.app = self;
app.doRefreshAJAXMenu(value);
});
this.setupPanelSelection();
partialNavigation.initialize();
}
@@ -207,6 +188,14 @@ class MayanApp {
});
}
setupAutoSubmit () {
$('body').on('change', '.select-auto-submit', function () {
if ($(this).val()) {
$(this.form).trigger('submit');
}
});
}
setupFormHotkeys () {
$('body').on('keypress', '.form-hotkey-enter', function (e) {
if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) {
@@ -237,22 +226,9 @@ class MayanApp {
app.lastChecked = null;
$('body').on('click', '.check-all', function (event) {
var $this = $(this);
var checked = $(event.target).prop('checked');
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.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 () {
$('.scrollable').scrollview();
}

View File

@@ -136,9 +136,6 @@
},
{% endfor %}
];
$(function () {
$('[data-toggle="tooltip"]').tooltip();
})
</script>
{% block javascript %}{% endblock %}

View File

@@ -33,7 +33,7 @@
}
</script>
</head>
<body id="body-plain">
<body>
{% block content_plain %}{% endblock %}
<script src="{% static 'appearance/node_modules/jquery/dist/jquery.min.js' %}" type="text/javascript"></script>

View File

@@ -11,9 +11,41 @@
{% include 'appearance/no_results.html' %}
</div>
{% else %}
{% include "appearance/list_header.html" %}
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
<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>
<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"/>&nbsp;
{{ 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">
{% for object in object_list %}
<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="form-group">
<div class="checkbox">
<label for="id_indexes_0" style="cursor: auto;">
{% if links_multi_menus_results %}
<input class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" style="cursor: pointer;" type="checkbox" />
<label for="id_indexes_0">
{% if multi_item_actions %}
<input class="form-multi-object-action-checkbox check-all-slave checkbox" type="checkbox" name="pk_{{ object.pk }}" />
{% endif %}
<span style="color: white; word-break: break-all; overflow-wrap: break-word;">
@@ -36,7 +68,12 @@
{% else %}
{% navigation_get_source_columns source=object only_identifier=True as source_column %}
{% 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 %}
</span>
</label>
@@ -45,10 +82,11 @@
</div>
<div class="panel-body">
{% if not hide_columns %}
{% navigation_get_source_columns source=object exclude_identifier=True as 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 %}
{% endif %}
@@ -98,6 +136,7 @@
</div>
{% endfor %}
</div>
{% include 'pagination/pagination.html' %}
</div>
{% endif %}
</div>

View File

@@ -1,7 +1,6 @@
{% load i18n %}
{% load static %}
{% load appearance_tags %}
{% load common_tags %}
{% load navigation_tags %}
@@ -12,16 +11,44 @@
{% include 'appearance/no_results.html' %}
</div>
{% else %}
{% include "appearance/list_header.html" %}
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
<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>
<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">
<table class="table table-condensed table-striped">
<tbody>
{% if not hide_header %}
<tr>
{% if links_multi_menus_results %}
<th class="first"></th>
{% if multi_item_actions %}
<th class="first"><input class="checkbox check-all" type="checkbox" /></th>
{% endif %}
{% if not hide_object %}
@@ -31,40 +58,30 @@
{% if source_column %}
<th>
{% 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 icon_sort %}{{ icon_sort.render }}{% endif %}
{% endif %}
</a>
{% else %}
{{ source_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 %}
</th>
{% endif %}
{% endif %}
{% if not hide_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>
{% if source_column.is_sortable %}
<a href="{% navigation_get_sort_field_querystring column=source_column %}">{{ source_column.label }}</a>
{% if source_column.get_sort_field == sort_field %}
{% if column.is_sortable %}
<a href="{% navigation_get_sort_field_querystring column=column %}">{{ column.label }}
{% if column.get_sort_field == sort_field %}
{% if icon_sort %}{{ icon_sort.render }}{% endif %}
{% endif %}
</a>
{% else %}
{{ source_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>
{{ column.label }}
{% endif %}
</th>
{% endfor %}
@@ -82,9 +99,9 @@
{% for object in object_list %}
<tr>
{% if links_multi_menus_results %}
{% if multi_item_actions %}
<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>
{% endif %}
@@ -95,7 +112,11 @@
{% navigation_source_column_resolve column=source_column as column_value %}
{% if column_value %}
<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>
{% endif %}
{% endif %}
@@ -149,6 +170,7 @@
</tbody>
</table>
</div>
{% include 'pagination/pagination.html' %}
</div>
{% endif %}
</div>

View File

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

View File

@@ -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 }}">&lsaquo;&lsaquo;</a>
{% else %}
<a class="btn btn-default btn-sm disabled" href="#">&lsaquo;&lsaquo;</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 }}">&rsaquo;&rsaquo;</a>
{% else %}
<a class="btn btn-default btn-sm disabled" href="#">&rsaquo;&rsaquo;</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 %}

View File

@@ -3,11 +3,10 @@
{% load navigation_tags %}
{% load smart_settings_tags %}
{% spaceless %}
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container-fluid">
<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="icon-bar"></span>
<span class="icon-bar"></span>
@@ -15,10 +14,9 @@
</button>
<a class="navbar-brand" href="{% url home_view %}">{% smart_setting 'COMMON_PROJECT_TITLE' %}</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
{% navigation_resolve_menu name='topbar' as topbar_menus_results %}
<ul class="nav navbar-nav">
{% navigation_resolve_menu name='main' as topbar_menus_results %}
{% for tobpar_menu_result in topbar_menus_results %}
{% for link_group in tobpar_menu_result.link_groups %}
{% for link in link_group.links %}
@@ -36,8 +34,24 @@
{% 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>
</div>
</div>
</nav>
{% endspaceless %}

View File

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

View File

@@ -32,11 +32,8 @@
{% if appearance_type == 'plain' %}
{% block content_plain %}{% endblock %}
{% else %}
<div id="menu-topbar">
{% include 'appearance/menu_topbar.html' %}
</div>
<div id="menu-main">
{% include 'appearance/menu_main.html' %}
{% include 'appearance/main_menu.html' %}
</div>
<div class="main">
<div class="row zero-margin">
@@ -104,18 +101,11 @@
var app = new MayanApp({
ajaxMenusOptions: [
{
callback: MayanApp.updateNavbarState,
interval: 5000,
menuSelector: '#menu-main',
name: '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" %}'
},
]
});

View File

@@ -11,7 +11,7 @@
{% if page %}
{% 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 %}
<li><a href="?{{ page.querystring }}">{{ page }}</a></li>
{% endifequal %}

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
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

View File

@@ -17,7 +17,7 @@
{% motd %}
<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-heading">
<h3 class="panel-title">&nbsp;</h3>

View File

@@ -14,6 +14,7 @@ from django.utils.http import urlunquote_plus
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.smart_settings.classes import Namespace
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 ..settings import setting_maximum_session_length
@@ -261,7 +262,7 @@ class UserLoginTestCase(GenericViewTestCase):
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):
self._create_test_user()

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
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

View File

@@ -4,7 +4,7 @@
{% if autoadmin_properties.account %}
<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>
<div class="panel panel-primary">
<div class="panel-heading">

View File

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

View File

@@ -32,10 +32,7 @@ class Cabinet(MPTTModel):
blank=True, db_index=True, null=True, on_delete=models.CASCADE,
related_name='children', to='self'
)
label = models.CharField(
help_text=_('A short text used to identify the cabinet.'),
max_length=128, verbose_name=_('Label')
)
label = models.CharField(max_length=128, verbose_name=_('Label'))
documents = models.ManyToManyField(
blank=True, related_name='cabinets', to=Document,
verbose_name=_('Documents')

View File

@@ -12,62 +12,55 @@ from .views import (
CabinetDeleteView, CabinetDetailView, CabinetEditView, CabinetListView,
)
urlpatterns_cabinets = [
urlpatterns = [
url(
regex=r'^cabinets/$', view=CabinetListView.as_view(), name='cabinet_list'
regex=r'^list/$', view=CabinetListView.as_view(), name='cabinet_list'
),
url(
regex=r'^cabinets/create/$', view=CabinetCreateView.as_view(),
name='cabinet_create'
),
url(
regex=r'^cabinets/(?P<pk>\d+)/children/add/$', view=CabinetChildAddView.as_view(),
regex=r'^(?P<pk>\d+)/child/add/$', view=CabinetChildAddView.as_view(),
name='cabinet_child_add'
),
url(
regex=r'^cabinets/(?P<pk>\d+)/delete/$', view=CabinetDeleteView.as_view(),
name='cabinet_delete'
regex=r'^create/$', view=CabinetCreateView.as_view(),
name='cabinet_create'
),
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'
),
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'
),
]
urlpatterns_documents_cabinets = [
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'
),
url(
regex=r'^documents/multiple/cabinets/add/$',
regex=r'^document/multiple/cabinet/add/$',
view=DocumentAddToCabinetView.as_view(),
name='document_multiple_cabinet_add'
),
url(
regex=r'^documents/(?P<pk>\d+)/cabinets/remove/$',
regex=r'^document/(?P<pk>\d+)/cabinet/remove/$',
view=DocumentRemoveFromCabinetView.as_view(),
name='document_cabinet_remove'
),
url(
regex=r'^documents/multiple/cabinets/remove/$',
regex=r'^document/multiple/cabinet/remove/$',
view=DocumentRemoveFromCabinetView.as_view(),
name='multiple_document_cabinet_remove'
),
url(
regex=r'^documents/(?P<pk>\d+)/cabinets/$',
regex=r'^document/(?P<pk>\d+)/cabinet/list/$',
view=DocumentCabinetListView.as_view(), name='document_cabinet_list'
),
]
urlpatterns = []
urlpatterns.extend(urlpatterns_cabinets)
urlpatterns.extend(urlpatterns_documents_cabinets)
api_urls = [
url(
regex=r'^cabinets/(?P<pk>[0-9]+)/documents/(?P<document_pk>[0-9]+)/$',

View File

@@ -6,12 +6,9 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.classes import ModelPermission
from mayan.apps.common.apps import MayanAppConfig
from mayan.apps.common.menus import (
menu_facet, menu_main, menu_multi_item, menu_secondary
)
from mayan.apps.common.menus import menu_facet, menu_main, menu_secondary
from mayan.apps.dashboards.dashboards import dashboard_main
from mayan.apps.events.classes import ModelEventType
from mayan.apps.navigation.classes import SourceColumn
from .dashboard_widgets import DashboardWidgetTotalCheckouts
from .events import (
@@ -20,9 +17,8 @@ from .events import (
)
from .handlers import handler_check_new_version_creation
from .links import (
link_check_in_document, link_check_in_document_multiple,
link_check_out_document, link_check_out_document_multiple,
link_check_out_info, link_check_out_list
link_check_in_document, link_check_out_document, link_check_out_info,
link_check_out_list
)
from .methods import (
method_check_in, method_get_check_out_info, method_get_check_out_state,
@@ -47,8 +43,6 @@ class CheckoutsApp(MayanAppConfig):
def ready(self):
super(CheckoutsApp, self).ready()
CheckedOutDocument = self.get_model(model_name='CheckedOutDocument')
DocumentCheckout = self.get_model(model_name='DocumentCheckout')
Document = apps.get_model(
app_label='documents', model_name='Document'
)
@@ -82,22 +76,6 @@ class CheckoutsApp(MayanAppConfig):
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(
widget=DashboardWidgetTotalCheckouts, order=-1
@@ -107,22 +85,6 @@ class CheckoutsApp(MayanAppConfig):
links=(link_check_out_info,), sources=(Document,)
)
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(
links=(link_check_out_document, link_check_in_document),
sources=(

View File

@@ -38,26 +38,16 @@ link_check_out_document = Link(
args='object.pk', condition=is_not_checked_out,
icon_class=icon_check_out_document,
permissions=(permission_document_check_out,),
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'
text=_('Check out document'), view='checkouts:check_out_document',
)
link_check_in_document = Link(
args='object.pk', icon_class=icon_check_in_document,
condition=is_checked_out, permissions=(
permission_document_check_in, permission_document_check_in_override
), 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'
), text=_('Check in document'), view='checkouts:check_in_document',
)
link_check_out_info = Link(
args='resolved_object.pk', icon_class=icon_check_out_info, permissions=(
permission_document_check_out_detail_view,
), text=_('Check in/out'), view='checkouts:check_out_info'
), text=_('Check in/out'), view='checkouts:check_out_info',
)

View File

@@ -6,7 +6,6 @@ from django.apps import apps
from django.db import models, transaction
from django.utils.timezone import now
from mayan.apps.acls.models import AccessControlList
from mayan.apps.documents.models import Document
from .events import (
@@ -15,53 +14,10 @@ from .events import (
)
from .exceptions import DocumentNotCheckedOut
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__)
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):
def are_document_new_versions_allowed(self, document, user=None):
try:
@@ -71,6 +27,25 @@ class DocumentCheckoutManager(models.Manager):
else:
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):
for document in self.expired_check_outs():
document.check_in()
@@ -82,11 +57,7 @@ class DocumentCheckoutManager(models.Manager):
)
def checked_out_documents(self):
CheckedOutDocument = apps.get_model(
app_label='checkouts', model_name='CheckedOutDocument'
)
return CheckedOutDocument.objects.filter(
return Document.objects.filter(
pk__in=self.model.objects.values('document__id')
)
@@ -103,11 +74,7 @@ class DocumentCheckoutManager(models.Manager):
return STATE_CHECKED_IN
def expired_check_outs(self):
CheckedOutDocument = apps.get_model(
app_label='checkouts', model_name='CheckedOutDocument'
)
expired_list = CheckedOutDocument.objects.filter(
expired_list = Document.objects.filter(
pk__in=self.model.objects.filter(
expiration_datetime__lte=now()
).values_list('document__pk', flat=True)
@@ -116,6 +83,9 @@ class DocumentCheckoutManager(models.Manager):
return expired_list
def get_by_natural_key(self, document_natural_key):
Document = apps.get_model(
app_label='documents', model_name='Document'
)
try:
document = Document.objects.get_by_natural_key(document_natural_key)
except Document.DoesNotExist:

View File

@@ -8,7 +8,7 @@ def method_check_in(self, user=None):
app_label='checkouts', model_name='DocumentCheckout'
)
return DocumentCheckout.business_logic.check_in_document(
return DocumentCheckout.objects.check_in_document(
document=self, user=user
)

View File

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

View File

@@ -14,10 +14,7 @@ from mayan.apps.documents.models import Document
from .events import event_document_check_out
from .exceptions import DocumentAlreadyCheckedOut
from .managers import (
DocumentCheckoutBusinessLogicManager, DocumentCheckoutManager,
NewVersionBlockManager
)
from .managers import DocumentCheckoutManager, NewVersionBlockManager
logger = logging.getLogger(__name__)
@@ -52,7 +49,6 @@ class DocumentCheckout(models.Model):
)
objects = DocumentCheckoutManager()
business_logic = DocumentCheckoutBusinessLogicManager()
class Meta:
ordering = ('pk',)
@@ -85,13 +81,13 @@ class DocumentCheckout(models.Model):
natural_key.dependencies = ['documents.Document']
def save(self, *args, **kwargs):
is_new = not self.pk
if not is_new or self.document.is_checked_out():
new_checkout = not self.pk
if not new_checkout or self.document.is_checked_out():
raise DocumentAlreadyCheckedOut
with transaction.atomic():
result = super(DocumentCheckout, self).save(*args, **kwargs)
if is_new:
if new_checkout:
event_document_check_out.commit(
actor=self.user, target=self.document
)
@@ -123,24 +119,3 @@ class NewVersionBlock(models.Model):
def natural_key(self):
return self.document.natural_key()
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')

View File

@@ -4,19 +4,13 @@ import datetime
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
class DocumentCheckoutTestMixin(object):
_test_document_check_out_seconds = 0.1
def _check_out_test_document(self, document=None, user=None):
if not document:
document = self.test_document
def _check_out_test_document(self, user=None):
if not user:
user = self._test_case_user
@@ -25,61 +19,7 @@ class DocumentCheckoutTestMixin(object):
)
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,
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')

View File

@@ -65,7 +65,7 @@ class CheckoutsAPITestCase(DocumentCheckoutTestMixin, DocumentTestMixin, BaseAPI
force_text(self.test_document.uuid)
)
def _request_test_document_check_out_view(self):
def _request_document_checkout_view(self):
return self.post(
viewname='rest_api:checkout-document-list', data={
'document_pk': self.test_document.pk,
@@ -74,7 +74,7 @@ class CheckoutsAPITestCase(DocumentCheckoutTestMixin, DocumentTestMixin, BaseAPI
)
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(DocumentCheckout.objects.count(), 0)
@@ -82,7 +82,7 @@ class CheckoutsAPITestCase(DocumentCheckoutTestMixin, DocumentTestMixin, BaseAPI
def test_document_checkout_with_access(self):
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(

View File

@@ -7,7 +7,8 @@ from mayan.apps.documents.tests import GenericDocumentTestCase, DocumentTestMixi
from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH
from ..exceptions import (
DocumentAlreadyCheckedOut, NewDocumentVersionNotAllowed
DocumentAlreadyCheckedOut, DocumentNotCheckedOut,
NewDocumentVersionNotAllowed
)
from ..models import DocumentCheckout, NewVersionBlock
@@ -48,6 +49,10 @@ class DocumentCheckoutTestCase(DocumentCheckoutTestMixin, GenericDocumentTestCas
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):
self._check_out_test_document()

View File

@@ -1,5 +1,6 @@
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.tests import GenericDocumentViewTestCase
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
)
from .mixins import DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin
from .mixins import DocumentCheckoutTestMixin
class DocumentCheckoutViewTestCase(
DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin,
GenericDocumentViewTestCase
):
def test_document_check_in_get_view_no_permission(self):
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
class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentViewTestCase):
def _request_document_check_in_get_view(self):
return self.get(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
self.assertTrue(self.test_document.is_checked_out())
def test_document_check_in_get_view_with_access(self):
def test_check_in_document_get_view_no_permission(self):
self._check_out_test_document()
self.grant_access(
obj=self.test_document, permission=permission_document_check_in
)
response = self._request_test_document_check_in_get_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 test_document_check_in_post_view_no_permission(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):
def test_check_in_document_get_view_with_access(self):
self._check_out_test_document()
self.grant_access(
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.assertFalse(self.test_document.is_checked_out())
@@ -67,93 +79,24 @@ class DocumentCheckoutViewTestCase(
)
)
def test_document_multiple_check_in_post_view_no_permission(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])
response = self._request_test_document_multiple_check_in_post_view()
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 _request_document_checkout_view(self):
return self.post(
viewname='checkouts:check_out_document', kwargs={
'pk': self.test_document.pk
}, data={
'expiration_datetime_0': 2,
'expiration_datetime_1': TIME_DELTA_UNIT_DAYS,
'block_new_version': True
}
)
def test_document_multiple_check_in_post_view_with_document_0_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
)
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)
def test_check_out_document_view_no_permission(self):
response = self._request_document_checkout_view()
self.assertEqual(response.status_code, 403)
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(
obj=self.test_document, permission=permission_document_check_out
)
@@ -162,117 +105,28 @@ class DocumentCheckoutViewTestCase(
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.assertTrue(self.test_document.is_checked_out())
def test_document_multiple_check_out_post_view_no_permission(self):
# Upload second document
self.upload_document()
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
def _request_check_out_detail_view(self):
return self.get(
viewname='checkouts:check_out_info', kwargs={
'pk': self.test_document.pk
}
)
response = self._request_test_document_multiple_check_out_post_view()
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):
def test_checkout_detail_view_no_permission(self):
self._check_out_test_document()
response = self._request_test_document_check_out_detail_view()
response = self._request_check_out_detail_view()
self.assertNotContains(
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.grant_access(
@@ -280,12 +134,15 @@ class DocumentCheckoutViewTestCase(
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(
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.grant_access(
@@ -293,12 +150,12 @@ class DocumentCheckoutViewTestCase(
permission=permission_document_view
)
response = self._request_test_document_check_out_list_view()
response = self._request_check_out_list_view()
self.assertNotContains(
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.grant_access(
@@ -310,54 +167,12 @@ class DocumentCheckoutViewTestCase(
permission=permission_document_view
)
response = self._request_test_document_check_out_list_view()
response = self._request_check_out_list_view()
self.assertContains(
response=response, text=self.test_document.label, status_code=200
)
def test_document_check_in_forcefull_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_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):
def test_document_new_version_after_check_out(self):
"""
Gitlab issue #231
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
response.context.current_app = None
resolved_link = link_document_version_upload.resolve(
context=response.context
)
resolved_link = link_document_version_upload.resolve(context=response.context)
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())

View File

@@ -4,34 +4,25 @@ from django.conf.urls import url
from .api_views import APICheckedoutDocumentListView, APICheckedoutDocumentView
from .views import (
DocumentCheckinView, DocumentCheckoutDetailView, DocumentCheckoutView,
DocumentCheckoutListView
CheckoutDocumentView, CheckoutDetailView, CheckoutListView,
DocumentCheckinView
)
urlpatterns = [
url(
regex=r'^documents/$', view=DocumentCheckoutListView.as_view(),
name='check_out_list'
regex=r'^list/$', view=CheckoutListView.as_view(), name='check_out_list'
),
url(
regex=r'^documents/(?P<pk>\d+)/check_in/$', view=DocumentCheckinView.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(),
regex=r'^(?P<pk>\d+)/check/out/$', view=CheckoutDocumentView.as_view(),
name='check_out_document'
),
url(
regex=r'^documents/multiple/check_out/$',
name='check_out_document_multiple', view=DocumentCheckoutView.as_view()
regex=r'^(?P<pk>\d+)/check/in/$', view=DocumentCheckinView.as_view(),
name='check_in_document'
),
url(
regex=r'^documents/(?P<pk>\d+)/checkout/info/$',
view=DocumentCheckoutDetailView.as_view(), name='check_out_info'
regex=r'^(?P<pk>\d+)/check/info/$', view=CheckoutDetailView.as_view(),
name='check_out_info'
),
]

View File

@@ -1,16 +1,20 @@
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.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.common.generics import (
MultipleObjectConfirmActionView, MultipleObjectFormActionView,
SingleObjectDetailView
ConfirmView, SingleObjectCreateView, SingleObjectDetailView
)
from mayan.apps.common.utils import encapsulate
from mayan.apps.documents.models import Document
from mayan.apps.documents.views import DocumentListView
from .exceptions import DocumentAlreadyCheckedOut, DocumentNotCheckedOut
from .forms import DocumentCheckoutForm, DocumentCheckoutDefailForm
from .icons import icon_check_out_info
from .models import DocumentCheckout
@@ -20,124 +24,159 @@ from .permissions import (
)
class DocumentCheckinView(MultipleObjectConfirmActionView):
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.'
class DocumentCheckinView(ConfirmView):
def get_extra_context(self):
queryset = self.get_object_list()
document = self.get_object()
result = {
'title': ungettext(
singular='Check in %(count)d document',
plural='Check in %(count)d documents',
number=queryset.count()
) % {
'count': queryset.count(),
}
context = {
'object': document,
}
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Check in document: %s'
) % queryset.first()
}
)
if document.get_check_out_info().user != self.request.user:
context['title'] = _(
'You didn\'t originally checked out this document. '
'Forcefully check in the document: %s?'
) % document
else:
context['title'] = _('Check in the document: %s?') % document
return result
return context
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]}
def get_object(self):
return get_object_or_404(klass=Document, pk=self.kwargs['pk'])
def get_post_action_redirect(self):
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:
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):
# object_permission is None to disable restricting queryset mixin
# and restrict the queryset ourselves from two permissions
try:
document.check_in(user=self.request.user)
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(
permission=permission_document_check_in, queryset=source_queryset,
class CheckoutDocumentView(SingleObjectCreateView):
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
)
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):
queryset = self.get_object_list()
result = {
'title': ungettext(
singular='Checkout %(count)d document',
plural='Checkout %(count)d documents',
number=queryset.count()
) % {
'count': queryset.count(),
context = super(CheckoutListView, self).get_extra_context()
context.update(
{
'extra_columns': (
{
'name': _('User'),
'attribute': encapsulate(
lambda document: document.get_check_out_info().user.get_full_name() or document.get_check_out_info().user
)
},
{
'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
model = Document
object_permission = permission_document_check_out_detail_view
@@ -149,27 +188,3 @@ class DocumentCheckoutDetailView(SingleObjectDetailView):
'Check out details for document: %s'
) % 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

View File

@@ -32,8 +32,8 @@ class SplitTimeDeltaWidget(forms.widgets.MultiWidget):
return (None, None)
def value_from_datadict(self, querydict, files, name):
unit = querydict.get('{}_0'.format(name))
period = querydict.get('{}_1'.format(name))
unit = querydict.get('{}_1'.format(name))
period = querydict.get('{}_0'.format(name))
if not unit or not period:
return now()

View File

@@ -27,7 +27,9 @@ from .links import (
)
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 (
setting_auto_logging, setting_production_error_log_path,
setting_production_error_logging
@@ -95,10 +97,7 @@ class CommonApp(MayanAppConfig):
)
Template(
name='menu_main', template_name='appearance/menu_main.html'
)
Template(
name='menu_topbar', template_name='appearance/menu_topbar.html'
name='menu_main', template_name='appearance/main_menu.html'
)
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(
links=(link_object_error_list_clear,), sources=(
'common:object_error_list',

View File

@@ -61,9 +61,102 @@ PythonDependency(
SOFTWARE.
''', 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(
module=__name__, name='django-downloadview', version_string='==1.9'
)
PythonDependency(
module=__name__, name='django-environ', version_string='==0.4.5'
)
PythonDependency(
module=__name__, name='django-formtools', version_string='==2.1'
)
@@ -290,10 +383,6 @@ PythonDependency(
module=__name__, environment=environment_development, name='Werkzeug',
version_string='==0.15.4'
)
PythonDependency(
module=__name__, environment=environment_development, name='devpi-server',
version_string='==5.0.0'
)
PythonDependency(
environment=environment_development, module=__name__,
name='django-debug-toolbar', version_string='==1.11'

View File

@@ -2,7 +2,6 @@ from __future__ import unicode_literals
from django.http import QueryDict
from django.utils.encoding import force_bytes
from django.utils.six import PY3
class URL(object):
@@ -21,7 +20,9 @@ class URL(object):
def to_string(self):
if self._args.keys():
query = '?{}'.format(self._args.urlencode())
query = force_bytes(
'?{}'.format(self._args.urlencode())
)
else:
query = ''
@@ -30,9 +31,6 @@ class URL(object):
else:
path = ''
result = '{}{}'.format(path, query)
result = force_bytes('{}{}'.format(path, query))
if PY3:
return result
else:
return force_bytes(result)
return result

View File

@@ -41,9 +41,6 @@ icon_object_errors = Icon(
icon_object_error_list = Icon(
driver_name='fontawesome', symbol='exclamation-triangle'
)
icon_object_error_list_clear = Icon(
driver_name='fontawesome', symbol='times'
)
icon_ok = Icon(
driver_name='fontawesome', symbol='check'
)

View File

@@ -50,13 +50,12 @@ link_documentation = Link(
text=_('Documentation'), url='https://docs.mayan-edms.com'
)
link_object_error_list = Link(
icon_class_path='mayan.apps.common.icons.icon_object_error_list',
kwargs=get_kwargs_factory('resolved_object'),
icon_class_path='mayan.apps.common.icons.icon_object_error_list',
permissions=(permission_error_log_view,), text=_('Errors'),
view='common:object_error_list',
)
link_object_error_list_clear = Link(
icon_class_path='mayan.apps.common.icons.icon_object_error_list_clear',
kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_error_log_view,), text=_('Clear all'),
view='common:object_error_list_clear',

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

View File

@@ -28,8 +28,8 @@ class Command(management.BaseCommand):
)
parser.add_argument(
'--no-dependencies', action='store_true', dest='no_dependencies',
help='Don\'t download dependencies.',
'--no-javascript', action='store_true', dest='no_javascript',
help='Don\'t download the JavaScript dependencies.',
)
def initialize_system(self, force=False):
@@ -88,9 +88,9 @@ class Command(management.BaseCommand):
self.initialize_system(force=options.get('force', False))
pre_initial_setup.send(sender=self)
if not options.get('no_dependencies', False):
if not options.get('no_javascript', False):
management.call_command(
command_name='installdependencies', interactive=False
command_name='installjavascript', interactive=False
)
management.call_command(

View File

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

View File

@@ -11,8 +11,8 @@ class Command(management.BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
'--no-dependencies', action='store_true', dest='no_dependencies',
help='Don\'t download dependencies.',
'--no-javascript', action='store_true', dest='no_javascript',
help='Don\'t download the JavaScript dependencies.',
)
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(
command_name='installdependencies', interactive=False
command_name='installjavascript', interactive=False
)
try:

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.core import management
from django_celery_beat.models import IntervalSchedule, PeriodicTask
from djcelery.models import IntervalSchedule, PeriodicTask
class Command(management.BaseCommand):

View File

@@ -17,7 +17,6 @@ menu_object = Menu(label=_('Actions'), name='object')
menu_secondary = Menu(label=_('Secondary'), name='secondary')
menu_setup = Menu(name='setup')
menu_tools = Menu(name='tools')
menu_topbar = Menu(name='topbar')
menu_user = Menu(
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

View File

@@ -1,7 +1,6 @@
from __future__ import unicode_literals
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.http import Http404, HttpResponseRedirect
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.views.generic.detail import SingleObjectMixin
from mayan.apps.acls.classes import ModelPermission
from mayan.apps.acls.models import AccessControlList
from mayan.apps.permissions import Permission
@@ -19,28 +17,6 @@ from .literals import PK_LIST_SEPARATOR
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):
"""
Mixin to populate the extra data needed for delete views
@@ -127,15 +103,7 @@ class ExternalObjectMixin(object):
'get_external_object_queryset() method.'
)
queryset = self.external_object_queryset
if not queryset:
manager = ModelPermission.get_manager(
model=self.external_object_class
)
queryset = manager.all()
return queryset
return self.external_object_queryset or self.external_object_class.objects.all()
def get_external_object_queryset_filtered(self):
queryset = self.get_external_object_queryset()
@@ -150,20 +118,6 @@ class ExternalObjectMixin(object):
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):
"""
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):
return ungettext(
singular=self.success_message,
plural=self.success_message_plural,
number=count
self.success_message,
self.success_message_plural,
count
) % {
'count': count,
}
@@ -317,15 +271,14 @@ class ObjectActionMixin(object):
pass
except ActionError:
messages.error(
message=self.error_message % {'instance': instance},
request=self.request
self.request, self.error_message % {'instance': instance}
)
else:
self.action_count += 1
messages.success(
message=self.get_success_message(count=self.action_count),
request=self.request
self.request,
self.get_success_message(count=self.action_count)
)

View File

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

View File

@@ -6,10 +6,11 @@ from django.conf import settings
from django.utils.translation import ugettext_lazy as _
import mayan
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.smart_settings import Namespace
from .literals import DEFAULT_COMMON_HOME_VIEW
namespace = Namespace(label=_('Common'), name='common')
setting_auto_logging = namespace.add_setting(
@@ -94,5 +95,322 @@ setting_shared_storage = namespace.add_setting(
)
setting_shared_storage_arguments = namespace.add_setting(
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'
)
)

View File

@@ -1,11 +1,23 @@
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 (
setting_shared_storage, setting_shared_storage_arguments
)
storage_sharedupload = get_storage_subclass(
storage_sharedupload = import_string(
dotted_path=setting_shared_storage.value
)(**setting_shared_storage_arguments.value)
)(
**yaml.load(
stream=setting_shared_storage_arguments.value or '{}',
Loader=SafeLoader
)
)

View File

@@ -7,7 +7,6 @@ from django_downloadview import assert_download_response
from mayan.apps.acls.tests.mixins import ACLTestCaseMixin
from mayan.apps.permissions.classes import Permission
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.user_management.tests.mixins import UserTestMixin
from .mixins import (
ClientMethodsTestCaseMixin, ConnectionsCheckTestCaseMixin,
@@ -22,7 +21,7 @@ class BaseTestCase(
SilenceLoggerTestCaseMixin, ConnectionsCheckTestCaseMixin,
RandomPrimaryKeyModelMonkeyPatchMixin, ACLTestCaseMixin,
ModelTestCaseMixin, OpenFileCheckTestCaseMixin,
TempfileCheckTestCasekMixin, UserTestMixin, TestCase
TempfileCheckTestCasekMixin, TestCase
):
"""
This is the most basic test case class any test in the project should use.

View File

@@ -4,6 +4,7 @@ from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from mayan.apps.acls.classes import ModelPermission
from mayan.apps.user_management.tests.mixins import UserTestMixin
from ..models import ErrorLogEntry
from ..permissions_runtime import permission_error_log_view
@@ -12,7 +13,7 @@ from .base import GenericViewTestCase
from .literals import TEST_ERROR_LOG_ENTRY_RESULT
class CommonViewTestCase(GenericViewTestCase):
class CommonViewTestCase(UserTestMixin, GenericViewTestCase):
def _request_about_view(self):
return self.get(viewname='common:about_view')

View File

@@ -1,10 +1,6 @@
from __future__ import absolute_import, unicode_literals
from contextlib import contextmanager
import sys
from django.utils.encoding import force_text
class NullFile(object):
def write(self, string):
@@ -17,9 +13,3 @@ def mute_stdout():
sys.stdout = NullFile()
yield
sys.stdout = stdout_old
def as_id_list(items):
return ','.join(
[force_text(item.pk) for item in items]
)

View File

@@ -1,7 +1,7 @@
from __future__ import unicode_literals
from django.conf.urls import url
from django.views.i18n import JavaScriptCatalog
from django.views.i18n import JavaScriptCatalog, set_language
from .api_views import (
APIContentTypeList, APITemplateDetailView, APITemplateListView
@@ -10,10 +10,30 @@ from .views import (
AboutView, CurrentUserLocaleProfileDetailsView,
CurrentUserLocaleProfileEditView, FaviconRedirectView, HomeView,
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(
regex=r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/errors/$',
view=ObjectErrorLogEntryListView.as_view(), name='object_error_list'
@@ -25,20 +45,7 @@ urlpatterns_error_logs = [
),
]
urlpatterns_user_locale = [
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 = [
urlpatterns += [
url(
regex=r'^favicon\.ico$', view=FaviconRedirectView.as_view()
),
@@ -46,21 +53,11 @@ urlpatterns_misc = [
regex=r'^jsi18n/(?P<packages>\S+?)/$', view=JavaScriptCatalog.as_view(),
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 = [
url(
regex=r'^content_types/$', view=APIContentTypeList.as_view(),

View File

@@ -21,6 +21,14 @@ def check_for_sqlite():
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):
try:
local_field_name, remaining_field_path = related_field_name.split(

View File

@@ -1,11 +1,15 @@
from __future__ import absolute_import, unicode_literals
from json import dumps
from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.http import HttpResponseRedirect
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.http import urlencode
from django.utils.translation import ugettext_lazy as _
from django.views.generic import RedirectView
@@ -216,3 +220,67 @@ class ToolsListView(SimpleView):
'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})
)
)

View File

@@ -7,6 +7,11 @@ import shutil
from PIL import Image
import PyPDF2
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.translation import ugettext_lazy as _
@@ -15,14 +20,16 @@ from mayan.apps.storage.utils import NamedTemporaryFile
from ..classes import ConverterBase
from ..exceptions import PageCountError
from ..settings import setting_graphics_backend_arguments
from ..settings import setting_graphics_backend_config
from ..literals import (
DEFAULT_PDFTOPPM_DPI, DEFAULT_PDFTOPPM_FORMAT, DEFAULT_PDFTOPPM_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
)
@@ -32,20 +39,26 @@ except sh.CommandNotFound:
pdftoppm = None
else:
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_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 = 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
)

View File

@@ -7,6 +7,12 @@ import shutil
from PIL import Image
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 _
@@ -21,13 +27,15 @@ from .literals import (
CONVERTER_OFFICE_FILE_MIMETYPES, DEFAULT_LIBREOFFICE_PATH,
DEFAULT_PAGE_NUMBER, DEFAULT_PILLOW_FORMAT
)
from .settings import setting_graphics_backend_arguments
libreoffice_path = setting_graphics_backend_arguments.value.get(
'libreoffice_path', DEFAULT_LIBREOFFICE_PATH
)
from .settings import setting_graphics_backend_config
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):
@@ -54,7 +62,9 @@ class ConverterBase(object):
pass
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
)
@@ -146,7 +156,7 @@ class ConverterBase(object):
logger.error('Exception launching Libre Office; %s', exception)
raise
finally:
fs_cleanup(filename=libreoffice_home_directory)
fs_cleanup(libreoffice_home_directory)
# LibreOffice return a PDF file with the same name as the input
# provided but with the .pdf extension.
@@ -180,7 +190,7 @@ class ConverterBase(object):
shutil.copyfileobj(
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)
return temporary_converted_file_object

View File

@@ -2,12 +2,15 @@ from __future__ import unicode_literals
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.serialization import yaml_load
from .models import Transformation
@@ -18,7 +21,7 @@ class TransformationForm(forms.ModelForm):
def clean(self):
try:
yaml_load(stream=self.cleaned_data['arguments'])
yaml.load(stream=self.cleaned_data['arguments'], Loader=SafeLoader)
except yaml.YAMLError:
raise ValidationError(
_(

View File

@@ -2,11 +2,16 @@ from __future__ import unicode_literals
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.db import models, transaction
from mayan.apps.common.serialization import yaml_dump, yaml_load
from .transformations import BaseTransformation
logger = logging.getLogger(__name__)
@@ -18,8 +23,8 @@ class TransformationManager(models.Manager):
self.create(
content_type=content_type, object_id=obj.pk,
name=transformation.name, arguments=yaml_dump(
data=arguments
name=transformation.name, arguments=yaml.dump(
data=arguments, Dumper=SafeDumper
)
)
@@ -91,8 +96,9 @@ class TransformationManager(models.Manager):
# Some transformations don't require arguments
# return an empty dictionary as ** doesn't allow None
if transformation.arguments:
kwargs = yaml_load(
kwargs = yaml.load(
stream=transformation.arguments,
Loader=SafeLoader
)
else:
kwargs = {}

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
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_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.'),
global_name='CONVERTER_GRAPHICS_BACKEND',
)
setting_graphics_backend_arguments = namespace.add_setting(
default={
'libreoffice_path': DEFAULT_LIBREOFFICE_PATH,
'pdftoppm_dpi': DEFAULT_PDFTOPPM_DPI,
'pdftoppm_format': DEFAULT_PDFTOPPM_FORMAT,
'pdftoppm_path': DEFAULT_PDFTOPPM_PATH,
'pdfinfo_path': DEFAULT_PDFINFO_PATH,
'pillow_format': DEFAULT_PILLOW_FORMAT,
}, help_text=_(
setting_graphics_backend_config = namespace.add_setting(
default='''
{{
libreoffice_path: {},
pdftoppm_dpi: {},
pdftoppm_format: {},
pdftoppm_path: {},
pdfinfo_path: {},
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.'
), global_name='CONVERTER_GRAPHICS_BACKEND_ARGUMENTS'
), global_name='CONVERTER_GRAPHICS_BACKEND_CONFIG', quoted=True
)

View File

@@ -121,7 +121,7 @@ class TransformationTestCase(GenericDocumentTestCase):
arguments={'top': '10'}
)
self.assertTrue(document_page.generate_image())
self.assertTrue(document_page.generate_image().startswith('page'))
def test_crop_transformation_invalid_arguments(self):
self._silence_logger(name='mayan.apps.converter.managers')
@@ -132,7 +132,8 @@ class TransformationTestCase(GenericDocumentTestCase):
obj=document_page, transformation=TransformationCrop,
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):
self._silence_logger(name='mayan.apps.converter.managers')
@@ -144,7 +145,7 @@ class TransformationTestCase(GenericDocumentTestCase):
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):
self._silence_logger(name='mayan.apps.converter.managers')
@@ -161,7 +162,7 @@ class TransformationTestCase(GenericDocumentTestCase):
arguments={'left': '1000', 'right': '10000'}
)
self.assertTrue(document_page.generate_image())
self.assertTrue(document_page.generate_image().startswith('page'))
def test_lineart_transformations(self):
document_page = self.test_document.pages.first()
@@ -171,7 +172,7 @@ class TransformationTestCase(GenericDocumentTestCase):
arguments={}
)
self.assertTrue(document_page.generate_image())
self.assertTrue(document_page.generate_image().startswith('page'))
def test_rotate_transformations(self):
document_page = self.test_document.pages.first()
@@ -181,18 +182,18 @@ class TransformationTestCase(GenericDocumentTestCase):
arguments={}
)
self.assertTrue(document_page.generate_image())
self.assertTrue(document_page.generate_image().startswith('page'))
Transformation.objects.add_to_object(
obj=document_page, transformation=TransformationRotate180,
arguments={}
)
self.assertTrue(document_page.generate_image())
self.assertTrue(document_page.generate_image().startswith('page'))
Transformation.objects.add_to_object(
obj=document_page, transformation=TransformationRotate270,
arguments={}
)
self.assertTrue(document_page.generate_image())
self.assertTrue(document_page.generate_image().startswith('page'))

View File

@@ -308,6 +308,12 @@ class TransformationDrawRectanglePercent(BaseTransformation):
if bottom > 100:
bottom = 100
#if left > right:
# left, right = right, left
#if top > bottom:
# top, bottom = bottom, top
logger.debug(
'left: %f, top: %f, right: %f, bottom: %f', left, top, right,
bottom
@@ -519,9 +525,7 @@ class TransformationZoom(BaseTransformation):
BaseTransformation.register(transformation=TransformationCrop)
BaseTransformation.register(transformation=TransformationDrawRectangle)
BaseTransformation.register(
transformation=TransformationDrawRectanglePercent
)
BaseTransformation.register(transformation=TransformationDrawRectanglePercent)
BaseTransformation.register(transformation=TransformationFlip)
BaseTransformation.register(transformation=TransformationGaussianBlur)
BaseTransformation.register(transformation=TransformationLineArt)

View File

@@ -9,19 +9,19 @@ from .views import (
urlpatterns = [
url(
regex=r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/transformations/$',
view=TransformationListView.as_view(), name='transformation_list'
),
url(
regex=r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/transformations/create/$',
regex=r'^create_for/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/$',
view=TransformationCreateView.as_view(), name='transformation_create'
),
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'
),
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'
),
]

View File

@@ -2,12 +2,15 @@ from __future__ import unicode_literals
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.serialization import yaml_load
@deconstructible
class YAMLValidator(object):
@@ -17,7 +20,7 @@ class YAMLValidator(object):
def __call__(self, value):
value = value.strip()
try:
yaml_load(stream=value)
yaml.load(stream=value, Loader=SafeLoader)
except yaml.error.YAMLError:
raise ValidationError(
_('Enter a valid YAML value.'),

View File

@@ -1,6 +1,5 @@
from __future__ import unicode_literals
from django.contrib.humanize.templatetags.humanize import intcomma
from django.template import loader
@@ -86,8 +85,7 @@ class DashboardWidgetNumeric(BaseDashboardWidget):
def get_context(self):
return {
'count': intcomma(value=self.count),
'count_raw': self.count,
'count': self.count,
'icon_class': self.icon_class,
'label': self.label,
'link': self.link,

View File

@@ -1,7 +1,5 @@
{% load i18n %}
{% load appearance_tags %}
<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-heading">
@@ -11,7 +9,7 @@
<i class="dashboard-widget-icon {{ icon }}"></i>
{% elif icon_class %}
<div class="dashboard-widget-icon">
{% appearance_icon_render icon_class enable_shadow=True %}
{{ icon_class.render }}
</div>
{% endif %}
</div>

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