Compare commits

..

76 Commits

Author SHA1 Message Date
Roberto Rosario
1bcc9332b2 Merge remote-tracking branch 'origin/versions/micro' into client_bc_merge_micro
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-07 10:03:03 -04:00
Roberto Rosario
e3267d3973 Merge branch 'versions/minor' into bc_merge
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-05 01:06:25 -04:00
Roberto Rosario
eb7cbc73ee Merge branch 'features/weblinks' into clients/bc 2019-07-01 15:44:03 -04:00
Roberto Rosario
596b5ccf67 MVP of the weblinks app
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-01 15:43:15 -04:00
Roberto Rosario
34838a438d Merge branch 'features/workflow_context' into clients/bc
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-01 10:01:15 -04:00
Roberto Rosario
572690e2bc Finish workflow context implementation
Improve workflow instance detail view.
Add workflow transition field widget support.
Fix workflow transition field required support.
Update tests.

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

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

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-30 09:51:22 -04:00
Roberto Rosario
42db8255d1 Merge branch 'versions/minor' into features/workflow_context
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-29 20:35:25 -04:00
Roberto Rosario
22ba6cfb49 Improve email metadata support
The feature can now work on emails with nested parts.
Also the metadata.yaml attachment no longer needs to be the
first attachment.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-29 02:13:51 -04:00
Roberto Rosario
02bba73ca7 Reduce code used to set bulk metadata
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-29 02:13:45 -04:00
Roberto Rosario
d0daf559c7 Remove the INSTALLED_APPS setting
The INSTALLED APPS setting is now replaced by the
new COMMON_EXTRA_APPS and COMMON_DISABLED_APPS.

Exposing the INSTALLED_APPS setting had the side effect
of blocking new apps that were added in new versions.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 23:52:46 -04:00
Roberto Rosario
f8f6700459 Add redirection after trashing a document
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 23:52:31 -04:00
Roberto Rosario
c318d37445 Fix IMAP4 store flags argument, GitLab issue #606
Python's documentation is incorrect, argument name is flag_list.
Closes GitLab issue #606. Thanks to Samuel Aebi (@samuelaebi)
for the report and debug information.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 23:50:53 -04:00
Roberto Rosario
246fc15988 Merge branch 'features/workflow_context' into clients/bc 2019-06-28 15:50:02 -04:00
Roberto Rosario
14d45cbe90 Use polylines for the edge splines
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 15:48:44 -04:00
Roberto Rosario
42a544c6e3 Merge branch 'features/workflow_context' into clients/bc 2019-06-28 15:37:21 -04:00
Roberto Rosario
75be11bc96 Hightlight initial state
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 15:35:33 -04:00
Roberto Rosario
ebf29d0eed Add actions to workflow preview
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 15:35:27 -04:00
Roberto Rosario
a391d27b44 Add transition form comment help text
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 14:33:37 -04:00
Roberto Rosario
753c9b8b4b Merge branch 'versions/minor' into features/workflow_context 2019-06-28 14:08:58 -04:00
Roberto Rosario
9cb6c6599d Create system user after migration
Move the code to trigger on the post_migrate signal.
Avoid "database not ready" errors during tests and initialsetup.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 13:59:00 -04:00
Roberto Rosario
8bd0d0166b Merge branch 'versions/minor' into clients/bc 2019-06-28 13:24:27 -04:00
Roberto Rosario
bee0c0b189 Add authentication events
Add event to track failed logins, password reset starts, and password
reset completions.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 17:04:44 -04:00
Roberto Rosario
744bfefa5c Add workflow email action template support
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 12:10:31 -04:00
Roberto Rosario
850fb16c8c Add automatic execution test
Add test for automatic email action execution on document upload.

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

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 08:39:48 -04:00
Roberto Rosario
fdef757fd0 Add redactions app JavaScript dependencies
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 08:22:53 -04:00
Roberto Rosario
3608ee1141 Remove included cropper.js files
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 08:17:50 -04:00
Roberto Rosario
7fb3d61dff [Fix] Change to relative imports
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 07:03:55 -04:00
Roberto Rosario
e9aa11673b Initial commit of the workflow mail action
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 07:03:31 -04:00
Roberto Rosario
03a7aa5daf Add missing migrations
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 15:04:30 -04:00
Roberto Rosario
755f20c5c4 Fix importer logging
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 14:20:00 -04:00
Roberto Rosario
64772e2e90 Update changes file
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 14:18:29 -04:00
Roberto Rosario
75a4a426e0 Remove duplicated trashed document preview
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 14:18:11 -04:00
Roberto Rosario
42a7ebeea2 Finish redactions app
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 14:16:11 -04:00
Roberto Rosario
3d22f48555 Add draw box by percentage
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 00:13:20 -04:00
Roberto Rosario
488e048d8f Remove old remarks and add redirect
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 00:12:41 -04:00
Roberto Rosario
2f82559a5c Add verbose name for the Redaction model
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 00:12:08 -04:00
Roberto Rosario
7d5b7b9fc4 Fix static media folder
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 00:11:52 -04:00
Roberto Rosario
7aa68b8bbf Initial commit of the redactions app
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 12:13:49 -04:00
Roberto Rosario
aecde926f2 Fix varaible typo
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 12:08:25 -04:00
Roberto Rosario
6b95628e56 Add rectangle drawing transformation
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 10:23:30 -04:00
Roberto Rosario
56a1b97b46 Update changes file
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 09:17:01 -04:00
Roberto Rosario
34a5a54c8b Add sortable index instance label column
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 09:15:52 -04:00
Roberto Rosario
0c17ab3f8a Improve source column exclusion
Improve for model subclasses in partial querysets.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 09:08:02 -04:00
Roberto Rosario
c967a25f82 Support exclusions from source columns
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 00:16:29 -04:00
Roberto Rosario
7562588c42 Fix typo
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 23:55:02 -04:00
Roberto Rosario
a1a706b7b9 Add link to sort individual indexes
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 23:50:01 -04:00
Roberto Rosario
d623cb2df5 Sort function
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 23:04:49 -04:00
Roberto Rosario
488ddcf1e1 Rename CHANGES file
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 23:04:07 -04:00
Roberto Rosario
3d39893f17 Add columns to show document count per workflow
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 23:03:06 -04:00
Roberto Rosario
3694839d97 Use Select2 for the document type selection form
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 19:29:12 -04:00
Roberto Rosario
cce27aceca Allow client builds
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 16:42:59 -04:00
Roberto Rosario
c73d251370 Generate metadata by name not label
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 16:32:14 -04:00
Roberto Rosario
091f0d1cfd Generate new metadata when label is ambiguous
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 16:20:56 -04:00
Roberto Rosario
d2affdcf21 Merge branch 'feature/document_importer' into nightly
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-21 17:39:19 -04:00
Roberto Rosario
885d430b98 Merge branch 'versions/minor' into nightly 2019-06-21 17:38:08 -04:00
Roberto Rosario
39eabe1c54 Associate metadata to all types
Previously metadata types were associated to documents types
if the metadata type was newly created.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-21 17:37:00 -04:00
Roberto Rosario
f6ad579829 Merge branch 'versions/minor' into nightly
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-21 12:05:19 -04:00
Roberto Rosario
6fc9e46882 Merge branch 'versions/minor' into feature/document_importer 2019-06-21 11:53:09 -04:00
Roberto Rosario
2d326a679d Merge branch 'master' into feature/document_importer 2019-06-21 11:53:03 -04:00
Roberto Rosario
aa8c2db446 Merge branch 'master' into feature/document_importer
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-21 00:06:49 -04:00
Roberto Rosario
925b55d76d Support ignoring certain rows
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-20 10:12:53 -04:00
Roberto Rosario
5808d3653d Add support for ignoring import errors
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-20 10:05:24 -04:00
Roberto Rosario
bc072f7b7e Add column mapping support
Add support for specifying metadata columns.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-19 17:47:32 -04:00
Roberto Rosario
b3d59eee39 Add MVP of the importer app
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-19 16:02:00 -04:00
Roberto Rosario
7d379a52af Add a reusable task to upload documents
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-19 16:00:59 -04:00
Roberto Rosario
499ab1f3e7 Allow disabling the random primary key test mixin
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-19 15:59:15 -04:00
541 changed files with 8894 additions and 16003 deletions

View File

@@ -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,11 +58,12 @@ 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
- staging
- /^clients\/.+$/
job_documentation_build:
stage: build_documentation
@@ -152,9 +153,7 @@ 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-virtualenv python3-dev tesseract-ocr tesseract-ocr-deu
- virtualenv venv -p /usr/bin/python3
- . venv/bin/activate
- 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
- pip install -r requirements.txt -r requirements/testing-base.txt
only:
- releases/all
@@ -162,6 +161,7 @@ job_push_python:
- releases/python
- staging
- nightly
- /^clients\/.+$/
test-mysql:
<<: *test_base
@@ -172,7 +172,6 @@ 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
@@ -188,7 +187,6 @@ 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:
@@ -197,7 +195,6 @@ 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:

View File

@@ -115,12 +115,6 @@ source_lang = en
source_file = mayan/apps/events/locale/en/LC_MESSAGES/django.po
type = PO
[mayan-edms.file_caching-3-0]
file_filter = mayan/apps/file_caching/locale/<lang>/LC_MESSAGES/django.po
source_lang = en
source_file = mayan/apps/file_caching/locale/en/LC_MESSAGES/django.po
type = PO
[mayan-edms.file_metadata-3-0]
file_filter = mayan/apps/file_metadata/locale/<lang>/LC_MESSAGES/django.po
source_lang = en
@@ -228,10 +222,3 @@ file_filter = mayan/apps/user_management/locale/<lang>/LC_MESSAGES/django.po
source_lang = en
source_file = mayan/apps/user_management/locale/en/LC_MESSAGES/django.po
type = PO
[mayan-edms.weblink-3-0]
file_filter = mayan/apps/weblinks/locale/<lang>/LC_MESSAGES/django.po
source_lang = en
source_file = mayan/apps/weblinks/locale/en/LC_MESSAGES/django.po
type = PO

20
CHANGES_BC.rst Normal file
View File

@@ -0,0 +1,20 @@
- Use Select2 widget for the document type selection form.
- Update source column matching to be additive and not exclusive.
- Add two columns to show the number of documents per workflow and
workflow state.
- Sort module.
- Add link to sort individual indexes.
- Support exclusions from source columns.
- Improve source column exclusion. Improve for model subclasses in partial querysets.
- Add sortable index instance label column.
- Add rectangle drawing transformation.
- Redactions app.
- Remove duplicated trashed document preview.
- Add label to trashed date and time document source column.
- Tag created event fix.
3.2.3 (2019-06-21)
* Add a reusable task to upload documents.
* Add MVP of the importer app.
3.2.4-3.2.8 (2019-10-07)

View File

@@ -1,89 +1,3 @@
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.
- Remove the DOCUMENTS_DISABLE_BASE_IMAGE_CACHE,
DOCUMENTS_DISABLE_TRANSFORMED_IMAGE_CACHE, and
DOCUMENTS_FIX_ORIENTATION settings.
3.2.8 (2019-10-01)
==================
- Fix error when accessing some API entry points without

View File

@@ -1,15 +1,5 @@
.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
@@ -28,7 +18,7 @@ clean-pyc: ## Remove Python artifacts.
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -R -f {} +
# Testing
@@ -43,10 +33,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 $(DOCKER_POSTGRES_IMAGE)
docker run -d --name test-postgres -p 5432:5432 -v test-postgres:/var/lib/postgresql/data healthcheck/postgres
sudo apt-get install -q libpq-dev
pip install psycopg2==$(PYTHON_PSYCOPG2_VERSION)
while ! nc -z 127.0.0.1 5432; do sleep 1; done
pip install psycopg2
while ! docker inspect --format='{{json .State.Health}}' test-postgres|grep 'Status":"healthy"'; 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
@@ -63,10 +53,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 $(DOCKER_MYSQL_IMAGE)
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
sudo apt-get install -q libmysqlclient-dev mysql-client
pip install mysqlclient==$(PYTHON_MYSQL_VERSION)
while ! nc -z 127.0.0.1 3306; do sleep 1; done
pip install mysqlclient
while ! docker inspect --format='{{json .State.Health}}' test-mysql|grep 'Status":"healthy"'; 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.
@@ -85,7 +75,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 $(DOCKER_ORACLE_IMAGE)
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
# https://gist.github.com/kimus/10012910
pip install cx_Oracle
while ! nc -z 127.0.0.1 49161; do sleep 1; done
@@ -244,21 +234,20 @@ generate-requirements: ## Generate all requirements files from the project deped
# Dev server
runserver: ## Run the development server.
./manage.py runserver --nothreading --settings=mayan.settings.development $(ADDRPORT)
./manage.py runserver --settings=mayan.settings.development $(ADDRPORT)
runserver_plus: ## Run the Django extension's development server.
./manage.py runserver_plus --nothreading --settings=mayan.settings.development $(ADDRPORT)
./manage.py runserver_plus --settings=mayan.settings.development $(ADDRPORT)
shell_plus: ## Run the shell_plus command.
./manage.py shell_plus --settings=mayan.settings.development
test-with-docker-services-on: ## Launch and initialize production-like services using Docker (Postgres and Redis).
docker run -d --name redis -p 6379:6379 $(DOCKER_REDIS_IMAGE)
docker run -d --name postgres -p 5432:5432 $(DOCKER_POSTGRES_IMAGE)
docker run -d --name redis -p 6379:6379 redis
docker run -d --name postgres -p 5432:5432 postgres
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.
@@ -269,10 +258,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.
DJANGO_SETTINGS_MODULE=mayan.settings.staging.docker ./manage.py celery worker -A mayan -B -l INFO -O fair
./manage.py celery worker --settings=mayan.settings.staging.docker -B -l INFO -O fair
docker-mysql-on: ## Launch and initialize a MySQL Docker container.
docker run -d --name mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=True -e MYSQL_DATABASE=mayan_edms $(DOCKER_MYSQL_IMAGE)
docker run -d --name mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=True -e MYSQL_DATABASE=mayan_edms mysql
while ! nc -z 127.0.0.1 3306; do sleep 1; done
docker-mysql-off: ## Stop and delete the MySQL Docker container.
@@ -280,7 +269,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 $(DOCKER_POSTGRES_IMAGE)
docker run -d --name postgres -p 5432:5432 postgres
while ! nc -z 127.0.0.1 5432; do sleep 1; done
docker-postgres-off: ## Stop and delete the PostgreSQL Docker container.

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env bash
INSTALLATION_DIRECTORY=/home/vagrant/mayan-edms/
DB_NAME=mayan_edms
DB_PASSWORD=test123
cat << EOF | sudo tee -a /etc/motd.tail
**********************************sudo apt
Mayan EDMS Vagrant Development Box
**********************************
EOF
# Update sources
echo -e "\n -> Running apt-get update & upgrade \n"
sudo apt-get -qq update
sudo apt-get -y upgrade
echo -e "\n -> Installing core binaries \n"
sudo apt-get -y install git-core python-virtualenv gcc python-dev libjpeg-dev libpng-dev libtiff-dev tesseract-ocr poppler-utils libreoffice
echo -e "\n -> Cloning development branch of repository \n"
git clone /mayan-edms-repository/ $INSTALLATION_DIRECTORY
cd $INSTALLATION_DIRECTORY
git checkout development
git reset HEAD --hard
echo -e "\n -> Setting up virtual env \n"
virtualenv venv
source venv/bin/activate
echo -e "\n -> Installing python dependencies \n"
pip install -r requirements.txt
echo -e "\n -> Running Mayan EDMS initial setup \n"
./manage.py initialsetup
echo -e "\n -> Installing Redis server \n"
sudo apt-get install -y redis-server
pip install redis
echo -e "\n -> Installing testing software \n"
pip install coverage
echo -e "\n -> Installing MySQL \n"
sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password password '$DB_PASSWORD
sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password '$DB_PASSWORD
sudo apt-get install -y mysql-server libmysqlclient-dev
# Create a passwordless root and travis users
mysql -u root -p$DB_PASSWORD -e "SET PASSWORD = PASSWORD('');"
mysql -u root -e "CREATE USER 'travis'@'localhost' IDENTIFIED BY '';GRANT ALL PRIVILEGES ON * . * TO 'travis'@'localhost';FLUSH PRIVILEGES;"
mysql -u travis -e "CREATE DATABASE $DB_NAME;"
pip install mysql-python
echo -e "\n -> Installing PostgreSQL \n"
sudo apt-get install -y postgresql postgresql-server-dev-all
sudo -u postgres psql -c 'create database mayan_edms;' -U postgres
sudo cat > /etc/postgresql/9.3/main/pg_hba.conf << EOF
local all postgres trust
# TYPE DATABASE USER ADDRESS METHOD
# "local" is for Unix domain socket connections only
local all all peer
# IPv4 local connections:
host all all 127.0.0.1/32 md5
# IPv6 local connections:
host all all ::1/128 md5
EOF
pip install -q psycopg2

