Compare commits

..

17 Commits

Author SHA1 Message Date
Roberto Rosario
36b89cf0ea Merge branch 'versions/minor' into features/workflow_email_action
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-13 02:56:02 -04:00
Roberto Rosario
601bff304f Merge branch 'versions/minor' into features/workflow_email_action
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 00:20:38 -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
382 changed files with 1964 additions and 25972 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.

View File

@@ -1,3 +1,8 @@
Importer branch
===============
* Add a reusable task to upload documents.
* Add MVP of the importer app.
3.3 (2019-XX-XX)
================
- Add support for icon shadows.
@@ -36,47 +41,14 @@
- 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.
- Replace djcelery and replace it with django-celery-beat.
- Update Celery to version 4.3.0
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.
- Update the EXIFTOOL driver to run for all documents
regardless of MIME type.
- 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.
3.2.6 (2019-07-10)
==================
- Remove the smart settings app * import.
- Encode settings YAML before hashing.
- Fix document icon used in the workflow runtime links.
- Add trashed date time label.
- Fix thumbnail generation issue. GitLab issue #637.
* Remove the smart settings app * import.
* Encode settings YAML before hashing.
* 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.
@@ -129,6 +101,7 @@
- Add support for disabling the random primary key
test mixin.
- Add a reusable task to upload documents.
- Add MVP of the importer app.
- Fix mailing profile log columns mappings.
GitLab issue #626. Thanks to Jesaja Everling (@jeverling)
for the report.

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

@@ -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', 'mailer', 'mayan_statistics', 'metadata', 'mirroring',
'motd', 'navigation', 'ocr', 'permissions', 'platform', 'rest_api',
'smart_settings', 'sources', 'storage', 'tags', 'task_manager',
'user_management'
)
LANGUAGE_LIST = (

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,32 +96,31 @@ 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_STATIC_ROOT=${PROJECT_INSTALL_DIR}/static mayan-edms.py preparestatic --link --noinput
@@ -128,7 +128,7 @@ RUN python3 -m venv "${PROJECT_INSTALL_DIR}" \
COPY --chown=mayan:mayan requirements/testing-base.txt "${PROJECT_INSTALL_DIR}"
####
# Final image - BASE_IMAGE + BUILDER_IMAGE artifact (Mayan install directory)
# Final image - BASE_IMAGE + Mayan install directory from the builder image
####
FROM BASE_IMAGE
@@ -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

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

@@ -220,11 +220,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

@@ -49,41 +49,6 @@ Changes
- 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
--------
@@ -91,17 +56,17 @@ 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
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.
@@ -195,13 +160,7 @@ Backward incompatible changes
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.
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/

View File

@@ -3,7 +3,7 @@ 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'
__build_string__ = 'v3.2.6_Wed Jul 10 03:18:15 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

@@ -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%;
}
@@ -212,18 +220,6 @@ a i {
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 +249,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,14 +266,6 @@ 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;
@@ -540,20 +536,5 @@ a i {
}
.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;
box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.5);
}

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,34 +17,28 @@ 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;
}, {});
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]);
// 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');
});
event.preventDefault();
partialNavigation.setLocation(
$(this).attr('href') + '?id_list=' + id_list.join(',')
);
});
// Set the form data as the data to send
options.data = formArray;
}
}
static setupNavBarState () {
@@ -172,10 +166,10 @@ class MayanApp {
var self = this;
this.setupAJAXSpinner();
this.setupAutoSubmit();
this.setupFormHotkeys();
this.setupFullHeightResizing();
this.setupItemsSelector();
MayanApp.setupMultiItemActions();
this.setupNavbarCollapse();
MayanApp.setupNavBarState();
this.setupNewWindowAnchor();
@@ -183,7 +177,6 @@ class MayanApp {
value.app = self;
app.doRefreshAJAXMenu(value);
});
this.setupPanelSelection();
partialNavigation.initialize();
}
@@ -207,6 +200,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 +238,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 +286,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,6 +82,7 @@
</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 %}
@@ -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

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

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

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

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

@@ -315,6 +315,43 @@ class DocumentCheckoutViewTestCase(
response=response, text=self.test_document.label, status_code=200
)
def test_document_check_out_new_version(self):
"""
Gitlab issue #231
User shown option to upload new version of a document even though it
is blocked by checkout - v2.0.0b2
Expected results:
- Link to upload version view should not resolve
- Upload version view should reject request
"""
self._create_test_case_superuser()
self._check_out_test_document()
self.login_superuser()
response = self.post(
viewname='sources:upload_version', kwargs={
'document_pk': self.test_document.pk
}, follow=True
)
self.assertContains(
response=response, text='blocked from uploading',
status_code=200
)
response = self.get(
viewname='documents:document_version_list', kwargs={
'pk': self.test_document.pk
}, follow=True
)
# Needed by the url view resolver
response.context.current_app = None
resolved_link = link_document_version_upload.resolve(context=response.context)
self.assertEqual(resolved_link, None)
def test_document_check_in_forcefull_view_no_permission(self):
# Gitlab issue #237
# Forcefully checking in a document by a user without adequate
@@ -351,47 +388,3 @@ class DocumentCheckoutViewTestCase(
)
self.assertEqual(response.status_code, 302)
self.assertFalse(self.test_document.is_checked_out())
class NewVersionBlockViewTestCase(
DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin,
GenericDocumentViewTestCase):
def test_document_check_out_new_version(self):
"""
Gitlab issue #231
User shown option to upload new version of a document even though it
is blocked by checkout - v2.0.0b2
Expected results:
- Link to upload version view should not resolve
- Upload version view should reject request
"""
self._create_test_case_superuser()
self._check_out_test_document()
self.login_superuser()
response = self.post(
viewname='sources:upload_version', kwargs={
'document_pk': self.test_document.pk
}, follow=True
)
self.assertContains(
response=response, text='blocked from uploading',
status_code=200
)
response = self.get(
viewname='documents:document_version_list', kwargs={
'pk': self.test_document.pk
}, follow=True
)
# Needed by the url view resolver
response.context.current_app = None
resolved_link = link_document_version_upload.resolve(
context=response.context
)
self.assertEqual(resolved_link, None)

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

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

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

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

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

