Compare commits
202 Commits
features/f
...
versions/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e2ef57e00 | ||
|
|
756765ce4a | ||
|
|
53096b8bdd | ||
|
|
8aa2567a56 | ||
|
|
ce6e568001 | ||
|
|
d1f0e23c53 | ||
|
|
3f33bdd9c2 | ||
|
|
b2390843ab | ||
|
|
fc14341d40 | ||
|
|
57dd5b1bca | ||
|
|
c731ab7050 | ||
|
|
bd0d298be3 | ||
|
|
cc8147d002 | ||
|
|
1b327b99f0 | ||
|
|
7b3a83ee39 | ||
|
|
4659269349 | ||
|
|
517bb4e9a2 | ||
|
|
162cd256e7 | ||
|
|
339b7dd836 | ||
|
|
949c0ab285 | ||
|
|
cb6cb4121f | ||
|
|
042727aaa9 | ||
|
|
5b304ea742 | ||
|
|
ce4413d539 | ||
|
|
547c31d216 | ||
|
|
f4293a7c06 | ||
|
|
1779d482ac | ||
|
|
e0e4f238f6 | ||
|
|
fecfb37a84 | ||
|
|
3e2aaf391e | ||
|
|
230fde0ab2 | ||
|
|
d9865af200 | ||
|
|
72f8fcf720 | ||
|
|
30668d9d0b | ||
|
|
d5aab12b8d | ||
|
|
ebc0a5f449 | ||
|
|
415d3bcd2f | ||
|
|
b985f2ef05 | ||
|
|
15c953815e | ||
|
|
390e552c1f | ||
|
|
9041f00caa | ||
|
|
b0163319eb | ||
|
|
762cdc5b89 | ||
|
|
396cbb4b22 | ||
|
|
8b0cd93526 | ||
|
|
f97ccb693b | ||
|
|
c3b539ba19 | ||
|
|
d190dbca03 | ||
|
|
4384452423 | ||
|
|
76fef4f247 | ||
|
|
f5d0e4d560 | ||
|
|
4b3ab82ee2 | ||
|
|
f8eda67bd5 | ||
|
|
58bcf20a46 | ||
|
|
49979dede5 | ||
| 09f481f5f0 | |||
|
|
a250919acc | ||
|
|
38980e5f75 | ||
|
|
6503d9474d | ||
|
|
e7734def58 | ||
|
|
f50d22b382 | ||
|
|
ad37228466 | ||
|
|
0917bd57b3 | ||
|
|
4dd270e75b | ||
|
|
3428c6aa20 | ||
|
|
eb1fb8511b | ||
|
|
bdbc7ef086 | ||
|
|
abea863184 | ||
|
|
d394583729 | ||
|
|
4db59c0808 | ||
|
|
12f24316a1 | ||
|
|
ef0843276b | ||
|
|
e20102333e | ||
|
|
4ecf075fd4 | ||
|
|
cc81a6905a | ||
|
|
3c9454160f | ||
|
|
2e1600c334 | ||
|
|
3e9f30cb91 | ||
|
|
a3a78f755d | ||
|
|
3988dedebf | ||
|
|
ff34c7d00a | ||
|
|
fe2de33e98 | ||
|
|
3efd1bd89d | ||
|
|
ea516cbc23 | ||
|
|
52ad3e7418 | ||
|
|
a001b4bbb3 | ||
|
|
31ed0e1ac8 | ||
|
|
9ad82695d9 | ||
|
|
69af4dd6b3 | ||
|
|
1c7ceca432 | ||
|
|
c05dcf5b05 | ||
|
|
85b05dd6ec | ||
|
|
9752584135 | ||
|
|
fd0d5728a1 | ||
|
|
88863fd6d0 | ||
|
|
3a7025d9c4 | ||
|
|
150c5d8cc2 | ||
|
|
93ba547350 | ||
|
|
f920dffc01 | ||
|
|
c2e99e6efb | ||
|
|
ff6674cc4a | ||
|
|
669dfeb30a | ||
|
|
6635bb4235 | ||
|
|
88bc29e4d7 | ||
|
|
9315776926 | ||
|
|
40a306996c | ||
|
|
033cecd946 | ||
|
|
ee63829e7f | ||
|
|
e4bc007bba | ||
|
|
84b329f661 | ||
|
|
4c73239dde | ||
|
|
2e12a6af41 | ||
|
|
3d7e6b6fbe | ||
|
|
6f907d156a | ||
|
|
fac77a2f73 | ||
|
|
0c3b6e5388 | ||
|
|
e652c7208c | ||
|
|
53928b2ab6 | ||
|
|
afc6b54520 | ||
|
|
070355033e | ||
|
|
0029d3074f | ||
|
|
4558894faf | ||
|
|
adeea6247f | ||
|
|
3563297d48 | ||
|
|
1e1b4dedf4 | ||
|
|
d65bbb718a | ||
|
|
5352c6ac6f | ||
|
|
cb7d5bf82a | ||
|
|
41a7d00e9e | ||
|
|
82d7339a64 | ||
|
|
e889021f43 | ||
|
|
d3a53fb53a | ||
|
|
b6565effb5 | ||
|
|
cf43ef2f73 | ||
|
|
6ca2845d19 | ||
|
|
ab601f9180 | ||
|
|
0b42567179 | ||
|
|
030ddd68e0 | ||
|
|
649571ebb1 | ||
|
|
b99bb88008 | ||
|
|
fd08a23339 | ||
|
|
917ec55ada | ||
|
|
ec4644b5c9 | ||
|
|
ff86c4c518 | ||
|
|
daebf2ddcc | ||
|
|
49a16acdf5 | ||
|
|
8c064c953a | ||
|
|
3c7a23a5a9 | ||
|
|
6bcf35bef5 | ||
|
|
7ef6102876 | ||
|
|
4363bba0fe | ||
|
|
e2f2181ebb | ||
|
|
d4f7e2cd16 | ||
|
|
058e36b4a9 | ||
|
|
1ddd5f26b1 | ||
|
|
44652d49fb | ||
|
|
119c1bde76 | ||
|
|
ed227b4111 | ||
|
|
c44090aca6 | ||
|
|
8a7da6a103 | ||
|
|
3e3b1f75a0 | ||
|
|
1ab7b7b9b1 | ||
|
|
3fab5c1427 | ||
|
|
516c3aeb2c | ||
|
|
3ac1000b46 | ||
|
|
4adeefc978 | ||
|
|
8bc4b6a95e | ||
|
|
37e85590e8 | ||
|
|
78a0189e1c | ||
|
|
91b0b2d9c3 | ||
|
|
8a54deba3d | ||
|
|
22da1e0a78 | ||
|
|
c9668d62e5 | ||
|
|
7a01a77c43 | ||
|
|
9564db398f | ||
|
|
7faa24eb7b | ||
|
|
51f278301b | ||
|
|
2cc35c3c61 | ||
|
|
8c73fda1ae | ||
|
|
8811c8269f | ||
|
|
f36f99c5fb | ||
|
|
0e972eff06 | ||
|
|
7913b5ddcc | ||
|
|
1c86ea5b5b | ||
|
|
ec6a3bd960 | ||
|
|
080553c797 | ||
|
|
08ee07e652 | ||
|
|
d7d77fcb55 | ||
|
|
bb5324ef50 | ||
|
|
4c212f6ea4 | ||
|
|
941356ed69 | ||
|
|
97804b255b | ||
|
|
06c3ef6583 | ||
|
|
6cd857e2bf | ||
|
|
fbb0f0b9bd | ||
|
|
9e068c3e83 | ||
|
|
72a3807354 | ||
|
|
109fcba795 | ||
|
|
01380e0572 | ||
|
|
5146c6d202 | ||
|
|
300bdbfc8a | ||
|
|
a0331e0236 |
@@ -19,7 +19,7 @@ job_docker_build:
|
||||
- docker build --pull -t "$CI_REGISTRY_IMAGE" -f docker/Dockerfile .
|
||||
- VERSION=`cat docker/rootfs/version`
|
||||
- docker tag "$CI_REGISTRY_IMAGE" "$CI_REGISTRY_IMAGE:$VERSION"
|
||||
- docker run --rm "$CI_REGISTRY_IMAGE:$VERSION" run-tests
|
||||
- docker run --rm "$CI_REGISTRY_IMAGE:$VERSION" run_tests
|
||||
- docker push "$CI_REGISTRY_IMAGE:$VERSION"
|
||||
- docker push "$CI_REGISTRY_IMAGE:latest"
|
||||
- docker tag "$CI_REGISTRY_IMAGE:$VERSION" registry-1.docker.io/mayanedms/mayanedms:"$VERSION"
|
||||
@@ -58,7 +58,7 @@ job_docker_nightly:
|
||||
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
|
||||
script:
|
||||
- docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" -f docker/Dockerfile .
|
||||
- docker run --rm "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" run-tests
|
||||
- docker run --rm "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" run_tests
|
||||
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
|
||||
only:
|
||||
- nightly
|
||||
@@ -152,7 +152,9 @@ job_push_python:
|
||||
- locale-gen en_US.UTF-8
|
||||
- update-locale LANG=en_US.UTF-8
|
||||
- export LC_ALL=en_US.UTF-8
|
||||
- apt-get install -qq curl exiftool gcc ghostscript gnupg1 graphviz libfuse2 libjpeg-dev libmagic1 libpng-dev libtiff-dev poppler-utils libreoffice poppler-utils python-dev python-pip tesseract-ocr tesseract-ocr-deu
|
||||
- apt-get install -qq curl exiftool gcc ghostscript gnupg1 graphviz libfuse2 libjpeg-dev libmagic1 libpng-dev libtiff-dev poppler-utils libreoffice poppler-utils python-dev python-virtualenv python3-dev tesseract-ocr tesseract-ocr-deu
|
||||
- virtualenv venv -p /usr/bin/python3
|
||||
- . venv/bin/activate
|
||||
- pip install -r requirements.txt -r requirements/testing-base.txt
|
||||
only:
|
||||
- releases/all
|
||||
@@ -170,6 +172,7 @@ test-mysql:
|
||||
- mysql:8.0.3
|
||||
script:
|
||||
- apt-get install -qq libmysqlclient-dev mysql-client
|
||||
- . venv/bin/activate
|
||||
- pip install mysqlclient
|
||||
- mysql -h"$MYSQL_PORT_3306_TCP_ADDR" -P"$MYSQL_PORT_3306_TCP_PORT" -uroot -p"$MYSQL_ENV_MYSQL_ROOT_PASSWORD" -e "set global character_set_server=utf8mb4;"
|
||||
- python manage.py test --mayan-apps --settings=mayan.settings.testing.gitlab-ci.db_mysql --nomigrations
|
||||
@@ -185,6 +188,7 @@ test-postgres:
|
||||
- postgres
|
||||
script:
|
||||
- apt-get install -qq libpq-dev
|
||||
- . venv/bin/activate
|
||||
- pip install psycopg2
|
||||
- python manage.py test --mayan-apps --settings=mayan.settings.testing.gitlab-ci.db_postgres --nomigrations
|
||||
tags:
|
||||
@@ -193,6 +197,7 @@ test-postgres:
|
||||
test-sqlite:
|
||||
<<: *test_base
|
||||
script:
|
||||
- . venv/bin/activate
|
||||
- python manage.py test --mayan-apps --settings=mayan.settings.testing.gitlab-ci --nomigrations
|
||||
|
||||
deploy_demo:
|
||||
|
||||
13
.tx/config
13
.tx/config
@@ -115,6 +115,12 @@ 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
|
||||
@@ -222,3 +228,10 @@ 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
|
||||
|
||||
|
||||
89
HISTORY.rst
89
HISTORY.rst
@@ -1,4 +1,90 @@
|
||||
3.2.8 (2019-XX-XX)
|
||||
3.3 (2019-XX-XX)
|
||||
================
|
||||
- Add support for icon shadows.
|
||||
- Add icons and no-result template to the object error log view and
|
||||
links.
|
||||
- Use Select2 widget for the document type selection form.
|
||||
- Backport the vertical main menu update.
|
||||
- Backport workflow preview refactor. GitLab issue #532.
|
||||
- Add support for source column inheritance.
|
||||
- Add support for source column exclusion.
|
||||
- Backport workflow context support.
|
||||
- Backport workflow transitions field support.
|
||||
- Backport workflow email action.
|
||||
- Backport individual index rebuild support.
|
||||
- Rename the installjavascript command to installdependencies.
|
||||
- Remove database conversion command.
|
||||
- Remove support for quoted configuration entries. Support unquoted,
|
||||
nested dictionaries in the configuration. Requires manual
|
||||
update of existing config.yml files.
|
||||
- Support user specified locations for the configuration file with the
|
||||
CONFIGURATION_FILEPATH (MAYAN_CONFIGURATION_FILEPATH environment variable),
|
||||
and CONFIGURATION_LAST_GOOD_FILEPATH
|
||||
(MAYAN_CONFIGURATION_LAST_GOOD_FILEPATH environment variable) settings.
|
||||
- Move bootstrapped settings code to their own module in the smart_settings
|
||||
apps.
|
||||
- Remove individual database configuration options. All database
|
||||
configuration is now done using MAYAN_DATABASES to mirror Django way of
|
||||
doing atabase etup.
|
||||
- Added support for YAML encoded environment variables to the platform
|
||||
templates apps.
|
||||
- Move YAML code to its own module.
|
||||
- Move Django and Celery settings.
|
||||
- Backport FakeStorageSubclass from versions/next.
|
||||
- Remove django-environ.
|
||||
- Support checking in and out multiple documents.
|
||||
- Remove encapsulate helper.
|
||||
- Add support for menu inheritance.
|
||||
- Emphasize source column labels.
|
||||
- Backport file cache manager app.
|
||||
- Convert document image cache to use file cache manager app.
|
||||
Add setting DOCUMENTS_CACHE_MAXIMUM_SIZE defaults to 500 MB.
|
||||
- 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.
|
||||
- Add support for converter layers.
|
||||
- Add redactions app.
|
||||
- Unify all line endings to be Linux style.
|
||||
- Add support for changing the system messages position.
|
||||
GitLab issue #640. Thanks to Matthias Urhahn (@d4rken).
|
||||
- Update Docker deploy script. Use alpine postgres version.
|
||||
Support Docker networks and make it the default.
|
||||
Delete the containers to allow the script to be idempotent.
|
||||
Deploy a Redis container.
|
||||
- Improve document version upload form.
|
||||
- Use dropzone for document version upload form.
|
||||
- Allow the "Execute document tools" permission to be
|
||||
granted via ACL.
|
||||
|
||||
|
||||
3.2.8 (2019-10-01)
|
||||
==================
|
||||
- Fix error when accessing some API entry points without
|
||||
being authenticated.
|
||||
@@ -17,6 +103,7 @@
|
||||
of selection of documents.
|
||||
- Add parsed content deleted event.
|
||||
- Allow scaling of UI on mobile devices.
|
||||
- Add Chinese fonts to the Docker image
|
||||
|
||||
3.2.7 (2019-08-28)
|
||||
==================
|
||||
|
||||
41
Makefile
41
Makefile
@@ -1,5 +1,15 @@
|
||||
.PHONY: clean-pyc clean-build
|
||||
|
||||
DOCKER_MYSQL_IMAGE = mysql:8.0
|
||||
DOCKER_ORACLE_IMAGE = wnameless/oracle-xe-11g
|
||||
DOCKER_POSTGRES_IMAGE = postgres:9.6-alpine
|
||||
DOCKER_REDIS_IMAGE = redis:5.0-alpine
|
||||
|
||||
PYTHON_MYSQL_VERSION = 1.4.4
|
||||
PYTHON_PSYCOPG2_VERSION = 2.8.3
|
||||
PYTHON_RABBITMQ_VERSION = 2.0.0
|
||||
PYTHON_REDIS_VERSION = 3.2.1
|
||||
|
||||
help:
|
||||
@echo "Usage: make <target>\n"
|
||||
@awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z_-]+:.*?## / { printf " * %-40s -%s\n", $$1, $$2 }' $(MAKEFILE_LIST)|sort
|
||||
@@ -18,7 +28,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
|
||||
|
||||
@@ -33,10 +43,10 @@ test-all: clean-pyc
|
||||
test-launch-postgres:
|
||||
@docker rm -f test-postgres || true
|
||||
@docker volume rm test-postgres || true
|
||||
docker run -d --name test-postgres -p 5432:5432 -v test-postgres:/var/lib/postgresql/data healthcheck/postgres
|
||||
docker run -d --name test-postgres -p 5432:5432 -v test-postgres:/var/lib/postgresql/data $(DOCKER_POSTGRES_IMAGE)
|
||||
sudo apt-get install -q libpq-dev
|
||||
pip install psycopg2
|
||||
while ! docker inspect --format='{{json .State.Health}}' test-postgres|grep 'Status":"healthy"'; do sleep 1; done
|
||||
pip install psycopg2==$(PYTHON_PSYCOPG2_VERSION)
|
||||
while ! nc -z 127.0.0.1 5432; do sleep 1; done
|
||||
|
||||
test-with-postgres: ## MODULE=<python module name> - Run tests for a single app, module or test class against a Postgres database container.
|
||||
test-with-postgres: test-launch-postgres
|
||||
@@ -53,10 +63,10 @@ test-with-postgres-all: test-launch-postgres
|
||||
test-launch-mysql:
|
||||
@docker rm -f test-mysql || true
|
||||
@docker volume rm test-mysql || true
|
||||
docker run -d --name test-mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=True -e MYSQL_DATABASE=mayan -v test-mysql:/var/lib/mysql healthcheck/mysql
|
||||
docker run -d --name test-mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=True -e MYSQL_DATABASE=mayan -v test-mysql:/var/lib/mysql $(DOCKER_MYSQL_IMAGE)
|
||||
sudo apt-get install -q libmysqlclient-dev mysql-client
|
||||
pip install mysqlclient
|
||||
while ! docker inspect --format='{{json .State.Health}}' test-mysql|grep 'Status":"healthy"'; do sleep 1; done
|
||||
pip install mysqlclient==$(PYTHON_MYSQL_VERSION)
|
||||
while ! nc -z 127.0.0.1 3306; do sleep 1; done
|
||||
mysql -h 127.0.0.1 -P 3306 -uroot -e "set global character_set_server=utf8mb4;"
|
||||
|
||||
test-with-mysql: ## MODULE=<python module name> - Run tests for a single app, module or test class against a MySQL database container.
|
||||
@@ -75,7 +85,7 @@ test-with-mysql-all: test-launch-mysql
|
||||
test-launch-oracle:
|
||||
@docker rm -f test-oracle || true
|
||||
@docker volume rm test-oracle || true
|
||||
docker run -d --name test-oracle -p 49160:22 -p 49161:1521 -e ORACLE_ALLOW_REMOTE=true -v test-oracle:/u01/app/oracle wnameless/oracle-xe-11g
|
||||
docker run -d --name test-oracle -p 49160:22 -p 49161:1521 -e ORACLE_ALLOW_REMOTE=true -v test-oracle:/u01/app/oracle $(DOCKER_ORACLE_IMAGE)
|
||||
# https://gist.github.com/kimus/10012910
|
||||
pip install cx_Oracle
|
||||
while ! nc -z 127.0.0.1 49161; do sleep 1; done
|
||||
@@ -234,20 +244,21 @@ generate-requirements: ## Generate all requirements files from the project deped
|
||||
# Dev server
|
||||
|
||||
runserver: ## Run the development server.
|
||||
./manage.py runserver --settings=mayan.settings.development $(ADDRPORT)
|
||||
./manage.py runserver --nothreading --settings=mayan.settings.development $(ADDRPORT)
|
||||
|
||||
runserver_plus: ## Run the Django extension's development server.
|
||||
./manage.py runserver_plus --settings=mayan.settings.development $(ADDRPORT)
|
||||
./manage.py runserver_plus --nothreading --settings=mayan.settings.development $(ADDRPORT)
|
||||
|
||||
shell_plus: ## Run the shell_plus command.
|
||||
./manage.py shell_plus --settings=mayan.settings.development
|
||||
|
||||
test-with-docker-services-on: ## Launch and initialize production-like services using Docker (Postgres and Redis).
|
||||
docker run -d --name redis -p 6379:6379 redis
|
||||
docker run -d --name postgres -p 5432:5432 postgres
|
||||
docker run -d --name redis -p 6379:6379 $(DOCKER_REDIS_IMAGE)
|
||||
docker run -d --name postgres -p 5432:5432 $(DOCKER_POSTGRES_IMAGE)
|
||||
while ! nc -z 127.0.0.1 6379; do sleep 1; done
|
||||
while ! nc -z 127.0.0.1 5432; do sleep 1; done
|
||||
sleep 4
|
||||
pip install psycopg2==$(PYTHON_PSYCOPG2_VERSION) redis==$(PYTHON_REDIS_VERSION)
|
||||
./manage.py initialsetup --settings=mayan.settings.staging.docker
|
||||
|
||||
test-with-docker-services-off: ## Stop and delete the Docker production-like services.
|
||||
@@ -258,10 +269,10 @@ 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.
|
||||
./manage.py celery worker --settings=mayan.settings.staging.docker -B -l INFO -O fair
|
||||
DJANGO_SETTINGS_MODULE=mayan.settings.staging.docker ./manage.py celery worker -A mayan -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
|
||||
docker run -d --name mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=True -e MYSQL_DATABASE=mayan_edms $(DOCKER_MYSQL_IMAGE)
|
||||
while ! nc -z 127.0.0.1 3306; do sleep 1; done
|
||||
|
||||
docker-mysql-off: ## Stop and delete the MySQL Docker container.
|
||||
@@ -269,7 +280,7 @@ docker-mysql-off: ## Stop and delete the MySQL Docker container.
|
||||
docker rm mysql
|
||||
|
||||
docker-postgres-on: ## Launch and initialize a PostgreSQL Docker container.
|
||||
docker run -d --name postgres -p 5432:5432 postgres
|
||||
docker run -d --name postgres -p 5432:5432 $(DOCKER_POSTGRES_IMAGE)
|
||||
while ! nc -z 127.0.0.1 5432; do sleep 1; done
|
||||
|
||||
docker-postgres-off: ## Stop and delete the PostgreSQL Docker container.
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
INSTALLATION_DIRECTORY=/home/vagrant/mayan-edms/
|
||||
DB_NAME=mayan_edms
|
||||
DB_PASSWORD=test123
|
||||
|
||||
cat << EOF | sudo tee -a /etc/motd.tail
|
||||
**********************************sudo apt
|
||||
|
||||
Mayan EDMS Vagrant Development Box
|
||||
|
||||
**********************************
|
||||
EOF
|
||||
|
||||
# Update sources
|
||||
echo -e "\n -> Running apt-get update & upgrade \n"
|
||||
sudo apt-get -qq update
|
||||
sudo apt-get -y upgrade
|
||||
|
||||
echo -e "\n -> Installing core binaries \n"
|
||||
sudo apt-get -y install git-core python-virtualenv gcc python-dev libjpeg-dev libpng-dev libtiff-dev tesseract-ocr poppler-utils libreoffice
|
||||
|
||||
echo -e "\n -> Cloning development branch of repository \n"
|
||||
git clone /mayan-edms-repository/ $INSTALLATION_DIRECTORY
|
||||
cd $INSTALLATION_DIRECTORY
|
||||
git checkout development
|
||||
git reset HEAD --hard
|
||||
|
||||
echo -e "\n -> Setting up virtual env \n"
|
||||
virtualenv venv
|
||||
source venv/bin/activate
|
||||
|
||||
echo -e "\n -> Installing python dependencies \n"
|
||||
pip install -r requirements.txt
|
||||
|
||||
echo -e "\n -> Running Mayan EDMS initial setup \n"
|
||||
./manage.py initialsetup
|
||||
|
||||
echo -e "\n -> Installing Redis server \n"
|
||||
sudo apt-get install -y redis-server
|
||||
pip install redis
|
||||
|
||||
echo -e "\n -> Installing testing software \n"
|
||||
pip install coverage
|
||||
|
||||
echo -e "\n -> Installing MySQL \n"
|
||||
sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password password '$DB_PASSWORD
|
||||
sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password '$DB_PASSWORD
|
||||
sudo apt-get install -y mysql-server libmysqlclient-dev
|
||||
# Create a passwordless root and travis users
|
||||
mysql -u root -p$DB_PASSWORD -e "SET PASSWORD = PASSWORD('');"
|
||||
mysql -u root -e "CREATE USER 'travis'@'localhost' IDENTIFIED BY '';GRANT ALL PRIVILEGES ON * . * TO 'travis'@'localhost';FLUSH PRIVILEGES;"
|
||||
mysql -u travis -e "CREATE DATABASE $DB_NAME;"
|
||||
pip install mysql-python
|
||||
|
||||
echo -e "\n -> Installing PostgreSQL \n"
|
||||
sudo apt-get install -y postgresql postgresql-server-dev-all
|
||||
sudo -u postgres psql -c 'create database mayan_edms;' -U postgres
|
||||
sudo cat > /etc/postgresql/9.3/main/pg_hba.conf << EOF
|
||||
local all postgres trust
|
||||
|
||||
# TYPE DATABASE USER ADDRESS METHOD
|
||||
|
||||
# "local" is for Unix domain socket connections only
|
||||
local all all peer
|
||||
# IPv4 local connections:
|
||||
host all all 127.0.0.1/32 md5
|
||||
# IPv6 local connections:
|
||||
host all all ::1/128 md5
|
||||
EOF
|
||||
|
||||
pip install -q psycopg2
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,21 +5,25 @@ set -e
|
||||
# $ curl -fsSL get.mayan-edms.com -o get-mayan-edms.sh
|
||||
# $ sh get-mayan-edms.sh
|
||||
#
|
||||
# NOTE: Make sure to verify the contents of the script
|
||||
# NOTE: Before executing, make sure to verify the contents of the script
|
||||
# you downloaded matches the contents of docker.sh
|
||||
# located at https://gitlab.com/mayan-edms/mayan-edms/blob/master/contrib/scripts/install/docker.sh
|
||||
# before executing.
|
||||
|
||||
: ${VERBOSE:=true}
|
||||
: ${INSTALL_DOCKER:=false}
|
||||
: ${DELETE_VOLUMES:=false}
|
||||
: ${USE_DOCKER_NETWORK:=true}
|
||||
: ${DOCKER_NETWORK_NAME:=mayan}
|
||||
: ${DATABASE_USER:=mayan}
|
||||
: ${DATABASE_NAME:=mayan}
|
||||
: ${DATABASE_PASSWORD:=mayanuserpass}
|
||||
: ${DOCKER_POSTGRES_IMAGE:=postgres:9.6}
|
||||
: ${DOCKER_POSTGRES_IMAGE:=postgres:9.6-alpine}
|
||||
: ${DOCKER_POSTGRES_CONTAINER:=mayan-edms-postgres}
|
||||
: ${DOCKER_POSTGRES_VOLUME:=/docker-volumes/mayan-edms/postgres}
|
||||
: ${DOCKER_POSTGRES_PORT:=5432}
|
||||
: ${DOCKER_REDIS_IMAGE:=redis:5.0-alpine}
|
||||
: ${DOCKER_REDIS_CONTAINER:=mayan-edms-redis}
|
||||
: ${DOCKER_REDIS_PORT:=6379}
|
||||
: ${DOCKER_MAYAN_IMAGE:=mayanedms/mayanedms:latest}
|
||||
: ${DOCKER_MAYAN_CONTAINER:=mayan-edms}
|
||||
: ${DOCKER_MAYAN_VOLUME:=/docker-volumes/mayan-edms/media}
|
||||
@@ -44,6 +48,8 @@ echo "Variable values to be used:"
|
||||
echo "---------------------------"
|
||||
echo "INSTALL_DOCKER: $INSTALL_DOCKER"
|
||||
echo "DELETE_VOLUMES: $DELETE_VOLUMES"
|
||||
echo "USE_DOCKER_NETWORK: $USE_DOCKER_NETWORK"
|
||||
echo "DOCKER_NETWORK_NAME: $DOCKER_NETWORK_NAME"
|
||||
echo "DATABASE_USER: $DATABASE_USER"
|
||||
echo "DATABASE_NAME: $DATABASE_NAME"
|
||||
echo "DATABASE_PASSWORD: $DATABASE_PASSWORD"
|
||||
@@ -51,10 +57,17 @@ echo "DOCKER_POSTGRES_IMAGE: $DOCKER_POSTGRES_IMAGE"
|
||||
echo "DOCKER_POSTGRES_CONTAINER: $DOCKER_POSTGRES_CONTAINER"
|
||||
echo "DOCKER_POSTGRES_VOLUME: $DOCKER_POSTGRES_VOLUME"
|
||||
echo "DOCKER_POSTGRES_PORT: $DOCKER_POSTGRES_PORT"
|
||||
echo "DOCKER_REDIS_IMAGE: $DOCKER_REDIS_IMAGE"
|
||||
echo "DOCKER_REDIS_CONTAINER: $DOCKER_REDIS_CONTAINER"
|
||||
echo "DOCKER_REDIS_PORT: $DOCKER_REDIS_PORT"
|
||||
echo "DOCKER_MAYAN_IMAGE: $DOCKER_MAYAN_IMAGE"
|
||||
echo "DOCKER_MAYAN_CONTAINER: $DOCKER_MAYAN_CONTAINER"
|
||||
echo "DOCKER_MAYAN_VOLUME: $DOCKER_MAYAN_VOLUME"
|
||||
echo "\nStarting in 10 seconds."
|
||||
echo
|
||||
echo "Override any of them by setting them before the script. "
|
||||
echo "Example: INSTALL_DOCKER=true sh get-mayan-edms.sh"
|
||||
|
||||
echo "\nStarting in 10 seconds. Press CTRL+C to cancel."
|
||||
sleep 10
|
||||
fi
|
||||
|
||||
@@ -72,33 +85,62 @@ if [ -z `which docker` ]; then
|
||||
fi
|
||||
|
||||
echo -n "* Removing existing Mayan EDMS and PostgreSQL containers (no data will be lost)..."
|
||||
true || docker stop $DOCKER_MAYAN_CONTAINER >/dev/null 2>&1
|
||||
true || docker rm $DOCKER_MAYAN_CONTAINER >/dev/null 2>&1
|
||||
true || docker stop $DOCKER_POSTGRES_CONTAINER >/dev/null 2>&1
|
||||
true || docker rm $DOCKER_POSTGRES_CONTAINER >/dev/null 2>&1
|
||||
docker rm -f $DOCKER_REDIS_CONTAINER >/dev/null 2>&1 || true
|
||||
docker rm -f $DOCKER_POSTGRES_CONTAINER >/dev/null 2>&1 || true
|
||||
docker rm -f $DOCKER_MAYAN_CONTAINER >/dev/null 2>&1 || true
|
||||
echo "Done"
|
||||
|
||||
if [ "$DELETE_VOLUMES" = true ]; then
|
||||
echo -n "* Deleting Docker volumes in 5 seconds (warning: this delete all document data)..."
|
||||
echo -n "* Deleting Docker volumes in 5 seconds (warning: this will delete all document data). Press CTRL+C to cancel..."
|
||||
sleep 5
|
||||
true || rm DOCKER_MAYAN_VOLUME -Rf
|
||||
true || rm DOCKER_POSTGRES_VOLUME -Rf
|
||||
rm DOCKER_MAYAN_VOLUME -Rf || true
|
||||
rm DOCKER_POSTGRES_VOLUME -Rf || true
|
||||
echo "Done"
|
||||
fi
|
||||
|
||||
echo -n "* Pulling (downloading) the Mayan EDMS Docker image..."
|
||||
docker pull $DOCKER_MAYAN_IMAGE >/dev/null
|
||||
echo -n "* Pulling (downloading) the Redis Docker image..."
|
||||
docker pull $DOCKER_REDIS_IMAGE > /dev/null
|
||||
echo "Done"
|
||||
|
||||
echo -n "* Pulling (downloading) the PostgreSQL Docker image..."
|
||||
docker pull $DOCKER_POSTGRES_IMAGE > /dev/null
|
||||
echo "Done"
|
||||
|
||||
echo -n "* Pulling (downloading) the Mayan EDMS Docker image..."
|
||||
docker pull $DOCKER_MAYAN_IMAGE >/dev/null
|
||||
echo "Done"
|
||||
|
||||
if [ "$USE_DOCKER_NETWORK" = true ]; then
|
||||
echo -n "* Creating Docker network..."
|
||||
docker network create $DOCKER_NETWORK_NAME 2> /dev/null || true
|
||||
# Ignore error if the network already exists
|
||||
echo "Done"
|
||||
fi
|
||||
|
||||
if [ "$USE_DOCKER_NETWORK" = true ]; then
|
||||
NETWORK_ARGUMENT="--network=$DOCKER_NETWORK_NAME"
|
||||
POSTGRES_PORT_ARGUMENT=""
|
||||
REDIS_PORT_ARGUMENT=""
|
||||
MAYAN_DATABASE_PORT_ARGUMENT=""
|
||||
MAYAN_DATABASE_HOST_ARGUMENT="-e MAYAN_DATABASE_HOST=$DOCKER_POSTGRES_CONTAINER"
|
||||
MAYAN_BROKER_URL_ARGUMENT="-e MAYAN_BROKER_URL=redis://$DOCKER_REDIS_CONTAINER:6379/0"
|
||||
MAYAN_CELERY_RESULT_BACKEND_ARGUMENT="-e MAYAN_CELERY_RESULT_BACKEND=redis://$DOCKER_REDIS_CONTAINER:6379/1"
|
||||
else
|
||||
NETWORK_ARGUMENT=""
|
||||
POSTGRES_PORT_ARGUMENT="-e $DOCKER_POSTGRES_PORT:5432"
|
||||
REDIS_PORT_ARGUMENT="-e $DOCKER_REDIS_PORT:6379"
|
||||
MAYAN_DATABASE_PORT_ARGUMENT="-e MAYAN_DATABASE_PORT=$DOCKER_POSTGRES_PORT"
|
||||
MAYAN_DATABASE_HOST_ARGUMENT="-e MAYAN_DATABASE_HOST=172.17.0.1"
|
||||
MAYAN_BROKER_URL_ARGUMENT="-e MAYAN_BROKER_URL=redis://172.17.0.1:6379/0"
|
||||
MAYAN_CELERY_RESULT_BACKEND_ARGUMENT="-e MAYAN_CELERY_RESULT_BACKEND=redis://172.17.0.1:6379/1"
|
||||
fi
|
||||
|
||||
echo -n "* Deploying the PostgreSQL container..."
|
||||
docker run -d \
|
||||
--name $DOCKER_POSTGRES_CONTAINER \
|
||||
$NETWORK_ARGUMENT \
|
||||
--restart=always \
|
||||
-p $DOCKER_POSTGRES_PORT:5432 \
|
||||
$POSTGRES_PORT_ARGUMENT \
|
||||
-e POSTGRES_USER=$DATABASE_USER \
|
||||
-e POSTGRES_DB=$DATABASE_NAME \
|
||||
-e POSTGRES_PASSWORD=$DATABASE_PASSWORD \
|
||||
@@ -106,6 +148,24 @@ docker run -d \
|
||||
$DOCKER_POSTGRES_IMAGE >/dev/null
|
||||
echo "Done"
|
||||
|
||||
echo -n "* Deploying the Redis container..."
|
||||
docker run -d \
|
||||
--name $DOCKER_REDIS_CONTAINER \
|
||||
$NETWORK_ARGUMENT \
|
||||
--restart=always \
|
||||
$REDIS_PORT_ARGUMENT \
|
||||
$DOCKER_REDIS_IMAGE \
|
||||
redis-server \
|
||||
--appendonly no \
|
||||
--databases 2 \
|
||||
--maxmemory 100mb \
|
||||
--maxmemory-policy allkeys-lru \
|
||||
--maxclients 500 \
|
||||
--save "" \
|
||||
--tcp-backlog 256 \
|
||||
>/dev/null
|
||||
echo "Done"
|
||||
|
||||
echo -n "* Waiting for the PostgreSQL container to be ready (10 seconds)..."
|
||||
sleep 10
|
||||
echo "Done"
|
||||
@@ -113,15 +173,18 @@ echo "Done"
|
||||
echo -n "* Deploying Mayan EDMS container..."
|
||||
docker run -d \
|
||||
--name $DOCKER_MAYAN_CONTAINER \
|
||||
$NETWORK_ARGUMENT \
|
||||
--restart=always \
|
||||
-p 80:8000 \
|
||||
-e MAYAN_DATABASE_ENGINE=django.db.backends.postgresql \
|
||||
-e MAYAN_DATABASE_HOST=172.17.0.1 \
|
||||
$MAYAN_DATABASE_HOST_ARGUMENT \
|
||||
$MAYAN_DATABASE_PORT_ARGUMENT \
|
||||
-e MAYAN_DATABASE_NAME=$DATABASE_NAME \
|
||||
-e MAYAN_DATABASE_PASSWORD=$DATABASE_PASSWORD \
|
||||
-e MAYAN_DATABASE_USER=$DATABASE_USER \
|
||||
-e MAYAN_DATABASE_PORT=$DOCKER_POSTGRES_PORT \
|
||||
-e MAYAN_DATABASE_CONN_MAX_AGE=0 \
|
||||
$MAYAN_BROKER_URL_ARGUMENT \
|
||||
$MAYAN_CELERY_RESULT_BACKEND_ARGUMENT \
|
||||
-v $DOCKER_MAYAN_VOLUME:/var/lib/mayan \
|
||||
$DOCKER_MAYAN_IMAGE >/dev/null
|
||||
echo "Done"
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# ====== CONFIG ======
|
||||
INSTALLATION_DIRECTORY=/usr/share/mayan-edms/
|
||||
DB_NAME=mayan_edms
|
||||
DB_USERNAME=mayan
|
||||
DB_PASSWORD=test123
|
||||
# ==== END CONFIG ====
|
||||
|
||||
cat << EOF | tee -a /etc/motd.tail
|
||||
**********************************
|
||||
|
||||
Mayan EDMS Vagrant Production Box
|
||||
|
||||
**********************************
|
||||
EOF
|
||||
|
||||
echo -e "\n -> Running apt-get update & upgrade \n"
|
||||
apt-get -qq update
|
||||
apt-get -y upgrade
|
||||
|
||||
echo -e "\n -> Installing core binaries \n"
|
||||
apt-get install nginx supervisor redis-server postgresql libpq-dev libjpeg-dev libmagic1 libpng-dev libreoffice libtiff-dev gcc ghostscript gpgv python-dev python-virtualenv tesseract-ocr poppler-utils -y
|
||||
|
||||
echo -e "\n -> Setting up virtualenv \n"
|
||||
rm -f ${INSTALLATION_DIRECTORY}
|
||||
virtualenv ${INSTALLATION_DIRECTORY}
|
||||
source ${INSTALLATION_DIRECTORY}bin/activate
|
||||
|
||||
echo -e "\n -> Installing Mayan EDMS from PyPI \n"
|
||||
pip install mayan-edms
|
||||
|
||||
echo -e "\n -> Installing Python client for PostgreSQL, Redis, and uWSGI \n"
|
||||
pip install psycopg2 redis uwsgi
|
||||
|
||||
echo -e "\n -> Creating the database for the installation \n"
|
||||
echo "CREATE USER mayan WITH PASSWORD '$DB_PASSWORD';" | sudo -u postgres psql
|
||||
sudo -u postgres createdb -O $DB_USERNAME $DB_NAME
|
||||
|
||||
echo -e "\n -> Creating the directories for the logs \n"
|
||||
mkdir /var/log/mayan
|
||||
|
||||
echo -e "\n -> Making a convenience symlink \n"
|
||||
cd ${INSTALLATION_DIRECTORY}
|
||||
ln -s lib/python2.7/site-packages/mayan .
|
||||
|
||||
echo -e "\n -> Creating an initial settings file \n"
|
||||
mayan-edms.py createsettings
|
||||
|
||||
echo -e "\n -> Updating the mayan/settings/local.py file \n"
|
||||
cat >> mayan/settings/local.py << EOF
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||
'NAME': '$DB_NAME',
|
||||
'USER': '$DB_USERNAME',
|
||||
'PASSWORD': '$DB_PASSWORD',
|
||||
'HOST': 'localhost',
|
||||
'PORT': '5432',
|
||||
}
|
||||
}
|
||||
|
||||
BROKER_URL = 'redis://127.0.0.1:6379/0'
|
||||
CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0'
|
||||
EOF
|
||||
|
||||
echo -e "\n -> Migrating the database or initialize the project \n"
|
||||
mayan-edms.py initialsetup
|
||||
|
||||
echo -e "\n -> Disabling the default NGINX site \n"
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
echo -e "\n -> Creating a uwsgi.ini file \n"
|
||||
cat > uwsgi.ini << EOF
|
||||
[uwsgi]
|
||||
chdir = ${INSTALLATION_DIRECTORY}lib/python2.7/site-packages/mayan
|
||||
chmod-socket = 664
|
||||
chown-socket = www-data:www-data
|
||||
env = DJANGO_SETTINGS_MODULE=mayan.settings.production
|
||||
gid = www-data
|
||||
logto = /var/log/uwsgi/%n.log
|
||||
pythonpath = ${INSTALLATION_DIRECTORY}lib/python2.7/site-packages
|
||||
master = True
|
||||
max-requests = 5000
|
||||
socket = ${INSTALLATION_DIRECTORY}uwsgi.sock
|
||||
uid = www-data
|
||||
vacuum = True
|
||||
wsgi-file = ${INSTALLATION_DIRECTORY}lib/python2.7/site-packages/mayan/wsgi.py
|
||||
EOF
|
||||
|
||||
echo -e "\n -> Creating the directory for the uWSGI log files \n"
|
||||
mkdir -p /var/log/uwsgi
|
||||
|
||||
echo -e "\n -> Creating the NGINX site file for Mayan EDMS, /etc/nginx/sites-available/mayan \n"
|
||||
cat > /etc/nginx/sites-available/mayan << EOF
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:${INSTALLATION_DIRECTORY}uwsgi.sock;
|
||||
|
||||
client_max_body_size 30M; # Increse if your plan to upload bigger documents
|
||||
proxy_read_timeout 30s; # Increase if your document uploads take more than 30 seconds
|
||||
}
|
||||
|
||||
location /static {
|
||||
alias ${INSTALLATION_DIRECTORY}mayan/media/static;
|
||||
expires 1h;
|
||||
}
|
||||
|
||||
location /favicon.ico {
|
||||
alias ${INSTALLATION_DIRECTORY}mayan/media/static/appearance/images/favicon.ico;
|
||||
expires 1h;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo -e "\n -> Enabling the NGINX site for Mayan EDMS \n"
|
||||
ln -s /etc/nginx/sites-available/mayan /etc/nginx/sites-enabled/
|
||||
|
||||
echo -e "\n -> Creating the supervisor file for the uWSGI process, /etc/supervisor/conf.d/mayan-uwsgi.conf \n"
|
||||
cat > /etc/supervisor/conf.d/mayan-uwsgi.conf << EOF
|
||||
[program:mayan-uwsgi]
|
||||
command = ${INSTALLATION_DIRECTORY}bin/uwsgi --ini ${INSTALLATION_DIRECTORY}uwsgi.ini
|
||||
user = root
|
||||
autostart = true
|
||||
autorestart = true
|
||||
redirect_stderr = true
|
||||
EOF
|
||||
|
||||
echo -e "\n -> Creating the supervisor file for the Celery worker, /etc/supervisor/conf.d/mayan-celery.conf \n"
|
||||
cat > /etc/supervisor/conf.d/mayan-celery.conf << EOF
|
||||
[program:mayan-worker]
|
||||
command = ${INSTALLATION_DIRECTORY}bin/python ${INSTALLATION_DIRECTORY}bin/mayan-edms.py celery --settings=mayan.settings.production worker -Ofair -l ERROR
|
||||
directory = ${INSTALLATION_DIRECTORY}
|
||||
user = www-data
|
||||
stdout_logfile = /var/log/mayan/worker-stdout.log
|
||||
stderr_logfile = /var/log/mayan/worker-stderr.log
|
||||
autostart = true
|
||||
autorestart = true
|
||||
startsecs = 10
|
||||
stopwaitsecs = 10
|
||||
killasgroup = true
|
||||
priority = 998
|
||||
|
||||
[program:mayan-beat]
|
||||
command = ${INSTALLATION_DIRECTORY}bin/python ${INSTALLATION_DIRECTORY}bin/mayan-edms.py celery --settings=mayan.settings.production beat -l ERROR
|
||||
directory = ${INSTALLATION_DIRECTORY}
|
||||
user = www-data
|
||||
numprocs = 1
|
||||
stdout_logfile = /var/log/mayan/beat-stdout.log
|
||||
stderr_logfile = /var/log/mayan/beat-stderr.log
|
||||
autostart = true
|
||||
autorestart = true
|
||||
startsecs = 10
|
||||
stopwaitsecs = 1
|
||||
killasgroup = true
|
||||
priority = 998
|
||||
EOF
|
||||
|
||||
echo -e "\n -> Collecting the static files \n"
|
||||
mayan-edms.py preparestatic --noinput
|
||||
|
||||
echo -e "\n -> Making the installation directory readable and writable by the webserver user \n"
|
||||
chown www-data:www-data ${INSTALLATION_DIRECTORY} -R
|
||||
|
||||
echo -e "\n -> Restarting the services \n"
|
||||
/etc/init.d/nginx restart
|
||||
/etc/init.d/supervisor restart
|
||||
@@ -13,11 +13,12 @@ APP_LIST = (
|
||||
'checkouts', 'common', 'converter', 'dashboards', 'dependencies',
|
||||
'django_gpg', 'document_comments', 'document_indexing',
|
||||
'document_parsing', 'document_signatures', 'document_states',
|
||||
'documents', 'dynamic_search', 'events', 'file_metadata', 'linking',
|
||||
'lock_manager', 'mayan_statistics', 'mailer', 'metadata', 'mirroring',
|
||||
'motd', 'navigation', 'ocr', 'permissions', 'platform', 'rest_api',
|
||||
'smart_settings', 'sources', 'storage', 'tags', 'task_manager',
|
||||
'user_management'
|
||||
'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'
|
||||
)
|
||||
|
||||
LANGUAGE_LIST = (
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
NAME="mayan-edms"
|
||||
DJANGODIR=/usr/share/mayan-edms
|
||||
SOCKFILE=/var/tmp/filesystem.sock
|
||||
USER=www-data
|
||||
GROUP=www-data
|
||||
NUM_WORKERS=3
|
||||
DJANGO_SETTINGS_MODULE=mayan.settings.production
|
||||
DJANGO_WSGI_MODULE=mayan.wsgi
|
||||
TIMEOUT=600
|
||||
|
||||
echo "Starting $NAME as `whoami`"
|
||||
|
||||
# Activate the virtual environment
|
||||
cd $DJANGODIR
|
||||
source bin/activate
|
||||
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
|
||||
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
|
||||
|
||||
# Create the run directory if it doesn't exist
|
||||
RUNDIR=$(dirname $SOCKFILE)
|
||||
test -d $RUNDIR || mkdir -p $RUNDIR
|
||||
|
||||
# Start your Django Unicorn
|
||||
# Programs meant to be run under supervisor should not daemonize themselves (do not use --daemon)
|
||||
exec bin/gunicorn ${DJANGO_WSGI_MODULE}:application \
|
||||
--name $NAME \
|
||||
--workers $NUM_WORKERS \
|
||||
--user=$USER --group=$GROUP \
|
||||
--log-level=debug \
|
||||
--bind=unix:$SOCKFILE \
|
||||
--timeout=$TIMEOUT
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# BASE_IMAGE - Bare bones image with the base packages needed to run Mayan EDMS
|
||||
####
|
||||
|
||||
FROM debian:9.8-slim as BASE_IMAGE
|
||||
FROM debian:10.0-slim as BASE_IMAGE
|
||||
|
||||
LABEL maintainer="Roberto Rosario roberto.rosario@mayan-edms.com"
|
||||
|
||||
@@ -22,18 +22,21 @@ RUN set -x \
|
||||
&& DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
exiftool \
|
||||
fonts-arphic-uming \
|
||||
fonts-arphic-ukai \
|
||||
ghostscript \
|
||||
gpgv \
|
||||
gnupg1 \
|
||||
graphviz \
|
||||
libfuse2 \
|
||||
libmagic1 \
|
||||
libmariadbclient18 \
|
||||
libmariadb3 \
|
||||
libreoffice \
|
||||
libpq5 \
|
||||
poppler-utils \
|
||||
redis-server \
|
||||
python3-distutils \
|
||||
sane-utils \
|
||||
sudo \
|
||||
supervisor \
|
||||
@@ -52,22 +55,20 @@ 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 \
|
||||
# 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
|
||||
|
||||
; fi
|
||||
|
||||
####
|
||||
# BUILDER_IMAGE - This image buildS the Python package and is discarded afterwards
|
||||
# BUILDER_IMAGE - This image builds the Python package and is discarded afterwards
|
||||
# only the build artifact is carried over to the next image.
|
||||
####
|
||||
|
||||
# Reuse image
|
||||
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
|
||||
@@ -96,39 +97,40 @@ apt-get install -y --no-install-recommends \
|
||||
libssl-dev \
|
||||
g++ \
|
||||
gcc \
|
||||
python-dev \
|
||||
python-virtualenv \
|
||||
python3-dev \
|
||||
python3-venv \
|
||||
&& mkdir -p "${PROJECT_INSTALL_DIR}" \
|
||||
&& chown -R mayan:mayan "${PROJECT_INSTALL_DIR}" \
|
||||
&& chown -R mayan:mayan /src
|
||||
|
||||
USER mayan
|
||||
RUN python -m virtualenv "${PROJECT_INSTALL_DIR}" \
|
||||
RUN python3 -m venv "${PROJECT_INSTALL_DIR}" \
|
||||
&& . "${PROJECT_INSTALL_DIR}/bin/activate" \
|
||||
&& 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 \
|
||||
&& 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 \
|
||||
# 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 --no-use-pep517 \
|
||||
pip install --no-cache-dir \
|
||||
psutil==5.6.2 \
|
||||
; fi \
|
||||
# Install the Python packages needed to build Mayan EDMS
|
||||
&& pip install --no-cache-dir --no-use-pep517 -r /src/requirements/build.txt \
|
||||
&& pip install --no-cache-dir -r /src/requirements/build.txt \
|
||||
# Build Mayan EDMS
|
||||
&& python setup.py sdist \
|
||||
&& python3 setup.py sdist \
|
||||
# Install the built Mayan EDMS package
|
||||
&& pip install --no-cache-dir --no-use-pep517 dist/mayan* \
|
||||
&& pip install --no-cache-dir dist/mayan* \
|
||||
# Install the static content
|
||||
&& mayan-edms.py installjavascript \
|
||||
&& mayan-edms.py installdependencies \
|
||||
&& MAYAN_STATIC_ROOT=${PROJECT_INSTALL_DIR}/static mayan-edms.py preparestatic --link --noinput
|
||||
|
||||
COPY --chown=mayan:mayan requirements/testing-base.txt "${PROJECT_INSTALL_DIR}"
|
||||
|
||||
####
|
||||
# Final image - BASE_IMAGE + Mayan install directory from the builder image
|
||||
# Final image - BASE_IMAGE + BUILDER_IMAGE artifact (Mayan install directory)
|
||||
####
|
||||
|
||||
FROM BASE_IMAGE
|
||||
@@ -144,7 +146,7 @@ VOLUME ["/var/lib/mayan"]
|
||||
ENTRYPOINT ["entrypoint.sh"]
|
||||
|
||||
EXPOSE 8000
|
||||
CMD ["mayan"]
|
||||
CMD ["run_all"]
|
||||
|
||||
RUN ${PROJECT_INSTALL_DIR}/bin/mayan-edms.py platformtemplate supervisord_docker > /etc/supervisor/conf.d/mayan.conf \
|
||||
&& apt-get clean autoclean \
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
APT_PROXY ?= `/sbin/ip route|awk '/docker0/ { print $$9 }'`:3142
|
||||
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)
|
||||
|
||||
IMAGE_VERSION ?= `cat docker/rootfs/version`
|
||||
CONSOLE_COLUMNS ?= `echo $$(tput cols)`
|
||||
CONSOLE_LINES ?= `echo $$(tput lines)`
|
||||
@@ -7,7 +12,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) .
|
||||
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-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
|
||||
@@ -23,3 +28,13 @@ 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
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
version: '2.1'
|
||||
|
||||
volumes:
|
||||
broker:
|
||||
driver: local
|
||||
app:
|
||||
driver: local
|
||||
db:
|
||||
driver: local
|
||||
results:
|
||||
driver: local
|
||||
|
||||
services:
|
||||
broker:
|
||||
container_name: mayan-edms-broker
|
||||
image: healthcheck/rabbitmq
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: mayan
|
||||
RABBITMQ_DEFAULT_PASS: mayan
|
||||
RABBITMQ_DEFAULT_VHOST: mayan
|
||||
volumes:
|
||||
- broker:/var/lib/rabbitmq
|
||||
results:
|
||||
container_name: mayan-edms-results
|
||||
image: healthcheck/redis
|
||||
volumes:
|
||||
- results:/data
|
||||
#db:
|
||||
# container_name: mayan-edms-db
|
||||
# image: healthcheck/mysql
|
||||
# environment:
|
||||
# MYSQL_DATABASE: mayan
|
||||
# MYSQL_PASSWORD: mayan-password
|
||||
# MYSQL_ROOT_PASSWORD: root-password
|
||||
# MYSQL_USER: mayan
|
||||
# volumes:
|
||||
# - db:/var/lib/mysql
|
||||
db:
|
||||
container_name: mayan-edms-db
|
||||
image: healthcheck/postgres
|
||||
environment:
|
||||
POSTGRES_DB: mayan
|
||||
POSTGRES_PASSWORD: mayan-password
|
||||
POSTGRES_USER: mayan
|
||||
volumes:
|
||||
- db:/var/lib/postgresql/data
|
||||
mayan-edms:
|
||||
container_name: mayan-edms-app
|
||||
image: mayan-edms/next
|
||||
build:
|
||||
context: ./
|
||||
args:
|
||||
- APT_PROXY=172.18.0.1:3142
|
||||
depends_on:
|
||||
broker:
|
||||
condition: service_healthy
|
||||
db:
|
||||
condition: service_healthy
|
||||
results:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
MAYAN_BROKER_URL: amqp://mayan:mayan@broker:5672/mayan
|
||||
MAYAN_CELERY_RESULT_BACKEND: redis://results:6379/0
|
||||
MAYAN_DATABASE_ENGINE: django.db.backends.postgresql
|
||||
MAYAN_DATABASE_HOST: db
|
||||
MAYAN_DATABASE_NAME: mayan
|
||||
MAYAN_DATABASE_PASSWORD: mayan-password
|
||||
MAYAN_DATABASE_USER: mayan
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- app:/var/lib/mayan
|
||||
@@ -1,58 +1,130 @@
|
||||
version: '2.1'
|
||||
version: '3.7'
|
||||
|
||||
volumes:
|
||||
broker:
|
||||
driver: local
|
||||
app:
|
||||
driver: local
|
||||
db:
|
||||
driver: local
|
||||
results:
|
||||
driver: local
|
||||
networks:
|
||||
mayan-bridge:
|
||||
driver: bridge
|
||||
|
||||
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/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: mayanedms/mayanedms:latest
|
||||
app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: ./docker/Dockerfile
|
||||
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
|
||||
- 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
|
||||
volumes:
|
||||
- app:/var/lib/mayan
|
||||
- /docker-volumes/mayan-edms/media:/var/lib/mayan
|
||||
|
||||
postgresql:
|
||||
environment:
|
||||
POSTGRES_DB: mayan
|
||||
POSTGRES_PASSWORD: mayandbpass
|
||||
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
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
# Use bash and not sh to support argument slicing "${@:2}"
|
||||
# sh defaults to dash instead of bash.
|
||||
|
||||
set -e
|
||||
echo "mayan: starting entrypoint.sh"
|
||||
@@ -11,17 +14,13 @@ DEFAULT_USER_GID=1000
|
||||
MAYAN_USER_UID=${MAYAN_USER_UID:-${DEFAULT_USER_UID}}
|
||||
MAYAN_USER_GID=${MAYAN_USER_GID:-${DEFAULT_USER_GID}}
|
||||
|
||||
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}
|
||||
@@ -29,8 +28,8 @@ 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:-1}
|
||||
MAYAN_WORKER_MEDIUM_CONCURRENCY=${MAYAN_WORKER_MEDIUM_CONCURRENCY:-1}
|
||||
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:-1}
|
||||
|
||||
update_uid_gid() {
|
||||
@@ -67,11 +66,9 @@ else
|
||||
fi
|
||||
export MAYAN_WORKER_SLOW_CONCURRENCY
|
||||
|
||||
export CELERY_ALWAYS_EAGER=False
|
||||
# Allow importing of user setting modules
|
||||
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 "$@"
|
||||
@@ -79,9 +76,9 @@ apt_get_install() {
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
}
|
||||
|
||||
initialize() {
|
||||
echo "mayan: initialize()"
|
||||
su mayan -c "${MAYAN_BIN} initialsetup --force --no-javascript"
|
||||
initialsetup() {
|
||||
echo "mayan: initialsetup()"
|
||||
su mayan -c "${MAYAN_BIN} initialsetup --force --no-dependencies"
|
||||
}
|
||||
|
||||
os_package_installs() {
|
||||
@@ -98,43 +95,71 @@ pip_installs() {
|
||||
fi
|
||||
}
|
||||
|
||||
start() {
|
||||
run_all() {
|
||||
echo "mayan: start()"
|
||||
rm -rf /var/run/supervisor.sock
|
||||
exec /usr/bin/supervisord -nc /etc/supervisor/supervisord.conf
|
||||
}
|
||||
|
||||
upgrade() {
|
||||
echo "mayan: upgrade()"
|
||||
su mayan -c "${MAYAN_BIN} performupgrade --no-javascript"
|
||||
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}}
|
||||
}
|
||||
|
||||
os_package_installs || true
|
||||
pip_installs || true
|
||||
chown mayan:mayan /var/lib/mayan -R
|
||||
|
||||
case "$1" in
|
||||
|
||||
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_initialsetup)
|
||||
initialsetup
|
||||
;;
|
||||
|
||||
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_performupgrade)
|
||||
performupgrade
|
||||
;;
|
||||
|
||||
*) su mayan -c "$@";
|
||||
;;
|
||||
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 "$@"
|
||||
;;
|
||||
|
||||
esac
|
||||
|
||||
5
docker/rootfs/usr/local/bin/run_celery.sh
Executable file
5
docker/rootfs/usr/local/bin/run_celery.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/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 $@"
|
||||
7
docker/rootfs/usr/local/bin/run_frontend.sh
Executable file
7
docker/rootfs/usr/local/bin/run_frontend.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/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}"
|
||||
8
docker/rootfs/usr/local/bin/run_worker.sh
Executable file
8
docker/rootfs/usr/local/bin/run_worker.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
QUEUE_LIST=`MAYAN_WORKER_NAME=$1 su mayan -c "${MAYAN_PYTHON_BIN_DIR}mayan-edms.py platformtemplate worker_queues"`
|
||||
|
||||
# Use -A and not --app. Both are the same but behave differently
|
||||
# -A can be located before the command while --app cannot.
|
||||
# Pass ${@:2} to allow overriding the defaults arguments
|
||||
su mayan -c "${MAYAN_PYTHON_BIN_DIR}celery -A mayan worker -Ofair -l ERROR -Q $QUEUE_LIST ${@:2}"
|
||||
@@ -1 +1 @@
|
||||
3.2.7
|
||||
3.3beta1
|
||||
|
||||
@@ -9,24 +9,32 @@ volumes:
|
||||
services:
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
environment:
|
||||
POSTGRES_DB: mayan
|
||||
POSTGRES_PASSWORD: mayan-password
|
||||
POSTGRES_PASSWORD: mayandbpass
|
||||
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
|
||||
|
||||
@@ -127,9 +127,8 @@ For another setup that offers more performance and scalability refer to the
|
||||
|
||||
::
|
||||
|
||||
sudo -u mayan MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
|
||||
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
|
||||
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
||||
sudo -u mayan MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \
|
||||
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
||||
/opt/mayan-edms/bin/mayan-edms.py initialsetup
|
||||
|
||||
|
||||
@@ -148,9 +147,8 @@ For another setup that offers more performance and scalability refer to the
|
||||
------------------------------------------------------------------------
|
||||
::
|
||||
|
||||
sudo MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
|
||||
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
|
||||
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
||||
sudo mayan MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \
|
||||
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
||||
/opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf
|
||||
|
||||
|
||||
@@ -222,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_BROKER_URL="redis://127.0.0.1:6379/0",
|
||||
MAYAN_CELERY_BROKER_URL="redis://127.0.0.1:6379/0",
|
||||
|
||||
with::
|
||||
|
||||
MAYAN_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
|
||||
MAYAN_CELERY_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
|
||||
|
||||
increase the number of Gunicorn workers to 3 in the line (``-w 2`` section)::
|
||||
|
||||
|
||||
@@ -533,7 +533,7 @@ Release using GitLab CI
|
||||
::
|
||||
|
||||
git checkout releases/all
|
||||
git merge versions/next
|
||||
git merge <corresponding branch>
|
||||
|
||||
#. Push code to trigger builds:
|
||||
::
|
||||
|
||||
@@ -49,12 +49,7 @@ Finally create and run a Mayan EDMS container::
|
||||
--name mayan-edms \
|
||||
--restart=always \
|
||||
-p 80:8000 \
|
||||
-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 \
|
||||
-e MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'172.17.0.1'}}" \
|
||||
-v /docker-volumes/mayan-edms/media:/var/lib/mayan \
|
||||
mayanedms/mayanedms:<version>
|
||||
|
||||
@@ -108,12 +103,7 @@ instead of the IP address of the Docker host (``172.17.0.1``)::
|
||||
--network=mayan \
|
||||
--restart=always \
|
||||
-p 80:8000 \
|
||||
-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 \
|
||||
-e MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'mayan-edms-postgres'}}" \
|
||||
-v /docker-volumes/mayan-edms/media:/var/lib/mayan \
|
||||
mayanedms/mayanedms:<version>
|
||||
|
||||
@@ -137,101 +127,20 @@ To start the container again::
|
||||
Environment Variables
|
||||
---------------------
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
``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_TIMEOUT``
|
||||
|
||||
Optional. Changes the amount of time the frontend worker will wait for a
|
||||
request to finish before raising a timeout error. The default is 120
|
||||
seconds.
|
||||
|
||||
``MAYAN_GUNICORN_WORKERS``
|
||||
|
||||
@@ -275,6 +184,15 @@ Optional. Changes the GID 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
|
||||
@@ -442,6 +360,7 @@ 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.
|
||||
|
||||
@@ -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_BROKER_URL environment variable (https://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls)
|
||||
Pass the MAYAN_CELERY_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_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
|
||||
-e MAYAN_CELERY_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
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
Version 3.2.8
|
||||
=============
|
||||
|
||||
Released: XX, 2019
|
||||
Released: October 1, 2019
|
||||
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
|
||||
API
|
||||
^^^
|
||||
|
||||
Fix an error when accessing some API entry points without
|
||||
being authenticated. Accessing API endpoints without being authenticated
|
||||
will now always return empty results.
|
||||
|
||||
|
||||
Cabinets
|
||||
^^^^^^^^
|
||||
|
||||
@@ -14,25 +23,43 @@ Tweaked the jstree component's appearance to cope with long labels.
|
||||
Added a scrollbar, reduced the font size, switched to a sans serif font,
|
||||
and reduced padding. Thanks for forum user @briboe for the report.
|
||||
|
||||
Workflow actions to add and remove documents from cabinets was added.
|
||||
|
||||
Other changes
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
- Fix error when accessing some API entry points without
|
||||
being authenticated.
|
||||
- Add cabinet add and remove workflow actions.
|
||||
- Update Django to version 1.11.24.
|
||||
- Update jQuery to version 3.4.1
|
||||
- Add support for deleting the OCR content of a document
|
||||
or selection of documents.
|
||||
- Add OCR content deleted event.
|
||||
- Add missing recursive option to Docker entrypoint
|
||||
chown. GitLab issue #668. Thanks to John Wice (@brilthor)
|
||||
for the report.
|
||||
- Add support for deleting the parsed content of a document
|
||||
of selection of documents.
|
||||
- Add parsed content deleted event.
|
||||
- Allow scaling of UI on mobile devices.
|
||||
Dependencies
|
||||
^^^^^^^^^^^^
|
||||
|
||||
The Django version used was updated to version 1.11.24. The jQuery version
|
||||
used was updated to version 3.4.1. Both as fully backwards compatible with
|
||||
their previous versions.
|
||||
|
||||
|
||||
OCR
|
||||
^^^
|
||||
|
||||
Support was added to delete the content of document's OCR or parsed content.
|
||||
Events for both situations was added allowing content deletion to be used
|
||||
as workflow transition triggers.
|
||||
|
||||
|
||||
Docker
|
||||
^^^^^^
|
||||
|
||||
A missing recursive option was added to the Docker entrypoint
|
||||
command "chown" to change the ownership of files when specifying a custom
|
||||
UID or GID. Closes GitLab issue #668. Thanks to John Wice (@brilthor)
|
||||
for the report.
|
||||
|
||||
Two fonts were added to the Docker image to support rendering Chinese office
|
||||
documents. Closes GitLab issue #666. Thanks to javawcy (@javawcy) and forum
|
||||
user @leoliu for the report and help closing this issue.
|
||||
|
||||
|
||||
Usability
|
||||
^^^^^^^^^
|
||||
|
||||
Descriptions for screenreaders was added via image alt tag. The user interface
|
||||
will also now allow scaling.
|
||||
|
||||
|
||||
Removals
|
||||
@@ -126,7 +153,9 @@ Backward incompatible changes
|
||||
Bugs fixed or issues closed
|
||||
---------------------------
|
||||
|
||||
- :gitlab-issue:`666` Chinese document such as .doc can't display well.
|
||||
- :gitlab-issue:`668` Permission denied errors with custom uid persist (650 needs re-open)
|
||||
- :forum-topic:`1120` Cabinet Presentation
|
||||
- :forum-topic:`2202` Cannot display Chinese character and cannot identify Excel files
|
||||
|
||||
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/
|
||||
|
||||
213
docs/releases/3.3.rst
Normal file
213
docs/releases/3.3.rst
Normal file
@@ -0,0 +1,213 @@
|
||||
Version 3.3
|
||||
===========
|
||||
|
||||
Released: XX XX, 2019
|
||||
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- Add support for icon shadows.
|
||||
- Add icons and no-result template to the object error log view and
|
||||
links.
|
||||
- Use Select2 widget for the document type selection form.
|
||||
- Backport the vertical main menu update. This update splits the previous
|
||||
main menu into a new menu in the same location as the previous one
|
||||
now called the top bar, and a new vertical main menu on the left side.
|
||||
The vertical menu remain open even when clicking on items and upon
|
||||
a browser refresh will also restore its state to match the selected
|
||||
view.
|
||||
- Backport workflow preview refactor. GitLab issue #532.
|
||||
- Add support for source column inheritance.
|
||||
- Add support for source column exclusion.
|
||||
- Backport workflow context support.
|
||||
- Backport workflow transitions field support.
|
||||
- Backport workflow email action.
|
||||
- Backport individual index rebuild support.
|
||||
- Rename the installjavascript command to installdependencies.
|
||||
- Remove database conversion command.
|
||||
- Remove support for quoted configuration entries. Support unquoted,
|
||||
nested dictionaries in the configuration. Requires manual
|
||||
update of existing config.yml files.
|
||||
- Support user specified locations for the configuration file with the
|
||||
CONFIGURATION_FILEPATH (MAYAN_CONFIGURATION_FILEPATH environment variable), and
|
||||
CONFIGURATION_LAST_GOOD_FILEPATH
|
||||
(MAYAN_CONFIGURATION_LAST_GOOD_FILEPATH environment variable) settings.
|
||||
- Move bootstrapped settings code to their own module in the smart_settings apps.
|
||||
- Remove individual database configuration options. All database configuration
|
||||
is now done using MAYAN_DATABASES to mirror Django way of doing database setup.
|
||||
- Added support for YAML encoded environment variables to the platform
|
||||
templates apps.
|
||||
- Move YAML code to its own module. Code now resides in common.serialization
|
||||
in the form of two new functions: yaml_load and yaml_dump.
|
||||
- Move Django and Celery settings. Django settings now reside in the smart
|
||||
settings app. Celery settings now reside in the task manager app.
|
||||
- Backport FakeStorageSubclass from versions/next. Placeholder class to allow
|
||||
serializing the real storage subclass to support migrations.
|
||||
Used by all configurable storages.
|
||||
- Support checking in and out multiple documents.
|
||||
- Remove encapsulate helper.
|
||||
- Add support for menu inheritance.
|
||||
- Emphasize source column labels.
|
||||
- Backport file cache manager app.
|
||||
- Convert document image cache to use file cache manager app.
|
||||
Add setting DOCUMENTS_CACHE_MAXIMUM_SIZE defaults to 500 MB.
|
||||
- Update Celery to version 4.3.0. Settings changed:
|
||||
MAYAN_BROKER_URL to MAYAN_CELERY_BROKER_URL,
|
||||
MAYAN_CELERY_ALWAYS_EAGER to MAYAN_CELERY_TASK_ALWAYS_EAGER.
|
||||
- Replace djcelery and replace it with django-celery-beat.
|
||||
- Update Celery to version 4.3.0 with 55e9b2263cbdb9b449361412fd18d8ee0a442dd3
|
||||
from versions/next, code from GitLab issue #594 and GitLab merge request !55.
|
||||
Thanks to Jakob Haufe (@sur5r) and Jesaja Everling (@jeverling)
|
||||
for much of the research and code updates.
|
||||
- Support wildcard MIME type associations for the file metadata drivers.
|
||||
- Rename MAYAN_GUID to MAYAN_GID
|
||||
- Update Gunicorn to use sync workers.
|
||||
- Include devpi-server as a development dependency.
|
||||
- Update default Docker stack file.
|
||||
- Remove Redis from the Docker image.
|
||||
- Add Celery flower to the Docker image.
|
||||
- Allow PIP proxying to the Docker image during build.
|
||||
- Default Celery worker concurrency to 0 (auto).
|
||||
- Set DJANGO_SETTINGS_MODULE environment variable to make it
|
||||
available to sub processes.
|
||||
- Add entrypoint commands to run single workers, single gunicorn
|
||||
or single celery commands like "flower".
|
||||
- Add platform template to return queues for a worker.
|
||||
- Remove task inspection from task manager app.
|
||||
- Move pagination navigation inside the toolbar.
|
||||
- Remove document image clear link and view.
|
||||
This is now handled by the file caching app.
|
||||
- Add web links app.
|
||||
- Add support to display column help text
|
||||
as a tooltip.
|
||||
- Update numeric dashboard widget to display
|
||||
thousand commas.
|
||||
- Add support for disabling document pages.
|
||||
- Add support for converter layers.
|
||||
- Add redactions app.
|
||||
- Unify all line endings to be Linux style.
|
||||
- Add support for changing the system messages position.
|
||||
GitLab issue #640. Thanks to Matthias Urhahn (@d4rken).
|
||||
|
||||
Removals
|
||||
--------
|
||||
|
||||
- Database conversion. Reason for removal: The database conversions support
|
||||
provided by this feature (SQLite to PostgreSQL) was being confused with
|
||||
database migrations and upgrades.
|
||||
|
||||
Database upgrades are the responsibility of the app and the framework.
|
||||
Database conversions however are not the responsibility of the app (Mayan),
|
||||
they are the responsibility of the framework.
|
||||
|
||||
Database conversion is outside the scope of what Mayan does but we added
|
||||
the code, management command, instructions and testing setup to provide
|
||||
this to our users until the framework (Django) decided to add this
|
||||
themselves (like they did with migrations).
|
||||
|
||||
Continued confusion about the purpose of the feature and confusion about
|
||||
how errors with this feature were a reflexion of the code quality of
|
||||
Mayan necessitated the removal of the database conversion feature.
|
||||
|
||||
- Django environ
|
||||
|
||||
|
||||
Upgrading from a previous version
|
||||
---------------------------------
|
||||
|
||||
If installed via Python's PIP
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Remove deprecated requirements::
|
||||
|
||||
sudo -u mayan curl https://gitlab.com/mayan-edms/mayan-edms/raw/master/removals.txt -o /tmp/removals.txt && sudo -u mayan /opt/mayan-edms/bin/pip uninstall -y -r /tmp/removals.txt
|
||||
|
||||
Type in the console::
|
||||
|
||||
/opt/mayan-edms/bin/pip install mayan-edms==3.3
|
||||
|
||||
the requirements will also be updated automatically.
|
||||
|
||||
|
||||
Using Git
|
||||
^^^^^^^^^
|
||||
|
||||
If you installed Mayan EDMS by cloning the Git repository issue the commands::
|
||||
|
||||
git reset --hard HEAD
|
||||
git pull
|
||||
|
||||
otherwise download the compressed archived and uncompress it overriding the
|
||||
existing installation.
|
||||
|
||||
Remove deprecated requirements::
|
||||
|
||||
pip uninstall -y -r removals.txt
|
||||
|
||||
Next upgrade/add the new requirements::
|
||||
|
||||
pip install --upgrade -r requirements.txt
|
||||
|
||||
|
||||
Common steps
|
||||
^^^^^^^^^^^^
|
||||
|
||||
Perform these steps after updating the code from either step above.
|
||||
|
||||
Make a backup of your supervisord file::
|
||||
|
||||
sudo cp /etc/supervisor/conf.d/mayan.conf /etc/supervisor/conf.d/mayan.conf.bck
|
||||
|
||||
Update the supervisord configuration file. Replace the environment
|
||||
variables values show here with your respective settings. This step will refresh
|
||||
the supervisord configuration file with the new queues and the latest
|
||||
recommended layout::
|
||||
|
||||
sudo MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \
|
||||
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
||||
/opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf
|
||||
|
||||
Edit the supervisord configuration file and update any setting the template
|
||||
generator missed::
|
||||
|
||||
sudo vi /etc/supervisor/conf.d/mayan.conf
|
||||
|
||||
Migrate existing database schema with::
|
||||
|
||||
sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media /opt/mayan-edms/bin/mayan-edms.py performupgrade
|
||||
|
||||
Add new static media::
|
||||
|
||||
sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media /opt/mayan-edms/bin/mayan-edms.py preparestatic --noinput
|
||||
|
||||
The upgrade procedure is now complete.
|
||||
|
||||
|
||||
Backward incompatible changes
|
||||
-----------------------------
|
||||
|
||||
- Update quoted settings to be unquoted:
|
||||
|
||||
- COMMON_SHARED_STORAGE_ARGUMENTS
|
||||
- CONVERTER_GRAPHICS_BACKEND_ARGUMENTS
|
||||
- DOCUMENTS_CACHE_STORAGE_BACKEND_ARGUMENTS
|
||||
- DOCUMENTS_STORAGE_BACKEND_ARGUMENTS
|
||||
- FILE_METADATA_DRIVERS_ARGUMENTS
|
||||
- SIGNATURES_STORAGE_BACKEND_ARGUMENTS
|
||||
|
||||
|
||||
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:`640` UX: "Toast" Popup position prevents access to actions
|
||||
- :gitlab-issue:`644` Update sane-utils package in docker image.
|
||||
|
||||
|
||||
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/
|
||||
@@ -20,6 +20,7 @@ versions of the documentation contain the release notes for any later releases.
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
3.3
|
||||
3.2.8
|
||||
3.2.7
|
||||
3.2.6
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
__title__ = 'Mayan EDMS'
|
||||
__version__ = '3.2.7'
|
||||
__build__ = 0x030207
|
||||
__build_string__ = 'v3.2.7_Wed Aug 28 17:31:08 2019 -0400'
|
||||
__version__ = '3.3beta1'
|
||||
__build__ = 0x030300
|
||||
__build_string__ = 'v3.3beta1-9-g1b327b99f0_Tue Oct 8 15:15:08 2019 -0400'
|
||||
__django_version__ = '1.11'
|
||||
__author__ = 'Roberto Rosario'
|
||||
__author_email__ = 'roberto.rosario@mayan-edms.com'
|
||||
|
||||
@@ -12,6 +12,7 @@ logger = logging.getLogger(__name__)
|
||||
class ModelPermission(object):
|
||||
_functions = {}
|
||||
_inheritances = {}
|
||||
_manager_names = {}
|
||||
_registry = {}
|
||||
|
||||
@classmethod
|
||||
@@ -20,22 +21,6 @@ class ModelPermission(object):
|
||||
# TODO: Find method to revert the add_to_class('acls'...)
|
||||
# delattr doesn't work.
|
||||
|
||||
@classmethod
|
||||
def register(cls, model, permissions):
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
|
||||
cls._registry.setdefault(model, [])
|
||||
for permission in permissions:
|
||||
cls._registry[model].append(permission)
|
||||
|
||||
AccessControlList = apps.get_model(
|
||||
app_label='acls', model_name='AccessControlList'
|
||||
)
|
||||
|
||||
model.add_to_class(
|
||||
name='acls', value=GenericRelation(AccessControlList)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_classes(cls, as_content_type=False):
|
||||
ContentType = apps.get_model(
|
||||
@@ -97,6 +82,40 @@ 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(cls, model, permissions):
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
|
||||
cls._registry.setdefault(model, [])
|
||||
for permission in permissions:
|
||||
cls._registry[model].append(permission)
|
||||
|
||||
AccessControlList = apps.get_model(
|
||||
app_label='acls', model_name='AccessControlList'
|
||||
)
|
||||
|
||||
model.add_to_class(
|
||||
name='acls', value=GenericRelation(AccessControlList)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def register_function(cls, model, function):
|
||||
cls._functions[model] = function
|
||||
@@ -104,3 +123,7 @@ 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
|
||||
|
||||
@@ -45,8 +45,8 @@ class AccessControlListManager(models.Manager):
|
||||
# 4: No related field, but has an inherited related field, solved by
|
||||
# recursion, branches to #2 or #3.
|
||||
# 5: Inherited field of a related field
|
||||
# -- Not addressed yet --
|
||||
# 6: Inherited field of a related field that is Generic Foreign Key
|
||||
# -- Not addressed yet --
|
||||
# 7: Has a related function
|
||||
result = []
|
||||
|
||||
@@ -58,10 +58,28 @@ class AccessControlListManager(models.Manager):
|
||||
if isinstance(related_field, GenericForeignKey):
|
||||
# Case 3: Generic Foreign Key, multiple ContentTypes + object
|
||||
# id combinations
|
||||
# Also handles case #6 using the parent related field
|
||||
# reference template.
|
||||
|
||||
# Craft a double underscore reference to a previous related
|
||||
# field in the case where multiple related fields are
|
||||
# associated.
|
||||
# Example: object_layer__content_type
|
||||
recuisive_related_reference = '__'.join(related_field_name.split('__')[0:-1])
|
||||
|
||||
# If there is at least one parent related field we add a
|
||||
# double underscore to make it a valid filter template.
|
||||
if recuisive_related_reference:
|
||||
recuisive_related_reference = '{}__'.format(recuisive_related_reference)
|
||||
|
||||
content_type_object_id_queryset = queryset.annotate(
|
||||
ct_fk_combination=Concat(
|
||||
related_field.ct_field, Value('-'),
|
||||
related_field.fk_field, output_field=CharField()
|
||||
'{}{}'.format(
|
||||
recuisive_related_reference, related_field.ct_field
|
||||
), Value('-'),
|
||||
'{}{}'.format(
|
||||
recuisive_related_reference, related_field.fk_field
|
||||
), output_field=CharField()
|
||||
)
|
||||
).values('ct_fk_combination')
|
||||
|
||||
@@ -75,8 +93,7 @@ class AccessControlListManager(models.Manager):
|
||||
ct_fk_combination__in=content_type_object_id_queryset
|
||||
).values('object_id')
|
||||
|
||||
field_lookup = 'object_id__in'
|
||||
|
||||
field_lookup = '{}object_id__in'.format(recuisive_related_reference)
|
||||
result.append(Q(**{field_lookup: acl_filter}))
|
||||
else:
|
||||
# Case 2: Related field of a single type, single ContentType,
|
||||
@@ -97,6 +114,7 @@ class AccessControlListManager(models.Manager):
|
||||
|
||||
# Case 5: Related field, has an inherited related field itself
|
||||
# Bubble up permssion check
|
||||
# Recurse and reduce
|
||||
# TODO: Add relationship support: OR or AND
|
||||
# TODO: OR for document pages, version, doc, and types
|
||||
# TODO: AND for new cabinet levels ACLs
|
||||
@@ -200,28 +218,26 @@ class AccessControlListManager(models.Manager):
|
||||
|
||||
return result
|
||||
|
||||
def check_access(self, obj, permissions, user, manager=None):
|
||||
def check_access(self, obj, permissions, user):
|
||||
# Allow specific managers for models that have more than one
|
||||
# for example the Document model when checking for access for a trashed
|
||||
# document.
|
||||
|
||||
if manager:
|
||||
source_queryset = manager.all()
|
||||
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:
|
||||
meta = getattr(obj, '_meta', None)
|
||||
manager = ModelPermission.get_manager(model=obj._meta.model)
|
||||
source_queryset = manager.all()
|
||||
|
||||
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()
|
||||
restricted_queryset = manager.none()
|
||||
for permission in permissions:
|
||||
# Default relationship betweens permissions is OR
|
||||
# TODO: Add support for AND relationship
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import absolute_import, unicode_literals
|
||||
from rest_framework import status
|
||||
|
||||
from mayan.apps.permissions.tests.literals import TEST_ROLE_LABEL
|
||||
from mayan.apps.rest_api.tests import BaseAPITestCase
|
||||
from mayan.apps.rest_api.tests.base import BaseAPITestCase
|
||||
|
||||
from ..models import AccessControlList
|
||||
from ..permissions import permission_acl_edit, permission_acl_view
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from mayan.apps.common.tests import BaseTestCase
|
||||
from mayan.apps.common.tests.base import BaseTestCase
|
||||
|
||||
from ..classes import ModelPermission
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from mayan.apps.common.tests import GenericViewTestCase
|
||||
from mayan.apps.common.tests.base import GenericViewTestCase
|
||||
|
||||
from ..links import (
|
||||
link_acl_delete, link_acl_list, link_acl_create, link_acl_permissions
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import absolute_import, unicode_literals
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import models
|
||||
|
||||
from mayan.apps.common.tests import BaseTestCase
|
||||
from mayan.apps.common.tests.base import BaseTestCase
|
||||
|
||||
from ..classes import ModelPermission
|
||||
from ..models import AccessControlList
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from mayan.apps.common.tests import GenericViewTestCase
|
||||
from mayan.apps.common.tests.base import GenericViewTestCase
|
||||
|
||||
from ..models import AccessControlList
|
||||
from ..permissions import permission_acl_edit, permission_acl_view
|
||||
|
||||
@@ -16,7 +16,6 @@ from mayan.apps.permissions.models import Role
|
||||
from .classes import ModelPermission
|
||||
from .permissions import permission_acl_edit
|
||||
|
||||
__all__ = ('GrantAccessAction', 'RevokeAccessAction')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -57,7 +56,7 @@ class GrantAccessAction(WorkflowAction):
|
||||
}
|
||||
}
|
||||
field_order = ('content_type', 'object_id', 'roles', 'permissions')
|
||||
label = _('Grant access')
|
||||
label = _('Grant object access')
|
||||
widgets = {
|
||||
'content_type': {
|
||||
'class': 'django.forms.widgets.Select', 'kwargs': {
|
||||
@@ -140,7 +139,7 @@ class GrantAccessAction(WorkflowAction):
|
||||
|
||||
|
||||
class RevokeAccessAction(GrantAccessAction):
|
||||
label = _('Revoke access')
|
||||
label = _('Revoke object access')
|
||||
|
||||
def execute(self, context):
|
||||
self.get_execute_data()
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.template.loader import get_template
|
||||
|
||||
|
||||
class IconDriver(object):
|
||||
context = {}
|
||||
_registry = {}
|
||||
|
||||
@classmethod
|
||||
@@ -14,6 +15,17 @@ class IconDriver(object):
|
||||
def register(cls, driver_class):
|
||||
cls._registry[driver_class.name] = driver_class
|
||||
|
||||
def get_context(self):
|
||||
return self.context
|
||||
|
||||
def render(self, extra_context=None):
|
||||
context = self.get_context()
|
||||
if extra_context:
|
||||
context.update(extra_context)
|
||||
return get_template(template_name=self.template_name).render(
|
||||
context=context
|
||||
)
|
||||
|
||||
|
||||
class FontAwesomeDriver(IconDriver):
|
||||
name = 'fontawesome'
|
||||
@@ -22,10 +34,8 @@ class FontAwesomeDriver(IconDriver):
|
||||
def __init__(self, symbol):
|
||||
self.symbol = symbol
|
||||
|
||||
def render(self):
|
||||
return get_template(template_name=self.template_name).render(
|
||||
context={'symbol': self.symbol}
|
||||
)
|
||||
def get_context(self):
|
||||
return {'symbol': self.symbol}
|
||||
|
||||
|
||||
class FontAwesomeDualDriver(IconDriver):
|
||||
@@ -36,23 +46,21 @@ class FontAwesomeDualDriver(IconDriver):
|
||||
self.primary_symbol = primary_symbol
|
||||
self.secondary_symbol = secondary_symbol
|
||||
|
||||
def render(self):
|
||||
return get_template(template_name=self.template_name).render(
|
||||
context={
|
||||
'data': (
|
||||
{
|
||||
'class': 'fas fa-circle',
|
||||
'transform': 'down-3 right-10',
|
||||
'mask': 'fas fa-{}'.format(self.primary_symbol)
|
||||
},
|
||||
{'class': 'far fa-circle', 'transform': 'down-3 right-10'},
|
||||
{
|
||||
'class': 'fas fa-{}'.format(self.secondary_symbol),
|
||||
'transform': 'shrink-4 down-3 right-10'
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
def get_context(self):
|
||||
return {
|
||||
'data': (
|
||||
{
|
||||
'class': 'fas fa-circle',
|
||||
'transform': 'down-3 right-10',
|
||||
'mask': 'fas fa-{}'.format(self.primary_symbol)
|
||||
},
|
||||
{'class': 'far fa-circle', 'transform': 'down-3 right-10'},
|
||||
{
|
||||
'class': 'fas fa-{}'.format(self.secondary_symbol),
|
||||
'transform': 'shrink-4 down-3 right-10'
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class FontAwesomeCSSDriver(IconDriver):
|
||||
@@ -62,10 +70,8 @@ class FontAwesomeCSSDriver(IconDriver):
|
||||
def __init__(self, css_classes):
|
||||
self.css_classes = css_classes
|
||||
|
||||
def render(self):
|
||||
return get_template(template_name=self.template_name).render(
|
||||
context={'css_classes': self.css_classes}
|
||||
)
|
||||
def get_context(self):
|
||||
return {'css_classes': self.css_classes}
|
||||
|
||||
|
||||
class FontAwesomeMasksDriver(IconDriver):
|
||||
@@ -75,23 +81,23 @@ class FontAwesomeMasksDriver(IconDriver):
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
def render(self):
|
||||
return get_template(template_name=self.template_name).render(
|
||||
context={'data': self.data}
|
||||
)
|
||||
def get_context(self):
|
||||
return {'data': self.data}
|
||||
|
||||
|
||||
class FontAwesomeLayersDriver(IconDriver):
|
||||
name = 'fontawesome-layers'
|
||||
template_name = 'appearance/icons/font_awesome_layers.html'
|
||||
|
||||
def __init__(self, data):
|
||||
def __init__(self, data, shadow_class=None):
|
||||
self.data = data
|
||||
self.shadow_class = shadow_class
|
||||
|
||||
def render(self):
|
||||
return get_template(template_name=self.template_name).render(
|
||||
context={'data': self.data}
|
||||
)
|
||||
def get_context(self):
|
||||
return {
|
||||
'data': self.data,
|
||||
'shadow_class': self.shadow_class,
|
||||
}
|
||||
|
||||
|
||||
class Icon(object):
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
DEFAULT_MAXIMUM_TITLE_LENGTH = 120
|
||||
DEFAULT_MESSAGE_POSITION = 'top-right'
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mayan.apps.smart_settings.classes import Namespace
|
||||
|
||||
from .literals import DEFAULT_MAXIMUM_TITLE_LENGTH
|
||||
from .literals import DEFAULT_MAXIMUM_TITLE_LENGTH, DEFAULT_MESSAGE_POSITION
|
||||
|
||||
namespace = Namespace(label=_('Appearance'), name='appearance')
|
||||
|
||||
@@ -15,3 +15,11 @@ setting_max_title_length = namespace.add_setting(
|
||||
'title.'
|
||||
)
|
||||
)
|
||||
setting_message_position = namespace.add_setting(
|
||||
default=DEFAULT_MESSAGE_POSITION,
|
||||
global_name='APPEARANCE_MESSAGE_POSITION', help_text=_(
|
||||
'Position where the system message will be displayed. Options are: '
|
||||
'top-left, top-center, top-right, bottom-left, bottom-center, '
|
||||
'bottom-right.'
|
||||
)
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: 70px;
|
||||
padding-top: 60px;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
@@ -70,7 +70,8 @@ img.lazy-load-carousel {
|
||||
}
|
||||
|
||||
.label-tag {
|
||||
text-shadow: 0px 0px 2px #000
|
||||
text-shadow: 0px 0px 2px #000;
|
||||
box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.fancybox-nav span {
|
||||
@@ -88,19 +89,17 @@ hr {
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
border-top: 2px solid rgba(255, 255, 255, 0.7);
|
||||
border-left: 2px solid rgba(255, 255, 255, 0.7);
|
||||
border-right: 2px solid rgba(0, 0, 0, 0.7);
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.7);
|
||||
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5);
|
||||
margin-bottom: 15px;
|
||||
white-space: normal;
|
||||
min-height: 120px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.btn-block .fa {
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||
padding-top: 20px;
|
||||
text-shadow: 1px 1px 3px rgba(0, 0, 0, 1);
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.radio ul li {
|
||||
@@ -112,14 +111,10 @@ a i {
|
||||
}
|
||||
|
||||
.dashboard-widget {
|
||||
box-shadow: 1px 1px 1px rgba(0,0,0,0.3);
|
||||
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.7);
|
||||
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%;
|
||||
}
|
||||
@@ -170,7 +165,7 @@ a i {
|
||||
}
|
||||
.navbar-collapse {
|
||||
border-top: 1px solid transparent;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.1);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.navbar-fixed-top {
|
||||
top: 0;
|
||||
@@ -213,6 +208,22 @@ a i {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.source-column-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.panel-highlighted {
|
||||
box-shadow: 0px 0px 3px #18bc9c, 10px 10px 20px #000000;
|
||||
}
|
||||
|
||||
.panel-highlighted:hover {
|
||||
box-shadow: 0px 0px 3px #18bc9c, 10px 10px 20px #000000, 0px 0px 8px #000000;
|
||||
}
|
||||
|
||||
.panel-item:not(.panel-highlighted):hover {
|
||||
box-shadow: 0px 0px 8px #000000;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
@media (min-width:1200px) {
|
||||
.container-fluid {
|
||||
@@ -242,14 +253,6 @@ 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%;
|
||||
@@ -259,10 +262,18 @@ 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: 12px;
|
||||
right: 10px;
|
||||
top: 16px;
|
||||
left: 10px;
|
||||
z-index: 9999;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
@@ -328,7 +339,7 @@ a i {
|
||||
.main {
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
/*margin-left: 210px;*/
|
||||
margin-left: 210px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,6 +422,141 @@ a i {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Top navigation
|
||||
* Hide default border to remove 1px line.
|
||||
*/
|
||||
.navbar-fixed-top {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
|
||||
/* menu_main */
|
||||
/* Hide for mobile, show later */
|
||||
|
||||
#menu-main {
|
||||
display: none;
|
||||
background-color: #2c3e50;
|
||||
border-right: 1px solid #18bc9c;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding-top: 10px;
|
||||
position: fixed;
|
||||
top: 51px;
|
||||
width: 210px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
#menu-main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
text-align: center;
|
||||
width: 210px;
|
||||
}
|
||||
}
|
||||
|
||||
.main .page-header {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
margin-right: 0px;
|
||||
margin-left: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#accordion-sidebar a {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
#accordion-sidebar a[aria-expanded="true"] {
|
||||
background: #1a242f;
|
||||
}
|
||||
|
||||
#accordion-sidebar .panel {
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
#accordion-sidebar a {
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#accordion-sidebar .panel-heading {
|
||||
background-color: #2c3e50;
|
||||
color: white;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#accordion-sidebar .panel-heading:hover {
|
||||
background-color: #517394;
|
||||
}
|
||||
|
||||
#accordion-sidebar > .panel > div > .panel-body > ul > li > a:hover {
|
||||
background-color: #517394;
|
||||
}
|
||||
|
||||
#accordion-sidebar > .panel > div > .panel-body > ul > li.active {
|
||||
background: #1a242f;
|
||||
}
|
||||
|
||||
#accordion-sidebar .panel-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#accordion-sidebar .panel-body {
|
||||
font-size: 13px;
|
||||
border: 0px;
|
||||
background-color: #2c3e50;
|
||||
padding-top: 5px;
|
||||
padding-left: 20px;
|
||||
padding-right: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
#accordion-sidebar .panel-body li {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#accordion-sidebar .panel-body a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 9px;
|
||||
}
|
||||
|
||||
.navbar-fixed-top {
|
||||
box-shadow: 0px 3px 3px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 1px 1px 2px rgba(0, 0, 0, .3);
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
padding-left: 12px;
|
||||
padding-right: 15px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
#body-plain {
|
||||
padding-top: 0px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* jstree - cabinets */
|
||||
#jstree {
|
||||
max-width: 100%;
|
||||
@@ -418,5 +564,4 @@ a i {
|
||||
font: 11px Verdana, sans-serif;
|
||||
padding: 0px;
|
||||
padding-bottom: 10px; /* Padding for scrollbar */
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ var MayanAppClass = MayanApp;
|
||||
|
||||
var partialNavigation = new PartialNavigation({
|
||||
initialURL: initialURL,
|
||||
disabledAnchorClasses: ['disabled'],
|
||||
disabledAnchorClasses: [
|
||||
'btn-multi-item-action', 'disabled', 'pagination-disabled'
|
||||
],
|
||||
excludeAnchorClasses: ['fancybox', 'new_window', 'non-ajax'],
|
||||
formBeforeSerializeCallbacks: [MayanApp.MultiObjectFormProcess],
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ class MayanApp {
|
||||
constructor (options) {
|
||||
var self = this;
|
||||
|
||||
options = options || {
|
||||
this.options = options || {
|
||||
ajaxMenusOptions: []
|
||||
}
|
||||
|
||||
@@ -17,28 +17,44 @@ class MayanApp {
|
||||
|
||||
// Class methods and variables
|
||||
|
||||
static MultiObjectFormProcess ($form, options) {
|
||||
/*
|
||||
* ajaxForm callback to add the external item checkboxes to the
|
||||
* submitted form
|
||||
*/
|
||||
static countChecked() {
|
||||
var checkCount = $('.check-all-slave:checked').length;
|
||||
|
||||
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;
|
||||
}, {});
|
||||
if (checkCount) {
|
||||
$('#multi-item-title').hide();
|
||||
$('#multi-item-actions').show();
|
||||
} else {
|
||||
$('#multi-item-title').show();
|
||||
$('#multi-item-actions').hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Add all checked checkboxes to the form data
|
||||
$('.form-multi-object-action-checkbox:checked').each(function() {
|
||||
var $this = $(this);
|
||||
formArray[$this.attr('name')] = $this.attr('value');
|
||||
static setupMultiItemActions () {
|
||||
$('body').on('change', '.check-all-slave', function () {
|
||||
MayanApp.countChecked();
|
||||
});
|
||||
|
||||
$('body').on('click', '.btn-multi-item-action', function (event) {
|
||||
var id_list = [];
|
||||
$('.check-all-slave:checked').each(function (index, value) {
|
||||
//Split the name (ie:"pk_200") and extract only the ID
|
||||
id_list.push(value.name.split('_')[1]);
|
||||
});
|
||||
event.preventDefault();
|
||||
partialNavigation.setLocation(
|
||||
$(this).attr('href') + '?id_list=' + id_list.join(',')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static setupNavBarState () {
|
||||
$('body').on('click', '.a-main-menu-accordion-link', function (event) {
|
||||
$('.a-main-menu-accordion-link').each(function (index, value) {
|
||||
$(this).parent().removeClass('active');
|
||||
});
|
||||
|
||||
// Set the form data as the data to send
|
||||
options.data = formArray;
|
||||
}
|
||||
$(this).parent().addClass('active');
|
||||
});
|
||||
}
|
||||
|
||||
static updateNavbarState () {
|
||||
@@ -46,8 +62,10 @@ class MayanApp {
|
||||
var uriFragment = uri.fragment();
|
||||
$('.a-main-menu-accordion-link').each(function (index, value) {
|
||||
if (value.pathname === uriFragment) {
|
||||
$(this).closest('.collapse').addClass('in').parent().find('.collapsed').removeClass('collapsed').attr('aria-expanded', 'true');
|
||||
$(this).parent().addClass('active');
|
||||
var $this = $(this);
|
||||
|
||||
$this.closest('.collapse').addClass('in').parent().find('.collapsed').removeClass('collapsed').attr('aria-expanded', 'true');
|
||||
$this.parent().addClass('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -92,7 +110,7 @@ class MayanApp {
|
||||
'closeButton': true,
|
||||
'debug': false,
|
||||
'newestOnTop': true,
|
||||
'positionClass': 'toast-top-right',
|
||||
'positionClass': 'toast-' + this.options.messagePosition,
|
||||
'preventDuplicates': false,
|
||||
'onclick': null,
|
||||
'showDuration': '300',
|
||||
@@ -162,17 +180,19 @@ class MayanApp {
|
||||
var self = this;
|
||||
|
||||
this.setupAJAXSpinner();
|
||||
this.setupAutoSubmit();
|
||||
this.setupBodyAdjust();
|
||||
this.setupFormHotkeys();
|
||||
this.setupFullHeightResizing();
|
||||
this.setupItemsSelector();
|
||||
MayanApp.setupMultiItemActions();
|
||||
this.setupNavbarCollapse();
|
||||
MayanApp.setupNavBarState();
|
||||
this.setupNewWindowAnchor();
|
||||
$.each(this.ajaxMenusOptions, function(index, value) {
|
||||
value.app = self;
|
||||
app.doRefreshAJAXMenu(value);
|
||||
});
|
||||
this.setupPanelSelection();
|
||||
partialNavigation.initialize();
|
||||
}
|
||||
|
||||
@@ -196,14 +216,6 @@ class MayanApp {
|
||||
});
|
||||
}
|
||||
|
||||
setupAutoSubmit () {
|
||||
$('body').on('change', '.select-auto-submit', function () {
|
||||
if ($(this).val()) {
|
||||
$(this.form).trigger('submit');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupBodyAdjust () {
|
||||
var self = this;
|
||||
|
||||
@@ -242,9 +254,22 @@ 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');
|
||||
});
|
||||
@@ -290,6 +315,58 @@ 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();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
{% block content_plain %}{% endblock %}
|
||||
{% else %}
|
||||
<div class="">
|
||||
|
||||
{% navigation_resolve_menus names='facet,list facet' sort_results=True as facet_menus_link_results %}
|
||||
|
||||
<div class="row zero-margin">
|
||||
@@ -43,7 +44,7 @@
|
||||
{% if settings_changed %}
|
||||
<div class="alert alert-dismissible alert-warning">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
<p><strong>{% trans 'Warning' %}</strong> {% trans 'Settings updated, restart your installation for changes to take proper effect.' %}</p>
|
||||
<p><strong>{% trans 'Warning' %}</strong> {% trans 'Settings updated, restart your installation and refresh your browser for changes to take effect.' %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -138,6 +139,9 @@
|
||||
},
|
||||
{% endfor %}
|
||||
];
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
})
|
||||
</script>
|
||||
{% block javascript %}{% endblock %}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<body id="body-plain">
|
||||
{% block content_plain %}{% endblock %}
|
||||
|
||||
<script src="{% static 'appearance/node_modules/jquery/dist/jquery.min.js' %}" type="text/javascript"></script>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
{% for field in form.visible_fields %}
|
||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
<div class="form-group {% if field.errors %}has-error{% endif %} {{ form_field_css_classes }}">
|
||||
{# We display the label then the field for all except checkboxes #}
|
||||
{% if field|widget_type != 'checkboxinput' and not field.field.widget.attrs.hidden %}
|
||||
{% if not hide_labels %}{{ field.label_tag }}{% if field.field.required and not read_only %} ({% trans 'required' %}){% endif %}{% endif %}
|
||||
|
||||
@@ -11,41 +11,9 @@
|
||||
{% include 'appearance/no_results.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<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>
|
||||
{% include "appearance/list_header.html" %}
|
||||
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
|
||||
<div class="well center-block">
|
||||
<div class="clearfix">
|
||||
<div class="pull-right">
|
||||
<form action="{% url 'common:multi_object_action_view' %}" class="form-multi-object-action" method="get">
|
||||
{% if object_list %}
|
||||
{% if not hide_multi_item_actions %}
|
||||
{% get_multi_item_links_form object_list %}
|
||||
{% endif %}
|
||||
{% if multi_item_actions %}
|
||||
<fieldset style="margin-top: -10px;">
|
||||
<input class="check-all" type="checkbox"/>
|
||||
{{ multi_item_form }}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if object_list %}
|
||||
<hr style="border-bottom: 1px solid lightgrey;">
|
||||
{% endif %}
|
||||
|
||||
<div class="row row-items">
|
||||
{% for object in object_list %}
|
||||
<div class="{{ column_class|default:'col-xs-12 col-sm-4 col-md-3 col-lg-2' }}">
|
||||
@@ -53,9 +21,9 @@
|
||||
<div class="panel-heading">
|
||||
<div class="form-group">
|
||||
<div class="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 }}" />
|
||||
<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" />
|
||||
{% endif %}
|
||||
|
||||
<span style="color: white; word-break: break-all; overflow-wrap: break-word;">
|
||||
@@ -68,12 +36,7 @@
|
||||
{% else %}
|
||||
{% navigation_get_source_columns source=object only_identifier=True as source_column %}
|
||||
{% navigation_source_column_resolve column=source_column as 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 %}
|
||||
{{ column_value }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</label>
|
||||
@@ -82,11 +45,10 @@
|
||||
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
{% if not hide_columns %}
|
||||
{% navigation_get_source_columns source=object exclude_identifier=True as source_columns %}
|
||||
{% for column in source_columns %}
|
||||
<div class="text-center" style="">{% navigation_source_column_resolve column=column as column_value %}{% if column_value != '' %}{% if column.include_label %}{{ column.label }}: {% endif %}{{ column_value }}{% endif %}</div>
|
||||
<div class="text-center" style="">{% navigation_source_column_resolve column=column as column_value %}{% if column_value != '' %}{% if column.include_label %}<span class="source-column-label">{{ column.label }}</span>: {% endif %}{{ column_value }}{% endif %}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@@ -136,7 +98,6 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% include 'pagination/pagination.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% load appearance_tags %}
|
||||
{% load common_tags %}
|
||||
{% load navigation_tags %}
|
||||
|
||||
@@ -11,44 +12,16 @@
|
||||
{% include 'appearance/no_results.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<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>
|
||||
|
||||
{% include "appearance/list_header.html" %}
|
||||
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
|
||||
<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 multi_item_actions %}
|
||||
<th class="first"><input class="checkbox check-all" type="checkbox" /></th>
|
||||
{% if links_multi_menus_results %}
|
||||
<th class="first"></th>
|
||||
{% endif %}
|
||||
|
||||
{% if not hide_object %}
|
||||
@@ -57,32 +30,46 @@
|
||||
{% navigation_get_source_columns source=object_list only_identifier=True as source_column %}
|
||||
{% if source_column %}
|
||||
<th>
|
||||
{% if source_column.is_sortable %}
|
||||
<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 %}
|
||||
<span style="white-space: nowrap">
|
||||
{% 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 icon_sort %}{{ icon_sort.render }}{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ source_column.label }}
|
||||
{% 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 %}
|
||||
</span>
|
||||
</th>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if not hide_columns %}
|
||||
{% navigation_get_source_columns source=object_list exclude_identifier=True as source_columns %}
|
||||
{% for column in source_columns %}
|
||||
{% for source_column in source_columns %}
|
||||
<th>
|
||||
{% 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 %}
|
||||
<span style="white-space: nowrap">
|
||||
{% 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 icon_sort %}{{ icon_sort.render }}{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ source_column.label }}
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ 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 %}
|
||||
</span>
|
||||
</th>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@@ -99,9 +86,9 @@
|
||||
{% for object in object_list %}
|
||||
<tr>
|
||||
|
||||
{% if multi_item_actions %}
|
||||
{% if links_multi_menus_results %}
|
||||
<td>
|
||||
<input type="checkbox" class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" value="" />
|
||||
<input class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" type="checkbox" value="" />
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
@@ -112,11 +99,7 @@
|
||||
{% navigation_source_column_resolve column=source_column as column_value %}
|
||||
{% if column_value %}
|
||||
<td>
|
||||
{% 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 %}
|
||||
{{ column_value }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -170,7 +153,6 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'pagination/pagination.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<span class="fa-layers fa-fw" style="margin-right: 7px;">
|
||||
{% if enable_shadow %}
|
||||
<i class="{{ shadow_class }}" data-fa-transform="right-1 down-2" style="color:rgba(0, 0, 0, 0.3); stroke: rgba(255, 255, 255, 0.3); stroke-width: 20;"></i>
|
||||
{% endif %}
|
||||
{% for entry in data %}
|
||||
<i class="{{ entry.class }}" data-fa-transform="{{ entry.transform }}" data-fa-mask="{{ entry.mask }}"></i>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
<i class="fa fa-{{ symbol }}" style="padding-right: 5px; width: auto;"></i>
|
||||
{% if enable_shadow %}
|
||||
<span class="fa-layers fa-fw" >
|
||||
<i class="fa fa-{{ symbol }}" data-fa-transform="right-1 down-2" style="color:rgba(0, 0, 0, 0.3);stroke: rgba(255, 255, 255, 0.3); stroke-width: 20;"></i>
|
||||
<i class="fa fa-{{ symbol }}"></i>
|
||||
</span>
|
||||
{% else %}
|
||||
<i class="fa fa-{{ symbol }}" style="padding-right: 5px; width: auto;"></i>
|
||||
{% endif %}
|
||||
|
||||
28
mayan/apps/appearance/templates/appearance/list_header.html
Normal file
28
mayan/apps/appearance/templates/appearance/list_header.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% 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>
|
||||
90
mayan/apps/appearance/templates/appearance/list_toolbar.html
Normal file
90
mayan/apps/appearance/templates/appearance/list_toolbar.html
Normal file
@@ -0,0 +1,90 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% load common_tags %}
|
||||
{% load navigation_tags %}
|
||||
|
||||
{% if is_paginated or links_multi_menus_results %}
|
||||
<div class="well center-block toolbar">
|
||||
{% endif %}
|
||||
|
||||
{% if links_multi_menus_results %}
|
||||
<div class="pull-left">
|
||||
<div class="btn-toolbar" role="toolbar" style="margin-right: 10px;">
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-default btn-sm check-all" data-checked=false data-icon-checked="fa fa-check-square" data-icon-unchecked="far fa-square" title="{% trans 'Select/Deselect all' %}">
|
||||
<i class="far fa-square"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_paginated %}
|
||||
<div class="pull-left">
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group">
|
||||
{% if page_obj.has_previous %}
|
||||
<a class="btn btn-default btn-sm" href="?{{ page_obj.previous_page_number.querystring }}">‹‹</a>
|
||||
{% else %}
|
||||
<a class="btn btn-default btn-sm disabled" href="#">‹‹</a>
|
||||
{% endif %}
|
||||
|
||||
{% for page in page_obj.pages %}
|
||||
|
||||
{% if page %}
|
||||
|
||||
{% ifequal page page_obj.number %}
|
||||
<a class="active btn btn-default btn-sm pagination-disabled" href="#">{{ page }}</a>
|
||||
{% else %}
|
||||
<a class="btn btn-default btn-sm" href="?{{ page.querystring }}">{{ page }}</a>
|
||||
{% endifequal %}
|
||||
{% else %}
|
||||
<a class="btn btn-default btn-sm disabled" href="#">...</a>
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
{% if page_obj.has_next %}
|
||||
<a class="btn btn-default btn-sm" href="?{{ page_obj.next_page_number.querystring }}">››</a>
|
||||
{% else %}
|
||||
<a class="btn btn-default btn-sm disabled" href="#">››</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if links_multi_menus_results %}
|
||||
<p class="pull-right" id="multi-item-title" style="line-height: 16px; padding-top: 8px;">{% trans 'Select items to activate bulk actions. Use Shift + click to select many.' %}</p>
|
||||
|
||||
<div class="pull-right btn-group" id="multi-item-actions" style="display: none;">
|
||||
<button aria-expanded="true" class="btn btn-danger btn-sm dropdown-toggle" data-toggle="dropdown" type="button">
|
||||
{% trans 'Bulk actions' %}
|
||||
<span class="caret"></span>
|
||||
<span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
{% for multi_item_menu_results in links_multi_menus_results %}
|
||||
{% for link_group in multi_item_menu_results.link_groups %}
|
||||
{% with link_group.links as object_navigation_links %}
|
||||
{% with 'true' as as_li %}
|
||||
{% with 'true' as hide_active_anchor %}
|
||||
{% with 'btn-sm btn-multi-item-action' as link_classes %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
{% endfor %}
|
||||
{% if not forloop.last and link_group %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_paginated or links_multi_menus_results %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
70
mayan/apps/appearance/templates/appearance/menu_main.html
Normal file
70
mayan/apps/appearance/templates/appearance/menu_main.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% load navigation_tags %}
|
||||
{% load smart_settings_tags %}
|
||||
|
||||
{% load common_tags %}
|
||||
{% load navigation_tags %}
|
||||
|
||||
{% spaceless %}
|
||||
<div class="panel-group" id="accordion-sidebar" role="tablist" aria-multiselectable="true">
|
||||
{% navigation_resolve_menu name='main' as main_menus_results %}
|
||||
{% for main_menu_results in main_menus_results %}
|
||||
{% for link_group in main_menu_results.link_groups %}
|
||||
{% for link in link_group.links %}
|
||||
{% with 'active' as li_class_active %}
|
||||
{% with ' ' as link_classes %}
|
||||
{% if link|get_type == "<class 'mayan.apps.navigation.classes.Menu'>" %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="headingOne">
|
||||
<h4 class="panel-title">
|
||||
<a class="non-ajax collapsed" role="button" data-toggle="collapse" data-parent="#accordion-sidebar" href="#accordion-body-{{ forloop.counter }}" aria-expanded="false" aria-controls="collapseOne">
|
||||
<div class="pull-left">
|
||||
{% if link.icon %}
|
||||
<i class="hidden-xs hidden-sm hidden-md {{ link.icon }}"></i>
|
||||
{% endif %}
|
||||
{% if link.icon_class %}{{ link.icon_class.render }}{% endif %}
|
||||
{{ link.label }}
|
||||
</div>
|
||||
<div class="accordion-indicator pull-right"><span class="caret"></span></div>
|
||||
<div class="clearfix"></div>
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="accordion-body-{{ forloop.counter }}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingOne">
|
||||
<div class="panel-body">
|
||||
<ul class="list-unstyled">
|
||||
{% navigation_resolve_menu name=link.name as sub_menus_results %}
|
||||
{% for sub_menu_results in sub_menus_results %}
|
||||
{% for link_group in sub_menu_results.link_groups %}
|
||||
{% with '' as link_class_active %}
|
||||
{% with 'a-main-menu-accordion-link' as link_classes %}
|
||||
{% with 'true' as as_li %}
|
||||
{% with link_group.links as object_navigation_links %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="headingOne">
|
||||
<h4 class="panel-title">
|
||||
{% include 'navigation/generic_link_instance.html' %}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endspaceless %}
|
||||
@@ -3,10 +3,11 @@
|
||||
{% load navigation_tags %}
|
||||
{% load smart_settings_tags %}
|
||||
|
||||
{% spaceless %}
|
||||
<nav class="navbar navbar-default navbar-fixed-top">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
|
||||
<button aria-expanded="false" aria-controls="navbar" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" type="button">
|
||||
<span class="sr-only">{% trans 'Toggle navigation' %}</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
@@ -14,9 +15,10 @@
|
||||
</button>
|
||||
<a class="navbar-brand" href="{% url home_view %}">{% smart_setting 'COMMON_PROJECT_TITLE' %}</a>
|
||||
</div>
|
||||
|
||||
<div id="navbar" class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
{% navigation_resolve_menu name='main' as topbar_menus_results %}
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{% navigation_resolve_menu name='topbar' as topbar_menus_results %}
|
||||
{% for tobpar_menu_result in topbar_menus_results %}
|
||||
{% for link_group in tobpar_menu_result.link_groups %}
|
||||
{% for link in link_group.links %}
|
||||
@@ -34,24 +36,8 @@
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{% get_menu_links name='main' as menu_links %}
|
||||
{% for link_set in menu_links %}
|
||||
{% for link in link_set %}
|
||||
{% with 'true' as as_li %}
|
||||
{% with 'true' as hide_active_anchor %}
|
||||
{% with 'active' as li_class_active %}
|
||||
{% with 'first' as li_class_first %}
|
||||
{% with ' ' as link_classes %}
|
||||
{% include 'navigation/generic_subnavigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endspaceless %}
|
||||
@@ -31,8 +31,11 @@
|
||||
{% if appearance_type == 'plain' %}
|
||||
{% block content_plain %}{% endblock %}
|
||||
{% else %}
|
||||
<div id="menu-topbar">
|
||||
{% include 'appearance/menu_topbar.html' %}
|
||||
</div>
|
||||
<div id="menu-main">
|
||||
{% include 'appearance/main_menu.html' %}
|
||||
{% include 'appearance/menu_main.html' %}
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="body-spacer"></div>
|
||||
@@ -102,6 +105,7 @@
|
||||
ajaxMenusOptions: [
|
||||
{
|
||||
callback: function (options) {
|
||||
MayanApp.updateNavbarState();
|
||||
options.app.doBodyAdjust();
|
||||
},
|
||||
interval: 5000,
|
||||
@@ -109,7 +113,14 @@
|
||||
name: 'menu_main',
|
||||
url: '{% url "rest_api:template-detail" "menu_main" %}'
|
||||
},
|
||||
]
|
||||
{
|
||||
interval: 5000,
|
||||
menuSelector: '#menu-topbar',
|
||||
name: 'menu_topbar',
|
||||
url: '{% url "rest_api:template-detail" "menu_topbar" %}'
|
||||
},
|
||||
],
|
||||
messagePosition: '{% smart_setting "APPEARANCE_MESSAGE_POSITION" %}'
|
||||
});
|
||||
|
||||
var afterBaseLoad = function () {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{% if page %}
|
||||
|
||||
{% ifequal page page_obj.number %}
|
||||
<li class="active"><a class="disabled" href="#">{{ page }}</a></li>
|
||||
<li class="active"><a class="pagination-disabled" href="#">{{ page }}</a></li>
|
||||
{% else %}
|
||||
<li><a href="?{{ page.querystring }}">{{ page }}</a></li>
|
||||
{% endifequal %}
|
||||
|
||||
@@ -7,6 +7,11 @@ from django.utils.translation import ugettext_lazy as _
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def appearance_icon_render(icon_class, enable_shadow=False):
|
||||
return icon_class.render(extra_context={'enable_shadow': enable_shadow})
|
||||
|
||||
|
||||
@register.filter
|
||||
def get_choice_value(field):
|
||||
try:
|
||||
|
||||
@@ -11,7 +11,7 @@ from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlunquote_plus
|
||||
|
||||
from mayan.apps.common.tests import GenericViewTestCase
|
||||
from mayan.apps.common.tests.base 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.literals import TEST_USER_PASSWORD_EDITED
|
||||
|
||||
@@ -4,21 +4,23 @@
|
||||
|
||||
{% if autoadmin_properties.account %}
|
||||
<div class="row">
|
||||
<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="col-xs-12 col-sm-10 col-sm-offset-1 col-md-10 col-md-offset-1 col-lg-6 col-lg-offset-3">
|
||||
<br>
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans 'First time login' %}</h3>
|
||||
<h3 class="text-center panel-title">{% trans 'Automatic credentials' %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="content login">
|
||||
{% smart_setting 'COMMON_PROJECT_TITLE' as project_title %}
|
||||
<p>{% blocktrans %}You have just finished installing <strong>{{ project_title }}</strong>, congratulations!{% endblocktrans %}</p>
|
||||
<p>{% trans 'Login using the following credentials:' %}</p>
|
||||
<p>{% blocktrans with autoadmin_properties.account as account %}Username: <strong>{{ account }}</strong>{% endblocktrans %}</p>
|
||||
<p>{% blocktrans with autoadmin_properties.account.email as email %}Email: <strong>{{ email }}</strong>{% endblocktrans %}</p>
|
||||
<p>{% blocktrans with autoadmin_properties.password as password %}Password: <strong>{{ password }}</strong>{% endblocktrans %}</p>
|
||||
<p>{% trans 'Be sure to change the password to increase security and to disable this message.' %}</p>
|
||||
<p class="text-center">{% blocktrans %}You have just finished installing <strong>{{ project_title }}</strong>, congratulations!{% endblocktrans %}</p>
|
||||
<p class="text-center">{% trans 'Login using the following credentials:' %}</p>
|
||||
<p class="text-center">
|
||||
{% blocktrans with autoadmin_properties.account as account %}Username: <strong>{{ account }}</strong>{% endblocktrans %}<br>
|
||||
{% blocktrans with autoadmin_properties.account.email as email %}Email: <strong>{{ email }}</strong>{% endblocktrans %}<br>
|
||||
{% blocktrans with autoadmin_properties.password as password %}Password: <strong>{{ password }}</strong>{% endblocktrans %}
|
||||
</p>
|
||||
<p class="text-center">{% trans 'Be sure to change the password to increase security and to disable this message.' %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,5 +3,5 @@ from __future__ import unicode_literals
|
||||
TEST_ADMIN_USER_EMAIL = 'testemail@example.com'
|
||||
TEST_ADMIN_USER_PASSWORD = 'test admin user password'
|
||||
TEST_ADMIN_USER_USERNAME = 'test_admin_user_username'
|
||||
TEST_FIRST_TIME_LOGIN_TEXT = 'First time login'
|
||||
TEST_FIRST_TIME_LOGIN_TEXT = 'Automatic credentials'
|
||||
TEST_MOCK_VIEW_TEXT = 'mock view text'
|
||||
|
||||
@@ -2,20 +2,24 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core import management
|
||||
from django.test import TestCase
|
||||
|
||||
from mayan.apps.common.tests.base import BaseTestCase
|
||||
from mayan.apps.common.tests.utils import mute_stdout
|
||||
|
||||
from ..models import AutoAdminSingleton
|
||||
|
||||
|
||||
class AutoAdminManagementCommandTestCase(TestCase):
|
||||
class AutoAdminManagementCommandTestCase(BaseTestCase):
|
||||
create_test_case_user = False
|
||||
|
||||
def setUp(self):
|
||||
super(AutoAdminManagementCommandTestCase, self).setUp()
|
||||
with mute_stdout():
|
||||
management.call_command('createautoadmin')
|
||||
|
||||
def tearDown(self):
|
||||
AutoAdminSingleton.objects.all().delete()
|
||||
super(AutoAdminManagementCommandTestCase, self).tearDown()
|
||||
|
||||
def test_autoadmin_creation(self):
|
||||
autoadmin = AutoAdminSingleton.objects.get()
|
||||
|
||||
@@ -2,8 +2,7 @@ from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from mayan.apps.common.tests.base import BaseTestCase
|
||||
from mayan.apps.common.tests.utils import mute_stdout
|
||||
|
||||
from ..models import AutoAdminSingleton
|
||||
@@ -12,7 +11,7 @@ from ..settings import setting_username
|
||||
from .literals import TEST_ADMIN_USER_PASSWORD
|
||||
|
||||
|
||||
class AutoAdminHandlerTestCase(TestCase):
|
||||
class AutoAdminHandlerTestCase(BaseTestCase):
|
||||
def test_post_admin_creation(self):
|
||||
logging.disable(logging.INFO)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mayan.apps.common.settings import setting_home_view
|
||||
from mayan.apps.common.tests import GenericViewTestCase
|
||||
from mayan.apps.common.tests.base import GenericViewTestCase
|
||||
from mayan.apps.common.tests.utils import mute_stdout
|
||||
|
||||
from ..models import AutoAdminSingleton
|
||||
|
||||
20
mayan/apps/cabinets/migrations/0002_auto_20190729_0236.py
Normal file
20
mayan/apps/cabinets/migrations/0002_auto_20190729_0236.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.22 on 2019-07-29 02:36
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cabinets', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='cabinet',
|
||||
name='label',
|
||||
field=models.CharField(help_text='A short text used to identify the cabinet.', max_length=128, verbose_name='Label'),
|
||||
),
|
||||
]
|
||||
@@ -32,7 +32,10 @@ class Cabinet(MPTTModel):
|
||||
blank=True, db_index=True, null=True, on_delete=models.CASCADE,
|
||||
related_name='children', to='self'
|
||||
)
|
||||
label = models.CharField(max_length=128, verbose_name=_('Label'))
|
||||
label = models.CharField(
|
||||
help_text=_('A short text used to identify the cabinet.'),
|
||||
max_length=128, verbose_name=_('Label')
|
||||
)
|
||||
documents = models.ManyToManyField(
|
||||
blank=True, related_name='cabinets', to=Document,
|
||||
verbose_name=_('Documents')
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-2">
|
||||
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-2" style="margin-right: -25px;">{# Remove gutter #}
|
||||
<h4>{% trans 'Navigation:' %}</h4>
|
||||
<div id="jstree"></div>
|
||||
</div>
|
||||
|
||||
@@ -5,8 +5,8 @@ from django.utils.encoding import force_text
|
||||
from rest_framework import status
|
||||
|
||||
from mayan.apps.documents.permissions import permission_document_view
|
||||
from mayan.apps.documents.tests import DocumentTestMixin
|
||||
from mayan.apps.rest_api.tests import BaseAPITestCase
|
||||
from mayan.apps.documents.tests.mixins import DocumentTestMixin
|
||||
from mayan.apps.rest_api.tests.base import BaseAPITestCase
|
||||
|
||||
from ..models import Cabinet
|
||||
from ..permissions import (
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
||||
|
||||
from actstream.models import Action
|
||||
|
||||
from mayan.apps.common.tests import GenericViewTestCase
|
||||
from mayan.apps.common.tests.base import GenericViewTestCase
|
||||
from mayan.apps.documents.tests.test_models import GenericDocumentTestCase
|
||||
|
||||
from ..events import (
|
||||
|
||||
@@ -2,8 +2,8 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from mayan.apps.common.tests import BaseTestCase
|
||||
from mayan.apps.documents.tests import DocumentTestMixin
|
||||
from mayan.apps.common.tests.base import BaseTestCase
|
||||
from mayan.apps.documents.tests.mixins import DocumentTestMixin
|
||||
|
||||
from ..models import Cabinet
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from mayan.apps.common.tests import GenericViewTestCase
|
||||
from mayan.apps.common.tests.base import GenericViewTestCase
|
||||
from mayan.apps.documents.permissions import permission_document_view
|
||||
from mayan.apps.documents.tests import GenericDocumentViewTestCase
|
||||
from mayan.apps.documents.tests.base import GenericDocumentViewTestCase
|
||||
|
||||
from ..models import Cabinet
|
||||
from ..permissions import (
|
||||
|
||||
@@ -2,9 +2,8 @@ from __future__ import unicode_literals
|
||||
|
||||
from mayan.apps.documents.models import Document
|
||||
from mayan.apps.documents.permissions import permission_document_create
|
||||
from mayan.apps.documents.tests import (
|
||||
GenericDocumentViewTestCase, TEST_SMALL_DOCUMENT_PATH,
|
||||
)
|
||||
from mayan.apps.documents.tests.base import GenericDocumentViewTestCase
|
||||
from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH
|
||||
from mayan.apps.sources.models import WebFormSource
|
||||
from mayan.apps.sources.tests.literals import (
|
||||
TEST_SOURCE_LABEL, TEST_SOURCE_UNCOMPRESS_N
|
||||
@@ -34,7 +33,7 @@ class CabinetDocumentUploadTestCase(CabinetTestMixin, GenericDocumentViewTestCas
|
||||
def _request_upload_interactive_document_create_view(self):
|
||||
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
||||
return self.post(
|
||||
viewname='sources:upload_interactive', kwargs={
|
||||
viewname='sources:document_upload_interactive', kwargs={
|
||||
'source_id': self.test_source.pk
|
||||
}, data={
|
||||
'document_type_id': self.test_document_type.pk,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mayan.apps.common.tests import GenericViewTestCase
|
||||
from mayan.apps.common.tests.base import GenericViewTestCase
|
||||
from mayan.apps.document_states.tests.mixins import WorkflowTestMixin
|
||||
from mayan.apps.document_states.tests.test_workflow_actions import ActionTestCase
|
||||
|
||||
@@ -46,7 +46,7 @@ class CabinetWorkflowActionViewTestCase(
|
||||
self._create_test_workflow_state()
|
||||
|
||||
response = self.get(
|
||||
viewname='document_states:setup_workflow_state_action_create',
|
||||
viewname='document_states:workflow_template_state_action_create',
|
||||
kwargs={
|
||||
'pk': self.test_workflow_state.pk,
|
||||
'class_path': 'mayan.apps.cabinets.workflow_actions.CabinetAddAction'
|
||||
@@ -61,7 +61,7 @@ class CabinetWorkflowActionViewTestCase(
|
||||
self._create_test_cabinet()
|
||||
|
||||
response = self.get(
|
||||
viewname='document_states:setup_workflow_state_action_create',
|
||||
viewname='document_states:workflow_template_state_action_create',
|
||||
kwargs={
|
||||
'pk': self.test_workflow_state.pk,
|
||||
'class_path': 'mayan.apps.cabinets.workflow_actions.CabinetRemoveAction'
|
||||
|
||||
@@ -12,55 +12,62 @@ from .views import (
|
||||
CabinetDeleteView, CabinetDetailView, CabinetEditView, CabinetListView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
urlpatterns_cabinets = [
|
||||
url(
|
||||
regex=r'^list/$', view=CabinetListView.as_view(), name='cabinet_list'
|
||||
regex=r'^cabinets/$', view=CabinetListView.as_view(), name='cabinet_list'
|
||||
),
|
||||
url(
|
||||
regex=r'^(?P<pk>\d+)/child/add/$', view=CabinetChildAddView.as_view(),
|
||||
name='cabinet_child_add'
|
||||
),
|
||||
url(
|
||||
regex=r'^create/$', view=CabinetCreateView.as_view(),
|
||||
regex=r'^cabinets/create/$', view=CabinetCreateView.as_view(),
|
||||
name='cabinet_create'
|
||||
),
|
||||
url(
|
||||
regex=r'^(?P<pk>\d+)/edit/$', view=CabinetEditView.as_view(),
|
||||
name='cabinet_edit'
|
||||
regex=r'^cabinets/(?P<pk>\d+)/children/add/$', view=CabinetChildAddView.as_view(),
|
||||
name='cabinet_child_add'
|
||||
),
|
||||
url(
|
||||
regex=r'^(?P<pk>\d+)/delete/$', view=CabinetDeleteView.as_view(),
|
||||
regex=r'^cabinets/(?P<pk>\d+)/delete/$', view=CabinetDeleteView.as_view(),
|
||||
name='cabinet_delete'
|
||||
),
|
||||
url(
|
||||
regex=r'^(?P<pk>\d+)/$', view=CabinetDetailView.as_view(),
|
||||
name='cabinet_view'
|
||||
regex=r'^cabinets/(?P<pk>\d+)/edit/$', view=CabinetEditView.as_view(),
|
||||
name='cabinet_edit'
|
||||
),
|
||||
url(
|
||||
regex=r'^document/(?P<pk>\d+)/cabinet/add/$',
|
||||
regex=r'^cabinets/(?P<pk>\d+)/$', view=CabinetDetailView.as_view(),
|
||||
name='cabinet_view'
|
||||
),
|
||||
]
|
||||
|
||||
urlpatterns_documents_cabinets = [
|
||||
url(
|
||||
regex=r'^documents/(?P<pk>\d+)/cabinets/add/$',
|
||||
view=DocumentAddToCabinetView.as_view(), name='document_cabinet_add'
|
||||
),
|
||||
url(
|
||||
regex=r'^document/multiple/cabinet/add/$',
|
||||
regex=r'^documents/multiple/cabinets/add/$',
|
||||
view=DocumentAddToCabinetView.as_view(),
|
||||
name='document_multiple_cabinet_add'
|
||||
),
|
||||
url(
|
||||
regex=r'^document/(?P<pk>\d+)/cabinet/remove/$',
|
||||
regex=r'^documents/(?P<pk>\d+)/cabinets/remove/$',
|
||||
view=DocumentRemoveFromCabinetView.as_view(),
|
||||
name='document_cabinet_remove'
|
||||
),
|
||||
url(
|
||||
regex=r'^document/multiple/cabinet/remove/$',
|
||||
regex=r'^documents/multiple/cabinets/remove/$',
|
||||
view=DocumentRemoveFromCabinetView.as_view(),
|
||||
name='multiple_document_cabinet_remove'
|
||||
),
|
||||
url(
|
||||
regex=r'^document/(?P<pk>\d+)/cabinet/list/$',
|
||||
regex=r'^documents/(?P<pk>\d+)/cabinets/$',
|
||||
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]+)/$',
|
||||
|
||||
@@ -6,9 +6,12 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mayan.apps.acls.classes import ModelPermission
|
||||
from mayan.apps.common.apps import MayanAppConfig
|
||||
from mayan.apps.common.menus import menu_facet, menu_main, menu_secondary
|
||||
from mayan.apps.common.menus import (
|
||||
menu_facet, menu_main, menu_multi_item, menu_secondary
|
||||
)
|
||||
from mayan.apps.dashboards.dashboards import dashboard_main
|
||||
from mayan.apps.events.classes import ModelEventType
|
||||
from mayan.apps.navigation.classes import SourceColumn
|
||||
|
||||
from .dashboard_widgets import DashboardWidgetTotalCheckouts
|
||||
from .events import (
|
||||
@@ -17,8 +20,9 @@ from .events import (
|
||||
)
|
||||
from .handlers import handler_check_new_version_creation
|
||||
from .links import (
|
||||
link_check_in_document, link_check_out_document, link_check_out_info,
|
||||
link_check_out_list
|
||||
link_check_in_document, link_check_in_document_multiple,
|
||||
link_check_out_document, link_check_out_document_multiple,
|
||||
link_check_out_info, link_check_out_list
|
||||
)
|
||||
from .methods import (
|
||||
method_check_in, method_get_check_out_info, method_get_check_out_state,
|
||||
@@ -43,6 +47,8 @@ class CheckoutsApp(MayanAppConfig):
|
||||
def ready(self):
|
||||
super(CheckoutsApp, self).ready()
|
||||
|
||||
CheckedOutDocument = self.get_model(model_name='CheckedOutDocument')
|
||||
DocumentCheckout = self.get_model(model_name='DocumentCheckout')
|
||||
Document = apps.get_model(
|
||||
app_label='documents', model_name='Document'
|
||||
)
|
||||
@@ -76,6 +82,22 @@ class CheckoutsApp(MayanAppConfig):
|
||||
permission_document_check_out_detail_view
|
||||
)
|
||||
)
|
||||
ModelPermission.register_inheritance(
|
||||
model=DocumentCheckout, related='document'
|
||||
)
|
||||
|
||||
SourceColumn(
|
||||
attribute='get_user_display', include_label=True, order=99,
|
||||
source=CheckedOutDocument
|
||||
)
|
||||
SourceColumn(
|
||||
attribute='get_checkout_datetime', include_label=True, order=99,
|
||||
source=CheckedOutDocument
|
||||
)
|
||||
SourceColumn(
|
||||
attribute='get_checkout_expiration', include_label=True, order=99,
|
||||
source=CheckedOutDocument
|
||||
)
|
||||
|
||||
dashboard_main.add_widget(
|
||||
widget=DashboardWidgetTotalCheckouts, order=-1
|
||||
@@ -85,6 +107,22 @@ class CheckoutsApp(MayanAppConfig):
|
||||
links=(link_check_out_info,), sources=(Document,)
|
||||
)
|
||||
menu_main.bind_links(links=(link_check_out_list,), position=98)
|
||||
menu_multi_item.bind_links(
|
||||
links=(
|
||||
link_check_in_document_multiple,
|
||||
), sources=(CheckedOutDocument,)
|
||||
)
|
||||
menu_multi_item.bind_links(
|
||||
links=(
|
||||
link_check_in_document_multiple,
|
||||
link_check_out_document_multiple,
|
||||
), sources=(Document,)
|
||||
)
|
||||
menu_multi_item.unbind_links(
|
||||
links=(
|
||||
link_check_out_document_multiple,
|
||||
), sources=(CheckedOutDocument,)
|
||||
)
|
||||
menu_secondary.bind_links(
|
||||
links=(link_check_out_document, link_check_in_document),
|
||||
sources=(
|
||||
|
||||
@@ -10,7 +10,7 @@ from .models import DocumentCheckout
|
||||
from .widgets import SplitTimeDeltaWidget
|
||||
|
||||
|
||||
class DocumentCheckoutForm(forms.ModelForm):
|
||||
class DocumentCheckOutForm(forms.ModelForm):
|
||||
class Meta:
|
||||
fields = ('expiration_datetime', 'block_new_version')
|
||||
model = DocumentCheckout
|
||||
@@ -19,7 +19,7 @@ class DocumentCheckoutForm(forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class DocumentCheckoutDefailForm(DetailForm):
|
||||
class DocumentCheckOutDetailForm(DetailForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs['instance']
|
||||
|
||||
@@ -56,7 +56,7 @@ class DocumentCheckoutDefailForm(DetailForm):
|
||||
)
|
||||
|
||||
kwargs['extra_fields'] = extra_fields
|
||||
super(DocumentCheckoutDefailForm, self).__init__(*args, **kwargs)
|
||||
super(DocumentCheckOutDetailForm, self).__init__(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
fields = ()
|
||||
|
||||
@@ -38,16 +38,26 @@ link_check_out_document = Link(
|
||||
args='object.pk', condition=is_not_checked_out,
|
||||
icon_class=icon_check_out_document,
|
||||
permissions=(permission_document_check_out,),
|
||||
text=_('Check out document'), view='checkouts:check_out_document',
|
||||
text=_('Check out document'), view='checkouts:check_out_document'
|
||||
)
|
||||
link_check_out_document_multiple = Link(
|
||||
icon_class=icon_check_out_document,
|
||||
permissions=(permission_document_check_out,), text=_('Check out'),
|
||||
view='checkouts:check_out_document_multiple'
|
||||
)
|
||||
link_check_in_document = Link(
|
||||
args='object.pk', icon_class=icon_check_in_document,
|
||||
condition=is_checked_out, permissions=(
|
||||
permission_document_check_in, permission_document_check_in_override
|
||||
), text=_('Check in document'), view='checkouts:check_in_document',
|
||||
), text=_('Check in document'), view='checkouts:check_in_document'
|
||||
)
|
||||
link_check_in_document_multiple = Link(
|
||||
icon_class=icon_check_in_document,
|
||||
permissions=(permission_document_check_in,), text=_('Check in'),
|
||||
view='checkouts:check_in_document_multiple'
|
||||
)
|
||||
link_check_out_info = Link(
|
||||
args='resolved_object.pk', icon_class=icon_check_out_info, permissions=(
|
||||
permission_document_check_out_detail_view,
|
||||
), text=_('Check in/out'), view='checkouts:check_out_info',
|
||||
), text=_('Check in/out'), view='checkouts:check_out_info'
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.apps import apps
|
||||
from django.db import models, transaction
|
||||
from django.utils.timezone import now
|
||||
|
||||
from mayan.apps.acls.models import AccessControlList
|
||||
from mayan.apps.documents.models import Document
|
||||
|
||||
from .events import (
|
||||
@@ -14,10 +15,58 @@ from .events import (
|
||||
)
|
||||
from .exceptions import DocumentNotCheckedOut
|
||||
from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN
|
||||
from .permissions import (
|
||||
permission_document_check_in, permission_document_check_in_override
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DocumentCheckoutBusinessLogicManager(models.Manager):
|
||||
def check_in_document(self, document, user=None):
|
||||
# Convert any document submodel to the parent model class
|
||||
queryset = document._meta.default_manager.filter(pk=document.pk)
|
||||
|
||||
if not self.filter(document__pk__in=queryset).exists():
|
||||
raise DocumentNotCheckedOut
|
||||
|
||||
return self.check_in_documents(queryset=queryset, user=user)
|
||||
|
||||
def check_in_documents(self, queryset, user=None):
|
||||
if user:
|
||||
user_document_checkouts = AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_check_in,
|
||||
queryset=self.filter(user_id=user.pk, document__in=queryset),
|
||||
user=user
|
||||
)
|
||||
|
||||
others_document_checkouts = AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_check_in_override,
|
||||
queryset=self.exclude(user_id=user.pk, document__in=queryset),
|
||||
user=user
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
if user:
|
||||
for checkout in user_document_checkouts:
|
||||
event_document_check_in.commit(
|
||||
actor=user, target=checkout.document
|
||||
)
|
||||
checkout.delete()
|
||||
|
||||
for checkout in others_document_checkouts:
|
||||
event_document_forceful_check_in.commit(
|
||||
actor=user, target=checkout.document
|
||||
)
|
||||
checkout.delete()
|
||||
else:
|
||||
for checkout in self.filter(document__in=queryset):
|
||||
event_document_auto_check_in.commit(
|
||||
target=checkout.document
|
||||
)
|
||||
checkout.delete()
|
||||
|
||||
|
||||
class DocumentCheckoutManager(models.Manager):
|
||||
def are_document_new_versions_allowed(self, document, user=None):
|
||||
try:
|
||||
@@ -27,25 +76,6 @@ class DocumentCheckoutManager(models.Manager):
|
||||
else:
|
||||
return not check_out_info.block_new_version
|
||||
|
||||
def check_in_document(self, document, user=None):
|
||||
try:
|
||||
document_check_out = self.model.objects.get(document=document)
|
||||
except self.model.DoesNotExist:
|
||||
raise DocumentNotCheckedOut
|
||||
else:
|
||||
with transaction.atomic():
|
||||
if user:
|
||||
if self.get_check_out_info(document=document).user != user:
|
||||
event_document_forceful_check_in.commit(
|
||||
actor=user, target=document
|
||||
)
|
||||
else:
|
||||
event_document_check_in.commit(actor=user, target=document)
|
||||
else:
|
||||
event_document_auto_check_in.commit(target=document)
|
||||
|
||||
document_check_out.delete()
|
||||
|
||||
def check_in_expired_check_outs(self):
|
||||
for document in self.expired_check_outs():
|
||||
document.check_in()
|
||||
@@ -57,7 +87,11 @@ class DocumentCheckoutManager(models.Manager):
|
||||
)
|
||||
|
||||
def checked_out_documents(self):
|
||||
return Document.objects.filter(
|
||||
CheckedOutDocument = apps.get_model(
|
||||
app_label='checkouts', model_name='CheckedOutDocument'
|
||||
)
|
||||
|
||||
return CheckedOutDocument.objects.filter(
|
||||
pk__in=self.model.objects.values('document__id')
|
||||
)
|
||||
|
||||
@@ -74,7 +108,11 @@ class DocumentCheckoutManager(models.Manager):
|
||||
return STATE_CHECKED_IN
|
||||
|
||||
def expired_check_outs(self):
|
||||
expired_list = Document.objects.filter(
|
||||
CheckedOutDocument = apps.get_model(
|
||||
app_label='checkouts', model_name='CheckedOutDocument'
|
||||
)
|
||||
|
||||
expired_list = CheckedOutDocument.objects.filter(
|
||||
pk__in=self.model.objects.filter(
|
||||
expiration_datetime__lte=now()
|
||||
).values_list('document__pk', flat=True)
|
||||
@@ -83,9 +121,6 @@ class DocumentCheckoutManager(models.Manager):
|
||||
return expired_list
|
||||
|
||||
def get_by_natural_key(self, document_natural_key):
|
||||
Document = apps.get_model(
|
||||
app_label='documents', model_name='Document'
|
||||
)
|
||||
try:
|
||||
document = Document.objects.get_by_natural_key(document_natural_key)
|
||||
except Document.DoesNotExist:
|
||||
|
||||
@@ -8,7 +8,7 @@ def method_check_in(self, user=None):
|
||||
app_label='checkouts', model_name='DocumentCheckout'
|
||||
)
|
||||
|
||||
return DocumentCheckout.objects.check_in_document(
|
||||
return DocumentCheckout.business_logic.check_in_document(
|
||||
document=self, user=user
|
||||
)
|
||||
|
||||
|
||||
26
mayan/apps/checkouts/migrations/0008_checkedoutdocument.py
Normal file
26
mayan/apps/checkouts/migrations/0008_checkedoutdocument.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.20 on 2019-07-25 04:52
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('documents', '0050_auto_20190725_0451'),
|
||||
('checkouts', '0007_auto_20180310_1715'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CheckedOutDocument',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
},
|
||||
bases=('documents.document',),
|
||||
),
|
||||
]
|
||||
@@ -14,7 +14,10 @@ from mayan.apps.documents.models import Document
|
||||
|
||||
from .events import event_document_check_out
|
||||
from .exceptions import DocumentAlreadyCheckedOut
|
||||
from .managers import DocumentCheckoutManager, NewVersionBlockManager
|
||||
from .managers import (
|
||||
DocumentCheckoutBusinessLogicManager, DocumentCheckoutManager,
|
||||
NewVersionBlockManager
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -49,6 +52,7 @@ class DocumentCheckout(models.Model):
|
||||
)
|
||||
|
||||
objects = DocumentCheckoutManager()
|
||||
business_logic = DocumentCheckoutBusinessLogicManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
@@ -81,13 +85,13 @@ class DocumentCheckout(models.Model):
|
||||
natural_key.dependencies = ['documents.Document']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
new_checkout = not self.pk
|
||||
if not new_checkout or self.document.is_checked_out():
|
||||
is_new = not self.pk
|
||||
if not is_new or self.document.is_checked_out():
|
||||
raise DocumentAlreadyCheckedOut
|
||||
|
||||
with transaction.atomic():
|
||||
result = super(DocumentCheckout, self).save(*args, **kwargs)
|
||||
if new_checkout:
|
||||
if is_new:
|
||||
event_document_check_out.commit(
|
||||
actor=self.user, target=self.document
|
||||
)
|
||||
@@ -119,3 +123,24 @@ class NewVersionBlock(models.Model):
|
||||
def natural_key(self):
|
||||
return self.document.natural_key()
|
||||
natural_key.dependencies = ['documents.Document']
|
||||
|
||||
|
||||
class CheckedOutDocument(Document):
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
def get_user_display(self):
|
||||
check_out_info = self.get_check_out_info()
|
||||
return check_out_info.user.get_full_name() or check_out_info.user
|
||||
|
||||
get_user_display.short_description = _('User')
|
||||
|
||||
def get_checkout_datetime(self):
|
||||
return self.get_check_out_info().checkout_datetime
|
||||
|
||||
get_checkout_datetime.short_description = _('Checkout time and date')
|
||||
|
||||
def get_checkout_expiration(self):
|
||||
return self.get_check_out_info().expiration_datetime
|
||||
|
||||
get_checkout_expiration.short_description = _('Checkout expiration')
|
||||
|
||||
@@ -5,6 +5,7 @@ import datetime
|
||||
from django.utils.timezone import now
|
||||
|
||||
from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS
|
||||
from mayan.apps.common.tests.utils import as_id_list
|
||||
|
||||
from ..models import DocumentCheckout
|
||||
|
||||
@@ -64,16 +65,13 @@ class DocumentCheckoutViewTestMixin(object):
|
||||
}
|
||||
)
|
||||
|
||||
def _request_test_document_check_out_detail_view(self):
|
||||
return self.get(
|
||||
viewname='checkouts:check_out_info', kwargs={
|
||||
'pk': self.test_document.pk
|
||||
def _request_test_document_multiple_check_in_post_view(self):
|
||||
return self.post(
|
||||
viewname='checkouts:check_in_document_multiple', data={
|
||||
'id_list': as_id_list(items=self.test_documents)
|
||||
}
|
||||
)
|
||||
|
||||
def _request_test_document_check_out_list_view(self):
|
||||
return self.get(viewname='checkouts:check_out_list')
|
||||
|
||||
def _request_test_document_check_out_view(self):
|
||||
return self.post(
|
||||
viewname='checkouts:check_out_document', kwargs={
|
||||
@@ -84,3 +82,23 @@ class DocumentCheckoutViewTestMixin(object):
|
||||
'block_new_version': True
|
||||
}
|
||||
)
|
||||
|
||||
def _request_test_document_multiple_check_out_post_view(self):
|
||||
return self.post(
|
||||
viewname='checkouts:check_out_document_multiple', data={
|
||||
'block_new_version': True,
|
||||
'expiration_datetime_unit': TIME_DELTA_UNIT_DAYS,
|
||||
'expiration_datetime_amount': 99,
|
||||
'id_list': as_id_list(items=self.test_documents)
|
||||
}
|
||||
)
|
||||
|
||||
def _request_test_document_check_out_detail_view(self):
|
||||
return self.get(
|
||||
viewname='checkouts:check_out_info', kwargs={
|
||||
'pk': self.test_document.pk
|
||||
}
|
||||
)
|
||||
|
||||
def _request_test_document_check_out_list_view(self):
|
||||
return self.get(viewname='checkouts:check_out_list')
|
||||
|
||||
@@ -4,9 +4,9 @@ from django.utils.encoding import force_text
|
||||
|
||||
from rest_framework import status
|
||||
|
||||
from mayan.apps.documents.tests import DocumentTestMixin
|
||||
from mayan.apps.documents.permissions import permission_document_view
|
||||
from mayan.apps.rest_api.tests import BaseAPITestCase
|
||||
from mayan.apps.documents.tests.mixins import DocumentTestMixin
|
||||
from mayan.apps.rest_api.tests.base import BaseAPITestCase
|
||||
|
||||
from ..models import DocumentCheckout
|
||||
from ..permissions import (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mayan.apps.documents.tests import GenericDocumentViewTestCase
|
||||
from mayan.apps.documents.tests.base import GenericDocumentViewTestCase
|
||||
|
||||
from ..links import link_check_out_document, link_check_out_info
|
||||
from ..permissions import (
|
||||
|
||||
@@ -2,11 +2,10 @@ from __future__ import unicode_literals
|
||||
|
||||
import time
|
||||
|
||||
from mayan.apps.common.tests import BaseTestCase
|
||||
from mayan.apps.documents.tests import (
|
||||
GenericDocumentTestCase, DocumentTestMixin
|
||||
)
|
||||
from mayan.apps.common.tests.base import BaseTestCase
|
||||
from mayan.apps.documents.tests.base import GenericDocumentTestCase
|
||||
from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH
|
||||
from mayan.apps.documents.tests.mixins import DocumentTestMixin
|
||||
|
||||
from ..exceptions import (
|
||||
DocumentAlreadyCheckedOut, DocumentNotCheckedOut,
|
||||
@@ -53,7 +52,7 @@ class DocumentCheckoutTestCase(
|
||||
block_new_version=True
|
||||
)
|
||||
|
||||
def test_document_checkin_without_checkout(self):
|
||||
def test_document_check_in_without_check_out(self):
|
||||
with self.assertRaises(DocumentNotCheckedOut):
|
||||
self.test_document.check_in()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mayan.apps.documents.permissions import permission_document_view
|
||||
from mayan.apps.documents.tests import GenericDocumentViewTestCase
|
||||
from mayan.apps.documents.tests.base import GenericDocumentViewTestCase
|
||||
from mayan.apps.sources.links import link_document_version_upload
|
||||
|
||||
from ..literals import STATE_CHECKED_OUT, STATE_LABELS
|
||||
@@ -22,8 +22,8 @@ class DocumentCheckoutViewTestCase(
|
||||
self._check_out_test_document()
|
||||
|
||||
response = self._request_test_document_check_in_get_view()
|
||||
self.assertContains(
|
||||
response=response, text=self.test_document.label, status_code=200
|
||||
self.assertNotContains(
|
||||
response=response, text=self.test_document.label, status_code=404
|
||||
)
|
||||
|
||||
self.assertTrue(self.test_document.is_checked_out())
|
||||
@@ -46,7 +46,7 @@ class DocumentCheckoutViewTestCase(
|
||||
self._check_out_test_document()
|
||||
|
||||
response = self._request_test_document_check_in_post_view()
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
self.assertTrue(self.test_document.is_checked_out())
|
||||
|
||||
@@ -67,9 +67,89 @@ class DocumentCheckoutViewTestCase(
|
||||
)
|
||||
)
|
||||
|
||||
def test_document_multiple_check_in_post_view_no_permission(self):
|
||||
# Upload second document
|
||||
self.upload_document()
|
||||
|
||||
self._check_out_test_document(document=self.test_documents[0])
|
||||
self._check_out_test_document(document=self.test_documents[1])
|
||||
|
||||
response = self._request_test_document_multiple_check_in_post_view()
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
self.assertTrue(self.test_documents[0].is_checked_out())
|
||||
self.assertTrue(self.test_documents[1].is_checked_out())
|
||||
self.assertTrue(
|
||||
DocumentCheckout.objects.is_checked_out(
|
||||
document=self.test_documents[0]
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
DocumentCheckout.objects.is_checked_out(
|
||||
document=self.test_documents[1]
|
||||
)
|
||||
)
|
||||
|
||||
def test_document_multiple_check_in_post_view_with_document_0_access(self):
|
||||
# Upload second document
|
||||
self.upload_document()
|
||||
|
||||
self._check_out_test_document(document=self.test_documents[0])
|
||||
self._check_out_test_document(document=self.test_documents[1])
|
||||
|
||||
self.grant_access(
|
||||
obj=self.test_documents[0], permission=permission_document_check_in
|
||||
)
|
||||
|
||||
response = self._request_test_document_multiple_check_in_post_view()
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
self.assertFalse(self.test_documents[0].is_checked_out())
|
||||
self.assertTrue(self.test_documents[1].is_checked_out())
|
||||
self.assertFalse(
|
||||
DocumentCheckout.objects.is_checked_out(
|
||||
document=self.test_documents[0]
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
DocumentCheckout.objects.is_checked_out(
|
||||
document=self.test_documents[1]
|
||||
)
|
||||
)
|
||||
|
||||
def test_document_multiple_check_in_post_view_with_access(self):
|
||||
# Upload second document
|
||||
self.upload_document()
|
||||
|
||||
self._check_out_test_document(document=self.test_documents[0])
|
||||
self._check_out_test_document(document=self.test_documents[1])
|
||||
|
||||
self.grant_access(
|
||||
obj=self.test_documents[0], permission=permission_document_check_in
|
||||
)
|
||||
self.grant_access(
|
||||
obj=self.test_documents[1], permission=permission_document_check_in
|
||||
)
|
||||
|
||||
response = self._request_test_document_multiple_check_in_post_view()
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
self.assertFalse(self.test_documents[0].is_checked_out())
|
||||
self.assertFalse(self.test_documents[1].is_checked_out())
|
||||
self.assertFalse(
|
||||
DocumentCheckout.objects.is_checked_out(
|
||||
document=self.test_documents[0]
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
DocumentCheckout.objects.is_checked_out(
|
||||
document=self.test_documents[1]
|
||||
)
|
||||
)
|
||||
|
||||
def test_document_check_out_view_no_permission(self):
|
||||
response = self._request_test_document_check_out_view()
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
self.assertFalse(self.test_document.is_checked_out())
|
||||
|
||||
@@ -87,6 +167,102 @@ class DocumentCheckoutViewTestCase(
|
||||
|
||||
self.assertTrue(self.test_document.is_checked_out())
|
||||
|
||||
def test_document_multiple_check_out_post_view_no_permission(self):
|
||||
# Upload second document
|
||||
self.upload_document()
|
||||
|
||||
self.grant_access(
|
||||
obj=self.test_documents[0],
|
||||
permission=permission_document_check_out_detail_view
|
||||
)
|
||||
self.grant_access(
|
||||
obj=self.test_documents[1],
|
||||
permission=permission_document_check_out_detail_view
|
||||
)
|
||||
|
||||
response = self._request_test_document_multiple_check_out_post_view()
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
self.assertFalse(self.test_documents[0].is_checked_out())
|
||||
self.assertFalse(self.test_documents[1].is_checked_out())
|
||||
self.assertFalse(
|
||||
DocumentCheckout.objects.is_checked_out(
|
||||
document=self.test_documents[0]
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
DocumentCheckout.objects.is_checked_out(
|
||||
document=self.test_documents[1]
|
||||
)
|
||||
)
|
||||
|
||||
def test_document_multiple_check_out_post_view_with_document_access(self):
|
||||
# Upload second document
|
||||
self.upload_document()
|
||||
|
||||
self.grant_access(
|
||||
obj=self.test_documents[0], permission=permission_document_check_out
|
||||
)
|
||||
self.grant_access(
|
||||
obj=self.test_documents[0],
|
||||
permission=permission_document_check_out_detail_view
|
||||
)
|
||||
self.grant_access(
|
||||
obj=self.test_documents[1],
|
||||
permission=permission_document_check_out_detail_view
|
||||
)
|
||||
|
||||
response = self._request_test_document_multiple_check_out_post_view()
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
self.assertTrue(self.test_documents[0].is_checked_out())
|
||||
self.assertFalse(self.test_documents[1].is_checked_out())
|
||||
self.assertTrue(
|
||||
DocumentCheckout.objects.is_checked_out(
|
||||
document=self.test_documents[0]
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
DocumentCheckout.objects.is_checked_out(
|
||||
document=self.test_documents[1]
|
||||
)
|
||||
)
|
||||
|
||||
def test_document_multiple_check_out_post_view_with_access(self):
|
||||
# Upload second document
|
||||
self.upload_document()
|
||||
|
||||
self.grant_access(
|
||||
obj=self.test_documents[0], permission=permission_document_check_out
|
||||
)
|
||||
self.grant_access(
|
||||
obj=self.test_documents[1], permission=permission_document_check_out
|
||||
)
|
||||
self.grant_access(
|
||||
obj=self.test_documents[0],
|
||||
permission=permission_document_check_out_detail_view
|
||||
)
|
||||
self.grant_access(
|
||||
obj=self.test_documents[1],
|
||||
permission=permission_document_check_out_detail_view
|
||||
)
|
||||
|
||||
response = self._request_test_document_multiple_check_out_post_view()
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
self.assertTrue(self.test_documents[0].is_checked_out())
|
||||
self.assertTrue(self.test_documents[1].is_checked_out())
|
||||
self.assertTrue(
|
||||
DocumentCheckout.objects.is_checked_out(
|
||||
document=self.test_documents[0]
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
DocumentCheckout.objects.is_checked_out(
|
||||
document=self.test_documents[1]
|
||||
)
|
||||
)
|
||||
|
||||
def test_document_check_out_detail_view_no_permission(self):
|
||||
self._check_out_test_document()
|
||||
|
||||
@@ -156,19 +332,18 @@ class DocumentCheckoutViewTestCase(
|
||||
'pk': self.test_document.pk
|
||||
}
|
||||
)
|
||||
self.assertContains(
|
||||
response=response, text='Insufficient permissions', status_code=403
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
self.assertTrue(self.test_document.is_checked_out())
|
||||
|
||||
def test_document_check_in_forcefull_view_with_permission(self):
|
||||
def test_document_check_in_forcefull_view_with_access(self):
|
||||
self._create_test_user()
|
||||
# Check out document as test_user
|
||||
self._check_out_test_document(user=self.test_user)
|
||||
|
||||
self.grant_access(
|
||||
obj=self.test_document, permission=permission_document_check_in_override
|
||||
obj=self.test_document,
|
||||
permission=permission_document_check_in_override
|
||||
)
|
||||
|
||||
# Check in document as test_case_user
|
||||
@@ -201,7 +376,7 @@ class NewVersionBlockViewTestCase(
|
||||
self.login_superuser()
|
||||
|
||||
response = self.post(
|
||||
viewname='sources:upload_version', kwargs={
|
||||
viewname='sources:document_version_upload', kwargs={
|
||||
'document_pk': self.test_document.pk
|
||||
}, follow=True
|
||||
)
|
||||
@@ -219,6 +394,8 @@ class NewVersionBlockViewTestCase(
|
||||
|
||||
# Needed by the url view resolver
|
||||
response.context.current_app = None
|
||||
resolved_link = link_document_version_upload.resolve(context=response.context)
|
||||
resolved_link = link_document_version_upload.resolve(
|
||||
context=response.context
|
||||
)
|
||||
|
||||
self.assertEqual(resolved_link, None)
|
||||
|
||||
@@ -4,8 +4,8 @@ from django.conf.urls import url
|
||||
|
||||
from .api_views import APICheckedoutDocumentListView, APICheckedoutDocumentView
|
||||
from .views import (
|
||||
DocumentCheckOutView, DocumentCheckOutDetailView, DocumentCheckOutListView,
|
||||
DocumentCheckInView
|
||||
DocumentCheckInView, DocumentCheckOutDetailView, DocumentCheckOutListView,
|
||||
DocumentCheckOutView
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -14,16 +14,25 @@ urlpatterns = [
|
||||
name='check_out_list'
|
||||
),
|
||||
url(
|
||||
regex=r'^documents/(?P<pk>\d+)/check/out/$', view=DocumentCheckOutView.as_view(),
|
||||
name='check_out_document'
|
||||
regex=r'^documents/(?P<pk>\d+)/check/in/$',
|
||||
view=DocumentCheckInView.as_view(), name='check_in_document'
|
||||
),
|
||||
url(
|
||||
regex=r'^documents/(?P<pk>\d+)/check/in/$', view=DocumentCheckInView.as_view(),
|
||||
name='check_in_document'
|
||||
regex=r'^documents/multiple/check/in/$',
|
||||
name='check_in_document_multiple', view=DocumentCheckInView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^documents/(?P<pk>\d+)/check/info/$', view=DocumentCheckOutDetailView.as_view(),
|
||||
name='check_out_info'
|
||||
regex=r'^documents/(?P<pk>\d+)/check/out/$',
|
||||
view=DocumentCheckOutView.as_view(), name='check_out_document'
|
||||
),
|
||||
url(
|
||||
regex=r'^documents/multiple/check/out/$',
|
||||
name='check_out_document_multiple',
|
||||
view=DocumentCheckOutView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^documents/(?P<pk>\d+)/checkout/info/$',
|
||||
view=DocumentCheckOutDetailView.as_view(), name='check_out_info'
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext_lazy as _, ungettext
|
||||
|
||||
from mayan.apps.acls.models import AccessControlList
|
||||
from mayan.apps.common.generics import (
|
||||
ConfirmView, SingleObjectCreateView, SingleObjectDetailView
|
||||
MultipleObjectConfirmActionView, MultipleObjectFormActionView,
|
||||
SingleObjectDetailView
|
||||
)
|
||||
from mayan.apps.common.utils import encapsulate
|
||||
from mayan.apps.documents.models import Document
|
||||
from mayan.apps.documents.views import DocumentListView
|
||||
|
||||
from .exceptions import DocumentAlreadyCheckedOut, DocumentNotCheckedOut
|
||||
from .forms import DocumentCheckoutForm, DocumentCheckoutDefailForm
|
||||
from .forms import DocumentCheckOutForm, DocumentCheckOutDetailForm
|
||||
from .icons import icon_check_out_info
|
||||
from .models import DocumentCheckout
|
||||
from .permissions import (
|
||||
@@ -24,66 +20,125 @@ from .permissions import (
|
||||
)
|
||||
|
||||
|
||||
class DocumentCheckInView(ConfirmView):
|
||||
def get_extra_context(self):
|
||||
document = self.get_object()
|
||||
class DocumentCheckInView(MultipleObjectConfirmActionView):
|
||||
error_message = 'Unable to check in document "%(instance)s". %(exception)s'
|
||||
model = Document
|
||||
pk_url_kwarg = 'pk'
|
||||
success_message_singular = '%(count)d document checked in.'
|
||||
success_message_plural = '%(count)d documents checked in.'
|
||||
|
||||
context = {
|
||||
'object': document,
|
||||
def get_extra_context(self):
|
||||
queryset = self.get_object_list()
|
||||
|
||||
result = {
|
||||
'title': ungettext(
|
||||
singular='Check in %(count)d document',
|
||||
plural='Check in %(count)d documents',
|
||||
number=queryset.count()
|
||||
) % {
|
||||
'count': queryset.count(),
|
||||
}
|
||||
}
|
||||
|
||||
if document.get_check_out_info().user != self.request.user:
|
||||
context['title'] = _(
|
||||
'You didn\'t originally checked out this document. '
|
||||
'Forcefully check in the document: %s?'
|
||||
) % document
|
||||
if queryset.count() == 1:
|
||||
result.update(
|
||||
{
|
||||
'object': queryset.first(),
|
||||
'title': _(
|
||||
'Check in document: %s'
|
||||
) % queryset.first()
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def get_post_object_action_url(self):
|
||||
if self.action_count == 1:
|
||||
return reverse(
|
||||
viewname='checkouts:document_checkout_info',
|
||||
kwargs={'pk': self.action_id_list[0]}
|
||||
)
|
||||
else:
|
||||
context['title'] = _('Check in the document: %s?') % document
|
||||
super(DocumentCheckInView, self).get_post_action_redirect()
|
||||
|
||||
return context
|
||||
def get_source_queryset(self):
|
||||
# object_permission is None to disable restricting queryset mixin
|
||||
# and restrict the queryset ourselves from two permissions
|
||||
|
||||
def get_object(self):
|
||||
return get_object_or_404(klass=Document, pk=self.kwargs['pk'])
|
||||
source_queryset = super(DocumentCheckInView, self).get_source_queryset()
|
||||
|
||||
def get_post_action_redirect(self):
|
||||
return reverse(
|
||||
viewname='checkouts:check_out_info', kwargs={
|
||||
'pk': self.get_object().pk
|
||||
}
|
||||
check_in_queryset = AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_check_in, queryset=source_queryset,
|
||||
user=self.request.user
|
||||
)
|
||||
|
||||
def view_action(self):
|
||||
document = self.get_object()
|
||||
check_in_override_queryset = AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_check_in_override,
|
||||
queryset=source_queryset, user=self.request.user
|
||||
)
|
||||
|
||||
if document.get_check_out_info().user == self.request.user:
|
||||
AccessControlList.objects.check_access(
|
||||
obj=document, permissions=(permission_document_check_in,),
|
||||
user=self.request.user
|
||||
)
|
||||
else:
|
||||
AccessControlList.objects.check_access(
|
||||
obj=document,
|
||||
permissions=(permission_document_check_in_override,),
|
||||
user=self.request.user
|
||||
return check_in_queryset | check_in_override_queryset
|
||||
|
||||
def object_action(self, form, instance):
|
||||
DocumentCheckout.business_logic.check_in_document(
|
||||
document=instance, user=self.request.user
|
||||
)
|
||||
|
||||
|
||||
class DocumentCheckOutView(MultipleObjectFormActionView):
|
||||
error_message = 'Unable to checkout document "%(instance)s". %(exception)s'
|
||||
form_class = DocumentCheckOutForm
|
||||
model = Document
|
||||
object_permission = permission_document_check_out
|
||||
pk_url_kwarg = 'pk'
|
||||
success_message_singular = '%(count)d document checked out.'
|
||||
success_message_plural = '%(count)d documents checked out.'
|
||||
|
||||
def get_extra_context(self):
|
||||
queryset = self.get_object_list()
|
||||
|
||||
result = {
|
||||
'title': ungettext(
|
||||
singular='Checkout %(count)d document',
|
||||
plural='Checkout %(count)d documents',
|
||||
number=queryset.count()
|
||||
) % {
|
||||
'count': queryset.count(),
|
||||
}
|
||||
}
|
||||
|
||||
if queryset.count() == 1:
|
||||
result.update(
|
||||
{
|
||||
'object': queryset.first(),
|
||||
'title': _(
|
||||
'Check out document: %s'
|
||||
) % queryset.first()
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
document.check_in(user=self.request.user)
|
||||
except DocumentNotCheckedOut:
|
||||
messages.error(
|
||||
message=_('Document has not been checked out.'),
|
||||
request=self.request
|
||||
return result
|
||||
|
||||
def get_post_object_action_url(self):
|
||||
if self.action_count == 1:
|
||||
return reverse(
|
||||
viewname='checkouts:document_checkout_info',
|
||||
kwargs={'pk': self.action_id_list[0]}
|
||||
)
|
||||
else:
|
||||
messages.success(
|
||||
message=_(
|
||||
'Document "%s" checked in successfully.'
|
||||
) % document, request=self.request
|
||||
)
|
||||
super(DocumentCheckOutView, self).get_post_action_redirect()
|
||||
|
||||
def object_action(self, form, instance):
|
||||
DocumentCheckout.objects.check_out_document(
|
||||
block_new_version=form.cleaned_data['block_new_version'],
|
||||
document=instance,
|
||||
expiration_datetime=form.cleaned_data['expiration_datetime'],
|
||||
user=self.request.user,
|
||||
)
|
||||
|
||||
|
||||
class DocumentCheckOutDetailView(SingleObjectDetailView):
|
||||
form_class = DocumentCheckoutDefailForm
|
||||
form_class = DocumentCheckOutDetailForm
|
||||
model = Document
|
||||
object_permission = permission_document_check_out_detail_view
|
||||
|
||||
@@ -96,55 +151,6 @@ class DocumentCheckOutDetailView(SingleObjectDetailView):
|
||||
}
|
||||
|
||||
|
||||
class DocumentCheckOutView(SingleObjectCreateView):
|
||||
form_class = DocumentCheckoutForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.document = get_object_or_404(klass=Document, pk=self.kwargs['pk'])
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
obj=self.document, permissions=(permission_document_check_out,),
|
||||
user=request.user
|
||||
)
|
||||
|
||||
return super(
|
||||
DocumentCheckOutView, self
|
||||
).dispatch(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
try:
|
||||
instance = form.save(commit=False)
|
||||
instance.user = self.request.user
|
||||
instance.document = self.document
|
||||
instance.save()
|
||||
except DocumentAlreadyCheckedOut:
|
||||
messages.error(
|
||||
message=_('Document already checked out.'),
|
||||
request=self.request
|
||||
)
|
||||
else:
|
||||
messages.success(
|
||||
message=_(
|
||||
'Document "%s" checked out successfully.'
|
||||
) % self.document, request=self.request
|
||||
)
|
||||
|
||||
return HttpResponseRedirect(redirect_to=self.get_success_url())
|
||||
|
||||
def get_extra_context(self):
|
||||
return {
|
||||
'object': self.document,
|
||||
'title': _('Check out document: %s') % self.document
|
||||
}
|
||||
|
||||
def get_post_action_redirect(self):
|
||||
return reverse(
|
||||
viewname='checkouts:check_out_info', kwargs={
|
||||
'pk': self.document.pk
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class DocumentCheckOutListView(DocumentListView):
|
||||
def get_document_queryset(self):
|
||||
return AccessControlList.objects.restrict_queryset(
|
||||
@@ -157,34 +163,13 @@ class DocumentCheckOutListView(DocumentListView):
|
||||
context = super(DocumentCheckOutListView, self).get_extra_context()
|
||||
context.update(
|
||||
{
|
||||
'extra_columns': (
|
||||
{
|
||||
'name': _('User'),
|
||||
'attribute': encapsulate(
|
||||
lambda document: document.get_check_out_info().user.get_full_name() or document.get_check_out_info().user
|
||||
)
|
||||
},
|
||||
{
|
||||
'name': _('Checkout time and date'),
|
||||
'attribute': encapsulate(
|
||||
lambda document: document.get_check_out_info().checkout_datetime
|
||||
)
|
||||
},
|
||||
{
|
||||
'name': _('Checkout expiration'),
|
||||
'attribute': encapsulate(
|
||||
lambda document: document.get_check_out_info().expiration_datetime
|
||||
)
|
||||
},
|
||||
),
|
||||
'no_results_icon': icon_check_out_info,
|
||||
'no_results_text': _(
|
||||
'Checking out a document blocks certain document '
|
||||
'operations for a predetermined amount of '
|
||||
'time.'
|
||||
'Checking out a document, blocks certain operations '
|
||||
'for a predetermined amount of time.'
|
||||
),
|
||||
'no_results_title': _('No documents have been checked out'),
|
||||
'title': _('Documents checked out'),
|
||||
'title': _('Checked out documents'),
|
||||
}
|
||||
)
|
||||
return context
|
||||
|
||||
@@ -27,9 +27,7 @@ from .links import (
|
||||
)
|
||||
|
||||
from .literals import MESSAGE_SQLITE_WARNING
|
||||
from .menus import (
|
||||
menu_about, menu_main, menu_secondary, menu_user
|
||||
)
|
||||
from .menus import menu_about, menu_secondary, menu_topbar, menu_user
|
||||
from .settings import (
|
||||
setting_auto_logging, setting_production_error_log_path,
|
||||
setting_production_error_logging
|
||||
@@ -97,7 +95,10 @@ class CommonApp(MayanAppConfig):
|
||||
)
|
||||
|
||||
Template(
|
||||
name='menu_main', template_name='appearance/main_menu.html'
|
||||
name='menu_main', template_name='appearance/menu_main.html'
|
||||
)
|
||||
Template(
|
||||
name='menu_topbar', template_name='appearance/menu_topbar.html'
|
||||
)
|
||||
|
||||
menu_user.bind_links(
|
||||
@@ -112,7 +113,7 @@ class CommonApp(MayanAppConfig):
|
||||
)
|
||||
)
|
||||
|
||||
menu_main.bind_links(links=(menu_about, menu_user,), position=99)
|
||||
menu_topbar.bind_links(links=(menu_about, menu_user,), position=99)
|
||||
menu_secondary.bind_links(
|
||||
links=(link_object_error_list_clear,), sources=(
|
||||
'common:object_error_list',
|
||||
|
||||
@@ -61,102 +61,9 @@ 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'
|
||||
)
|
||||
@@ -383,6 +290,10 @@ 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'
|
||||
|
||||
@@ -53,8 +53,8 @@ class MultiFormView(DjangoFormView):
|
||||
template_name = 'appearance/generic_form.html'
|
||||
|
||||
def _create_form(self, form_name, klass):
|
||||
form_kwargs = self.get_form_kwargs(form_name)
|
||||
form_create_method = 'create_%s_form' % form_name
|
||||
form_kwargs = self.get_form_kwargs(form_name=form_name)
|
||||
form_create_method = 'create_{}_form'.format(form_name)
|
||||
if hasattr(self, form_create_method):
|
||||
form = getattr(self, form_create_method)(**form_kwargs)
|
||||
else:
|
||||
@@ -66,17 +66,17 @@ class MultiFormView(DjangoFormView):
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
form_classes = self.get_form_classes()
|
||||
self.forms = self.get_forms(form_classes)
|
||||
self.forms = self.get_forms(form_classes=form_classes)
|
||||
return super(MultiFormView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def forms_valid(self, forms):
|
||||
for form_name, form in forms.items():
|
||||
form_valid_method = '%s_form_valid' % form_name
|
||||
form_valid_method = '{}_form_valid'.format(form_name)
|
||||
|
||||
if hasattr(self, form_valid_method):
|
||||
return getattr(self, form_valid_method)(form)
|
||||
return getattr(self, form_valid_method)(form=form)
|
||||
|
||||
self.all_forms_valid(forms)
|
||||
self.all_forms_valid(forms=forms)
|
||||
|
||||
return HttpResponseRedirect(redirect_to=self.get_success_url())
|
||||
|
||||
@@ -98,14 +98,16 @@ class MultiFormView(DjangoFormView):
|
||||
|
||||
def get_form_kwargs(self, form_name):
|
||||
kwargs = {}
|
||||
kwargs.update({'initial': self.get_initial(form_name)})
|
||||
kwargs.update({'prefix': self.get_prefix(form_name)})
|
||||
kwargs.update({'initial': self.get_initial(form_name=form_name)})
|
||||
kwargs.update({'prefix': self.get_prefix(form_name=form_name)})
|
||||
|
||||
if self.request.method in ('POST', 'PUT'):
|
||||
kwargs.update({
|
||||
'data': self.request.POST,
|
||||
'files': self.request.FILES,
|
||||
})
|
||||
kwargs.update(
|
||||
{
|
||||
'data': self.request.POST,
|
||||
'files': self.request.FILES,
|
||||
}
|
||||
)
|
||||
|
||||
kwargs.update(self.get_form_extra_kwargs(form_name=form_name) or {})
|
||||
|
||||
@@ -118,13 +120,13 @@ class MultiFormView(DjangoFormView):
|
||||
return dict(
|
||||
[
|
||||
(
|
||||
key, self._create_form(key, klass)
|
||||
key, self._create_form(form_name=key, klass=klass)
|
||||
) for key, klass in form_classes.items()
|
||||
]
|
||||
)
|
||||
|
||||
def get_initial(self, form_name):
|
||||
initial_method = 'get_%s_initial' % form_name
|
||||
initial_method = 'get_{}_initial'.format(form_name)
|
||||
if hasattr(self, initial_method):
|
||||
return getattr(self, initial_method)()
|
||||
else:
|
||||
@@ -206,9 +208,9 @@ class AddRemoveView(
|
||||
getattr(self.main_object, self.related_field).add(*queryset)
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
'View %s must be called with a main_object_method_add, a '
|
||||
'View {} must be called with a main_object_method_add, a '
|
||||
'related_field, or an action_add '
|
||||
'method.' % self.__class__.__name__
|
||||
'method.'.format(self.__class__.__name__)
|
||||
)
|
||||
|
||||
def _action_remove(self, queryset):
|
||||
@@ -225,9 +227,9 @@ class AddRemoveView(
|
||||
getattr(self.main_object, self.related_field).remove(*queryset)
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
'View %s must be called with a main_object_method_remove, a '
|
||||
'View {} must be called with a main_object_method_remove, a '
|
||||
'related_field, or an action_remove '
|
||||
'method.' % self.__class__.__name__
|
||||
'method.'.format(self.__class__.__name__)
|
||||
)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
@@ -348,8 +350,10 @@ class AddRemoveView(
|
||||
def get_list_added_queryset(self):
|
||||
if not self.related_field:
|
||||
raise ImproperlyConfigured(
|
||||
'View %s must be called with either a related_field or '
|
||||
'override .get_list_added_queryset().' % self.__class__.__name__
|
||||
'View {} must be called with either a related_field or '
|
||||
'override .get_list_added_queryset().'.format(
|
||||
self.__class__.__name__
|
||||
)
|
||||
)
|
||||
|
||||
return self.get_secondary_object_list().filter(
|
||||
|
||||
@@ -2,6 +2,7 @@ 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):
|
||||
@@ -20,9 +21,7 @@ class URL(object):
|
||||
|
||||
def to_string(self):
|
||||
if self._args.keys():
|
||||
query = force_bytes(
|
||||
'?{}'.format(self._args.urlencode())
|
||||
)
|
||||
query = '?{}'.format(self._args.urlencode())
|
||||
else:
|
||||
query = ''
|
||||
|
||||
@@ -31,6 +30,9 @@ class URL(object):
|
||||
else:
|
||||
path = ''
|
||||
|
||||
result = force_bytes('{}{}'.format(path, query))
|
||||
result = '{}{}'.format(path, query)
|
||||
|
||||
return result
|
||||
if PY3:
|
||||
return result
|
||||
else:
|
||||
return force_bytes(result)
|
||||
|
||||
@@ -35,8 +35,14 @@ icon_menu_about = Icon(
|
||||
icon_menu_user = Icon(
|
||||
driver_name='fontawesome', symbol='user-circle'
|
||||
)
|
||||
icon_object_error_list_with_icon = Icon(
|
||||
driver_name='fontawesome', symbol='lock'
|
||||
icon_object_errors = Icon(
|
||||
driver_name='fontawesome', symbol='exclamation-triangle'
|
||||
)
|
||||
icon_object_error_list = Icon(
|
||||
driver_name='fontawesome', symbol='exclamation-triangle'
|
||||
)
|
||||
icon_object_error_list_clear = Icon(
|
||||
driver_name='fontawesome', symbol='times'
|
||||
)
|
||||
icon_ok = Icon(
|
||||
driver_name='fontawesome', symbol='check'
|
||||
|
||||
@@ -8,8 +8,8 @@ from mayan.apps.navigation.classes import Link
|
||||
from .icons import (
|
||||
icon_about, icon_current_user_locale_profile_details,
|
||||
icon_current_user_locale_profile_edit, icon_documentation,
|
||||
icon_forum, icon_license, icon_object_error_list_with_icon,
|
||||
icon_setup, icon_source_code, icon_support, icon_tools
|
||||
icon_forum, icon_license, icon_setup, icon_source_code, icon_support,
|
||||
icon_tools
|
||||
)
|
||||
from .permissions_runtime import permission_error_log_view
|
||||
|
||||
@@ -50,21 +50,17 @@ link_documentation = Link(
|
||||
text=_('Documentation'), url='https://docs.mayan-edms.com'
|
||||
)
|
||||
link_object_error_list = Link(
|
||||
icon_class_path='mayan.apps.common.icons.icon_object_error_list',
|
||||
kwargs=get_kwargs_factory('resolved_object'),
|
||||
permissions=(permission_error_log_view,), text=_('Errors'),
|
||||
view='common:object_error_list',
|
||||
)
|
||||
link_object_error_list_clear = Link(
|
||||
icon_class_path='mayan.apps.common.icons.icon_object_error_list_clear',
|
||||
kwargs=get_kwargs_factory('resolved_object'),
|
||||
permissions=(permission_error_log_view,), text=_('Clear all'),
|
||||
view='common:object_error_list_clear',
|
||||
)
|
||||
link_object_error_list_with_icon = Link(
|
||||
kwargs=get_kwargs_factory('resolved_object'),
|
||||
icon_class=icon_object_error_list_with_icon,
|
||||
permissions=(permission_error_log_view,), text=_('Errors'),
|
||||
view='common:error_list',
|
||||
)
|
||||
link_forum = Link(
|
||||
icon_class=icon_forum, tags='new_window', text=_('Forum'),
|
||||
url='https://forum.mayan-edms.com'
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import errno
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from pathlib2 import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import management
|
||||
from django.core.management.base import CommandError
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mayan.apps.documents.models import DocumentType
|
||||
from mayan.apps.storage.utils import fs_cleanup
|
||||
|
||||
from ...literals import MESSAGE_DEPRECATION_WARNING
|
||||
from ...warnings import DeprecationWarning
|
||||
|
||||
CONVERTDB_FOLDER = 'convertdb'
|
||||
CONVERTDB_OUTPUT_FILENAME = 'migrate.json'
|
||||
|
||||
|
||||
class Command(management.BaseCommand):
|
||||
help = 'Convert from a database backend to another one.'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
category=DeprecationWarning,
|
||||
message=force_text(MESSAGE_DEPRECATION_WARNING)
|
||||
)
|
||||
|
||||
super(Command, self).__init__(*args, **kwargs)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'args', metavar='app_label[.ModelName]', nargs='*',
|
||||
help=_(
|
||||
'Restricts dumped data to the specified app_label or '
|
||||
'app_label.ModelName.'
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
'--from', action='store', default='default', dest='from',
|
||||
help=_(
|
||||
'The database from which data will be exported. If omitted '
|
||||
'the database named "default" will be used.'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--to', action='store', default='default', dest='to',
|
||||
help=_(
|
||||
'The database to which data will be imported. If omitted '
|
||||
'the database named "default" will be used.'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--force', action='store_true', dest='force',
|
||||
help=_(
|
||||
'Force the conversion of the database even if the receiving '
|
||||
'database is not empty.'
|
||||
),
|
||||
)
|
||||
|
||||
def handle(self, *app_labels, **options):
|
||||
# Create the media/convertdb folder
|
||||
convertdb_folder_path = force_text(
|
||||
Path(
|
||||
settings.MEDIA_ROOT, CONVERTDB_FOLDER
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
os.makedirs(convertdb_folder_path)
|
||||
except OSError as exception:
|
||||
if exception.errno == errno.EEXIST:
|
||||
pass
|
||||
|
||||
convertdb_file_path = force_text(
|
||||
Path(
|
||||
convertdb_folder_path, CONVERTDB_OUTPUT_FILENAME
|
||||
)
|
||||
)
|
||||
|
||||
management.call_command(command_name='purgeperiodictasks')
|
||||
|
||||
management.call_command(
|
||||
'dumpdata', *app_labels, all=True,
|
||||
database=options['from'], natural_primary=True,
|
||||
natural_foreign=True, output=convertdb_file_path,
|
||||
interactive=False, format='json'
|
||||
)
|
||||
|
||||
if DocumentType.objects.using(options['to']).count() and not options['force']:
|
||||
fs_cleanup(convertdb_file_path)
|
||||
raise CommandError(
|
||||
'There is existing data in the database that will be '
|
||||
'used for the import. If you proceed with the conversion '
|
||||
'you might lose data. Please check your settings.'
|
||||
)
|
||||
|
||||
management.call_command(
|
||||
'loaddata', convertdb_file_path, database=options['to'], interactive=False,
|
||||
verbosity=3
|
||||
)
|
||||
fs_cleanup(convertdb_file_path)
|
||||
@@ -28,8 +28,8 @@ class Command(management.BaseCommand):
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--no-javascript', action='store_true', dest='no_javascript',
|
||||
help='Don\'t download the JavaScript dependencies.',
|
||||
'--no-dependencies', action='store_true', dest='no_dependencies',
|
||||
help='Don\'t download 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_javascript', False):
|
||||
if not options.get('no_dependencies', False):
|
||||
management.call_command(
|
||||
command_name='installjavascript', interactive=False
|
||||
command_name='installdependencies', interactive=False
|
||||
)
|
||||
|
||||
management.call_command(
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
SETTING_FILE_TEMPLATE = '''
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from .base import *
|
||||
|
||||
SECRET_KEY = '{0}'
|
||||
|
||||
'''
|
||||
|
||||
@@ -11,8 +11,8 @@ class Command(management.BaseCommand):
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--no-javascript', action='store_true', dest='no_javascript',
|
||||
help='Don\'t download the JavaScript dependencies.',
|
||||
'--no-dependencies', action='store_true', dest='no_dependencies',
|
||||
help='Don\'t download dependencies.',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
@@ -25,9 +25,9 @@ class Command(management.BaseCommand):
|
||||
)
|
||||
)
|
||||
|
||||
if not options.get('no_javascript', False):
|
||||
if not options.get('no_dependencies', False):
|
||||
management.call_command(
|
||||
command_name='installjavascript', interactive=False
|
||||
command_name='installdependencies', interactive=False
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.core import management
|
||||
|
||||
from djcelery.models import IntervalSchedule, PeriodicTask
|
||||
from django_celery_beat.models import IntervalSchedule, PeriodicTask
|
||||
|
||||
|
||||
class Command(management.BaseCommand):
|
||||
|
||||
@@ -17,6 +17,7 @@ menu_object = Menu(label=_('Actions'), name='object')
|
||||
menu_secondary = Menu(label=_('Secondary'), name='secondary')
|
||||
menu_setup = Menu(name='setup')
|
||||
menu_tools = Menu(name='tools')
|
||||
menu_topbar = Menu(name='topbar')
|
||||
menu_user = Menu(
|
||||
icon_class=icon_menu_user, name='user', label=_('User')
|
||||
)
|
||||
|
||||
32
mayan/apps/common/migrations/0012_auto_20190711_0548.py
Normal file
32
mayan/apps/common/migrations/0012_auto_20190711_0548.py
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user