File diff suppressed because it is too large Load Diff

View File

@@ -5,25 +5,21 @@ set -e
# $ curl -fsSL get.mayan-edms.com -o get-mayan-edms.sh
# $ sh get-mayan-edms.sh
#
# NOTE: Before executing, make sure to verify the contents of the script
# NOTE: 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-alpine}
: ${DOCKER_POSTGRES_IMAGE:=postgres:9.6}
: ${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}
@@ -48,8 +44,6 @@ 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"
@@ -57,17 +51,10 @@ 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
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."
echo "\nStarting in 10 seconds."
sleep 10
fi
@@ -85,62 +72,33 @@ if [ -z `which docker` ]; then
fi
echo -n "* Removing existing Mayan EDMS and PostgreSQL containers (no data will be lost)..."
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
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
echo "Done"
if [ "$DELETE_VOLUMES" = true ]; then
echo -n "* Deleting Docker volumes in 5 seconds (warning: this will delete all document data). Press CTRL+C to cancel..."
echo -n "* Deleting Docker volumes in 5 seconds (warning: this delete all document data)..."
sleep 5
rm DOCKER_MAYAN_VOLUME -Rf || true
rm DOCKER_POSTGRES_VOLUME -Rf || true
true || rm DOCKER_MAYAN_VOLUME -Rf
true || rm DOCKER_POSTGRES_VOLUME -Rf
echo "Done"
fi
echo -n "* Pulling (downloading) the Redis Docker image..."
docker pull $DOCKER_REDIS_IMAGE > /dev/null
echo -n "* Pulling (downloading) the Mayan EDMS Docker image..."
docker pull $DOCKER_MAYAN_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 \
$POSTGRES_PORT_ARGUMENT \
-p $DOCKER_POSTGRES_PORT:5432 \
-e POSTGRES_USER=$DATABASE_USER \
-e POSTGRES_DB=$DATABASE_NAME \
-e POSTGRES_PASSWORD=$DATABASE_PASSWORD \
@@ -148,24 +106,6 @@ $POSTGRES_PORT_ARGUMENT \
$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"
@@ -173,18 +113,15 @@ 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 \
$MAYAN_DATABASE_HOST_ARGUMENT \
$MAYAN_DATABASE_PORT_ARGUMENT \
-e MAYAN_DATABASE_HOST=172.17.0.1 \
-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"

View File

@@ -0,0 +1,171 @@
#!/usr/bin/env bash
# ====== CONFIG ======
INSTALLATION_DIRECTORY=/usr/share/mayan-edms/
DB_NAME=mayan_edms
DB_USERNAME=mayan
DB_PASSWORD=test123
# ==== END CONFIG ====
cat << EOF | tee -a /etc/motd.tail
**********************************
Mayan EDMS Vagrant Production Box
**********************************
EOF
echo -e "\n -> Running apt-get update & upgrade \n"
apt-get -qq update
apt-get -y upgrade
echo -e "\n -> Installing core binaries \n"
apt-get install nginx supervisor redis-server postgresql libpq-dev libjpeg-dev libmagic1 libpng-dev libreoffice libtiff-dev gcc ghostscript gpgv python-dev python-virtualenv tesseract-ocr poppler-utils -y
echo -e "\n -> Setting up virtualenv \n"
rm -f ${INSTALLATION_DIRECTORY}
virtualenv ${INSTALLATION_DIRECTORY}
source ${INSTALLATION_DIRECTORY}bin/activate
echo -e "\n -> Installing Mayan EDMS from PyPI \n"
pip install mayan-edms
echo -e "\n -> Installing Python client for PostgreSQL, Redis, and uWSGI \n"
pip install psycopg2 redis uwsgi
echo -e "\n -> Creating the database for the installation \n"
echo "CREATE USER mayan WITH PASSWORD '$DB_PASSWORD';" | sudo -u postgres psql
sudo -u postgres createdb -O $DB_USERNAME $DB_NAME
echo -e "\n -> Creating the directories for the logs \n"
mkdir /var/log/mayan
echo -e "\n -> Making a convenience symlink \n"
cd ${INSTALLATION_DIRECTORY}
ln -s lib/python2.7/site-packages/mayan .
echo -e "\n -> Creating an initial settings file \n"
mayan-edms.py createsettings
echo -e "\n -> Updating the mayan/settings/local.py file \n"
cat >> mayan/settings/local.py << EOF
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': '$DB_NAME',
'USER': '$DB_USERNAME',
'PASSWORD': '$DB_PASSWORD',
'HOST': 'localhost',
'PORT': '5432',
}
}
BROKER_URL = 'redis://127.0.0.1:6379/0'
CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0'
EOF
echo -e "\n -> Migrating the database or initialize the project \n"
mayan-edms.py initialsetup
echo -e "\n -> Disabling the default NGINX site \n"
rm -f /etc/nginx/sites-enabled/default
echo -e "\n -> Creating a uwsgi.ini file \n"
cat > uwsgi.ini << EOF
[uwsgi]
chdir = ${INSTALLATION_DIRECTORY}lib/python2.7/site-packages/mayan
chmod-socket = 664
chown-socket = www-data:www-data
env = DJANGO_SETTINGS_MODULE=mayan.settings.production
gid = www-data
logto = /var/log/uwsgi/%n.log
pythonpath = ${INSTALLATION_DIRECTORY}lib/python2.7/site-packages
master = True
max-requests = 5000
socket = ${INSTALLATION_DIRECTORY}uwsgi.sock
uid = www-data
vacuum = True
wsgi-file = ${INSTALLATION_DIRECTORY}lib/python2.7/site-packages/mayan/wsgi.py
EOF
echo -e "\n -> Creating the directory for the uWSGI log files \n"
mkdir -p /var/log/uwsgi
echo -e "\n -> Creating the NGINX site file for Mayan EDMS, /etc/nginx/sites-available/mayan \n"
cat > /etc/nginx/sites-available/mayan << EOF
server {
listen 80;
server_name localhost;
location / {
include uwsgi_params;
uwsgi_pass unix:${INSTALLATION_DIRECTORY}uwsgi.sock;
client_max_body_size 30M; # Increse if your plan to upload bigger documents
proxy_read_timeout 30s; # Increase if your document uploads take more than 30 seconds
}
location /static {
alias ${INSTALLATION_DIRECTORY}mayan/media/static;
expires 1h;
}
location /favicon.ico {
alias ${INSTALLATION_DIRECTORY}mayan/media/static/appearance/images/favicon.ico;
expires 1h;
}
}
EOF
echo -e "\n -> Enabling the NGINX site for Mayan EDMS \n"
ln -s /etc/nginx/sites-available/mayan /etc/nginx/sites-enabled/
echo -e "\n -> Creating the supervisor file for the uWSGI process, /etc/supervisor/conf.d/mayan-uwsgi.conf \n"
cat > /etc/supervisor/conf.d/mayan-uwsgi.conf << EOF
[program:mayan-uwsgi]
command = ${INSTALLATION_DIRECTORY}bin/uwsgi --ini ${INSTALLATION_DIRECTORY}uwsgi.ini
user = root
autostart = true
autorestart = true
redirect_stderr = true
EOF
echo -e "\n -> Creating the supervisor file for the Celery worker, /etc/supervisor/conf.d/mayan-celery.conf \n"
cat > /etc/supervisor/conf.d/mayan-celery.conf << EOF
[program:mayan-worker]
command = ${INSTALLATION_DIRECTORY}bin/python ${INSTALLATION_DIRECTORY}bin/mayan-edms.py celery --settings=mayan.settings.production worker -Ofair -l ERROR
directory = ${INSTALLATION_DIRECTORY}
user = www-data
stdout_logfile = /var/log/mayan/worker-stdout.log
stderr_logfile = /var/log/mayan/worker-stderr.log
autostart = true
autorestart = true
startsecs = 10
stopwaitsecs = 10
killasgroup = true
priority = 998
[program:mayan-beat]
command = ${INSTALLATION_DIRECTORY}bin/python ${INSTALLATION_DIRECTORY}bin/mayan-edms.py celery --settings=mayan.settings.production beat -l ERROR
directory = ${INSTALLATION_DIRECTORY}
user = www-data
numprocs = 1
stdout_logfile = /var/log/mayan/beat-stdout.log
stderr_logfile = /var/log/mayan/beat-stderr.log
autostart = true
autorestart = true
startsecs = 10
stopwaitsecs = 1
killasgroup = true
priority = 998
EOF
echo -e "\n -> Collecting the static files \n"
mayan-edms.py preparestatic --noinput
echo -e "\n -> Making the installation directory readable and writable by the webserver user \n"
chown www-data:www-data ${INSTALLATION_DIRECTORY} -R
echo -e "\n -> Restarting the services \n"
/etc/init.d/nginx restart
/etc/init.d/supervisor restart

View File

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

View File

@@ -0,0 +1,35 @@
#!/bin/bash
NAME="mayan-edms"
DJANGODIR=/usr/share/mayan-edms
SOCKFILE=/var/tmp/filesystem.sock
USER=www-data
GROUP=www-data
NUM_WORKERS=3
DJANGO_SETTINGS_MODULE=mayan.settings.production
DJANGO_WSGI_MODULE=mayan.wsgi
TIMEOUT=600
echo "Starting $NAME as `whoami`"
# Activate the virtual environment
cd $DJANGODIR
source bin/activate
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
# Create the run directory if it doesn't exist
RUNDIR=$(dirname $SOCKFILE)
test -d $RUNDIR || mkdir -p $RUNDIR
# Start your Django Unicorn
# Programs meant to be run under supervisor should not daemonize themselves (do not use --daemon)
exec bin/gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \
--workers $NUM_WORKERS \
--user=$USER --group=$GROUP \
--log-level=debug \
--bind=unix:$SOCKFILE \
--timeout=$TIMEOUT

View File

@@ -4,7 +4,7 @@
# BASE_IMAGE - Bare bones image with the base packages needed to run Mayan EDMS
####
FROM debian:10.0-slim as BASE_IMAGE
FROM debian:9.8-slim as BASE_IMAGE
LABEL maintainer="Roberto Rosario roberto.rosario@mayan-edms.com"
@@ -22,7 +22,6 @@ RUN set -x \
&& DEBIAN_FRONTEND=noninteractive \
apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
exiftool \
fonts-arphic-uming \
fonts-arphic-ukai \
@@ -32,11 +31,11 @@ apt-get update \
graphviz \
libfuse2 \
libmagic1 \
libmariadb3 \
libmariadbclient18 \
libreoffice \
libpq5 \
poppler-utils \
python3-distutils \
redis-server \
sane-utils \
sudo \
supervisor \
@@ -55,20 +54,21 @@ apt-get update \
&& if [ "$(uname -m)" = "armv7l" ]; then \
ln -s /usr/lib/arm-linux-gnueabihf/libz.so /usr/lib/ \
&& ln -s /usr/lib/arm-linux-gnueabihf/libjpeg.so /usr/lib/ \
; fi
; fi \
# Discard data when Redis runs out of memory
&& echo "maxmemory-policy allkeys-lru" >> /etc/redis/redis.conf \
# Disable saving the Redis database
echo "save \"\"" >> /etc/redis/redis.conf \
# Only provision 1 database
&& echo "databases 1" >> /etc/redis/redis.conf
####
# BUILDER_IMAGE - This image builds the Python package and is discarded afterwards
# only the build artifact is carried over to the next image.
# BUILDER_IMAGE - This image buildS the Python package and is discarded afterwards
####
# Reuse image
FROM BASE_IMAGE as BUILDER_IMAGE
# Python libraries caching
ARG PIP_INDEX_URL
ARG PIP_TRUSTED_HOST
WORKDIR /src
# Copy the source files needed to build the Python package
@@ -97,40 +97,39 @@ apt-get install -y --no-install-recommends \
libssl-dev \
g++ \
gcc \
python3-dev \
python3-venv \
python-dev \
python-virtualenv \
&& mkdir -p "${PROJECT_INSTALL_DIR}" \
&& chown -R mayan:mayan "${PROJECT_INSTALL_DIR}" \
&& chown -R mayan:mayan /src
USER mayan
RUN python3 -m venv "${PROJECT_INSTALL_DIR}" \
RUN python -m virtualenv "${PROJECT_INSTALL_DIR}" \
&& . "${PROJECT_INSTALL_DIR}/bin/activate" \
&& pip install --no-cache-dir \
librabbitmq==2.0.0 \
mysqlclient==1.4.2.post1 \
psycopg2==2.8.3 \
redis==3.2.1 \
flower==0.9.3 \
&& pip install --no-cache-dir --no-use-pep517 \
librabbitmq==1.6.1 \
mysql-python==1.2.5 \
psycopg2==2.7.3.2 \
redis==2.10.6 \
# psutil is needed by ARM builds otherwise gevent and gunicorn fail to start
&& UNAME=`uname -m` && if [ "${UNAME#*arm}" != $UNAME ]; then \
pip install --no-cache-dir \
pip install --no-cache-dir --no-use-pep517 \
psutil==5.6.2 \
; fi \
# Install the Python packages needed to build Mayan EDMS
&& pip install --no-cache-dir -r /src/requirements/build.txt \
&& pip install --no-cache-dir --no-use-pep517 -r /src/requirements/build.txt \
# Build Mayan EDMS
&& python3 setup.py sdist \
&& python setup.py sdist \
# Install the built Mayan EDMS package
&& pip install --no-cache-dir dist/mayan* \
&& pip install --no-cache-dir --no-use-pep517 dist/mayan* \
# Install the static content
&& mayan-edms.py installdependencies \
&& mayan-edms.py installjavascript \
&& MAYAN_STATIC_ROOT=${PROJECT_INSTALL_DIR}/static mayan-edms.py preparestatic --link --noinput
COPY --chown=mayan:mayan requirements/testing-base.txt "${PROJECT_INSTALL_DIR}"
####
# Final image - BASE_IMAGE + BUILDER_IMAGE artifact (Mayan install directory)
# Final image - BASE_IMAGE + Mayan install directory from the builder image
####
FROM BASE_IMAGE
@@ -146,7 +145,7 @@ VOLUME ["/var/lib/mayan"]
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 8000
CMD ["run_all"]
CMD ["mayan"]
RUN ${PROJECT_INSTALL_DIR}/bin/mayan-edms.py platformtemplate supervisord_docker > /etc/supervisor/conf.d/mayan.conf \
&& apt-get clean autoclean \