@@ -10,6 +10,7 @@ import sh
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.serialization import yaml_load
from mayan.apps.mimetype.api import get_mimetype
from mayan.apps.storage.settings import setting_temporary_directory
from mayan.apps.storage.utils import (
@@ -146,7 +147,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 +181,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

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

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

View File

@@ -11,35 +11,35 @@ from .views import (
urlpatterns = [
url(
regex=r'^keys/(?P<pk>\d+)/$', view=KeyDetailView.as_view(),
regex=r'^(?P<pk>\d+)/$', view=KeyDetailView.as_view(),
name='key_detail'
),
url(
regex=r'^keys/(?P<pk>\d+)/delete/$', view=KeyDeleteView.as_view(),
regex=r'^(?P<pk>\d+)/delete/$', view=KeyDeleteView.as_view(),
name='key_delete'
),
url(
regex=r'^keys/(?P<pk>\d+)/download/$', view=KeyDownloadView.as_view(),
regex=r'^(?P<pk>\d+)/download/$', view=KeyDownloadView.as_view(),
name='key_download'
),
url(
regex=r'^keys/private/$', view=PrivateKeyListView.as_view(),
regex=r'^list/private/$', view=PrivateKeyListView.as_view(),
name='key_private_list'
),
url(
regex=r'^keys/public/$', view=PublicKeyListView.as_view(),
regex=r'^list/public/$', view=PublicKeyListView.as_view(),
name='key_public_list'
),
url(
regex=r'^keys/upload/$', view=KeyUploadView.as_view(), name='key_upload'
regex=r'^upload/$', view=KeyUploadView.as_view(), name='key_upload'
),
url(regex=r'^keys/query/$', view=KeyQueryView.as_view(), name='key_query'),
url(regex=r'^query/$', view=KeyQueryView.as_view(), name='key_query'),
url(
regex=r'^keys/query/results/$', view=KeyQueryResultView.as_view(),
regex=r'^query/results/$', view=KeyQueryResultView.as_view(),
name='key_query_results'
),
url(
regex=r'^keys/receive/(?P<key_id>.+)/$', view=KeyReceive.as_view(),
regex=r'^receive/(?P<key_id>.+)/$', view=KeyReceive.as_view(),
name='key_receive'
),
]

View File

@@ -11,25 +11,25 @@ from .views import (
urlpatterns = [
url(
regex=r'^documents/(?P<pk>\d+)/comments/$',
view=DocumentCommentListView.as_view(), name='comments_for_document'
),
url(
regex=r'^documents/(?P<pk>\d+)/comments/add/$',
regex=r'^(?P<pk>\d+)/comment/add/$',
view=DocumentCommentCreateView.as_view(), name='comment_add'
),
url(
regex=r'^comments/(?P<pk>\d+)/delete/$',
regex=r'^comment/(?P<pk>\d+)/delete/$',
view=DocumentCommentDeleteView.as_view(), name='comment_delete'
),
url(
regex=r'^comments/(?P<pk>\d+)/$',
regex=r'^comment/(?P<pk>\d+)/$',
view=DocumentCommentDetailView.as_view(), name='comment_details'
),
url(
regex=r'^comments/(?P<pk>\d+)/edit/$',
regex=r'^comment/(?P<pk>\d+)/edit/$',
view=DocumentCommentEditView.as_view(), name='comment_edit'
),
url(
regex=r'^(?P<pk>\d+)/comment/list/$',
view=DocumentCommentListView.as_view(), name='comments_for_document'
),
]
api_urls = [

View File

@@ -196,36 +196,3 @@ class IndexToolsViewTestCase(
# An instance root exists
self.assertTrue(self.test_index.instance_root.pk)
def test_index_rebuild_view_no_permission(self):
self._create_test_index()
self.test_index.node_templates.create(
parent=self.test_index.template_root,
expression=TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION,
link_documents=True
)
response = self._request_test_index_rebuild_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(IndexInstanceNode.objects.count(), 0)
def test_index_rebuild_view_with_access(self):
self._create_test_index()
self.test_index.node_templates.create(
parent=self.test_index.template_root,
expression=TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION,
link_documents=True
)
self.grant_access(
obj=self.test_index,
permission=permission_document_indexing_rebuild
)
response = self._request_test_index_rebuild_view()
self.assertEqual(response.status_code, 302)
self.assertNotEqual(IndexInstanceNode.objects.count(), 0)

View File

@@ -15,81 +15,72 @@ from .views import (
TemplateNodeCreateView, TemplateNodeDeleteView, TemplateNodeEditView
)
urlpatterns_indexes = [
urlpatterns = [
url(
regex=r'^document_types/(?P<pk>\d+)/index_templates/$',
regex=r'^setup/document_types/(?P<pk>\d+)/index_templates/$',
view=DocumentTypeIndexesView.as_view(),
name='document_type_index_templates'
),
url(
regex=r'^indexes/$', view=SetupIndexListView.as_view(),
regex=r'^setup/index/list/$', view=SetupIndexListView.as_view(),
name='index_setup_list'
),
url(
regex=r'^indexes/create/$', view=SetupIndexCreateView.as_view(),
regex=r'^setup/index/create/$', view=SetupIndexCreateView.as_view(),
name='index_setup_create'
),
url(
regex=r'^indexes/(?P<pk>\d+)/delete/$',
view=SetupIndexDeleteView.as_view(), name='index_setup_delete'
),
url(
regex=r'^indexes/(?P<pk>\d+)/edit/$',
regex=r'^setup/index/(?P<pk>\d+)/edit/$',
view=SetupIndexEditView.as_view(), name='index_setup_edit'
),
url(
regex=r'^indexes/(?P<pk>\d+)/document_types/$',
regex=r'^setup/index/(?P<pk>\d+)/delete/$',
view=SetupIndexDeleteView.as_view(), name='index_setup_delete'
),
url(
regex=r'^setup/index/(?P<pk>\d+)/template/$',
view=SetupIndexTreeTemplateListView.as_view(), name='index_setup_view'
),
url(
regex=r'^setup/index/(?P<pk>\d+)/document_types/$',
view=SetupIndexDocumentTypesView.as_view(),
name='index_setup_document_types'
),
url(
regex=r'^indexes/(?P<pk>\d+)/rebuild/$',
regex=r'^setup/index/(?P<pk>\d+)/rebuild/$',
view=SetupIndexRebuildView.as_view(), name='index_setup_rebuild'
),
url(
regex=r'^indexes/(?P<pk>\d+)/nodes/$',
view=SetupIndexTreeTemplateListView.as_view(), name='index_setup_view'
),
url(
regex=r'^indexes/nodes/(?P<pk>\d+)/children/create/$',
regex=r'^setup/template/node/(?P<pk>\d+)/create/child/$',
view=TemplateNodeCreateView.as_view(), name='template_node_create'
),
url(
regex=r'^indexes/nodes/(?P<pk>\d+)/delete/$',
view=TemplateNodeDeleteView.as_view(), name='template_node_delete'
),
url(
regex=r'^indexes/nodes/(?P<pk>\d+)/edit/$',
regex=r'^setup/template/node/(?P<pk>\d+)/edit/$',
view=TemplateNodeEditView.as_view(), name='template_node_edit'
),
]
urlpatterns_index_instances = [
url(
regex=r'^index_instances/$', view=IndexListView.as_view(), name='index_list'
regex=r'^setup/template/node/(?P<pk>\d+)/delete/$',
view=TemplateNodeDeleteView.as_view(), name='template_node_delete'
),
url(
regex=r'^index/list/$', view=IndexListView.as_view(), name='index_list'
),
url(
regex=r'^index_instances/nodes/(?P<pk>\d+)/$',
regex=r'^instance/node/(?P<pk>\d+)/$',
view=IndexInstanceNodeView.as_view(), name='index_instance_node_view'
),
url(
regex=r'^documents/(?P<pk>\d+)/index_instances/$',
view=DocumentIndexNodeListView.as_view(), name='document_index_list'
),
]
urlpatterns_tools = [
url(
regex=r'^indexes/rebuild/$', view=IndexesRebuildView.as_view(),
name='rebuild_index_instances'
),
url(
regex=r'^list/for/document/(?P<pk>\d+)/$',
view=DocumentIndexNodeListView.as_view(), name='document_index_list'
),
]
urlpatterns = []
urlpatterns.extend(urlpatterns_indexes)
urlpatterns.extend(urlpatterns_index_instances)
urlpatterns.extend(urlpatterns_tools)
api_urls = [
url(
regex=r'^indexes/node/(?P<pk>[0-9]+)/documents/$',

View File

@@ -86,7 +86,7 @@ class DocumentParsingApp(MayanAppConfig):
)
ModelField(
model=Document, name='versions__version_pages__content__content'
model=Document, name='versions__pages__content__content'
)
ModelPermission.register(
@@ -118,7 +118,7 @@ class DocumentParsingApp(MayanAppConfig):
)
document_search.add_model_field(
field='versions__version_pages__content__content', label=_('Content')
field='versions__pages__content__content', label=_('Content')
)
document_page_search.add_model_field(

View File

@@ -10,11 +10,6 @@ from .permissions import (
permission_parse_document
)
def is_document_page_disabled(context):
return not context['resolved_object'].enabled
link_document_content = Link(
args='resolved_object.id',
icon_class_path='mayan.apps.document_parsing.icons.icon_document_content',
@@ -22,7 +17,7 @@ link_document_content = Link(
view='document_parsing:document_content'
)
link_document_page_content = Link(
args='resolved_object.id', conditional_disable=is_document_page_disabled,
args='resolved_object.id',
icon_class_path='mayan.apps.document_parsing.icons.icon_document_content',
permissions=(permission_content_view,), text=_('Content'),
view='document_parsing:document_page_content'

View File

@@ -12,37 +12,37 @@ from .views import (
urlpatterns = [
url(
regex=r'^signatures/(?P<pk>\d+)/details/$',
regex=r'^(?P<pk>\d+)/details/$',
view=DocumentVersionSignatureDetailView.as_view(),
name='document_version_signature_details'
),
url(
regex=r'^signatures/(?P<pk>\d+)/download/$',
regex=r'^signature/(?P<pk>\d+)/download/$',
view=DocumentVersionSignatureDownloadView.as_view(),
name='document_version_signature_download'
),
url(
regex=r'^documents/versions/(?P<pk>\d+)/signatures/$',
regex=r'^document/version/(?P<pk>\d+)/signatures/list/$',
view=DocumentVersionSignatureListView.as_view(),
name='document_version_signature_list'
),
url(
regex=r'^documents/versions/(?P<pk>\d+)/signatures/detached/upload/$',
regex=r'^documents/version/(?P<pk>\d+)/signature/detached/upload/$',
view=DocumentVersionSignatureUploadView.as_view(),
name='document_version_signature_upload'
),
url(
regex=r'^documents/versions/(?P<pk>\d+)/signatures/detached/create/$',
regex=r'^documents/version/(?P<pk>\d+)/signature/detached/create/$',
view=DocumentVersionDetachedSignatureCreateView.as_view(),
name='document_version_signature_detached_create'
),
url(
regex=r'^documents/versions/(?P<pk>\d+)/signatures/embedded/create/$',
regex=r'^documents/version/(?P<pk>\d+)/signature/embedded/create/$',
view=DocumentVersionEmbeddedSignatureCreateView.as_view(),
name='document_version_signature_embedded_create'
),
url(
regex=r'^signatures/(?P<pk>\d+)/delete/$',
regex=r'^signature/(?P<pk>\d+)/delete/$',
view=DocumentVersionSignatureDeleteView.as_view(),
name='document_version_signature_delete'
),

View File

@@ -27,6 +27,7 @@ from .serializers import (
)
from .settings import settings_workflow_image_cache_time
from .storages import storage_workflowimagecache
from .tasks import task_generate_workflow_image
@@ -203,8 +204,7 @@ class APIWorkflowImageView(generics.RetrieveAPIView):
)
cache_filename = task.get(timeout=WORKFLOW_IMAGE_TASK_TIMEOUT)
cache_file = self.get_object().cache_partition.get_file(filename=cache_filename)
with cache_file.open() as file_object:
with storage_workflowimagecache.open(cache_filename) as file_object:
response = HttpResponse(file_object.read(), content_type='image')
if '_hash' in request.GET:
patch_cache_control(

View File

@@ -1,7 +1,7 @@
from __future__ import unicode_literals
from django.apps import apps
from django.db.models.signals import post_migrate, post_save
from django.db.models.signals import post_save
from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.classes import ModelPermission
@@ -25,8 +25,7 @@ from .classes import DocumentStateHelper, WorkflowAction
from .events import event_workflow_created, event_workflow_edited
from .dependencies import * # NOQA
from .handlers import (
handler_create_workflow_image_cache, handler_index_document,
handler_launch_workflow, handler_trigger_transition
handler_index_document, handler_launch_workflow, handler_trigger_transition
)
from .html_widgets import WorkflowLogExtraDataWidget, widget_transition_events
from .links import (
@@ -280,6 +279,8 @@ class DocumentStatesApp(MayanAppConfig):
)
SourceColumn(
<<<<<<< HEAD
=======
attribute='name', is_identifier=True, is_sortable=True,
source=WorkflowTransitionField
)
@@ -304,6 +305,7 @@ class DocumentStatesApp(MayanAppConfig):
)
SourceColumn(
>>>>>>> versions/minor
source=WorkflowRuntimeProxy, label=_('Documents'),
func=lambda context: context['object'].get_document_count(
user=context['request'].user
@@ -329,17 +331,6 @@ class DocumentStatesApp(MayanAppConfig):
link_workflow_template_preview
), sources=(Workflow,)
)
menu_list_facet.unbind_links(
links=(
link_acl_list, link_events_for_object,
link_object_event_types_user_subcriptions_list,
link_workflow_template_document_types,
link_workflow_template_state_list, link_workflow_template_transition_list,
link_workflow_template_preview
), sources=(WorkflowRuntimeProxy,)
)
menu_list_facet.bind_links(
links=(
link_document_type_workflow_templates,
@@ -453,10 +444,6 @@ class DocumentStatesApp(MayanAppConfig):
# Index updating
post_migrate.connect(
dispatch_uid='workflows_handler_create_workflow_image_cache',
receiver=handler_create_workflow_image_cache,
)
post_save.connect(
dispatch_uid='workflows_handler_index_document_save',
receiver=handler_index_document,

View File

@@ -6,22 +6,6 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.document_indexing.tasks import task_index_document
from mayan.apps.events.classes import EventType
from .literals import (
WORKFLOW_IMAGE_CACHE_STORAGE_INSTANCE_PATH, WORKFLOW_IMAGE_CACHE_NAME
)
from .settings import setting_workflow_image_cache_maximum_size
def handler_create_workflow_image_cache(sender, **kwargs):
Cache = apps.get_model(app_label='file_caching', model_name='Cache')
Cache.objects.update_or_create(
defaults={
'label': _('Workflow images'),
'storage_instance_path': WORKFLOW_IMAGE_CACHE_STORAGE_INSTANCE_PATH,
'maximum_size': setting_workflow_image_cache_maximum_size.value,
}, name=WORKFLOW_IMAGE_CACHE_NAME,
)
def handler_index_document(sender, **kwargs):
task_index_document.apply_async(

View File

@@ -2,8 +2,6 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
DEFAULT_WORKFLOW_IMAGE_CACHE_MAXIMUM_SIZE = 50 * 2 ** 20 # 50 Megabytes
FIELD_TYPE_CHOICE_CHAR = 1
FIELD_TYPE_CHOICE_INTEGER = 2
FIELD_TYPE_CHOICES = (
@@ -32,6 +30,4 @@ WORKFLOW_ACTION_WHEN_CHOICES = (
(WORKFLOW_ACTION_ON_ENTRY, _('On entry')),
(WORKFLOW_ACTION_ON_EXIT, _('On exit')),
)
WORKFLOW_IMAGE_CACHE_NAME = 'workflow_images'
WORKFLOW_IMAGE_CACHE_STORAGE_INSTANCE_PATH = 'mayan.apps.document_states.storages.storage_workflowimagecache'
WORKFLOW_IMAGE_TASK_TIMEOUT = 60

View File

@@ -6,23 +6,24 @@ import logging
from furl import furl
from graphviz import Digraph
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django.apps import apps
from django.conf import settings
from django.core import serializers
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.files.base import ContentFile
from django.db import IntegrityError, models, transaction
from django.db.models import F, Max, Q
from django.urls import reverse
from django.utils.encoding import (
force_bytes, force_text, python_2_unicode_compatible
)
from django.utils.functional import cached_property
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.module_loading import import_string
from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.serialization import yaml_load
from mayan.apps.common.validators import YAMLValidator, validate_internal_name
from mayan.apps.documents.models import Document, DocumentType
from mayan.apps.documents.permissions import permission_document_view
@@ -32,11 +33,11 @@ from .error_logs import error_log_state_actions
from .events import event_workflow_created, event_workflow_edited
from .literals import (
FIELD_TYPE_CHOICES, WIDGET_CLASS_CHOICES, WORKFLOW_ACTION_WHEN_CHOICES,
WORKFLOW_ACTION_ON_ENTRY, WORKFLOW_ACTION_ON_EXIT,
WORKFLOW_IMAGE_CACHE_NAME
WORKFLOW_ACTION_ON_ENTRY, WORKFLOW_ACTION_ON_EXIT
)
from .managers import WorkflowManager
from .permissions import permission_workflow_transition
from .storages import storage_workflowimagecache
logger = logging.getLogger(__name__)
@@ -73,37 +74,19 @@ class Workflow(models.Model):
def __str__(self):
return self.label
@cached_property
def cache(self):
Cache = apps.get_model(app_label='file_caching', model_name='Cache')
return Cache.objects.get(name=WORKFLOW_IMAGE_CACHE_NAME)
@cached_property
def cache_partition(self):
partition, created = self.cache.partitions.get_or_create(
name='{}'.format(self.pk)
)
return partition
def delete(self, *args, **kwargs):
self.cache_partition.delete()
return super(Workflow, self).delete(*args, **kwargs)
def generate_image(self):
cache_filename = '{}'.format(self.get_hash())
cache_filename = '{}-{}'.format(self.id, self.get_hash())
image = self.render()
if self.cache_partition.get_file(filename=cache_filename):
logger.debug(
'workflow cache file "%s" found', cache_filename
)
else:
logger.debug(
'workflow cache file "%s" not found', cache_filename
# Since open "wb+" doesn't create files, check if the file
# exists, if not then create it
if not storage_workflowimagecache.exists(cache_filename):
storage_workflowimagecache.save(
name=cache_filename, content=ContentFile(content='')
)
image = self.render()
with self.cache_partition.create_file(filename=cache_filename) as file_object:
file_object.write(image)
with storage_workflowimagecache.open(cache_filename, mode='wb+') as file_object:
file_object.write(image)
return cache_filename
@@ -126,16 +109,12 @@ class Workflow(models.Model):
Workflow.objects.filter(pk=self.pk)
) + list(
WorkflowState.objects.filter(workflow__pk=self.pk)
) + list(
WorkflowStateAction.objects.filter(state__workflow__pk=self.pk)
) + list(
WorkflowTransition.objects.filter(workflow__pk=self.pk)
)
return hashlib.sha256(
force_bytes(
serializers.serialize('json', objects_lists)
)
serializers.serialize('json', objects_lists)
).hexdigest()
def get_initial_state(self):
@@ -486,7 +465,7 @@ class WorkflowTransitionField(models.Model):
return self.label
def get_widget_kwargs(self):
return yaml_load(stream=self.widget_kwargs)
return yaml.load(stream=self.widget_kwargs, Loader=SafeLoader)
@python_2_unicode_compatible

View File

@@ -7,20 +7,8 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.smart_settings.classes import Namespace
from .literals import DEFAULT_WORKFLOW_IMAGE_CACHE_MAXIMUM_SIZE
from .utils import callback_update_workflow_image_cache_size
namespace = Namespace(label=_('Workflows'), name='document_states')
setting_workflow_image_cache_maximum_size = namespace.add_setting(
global_name='WORKFLOW_IMAGE_CACHE_MAXIMUM_SIZE',
default=DEFAULT_WORKFLOW_IMAGE_CACHE_MAXIMUM_SIZE,
help_text=_(
'The threshold at which the WORKFLOW_IMAGE_CACHE_STORAGE_BACKEND will '
'start deleting the oldest workflow image cache files. Specify the '
'size in bytes.'
), post_edit_function=callback_update_workflow_image_cache_size
)
settings_workflow_image_cache_time = namespace.add_setting(
global_name='WORKFLOWS_IMAGE_CACHE_TIME', default='31556926',
help_text=_(

View File

@@ -1,11 +0,0 @@
from __future__ import unicode_literals
from mayan.apps.common.tests import BaseTestCase
from .mixins import WorkflowTestMixin
class WorkflowModelTestCase(WorkflowTestMixin, BaseTestCase):
def test_workflow_template_preview(self):
self._create_test_workflow()
self.assertTrue(self.test_workflow.get_api_image_url())

View File

@@ -225,7 +225,7 @@ urlpatterns_workflow_transition_fields = [
),
]
urlpatterns_tools = [
urlpatterns = [
url(
regex=r'^tools/workflows/launch/$',
view=ToolLaunchWorkflows.as_view(),
@@ -233,8 +233,6 @@ urlpatterns_tools = [
),
]
urlpatterns = []
urlpatterns.extend(urlpatterns_tools)
urlpatterns.extend(urlpatterns_workflow_instances)
urlpatterns.extend(urlpatterns_workflow_runtime_proxies)
urlpatterns.extend(urlpatterns_workflow_states)

View File

@@ -1,12 +0,0 @@
from __future__ import unicode_literals
from django.apps import apps
from .literals import WORKFLOW_IMAGE_CACHE_NAME
def callback_update_workflow_image_cache_size(setting):
Cache = apps.get_model(app_label='file_caching', model_name='Cache')
cache = Cache.objects.get(name=WORKFLOW_IMAGE_CACHE_NAME)
cache.maximum_size = setting.value
cache.save()

View File

@@ -1,30 +1,53 @@
from __future__ import absolute_import, unicode_literals
from django.contrib import messages
from django.db import transaction
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.urls import reverse
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.generics import (
FormView, SingleObjectCreateView, SingleObjectDeleteView,
AddRemoveView, ConfirmView, FormView, SingleObjectCreateView,
SingleObjectDeleteView, SingleObjectDetailView,
SingleObjectDynamicFormCreateView, SingleObjectDynamicFormEditView,
SingleObjectEditView, SingleObjectListView
)
from mayan.apps.common.mixins import ExternalObjectMixin
from mayan.apps.documents.events import event_document_type_edited
from mayan.apps.documents.models import DocumentType
from mayan.apps.documents.permissions import permission_document_type_edit
from mayan.apps.events.classes import EventType
from mayan.apps.events.models import StoredEventType
from ..classes import WorkflowAction
from ..events import event_workflow_edited
from ..forms import (
WorkflowActionSelectionForm, WorkflowStateActionDynamicForm,
WorkflowStateForm
WorkflowActionSelectionForm, WorkflowForm, WorkflowPreviewForm,
WorkflowStateActionDynamicForm, WorkflowStateForm, WorkflowTransitionForm,
WorkflowTransitionTriggerEventRelationshipFormSet
)
from ..icons import (
icon_workflow_template_list, icon_workflow_state, icon_workflow_state_action,
icon_workflow_transition, icon_workflow_transition_field
)
from ..icons import icon_workflow_state, icon_workflow_state_action
from ..links import (
link_workflow_template_state_create,
link_workflow_template_create, link_workflow_template_state_create,
link_workflow_template_state_action_selection,
link_workflow_template_transition_create,
link_workflow_template_transition_field_create,
)
from ..models import Workflow, WorkflowState, WorkflowStateAction
from ..permissions import permission_workflow_edit, permission_workflow_view
from ..models import (
Workflow, WorkflowState, WorkflowStateAction, WorkflowTransition,
WorkflowTransitionField
)
from ..permissions import (
permission_workflow_create, permission_workflow_delete,
permission_workflow_edit, permission_workflow_tools,
permission_workflow_view,
)
from ..tasks import task_launch_all_workflows
class WorkflowTemplateStateActionCreateView(SingleObjectDynamicFormCreateView):

View File

@@ -1,28 +1,53 @@
from __future__ import absolute_import, unicode_literals
from django.contrib import messages
from django.db import transaction
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.urls import reverse
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.generics import (
FormView, SingleObjectCreateView, SingleObjectDeleteView,
AddRemoveView, ConfirmView, FormView, SingleObjectCreateView,
SingleObjectDeleteView, SingleObjectDetailView,
SingleObjectDynamicFormCreateView, SingleObjectDynamicFormEditView,
SingleObjectEditView, SingleObjectListView
)
from mayan.apps.common.mixins import ExternalObjectMixin
from mayan.apps.documents.events import event_document_type_edited
from mayan.apps.documents.models import DocumentType
from mayan.apps.documents.permissions import permission_document_type_edit
from mayan.apps.events.classes import EventType
from mayan.apps.events.models import StoredEventType
from ..classes import WorkflowAction
from ..events import event_workflow_edited
from ..forms import (
WorkflowTransitionForm, WorkflowTransitionTriggerEventRelationshipFormSet
WorkflowActionSelectionForm, WorkflowForm, WorkflowPreviewForm,
WorkflowStateActionDynamicForm, WorkflowStateForm, WorkflowTransitionForm,
WorkflowTransitionTriggerEventRelationshipFormSet
)
from ..icons import (
icon_workflow_template_list, icon_workflow_state, icon_workflow_state_action,
icon_workflow_transition, icon_workflow_transition_field
)
from ..icons import icon_workflow_transition, icon_workflow_transition_field
from ..links import (
link_workflow_template_create, link_workflow_template_state_create,
link_workflow_template_state_action_selection,
link_workflow_template_transition_create,
link_workflow_template_transition_field_create,
)
from ..models import Workflow, WorkflowTransition, WorkflowTransitionField
from ..permissions import permission_workflow_edit, permission_workflow_view
from ..models import (
Workflow, WorkflowState, WorkflowStateAction, WorkflowTransition,
WorkflowTransitionField
)
from ..permissions import (
permission_workflow_create, permission_workflow_delete,
permission_workflow_edit, permission_workflow_tools,
permission_workflow_view,
)
from ..tasks import task_launch_all_workflows
class WorkflowTemplateTransitionCreateView(ExternalObjectMixin, SingleObjectCreateView):

View File

@@ -2,23 +2,46 @@ from __future__ import absolute_import, unicode_literals
from django.contrib import messages
from django.db import transaction
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.urls import reverse_lazy
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.generics import (
AddRemoveView, ConfirmView, SingleObjectCreateView, SingleObjectDeleteView,
SingleObjectDetailView, SingleObjectEditView, SingleObjectListView
AddRemoveView, ConfirmView, FormView, SingleObjectCreateView,
SingleObjectDeleteView, SingleObjectDetailView,
SingleObjectDynamicFormCreateView, SingleObjectDynamicFormEditView,
SingleObjectEditView, SingleObjectListView
)
from mayan.apps.common.mixins import ExternalObjectMixin
from mayan.apps.documents.events import event_document_type_edited
from mayan.apps.documents.models import DocumentType
from mayan.apps.documents.permissions import permission_document_type_edit
from mayan.apps.events.classes import EventType
from mayan.apps.events.models import StoredEventType
from ..classes import WorkflowAction
from ..events import event_workflow_edited
from ..forms import WorkflowForm, WorkflowPreviewForm
from ..icons import icon_workflow_template_list
from ..links import link_workflow_template_create
from ..models import Workflow
from ..forms import (
WorkflowActionSelectionForm, WorkflowForm, WorkflowPreviewForm,
WorkflowStateActionDynamicForm, WorkflowStateForm, WorkflowTransitionForm,
WorkflowTransitionTriggerEventRelationshipFormSet
)
from ..icons import (
icon_workflow_template_list, icon_workflow_state, icon_workflow_state_action,
icon_workflow_transition, icon_workflow_transition_field
)
from ..links import (
link_workflow_template_create, link_workflow_template_state_create,
link_workflow_template_state_action_selection,
link_workflow_template_transition_create,
link_workflow_template_transition_field_create,
)
from ..models import (
Workflow, WorkflowState, WorkflowStateAction, WorkflowTransition,
WorkflowTransitionField
)
from ..permissions import (
permission_workflow_create, permission_workflow_delete,
permission_workflow_edit, permission_workflow_tools,

View File

@@ -36,6 +36,7 @@ from .serializers import (
WritableDocumentTypeSerializer, WritableDocumentVersionSerializer
)
from .settings import settings_document_page_image_cache_time
from .storages import storage_documentimagecache
from .tasks import task_generate_document_page_image
logger = logging.getLogger(__name__)
@@ -164,7 +165,7 @@ class APIDocumentPageImageView(generics.RetrieveAPIView):
AccessControlList.objects.check_access(
obj=document, permissions=(permission_required,),
user=self.request.user
user=self.request.user, manager=Document.passthrough
)
return document
@@ -174,7 +175,7 @@ class APIDocumentPageImageView(generics.RetrieveAPIView):
)
def get_queryset(self):
return self.get_document_version().pages_all.all()
return self.get_document_version().pages.all()
def get_serializer(self, *args, **kwargs):
return None
@@ -204,13 +205,11 @@ class APIDocumentPageImageView(generics.RetrieveAPIView):
)
cache_filename = task.get(timeout=DOCUMENT_IMAGE_TASK_TIMEOUT)
cache_file = self.get_object().cache_partition.get_file(filename=cache_filename)
with cache_file.open() as file_object:
with storage_documentimagecache.open(cache_filename) as file_object:
response = HttpResponse(file_object.read(), content_type='image')
if '_hash' in request.GET:
patch_cache_control(
response=response,
max_age=settings_document_page_image_cache_time.value
response, max_age=settings_document_page_image_cache_time.value
)
return response

View File

@@ -1,6 +1,6 @@
from __future__ import absolute_import, unicode_literals
from django.db.models.signals import post_delete, post_migrate
from django.db.models.signals import post_delete
from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.classes import ModelPermission
@@ -43,11 +43,11 @@ from .events import (
event_document_view
)
from .handlers import (
handler_create_default_document_type, handler_create_document_cache,
handler_remove_empty_duplicates_lists, handler_scan_duplicates_for
handler_create_default_document_type, handler_remove_empty_duplicates_lists,
handler_scan_duplicates_for,
)
from .links import (
link_document_clear_transformations,
link_clear_image_cache, link_document_clear_transformations,
link_document_clone_transformations, link_document_delete,
link_document_document_type_edit, link_document_download,
link_document_duplicates_list, link_document_edit,
@@ -60,8 +60,6 @@ from .links import (
link_document_multiple_download, link_document_multiple_favorites_add,
link_document_multiple_favorites_remove, link_document_multiple_restore,
link_document_multiple_trash, link_document_multiple_update_page_count,
link_document_page_disable, link_document_page_multiple_disable,
link_document_page_enable, link_document_page_multiple_enable,
link_document_page_navigation_first, link_document_page_navigation_last,
link_document_page_navigation_next, link_document_page_navigation_previous,
link_document_page_return, link_document_page_rotate_left,
@@ -102,11 +100,6 @@ from .widgets import (
)
def is_document_page_enabled(context):
return context['object'].enabled
class DocumentsApp(MayanAppConfig):
app_namespace = 'documents'
app_url = 'documents'
@@ -221,21 +214,12 @@ class DocumentsApp(MayanAppConfig):
ModelPermission.register_inheritance(
model=Document, related='document_type',
)
ModelPermission.register_manager(
model=Document, manager_name='passthrough'
)
ModelPermission.register_inheritance(
model=DocumentPage, related='document_version__document',
)
ModelPermission.register_manager(
model=DocumentPage, manager_name='passthrough'
)
ModelPermission.register_inheritance(
model=DocumentPageResult, related='document_version__document',
)
ModelPermission.register_manager(
model=DocumentPageResult, manager_name='passthrough'
)
ModelPermission.register_inheritance(
model=DocumentTypeFilename, related='document_type',
)
@@ -278,21 +262,13 @@ class DocumentsApp(MayanAppConfig):
# DocumentPage
SourceColumn(
attribute='get_label', is_identifier=True,
is_object_absolute_url=True, source=DocumentPage,
widget_condition=is_document_page_enabled
is_object_absolute_url=True, source=DocumentPage
)
SourceColumn(
func=lambda context: document_page_thumbnail_widget.render(
instance=context['object']
), label=_('Thumbnail'), source=DocumentPage
)
SourceColumn(
attribute='enabled', include_label=True, source=DocumentPage,
widget=TwoStateWidget
)
SourceColumn(
attribute='page_number', include_label=True, source=DocumentPage
)
SourceColumn(
attribute='get_label', is_identifier=True,
@@ -401,7 +377,7 @@ class DocumentsApp(MayanAppConfig):
menu_setup.bind_links(links=(link_document_type_setup,))
menu_tools.bind_links(
links=(link_duplicated_document_scan,)
links=(link_clear_image_cache, link_duplicated_document_scan)
)
# Document type links
@@ -527,16 +503,6 @@ class DocumentsApp(MayanAppConfig):
link_document_page_navigation_last
), sources=(DocumentPage,)
)
menu_multi_item.bind_links(
links=(
link_document_page_multiple_disable,
link_document_page_multiple_enable
), sources=(DocumentPage,)
)
menu_object.bind_links(
links=(link_document_page_disable, link_document_page_enable),
sources=(DocumentPage,)
)
menu_list_facet.bind_links(
links=(link_transformation_list,), sources=(DocumentPage,)
)
@@ -561,10 +527,6 @@ class DocumentsApp(MayanAppConfig):
dispatch_uid='handler_create_default_document_type',
receiver=handler_create_default_document_type
)
post_migrate.connect(
dispatch_uid='documents_handler_create_document_cache',
receiver=handler_create_document_cache,
)
post_version_upload.connect(
dispatch_uid='handler_scan_duplicates_for',
receiver=handler_scan_duplicates_for

View File

@@ -1,13 +1,8 @@
from __future__ import unicode_literals
from django.apps import apps
from django.utils.translation import ugettext_lazy as _
from .literals import (
DEFAULT_DOCUMENT_TYPE_LABEL, DOCUMENT_CACHE_STORAGE_INSTANCE_PATH,
DOCUMENT_IMAGES_CACHE_NAME
)
from .settings import setting_document_cache_maximum_size
from .literals import DEFAULT_DOCUMENT_TYPE_LABEL
from .signals import post_initial_document_type
from .tasks import task_clean_empty_duplicate_lists, task_scan_duplicates_for
@@ -26,17 +21,6 @@ def handler_create_default_document_type(sender, **kwargs):
)
def handler_create_document_cache(sender, **kwargs):
Cache = apps.get_model(app_label='file_caching', model_name='Cache')
Cache.objects.update_or_create(
defaults={
'label': _('Document images'),
'storage_instance_path': DOCUMENT_CACHE_STORAGE_INSTANCE_PATH,
'maximum_size': setting_document_cache_maximum_size.value,
}, name=DOCUMENT_IMAGES_CACHE_NAME,
)
def handler_scan_duplicates_for(sender, instance, **kwargs):
task_scan_duplicates_for.apply_async(
kwargs={'document_id': instance.document.pk}

View File

@@ -12,6 +12,8 @@ icon_document_type = Icon(
icon_menu_documents = Icon(driver_name='fontawesome', symbol='book')
icon_clear_image_cache = Icon(driver_name='fontawesome', symbol='file-image')
icon_dashboard_document_types = icon_document_type
icon_dashboard_documents_in_trash = Icon(
driver_name='fontawesome', symbol='trash-alt'
@@ -25,6 +27,8 @@ icon_dashboard_new_documents_this_month = Icon(
icon_dashboard_total_document = Icon(
driver_name='fontawesome', symbol='book'
)
icon_document_quick_download = Icon(
driver_name='fontawesome', symbol='download'
)
@@ -102,14 +106,6 @@ icon_favorite_document_remove = Icon(
secondary_symbol='minus'
)
# Document pages
icon_document_page_disable = Icon(
driver_name='fontawesomecss', css_classes='far fa-eye-slash'
)
icon_document_page_enable = Icon(
driver_name='fontawesomecss', css_classes='far fa-eye'
)
icon_document_page_navigation_first = Icon(
driver_name='fontawesome', symbol='step-backward'
)

View File

@@ -8,7 +8,7 @@ from mayan.apps.converter.permissions import (
from mayan.apps.navigation.classes import Link
from .icons import (
icon_document_list_recent_access,
icon_clear_image_cache, icon_document_list_recent_access,
icon_recent_added_document_list, icon_document_page_navigation_first,
icon_document_page_navigation_last, icon_document_page_navigation_next,
icon_document_page_navigation_previous, icon_document_page_return,
@@ -19,14 +19,14 @@ from .icons import (
icon_duplicated_document_list, icon_duplicated_document_scan
)
from .permissions import (
permission_document_delete, permission_document_edit,
permission_document_download, permission_document_properties_edit,
permission_document_print, permission_document_restore,
permission_document_tools, permission_document_version_revert,
permission_document_view, permission_document_trash,
permission_document_type_create, permission_document_type_delete,
permission_document_type_edit, permission_document_type_view,
permission_empty_trash, permission_document_version_view
permission_document_delete, permission_document_download,
permission_document_properties_edit, permission_document_print,
permission_document_restore, permission_document_tools,
permission_document_version_revert, permission_document_view,
permission_document_trash, permission_document_type_create,
permission_document_type_delete, permission_document_type_edit,
permission_document_type_view, permission_empty_trash,
permission_document_version_view
)
from .settings import setting_zoom_max_level, setting_zoom_min_level
@@ -43,11 +43,11 @@ def is_not_current_version(context):
def is_first_page(context):
return context['resolved_object'].siblings.first() == context['resolved_object']
return context['resolved_object'].page_number <= 1
def is_last_page(context):
return context['resolved_object'].siblings.last() == context['resolved_object']
return context['resolved_object'].page_number >= context['resolved_object'].document_version.pages.count()
def is_max_zoom(context):
@@ -58,14 +58,6 @@ def is_min_zoom(context):
return context['zoom'] <= setting_zoom_min_level.value
def is_document_page_enabled(context):
return context['resolved_object'].enabled
def is_document_page_disabled(context):
return not context['resolved_object'].enabled
# Facet
link_document_preview = Link(
args='resolved_object.id',
@@ -272,37 +264,22 @@ link_document_list_deleted = Link(
text=_('Trash can'), view='documents:document_list_deleted'
)
# Tools
link_clear_image_cache = Link(
icon_class=icon_clear_image_cache,
description=_(
'Clear the graphics representations used to speed up the documents\' '
'display and interactive transformations results.'
), permissions=(permission_document_tools,),
text=_('Clear document image cache'),
view='documents:document_clear_image_cache'
)
link_trash_can_empty = Link(
permissions=(permission_empty_trash,), text=_('Empty trash'),
view='documents:trash_can_empty'
)
# Document pages
link_document_page_disable = Link(
condition=is_document_page_enabled,
icon_class_path='mayan.apps.documents.icons.icon_document_page_disable',
kwargs={'pk': 'resolved_object.id'},
permissions=(permission_document_edit,), text=_('Disable page'),
view='documents:document_page_disable'
)
link_document_page_multiple_disable = Link(
icon_class_path='mayan.apps.documents.icons.icon_document_page_disable',
text=_('Disable pages'),
view='documents:document_page_multiple_disable'
)
link_document_page_enable = Link(
condition=is_document_page_disabled,
icon_class_path='mayan.apps.documents.icons.icon_document_page_enable',
kwargs={'pk': 'resolved_object.id'},
permissions=(permission_document_edit,), text=_('Enable page'),
view='documents:document_page_enable'
)
link_document_page_multiple_enable = Link(
icon_class_path='mayan.apps.documents.icons.icon_document_page_enable',
text=_('Enable pages'),
view='documents:document_page_multiple_enable'
)
link_document_page_navigation_first = Link(
args='resolved_object.pk', conditional_disable=is_first_page,
icon_class=icon_document_page_navigation_first,
@@ -346,7 +323,6 @@ link_document_page_rotate_right = Link(
text=_('Rotate right'), view='documents:document_page_rotate_right',
)
link_document_page_view = Link(
conditional_disable=is_document_page_disabled,
icon_class_path='mayan.apps.documents.icons.icon_document_page_view',
permissions=(permission_document_view,), text=_('Page image'),
view='documents:document_page_view', args='resolved_object.pk'

View File

@@ -9,7 +9,6 @@ CHECK_TRASH_PERIOD_INTERVAL = 60
DELETE_STALE_STUBS_INTERVAL = 60 * 10 # 10 minutes
DEFAULT_DELETE_PERIOD = 30
DEFAULT_DELETE_TIME_UNIT = TIME_DELTA_UNIT_DAYS
DEFAULT_DOCUMENTS_CACHE_MAXIMUM_SIZE = 500 * 2 ** 20 # 500 Megabytes
DEFAULT_DOCUMENTS_HASH_BLOCK_SIZE = 65535
DEFAULT_LANGUAGE = 'eng'
DEFAULT_LANGUAGE_CODES = (
@@ -31,8 +30,6 @@ DEFAULT_LANGUAGE_CODES = (
DEFAULT_ZIP_FILENAME = 'document_bundle.zip'
DEFAULT_DOCUMENT_TYPE_LABEL = _('Default')
DOCUMENT_IMAGE_TASK_TIMEOUT = 120
DOCUMENT_IMAGES_CACHE_NAME = 'document_images'
DOCUMENT_CACHE_STORAGE_INSTANCE_PATH = 'mayan.apps.documents.storages.storage_documentimagecache'
STUB_EXPIRATION_INTERVAL = 60 * 60 * 24 # 24 hours
UPDATE_PAGE_COUNT_RETRY_DELAY = 10
UPLOAD_NEW_DOCUMENT_RETRY_DELAY = 10

View File

@@ -22,9 +22,28 @@ class DocumentManager(models.Manager):
def get_queryset(self):
return TrashCanQuerySet(
model=self.model, using=self._db
self.model, using=self._db
).filter(in_trash=False).filter(is_stub=False)
def invalidate_cache(self):
for document in self.model.objects.all():
document.invalidate_cache()
class DocumentPageCachedImage(models.Manager):
def get_by_natural_key(self, filename, document_page_natural_key):
DocumentPage = apps.get_model(
app_label='documents', model_name='DocumentPage'
)
try:
document_page = DocumentPage.objects.get_by_natural_key(
*document_page_natural_key
)
except DocumentPage.DoesNotExist:
raise self.model.DoesNotExist
return self.get(document_page__pk=document_page.pk, filename=filename)
class DocumentPageManager(models.Manager):
def get_by_natural_key(self, page_number, document_version_natural_key):
@@ -38,11 +57,6 @@ class DocumentPageManager(models.Manager):
return self.get(document_version__pk=document_version.pk, page_number=page_number)
def get_queryset(self):
return models.QuerySet(
model=self.model, using=self._db
).filter(enabled=True)
class DocumentTypeManager(models.Manager):
def check_delete_periods(self):

View File

@@ -1,37 +0,0 @@
from __future__ import unicode_literals
from django.db import migrations
from ..storages import storage_documentimagecache
def operation_clear_old_cache(apps, schema_editor):
DocumentPageCachedImage = apps.get_model(
'documents', 'DocumentPageCachedImage'
)
for cached_image in DocumentPageCachedImage.objects.using(schema_editor.connection.alias).all():
# Delete each cached image directly since the model doesn't exists and
# will not trigger the physical deletion of the stored file
storage_documentimagecache.delete(cached_image.filename)
cached_image.delete()
class Migration(migrations.Migration):
dependencies = [
('documents', '0048_auto_20190711_0544'),
]
operations = [
migrations.RunPython(
code=operation_clear_old_cache,
reverse_code=migrations.RunPython.noop
),
migrations.RemoveField(
model_name='documentpagecachedimage',
name='document_page',
),
migrations.DeleteModel(
name='DocumentPageCachedImage',
),
]

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-25 04:51
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('documents', '0049_auto_20190715_0454'),
]
operations = [
migrations.AlterField(
model_name='document',
name='language',
field=models.CharField(blank=True, default='eng', help_text='The dominant language in the document.', max_length=8, verbose_name='Language'),
),
]

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.22 on 2019-07-29 07:03
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('documents', '0050_auto_20190725_0451'),
]
operations = [
migrations.AddField(
model_name='documentpage',
name='enabled',
field=models.BooleanField(default=True, verbose_name='Enabled'),
),
]

View File

@@ -136,6 +136,10 @@ class Document(models.Model):
if latest_version:
return latest_version.get_api_image_url(*args, **kwargs)
def invalidate_cache(self):
for document_version in self.versions.all():
document_version.invalidate_cache()
@property
def is_in_trash(self):
return self.in_trash
@@ -236,18 +240,6 @@ class Document(models.Model):
def page_count(self):
return self.latest_version.page_count
@property
def pages_all(self):
try:
return self.latest_version.pages_all
except AttributeError:
# Document has no version yet
DocumentPage = apps.get_model(
app_label='documents', model_name='DocumentPage'
)
return DocumentPage.objects.none()
@property
def pages(self):
try:

View File

@@ -4,14 +4,13 @@ import logging
from furl import furl
from django.core.files.base import ContentFile
from django.db import models
from django.urls import reverse
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from mayan.apps.converter.literals import DEFAULT_ZOOM_LEVEL, DEFAULT_ROTATION
from mayan.apps.converter.models import Transformation
from mayan.apps.converter.transformations import (
BaseTransformation, TransformationResize, TransformationRotate,
@@ -19,16 +18,17 @@ from mayan.apps.converter.transformations import (
)
from mayan.apps.converter.utils import get_converter_class
from ..managers import DocumentPageManager
from ..managers import DocumentPageCachedImage, DocumentPageManager
from ..settings import (
setting_disable_base_image_cache, setting_disable_transformed_image_cache,
setting_display_width, setting_display_height, setting_zoom_max_level,
setting_zoom_min_level
)
from ..storages import storage_documentimagecache
from .document_version_models import DocumentVersion
__all__ = ('DocumentPage', 'DocumentPageResult')
__all__ = ('DocumentPage', 'DocumentPageCachedImage', 'DocumentPageResult')
logger = logging.getLogger(__name__)
@@ -38,17 +38,15 @@ class DocumentPage(models.Model):
Model that describes a document version page
"""
document_version = models.ForeignKey(
on_delete=models.CASCADE, related_name='version_pages', to=DocumentVersion,
on_delete=models.CASCADE, related_name='pages', to=DocumentVersion,
verbose_name=_('Document version')
)
enabled = models.BooleanField(default=True, verbose_name=_('Enabled'))
page_number = models.PositiveIntegerField(
db_index=True, default=1, editable=False,
verbose_name=_('Page number')
)
objects = DocumentPageManager()
passthrough = models.Manager()
class Meta:
ordering = ('page_number',)
@@ -58,15 +56,12 @@ class DocumentPage(models.Model):
def __str__(self):
return self.get_label()
@cached_property
def cache_partition(self):
partition, created = self.document_version.cache.partitions.get_or_create(
name=self.uuid
)
return partition
@property
def cache_filename(self):
return 'page-cache-{}'.format(self.uuid)
def delete(self, *args, **kwargs):
self.cache_partition.delete()
self.invalidate_cache()
super(DocumentPage, self).delete(*args, **kwargs)
def detect_orientation(self):
@@ -85,24 +80,29 @@ class DocumentPage(models.Model):
def generate_image(self, *args, **kwargs):
transformation_list = self.get_combined_transformation_list(*args, **kwargs)
combined_cache_filename = BaseTransformation.combine(transformation_list)
cache_filename = '{}-{}'.format(
self.cache_filename, BaseTransformation.combine(transformation_list)
)
# Check is transformed image is available
logger.debug('transformations cache filename: %s', combined_cache_filename)
logger.debug('transformations cache filename: %s', cache_filename)
if not setting_disable_transformed_image_cache.value and self.cache_partition.get_file(filename=combined_cache_filename):
if not setting_disable_transformed_image_cache.value and storage_documentimagecache.exists(cache_filename):
logger.debug(
'transformations cache file "%s" found', combined_cache_filename
'transformations cache file "%s" found', cache_filename
)
else:
logger.debug(
'transformations cache file "%s" not found', combined_cache_filename
'transformations cache file "%s" not found', cache_filename
)
image = self.get_image(transformations=transformation_list)
with self.cache_partition.create_file(filename=combined_cache_filename) as file_object:
with storage_documentimagecache.open(cache_filename, 'wb+') as file_object:
file_object.write(image.getvalue())
return combined_cache_filename
self.cached_images.create(filename=cache_filename)
return cache_filename
def get_absolute_url(self):
return reverse(
@@ -159,6 +159,7 @@ class DocumentPage(models.Model):
zoom_level = setting_zoom_max_level.value
# Generate transformation hash
transformation_list = []
# Stored transformations first
@@ -185,15 +186,13 @@ class DocumentPage(models.Model):
return transformation_list
def get_image(self, transformations=None):
cache_filename = 'base_image'
cache_filename = self.cache_filename
logger.debug('Page cache filename: %s', cache_filename)
cache_file = self.cache_partition.get_file(filename=cache_filename)
if not setting_disable_base_image_cache.value and cache_file:
if not setting_disable_base_image_cache.value and storage_documentimagecache.exists(cache_filename):
logger.debug('Page cache file "%s" found', cache_filename)
with cache_file.open() as file_object:
with storage_documentimagecache.open(cache_filename) as file_object:
converter = get_converter_class()(
file_object=file_object
)
@@ -201,8 +200,8 @@ class DocumentPage(models.Model):
converter.seek_page(page_number=0)
# This code is also repeated below to allow using a context
# manager with cache_file.open and close it automatically.
# Apply runtime transformations
# manager with storage_documentimagecache.open and close it
# automatically.
for transformation in transformations:
converter.transform(transformation=transformation)
@@ -219,11 +218,14 @@ class DocumentPage(models.Model):
page_image = converter.get_page()
# Since open "wb+" doesn't create files, create it explicitly
with self.cache_partition.create_file(filename=cache_filename) as file_object:
# Since open "wb+" doesn't create files, check if the file
# exists, if not then create it
if not storage_documentimagecache.exists(cache_filename):
storage_documentimagecache.save(name=cache_filename, content=ContentFile(content=''))
with storage_documentimagecache.open(cache_filename, 'wb+') as file_object:
file_object.write(page_image.getvalue())
# Apply runtime transformations
for transformation in transformations:
converter.transform(transformation=transformation)
@@ -234,8 +236,14 @@ class DocumentPage(models.Model):
'Error creating page cache file "%s"; %s',
cache_filename, exception
)
storage_documentimagecache.delete(cache_filename)
raise
def invalidate_cache(self):
storage_documentimagecache.delete(self.cache_filename)
for cached_image in self.cached_images.all():
cached_image.delete()
@property
def is_in_trash(self):
return self.document.is_in_trash
@@ -246,7 +254,7 @@ class DocumentPage(models.Model):
) % {
'document': force_text(self.document),
'page_num': self.page_number,
'total_pages': self.document_version.pages_all.count()
'total_pages': self.document_version.pages.count()
}
get_label.short_description = _('Label')
@@ -269,6 +277,38 @@ class DocumentPage(models.Model):
return '{}-{}'.format(self.document_version.uuid, self.pk)
class DocumentPageCachedImage(models.Model):
document_page = models.ForeignKey(
on_delete=models.CASCADE, related_name='cached_images',
to=DocumentPage, verbose_name=_('Document page')
)
datetime = models.DateTimeField(
auto_now_add=True, db_index=True, verbose_name=_('Date time')
)
filename = models.CharField(max_length=128, verbose_name=_('Filename'))
file_size = models.PositiveIntegerField(
db_index=True, default=0, verbose_name=_('File size')
)
objects = DocumentPageCachedImage()
class Meta:
verbose_name = _('Document page cached image')
verbose_name_plural = _('Document page cached images')
def delete(self, *args, **kwargs):
storage_documentimagecache.delete(self.filename)
return super(DocumentPageCachedImage, self).delete(*args, **kwargs)
def natural_key(self):
return (self.filename, self.document_page.natural_key())
natural_key.dependencies = ['documents.DocumentPage']
def save(self, *args, **kwargs):
self.file_size = storage_documentimagecache.size(self.filename)
return super(DocumentPageCachedImage, self).save(*args, **kwargs)
class DocumentPageResult(DocumentPage):
class Meta:
ordering = ('document_version__document', 'page_number')

View File

@@ -7,11 +7,11 @@ import shutil
import uuid
from django.apps import apps
from django.core.files.base import ContentFile
from django.db import models, transaction
from django.template import Template, Context
from django.urls import reverse
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from mayan.apps.converter.exceptions import InvalidOfficeFormat, PageCountError
@@ -21,11 +21,10 @@ from mayan.apps.converter.utils import get_converter_class
from mayan.apps.mimetype.api import get_mimetype
from ..events import event_document_new_version, event_document_version_revert
from ..literals import DOCUMENT_IMAGES_CACHE_NAME
from ..managers import DocumentVersionManager
from ..settings import setting_fix_orientation, setting_hash_block_size
from ..signals import post_document_created, post_version_upload
from ..storages import storage_documentversion
from ..storages import storage_documentversion, storage_documentimagecache
from .document_models import Document
@@ -62,6 +61,14 @@ class DocumentVersion(models.Model):
_pre_open_hooks = {}
_post_save_hooks = {}
@classmethod
def register_pre_open_hook(cls, order, func):
cls._pre_open_hooks[order] = func
@classmethod
def register_post_save_hook(cls, order, func):
cls._post_save_hooks[order] = func
document = models.ForeignKey(
on_delete=models.CASCADE, related_name='versions', to=Document,
verbose_name=_('Document')
@@ -111,35 +118,18 @@ class DocumentVersion(models.Model):
objects = DocumentVersionManager()
@classmethod
def register_pre_open_hook(cls, order, func):
cls._pre_open_hooks[order] = func
@classmethod
def register_post_save_hook(cls, order, func):
cls._post_save_hooks[order] = func
def __str__(self):
return self.get_rendered_string()
@cached_property
def cache(self):
Cache = apps.get_model(app_label='file_caching', model_name='Cache')
return Cache.objects.get(name=DOCUMENT_IMAGES_CACHE_NAME)
@cached_property
def cache_partition(self):
partition, created = self.cache.partitions.get_or_create(
name='version-{}'.format(self.uuid)
)
return partition
@property
def cache_filename(self):
return 'document-version-{}'.format(self.uuid)
def delete(self, *args, **kwargs):
for page in self.pages.all():
page.delete()
self.file.storage.delete(self.file.name)
self.cache_partition.delete()
return super(DocumentVersion, self).delete(*args, **kwargs)
@@ -174,36 +164,43 @@ class DocumentVersion(models.Model):
return first_page.get_api_image_url(*args, **kwargs)
def get_intermediate_file(self):
cache_filename = 'intermediate_file'
cache_file = self.cache_partition.get_file(filename=cache_filename)
if cache_file:
logger.debug('Intermidiate file found.')
return cache_file.open()
cache_filename = self.cache_filename
logger.debug('Intermidiate filename: %s', cache_filename)
if storage_documentimagecache.exists(cache_filename):
logger.debug('Intermidiate file "%s" found.', cache_filename)
return storage_documentimagecache.open(cache_filename)
else:
logger.debug('Intermidiate file not found.')
logger.debug('Intermidiate file "%s" not found.', cache_filename)
try:
with self.open() as version_file_object:
converter = get_converter_class()(
file_object=version_file_object
)
converter = get_converter_class()(file_object=version_file_object)
with converter.to_pdf() as pdf_file_object:
with self.cache_partition.create_file(filename=cache_filename) as file_object:
# Since open "wb+" doesn't create files, check if the file
# exists, if not then create it
if not storage_documentimagecache.exists(cache_filename):
storage_documentimagecache.save(
name=cache_filename, content=ContentFile(content='')
)
with storage_documentimagecache.open(cache_filename, mode='wb+') as file_object:
shutil.copyfileobj(
fsrc=pdf_file_object, fdst=file_object
)
return self.cache_partition.get_file(filename=cache_filename).open()
return storage_documentimagecache.open(cache_filename)
except InvalidOfficeFormat:
return self.open()
except Exception as exception:
# Cleanup in case of error
logger.error(
'Error creating intermediate file "%s"; %s.',
cache_filename, exception
)
cache_file = self.cache_partition.get_file(filename=cache_filename)
if cache_file:
cache_file.delete()
storage_documentimagecache.delete(cache_filename)
raise
def get_rendered_string(self, preserve_extension=False):
@@ -226,6 +223,11 @@ class DocumentVersion(models.Model):
return (self.checksum, self.document.natural_key())
natural_key.dependencies = ['documents.Document']
def invalidate_cache(self):
storage_documentimagecache.delete(self.cache_filename)
for page in self.pages.all():
page.invalidate_cache()
@property
def is_in_trash(self):
return self.document.is_in_trash
@@ -246,17 +248,6 @@ class DocumentVersion(models.Model):
return result
@property
def pages_all(self):
DocumentPage = apps.get_model(
app_label='documents', model_name='DocumentPage'
)
return DocumentPage.passthrough.filter(document_version=self)
@property
def pages(self):
return self.version_pages.all()
@property
def page_count(self):
"""

View File

@@ -61,6 +61,10 @@ queue_documents_periodic.add_task_type(
schedule=timedelta(seconds=DELETE_STALE_STUBS_INTERVAL),
)
queue_tools.add_task_type(
dotted_path='mayan.apps.documents.tasks.task_clear_image_cache',
label=_('Clear image cache')
)
queue_tools.add_task_type(
dotted_path='mayan.apps.documents.tasks.task_scan_duplicates_all',
label=_('Duplicated document scan')

View File

@@ -8,22 +8,11 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.smart_settings.classes import Namespace
from .literals import (
DEFAULT_DOCUMENTS_CACHE_MAXIMUM_SIZE, DEFAULT_DOCUMENTS_HASH_BLOCK_SIZE,
DEFAULT_LANGUAGE, DEFAULT_LANGUAGE_CODES
DEFAULT_DOCUMENTS_HASH_BLOCK_SIZE, DEFAULT_LANGUAGE, DEFAULT_LANGUAGE_CODES
)
from .utils import callback_update_cache_size
namespace = Namespace(label=_('Documents'), name='documents')
setting_document_cache_maximum_size = namespace.add_setting(
global_name='DOCUMENTS_CACHE_MAXIMUM_SIZE',
default=DEFAULT_DOCUMENTS_CACHE_MAXIMUM_SIZE,
help_text=_(
'The threshold at which the DOCUMENT_CACHE_STORAGE_BACKEND will start '
'deleting the oldest document image cache files. Specify the size in '
'bytes.'
), post_edit_function=callback_update_cache_size
)
setting_documentimagecache_storage = namespace.add_setting(
global_name='DOCUMENTS_CACHE_STORAGE_BACKEND',
default='django.core.files.storage.FileSystemStorage', help_text=_(

View File

@@ -42,6 +42,17 @@ def task_check_trash_periods():
DocumentType.objects.check_trash_periods()
@app.task(ignore_result=True)
def task_clear_image_cache():
Document = apps.get_model(
app_label='documents', model_name='Document'
)
logger.info('Starting document cache invalidation')
Document.objects.invalidate_cache()
logger.info('Finished document cache invalidation')
@app.task(ignore_result=True)
def task_delete_document(trashed_document_id):
DeletedDocument = apps.get_model(
@@ -71,7 +82,8 @@ def task_generate_document_page_image(document_page_id, *args, **kwargs):
app_label='documents', model_name='DocumentPage'
)
document_page = DocumentPage.passthrough.get(pk=document_page_id)
document_page = DocumentPage.objects.get(pk=document_page_id)
return document_page.generate_image(*args, **kwargs)

View File

@@ -154,11 +154,11 @@ class DocumentAPITestCase(DocumentTestMixin, BaseAPITestCase):
auto_upload_document = False
def _request_document_upload(self):
with open(TEST_DOCUMENT_PATH, mode='rb') as file_object:
with open(TEST_DOCUMENT_PATH, mode='rb') as file_descriptor:
return self.post(
viewname='rest_api:document-list', data={
'document_type': self.test_document_type.pk,
'file': file_object
'file': file_descriptor
}
)
@@ -208,12 +208,12 @@ class DocumentAPITestCase(DocumentTestMixin, BaseAPITestCase):
# is the latest.
time.sleep(1)
with open(TEST_DOCUMENT_PATH, mode='rb') as file_object:
with open(TEST_DOCUMENT_PATH, mode='rb') as file_descriptor:
return self.post(
viewname='rest_api:document-version-list', kwargs={
'pk': self.test_document.pk,
}, data={
'comment': '', 'file': file_object,
'comment': '', 'file': file_descriptor,
}
)

View File

@@ -2,152 +2,11 @@ from __future__ import unicode_literals
from django.utils.encoding import force_text
from ..permissions import (
permission_document_edit, permission_document_view
)
from ..permissions import permission_document_view
from .base import GenericDocumentViewTestCase
class DocumentPageDisableViewTestCase(GenericDocumentViewTestCase):
def setUp(self):
super(DocumentPageDisableViewTestCase, self).setUp()
self.test_document_page = self.test_document.pages_all.first()
def _request_test_document_page_disable_view(self):
return self.post(
viewname='documents:document_page_disable', kwargs={
'pk': self.test_document_page.pk
}
)
def test_document_page_disable_view_no_permission(self):
test_document_page_count = self.test_document.pages.count()
response = self._request_test_document_page_disable_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(
test_document_page_count, self.test_document.pages.count()
)
def test_document_page_disable_view_with_access(self):
self.grant_access(
obj=self.test_document, permission=permission_document_edit
)
test_document_page_count = self.test_document.pages.count()
response = self._request_test_document_page_disable_view()
self.assertEqual(response.status_code, 302)
self.assertNotEqual(
test_document_page_count, self.test_document.pages.count()
)
def _request_test_document_page_multiple_disable_view(self):
return self.post(
viewname='documents:document_page_multiple_disable', data={
'id_list': self.test_document_page.pk
}
)
def test_document_page_multiple_disable_view_no_permission(self):
test_document_page_count = self.test_document.pages.count()
response = self._request_test_document_page_multiple_disable_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(
test_document_page_count, self.test_document.pages.count()
)
def test_document_page_multiple_disable_view_with_access(self):
self.grant_access(
obj=self.test_document, permission=permission_document_edit
)
test_document_page_count = self.test_document.pages.count()
response = self._request_test_document_page_multiple_disable_view()
self.assertEqual(response.status_code, 302)
self.assertNotEqual(
test_document_page_count, self.test_document.pages.count()
)
def _disable_test_document_page(self):
self.test_document_page.enabled = False
self.test_document_page.save()
def _request_test_document_page_enable_view(self):
return self.post(
viewname='documents:document_page_enable', kwargs={
'pk': self.test_document_page.pk
}
)
def test_document_page_enable_view_no_permission(self):
self._disable_test_document_page()
test_document_page_count = self.test_document.pages.count()
response = self._request_test_document_page_enable_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(
test_document_page_count, self.test_document.pages.count()
)
def test_document_page_enable_view_with_access(self):
self._disable_test_document_page()
self.grant_access(
obj=self.test_document, permission=permission_document_edit
)
test_document_page_count = self.test_document.pages.count()
response = self._request_test_document_page_enable_view()
self.assertEqual(response.status_code, 302)
self.assertNotEqual(
test_document_page_count, self.test_document.pages.count()
)
def _request_test_document_page_multiple_enable_view(self):
return self.post(
viewname='documents:document_page_multiple_enable', data={
'id_list': self.test_document_page.pk
}
)
def test_document_page_multiple_enable_view_no_permission(self):
self._disable_test_document_page()
test_document_page_count = self.test_document.pages.count()
response = self._request_test_document_page_multiple_enable_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(
test_document_page_count, self.test_document.pages.count()
)
def test_document_page_multiple_enable_view_with_access(self):
self._disable_test_document_page()
self.grant_access(
obj=self.test_document, permission=permission_document_edit
)
test_document_page_count = self.test_document.pages.count()
response = self._request_test_document_page_multiple_enable_view()
self.assertEqual(response.status_code, 302)
self.assertNotEqual(
test_document_page_count, self.test_document.pages.count()
)
class DocumentPageViewTestCase(GenericDocumentViewTestCase):
def _request_test_document_page_list_view(self):
return self.get(

File diff suppressed because it is too large Load Diff

View File

@@ -2,17 +2,9 @@ from __future__ import unicode_literals
import pycountry
from django.apps import apps
from django.utils.translation import ugettext_lazy as _
from .literals import DOCUMENT_IMAGES_CACHE_NAME
def callback_update_cache_size(setting):
Cache = apps.get_model(app_label='file_caching', model_name='Cache')
cache = Cache.objects.get(name=DOCUMENT_IMAGES_CACHE_NAME)
cache.maximum_size = setting.value
cache.save()
from .settings import setting_language_codes
def get_language(language_code):
@@ -27,8 +19,6 @@ def get_language(language_code):
def get_language_choices():
from .settings import setting_language_codes
return sorted(
[
(

View File

@@ -7,12 +7,10 @@ from furl import furl
from django.contrib import messages
from django.urls import reverse
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _, ungettext
from django.utils.translation import ugettext_lazy as _
from django.views.generic import RedirectView
from mayan.apps.common.generics import (
MultipleObjectConfirmActionView, SimpleView, SingleObjectListView
)
from mayan.apps.common.generics import SimpleView, SingleObjectListView
from mayan.apps.common.mixins import ExternalObjectMixin
from mayan.apps.common.settings import setting_home_view
from mayan.apps.common.utils import resolve
@@ -22,20 +20,19 @@ from ..forms import DocumentPageForm
from ..icons import icon_document_pages
from ..links import link_document_update_page_count
from ..models import Document, DocumentPage
from ..permissions import permission_document_edit, permission_document_view
from ..permissions import permission_document_view
from ..settings import (
setting_rotation_step, setting_zoom_percent_step, setting_zoom_max_level,
setting_zoom_min_level
)
__all__ = (
'DocumentPageDisable', 'DocumentPageEnable', 'DocumentPageListView',
'DocumentPageNavigationFirst', 'DocumentPageNavigationLast',
'DocumentPageNavigationNext', 'DocumentPageNavigationPrevious',
'DocumentPageView', 'DocumentPageViewResetView',
'DocumentPageInteractiveTransformation', 'DocumentPageZoomInView',
'DocumentPageZoomOutView', 'DocumentPageRotateLeftView',
'DocumentPageRotateRightView'
'DocumentPageListView', 'DocumentPageNavigationFirst',
'DocumentPageNavigationLast', 'DocumentPageNavigationNext',
'DocumentPageNavigationPrevious', 'DocumentPageView',
'DocumentPageViewResetView', 'DocumentPageInteractiveTransformation',
'DocumentPageZoomInView', 'DocumentPageZoomOutView',
'DocumentPageRotateLeftView', 'DocumentPageRotateRightView'
)
logger = logging.getLogger(__name__)
@@ -65,7 +62,7 @@ class DocumentPageListView(ExternalObjectMixin, SingleObjectListView):
}
def get_source_queryset(self):
return self.external_object.pages_all
return self.external_object.pages.all()
class DocumentPageNavigationBase(ExternalObjectMixin, RedirectView):
@@ -131,17 +128,17 @@ class DocumentPageNavigationNext(DocumentPageNavigationBase):
def get_new_kwargs(self):
document_page = self.get_object()
new_document_page = document_page.siblings.filter(
page_number__gt=document_page.page_number
).first()
if new_document_page:
return {'pk': new_document_page.pk}
else:
try:
document_page = document_page.siblings.get(
page_number=document_page.page_number + 1
)
except DocumentPage.DoesNotExist:
messages.warning(
message=_(
'There are no more pages in this document'
), request=self.request
)
finally:
return {'pk': document_page.pk}
@@ -149,17 +146,17 @@ class DocumentPageNavigationPrevious(DocumentPageNavigationBase):
def get_new_kwargs(self):
document_page = self.get_object()
new_document_page = document_page.siblings.filter(
page_number__lt=document_page.page_number
).last()
if new_document_page:
return {'pk': new_document_page.pk}
else:
try:
document_page = document_page.siblings.get(
page_number=document_page.page_number - 1
)
except DocumentPage.DoesNotExist:
messages.warning(
message=_(
'You are already at the first page of this document'
), request=self.request
)
finally:
return {'pk': document_page.pk}
@@ -264,63 +261,3 @@ class DocumentPageRotateRightView(DocumentPageInteractiveTransformation):
query_dict['rotation'] = (
int(query_dict['rotation']) + setting_rotation_step.value
) % 360
class DocumentPageDisable(MultipleObjectConfirmActionView):
object_permission = permission_document_edit
pk_url_kwarg = 'pk'
success_message_singular = '%(count)d document page disabled.'
success_message_plural = '%(count)d document pages disabled.'
def get_extra_context(self):
queryset = self.object_list
result = {
'title': ungettext(
singular='Disable the selected document page?',
plural='Disable the selected document pages?',
number=queryset.count()
)
}
if queryset.count() == 1:
result['object'] = queryset.first()
return result
def get_source_queryset(self):
return DocumentPage.passthrough.all()
def object_action(self, form, instance):
instance.enabled = False
instance.save()
class DocumentPageEnable(MultipleObjectConfirmActionView):
object_permission = permission_document_edit
pk_url_kwarg = 'pk'
success_message_singular = '%(count)d document page enabled.'
success_message_plural = '%(count)d document pages enabled.'
def get_extra_context(self):
queryset = self.object_list
result = {
'title': ungettext(
singular='Enable the selected document page?',
plural='Enable the selected document pages?',
number=queryset.count()
)
}
if queryset.count() == 1:
result['object'] = queryset.first()
return result
def get_source_queryset(self):
return DocumentPage.passthrough.all()
def object_action(self, form, instance):
instance.enabled = True
instance.save()

View File

@@ -8,12 +8,25 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.generics import ConfirmView
from ..permissions import permission_document_tools
from ..tasks import task_scan_duplicates_all
from ..tasks import task_clear_image_cache, task_scan_duplicates_all
__all__ = ('ScanDuplicatedDocuments',)
__all__ = ('ClearImageCacheView', 'ScanDuplicatedDocuments')
logger = logging.getLogger(__name__)
class ClearImageCacheView(ConfirmView):
extra_context = {
'title': _('Clear the document image cache?')
}
view_permission = permission_document_tools
def view_action(self):
task_clear_image_cache.apply_async()
messages.success(
self.request, _('Document cache clearing queued successfully.')
)
class ScanDuplicatedDocuments(ConfirmView):
extra_context = {
'title': _('Scan for duplicated documents?')

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