View File

@@ -1,9 +1,4 @@
HOST_IP = `/sbin/ip route|awk '/docker0/ { print $$9 }'`
APT_PROXY ?= $(HOST_IP):3142
PIP_INDEX_URL ?= http://$(HOST_IP):3141/root/pypi/+simple/
PIP_TRUSTED_HOST ?= $(HOST_IP)
APT_PROXY ?= `/sbin/ip route|awk '/docker0/ { print $$9 }'`:3142
IMAGE_VERSION ?= `cat docker/rootfs/version`
CONSOLE_COLUMNS ?= `echo $$(tput cols)`
CONSOLE_LINES ?= `echo $$(tput lines)`
@@ -12,7 +7,7 @@ docker-build: ## Build a new image locally.
docker build -t mayanedms/mayanedms:$(IMAGE_VERSION) -f docker/Dockerfile .
docker-build-with-proxy: ## Build a new image locally using an APT proxy as APT_PROXY.
docker build -t mayanedms/mayanedms:$(IMAGE_VERSION) -f docker/Dockerfile --build-arg APT_PROXY=$(APT_PROXY) --build-arg PIP_INDEX_URL=$(PIP_INDEX_URL) --build-arg PIP_TRUSTED_HOST=$(PIP_TRUSTED_HOST) --build-arg HTTP_PROXY=$(HTTP_PROXY) --build-arg HTTPS_PROXY=$(HTTPS_PROXY) .
docker build -t mayanedms/mayanedms:$(IMAGE_VERSION) -f docker/Dockerfile --build-arg APT_PROXY=$(APT_PROXY) .
docker-shell: ## Launch a bash instance inside a running container. Pass the container name via DOCKER_CONTAINER.
docker exec -e TERM=$(TERM) -e "COLUMNS=$(CONSOLE_COLUMNS)" -e "LINES=$(CONSOLE_LINES)" -it $(DOCKER_CONTAINER) /bin/bash
@@ -28,13 +23,3 @@ docker-test-cleanup: ## Delete the test container and the test volume.
docker-test-all: ## Build and executed the test suite in a test container.
docker-test-all: docker-build-with-proxy
docker run --rm run-tests
docker-compose-build:
docker-compose -f docker/docker-compose.yml -p mayan-edms build
docker-compose-build-with-proxy:
docker-compose -f docker/docker-compose.yml -p mayan-edms build --build-arg APT_PROXY=$(APT_PROXY) --build-arg PIP_INDEX_URL=$(PIP_INDEX_URL) --build-arg PIP_TRUSTED_HOST=$(PIP_TRUSTED_HOST) --build-arg HTTP_PROXY=$(HTTP_PROXY) --build-arg HTTPS_PROXY=$(HTTPS_PROXY)
docker-compose-up:
docker-compose -f docker/docker-compose.yml -p mayan-edms up

View File

@@ -0,0 +1,72 @@
version: '2.1'
volumes:
broker:
driver: local
app:
driver: local
db:
driver: local
results:
driver: local
services:
broker:
container_name: mayan-edms-broker
image: healthcheck/rabbitmq
environment:
RABBITMQ_DEFAULT_USER: mayan
RABBITMQ_DEFAULT_PASS: mayan
RABBITMQ_DEFAULT_VHOST: mayan
volumes:
- broker:/var/lib/rabbitmq
results:
container_name: mayan-edms-results
image: healthcheck/redis
volumes:
- results:/data
#db:
# container_name: mayan-edms-db
# image: healthcheck/mysql
# environment:
# MYSQL_DATABASE: mayan
# MYSQL_PASSWORD: mayan-password
# MYSQL_ROOT_PASSWORD: root-password
# MYSQL_USER: mayan
# volumes:
# - db:/var/lib/mysql
db:
container_name: mayan-edms-db
image: healthcheck/postgres
environment:
POSTGRES_DB: mayan
POSTGRES_PASSWORD: mayan-password
POSTGRES_USER: mayan
volumes:
- db:/var/lib/postgresql/data
mayan-edms:
container_name: mayan-edms-app
image: mayan-edms/next
build:
context: ./
args:
- APT_PROXY=172.18.0.1:3142
depends_on:
broker:
condition: service_healthy
db:
condition: service_healthy
results:
condition: service_healthy
environment:
MAYAN_BROKER_URL: amqp://mayan:mayan@broker:5672/mayan
MAYAN_CELERY_RESULT_BACKEND: redis://results:6379/0
MAYAN_DATABASE_ENGINE: django.db.backends.postgresql
MAYAN_DATABASE_HOST: db
MAYAN_DATABASE_NAME: mayan
MAYAN_DATABASE_PASSWORD: mayan-password
MAYAN_DATABASE_USER: mayan
ports:
- "80:80"
volumes:
- app:/var/lib/mayan

View File

@@ -1,130 +1,58 @@
version: '3.7'
version: '2.1'
networks:
mayan-bridge:
driver: bridge
volumes:
broker:
driver: local
app:
driver: local
db:
driver: local
results:
driver: local
services:
app:
build:
context: ..
dockerfile: ./docker/Dockerfile
depends_on:
- postgresql
- redis
# Enable to use RabbitMQ
#- rabbitmq
environment: &mayan_env
# Enable to use RabbitMQ
# MAYAN_CELERY_BROKER_URL: amqp://mayan:mayanrabbitpass@broker:5672/mayan
# Disable Redis Broker to use RabbitMQ as Broker
MAYAN_CELERY_BROKER_URL: redis://redis:6379/1
MAYAN_CELERY_RESULT_BACKEND: redis://redis:6379/0
MAYAN_DATABASES: "{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayandbpass','USER':'mayan','HOST':'postgresql'}}"
image: mayanedms/mayanedms:3.2.6
networks:
- mayan-bridge
ports:
- "80:8000"
restart: unless-stopped
broker:
container_name: mayan-edms-broker
image: healthcheck/rabbitmq
environment:
RABBITMQ_DEFAULT_USER: mayan
RABBITMQ_DEFAULT_PASS: mayan
RABBITMQ_DEFAULT_VHOST: mayan
volumes:
- /docker-volumes/mayan-edms/media:/var/lib/mayan
postgresql:
- broker:/var/lib/rabbitmq
results:
container_name: mayan-edms-results
image: healthcheck/redis
volumes:
- results:/data
db:
container_name: mayan-edms-db
image: healthcheck/postgres
environment:
POSTGRES_DB: mayan
POSTGRES_PASSWORD: mayandbpass
POSTGRES_PASSWORD: mayan-password
POSTGRES_USER: mayan
image: postgres:9.6
networks:
- mayan-bridge
restart: unless-stopped
volumes:
- /docker-volumes/mayan-edms/postgres:/var/lib/postgresql/data
redis:
command:
- redis-server
- --databases
- "2"
- --maxmemory-policy
- allkeys-lru
- --save
- ""
image: redis:5.0
networks:
- mayan-bridge
restart: unless-stopped
# Optional services
# celery_flower:
# command:
# - run_celery
# - flower
# depends_on:
# - postgresql
# - redis
# # Enable to use RabbitMQ
# # - rabbitmq
# environment:
# <<: *mayan_env
# image: mayanedms/mayanedms:3.2.6
# networks:
# - mayan-bridge
# ports:
# - "5555:5555"
# restart: unless-stopped
# Enable to use RabbitMQ
# rabbitmq:
# container_name: mayan-edms-rabbitmq
# image: healthcheck/rabbitmq
# environment:
# RABBITMQ_DEFAULT_USER: mayan
# RABBITMQ_DEFAULT_PASS: mayanrabbitpass
# RABBITMQ_DEFAULT_VHOST: mayan
# networks:
# - mayan-bridge
# restart: unless-stopped
# volumes:
# - /docker-volumes/mayan-edms/rabbitmq:/var/lib/rabbitmq
# Enable to run stand alone workers
# worker_fast:
# command:
# - run_worker
# - fast
# depends_on:
# - postgresql
# - redis
# # Enable to use RabbitMQ
# # - rabbitmq
# environment:
# <<: *mayan_env
# image: mayanedms/mayanedms:3.2.6
# networks:
# - mayan-bridge
# restart: unless-stopped
# volumes:
# - /docker-volumes/mayan-edms/media:/var/lib/mayan
# Enable to run stand frontend gunicorn
# frontend:
# command:
# - run_frontend
# depends_on:
# - postgresql
# - redis
# # Enable to use RabbitMQ
# # - rabbitmq
# environment:
# <<: *mayan_env
# image: mayanedms/mayanedms:3.2.6
# networks:
# - mayan-bridge
# ports:
# - "81:8000"
# restart: unless-stopped
# volumes:
# - /docker-volumes/mayan-edms/media:/var/lib/mayan
- db:/var/lib/postgresql/data
mayan-edms:
container_name: mayan-edms-app
image: mayanedms/mayanedms:latest
depends_on:
broker:
condition: service_healthy
db:
condition: service_healthy
results:
condition: service_healthy
environment:
MAYAN_BROKER_URL: amqp://mayan:mayan@broker:5672/mayan
MAYAN_CELERY_RESULT_BACKEND: redis://results:6379/0
MAYAN_DATABASE_ENGINE: django.db.backends.postgresql
MAYAN_DATABASE_HOST: db
MAYAN_DATABASE_NAME: mayan
MAYAN_DATABASE_PASSWORD: mayan-password
MAYAN_DATABASE_USER: mayan
ports:
- "80:8000"
volumes:
- app:/var/lib/mayan

View File

@@ -1,7 +1,4 @@
#!/bin/bash
# Use bash and not sh to support argument slicing "${@:2}"
# sh defaults to dash instead of bash.
#!/bin/sh
set -e
echo "mayan: starting entrypoint.sh"
@@ -14,13 +11,17 @@ 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}
@@ -28,8 +29,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:-0}
MAYAN_WORKER_MEDIUM_CONCURRENCY=${MAYAN_WORKER_MEDIUM_CONCURRENCY:-0}
MAYAN_WORKER_FAST_CONCURRENCY=${MAYAN_WORKER_FAST_CONCURRENCY:-1}
MAYAN_WORKER_MEDIUM_CONCURRENCY=${MAYAN_WORKER_MEDIUM_CONCURRENCY:-1}
MAYAN_WORKER_SLOW_CONCURRENCY=${MAYAN_WORKER_SLOW_CONCURRENCY:-1}
update_uid_gid() {
@@ -66,9 +67,11 @@ else
fi
export MAYAN_WORKER_SLOW_CONCURRENCY
# Allow importing of user setting modules
export CELERY_ALWAYS_EAGER=False
export PYTHONPATH=$PYTHONPATH:$MAYAN_MEDIA_ROOT
chown mayan:mayan /var/lib/mayan -R
apt_get_install() {
apt-get -q update
apt-get install -y --force-yes --no-install-recommends --auto-remove "$@"
@@ -76,9 +79,9 @@ apt_get_install() {
rm -rf /var/lib/apt/lists/*
}
initialsetup() {
echo "mayan: initialsetup()"
su mayan -c "${MAYAN_BIN} initialsetup --force --no-dependencies"
initialize() {
echo "mayan: initialize()"
su mayan -c "${MAYAN_BIN} initialsetup --force --no-javascript"
}
os_package_installs() {
@@ -95,71 +98,43 @@ pip_installs() {
fi
}
run_all() {
start() {
echo "mayan: start()"
rm -rf /var/run/supervisor.sock
exec /usr/bin/supervisord -nc /etc/supervisor/supervisord.conf
}
performupgrade() {
echo "mayan: performupgrade()"
su mayan -c "${MAYAN_BIN} performupgrade --no-dependencies"
}
make_ready() {
# Check if this is a new install, otherwise try to upgrade the existing
# installation on subsequent starts
if [ ! -f $INSTALL_FLAG ]; then
initialsetup
else
performupgrade
fi
}
set_uid_guid() {
echo "mayan: changing uid/guid"
usermod mayan -u ${MAYAN_USER_UID:-${DEFAULT_USER_UID}}
groupmod mayan -g ${MAYAN_USER_GID:-${DEFAULT_USER_GID}}
upgrade() {
echo "mayan: upgrade()"
su mayan -c "${MAYAN_BIN} performupgrade --no-javascript"
}
os_package_installs || true
pip_installs || true
chown mayan:mayan /var/lib/mayan -R
case "$1" in
run_initialsetup)
initialsetup
;;
mayan) # Check if this is a new install, otherwise try to upgrade the existing
# installation on subsequent starts
if [ ! -f $INSTALL_FLAG ]; then
initialize
else
upgrade
fi
start
;;
run_performupgrade)
performupgrade
;;
run-tests) # Check if this is a new install, otherwise try to upgrade the existing
# installation on subsequent starts
if [ ! -f $INSTALL_FLAG ]; then
initialize
else
upgrade
fi
run-tests.sh
;;
run_all)
make_ready
run_all
;;
run_celery)
run_celery.sh "${@:2}"
;;
run_frontend)
run_frontend.sh
;;
run_tests)
make_ready
run_tests.sh
;;
run_worker)
run_worker.sh "${@:2}"
;;
*)
su mayan -c "$@"
;;
*) su mayan -c "$@";
;;
esac

View File

@@ -1,5 +0,0 @@
#!/bin/bash
# Use -A and not --app. Both are the same but behave differently
# -A can be located before the command while --app cannot.
su mayan -c "${MAYAN_PYTHON_BIN_DIR}celery -A mayan $@"

View File

@@ -1,7 +0,0 @@
#!/bin/bash
MAYAN_GUNICORN_MAX_REQUESTS=${MAYAN_GUNICORN_MAX_REQUESTS:-500}
MAYAN_GUNICORN_MAX_REQUESTS_JITTERS=${MAYAN_GUNICORN_MAX_REQUESTS_JITTERS:-50}
MAYAN_GUNICORN_WORKER_CLASS=${MAYAN_GUNICORN_WORKER_CLASS:-sync}
su mayan -c "${MAYAN_PYTHON_BIN_DIR}gunicorn -w ${MAYAN_GUNICORN_WORKERS} mayan.wsgi --max-requests ${MAYAN_GUNICORN_MAX_REQUESTS} --max-requests-jitter ${MAYAN_GUNICORN_MAX_REQUESTS_JITTERS} --worker-class ${MAYAN_GUNICORN_WORKER_CLASS} --bind 0.0.0.0:8000 --timeout ${MAYAN_GUNICORN_TIMEOUT}"

View File

@@ -1,8 +0,0 @@
#!/bin/bash
QUEUE_LIST=`MAYAN_WORKER_NAME=$1 su mayan -c "${MAYAN_PYTHON_BIN_DIR}mayan-edms.py platformtemplate worker_queues"`
# Use -A and not --app. Both are the same but behave differently
# -A can be located before the command while --app cannot.
# Pass ${@:2} to allow overriding the defaults arguments
su mayan -c "${MAYAN_PYTHON_BIN_DIR}celery -A mayan worker -Ofair -l ERROR -Q $QUEUE_LIST ${@:2}"

View File

@@ -1 +1 @@
3.3beta1
3.2.8

View File

@@ -9,32 +9,24 @@ volumes:
services:
db:
image: postgres
environment:
POSTGRES_DB: mayan
POSTGRES_PASSWORD: mayandbpass
POSTGRES_PASSWORD: mayan-password
POSTGRES_USER: mayan
image: postgres
volumes:
- db:/var/lib/postgresql/data
app:
environment:
MAYAN_CELERY_BROKER_URL: redis://redis:6379/1
MAYAN_CELERY_RESULT_BACKEND: redis://redis:6379/0
MAYAN_DATABASES: "{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayandbpass','USER':'mayan','HOST':'db'}}"
image: mayanedms/mayanedms:latest
ports:
- 80:8000
environment:
MAYAN_DATABASE_ENGINE: django.db.backends.postgresql
MAYAN_DATABASE_HOST: db
MAYAN_DATABASE_NAME: mayan
MAYAN_DATABASE_PASSWORD: mayan-password
MAYAN_DATABASE_USER: mayan
MAYAN_DATABASE_CONN_MAX_AGE: 0
volumes:
- app:/var/lib/mayan
redis:
command:
- redis-server
- --databases
- "2"
- --maxmemory-policy
- allkeys-lru
- --save
- ""
image: redis:5.0

View File

@@ -127,8 +127,9 @@ For another setup that offers more performance and scalability refer to the
::
sudo -u mayan MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
sudo -u mayan MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py initialsetup
@@ -147,8 +148,9 @@ For another setup that offers more performance and scalability refer to the
------------------------------------------------------------------------
::
sudo mayan MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
sudo MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf
@@ -220,11 +222,11 @@ of a restart or power failure. The Gunicorn workers are increased to 3.
---------------------------------------------------------------------
Replace (paying attention to the comma at the end)::
MAYAN_CELERY_BROKER_URL="redis://127.0.0.1:6379/0",
MAYAN_BROKER_URL="redis://127.0.0.1:6379/0",
with::
MAYAN_CELERY_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
MAYAN_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
increase the number of Gunicorn workers to 3 in the line (``-w 2`` section)::

View File

@@ -49,7 +49,12 @@ Finally create and run a Mayan EDMS container::
--name mayan-edms \
--restart=always \
-p 80:8000 \
-e MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'172.17.0.1'}}" \
-e MAYAN_DATABASE_ENGINE=django.db.backends.postgresql \
-e MAYAN_DATABASE_HOST=172.17.0.1 \
-e MAYAN_DATABASE_NAME=mayan \
-e MAYAN_DATABASE_PASSWORD=mayanuserpass \
-e MAYAN_DATABASE_USER=mayan \
-e MAYAN_DATABASE_CONN_MAX_AGE=0 \
-v /docker-volumes/mayan-edms/media:/var/lib/mayan \
mayanedms/mayanedms:<version>
@@ -103,7 +108,12 @@ instead of the IP address of the Docker host (``172.17.0.1``)::
--network=mayan \
--restart=always \
-p 80:8000 \
-e MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'mayan-edms-postgres'}}" \
-e MAYAN_DATABASE_ENGINE=django.db.backends.postgresql \
-e MAYAN_DATABASE_HOST=mayan-edms-postgres \
-e MAYAN_DATABASE_NAME=mayan \
-e MAYAN_DATABASE_PASSWORD=mayanuserpass \
-e MAYAN_DATABASE_USER=mayan \
-e MAYAN_DATABASE_CONN_MAX_AGE=0 \
-v /docker-volumes/mayan-edms/media:/var/lib/mayan \
mayanedms/mayanedms:<version>
@@ -127,14 +137,101 @@ To start the container again::
Environment Variables
---------------------
The common set of settings can also be modified via environment variables when
using the Docker image. In addition to the common set of settings, some Docker
image specific environment variables are available.
The Mayan EDMS image can be configure via environment variables.
``MAYAN_DATABASE_ENGINE``
Defaults to ``None``. This environment variable configures the database
backend to use. If left unset, SQLite will be used. The database backends
supported by this Docker image are:
- ``'django.db.backends.postgresql'``
- ``'django.db.backends.mysql'``
- ``'django.db.backends.sqlite3'``
When using the SQLite backend, the database file will be saved in the Docker
volume. The SQLite database as used by Mayan EDMS is meant only for development
or testing, never use it in production.
``MAYAN_DATABASE_NAME``
Defaults to 'mayan'. This optional environment variable can be used to define
the database name that Mayan EDMS will connect to. For more information read
the pertinent Django documentation page:
:django-docs:`Connecting to the database <ref/databases/#connecting-to-the-database>`
``MAYAN_DATABASE_USER``
Defaults to 'mayan'. This optional environment variable is used to set the
username that will be used to connect to the database. For more information
read the pertinent Django documentation page:
:django-docs:`Settings, USER <ref/settings/#user>`
``MAYAN_DATABASE_PASSWORD``
Defaults to ''. This optional environment variable is used to set the
password that will be used to connect to the database. For more information
read the pertinent Django documentation page:
:django-docs:`Settings, PASSWORD <ref/settings/#password>`
``MAYAN_DATABASE_HOST``
Defaults to `None`. This optional environment variable is used to set the
hostname that will be used to connect to the database. This can be the
hostname of another container or an IP address. For more information read
the pertinent Django documentation page:
:django-docs:`Settings, HOST <ref/settings/#host>`
``MAYAN_DATABASE_PORT``
Defaults to `None`. This optional environment variable is used to set the
port number to use when connecting to the database. An empty string means
the default port. Not used with SQLite. For more information read the
pertinent Django documentation page:
:django-docs:`Settings, PORT <ref/settings/#port>`
``MAYAN_BROKER_URL``
This optional environment variable determines the broker that Celery will use
to relay task messages between the frontend code and the background workers.
For more information read the pertinent Celery Kombu documentation page: `Broker URL`_
.. _Broker URL: http://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls
This Docker image supports using Redis and RabbitMQ as brokers.
Caveat: If the `MAYAN_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment
variables are specified, the built-in Redis server inside the container will
be disabled.
``MAYAN_CELERY_RESULT_BACKEND``
This optional environment variable determines the results backend that Celery
will use to relay result messages from the background workers to the frontend
code. For more information read the pertinent Celery Kombu documentation page:
`Task result backend settings`_
.. _Task result backend settings: http://docs.celeryproject.org/en/3.1/configuration.html#celery-result-backend
This Docker image supports using Redis and RabbitMQ as result backends.
Caveat: If the `MAYAN_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment
variables are specified, the built-in Redis server inside the container will
be disabled.
``MAYAN_SETTINGS_MODULE``
Optional. Allows loading an alternate settings file.
``MAYAN_DATABASE_CONN_MAX_AGE``
Amount in seconds to keep a database connection alive. Allow reuse of database
connections. For more information read the pertinent Django documentation
page: :django-docs:`Settings, CONN_MAX_AGE <ref/settings/#conn-max-age>`
According to new information Gunicorn's microthreads don't share connections
and will exhaust the available Postgres connections available if a number
other than 0 is used. Reference: https://serverfault.com/questions/635100/django-conn-max-age-persists-connections-but-doesnt-reuse-them-with-postgresq
and https://github.com/benoitc/gunicorn/issues/996
``MAYAN_GUNICORN_TIMEOUT``
@@ -184,15 +281,6 @@ 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
@@ -360,7 +448,6 @@ These are:
Nightly images
==============
The continuous integration pipeline used for testing development builds also
produces a resulting Docker image. These are build automatically and their
stability is not guaranteed. They should never be used in production.

View File

@@ -94,11 +94,11 @@ For the Docker image, launch a separate RabbitMQ container
docker run -d --name mayan-edms-rabbitmq -e RABBITMQ_DEFAULT_USER=mayan -e RABBITMQ_DEFAULT_PASS=mayanrabbitmqpassword -e RABBITMQ_DEFAULT_VHOST=mayan rabbitmq:3
Pass the MAYAN_CELERY_BROKER_URL environment variable (https://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls)
Pass the MAYAN_BROKER_URL environment variable (https://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls)
to the Mayan EDMS container so that it uses the RabbitMQ container the
message broker::
-e MAYAN_CELERY_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
-e MAYAN_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
When tasks finish, they leave behind a return status or the result of a
calculation, these are stored for a while so that whoever requested the

View File

@@ -19,7 +19,6 @@ Changes
GitLab issue #625. Thanks to Jesaja Everling (@jeverling)
for the report and the research.
Removals
--------

View File

@@ -12,7 +12,8 @@ Changes
- Fix help text of the platformtemplate command.
- Fix IMAP4 mailbox.store flags argument. Python's documentation
incorrectly state it is named flag_list. Closes GitLab issue
#606. Thanks to Samuel Aebi (@samuelaebi) for the report and
#606. Thanks to Samuel Aebi (@samuelaebi) for the report and
debug information.
debug information.
- Support configurable GUnicorn timeouts. Defaults to
current value of 120 seconds.

View File

@@ -1,213 +0,0 @@
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/

View File

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

View File

@@ -1,9 +1,9 @@
from __future__ import unicode_literals
__title__ = 'Mayan EDMS'
__version__ = '3.3beta1'
__build__ = 0x030300
__build_string__ = 'v3.3beta1-9-g1b327b99f0_Tue Oct 8 15:15:08 2019 -0400'
__version__ = '3.2.8'
__build__ = 0x030208
__build_string__ = 'v3.2.8_Tue Oct 1 13:31:40 2019 -0400'
__django_version__ = '1.11'
__author__ = 'Roberto Rosario'
__author_email__ = 'roberto.rosario@mayan-edms.com'

View File

@@ -12,7 +12,6 @@ logger = logging.getLogger(__name__)
class ModelPermission(object):
_functions = {}
_inheritances = {}
_manager_names = {}
_registry = {}
@classmethod
@@ -21,6 +20,22 @@ 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(
@@ -82,40 +97,6 @@ class ModelPermission(object):
def get_inheritance(cls, model):
return cls._inheritances[model]
@classmethod
def get_manager(cls, model):
try:
manager_name = cls.get_manager_name(model=model)
except KeyError:
manager_name = None
if manager_name:
manager = getattr(model, manager_name)
else:
manager = model._meta.default_manager
return manager
@classmethod
def get_manager_name(cls, model):
return cls._manager_names[model]
@classmethod
def register(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
@@ -123,7 +104,3 @@ class ModelPermission(object):
@classmethod
def register_inheritance(cls, model, related):
cls._inheritances[model] = related
@classmethod
def register_manager(cls, model, manager_name):
cls._manager_names[model] = manager_name

View File

@@ -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
# 6: Inherited field of a related field that is Generic Foreign Key
# -- Not addressed yet --
# 6: Inherited field of a related field that is Generic Foreign Key
# 7: Has a related function
result = []
@@ -58,28 +58,10 @@ 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(
'{}{}'.format(
recuisive_related_reference, related_field.ct_field
), Value('-'),
'{}{}'.format(
recuisive_related_reference, related_field.fk_field
), output_field=CharField()
related_field.ct_field, Value('-'),
related_field.fk_field, output_field=CharField()
)
).values('ct_fk_combination')
@@ -93,7 +75,8 @@ class AccessControlListManager(models.Manager):
ct_fk_combination__in=content_type_object_id_queryset
).values('object_id')
field_lookup = '{}object_id__in'.format(recuisive_related_reference)
field_lookup = 'object_id__in'
result.append(Q(**{field_lookup: acl_filter}))
else:
# Case 2: Related field of a single type, single ContentType,
@@ -114,7 +97,6 @@ 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
@@ -218,26 +200,28 @@ class AccessControlListManager(models.Manager):
return result
def check_access(self, obj, permissions, user):
def check_access(self, obj, permissions, user, manager=None):
# Allow specific managers for models that have more than one
# for example the Document model when checking for access for a trashed
# document.
meta = getattr(obj, '_meta', None)
if not meta:
logger.debug(
ugettext(
'Object "%s" is not a model and cannot be checked for '
'access.'
) % force_text(obj)
)
return True
else:
manager = ModelPermission.get_manager(model=obj._meta.model)
if manager:
source_queryset = manager.all()
else:
meta = getattr(obj, '_meta', None)
restricted_queryset = manager.none()
if not meta:
logger.debug(
ugettext(
'Object "%s" is not a model and cannot be checked for '
'access.'
) % force_text(obj)
)
return True
else:
source_queryset = obj._meta.default_manager.all()
restricted_queryset = obj._meta.default_manager.none()
for permission in permissions:
# Default relationship betweens permissions is OR
# TODO: Add support for AND relationship

View File

@@ -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.base import BaseAPITestCase
from mayan.apps.rest_api.tests import BaseAPITestCase
from ..models import AccessControlList
from ..permissions import permission_acl_edit, permission_acl_view

View File

@@ -1,6 +1,6 @@
from __future__ import absolute_import, unicode_literals
from mayan.apps.common.tests.base import BaseTestCase
from mayan.apps.common.tests import BaseTestCase
from ..classes import ModelPermission

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.urls import reverse
from mayan.apps.common.tests.base import GenericViewTestCase
from mayan.apps.common.tests import GenericViewTestCase
from ..links import (
link_acl_delete, link_acl_list, link_acl_create, link_acl_permissions

View File

@@ -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.base import BaseTestCase
from mayan.apps.common.tests import BaseTestCase
from ..classes import ModelPermission
from ..models import AccessControlList

View File

@@ -1,6 +1,6 @@
from __future__ import absolute_import, unicode_literals
from mayan.apps.common.tests.base import GenericViewTestCase
from mayan.apps.common.tests import GenericViewTestCase
from ..models import AccessControlList
from ..permissions import permission_acl_edit, permission_acl_view

View File

@@ -16,6 +16,7 @@ from mayan.apps.permissions.models import Role
from .classes import ModelPermission
from .permissions import permission_acl_edit
__all__ = ('GrantAccessAction', 'RevokeAccessAction')
logger = logging.getLogger(__name__)
@@ -56,7 +57,7 @@ class GrantAccessAction(WorkflowAction):
}
}
field_order = ('content_type', 'object_id', 'roles', 'permissions')
label = _('Grant object access')
label = _('Grant access')
widgets = {
'content_type': {
'class': 'django.forms.widgets.Select', 'kwargs': {
@@ -139,7 +140,7 @@ class GrantAccessAction(WorkflowAction):
class RevokeAccessAction(GrantAccessAction):
label = _('Revoke object access')
label = _('Revoke access')
def execute(self, context):
self.get_execute_data()

View File

@@ -4,7 +4,6 @@ from django.template.loader import get_template
class IconDriver(object):
context = {}
_registry = {}
@classmethod
@@ -15,17 +14,6 @@ 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'
@@ -34,8 +22,10 @@ class FontAwesomeDriver(IconDriver):
def __init__(self, symbol):
self.symbol = symbol
def get_context(self):
return {'symbol': self.symbol}
def render(self):
return get_template(template_name=self.template_name).render(
context={'symbol': self.symbol}
)
class FontAwesomeDualDriver(IconDriver):
@@ -46,21 +36,23 @@ class FontAwesomeDualDriver(IconDriver):
self.primary_symbol = primary_symbol
self.secondary_symbol = secondary_symbol
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'
},
)
}
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'
},
)
}
)
class FontAwesomeCSSDriver(IconDriver):
@@ -70,8 +62,10 @@ class FontAwesomeCSSDriver(IconDriver):
def __init__(self, css_classes):
self.css_classes = css_classes
def get_context(self):
return {'css_classes': self.css_classes}
def render(self):
return get_template(template_name=self.template_name).render(
context={'css_classes': self.css_classes}
)
class FontAwesomeMasksDriver(IconDriver):
@@ -81,23 +75,23 @@ class FontAwesomeMasksDriver(IconDriver):
def __init__(self, data):
self.data = data
def get_context(self):
return {'data': self.data}
def render(self):
return get_template(template_name=self.template_name).render(
context={'data': self.data}
)
class FontAwesomeLayersDriver(IconDriver):
name = 'fontawesome-layers'
template_name = 'appearance/icons/font_awesome_layers.html'
def __init__(self, data, shadow_class=None):
def __init__(self, data):
self.data = data
self.shadow_class = shadow_class
def get_context(self):
return {
'data': self.data,
'shadow_class': self.shadow_class,
}
def render(self):
return get_template(template_name=self.template_name).render(
context={'data': self.data}
)
class Icon(object):

View File

@@ -1,2 +1 @@
DEFAULT_MAXIMUM_TITLE_LENGTH = 120
DEFAULT_MESSAGE_POSITION = 'top-right'

View File

@@ -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, DEFAULT_MESSAGE_POSITION
from .literals import DEFAULT_MAXIMUM_TITLE_LENGTH
namespace = Namespace(label=_('Appearance'), name='appearance')
@@ -15,11 +15,3 @@ 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.'
)
)

View File

@@ -12,7 +12,7 @@
}
body {
padding-top: 60px;
padding-top: 70px;
}
.navbar-brand {
@@ -70,8 +70,7 @@ img.lazy-load-carousel {
}
.label-tag {
text-shadow: 0px 0px 2px #000;
box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.5);
text-shadow: 0px 0px 2px #000
}
.fancybox-nav span {
@@ -89,17 +88,19 @@ 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;
min-height: 120px;
padding-bottom: 1px;
padding-top: 20px;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 1);
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);
}
.radio ul li {
@@ -111,10 +112,14 @@ a i {
}
.dashboard-widget {
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.7);
box-shadow: 1px 1px 1px rgba(0,0,0,0.3);
border: 1px solid black;
}
.dashboard-widget .panel-heading i {
text-shadow: 1px 1px 1px rgba(0,0,0,0.3);
}
.dashboard-widget-icon {
font-size: 200%;
}
@@ -165,7 +170,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;
@@ -208,22 +213,6 @@ a i {
font-weight: bold;
}
.source-column-label {
font-weight: bold;
}
.panel-highlighted {
box-shadow: 0px 0px 3px #18bc9c, 10px 10px 20px #000000;
}
.panel-highlighted:hover {
box-shadow: 0px 0px 3px #18bc9c, 10px 10px 20px #000000, 0px 0px 8px #000000;
}
.panel-item:not(.panel-highlighted):hover {
box-shadow: 0px 0px 8px #000000;
}
/* Content */
@media (min-width:1200px) {
.container-fluid {
@@ -253,6 +242,14 @@ a i {
margin: auto;
}
.thin_border {
border: 1px solid black;
display: block;
margin-left: auto;
margin-right: auto;
}
.thin_border-thumbnail {
display: block;
max-width: 100%;
@@ -262,18 +259,10 @@ a i {
margin: auto;
}
/* Must go after .thin_border-thumbnail */
.thin_border {
border: 1px solid black;
display: inline;
margin-left: 0px;
margin-right: 0px;
}
#ajax-spinner {
position: fixed;
top: 16px;
left: 10px;
top: 12px;
right: 10px;
z-index: 9999;
width: 25px;
height: 25px;
@@ -339,7 +328,7 @@ a i {
.main {
padding-right: 0px;
padding-left: 0px;
margin-left: 210px;
/*margin-left: 210px;*/
}
}
@@ -422,141 +411,6 @@ 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%;

View File

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

View File

@@ -4,7 +4,7 @@ class MayanApp {
constructor (options) {
var self = this;
this.options = options || {
options = options || {
ajaxMenusOptions: []
}
@@ -17,55 +17,37 @@ class MayanApp {
// Class methods and variables
static countChecked() {
var checkCount = $('.check-all-slave:checked').length;
static MultiObjectFormProcess ($form, options) {
/*
* ajaxForm callback to add the external item checkboxes to the
* submitted form
*/
if (checkCount) {
$('#multi-item-title').hide();
$('#multi-item-actions').show();
} else {
$('#multi-item-title').show();
$('#multi-item-actions').hide();
if ($form.hasClass('form-multi-object-action')) {
// Turn form data into an object
var formArray = $form.serializeArray().reduce(function (obj, item) {
obj[item.name] = item.value;
return obj;
}, {});
// Add all checked checkboxes to the form data
$('.form-multi-object-action-checkbox:checked').each(function() {
var $this = $(this);
formArray[$this.attr('name')] = $this.attr('value');
});
// Set the form data as the data to send
options.data = formArray;
}
}
static setupMultiItemActions () {
$('body').on('change', '.check-all-slave', function () {
MayanApp.countChecked();
});
$('body').on('click', '.btn-multi-item-action', function (event) {
var id_list = [];
$('.check-all-slave:checked').each(function (index, value) {
//Split the name (ie:"pk_200") and extract only the ID
id_list.push(value.name.split('_')[1]);
});
event.preventDefault();
partialNavigation.setLocation(
$(this).attr('href') + '?id_list=' + id_list.join(',')
);
});
}
static setupNavBarState () {
$('body').on('click', '.a-main-menu-accordion-link', function (event) {
$('.a-main-menu-accordion-link').each(function (index, value) {
$(this).parent().removeClass('active');
});
$(this).parent().addClass('active');
});
}
static updateNavbarState () {
var uri = new URI(window.location.hash);
var uriFragment = uri.fragment();
$('.a-main-menu-accordion-link').each(function (index, value) {
if (value.pathname === uriFragment) {
var $this = $(this);
$this.closest('.collapse').addClass('in').parent().find('.collapsed').removeClass('collapsed').attr('aria-expanded', 'true');
$this.parent().addClass('active');
$(this).closest('.collapse').addClass('in').parent().find('.collapsed').removeClass('collapsed').attr('aria-expanded', 'true');
$(this).parent().addClass('active');
}
});
}
@@ -110,7 +92,7 @@ class MayanApp {
'closeButton': true,
'debug': false,
'newestOnTop': true,
'positionClass': 'toast-' + this.options.messagePosition,
'positionClass': 'toast-top-right',
'preventDuplicates': false,
'onclick': null,
'showDuration': '300',
@@ -180,19 +162,17 @@ 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();
}
@@ -216,6 +196,14 @@ class MayanApp {
});
}
setupAutoSubmit () {
$('body').on('change', '.select-auto-submit', function () {
if ($(this).val()) {
$(this.form).trigger('submit');
}
});
}
setupBodyAdjust () {
var self = this;
@@ -254,22 +242,9 @@ class MayanApp {
app.lastChecked = null;
$('body').on('click', '.check-all', function (event) {
var $this = $(this);
var checked = $(event.target).prop('checked');
var $checkBoxes = $('.check-all-slave');
if (checked === undefined) {
checked = $this.data('checked');
checked = !checked;
$this.data('checked', checked);
if (checked) {
$this.find('[data-fa-i2svg]').addClass($this.data('icon-checked')).removeClass($this.data('icon-unchecked'));
} else {
$this.find('[data-fa-i2svg]').addClass($this.data('icon-unchecked')).removeClass($this.data('icon-checked'));
}
}
$checkBoxes.prop('checked', checked);
$checkBoxes.trigger('change');
});
@@ -315,58 +290,6 @@ class MayanApp {
});
}
setupPanelSelection () {
var app = this;
// Setup panel highlighting on check
$('body').on('change', '.check-all-slave', function (event) {
var checked = $(event.target).prop('checked');
if (checked) {
$(this).closest('.panel-item').addClass('panel-highlighted');
} else {
$(this).closest('.panel-item').removeClass('panel-highlighted');
}
});
$('body').on('click', '.panel-item', function (event) {
var $this = $(this);
var targetSrc = $(event.target).prop('src');
var targetHref = $(event.target).prop('href');
var targetIsButton = event.target.tagName === 'BUTTON';
var lastChecked = null;
if ((targetSrc === undefined) && (targetHref === undefined) && (targetIsButton === false)) {
var $checkbox = $this.find('.check-all-slave');
var checked = $checkbox.prop('checked');
if (checked) {
$checkbox.prop('checked', '');
$checkbox.trigger('change');
} else {
$checkbox.prop('checked', 'checked');
$checkbox.trigger('change');
}
if(!app.lastChecked) {
app.lastChecked = $checkbox;
}
if (event.shiftKey) {
var $checkBoxes = $('.check-all-slave');
var start = $checkBoxes.index($checkbox);
var end = $checkBoxes.index(app.lastChecked);
$checkBoxes.slice(
Math.min(start, end), Math.max(start, end) + 1
).prop('checked', app.lastChecked.prop('checked')).trigger('change');
}
app.lastChecked = $checkbox;
window.getSelection().removeAllRanges();
}
});
}
setupScrollView () {
$('.scrollable').scrollview();
}

View File

@@ -23,7 +23,6 @@
{% 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">
@@ -44,7 +43,7 @@
{% if settings_changed %}
<div class="alert alert-dismissible alert-warning">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<p><strong>{% trans 'Warning' %}</strong> {% trans 'Settings updated, restart your installation and refresh your browser for changes to take effect.' %}</p>
<p><strong>{% trans 'Warning' %}</strong> {% trans 'Settings updated, restart your installation for changes to take proper effect.' %}</p>
</div>
{% endif %}
</div>
@@ -139,9 +138,6 @@
},
{% endfor %}
];
$(function () {
$('[data-toggle="tooltip"]').tooltip();
})
</script>
{% block javascript %}{% endblock %}

View File

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

View File

@@ -45,7 +45,7 @@
{{ field }}
{% endfor %}
{% for field in form.visible_fields %}
<div class="form-group {% if field.errors %}has-error{% endif %} {{ form_field_css_classes }}">
<div class="form-group {% if field.errors %}has-error{% endif %}">
{# 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 %}

View File

@@ -11,9 +11,41 @@
{% include 'appearance/no_results.html' %}
</div>
{% else %}
{% include "appearance/list_header.html" %}
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
<h4>
{% if page_obj %}
{% if page_obj.paginator.num_pages != 1 %}
{% blocktrans with page_obj.start_index as start and page_obj.end_index as end and page_obj.paginator.object_list|length as total and page_obj.number as page_number and page_obj.paginator.num_pages as total_pages %}Total ({{ start }} - {{ end }} out of {{ total }}) (Page {{ page_number }} of {{ total_pages }}){% endblocktrans %}
{% else %}
{% blocktrans with page_obj.paginator.object_list|length as total %}Total: {{ total }}{% endblocktrans %}
{% endif %}
{% else %}
{% blocktrans with object_list|length as total %}Total: {{ total }}{% endblocktrans %}
{% endif %}
</h4>
<hr>
<div class="well center-block">
<div class="clearfix">
<div class="pull-right">
<form action="{% url 'common:multi_object_action_view' %}" class="form-multi-object-action" method="get">
{% if object_list %}
{% if not hide_multi_item_actions %}
{% get_multi_item_links_form object_list %}
{% endif %}
{% if multi_item_actions %}
<fieldset style="margin-top: -10px;">
<input class="check-all" type="checkbox"/>&nbsp;
{{ multi_item_form }}
</fieldset>
{% endif %}
{% endif %}
</form>
</div>
</div>
{% if object_list %}
<hr style="border-bottom: 1px solid lightgrey;">
{% endif %}
<div class="row row-items">
{% for object in object_list %}
<div class="{{ column_class|default:'col-xs-12 col-sm-4 col-md-3 col-lg-2' }}">
@@ -21,9 +53,9 @@
<div class="panel-heading">
<div class="form-group">
<div class="checkbox">
<label for="id_indexes_0" style="cursor: auto;">
{% if links_multi_menus_results %}
<input class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" style="cursor: pointer;" type="checkbox" />
<label for="id_indexes_0">
{% if multi_item_actions %}
<input class="form-multi-object-action-checkbox check-all-slave checkbox" type="checkbox" name="pk_{{ object.pk }}" />
{% endif %}
<span style="color: white; word-break: break-all; overflow-wrap: break-word;">
@@ -36,7 +68,12 @@
{% else %}
{% navigation_get_source_columns source=object only_identifier=True as source_column %}
{% navigation_source_column_resolve column=source_column as column_value %}
{{ column_value }}
{% if source_column.is_attribute_absolute_url or source_column.is_object_absolute_url %}
<a href="{% navigation_source_column_get_absolute_url source_column=source_column obj=object %}">{{ column_value }}</a>
{% else %}
{{ column_value }}
{% endif %}
{% endif %}
</span>
</label>
@@ -45,10 +82,11 @@
</div>
<div class="panel-body">
{% if not hide_columns %}
{% navigation_get_source_columns source=object exclude_identifier=True as source_columns %}
{% for column in source_columns %}
<div class="text-center" style="">{% navigation_source_column_resolve column=column as column_value %}{% if column_value != '' %}{% if column.include_label %}<span class="source-column-label">{{ column.label }}</span>: {% endif %}{{ column_value }}{% endif %}</div>
<div class="text-center" style="">{% navigation_source_column_resolve column=column as column_value %}{% if column_value != '' %}{% if column.include_label %}{{ column.label }}: {% endif %}{{ column_value }}{% endif %}</div>
{% endfor %}
{% endif %}
@@ -98,6 +136,7 @@
</div>
{% endfor %}
</div>
{% include 'pagination/pagination.html' %}
</div>
{% endif %}
</div>

View File

@@ -1,7 +1,6 @@
{% load i18n %}
{% load static %}
{% load appearance_tags %}
{% load common_tags %}
{% load navigation_tags %}
@@ -12,16 +11,44 @@
{% include 'appearance/no_results.html' %}
</div>
{% else %}
{% include "appearance/list_header.html" %}
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
<h4>
{% if page_obj %}
{% if page_obj.paginator.num_pages != 1 %}
{% blocktrans with page_obj.start_index as start and page_obj.end_index as end and page_obj.paginator.object_list|length as total and page_obj.number as page_number and page_obj.paginator.num_pages as total_pages %}Total ({{ start }} - {{ end }} out of {{ total }}) (Page {{ page_number }} of {{ total_pages }}){% endblocktrans %}
{% else %}
{% blocktrans with page_obj.paginator.object_list|length as total %}Total: {{ total }}{% endblocktrans %}
{% endif %}
{% else %}
{% blocktrans with object_list|length as total %}Total: {{ total }}{% endblocktrans %}
{% endif %}
</h4>
<hr>
<div class="well center-block">
<div class="clearfix">
<div class="pull-right">
<form action="{% url 'common:multi_object_action_view' %}" class="form-multi-object-action" method="get">
{% if object_list %}
{% if not hide_multi_item_actions %}
{% get_multi_item_links_form object_list %}
{% endif %}
{% if multi_item_actions %}
<fieldset style="margin-top: -10px; margin-bottom: 10px;">
{{ multi_item_form }}
</fieldset>
{% endif %}
{% endif %}
</form>
</div>
</div>
<div class="table-responsive">
<table class="table table-condensed table-striped">
<tbody>
{% if not hide_header %}
<tr>
{% if links_multi_menus_results %}
<th class="first"></th>
{% if multi_item_actions %}
<th class="first"><input class="checkbox check-all" type="checkbox" /></th>
{% endif %}
{% if not hide_object %}
@@ -30,46 +57,32 @@
{% navigation_get_source_columns source=object_list only_identifier=True as source_column %}
{% if source_column %}
<th>
<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 }}
{% 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 %}
{% 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>
</a>
{% else %}
{{ source_column.label }}
{% endif %}
</th>
{% endif %}
{% endif %}
{% if not hide_columns %}
{% navigation_get_source_columns source=object_list exclude_identifier=True as source_columns %}
{% for source_column in source_columns %}
{% for column in source_columns %}
<th>
<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 }}
{% if column.is_sortable %}
<a href="{% navigation_get_sort_field_querystring column=column %}">{{ column.label }}
{% if column.get_sort_field == sort_field %}
{% if icon_sort %}{{ icon_sort.render }}{% endif %}
{% endif %}
{% 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>
</a>
{% else %}
{{ column.label }}
{% endif %}
</th>
{% endfor %}
{% endif %}
@@ -86,9 +99,9 @@
{% for object in object_list %}
<tr>
{% if links_multi_menus_results %}
{% if multi_item_actions %}
<td>
<input class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" type="checkbox" value="" />
<input type="checkbox" class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" value="" />
</td>
{% endif %}
@@ -99,7 +112,11 @@
{% navigation_source_column_resolve column=source_column as column_value %}
{% if column_value %}
<td>
{{ column_value }}
{% if source_column.is_attribute_absolute_url or source_column.is_object_absolute_url %}
<a href="{% navigation_source_column_get_absolute_url source_column=source_column obj=object %}">{{ column_value }}</a>
{% else %}
{{ column_value }}
{% endif %}
</td>
{% endif %}
{% endif %}
@@ -153,6 +170,7 @@
</tbody>
</table>
</div>
{% include 'pagination/pagination.html' %}
</div>
{% endif %}
</div>

View File

@@ -1,7 +1,4 @@
<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 %}

View File

@@ -1,8 +1 @@
{% 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 %}
<i class="fa fa-{{ symbol }}" style="padding-right: 5px; width: auto;"></i>

View File

@@ -1,28 +0,0 @@
{% load i18n %}
{% load static %}
{% load common_tags %}
{% load navigation_tags %}
{% if object_list %}
<h4>
{% if page_obj %}
{% if page_obj.paginator.num_pages != 1 %}
{% blocktrans with page_obj.start_index as start and page_obj.end_index as end and page_obj.paginator.object_list|length as total and page_obj.number as page_number and page_obj.paginator.num_pages as total_pages %}Total ({{ start }} - {{ end }} out of {{ total }}) (Page {{ page_number }} of {{ total_pages }}){% endblocktrans %}
{% else %}
{% blocktrans with page_obj.paginator.object_list|length as total %}Total: {{ total }}{% endblocktrans %}
{% endif %}
{% else %}
{% blocktrans with object_list|length as total %}Total: {{ total }}{% endblocktrans %}
{% endif %}
</h4>
<hr>
{% if not hide_multi_item_actions %}
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
{% endif %}
{% endif %}
<div class="clearfix">
{% include 'appearance/list_toolbar.html' %}
</div>

View File

@@ -1,90 +0,0 @@
{% load i18n %}
{% load common_tags %}
{% load navigation_tags %}
{% if is_paginated or links_multi_menus_results %}
<div class="well center-block toolbar">
{% endif %}
{% if links_multi_menus_results %}
<div class="pull-left">
<div class="btn-toolbar" role="toolbar" style="margin-right: 10px;">
<div class="btn-group">
<a class="btn btn-default btn-sm check-all" data-checked=false data-icon-checked="fa fa-check-square" data-icon-unchecked="far fa-square" title="{% trans 'Select/Deselect all' %}">
<i class="far fa-square"></i>
</a>
</div>
</div>
</div>
{% endif %}
{% if is_paginated %}
<div class="pull-left">
<div class="btn-toolbar" role="toolbar">
<div class="btn-group">
{% if page_obj.has_previous %}
<a class="btn btn-default btn-sm" href="?{{ page_obj.previous_page_number.querystring }}">&lsaquo;&lsaquo;</a>
{% else %}
<a class="btn btn-default btn-sm disabled" href="#">&lsaquo;&lsaquo;</a>
{% endif %}
{% for page in page_obj.pages %}
{% if page %}
{% ifequal page page_obj.number %}
<a class="active btn btn-default btn-sm pagination-disabled" href="#">{{ page }}</a>
{% else %}
<a class="btn btn-default btn-sm" href="?{{ page.querystring }}">{{ page }}</a>
{% endifequal %}
{% else %}
<a class="btn btn-default btn-sm disabled" href="#">...</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<a class="btn btn-default btn-sm" href="?{{ page_obj.next_page_number.querystring }}">&rsaquo;&rsaquo;</a>
{% else %}
<a class="btn btn-default btn-sm disabled" href="#">&rsaquo;&rsaquo;</a>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% if links_multi_menus_results %}
<p class="pull-right" id="multi-item-title" style="line-height: 16px; padding-top: 8px;">{% trans 'Select items to activate bulk actions. Use Shift + click to select many.' %}</p>
<div class="pull-right btn-group" id="multi-item-actions" style="display: none;">
<button aria-expanded="true" class="btn btn-danger btn-sm dropdown-toggle" data-toggle="dropdown" type="button">
{% trans 'Bulk actions' %}
<span class="caret"></span>
<span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
</button>
<ul class="dropdown-menu" role="menu">
{% for multi_item_menu_results in links_multi_menus_results %}
{% for link_group in multi_item_menu_results.link_groups %}
{% with link_group.links as object_navigation_links %}
{% with 'true' as as_li %}
{% with 'true' as hide_active_anchor %}
{% with 'btn-sm btn-multi-item-action' as link_classes %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endfor %}
{% if not forloop.last and link_group %}
<li class="divider"></li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}
{% if is_paginated or links_multi_menus_results %}
<div class="clearfix"></div>
</div>
{% endif %}

View File

@@ -3,11 +3,10 @@
{% load navigation_tags %}
{% load smart_settings_tags %}
{% spaceless %}
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<button aria-expanded="false" aria-controls="navbar" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" type="button">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">{% trans 'Toggle navigation' %}</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
@@ -15,10 +14,9 @@
</button>
<a class="navbar-brand" href="{% url home_view %}">{% smart_setting 'COMMON_PROJECT_TITLE' %}</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
{% navigation_resolve_menu name='topbar' as topbar_menus_results %}
<ul class="nav navbar-nav">
{% navigation_resolve_menu name='main' as topbar_menus_results %}
{% for tobpar_menu_result in topbar_menus_results %}
{% for link_group in tobpar_menu_result.link_groups %}
{% for link in link_group.links %}
@@ -36,8 +34,24 @@
{% endfor %}
{% endfor %}
{% endfor %}
{% get_menu_links name='main' as menu_links %}
{% for link_set in menu_links %}
{% for link in link_set %}
{% with 'true' as as_li %}
{% with 'true' as hide_active_anchor %}
{% with 'active' as li_class_active %}
{% with 'first' as li_class_first %}
{% with ' ' as link_classes %}
{% include 'navigation/generic_subnavigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endfor %}
{% endfor %}
</ul>
</div>
</div>
</nav>
{% endspaceless %}

View File

@@ -1,70 +0,0 @@
{% load i18n %}
{% load navigation_tags %}
{% load smart_settings_tags %}
{% load common_tags %}
{% load navigation_tags %}
{% spaceless %}
<div class="panel-group" id="accordion-sidebar" role="tablist" aria-multiselectable="true">
{% navigation_resolve_menu name='main' as main_menus_results %}
{% for main_menu_results in main_menus_results %}
{% for link_group in main_menu_results.link_groups %}
{% for link in link_group.links %}
{% with 'active' as li_class_active %}
{% with ' ' as link_classes %}
{% if link|get_type == "<class 'mayan.apps.navigation.classes.Menu'>" %}
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title">
<a class="non-ajax collapsed" role="button" data-toggle="collapse" data-parent="#accordion-sidebar" href="#accordion-body-{{ forloop.counter }}" aria-expanded="false" aria-controls="collapseOne">
<div class="pull-left">
{% if link.icon %}
<i class="hidden-xs hidden-sm hidden-md {{ link.icon }}"></i>
{% endif %}
{% if link.icon_class %}{{ link.icon_class.render }}{% endif %}
{{ link.label }}
</div>
<div class="accordion-indicator pull-right"><span class="caret"></span></div>
<div class="clearfix"></div>
</a>
</h4>
</div>
<div id="accordion-body-{{ forloop.counter }}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingOne">
<div class="panel-body">
<ul class="list-unstyled">
{% navigation_resolve_menu name=link.name as sub_menus_results %}
{% for sub_menu_results in sub_menus_results %}
{% for link_group in sub_menu_results.link_groups %}
{% with '' as link_class_active %}
{% with 'a-main-menu-accordion-link' as link_classes %}
{% with 'true' as as_li %}
{% with link_group.links as object_navigation_links %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endfor %}
{% endfor %}
</ul>
</div>
</div>
</div>
{% else %}
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title">
{% include 'navigation/generic_link_instance.html' %}
</h4>
</div>
</div>
{% endif %}
{% endwith %}
{% endwith %}
{% endfor %}
{% endfor %}
{% endfor %}
</div>
{% endspaceless %}

View File

@@ -31,11 +31,8 @@
{% if appearance_type == 'plain' %}
{% block content_plain %}{% endblock %}
{% else %}
<div id="menu-topbar">
{% include 'appearance/menu_topbar.html' %}
</div>
<div id="menu-main">
{% include 'appearance/menu_main.html' %}
{% include 'appearance/main_menu.html' %}
</div>
<div class="main">
<div class="body-spacer"></div>
@@ -105,7 +102,6 @@
ajaxMenusOptions: [
{
callback: function (options) {
MayanApp.updateNavbarState();
options.app.doBodyAdjust();
},
interval: 5000,
@@ -113,14 +109,7 @@
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 () {

View File

@@ -11,7 +11,7 @@
{% if page %}
{% ifequal page page_obj.number %}
<li class="active"><a class="pagination-disabled" href="#">{{ page }}</a></li>
<li class="active"><a class="disabled" href="#">{{ page }}</a></li>
{% else %}
<li><a href="?{{ page.querystring }}">{{ page }}</a></li>
{% endifequal %}

View File

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

View File

@@ -0,0 +1,19 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.events.classes import EventTypeNamespace
namespace = EventTypeNamespace(
label=_('Authentication'), name='authentication'
)
event_user_authentication_error = namespace.add_event_type(
label=_('User authentication error'), name='user_authentication_error'
)
event_user_password_reset_started = namespace.add_event_type(
label=_('User password reset started'), name='user_password_reset_started'
)
event_user_password_reset_complete = namespace.add_event_type(
label=_('User password reset complete'), name='user_password_reset_complete'
)

View File

@@ -0,0 +1,82 @@
from __future__ import unicode_literals
from django.conf import settings
from django.contrib.auth.views import (
INTERNAL_RESET_SESSION_TOKEN, INTERNAL_RESET_URL_TOKEN,
)
from django.core import mail
from actstream.models import Action
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.events.utils import create_system_user
from ..events import (
event_user_authentication_error, event_user_password_reset_complete,
event_user_password_reset_started
)
class AuthenticationEventsTestCase(GenericViewTestCase):
auto_login_user = False
def setUp(self):
super(AuthenticationEventsTestCase, self).setUp()
create_system_user()
def test_user_authentication_failure_event(self):
Action.objects.all().delete()
response = self.post(viewname=settings.LOGIN_URL)
self.assertEqual(response.status_code, 200)
action = Action.objects.last()
self.assertEqual(action.verb, event_user_authentication_error.id)
def test_user_password_reset_started_event(self):
Action.objects.all().delete()
response = self.post(
viewname='authentication:password_reset_view', data={
'email': self._test_case_user.email,
}
)
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 1)
action = Action.objects.last()
self.assertEqual(action.verb, event_user_password_reset_started.id)
def test_user_password_reset_complete_event(self):
response = self.post(
viewname='authentication:password_reset_view', data={
'email': self._test_case_user.email,
}
)
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 1)
email_parts = mail.outbox[0].body.replace('\n', '').split('/')
uidb64 = email_parts[-3]
token = email_parts[-2]
# Add the token to the session
session = self.client.session
session[INTERNAL_RESET_SESSION_TOKEN] = token
session.save()
Action.objects.all().delete()
new_password = 'new_password_123'
response = self.post(
viewname='authentication:password_reset_confirm_view',
kwargs={'uidb64': uidb64, 'token': INTERNAL_RESET_URL_TOKEN}, data={
'new_password1': new_password,
'new_password2': new_password
}
)
self.assertNotIn(INTERNAL_RESET_SESSION_TOKEN, self.client.session)
action = Action.objects.last()
self.assertEqual(action.verb, event_user_password_reset_complete.id)

View File

@@ -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.base import GenericViewTestCase
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.user_management.permissions import permission_user_edit
from mayan.apps.user_management.tests.literals import TEST_USER_PASSWORD_EDITED

View File

@@ -21,8 +21,13 @@ from mayan.apps.common.generics import MultipleObjectFormActionView
from mayan.apps.common.settings import (
setting_home_view, setting_project_title, setting_project_url
)
from mayan.apps.events.utils import get_system_user
from mayan.apps.user_management.permissions import permission_user_edit
from .events import (
event_user_authentication_error, event_user_password_reset_complete,
event_user_password_reset_started
)
from .forms import EmailAuthenticationForm, UsernameAuthenticationForm
from .settings import setting_login_method, setting_maximum_session_length
@@ -57,6 +62,10 @@ class MayanLoginView(StrongholdPublicMixin, LoginView):
return result
def form_invalid(self, form):
event_user_authentication_error.commit(actor=get_system_user())
return super(MayanLoginView, self).form_invalid(form=form)
def get_form_class(self):
if setting_login_method.value == 'email':
return EmailAuthenticationForm
@@ -112,6 +121,10 @@ class MayanPasswordResetConfirmView(StrongholdPublicMixin, PasswordResetConfirmV
)
template_name = 'authentication/password_reset_confirm.html'
def post(self, *args, **kwargs):
event_user_password_reset_complete.commit(actor=get_system_user())
return super(MayanPasswordResetConfirmView, self).post(*args, **kwargs)
class MayanPasswordResetDoneView(StrongholdPublicMixin, PasswordResetDoneView):
extra_context = {
@@ -137,6 +150,10 @@ class MayanPasswordResetView(StrongholdPublicMixin, PasswordResetView):
)
template_name = 'authentication/password_reset_form.html'
def post(self, *args, **kwargs):
event_user_password_reset_started.commit(actor=get_system_user())
return super(MayanPasswordResetView, self).post(*args, **kwargs)
class UserSetPasswordView(MultipleObjectFormActionView):
form_class = SetPasswordForm

View File

@@ -4,23 +4,21 @@
{% if autoadmin_properties.account %}
<div class="row">
<div class="col-xs-12 col-sm-10 col-sm-offset-1 col-md-10 col-md-offset-1 col-lg-6 col-lg-offset-3">
<div class="col-xs-10 col-xs-offset-1 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-4 col-lg-offset-4">
<br>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="text-center panel-title">{% trans 'Automatic credentials' %}</h3>
<h3 class="panel-title">{% trans 'First time login' %}</h3>
</div>
<div class="panel-body">
<div class="content login">
{% smart_setting 'COMMON_PROJECT_TITLE' as project_title %}
<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>
<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>
</div>
</div>
</div>

View File

@@ -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 = 'Automatic credentials'
TEST_FIRST_TIME_LOGIN_TEXT = 'First time login'
TEST_MOCK_VIEW_TEXT = 'mock view text'

View File

@@ -2,24 +2,20 @@ 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(BaseTestCase):
create_test_case_user = False
class AutoAdminManagementCommandTestCase(TestCase):
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()

View File

@@ -2,7 +2,8 @@ from __future__ import unicode_literals
import logging
from mayan.apps.common.tests.base import BaseTestCase
from django.test import TestCase
from mayan.apps.common.tests.utils import mute_stdout
from ..models import AutoAdminSingleton
@@ -11,7 +12,7 @@ from ..settings import setting_username
from .literals import TEST_ADMIN_USER_PASSWORD
class AutoAdminHandlerTestCase(BaseTestCase):
class AutoAdminHandlerTestCase(TestCase):
def test_post_admin_creation(self):
logging.disable(logging.INFO)

View File

@@ -1,7 +1,7 @@
from __future__ import unicode_literals
from mayan.apps.common.settings import setting_home_view
from mayan.apps.common.tests.base import GenericViewTestCase
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.common.tests.utils import mute_stdout
from ..models import AutoAdminSingleton

View File

@@ -10,14 +10,12 @@ from mayan.apps.common.menus import (
menu_facet, menu_list_facet, menu_main, menu_multi_item, menu_object,
menu_secondary
)
from mayan.apps.documents.search import (
document_page_search, document_search, document_version_page_search
)
from mayan.apps.events.classes import ModelEventType
from mayan.apps.events.links import (
link_events_for_object, link_object_event_types_user_subcriptions_list,
)
from mayan.apps.events.permissions import permission_events_view
from mayan.apps.documents.search import document_page_search, document_search
from mayan.apps.navigation.classes import SourceColumn
from .dependencies import * # NOQA
@@ -117,16 +115,12 @@ class CabinetsApp(MayanAppConfig):
)
document_page_search.add_model_field(
field='document__cabinets__label',
field='document_version__document__cabinets__label',
label=_('Cabinets')
)
document_search.add_model_field(
field='cabinets__label', label=_('Cabinets')
)
document_version_page_search.add_model_field(
field='document_version__document__cabinets__label',
label=_('Cabinets')
)
menu_facet.bind_links(
links=(link_document_cabinet_list,), sources=(Document,)

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.22 on 2019-07-29 02:36
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cabinets', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='cabinet',
name='label',
field=models.CharField(help_text='A short text used to identify the cabinet.', max_length=128, verbose_name='Label'),
),
]

View File

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

View File

@@ -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.mixins import DocumentTestMixin
from mayan.apps.rest_api.tests.base import BaseAPITestCase
from mayan.apps.documents.tests import DocumentTestMixin
from mayan.apps.rest_api.tests import BaseAPITestCase
from ..models import Cabinet
from ..permissions import (

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from actstream.models import Action
from mayan.apps.common.tests.base import GenericViewTestCase
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.documents.tests.test_models import GenericDocumentTestCase
from ..events import (

View File

@@ -2,8 +2,8 @@ from __future__ import unicode_literals
from django.core.exceptions import ValidationError
from mayan.apps.common.tests.base import BaseTestCase
from mayan.apps.documents.tests.mixins import DocumentTestMixin
from mayan.apps.common.tests import BaseTestCase
from mayan.apps.documents.tests import DocumentTestMixin
from ..models import Cabinet

View File

@@ -1,8 +1,8 @@
from __future__ import absolute_import, unicode_literals
from mayan.apps.common.tests.base import GenericViewTestCase
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.documents.tests.base import GenericDocumentViewTestCase
from mayan.apps.documents.tests import GenericDocumentViewTestCase
from ..models import Cabinet
from ..permissions import (

View File

@@ -2,8 +2,9 @@ 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.base import GenericDocumentViewTestCase
from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH
from mayan.apps.documents.tests import (
GenericDocumentViewTestCase, 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
@@ -33,7 +34,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:document_upload_interactive', kwargs={
viewname='sources:upload_interactive', kwargs={
'source_id': self.test_source.pk
}, data={
'document_type_id': self.test_document_type.pk,

View File

@@ -1,6 +1,6 @@
from __future__ import unicode_literals
from mayan.apps.common.tests.base import GenericViewTestCase
from mayan.apps.common.tests 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:workflow_template_state_action_create',
viewname='document_states:setup_workflow_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:workflow_template_state_action_create',
viewname='document_states:setup_workflow_state_action_create',
kwargs={
'pk': self.test_workflow_state.pk,
'class_path': 'mayan.apps.cabinets.workflow_actions.CabinetRemoveAction'

View File

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

View File

@@ -6,12 +6,9 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.classes import ModelPermission
from mayan.apps.common.apps import MayanAppConfig
from mayan.apps.common.menus import (
menu_facet, menu_main, menu_multi_item, menu_secondary
)
from mayan.apps.common.menus import menu_facet, menu_main, menu_secondary
from mayan.apps.dashboards.dashboards import dashboard_main
from mayan.apps.events.classes import ModelEventType
from mayan.apps.navigation.classes import SourceColumn
from .dashboard_widgets import DashboardWidgetTotalCheckouts
from .events import (
@@ -20,9 +17,8 @@ from .events import (
)
from .handlers import handler_check_new_version_creation
from .links import (
link_check_in_document, link_check_in_document_multiple,
link_check_out_document, link_check_out_document_multiple,
link_check_out_info, link_check_out_list
link_check_in_document, link_check_out_document, link_check_out_info,
link_check_out_list
)
from .methods import (
method_check_in, method_get_check_out_info, method_get_check_out_state,
@@ -47,8 +43,6 @@ class CheckoutsApp(MayanAppConfig):
def ready(self):
super(CheckoutsApp, self).ready()
CheckedOutDocument = self.get_model(model_name='CheckedOutDocument')
DocumentCheckout = self.get_model(model_name='DocumentCheckout')
Document = apps.get_model(
app_label='documents', model_name='Document'
)
@@ -82,22 +76,6 @@ class CheckoutsApp(MayanAppConfig):
permission_document_check_out_detail_view
)
)
ModelPermission.register_inheritance(
model=DocumentCheckout, related='document'
)
SourceColumn(
attribute='get_user_display', include_label=True, order=99,
source=CheckedOutDocument
)
SourceColumn(
attribute='get_checkout_datetime', include_label=True, order=99,
source=CheckedOutDocument
)
SourceColumn(
attribute='get_checkout_expiration', include_label=True, order=99,
source=CheckedOutDocument
)
dashboard_main.add_widget(
widget=DashboardWidgetTotalCheckouts, order=-1
@@ -107,22 +85,6 @@ class CheckoutsApp(MayanAppConfig):
links=(link_check_out_info,), sources=(Document,)
)
menu_main.bind_links(links=(link_check_out_list,), position=98)
menu_multi_item.bind_links(
links=(
link_check_in_document_multiple,
), sources=(CheckedOutDocument,)
)
menu_multi_item.bind_links(
links=(
link_check_in_document_multiple,
link_check_out_document_multiple,
), sources=(Document,)
)
menu_multi_item.unbind_links(
links=(
link_check_out_document_multiple,
), sources=(CheckedOutDocument,)
)
menu_secondary.bind_links(
links=(link_check_out_document, link_check_in_document),
sources=(

View File

@@ -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 DocumentCheckOutDetailForm(DetailForm):
class DocumentCheckoutDefailForm(DetailForm):
def __init__(self, *args, **kwargs):
instance = kwargs['instance']
@@ -56,7 +56,7 @@ class DocumentCheckOutDetailForm(DetailForm):
)
kwargs['extra_fields'] = extra_fields
super(DocumentCheckOutDetailForm, self).__init__(*args, **kwargs)
super(DocumentCheckoutDefailForm, self).__init__(*args, **kwargs)
class Meta:
fields = ()

View File

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

View File

@@ -6,7 +6,6 @@ from django.apps import apps
from django.db import models, transaction
from django.utils.timezone import now
from mayan.apps.acls.models import AccessControlList
from mayan.apps.documents.models import Document
from .events import (
@@ -15,58 +14,10 @@ from .events import (
)
from .exceptions import DocumentNotCheckedOut
from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN
from .permissions import (
permission_document_check_in, permission_document_check_in_override
)
logger = logging.getLogger(__name__)
class DocumentCheckoutBusinessLogicManager(models.Manager):
def check_in_document(self, document, user=None):
# 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:
@@ -76,6 +27,25 @@ class DocumentCheckoutManager(models.Manager):
else:
return not check_out_info.block_new_version
def check_in_document(self, document, user=None):
try:
document_check_out = self.model.objects.get(document=document)
except self.model.DoesNotExist:
raise DocumentNotCheckedOut
else:
with transaction.atomic():
if user:
if self.get_check_out_info(document=document).user != user:
event_document_forceful_check_in.commit(
actor=user, target=document
)
else:
event_document_check_in.commit(actor=user, target=document)
else:
event_document_auto_check_in.commit(target=document)
document_check_out.delete()
def check_in_expired_check_outs(self):
for document in self.expired_check_outs():
document.check_in()
@@ -87,11 +57,7 @@ class DocumentCheckoutManager(models.Manager):
)
def checked_out_documents(self):
CheckedOutDocument = apps.get_model(
app_label='checkouts', model_name='CheckedOutDocument'
)
return CheckedOutDocument.objects.filter(
return Document.objects.filter(
pk__in=self.model.objects.values('document__id')
)
@@ -108,11 +74,7 @@ class DocumentCheckoutManager(models.Manager):
return STATE_CHECKED_IN
def expired_check_outs(self):
CheckedOutDocument = apps.get_model(
app_label='checkouts', model_name='CheckedOutDocument'
)
expired_list = CheckedOutDocument.objects.filter(
expired_list = Document.objects.filter(
pk__in=self.model.objects.filter(
expiration_datetime__lte=now()
).values_list('document__pk', flat=True)
@@ -121,6 +83,9 @@ class DocumentCheckoutManager(models.Manager):
return expired_list
def get_by_natural_key(self, document_natural_key):
Document = apps.get_model(
app_label='documents', model_name='Document'
)
try:
document = Document.objects.get_by_natural_key(document_natural_key)
except Document.DoesNotExist:

View File

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

View File

@@ -14,10 +14,7 @@ from mayan.apps.documents.models import Document
from .events import event_document_check_out
from .exceptions import DocumentAlreadyCheckedOut
from .managers import (
DocumentCheckoutBusinessLogicManager, DocumentCheckoutManager,
NewVersionBlockManager
)
from .managers import DocumentCheckoutManager, NewVersionBlockManager
logger = logging.getLogger(__name__)
@@ -52,7 +49,6 @@ class DocumentCheckout(models.Model):
)
objects = DocumentCheckoutManager()
business_logic = DocumentCheckoutBusinessLogicManager()
class Meta:
ordering = ('pk',)
@@ -85,13 +81,13 @@ class DocumentCheckout(models.Model):
natural_key.dependencies = ['documents.Document']
def save(self, *args, **kwargs):
is_new = not self.pk
if not is_new or self.document.is_checked_out():
new_checkout = not self.pk
if not new_checkout or self.document.is_checked_out():
raise DocumentAlreadyCheckedOut
with transaction.atomic():
result = super(DocumentCheckout, self).save(*args, **kwargs)
if is_new:
if new_checkout:
event_document_check_out.commit(
actor=self.user, target=self.document
)
@@ -123,24 +119,3 @@ class NewVersionBlock(models.Model):
def natural_key(self):
return self.document.natural_key()
natural_key.dependencies = ['documents.Document']
class CheckedOutDocument(Document):
class Meta:
proxy = True
def get_user_display(self):
check_out_info = self.get_check_out_info()
return check_out_info.user.get_full_name() or check_out_info.user
get_user_display.short_description = _('User')
def get_checkout_datetime(self):
return self.get_check_out_info().checkout_datetime
get_checkout_datetime.short_description = _('Checkout time and date')
def get_checkout_expiration(self):
return self.get_check_out_info().expiration_datetime
get_checkout_expiration.short_description = _('Checkout expiration')

View File

@@ -5,7 +5,6 @@ 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
@@ -65,13 +64,16 @@ class DocumentCheckoutViewTestMixin(object):
}
)
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_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')
def _request_test_document_check_out_view(self):
return self.post(
viewname='checkouts:check_out_document', kwargs={
@@ -82,23 +84,3 @@ 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')

View File

@@ -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.documents.tests.mixins import DocumentTestMixin
from mayan.apps.rest_api.tests.base import BaseAPITestCase
from mayan.apps.rest_api.tests import BaseAPITestCase
from ..models import DocumentCheckout
from ..permissions import (

View File

@@ -1,6 +1,6 @@
from __future__ import unicode_literals
from mayan.apps.documents.tests.base import GenericDocumentViewTestCase
from mayan.apps.documents.tests import GenericDocumentViewTestCase
from ..links import link_check_out_document, link_check_out_info
from ..permissions import (

View File

@@ -2,10 +2,11 @@ from __future__ import unicode_literals
import time
from mayan.apps.common.tests.base import BaseTestCase
from mayan.apps.documents.tests.base import GenericDocumentTestCase
from mayan.apps.common.tests import BaseTestCase
from mayan.apps.documents.tests import (
GenericDocumentTestCase, DocumentTestMixin
)
from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH
from mayan.apps.documents.tests.mixins import DocumentTestMixin
from ..exceptions import (
DocumentAlreadyCheckedOut, DocumentNotCheckedOut,
@@ -52,7 +53,7 @@ class DocumentCheckoutTestCase(
block_new_version=True
)
def test_document_check_in_without_check_out(self):
def test_document_checkin_without_checkout(self):
with self.assertRaises(DocumentNotCheckedOut):
self.test_document.check_in()

View File

@@ -1,7 +1,7 @@
from __future__ import unicode_literals
from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.documents.tests.base import GenericDocumentViewTestCase
from mayan.apps.documents.tests 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.assertNotContains(
response=response, text=self.test_document.label, status_code=404
self.assertContains(
response=response, text=self.test_document.label, status_code=200
)
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, 404)
self.assertEqual(response.status_code, 403)
self.assertTrue(self.test_document.is_checked_out())
@@ -67,89 +67,9 @@ 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, 404)
self.assertEqual(response.status_code, 403)
self.assertFalse(self.test_document.is_checked_out())
@@ -167,102 +87,6 @@ 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()
@@ -332,18 +156,19 @@ class DocumentCheckoutViewTestCase(
'pk': self.test_document.pk
}
)
self.assertEqual(response.status_code, 302)
self.assertContains(
response=response, text='Insufficient permissions', status_code=403
)
self.assertTrue(self.test_document.is_checked_out())
def test_document_check_in_forcefull_view_with_access(self):
def test_document_check_in_forcefull_view_with_permission(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
@@ -376,7 +201,7 @@ class NewVersionBlockViewTestCase(
self.login_superuser()
response = self.post(
viewname='sources:document_version_upload', kwargs={
viewname='sources:upload_version', kwargs={
'document_pk': self.test_document.pk
}, follow=True
)
@@ -394,8 +219,6 @@ 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)

View File

@@ -4,8 +4,8 @@ from django.conf.urls import url
from .api_views import APICheckedoutDocumentListView, APICheckedoutDocumentView
from .views import (
DocumentCheckInView, DocumentCheckOutDetailView, DocumentCheckOutListView,
DocumentCheckOutView
DocumentCheckOutView, DocumentCheckOutDetailView, DocumentCheckOutListView,
DocumentCheckInView
)
urlpatterns = [
@@ -14,25 +14,16 @@ urlpatterns = [
name='check_out_list'
),
url(
regex=r'^documents/(?P<pk>\d+)/check/in/$',
view=DocumentCheckInView.as_view(), name='check_in_document'
regex=r'^documents/(?P<pk>\d+)/check/out/$', view=DocumentCheckOutView.as_view(),
name='check_out_document'
),
url(
regex=r'^documents/multiple/check/in/$',
name='check_in_document_multiple', view=DocumentCheckInView.as_view()
regex=r'^documents/(?P<pk>\d+)/check/in/$', view=DocumentCheckInView.as_view(),
name='check_in_document'
),
url(
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'
regex=r'^documents/(?P<pk>\d+)/check/info/$', view=DocumentCheckOutDetailView.as_view(),
name='check_out_info'
),
]

View File

@@ -1,17 +1,21 @@
from __future__ import absolute_import, unicode_literals
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _, ungettext
from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.generics import (
MultipleObjectConfirmActionView, MultipleObjectFormActionView,
SingleObjectDetailView
ConfirmView, SingleObjectCreateView, SingleObjectDetailView
)
from mayan.apps.common.utils import encapsulate
from mayan.apps.documents.models import Document
from mayan.apps.documents.views import DocumentListView
from .forms import DocumentCheckOutForm, DocumentCheckOutDetailForm
from .exceptions import DocumentAlreadyCheckedOut, DocumentNotCheckedOut
from .forms import DocumentCheckoutForm, DocumentCheckoutDefailForm
from .icons import icon_check_out_info
from .models import DocumentCheckout
from .permissions import (
@@ -20,125 +24,66 @@ from .permissions import (
)
class DocumentCheckInView(MultipleObjectConfirmActionView):
error_message = 'Unable to check in document "%(instance)s". %(exception)s'
model = Document
pk_url_kwarg = 'pk'
success_message_singular = '%(count)d document checked in.'
success_message_plural = '%(count)d documents checked in.'
class DocumentCheckInView(ConfirmView):
def get_extra_context(self):
queryset = self.get_object_list()
document = self.get_object()
result = {
'title': ungettext(
singular='Check in %(count)d document',
plural='Check in %(count)d documents',
number=queryset.count()
) % {
'count': queryset.count(),
}
context = {
'object': document,
}
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Check in document: %s'
) % queryset.first()
}
)
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]}
)
if document.get_check_out_info().user != self.request.user:
context['title'] = _(
'You didn\'t originally checked out this document. '
'Forcefully check in the document: %s?'
) % document
else:
super(DocumentCheckInView, self).get_post_action_redirect()
context['title'] = _('Check in the document: %s?') % document
def get_source_queryset(self):
# object_permission is None to disable restricting queryset mixin
# and restrict the queryset ourselves from two permissions
return context
source_queryset = super(DocumentCheckInView, self).get_source_queryset()
def get_object(self):
return get_object_or_404(klass=Document, pk=self.kwargs['pk'])
check_in_queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_check_in, queryset=source_queryset,
user=self.request.user
)
check_in_override_queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_check_in_override,
queryset=source_queryset, user=self.request.user
)
return check_in_queryset | check_in_override_queryset
def object_action(self, form, instance):
DocumentCheckout.business_logic.check_in_document(
document=instance, user=self.request.user
)
class DocumentCheckOutView(MultipleObjectFormActionView):
error_message = 'Unable to checkout document "%(instance)s". %(exception)s'
form_class = DocumentCheckOutForm
model = Document
object_permission = permission_document_check_out
pk_url_kwarg = 'pk'
success_message_singular = '%(count)d document checked out.'
success_message_plural = '%(count)d documents checked out.'
def get_extra_context(self):
queryset = self.get_object_list()
result = {
'title': ungettext(
singular='Checkout %(count)d document',
plural='Checkout %(count)d documents',
number=queryset.count()
) % {
'count': queryset.count(),
def get_post_action_redirect(self):
return reverse(
viewname='checkouts:check_out_info', kwargs={
'pk': self.get_object().pk
}
}
)
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Check out document: %s'
) % queryset.first()
}
)
def view_action(self):
document = self.get_object()
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]}
if document.get_check_out_info().user == self.request.user:
AccessControlList.objects.check_access(
obj=document, permissions=(permission_document_check_in,),
user=self.request.user
)
else:
super(DocumentCheckOutView, self).get_post_action_redirect()
AccessControlList.objects.check_access(
obj=document,
permissions=(permission_document_check_in_override,),
user=self.request.user
)
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,
)
try:
document.check_in(user=self.request.user)
except DocumentNotCheckedOut:
messages.error(
message=_('Document has not been checked out.'),
request=self.request
)
else:
messages.success(
message=_(
'Document "%s" checked in successfully.'
) % document, request=self.request
)
class DocumentCheckOutDetailView(SingleObjectDetailView):
form_class = DocumentCheckOutDetailForm
form_class = DocumentCheckoutDefailForm
model = Document
object_permission = permission_document_check_out_detail_view
@@ -151,6 +96,55 @@ 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(
@@ -163,13 +157,34 @@ 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 operations '
'for a predetermined amount of time.'
'Checking out a document blocks certain document '
'operations for a predetermined amount of '
'time.'
),
'no_results_title': _('No documents have been checked out'),
'title': _('Checked out documents'),
'title': _('Documents checked out'),
}
)
return context

View File

@@ -27,7 +27,9 @@ from .links import (
)
from .literals import MESSAGE_SQLITE_WARNING
from .menus import menu_about, menu_secondary, menu_topbar, menu_user
from .menus import (
menu_about, menu_main, menu_secondary, menu_user
)
from .settings import (
setting_auto_logging, setting_production_error_log_path,
setting_production_error_logging
@@ -95,10 +97,7 @@ class CommonApp(MayanAppConfig):
)
Template(
name='menu_main', template_name='appearance/menu_main.html'
)
Template(
name='menu_topbar', template_name='appearance/menu_topbar.html'
name='menu_main', template_name='appearance/main_menu.html'
)
menu_user.bind_links(
@@ -113,7 +112,7 @@ class CommonApp(MayanAppConfig):
)
)
menu_topbar.bind_links(links=(menu_about, menu_user,), position=99)
menu_main.bind_links(links=(menu_about, menu_user,), position=99)
menu_secondary.bind_links(
links=(link_object_error_list_clear,), sources=(
'common:object_error_list',

View File

@@ -61,9 +61,102 @@ PythonDependency(
SOFTWARE.
''', module=__name__, name='PyYAML', version_string='==5.1.1'
)
PythonDependency(
copyright_text='''
Copyright (c) 2015 Ask Solem & contributors. All rights reserved.
Copyright (c) 2012-2014 GoPivotal, Inc. All rights reserved.
Copyright (c) 2009, 2010, 2011, 2012 Ask Solem, and individual contributors. All rights reserved.
Celery is licensed under The BSD License (3 Clause, also known as
the new BSD license). The license is an OSI approved Open Source
license and is GPL-compatible(1).
The license text can also be found here:
http://www.opensource.org/licenses/BSD-3-Clause
License
=======
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Ask Solem, nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Ask Solem OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
Documentation License
=====================
The documentation portion of Celery (the rendered contents of the
"docs" directory of a software distribution or checkout) is supplied
under the Creative Commons Attribution-Noncommercial-Share Alike 3.0
United States License as described by
http://creativecommons.org/licenses/by-nc-sa/3.0/us/
Footnotes
=========
(1) A GPL-compatible license makes it possible to
combine Celery with other software that is released
under the GPL, it does not mean that we're distributing
Celery under the GPL license. The BSD license, unlike the GPL,
let you distribute a modified version without making your
changes open source.
''', module=__name__, name='celery', version_string='==3.1.24'
)
PythonDependency(
copyright_text='''
Copyright (c) 2012-2013 GoPivotal, Inc. All Rights Reserved.
Copyright (c) 2009-2012 Ask Solem. All Rights Reserved.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
Neither the name of Ask Solem nor the names of its contributors may be used
to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
''', module=__name__, name='django-celery', version_string='==3.2.1'
)
PythonDependency(
module=__name__, name='django-downloadview', version_string='==1.9'
)
PythonDependency(
module=__name__, name='django-environ', version_string='==0.4.5'
)
PythonDependency(
module=__name__, name='django-formtools', version_string='==2.1'
)
@@ -290,10 +383,6 @@ PythonDependency(
module=__name__, environment=environment_development, name='Werkzeug',
version_string='==0.15.4'
)
PythonDependency(
module=__name__, environment=environment_development, name='devpi-server',
version_string='==5.0.0'
)
PythonDependency(
environment=environment_development, module=__name__,
name='django-debug-toolbar', version_string='==1.11'

View File

@@ -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_name)
form_create_method = 'create_{}_form'.format(form_name)
form_kwargs = self.get_form_kwargs(form_name)
form_create_method = 'create_%s_form' % 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=form_classes)
self.forms = self.get_forms(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 = '{}_form_valid'.format(form_name)
form_valid_method = '%s_form_valid' % form_name
if hasattr(self, form_valid_method):
return getattr(self, form_valid_method)(form=form)
return getattr(self, form_valid_method)(form)
self.all_forms_valid(forms=forms)
self.all_forms_valid(forms)
return HttpResponseRedirect(redirect_to=self.get_success_url())
@@ -98,16 +98,14 @@ class MultiFormView(DjangoFormView):
def get_form_kwargs(self, form_name):
kwargs = {}
kwargs.update({'initial': self.get_initial(form_name=form_name)})
kwargs.update({'prefix': self.get_prefix(form_name=form_name)})
kwargs.update({'initial': self.get_initial(form_name)})
kwargs.update({'prefix': self.get_prefix(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 {})
@@ -120,13 +118,13 @@ class MultiFormView(DjangoFormView):
return dict(
[
(
key, self._create_form(form_name=key, klass=klass)
key, self._create_form(key, klass)
) for key, klass in form_classes.items()
]
)
def get_initial(self, form_name):
initial_method = 'get_{}_initial'.format(form_name)
initial_method = 'get_%s_initial' % form_name
if hasattr(self, initial_method):
return getattr(self, initial_method)()
else:
@@ -208,9 +206,9 @@ class AddRemoveView(
getattr(self.main_object, self.related_field).add(*queryset)
else:
raise ImproperlyConfigured(
'View {} must be called with a main_object_method_add, a '
'View %s must be called with a main_object_method_add, a '
'related_field, or an action_add '
'method.'.format(self.__class__.__name__)
'method.' % self.__class__.__name__
)
def _action_remove(self, queryset):
@@ -227,9 +225,9 @@ class AddRemoveView(
getattr(self.main_object, self.related_field).remove(*queryset)
else:
raise ImproperlyConfigured(
'View {} must be called with a main_object_method_remove, a '
'View %s must be called with a main_object_method_remove, a '
'related_field, or an action_remove '
'method.'.format(self.__class__.__name__)
'method.' % self.__class__.__name__
)
def dispatch(self, request, *args, **kwargs):
@@ -350,10 +348,8 @@ class AddRemoveView(
def get_list_added_queryset(self):
if not self.related_field:
raise ImproperlyConfigured(
'View {} must be called with either a related_field or '
'override .get_list_added_queryset().'.format(
self.__class__.__name__
)
'View %s must be called with either a related_field or '
'override .get_list_added_queryset().' % self.__class__.__name__
)
return self.get_secondary_object_list().filter(

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,107 @@
from __future__ import unicode_literals
import errno
import os
import warnings
from pathlib2 import Path
from django.conf import settings
from django.core import management
from django.core.management.base import CommandError
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from mayan.apps.documents.models import DocumentType
from mayan.apps.storage.utils import fs_cleanup
from ...literals import MESSAGE_DEPRECATION_WARNING
from ...warnings import DeprecationWarning
CONVERTDB_FOLDER = 'convertdb'
CONVERTDB_OUTPUT_FILENAME = 'migrate.json'
class Command(management.BaseCommand):
help = 'Convert from a database backend to another one.'
def __init__(self, *args, **kwargs):
warnings.warn(
category=DeprecationWarning,
message=force_text(MESSAGE_DEPRECATION_WARNING)
)
super(Command, self).__init__(*args, **kwargs)
def add_arguments(self, parser):
parser.add_argument(
'args', metavar='app_label[.ModelName]', nargs='*',
help=_(
'Restricts dumped data to the specified app_label or '
'app_label.ModelName.'
)
)
parser.add_argument(
'--from', action='store', default='default', dest='from',
help=_(
'The database from which data will be exported. If omitted '
'the database named "default" will be used.'
),
)
parser.add_argument(
'--to', action='store', default='default', dest='to',
help=_(
'The database to which data will be imported. If omitted '
'the database named "default" will be used.'
),
)
parser.add_argument(
'--force', action='store_true', dest='force',
help=_(
'Force the conversion of the database even if the receiving '
'database is not empty.'
),
)
def handle(self, *app_labels, **options):
# Create the media/convertdb folder
convertdb_folder_path = force_text(
Path(
settings.MEDIA_ROOT, CONVERTDB_FOLDER
)
)
try:
os.makedirs(convertdb_folder_path)
except OSError as exception:
if exception.errno == errno.EEXIST:
pass
convertdb_file_path = force_text(
Path(
convertdb_folder_path, CONVERTDB_OUTPUT_FILENAME
)
)
management.call_command(command_name='purgeperiodictasks')
management.call_command(
'dumpdata', *app_labels, all=True,
database=options['from'], natural_primary=True,
natural_foreign=True, output=convertdb_file_path,
interactive=False, format='json'
)
if DocumentType.objects.using(options['to']).count() and not options['force']:
fs_cleanup(convertdb_file_path)
raise CommandError(
'There is existing data in the database that will be '
'used for the import. If you proceed with the conversion '
'you might lose data. Please check your settings.'
)
management.call_command(
'loaddata', convertdb_file_path, database=options['to'], interactive=False,
verbosity=3
)
fs_cleanup(convertdb_file_path)

View File

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

View File

@@ -0,0 +1,10 @@
from __future__ import unicode_literals
SETTING_FILE_TEMPLATE = '''
from __future__ import absolute_import, unicode_literals
from .base import *
SECRET_KEY = '{0}'
'''

View File

@@ -11,8 +11,8 @@ class Command(management.BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
'--no-dependencies', action='store_true', dest='no_dependencies',
help='Don\'t download dependencies.',
'--no-javascript', action='store_true', dest='no_javascript',
help='Don\'t download the JavaScript dependencies.',
)
def handle(self, *args, **options):
@@ -25,9 +25,9 @@ class Command(management.BaseCommand):
)
)
if not options.get('no_dependencies', False):
if not options.get('no_javascript', False):
management.call_command(
command_name='installdependencies', interactive=False
command_name='installjavascript', interactive=False
)
try:

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