Compare commits

...

105 Commits

Author SHA1 Message Date
Roberto Rosario
36b89cf0ea Merge branch 'versions/minor' into features/workflow_email_action
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-13 02:56:02 -04:00
Roberto Rosario
6bcf35bef5 Add database conversion removal explanation
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 05:17:15 -04:00
Roberto Rosario
7ef6102876 Update release notes
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:52:34 -04:00
Roberto Rosario
4363bba0fe Remove encapsulate
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:50:37 -04:00
Roberto Rosario
e2f2181ebb Complete multiple check in/out support
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:49:39 -04:00
Roberto Rosario
d4f7e2cd16 Support creating multiple test users
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:49:09 -04:00
Roberto Rosario
058e36b4a9 Introspect proxy's parent only it is a model
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:48:00 -04:00
Roberto Rosario
1ddd5f26b1 Support menu inheritance
Proxy models will now inherit the menus from their parents.
Added to allow checked out documents to show multi item links
of their parents.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:40:48 -04:00
Roberto Rosario
44652d49fb Add test utility to return an id_list
Makes creating an id_list for testing from a list test instances
easier.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:39:48 -04:00
Roberto Rosario
119c1bde76 Add user test mixin to base test class
Allow tests to create test users.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:39:18 -04:00
Roberto Rosario
ed227b4111 Emphasize source column labels
Use the same CSS style as the view's extra_columns.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:38:06 -04:00
Roberto Rosario
c44090aca6 Initial commit to support multidocument checkouts
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-11 20:00:17 -04:00
Roberto Rosario
8a7da6a103 Update release notes closed issues
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-11 02:26:24 -04:00
Roberto Rosario
3e3b1f75a0 Remove django-environ
Work done in 9564db398f

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-11 02:02:45 -04:00
Roberto Rosario
1ab7b7b9b1 Backport FakeStorageSubclass from versions/next
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-11 01:56:06 -04:00
Roberto Rosario
3fab5c1427 Return empty dict if there is no config file
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-11 01:31:37 -04:00
Roberto Rosario
516c3aeb2c Add default for OCR backend argument setting
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-11 01:31:05 -04:00
Roberto Rosario
3ac1000b46 Merge remote-tracking branch 'origin/features/move_django_settings' into merge_features 2019-07-11 01:21:40 -04:00
Roberto Rosario
4adeefc978 Merge remote-tracking branch 'origin/features/move_yaml_code' into merge_features
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-11 01:21:23 -04:00
Roberto Rosario
8bc4b6a95e 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.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-10 19:35:42 -04:00
Roberto Rosario
37e85590e8 Move Django and Celery settings
Django settings now reside in the smart settings app.
Celery settings now reside in the task manager app.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-10 19:02:22 -04:00
Roberto Rosario
78a0189e1c Add YAML env variables support to platform app
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-10 00:34:09 -04:00
Roberto Rosario
91b0b2d9c3 Update smart setting's app URLs for uniformity
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-09 15:46:09 -04:00
Roberto Rosario
8a54deba3d Unify individual database configuration options
All database configuration is now done using MAYAN_DATABASES to
mirror Django way of doing database setup.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-09 15:45:30 -04:00
Roberto Rosario
22da1e0a78 Update import
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-09 15:43:39 -04:00
Roberto Rosario
c9668d62e5 Move mailer defaults to the literals module
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-09 15:43:15 -04:00
Roberto Rosario
7a01a77c43 Remove smart_settings * import
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-09 15:42:57 -04:00
Roberto Rosario
9564db398f Backport configuration file improvements
Remove support for quoted entried. Support unquoted entries. Support
custom location for the config files.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-09 15:40:20 -04:00
Roberto Rosario
7faa24eb7b Remove database conversion command
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 02:42:11 -04:00
Roberto Rosario
51f278301b Sort list of apps
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 02:40:24 -04:00
Roberto Rosario
2cc35c3c61 Remove outdated contrib scripts
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 02:37:58 -04:00
Roberto Rosario
8c73fda1ae Rename installjavascript to installdependencies
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 02:35:14 -04:00
Roberto Rosario
8811c8269f Rename document states apps view and URLs.
Object layout: WorkflowTemplate, WorkflowInstance, WorkflowRuntimeProxy,
WorkflowTemplateState, WorkflowTemplateTransition.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 02:21:58 -04:00
Roberto Rosario
f36f99c5fb Split workflow URL patterns
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 01:23:49 -04:00
Roberto Rosario
0e972eff06 Fix typos and PEP8 warnings
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 01:12:25 -04:00
Roberto Rosario
7913b5ddcc Sort dictionary entry
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 01:06:58 -04:00
Roberto Rosario
1c86ea5b5b Backport individual index rebuild support
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 01:03:39 -04:00
Roberto Rosario
ec6a3bd960 Move AJAX spinner to the left of the top bar
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 00:43:14 -04:00
Roberto Rosario
080553c797 Add trashed date time label and position
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 00:38:47 -04:00
Roberto Rosario
08ee07e652 Remove duplicated trashed document previews
Side effect of source column inheritance added in
06c3ef6583.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 00:37:47 -04:00
Roberto Rosario
d7d77fcb55 Backport workflow email action
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 00:27:29 -04:00
Roberto Rosario
601bff304f Merge branch 'versions/minor' into features/workflow_email_action
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 00:20:38 -04:00
Roberto Rosario
bb5324ef50 Encode settings YAML before hashing
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-07-06 17:14:44 -04:00
Roberto Rosario
4c212f6ea4 Backport workflow context and field support
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-06 04:13:26 -04:00
Roberto Rosario
941356ed69 Add a general use YAML validator
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-06 04:11:43 -04:00
Roberto Rosario
97804b255b Add and exclude Index instance columns
Exclude inherited columns from the Index models.
Add the label columns to Index instances.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-06 04:10:41 -04:00
Roberto Rosario
06c3ef6583 Add source column inheritance and exclusions
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-06 04:09:44 -04:00
Roberto Rosario
6cd857e2bf Use Select2 widget for the document type selection form
This was committed in 109fcba795 without
adding the actual change.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-06 02:44:00 -04:00
Roberto Rosario
fbb0f0b9bd Backport workflow preview refactor
GitLab issue #532.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-06 02:41:16 -04:00
Roberto Rosario
9e068c3e83 Add topbar shadow
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-06 02:01:48 -04:00
Roberto Rosario
72a3807354 Add vertical main menu
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-06 01:53:45 -04:00
Roberto Rosario
109fcba795 Use Select2 for the document type selection form
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-05 23:26:11 -04:00
Roberto Rosario
01380e0572 Merge branch 'versions/minor' of gitlab.com:mayan-edms/mayan-edms into versions/minor 2019-07-05 23:23:50 -04:00
Roberto Rosario
5146c6d202 Tweak setup buttom border and tag shadows
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-05 23:23:34 -04:00
Roberto Rosario
300bdbfc8a Tweak setup buttom border and tag shadows
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-05 21:34:20 -04:00
Roberto Rosario
a0331e0236 Add support for icon shadows
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-05 21:26:45 -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
171 changed files with 6357 additions and 4925 deletions

View File

@@ -63,6 +63,7 @@ job_docker_nightly:
only:
- nightly
- staging
- /^clients\/.+$/
job_documentation_build:
stage: build_documentation
@@ -160,6 +161,7 @@ job_push_python:
- releases/python
- staging
- nightly
- /^clients\/.+$/
test-mysql:
<<: *test_base

14
CHANGES_BC.rst Normal file
View File

@@ -0,0 +1,14 @@
- 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.

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -14,7 +14,7 @@ APP_LIST = (
'django_gpg', 'document_comments', 'document_indexing',
'document_parsing', 'document_signatures', 'document_states',
'documents', 'dynamic_search', 'events', 'file_metadata', 'linking',
'lock_manager', 'mayan_statistics', 'mailer', 'metadata', 'mirroring',
'lock_manager', 'mailer', 'mayan_statistics', 'metadata', 'mirroring',
'motd', 'navigation', 'ocr', 'permissions', 'platform', 'rest_api',
'smart_settings', 'sources', 'storage', 'tags', 'task_manager',
'user_management'

View File

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

View File

@@ -122,7 +122,7 @@ RUN python -m virtualenv "${PROJECT_INSTALL_DIR}" \
# Install the built Mayan EDMS package
&& pip install --no-cache-dir --no-use-pep517 dist/mayan* \
# Install the static content
&& mayan-edms.py installjavascript \
&& mayan-edms.py installdependencies \
&& MAYAN_STATIC_ROOT=${PROJECT_INSTALL_DIR}/static mayan-edms.py preparestatic --link --noinput
COPY --chown=mayan:mayan requirements/testing-base.txt "${PROJECT_INSTALL_DIR}"

View File

@@ -127,9 +127,8 @@ For another setup that offers more performance and scalability refer to the
::
sudo -u mayan MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
sudo -u mayan MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py initialsetup
@@ -148,9 +147,8 @@ For another setup that offers more performance and scalability refer to the
------------------------------------------------------------------------
::
sudo MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
sudo mayan MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf

View File

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

166
docs/releases/3.3.rst Normal file
View File

@@ -0,0 +1,166 @@
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.
Removals
--------
- Database conversion. Reason for removal. The database conversions support
provided by this feature (SQLite to PostgreSQL) was being confused with
database migrations and upgrades.
Database upgrades are the responsibility of the app and the framework.
Database conversions however are not the responsibility of the app (Mayan),
they are the responsibility of the framework.
Database conversion is outside the scope of what Mayan does but we added
the code, management command, instructions and testing setup to provide
this to our users until the framework (Django) decided to add this
themselves (like they did with migrations).
Continued confusion about the purpose of the feature and confusion about
how errors with this feature were a reflexion of the code quality of
Mayannecessitated the removal of the database conversion feature.
- Django environ
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:`532` Workflow preview isn't updated right after transitions are modified
- :gitlab-issue:`634` Failing docker entrypoint when using secret config
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/

View File

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

View File

@@ -4,6 +4,7 @@ from django.template.loader import get_template
class IconDriver(object):
context = {}
_registry = {}
@classmethod
@@ -14,6 +15,17 @@ class IconDriver(object):
def register(cls, driver_class):
cls._registry[driver_class.name] = driver_class
def get_context(self):
return self.context
def render(self, extra_context=None):
context = self.get_context()
if extra_context:
context.update(extra_context)
return get_template(template_name=self.template_name).render(
context=context
)
class FontAwesomeDriver(IconDriver):
name = 'fontawesome'
@@ -22,10 +34,8 @@ class FontAwesomeDriver(IconDriver):
def __init__(self, symbol):
self.symbol = symbol
def render(self):
return get_template(template_name=self.template_name).render(
context={'symbol': self.symbol}
)
def get_context(self):
return {'symbol': self.symbol}
class FontAwesomeDualDriver(IconDriver):
@@ -36,9 +46,8 @@ class FontAwesomeDualDriver(IconDriver):
self.primary_symbol = primary_symbol
self.secondary_symbol = secondary_symbol
def render(self):
return get_template(template_name=self.template_name).render(
context={
def get_context(self):
return {
'data': (
{
'class': 'fas fa-circle',
@@ -52,7 +61,6 @@ class FontAwesomeDualDriver(IconDriver):
},
)
}
)
class FontAwesomeCSSDriver(IconDriver):
@@ -62,10 +70,8 @@ class FontAwesomeCSSDriver(IconDriver):
def __init__(self, css_classes):
self.css_classes = css_classes
def render(self):
return get_template(template_name=self.template_name).render(
context={'css_classes': self.css_classes}
)
def get_context(self):
return {'css_classes': self.css_classes}
class FontAwesomeMasksDriver(IconDriver):
@@ -75,23 +81,23 @@ class FontAwesomeMasksDriver(IconDriver):
def __init__(self, data):
self.data = data
def render(self):
return get_template(template_name=self.template_name).render(
context={'data': self.data}
)
def get_context(self):
return {'data': self.data}
class FontAwesomeLayersDriver(IconDriver):
name = 'fontawesome-layers'
template_name = 'appearance/icons/font_awesome_layers.html'
def __init__(self, data):
def __init__(self, data, shadow_class=None):
self.data = data
self.shadow_class = shadow_class
def render(self):
return get_template(template_name=self.template_name).render(
context={'data': self.data}
)
def get_context(self):
return {
'data': self.data,
'shadow_class': self.shadow_class,
}
class Icon(object):

View File

@@ -12,7 +12,7 @@
}
body {
padding-top: 70px;
padding-top: 60px;
}
.navbar-brand {
@@ -70,7 +70,8 @@ img.lazy-load-carousel {
}
.label-tag {
text-shadow: 0px 0px 2px #000
text-shadow: 0px 0px 2px #000;
box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.5);
}
.fancybox-nav span {
@@ -88,21 +89,23 @@ hr {
}
.btn-block {
border-top: 2px solid rgba(255, 255, 255, 0.7);
border-left: 2px solid rgba(255, 255, 255, 0.7);
border-right: 2px solid rgba(0, 0, 0, 0.7);
border-bottom: 2px solid rgba(0, 0, 0, 0.7);
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5);
margin-bottom: 15px;
white-space: normal;
min-height: 120px;
padding-top: 20px;
padding-bottom: 1px;
padding-top: 20px;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
white-space: normal;
}
.btn-block .fa {
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
}
.btn-block {
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
}
.radio ul li {
list-style-type:none;
}
@@ -213,6 +216,10 @@ a i {
font-weight: bold;
}
.source-column-label {
font-weight: bold;
}
/* Content */
@media (min-width:1200px) {
.container-fluid {
@@ -261,8 +268,8 @@ a i {
#ajax-spinner {
position: fixed;
top: 12px;
right: 10px;
top: 16px;
left: 10px;
z-index: 9999;
width: 25px;
height: 25px;
@@ -328,7 +335,7 @@ a i {
.main {
padding-right: 0px;
padding-left: 0px;
/*margin-left: 210px;*/
margin-left: 210px;
}
}
@@ -410,3 +417,124 @@ a i {
.btn-list {
margin-bottom: 2px;
}
/*
* Top navigation
* Hide default border to remove 1px line.
*/
.navbar-fixed-top {
border: 0;
}
/* menu_main */
/* Hide for mobile, show later */
#menu-main {
display: none;
background-color: #2c3e50;
border-right: 1px solid #18bc9c;
bottom: 0;
left: 0;
overflow-x: hidden;
overflow-y: auto;
padding-top: 10px;
position: fixed;
top: 51px;
width: 210px;
z-index: 1000;
}
@media (min-width: 768px) {
#menu-main {
display: block;
}
.navbar-brand {
text-align: center;
width: 210px;
}
}
.main .page-header {
margin-top: 0;
}
.navbar-brand {
}
.navbar-brand {
outline: none;
}
.container-fluid {
margin-right: 0px;
margin-left: 0px;
width: 100%;
}
#accordion-sidebar a {
padding: 10px 15px;
}
#accordion-sidebar a[aria-expanded="true"] {
background: #1a242f;
}
#accordion-sidebar .panel {
border: 0px;
}
#accordion-sidebar a {
text-decoration: none;
outline: none;
position: relative;
display: block;
}
#accordion-sidebar .panel-heading {
background-color: #2c3e50;
color: white;
padding: 0px;
}
#accordion-sidebar .panel-heading:hover {
background-color: #517394;
}
#accordion-sidebar > .panel > div > .panel-body > ul > li > a:hover {
background-color: #517394;
}
#accordion-sidebar > .panel > div > .panel-body > ul > li.active {
background: #1a242f;
}
#accordion-sidebar .panel-title {
font-size: 15px;
}
#accordion-sidebar .panel-body {
font-size: 13px;
border: 0px;
background-color: #2c3e50;
padding-top: 5px;
padding-left: 20px;
padding-right: 0px;
padding-bottom: 0px;
}
#accordion-sidebar .panel-body li {
padding: 0px;
}
#accordion-sidebar .panel-body a {
color: white;
text-decoration: none;
padding: 9px;
}
.navbar-fixed-top {
box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.5);
}

View File

@@ -41,6 +41,17 @@ class MayanApp {
}
}
static setupNavBarState () {
$('body').on('click', '.a-main-menu-accordion-link', function (event) {
console.log('ad');
$('.a-main-menu-accordion-link').each(function (index, value) {
$(this).parent().removeClass('active');
});
$(this).parent().addClass('active');
});
}
static updateNavbarState () {
var uri = new URI(window.location.hash);
var uriFragment = uri.fragment();
@@ -160,6 +171,7 @@ class MayanApp {
this.setupFullHeightResizing();
this.setupItemsSelector();
this.setupNavbarCollapse();
MayanApp.setupNavBarState();
this.setupNewWindowAnchor();
$.each(this.ajaxMenusOptions, function(index, value) {
value.app = self;

View File

@@ -86,7 +86,7 @@
{% if not hide_columns %}
{% navigation_get_source_columns source=object exclude_identifier=True as source_columns %}
{% for column in source_columns %}
<div class="text-center" style="">{% navigation_source_column_resolve column=column as column_value %}{% if column_value != '' %}{% if column.include_label %}{{ column.label }}: {% endif %}{{ column_value }}{% endif %}</div>
<div class="text-center" style="">{% navigation_source_column_resolve column=column as column_value %}{% if column_value != '' %}{% if column.include_label %}<span class="source-column-label">{{ column.label }}</span>: {% endif %}{{ column_value }}{% endif %}</div>
{% endfor %}
{% endif %}

View File

@@ -1,4 +1,7 @@
<span class="fa-layers fa-fw" style="margin-right: 7px;">
{% if enable_shadow %}
<i class="{{ shadow_class }}" data-fa-transform="right-1 down-2" style="color:rgba(0, 0, 0, 0.3); stroke: rgba(255, 255, 255, 0.3); stroke-width: 20;"></i>
{% endif %}
{% for entry in data %}
<i class="{{ entry.class }}" data-fa-transform="{{ entry.transform }}" data-fa-mask="{{ entry.mask }}"></i>
{% endfor %}

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,11 @@ from django.utils.translation import ugettext_lazy as _
register = Library()
@register.simple_tag
def appearance_icon_render(icon_class, enable_shadow=False):
return icon_class.render(extra_context={'enable_shadow': enable_shadow})
@register.filter
def get_choice_value(field):
try:

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ from django.apps import apps
from django.db import models, transaction
from django.utils.timezone import now
from mayan.apps.acls.models import AccessControlList
from mayan.apps.documents.models import Document
from .events import (
@@ -14,10 +15,53 @@ from .events import (
)
from .exceptions import DocumentNotCheckedOut
from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN
from .permissions import (
permission_document_check_in, permission_document_check_in_override
)
logger = logging.getLogger(__name__)
class DocumentCheckoutBusinessLogicManager(models.Manager):
def check_in_document(self, document, user=None):
queryset = document._meta.default_manager.filter(pk=document.pk)
return self.check_in_documents(queryset=queryset, user=user)
def check_in_documents(self, queryset, user=None):
if user:
user_document_checkouts = AccessControlList.objects.restrict_queryset(
permission=permission_document_check_in,
queryset=self.filter(user_id=user.pk, document__in=queryset),
user=user
)
others_document_checkouts = AccessControlList.objects.restrict_queryset(
permission=permission_document_check_in_override,
queryset=self.exclude(user_id=user.pk, document__in=queryset),
user=user
)
with transaction.atomic():
if user:
for checkout in user_document_checkouts:
event_document_check_in.commit(
actor=user, target=checkout.document
)
checkout.delete()
for checkout in others_document_checkouts:
event_document_forceful_check_in.commit(
actor=user, target=checkout.document
)
checkout.delete()
else:
for checkout in self.filter(document__in=queryset):
event_document_auto_check_in.commit(
target=checkout.document
)
checkout.delete()
class DocumentCheckoutManager(models.Manager):
def are_document_new_versions_allowed(self, document, user=None):
try:
@@ -27,25 +71,6 @@ class DocumentCheckoutManager(models.Manager):
else:
return not check_out_info.block_new_version
def check_in_document(self, document, user=None):
try:
document_check_out = self.model.objects.get(document=document)
except self.model.DoesNotExist:
raise DocumentNotCheckedOut
else:
with transaction.atomic():
if user:
if self.get_check_out_info(document=document).user != user:
event_document_forceful_check_in.commit(
actor=user, target=document
)
else:
event_document_check_in.commit(actor=user, target=document)
else:
event_document_auto_check_in.commit(target=document)
document_check_out.delete()
def check_in_expired_check_outs(self):
for document in self.expired_check_outs():
document.check_in()
@@ -57,7 +82,11 @@ class DocumentCheckoutManager(models.Manager):
)
def checked_out_documents(self):
return Document.objects.filter(
CheckedOutDocument = apps.get_model(
app_label='checkouts', model_name='CheckedOutDocument'
)
return CheckedOutDocument.objects.filter(
pk__in=self.model.objects.values('document__id')
)
@@ -74,7 +103,11 @@ class DocumentCheckoutManager(models.Manager):
return STATE_CHECKED_IN
def expired_check_outs(self):
expired_list = Document.objects.filter(
CheckedOutDocument = apps.get_model(
app_label='checkouts', model_name='CheckedOutDocument'
)
expired_list = CheckedOutDocument.objects.filter(
pk__in=self.model.objects.filter(
expiration_datetime__lte=now()
).values_list('document__pk', flat=True)
@@ -83,9 +116,6 @@ class DocumentCheckoutManager(models.Manager):
return expired_list
def get_by_natural_key(self, document_natural_key):
Document = apps.get_model(
app_label='documents', model_name='Document'
)
try:
document = Document.objects.get_by_natural_key(document_natural_key)
except Document.DoesNotExist:

View File

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

View File

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

View File

@@ -4,13 +4,19 @@ import datetime
from django.utils.timezone import now
from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS
from mayan.apps.common.tests.utils import as_id_list
from ..models import DocumentCheckout
class DocumentCheckoutTestMixin(object):
_test_document_check_out_seconds = 0.1
def _check_out_test_document(self, user=None):
def _check_out_test_document(self, document=None, user=None):
if not document:
document = self.test_document
if not user:
user = self._test_case_user
@@ -19,7 +25,61 @@ class DocumentCheckoutTestMixin(object):
)
self.test_check_out = DocumentCheckout.objects.check_out_document(
block_new_version=True, document=self.test_document,
block_new_version=True, document=document,
expiration_datetime=self._check_out_expiration_datetime,
user=user
)
class DocumentCheckoutViewTestMixin(object):
def _request_test_document_check_in_get_view(self):
return self.get(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
def _request_test_document_check_in_post_view(self):
return self.post(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
def _request_test_document_multiple_check_in_post_view(self):
return self.post(
viewname='checkouts:check_in_document_multiple', data={
'id_list': as_id_list(items=self.test_documents)
}
)
def _request_test_document_check_out_view(self):
return self.post(
viewname='checkouts:check_out_document', kwargs={
'pk': self.test_document.pk
}, data={
'block_new_version': True,
'expiration_datetime_0': TIME_DELTA_UNIT_DAYS,
'expiration_datetime_1': 2
}
)
def _request_test_document_multiple_check_out_post_view(self):
return self.post(
viewname='checkouts:check_out_document_multiple', data={
'block_new_version': True,
'expiration_datetime_0': TIME_DELTA_UNIT_DAYS,
'expiration_datetime_1': 2,
'id_list': as_id_list(items=self.test_documents)
}
)
def _request_test_document_check_out_detail_view(self):
return self.get(
viewname='checkouts:check_out_info', kwargs={
'pk': self.test_document.pk
}
)
def _request_test_document_check_out_list_view(self):
return self.get(viewname='checkouts:check_out_list')

View File

@@ -65,7 +65,7 @@ class CheckoutsAPITestCase(DocumentCheckoutTestMixin, DocumentTestMixin, BaseAPI
force_text(self.test_document.uuid)
)
def _request_document_checkout_view(self):
def _request_test_document_check_out_view(self):
return self.post(
viewname='rest_api:checkout-document-list', data={
'document_pk': self.test_document.pk,
@@ -74,7 +74,7 @@ class CheckoutsAPITestCase(DocumentCheckoutTestMixin, DocumentTestMixin, BaseAPI
)
def test_document_checkout_no_access(self):
response = self._request_document_checkout_view()
response = self._request_test_document_check_out_view()
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(DocumentCheckout.objects.count(), 0)
@@ -82,7 +82,7 @@ class CheckoutsAPITestCase(DocumentCheckoutTestMixin, DocumentTestMixin, BaseAPI
def test_document_checkout_with_access(self):
self.grant_access(permission=permission_document_check_out, obj=self.test_document)
response = self._request_document_checkout_view()
response = self._request_test_document_check_out_view()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(

View File

@@ -7,8 +7,7 @@ from mayan.apps.documents.tests import GenericDocumentTestCase, DocumentTestMixi
from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH
from ..exceptions import (
DocumentAlreadyCheckedOut, DocumentNotCheckedOut,
NewDocumentVersionNotAllowed
DocumentAlreadyCheckedOut, NewDocumentVersionNotAllowed
)
from ..models import DocumentCheckout, NewVersionBlock
@@ -49,10 +48,6 @@ class DocumentCheckoutTestCase(DocumentCheckoutTestMixin, GenericDocumentTestCas
block_new_version=True
)
def test_checkin_without_checkout(self):
with self.assertRaises(DocumentNotCheckedOut):
self.test_document.check_in()
def test_auto_check_in(self):
self._check_out_test_document()

View File

@@ -1,6 +1,5 @@
from __future__ import unicode_literals
from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS
from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.documents.tests import GenericDocumentViewTestCase
from mayan.apps.sources.links import link_document_version_upload
@@ -12,64 +11,53 @@ from ..permissions import (
permission_document_check_out, permission_document_check_out_detail_view
)
from .mixins import DocumentCheckoutTestMixin
from .mixins import DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin
class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentViewTestCase):
def _request_document_check_in_get_view(self):
return self.get(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
def test_check_in_document_get_view_no_permission(self):
class DocumentCheckoutViewTestCase(
DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin,
GenericDocumentViewTestCase
):
def test_document_check_in_get_view_no_permission(self):
self._check_out_test_document()
response = self._request_document_check_in_get_view()
self.assertContains(
response=response, text=self.test_document.label, status_code=200
response = self._request_test_document_check_in_get_view()
self.assertNotContains(
response=response, text=self.test_document.label, status_code=404
)
self.assertTrue(self.test_document.is_checked_out())
def test_check_in_document_get_view_with_access(self):
def test_document_check_in_get_view_with_access(self):
self._check_out_test_document()
self.grant_access(
obj=self.test_document, permission=permission_document_check_in
)
response = self._request_document_check_in_get_view()
response = self._request_test_document_check_in_get_view()
self.assertContains(
response=response, text=self.test_document.label, status_code=200
)
self.assertTrue(self.test_document.is_checked_out())
def _request_document_check_in_post_view(self):
return self.post(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
def test_check_in_document_post_view_no_permission(self):
def test_document_check_in_post_view_no_permission(self):
self._check_out_test_document()
response = self._request_document_check_in_post_view()
self.assertEqual(response.status_code, 403)
response = self._request_test_document_check_in_post_view()
self.assertEqual(response.status_code, 404)
self.assertTrue(self.test_document.is_checked_out())
def test_check_in_document_post_view_with_access(self):
def test_document_check_in_post_view_with_access(self):
self._check_out_test_document()
self.grant_access(
obj=self.test_document, permission=permission_document_check_in
)
response = self._request_document_check_in_post_view()
response = self._request_test_document_check_in_post_view()
self.assertEqual(response.status_code, 302)
self.assertFalse(self.test_document.is_checked_out())
@@ -79,24 +67,93 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
)
)
def _request_document_checkout_view(self):
return self.post(
viewname='checkouts:check_out_document', kwargs={
'pk': self.test_document.pk
}, data={
'expiration_datetime_0': 2,
'expiration_datetime_1': TIME_DELTA_UNIT_DAYS,
'block_new_version': True
}
def test_document_multiple_check_in_post_view_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_check_out_document_view_no_permission(self):
response = self._request_document_checkout_view()
self.assertEqual(response.status_code, 403)
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.assertFalse(self.test_document.is_checked_out())
def test_check_out_document_view_with_access(self):
def test_document_check_out_view_with_access(self):
self.grant_access(
obj=self.test_document, permission=permission_document_check_out
)
@@ -105,28 +162,117 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
permission=permission_document_check_out_detail_view
)
response = self._request_document_checkout_view()
response = self._request_test_document_check_out_view()
self.assertEqual(response.status_code, 302)
self.assertTrue(self.test_document.is_checked_out())
def _request_check_out_detail_view(self):
return self.get(
viewname='checkouts:check_out_info', kwargs={
'pk': self.test_document.pk
}
def test_document_multiple_check_out_post_view_no_permission(self):
# Upload second document
self.upload_document()
self.grant_access(
obj=self.test_documents[0],
permission=permission_document_check_out_detail_view
)
self.grant_access(
obj=self.test_documents[1],
permission=permission_document_check_out_detail_view
)
def test_checkout_detail_view_no_permission(self):
response = self._request_test_document_multiple_check_out_post_view()
self.assertEqual(response.status_code, 404)
self.assertFalse(self.test_documents[0].is_checked_out())
self.assertFalse(self.test_documents[1].is_checked_out())
self.assertFalse(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[0]
)
)
self.assertFalse(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[1]
)
)
def test_document_multiple_check_out_post_view_with_document_0_access(self):
# Upload second document
self.upload_document()
self.grant_access(
obj=self.test_documents[0], permission=permission_document_check_out
)
self.grant_access(
obj=self.test_documents[0],
permission=permission_document_check_out_detail_view
)
self.grant_access(
obj=self.test_documents[1],
permission=permission_document_check_out_detail_view
)
response = self._request_test_document_multiple_check_out_post_view()
self.assertEqual(response.status_code, 302)
self.assertTrue(self.test_documents[0].is_checked_out())
self.assertFalse(self.test_documents[1].is_checked_out())
self.assertTrue(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[0]
)
)
self.assertFalse(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[1]
)
)
def test_document_multiple_check_out_post_view_with_access(self):
# Upload second document
self.upload_document()
self.grant_access(
obj=self.test_documents[0], permission=permission_document_check_out
)
self.grant_access(
obj=self.test_documents[1], permission=permission_document_check_out
)
self.grant_access(
obj=self.test_documents[0],
permission=permission_document_check_out_detail_view
)
self.grant_access(
obj=self.test_documents[1],
permission=permission_document_check_out_detail_view
)
response = self._request_test_document_multiple_check_out_post_view()
self.assertEqual(response.status_code, 302)
self.assertTrue(self.test_documents[0].is_checked_out())
self.assertTrue(self.test_documents[1].is_checked_out())
self.assertTrue(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[0]
)
)
self.assertTrue(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[1]
)
)
def test_document_check_out_detail_view_no_permission(self):
self._check_out_test_document()
response = self._request_check_out_detail_view()
response = self._request_test_document_check_out_detail_view()
self.assertNotContains(
response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=404
)
def test_checkout_detail_view_with_access(self):
def test_document_check_out_detail_view_with_access(self):
self._check_out_test_document()
self.grant_access(
@@ -134,15 +280,12 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
permission=permission_document_check_out_detail_view
)
response = self._request_check_out_detail_view()
response = self._request_test_document_check_out_detail_view()
self.assertContains(
response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=200
)
def _request_check_out_list_view(self):
return self.get(viewname='checkouts:check_out_list')
def test_checkout_list_view_no_permission(self):
def test_document_checkout_list_view_no_permission(self):
self._check_out_test_document()
self.grant_access(
@@ -150,12 +293,12 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
permission=permission_document_view
)
response = self._request_check_out_list_view()
response = self._request_test_document_check_out_list_view()
self.assertNotContains(
response=response, text=self.test_document.label, status_code=200
)
def test_checkout_list_view_with_access(self):
def test_document_checkout_list_view_with_access(self):
self._check_out_test_document()
self.grant_access(
@@ -167,12 +310,12 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
permission=permission_document_view
)
response = self._request_check_out_list_view()
response = self._request_test_document_check_out_list_view()
self.assertContains(
response=response, text=self.test_document.label, status_code=200
)
def test_document_new_version_after_check_out(self):
def test_document_check_out_new_version(self):
"""
Gitlab issue #231
User shown option to upload new version of a document even though it
@@ -209,45 +352,39 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
self.assertEqual(resolved_link, None)
def test_forcefull_check_in_document_view_no_permission(self):
def test_document_check_in_forcefull_view_no_permission(self):
# Gitlab issue #237
# Forcefully checking in a document by a user without adequate
# permissions throws out an error
self._create_test_case_superuser()
self._check_out_test_document(user=self._test_case_superuser)
self._create_test_user()
self._check_out_test_document(user=self.test_user)
self.grant_access(
obj=self.test_document, permission=permission_document_check_in
)
response = self.post(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
self.assertContains(
response=response, text='Insufficient permissions', status_code=403
)
self.assertTrue(self.test_document.is_checked_out())
def test_forcefull_check_in_document_view_with_permission(self):
self._create_test_case_superuser()
self._check_out_test_document(user=self._test_case_superuser)
self.grant_access(
obj=self.test_document, permission=permission_document_check_in
)
self.grant_access(
obj=self.test_document, permission=permission_document_check_in_override
)
response = self.post(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
self.assertEqual(response.status_code, 302)
self.assertTrue(self.test_document.is_checked_out())
def test_document_check_in_forcefull_view_with_access(self):
self._create_test_user()
self._check_out_test_document(user=self.test_user)
self.grant_access(
obj=self.test_document,
permission=permission_document_check_in_override
)
response = self.post(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
self.assertEqual(response.status_code, 302)
self.assertFalse(self.test_document.is_checked_out())

View File

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

View File

@@ -1,20 +1,16 @@
from __future__ import absolute_import, unicode_literals
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _, ungettext
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.generics import (
ConfirmView, SingleObjectCreateView, SingleObjectDetailView
MultipleObjectConfirmActionView, MultipleObjectFormActionView,
SingleObjectDetailView
)
from mayan.apps.common.utils import encapsulate
from mayan.apps.documents.models import Document
from mayan.apps.documents.views import DocumentListView
from .exceptions import DocumentAlreadyCheckedOut, DocumentNotCheckedOut
from .forms import DocumentCheckoutForm, DocumentCheckoutDefailForm
from .icons import icon_check_out_info
from .models import DocumentCheckout
@@ -24,159 +20,124 @@ from .permissions import (
)
class DocumentCheckinView(ConfirmView):
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.'
def get_extra_context(self):
document = self.get_object()
queryset = self.get_object_list()
context = {
'object': document,
result = {
'title': ungettext(
singular='Check in %(count)d document',
plural='Check in %(count)d documents',
number=queryset.count()
) % {
'count': queryset.count(),
}
}
if document.get_check_out_info().user != self.request.user:
context['title'] = _(
'You didn\'t originally checked out this document. '
'Forcefully check in the document: %s?'
) % document
else:
context['title'] = _('Check in the document: %s?') % document
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Check in document: %s'
) % queryset.first()
}
)
return context
return result
def get_object(self):
return get_object_or_404(klass=Document, pk=self.kwargs['pk'])
def get_post_action_redirect(self):
def get_post_object_action_url(self):
if self.action_count == 1:
return reverse(
viewname='checkouts:check_out_info', kwargs={
'pk': self.get_object().pk
}
)
def view_action(self):
document = self.get_object()
if document.get_check_out_info().user == self.request.user:
AccessControlList.objects.check_access(
obj=document, permissions=(permission_document_check_in,),
user=self.request.user
viewname='checkouts:document_checkout_info',
kwargs={'pk': self.action_id_list[0]}
)
else:
AccessControlList.objects.check_access(
obj=document,
permissions=(permission_document_check_in_override,),
super(DocumentCheckinView, self).get_post_action_redirect()
def get_source_queryset(self):
# object_permission is None to disable restricting queryset mixin
# and restrict the queryset ourselves from two permissions
source_queryset = super(DocumentCheckinView, self).get_source_queryset()
check_in_queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_check_in, queryset=source_queryset,
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
check_in_override_queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_check_in_override,
queryset=source_queryset, user=self.request.user
)
else:
messages.success(
message=_(
'Document "%s" checked in successfully.'
) % document, request=self.request
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 CheckoutDocumentView(SingleObjectCreateView):
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 dispatch(self, request, *args, **kwargs):
self.document = get_object_or_404(klass=Document, pk=self.kwargs['pk'])
def get_extra_context(self):
queryset = self.get_object_list()
AccessControlList.objects.check_access(
obj=self.document, permissions=(permission_document_check_out,),
user=request.user
result = {
'title': ungettext(
singular='Checkout %(count)d document',
plural='Checkout %(count)d documents',
number=queryset.count()
) % {
'count': queryset.count(),
}
}
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Check out document: %s'
) % queryset.first()
}
)
return super(
CheckoutDocumentView, self
).dispatch(request, *args, **kwargs)
return result
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
def get_post_object_action_url(self):
if self.action_count == 1:
return reverse(
viewname='checkouts:document_checkout_info',
kwargs={'pk': self.action_id_list[0]}
)
else:
messages.success(
message=_(
'Document "%s" checked out successfully.'
) % self.document, request=self.request
)
super(DocumentCheckoutView, self).get_post_action_redirect()
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
}
def object_action(self, form, instance):
DocumentCheckout.objects.check_out_document(
block_new_version=form.cleaned_data['block_new_version'],
document=instance,
expiration_datetime=form.cleaned_data['expiration_datetime'],
user=self.request.user,
)
class CheckoutListView(DocumentListView):
def get_document_queryset(self):
return AccessControlList.objects.restrict_queryset(
permission=permission_document_check_out_detail_view,
queryset=DocumentCheckout.objects.checked_out_documents(),
user=self.request.user
)
def get_extra_context(self):
context = super(CheckoutListView, self).get_extra_context()
context.update(
{
'extra_columns': (
{
'name': _('User'),
'attribute': encapsulate(
lambda document: document.get_check_out_info().user.get_full_name() or document.get_check_out_info().user
)
},
{
'name': _('Checkout time and date'),
'attribute': encapsulate(
lambda document: document.get_check_out_info().checkout_datetime
)
},
{
'name': _('Checkout expiration'),
'attribute': encapsulate(
lambda document: document.get_check_out_info().expiration_datetime
)
},
),
'no_results_icon': icon_check_out_info,
'no_results_text': _(
'Checking out a document blocks certain document '
'operations for a predetermined amount of '
'time.'
),
'no_results_title': _('No documents have been checked out'),
'title': _('Documents checked out'),
}
)
return context
class CheckoutDetailView(SingleObjectDetailView):
class DocumentCheckoutDetailView(SingleObjectDetailView):
form_class = DocumentCheckoutDefailForm
model = Document
object_permission = permission_document_check_out_detail_view
@@ -188,3 +149,27 @@ class CheckoutDetailView(SingleObjectDetailView):
'Check out details for document: %s'
) % self.object
}
class DocumentCheckoutListView(DocumentListView):
def get_document_queryset(self):
return AccessControlList.objects.restrict_queryset(
permission=permission_document_check_out_detail_view,
queryset=DocumentCheckout.objects.checked_out_documents(),
user=self.request.user
)
def get_extra_context(self):
context = super(DocumentCheckoutListView, self).get_extra_context()
context.update(
{
'no_results_icon': icon_check_out_info,
'no_results_text': _(
'Checking out a document, blocks certain operations '
'for a predetermined amount of time.'
),
'no_results_title': _('No documents have been checked out'),
'title': _('Checked out documents'),
}
)
return context

View File

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

View File

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

View File

@@ -35,8 +35,14 @@ icon_menu_about = Icon(
icon_menu_user = Icon(
driver_name='fontawesome', symbol='user-circle'
)
icon_object_error_list_with_icon = Icon(
driver_name='fontawesome', symbol='lock'
icon_object_errors = Icon(
driver_name='fontawesome', symbol='exclamation-triangle'
)
icon_object_error_list = Icon(
driver_name='fontawesome', symbol='exclamation-triangle'
)
icon_object_error_list_clear = Icon(
driver_name='fontawesome', symbol='times'
)
icon_ok = Icon(
driver_name='fontawesome', symbol='check'

View File

@@ -8,8 +8,8 @@ from mayan.apps.navigation.classes import Link
from .icons import (
icon_about, icon_current_user_locale_profile_details,
icon_current_user_locale_profile_edit, icon_documentation,
icon_forum, icon_license, icon_object_error_list_with_icon,
icon_setup, icon_source_code, icon_support, icon_tools
icon_forum, icon_license, icon_setup, icon_source_code, icon_support,
icon_tools
)
from .permissions_runtime import permission_error_log_view
@@ -50,21 +50,17 @@ link_documentation = Link(
text=_('Documentation'), url='https://docs.mayan-edms.com'
)
link_object_error_list = Link(
icon_class_path='mayan.apps.common.icons.icon_object_error_list',
kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_error_log_view,), text=_('Errors'),
view='common:object_error_list',
)
link_object_error_list_clear = Link(
icon_class_path='mayan.apps.common.icons.icon_object_error_list_clear',
kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_error_log_view,), text=_('Clear all'),
view='common:object_error_list_clear',
)
link_object_error_list_with_icon = Link(
kwargs=get_kwargs_factory('resolved_object'),
icon_class=icon_object_error_list_with_icon,
permissions=(permission_error_log_view,), text=_('Errors'),
view='common:error_list',
)
link_forum = Link(
icon_class=icon_forum, tags='new_window', text=_('Forum'),
url='https://forum.mayan-edms.com'

View File

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

View File

@@ -17,6 +17,7 @@ menu_object = Menu(label=_('Actions'), name='object')
menu_secondary = Menu(label=_('Secondary'), name='secondary')
menu_setup = Menu(name='setup')
menu_tools = Menu(name='tools')
menu_topbar = Menu(name='topbar')
menu_user = Menu(
icon_class=icon_menu_user, name='user', label=_('User')
)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
from __future__ import unicode_literals
import yaml
try:
from yaml import CSafeLoader as SafeLoader, CSafeDumper as SafeDumper
except ImportError:
from yaml import SafeLoader, SafeDumper
def yaml_dump(*args, **kwargs):
defaults = {'Dumper': SafeDumper}
defaults.update(kwargs)
return yaml.dump(*args, **defaults)
def yaml_load(*args, **kwargs):
defaults = {'Loader': SafeLoader}
defaults.update(kwargs)
return yaml.load(*args, **defaults)

View File

@@ -10,7 +10,6 @@ from mayan.apps.smart_settings.classes import Namespace
from .literals import DEFAULT_COMMON_HOME_VIEW
namespace = Namespace(label=_('Common'), name='common')
setting_auto_logging = namespace.add_setting(
@@ -95,322 +94,5 @@ setting_shared_storage = namespace.add_setting(
)
setting_shared_storage_arguments = namespace.add_setting(
global_name='COMMON_SHARED_STORAGE_ARGUMENTS',
default='{{location: {}}}'.format(
os.path.join(settings.MEDIA_ROOT, 'shared_files')
), quoted=True
)
namespace = Namespace(label=_('Django'), name='django')
setting_django_allowed_hosts = namespace.add_setting(
global_name='ALLOWED_HOSTS', default=settings.ALLOWED_HOSTS,
help_text=_(
'A list of strings representing the host/domain names that this site '
'can serve. This is a security measure to prevent HTTP Host header '
'attacks, which are possible even under many seemingly-safe web '
'server configurations. Values in this list can be '
'fully qualified names (e.g. \'www.example.com\'), in which case '
'they will be matched against the request\'s Host header exactly '
'(case-insensitive, not including port). A value beginning with a '
'period can be used as a subdomain wildcard: \'.example.com\' will '
'match example.com, www.example.com, and any other subdomain of '
'example.com. A value of \'*\' will match anything; in this case you '
'are responsible to provide your own validation of the Host header '
'(perhaps in a middleware; if so this middleware must be listed '
'first in MIDDLEWARE).'
),
)
setting_django_append_slash = namespace.add_setting(
global_name='APPEND_SLASH', default=settings.APPEND_SLASH,
help_text=_(
'When set to True, if the request URL does not match any of the '
'patterns in the URLconf and it doesn\'t end in a slash, an HTTP '
'redirect is issued to the same URL with a slash appended. Note '
'that the redirect may cause any data submitted in a POST request '
'to be lost. The APPEND_SLASH setting is only used if '
'CommonMiddleware is installed (see Middleware). See also '
'PREPEND_WWW.'
)
)
setting_django_auth_password_validators = namespace.add_setting(
global_name='AUTH_PASSWORD_VALIDATORS',
default=settings.AUTH_PASSWORD_VALIDATORS,
help_text=_(
'The list of validators that are used to check the strength of '
'user\'s passwords.'
)
)
setting_django_databases = namespace.add_setting(
global_name='DATABASES', default=settings.DATABASES,
help_text=_(
'A dictionary containing the settings for all databases to be used '
'with Django. It is a nested dictionary whose contents map a '
'database alias to a dictionary containing the options for an '
'individual database. The DATABASES setting must configure a '
'default database; any number of additional databases may also '
'be specified.'
),
)
setting_django_data_upload_max_memory_size = namespace.add_setting(
global_name='DATA_UPLOAD_MAX_MEMORY_SIZE',
default=settings.DATA_UPLOAD_MAX_MEMORY_SIZE,
help_text=_(
'Default: 2621440 (i.e. 2.5 MB). The maximum size in bytes that a '
'request body may be before a SuspiciousOperation '
'(RequestDataTooBig) is raised. The check is done when accessing '
'request.body or request.POST and is calculated against the total '
'request size excluding any file upload data. You can set this to '
'None to disable the check. Applications that are expected to '
'receive unusually large form posts should tune this setting. The '
'amount of request data is correlated to the amount of memory '
'needed to process the request and populate the GET and POST '
'dictionaries. Large requests could be used as a '
'denial-of-service attack vector if left unchecked. Since web '
'servers don\'t typically perform deep request inspection, it\'s '
'not possible to perform a similar check at that level. See also '
'FILE_UPLOAD_MAX_MEMORY_SIZE.'
),
)
setting_django_default_from_email = namespace.add_setting(
global_name='DEFAULT_FROM_EMAIL',
default=settings.DEFAULT_FROM_EMAIL,
help_text=_(
'Default: \'webmaster@localhost\' '
'Default email address to use for various automated correspondence '
'from the site manager(s). This doesn\'t include error messages sent '
'to ADMINS and MANAGERS; for that, see SERVER_EMAIL.'
),
)
setting_django_disallowed_user_agents = namespace.add_setting(
global_name='DISALLOWED_USER_AGENTS',
default=settings.DISALLOWED_USER_AGENTS,
help_text=_(
'Default: [] (Empty list). List of compiled regular expression '
'objects representing User-Agent strings that are not allowed to '
'visit any page, systemwide. Use this for bad robots/crawlers. '
'This is only used if CommonMiddleware is installed '
'(see Middleware).'
),
)
setting_django_email_backend = namespace.add_setting(
global_name='EMAIL_BACKEND',
default=settings.EMAIL_BACKEND,
help_text=_(
'Default: \'django.core.mail.backends.smtp.EmailBackend\'. The '
'backend to use for sending emails.'
),
)
setting_django_email_host = namespace.add_setting(
global_name='EMAIL_HOST',
default=settings.EMAIL_HOST,
help_text=_(
'Default: \'localhost\'. The host to use for sending email.'
),
)
setting_django_email_host_password = namespace.add_setting(
global_name='EMAIL_HOST_PASSWORD',
default=settings.EMAIL_HOST_PASSWORD,
help_text=_(
'Default: \'\' (Empty string). Password to use for the SMTP '
'server defined in EMAIL_HOST. This setting is used in '
'conjunction with EMAIL_HOST_USER when authenticating to the '
'SMTP server. If either of these settings is empty, '
'Django won\'t attempt authentication.'
),
)
setting_django_email_host_user = namespace.add_setting(
global_name='EMAIL_HOST_USER',
default=settings.EMAIL_HOST_USER,
help_text=_(
'Default: \'\' (Empty string). Username to use for the SMTP '
'server defined in EMAIL_HOST. If empty, Django won\'t attempt '
'authentication.'
),
)
setting_django_email_port = namespace.add_setting(
global_name='EMAIL_PORT',
default=settings.EMAIL_PORT,
help_text=_(
'Default: 25. Port to use for the SMTP server defined in EMAIL_HOST.'
),
)
setting_django_email_timeout = namespace.add_setting(
global_name='EMAIL_TIMEOUT',
default=settings.EMAIL_TIMEOUT,
help_text=_(
'Default: None. Specifies a timeout in seconds for blocking '
'operations like the connection attempt.'
),
)
setting_django_email_user_tls = namespace.add_setting(
global_name='EMAIL_USE_TLS',
default=settings.EMAIL_USE_TLS,
help_text=_(
'Default: False. Whether to use a TLS (secure) connection when '
'talking to the SMTP server. This is used for explicit TLS '
'connections, generally on port 587. If you are experiencing '
'hanging connections, see the implicit TLS setting EMAIL_USE_SSL.'
),
)
setting_django_email_user_ssl = namespace.add_setting(
global_name='EMAIL_USE_SSL',
default=settings.EMAIL_USE_SSL,
help_text=_(
'Default: False. Whether to use an implicit TLS (secure) connection '
'when talking to the SMTP server. In most email documentation this '
'type of TLS connection is referred to as SSL. It is generally used '
'on port 465. If you are experiencing problems, see the explicit '
'TLS setting EMAIL_USE_TLS. Note that EMAIL_USE_TLS/EMAIL_USE_SSL '
'are mutually exclusive, so only set one of those settings to True.'
),
)
setting_django_file_upload_max_memory_size = namespace.add_setting(
global_name='FILE_UPLOAD_MAX_MEMORY_SIZE',
default=settings.FILE_UPLOAD_MAX_MEMORY_SIZE,
help_text=_(
'Default: 2621440 (i.e. 2.5 MB). The maximum size (in bytes) '
'that an upload will be before it gets streamed to the file '
'system. See Managing files for details. See also '
'DATA_UPLOAD_MAX_MEMORY_SIZE.'
),
)
setting_django_login_url = namespace.add_setting(
global_name='LOGIN_URL',
default=settings.LOGIN_URL,
help_text=_(
'Default: \'/accounts/login/\' The URL where requests are '
'redirected for login, especially when using the login_required() '
'decorator. This setting also accepts named URL patterns which '
'can be used to reduce configuration duplication since you '
'don\'t have to define the URL in two places (settings '
'and URLconf).'
)
)
setting_django_login_redirect_url = namespace.add_setting(
global_name='LOGIN_REDIRECT_URL',
default=settings.LOGIN_REDIRECT_URL,
help_text=_(
'Default: \'/accounts/profile/\' The URL where requests are '
'redirected after login when the contrib.auth.login view gets no '
'next parameter. This is used by the login_required() decorator, '
'for example. This setting also accepts named URL patterns which '
'can be used to reduce configuration duplication since you don\'t '
'have to define the URL in two places (settings and URLconf).'
),
)
setting_django_logout_redirect_url = namespace.add_setting(
global_name='LOGOUT_REDIRECT_URL',
default=settings.LOGOUT_REDIRECT_URL,
help_text=_(
'Default: None. The URL where requests are redirected after a user '
'logs out using LogoutView (if the view doesn\'t get a next_page '
'argument). If None, no redirect will be performed and the logout '
'view will be rendered. This setting also accepts named URL '
'patterns which can be used to reduce configuration duplication '
'since you don\'t have to define the URL in two places (settings '
'and URLconf).'
)
)
setting_django_internal_ips = namespace.add_setting(
global_name='INTERNAL_IPS',
default=settings.INTERNAL_IPS,
help_text=_(
'A list of IP addresses, as strings, that: Allow the debug() '
'context processor to add some variables to the template context. '
'Can use the admindocs bookmarklets even if not logged in as a '
'staff user. Are marked as "internal" (as opposed to "EXTERNAL") '
'in AdminEmailHandler emails.'
),
)
setting_django_languages = namespace.add_setting(
global_name='LANGUAGES',
default=settings.LANGUAGES,
help_text=_(
'A list of all available languages. The list is a list of '
'two-tuples in the format (language code, language name) '
'for example, (\'ja\', \'Japanese\'). This specifies which '
'languages are available for language selection. '
'Generally, the default value should suffice. Only set this '
'setting if you want to restrict language selection to a '
'subset of the Django-provided languages. '
),
)
setting_django_language_code = namespace.add_setting(
global_name='LANGUAGE_CODE',
default=settings.LANGUAGE_CODE,
help_text=_(
'A string representing the language code for this installation. '
'This should be in standard language ID format. For example, U.S. '
'English is "en-us". It serves two purposes: If the locale '
'middleware isn\'t in use, it decides which translation is served '
'to all users. If the locale middleware is active, it provides a '
'fallback language in case the user\'s preferred language can\'t '
'be determined or is not supported by the website. It also provides '
'the fallback translation when a translation for a given literal '
'doesn\'t exist for the user\'s preferred language.'
),
)
setting_django_static_url = namespace.add_setting(
global_name='STATIC_URL',
default=settings.STATIC_URL,
help_text=_(
'URL to use when referring to static files located in STATIC_ROOT. '
'Example: "/static/" or "http://static.example.com/" '
'If not None, this will be used as the base path for asset '
'definitions (the Media class) and the staticfiles app. '
'It must end in a slash if set to a non-empty value.'
),
)
setting_django_staticfiles_storage = namespace.add_setting(
global_name='STATICFILES_STORAGE',
default=settings.STATICFILES_STORAGE,
help_text=_(
'The file storage engine to use when collecting static files with '
'the collectstatic management command. A ready-to-use instance of '
'the storage backend defined in this setting can be found at '
'django.contrib.staticfiles.storage.staticfiles_storage.'
),
)
setting_django_time_zone = namespace.add_setting(
global_name='TIME_ZONE',
default=settings.TIME_ZONE,
help_text=_(
'A string representing the time zone for this installation. '
'Note that this isn\'t necessarily the time zone of the server. '
'For example, one server may serve multiple Django-powered sites, '
'each with a separate time zone setting.'
),
)
setting_django_wsgi_application = namespace.add_setting(
global_name='WSGI_APPLICATION',
default=settings.WSGI_APPLICATION,
help_text=_(
'The full Python path of the WSGI application object that Django\'s '
'built-in servers (e.g. runserver) will use. The django-admin '
'startproject management command will create a simple wsgi.py '
'file with an application callable in it, and point this setting '
'to that application.'
),
)
namespace = Namespace(label=_('Celery'), name='celery')
setting_celery_broker_url = namespace.add_setting(
global_name='BROKER_URL', default=settings.BROKER_URL,
help_text=_(
'Default: "amqp://". Default broker URL. This must be a URL in '
'the form of: transport://userid:password@hostname:port/virtual_host '
'Only the scheme part (transport://) is required, the rest is '
'optional, and defaults to the specific transports default values.'
),
)
setting_celery_result_backend = namespace.add_setting(
global_name='CELERY_RESULT_BACKEND',
default=settings.CELERY_RESULT_BACKEND,
help_text=_(
'Default: No result backend enabled by default. The backend used '
'to store task results (tombstones). Refer to '
'http://docs.celeryproject.org/en/v4.1.0/userguide/configuration.'
'html#result-backend'
)
default={'location': os.path.join(settings.MEDIA_ROOT, 'shared_files')}
)

View File

@@ -1,23 +1,11 @@
from __future__ import unicode_literals
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django.utils.module_loading import import_string
from mayan.apps.storage.utils import get_storage_subclass
from .settings import (
setting_shared_storage, setting_shared_storage_arguments
)
storage_sharedupload = import_string(
storage_sharedupload = get_storage_subclass(
dotted_path=setting_shared_storage.value
)(
**yaml.load(
stream=setting_shared_storage_arguments.value or '{}',
Loader=SafeLoader
)
)
)(**setting_shared_storage_arguments.value)

View File

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

View File

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

View File

@@ -21,14 +21,6 @@ def check_for_sqlite():
return settings.DATABASES['default']['ENGINE'] == DJANGO_SQLITE_BACKEND and settings.DEBUG is False
def encapsulate(function):
# Workaround Django ticket 15791
# Changeset 16045
# http://stackoverflow.com/questions/6861601/
# cannot-resolve-callable-context-variable/6955045#6955045
return lambda: function
def get_related_field(model, related_field_name):
try:
local_field_name, remaining_field_path = related_field_name.split(

View File

@@ -1,9 +1,18 @@
from __future__ import unicode_literals
import json
import re
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.utils import six
from django.utils.deconstruct import deconstructible
from django.utils.functional import SimpleLazyObject
from django.utils.translation import ugettext_lazy as _
@@ -23,6 +32,54 @@ def _lazy_re_compile(regex, flags=0):
return SimpleLazyObject(_compile)
@deconstructible
class JSONValidator(object):
"""
Validates that the input is JSON compliant.
"""
def __call__(self, value):
value = value.strip()
try:
json.loads(stream=value)
except ValueError:
raise ValidationError(
_('Enter a valid JSON value.'),
code='invalid'
)
def __eq__(self, other):
return (
isinstance(other, JSONValidator)
)
def __ne__(self, other):
return not (self == other)
@deconstructible
class YAMLValidator(object):
"""
Validates that the input is YAML compliant.
"""
def __call__(self, value):
value = value.strip()
try:
yaml.load(stream=value, Loader=SafeLoader)
except yaml.error.YAMLError:
raise ValidationError(
_('Enter a valid YAML value.'),
code='invalid'
)
def __eq__(self, other):
return (
isinstance(other, YAMLValidator)
)
def __ne__(self, other):
return not (self == other)
internal_name_re = _lazy_re_compile(r'^[a-zA-Z0-9_]+\Z')
validate_internal_name = RegexValidator(
internal_name_re, _(

View File

@@ -21,7 +21,7 @@ from .forms import (
from .generics import (
ConfirmView, SingleObjectEditView, SingleObjectListView, SimpleView
)
from .icons import icon_setup
from .icons import icon_object_errors, icon_setup
from .menus import menu_tools, menu_setup
from .permissions_runtime import permission_error_log_view
from .settings import setting_home_view
@@ -155,6 +155,14 @@ class ObjectErrorLogEntryListView(SingleObjectListView):
{'name': _('Result'), 'attribute': 'result'},
),
'hide_object': True,
'no_results_icon': icon_object_errors,
'no_results_text': _(
'This view displays the error log of different object. '
'An empty list is a good thing.'
),
'no_results_title': _(
'There are no error log entries'
),
'object': self.get_object(),
'title': _('Error log entries for: %s' % self.get_object()),
}

View File

@@ -7,11 +7,6 @@ import shutil
from PIL import Image
import PyPDF2
import sh
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
@@ -20,16 +15,14 @@ from mayan.apps.storage.utils import NamedTemporaryFile
from ..classes import ConverterBase
from ..exceptions import PageCountError
from ..settings import setting_graphics_backend_config
from ..settings import setting_graphics_backend_arguments
from ..literals import (
DEFAULT_PDFTOPPM_DPI, DEFAULT_PDFTOPPM_FORMAT, DEFAULT_PDFTOPPM_PATH,
DEFAULT_PDFINFO_PATH
)
pdftoppm_path = yaml.load(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
).get(
pdftoppm_path = setting_graphics_backend_arguments.value.get(
'pdftoppm_path', DEFAULT_PDFTOPPM_PATH
)
@@ -39,26 +32,20 @@ except sh.CommandNotFound:
pdftoppm = None
else:
pdftoppm_format = '-{}'.format(
yaml.load(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
).get(
setting_graphics_backend_arguments.value.get(
'pdftoppm_format', DEFAULT_PDFTOPPM_FORMAT
)
)
pdftoppm_dpi = format(
yaml.load(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
).get(
setting_graphics_backend_arguments.value.get(
'pdftoppm_dpi', DEFAULT_PDFTOPPM_DPI
)
)
pdftoppm = pdftoppm.bake(pdftoppm_format, '-r', pdftoppm_dpi)
pdfinfo_path = yaml.load(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
).get(
pdfinfo_path = setting_graphics_backend_arguments.value.get(
'pdfinfo_path', DEFAULT_PDFINFO_PATH
)

View File

@@ -7,15 +7,10 @@ import shutil
from PIL import Image
import sh
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.serialization import yaml_load
from mayan.apps.mimetype.api import get_mimetype
from mayan.apps.storage.settings import setting_temporary_directory
from mayan.apps.storage.utils import (
@@ -27,16 +22,14 @@ from .literals import (
CONVERTER_OFFICE_FILE_MIMETYPES, DEFAULT_LIBREOFFICE_PATH,
DEFAULT_PAGE_NUMBER, DEFAULT_PILLOW_FORMAT
)
from .settings import setting_graphics_backend_config
from .settings import setting_graphics_backend_arguments
logger = logging.getLogger(__name__)
BACKEND_CONFIG = yaml.load(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
)
libreoffice_path = BACKEND_CONFIG.get(
libreoffice_path = setting_graphics_backend_arguments.value.get(
'libreoffice_path', DEFAULT_LIBREOFFICE_PATH
)
logger = logging.getLogger(__name__)
class ConverterBase(object):
def __init__(self, file_object, mime_type=None):
@@ -62,9 +55,7 @@ class ConverterBase(object):
pass
def get_page(self, output_format=None):
output_format = output_format or yaml.load(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
).get(
output_format = output_format or setting_graphics_backend_arguments.value.get(
'pillow_format', DEFAULT_PILLOW_FORMAT
)

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-06-26 19:04
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('converter', '0013_auto_20180823_2353'),
]
operations = [
migrations.AlterField(
model_name='transformation',
name='name',
field=models.CharField(choices=[('crop', 'Crop: left, top, right, bottom'), ('draw_rectangle', 'Draw rectangle: left, top, right, bottom, fillcolor, outlinecolor, outlinewidth'), ('draw_rectangle_percent', 'Draw rectangle (percents coordinates): left, top, right, bottom, fillcolor, outlinecolor, outlinewidth'), ('flip', 'Flip'), ('gaussianblur', 'Gaussian blur: radius'), ('lineart', 'Line art'), ('mirror', 'Mirror'), ('resize', 'Resize: width, height'), ('rotate', 'Rotate: degrees, fillcolor'), ('rotate180', 'Rotate 180 degrees'), ('rotate270', 'Rotate 270 degrees'), ('rotate90', 'Rotate 90 degrees'), ('unsharpmask', 'Unsharp masking: radius, percent, threshold'), ('zoom', 'Zoom: percent')], max_length=128, verbose_name='Name'),
),
]

View File

@@ -16,22 +16,15 @@ setting_graphics_backend = namespace.add_setting(
help_text=_('Graphics conversion backend to use.'),
global_name='CONVERTER_GRAPHICS_BACKEND',
)
setting_graphics_backend_config = namespace.add_setting(
default='''
{{
libreoffice_path: {},
pdftoppm_dpi: {},
pdftoppm_format: {},
pdftoppm_path: {},
pdfinfo_path: {},
pillow_format: {}
}}
'''.replace('\n', '').format(
DEFAULT_LIBREOFFICE_PATH, DEFAULT_PDFTOPPM_DPI,
DEFAULT_PDFTOPPM_FORMAT, DEFAULT_PDFTOPPM_PATH, DEFAULT_PDFINFO_PATH,
DEFAULT_PILLOW_FORMAT
), help_text=_(
setting_graphics_backend_arguments = namespace.add_setting(
default={
'libreoffice_path': DEFAULT_LIBREOFFICE_PATH,
'pdftoppm_dpi': DEFAULT_PDFTOPPM_DPI,
'pdftoppm_format': DEFAULT_PDFTOPPM_FORMAT,
'pdftoppm_path': DEFAULT_PDFTOPPM_PATH,
'pdfinfo_path': DEFAULT_PDFINFO_PATH,
'pillow_format': DEFAULT_PILLOW_FORMAT,
}, help_text=_(
'Configuration options for the graphics conversion backend.'
), global_name='CONVERTER_GRAPHICS_BACKEND_CONFIG', quoted=True
), global_name='CONVERTER_GRAPHICS_BACKEND_ARGUMENTS'
)

View File

@@ -3,7 +3,7 @@ from __future__ import unicode_literals
import hashlib
import logging
from PIL import Image, ImageColor, ImageFilter
from PIL import Image, ImageColor, ImageDraw, ImageFilter
from django.utils.translation import string_concat, ugettext_lazy as _
from django.utils.encoding import force_bytes
@@ -151,6 +151,214 @@ class TransformationCrop(BaseTransformation):
return self.image.crop((left, top, right, bottom))
class TransformationDrawRectangle(BaseTransformation):
arguments = (
'left', 'top', 'right', 'bottom', 'fillcolor', 'outlinecolor',
'outlinewidth'
)
label = _('Draw rectangle')
name = 'draw_rectangle'
def execute_on(self, *args, **kwargs):
super(TransformationDrawRectangle, self).execute_on(*args, **kwargs)
try:
left = int(self.left or '0')
except ValueError:
left = 0
try:
top = int(self.top or '0')
except ValueError:
top = 0
try:
right = int(self.right or '0')
except ValueError:
right = 0
try:
bottom = int(self.bottom or '0')
except ValueError:
bottom = 0
if left < 0:
left = 0
if left > self.image.size[0] - 1:
left = self.image.size[0] - 1
if top < 0:
top = 0
if top > self.image.size[1] - 1:
top = self.image.size[1] - 1
if right < 0:
right = 0
if right > self.image.size[0] - 1:
right = self.image.size[0] - 1
if bottom < 0:
bottom = 0
if bottom > self.image.size[1] - 1:
bottom = self.image.size[1] - 1
# Invert right value
# Pillow uses left, top, right, bottom to define a viewport
# of real coordinates
# We invert the right and bottom to define a viewport
# that can crop from the right and bottom borders without
# having to know the real dimensions of an image
right = self.image.size[0] - right
bottom = self.image.size[1] - bottom
if left > right:
left = right - 1
if top > bottom:
top = bottom - 1
logger.debug(
'left: %f, top: %f, right: %f, bottom: %f', left, top, right,
bottom
)
fillcolor_value = getattr(self, 'fillcolor', None)
if fillcolor_value:
fill_color = ImageColor.getrgb(fillcolor_value)
else:
fill_color = 0
outlinecolor_value = getattr(self, 'outlinecolor', None)
if outlinecolor_value:
outline_color = ImageColor.getrgb(outlinecolor_value)
else:
outline_color = None
outlinewidth_value = getattr(self, 'outlinewidth', None)
if outlinewidth_value:
outline_width = int(outlinewidth_value)
else:
outline_width = 0
draw = ImageDraw.Draw(self.image)
draw.rectangle(
(left, top, right, bottom), fill=fill_color, outline=outline_color,
width=outline_width
)
return self.image
class TransformationDrawRectanglePercent(BaseTransformation):
arguments = (
'left', 'top', 'right', 'bottom', 'fillcolor', 'outlinecolor',
'outlinewidth'
)
label = _('Draw rectangle (percents coordinates)')
name = 'draw_rectangle_percent'
def execute_on(self, *args, **kwargs):
super(TransformationDrawRectanglePercent, self).execute_on(*args, **kwargs)
try:
left = float(self.left or '0')
except ValueError:
left = 0
try:
top = float(self.top or '0')
except ValueError:
top = 0
try:
right = float(self.right or '0')
except ValueError:
right = 0
try:
bottom = float(self.bottom or '0')
except ValueError:
bottom = 0
if left < 0:
left = 0
if left > 100:
left = 100
if top < 0:
top = 0
if top > 100:
top = 100
if right < 0:
right = 0
if right > 100:
right = 100
if bottom < 0:
bottom = 0
if bottom > 100:
bottom = 100
#if left > right:
# left, right = right, left
#if top > bottom:
# top, bottom = bottom, top
logger.debug(
'left: %f, top: %f, right: %f, bottom: %f', left, top, right,
bottom
)
fillcolor_value = getattr(self, 'fillcolor', None)
if fillcolor_value:
fill_color = ImageColor.getrgb(fillcolor_value)
else:
fill_color = 0
outlinecolor_value = getattr(self, 'outlinecolor', None)
if outlinecolor_value:
outline_color = ImageColor.getrgb(outlinecolor_value)
else:
outline_color = None
outlinewidth_value = getattr(self, 'outlinewidth', None)
if outlinewidth_value:
outline_width = int(outlinewidth_value)
else:
outline_width = 0
left = left / 100.0 * self.image.size[0]
top = top / 100.0 * self.image.size[1]
# Invert right value
# Pillow uses left, top, right, bottom to define a viewport
# of real coordinates
# We invert the right and bottom to define a viewport
# that can crop from the right and bottom borders without
# having to know the real dimensions of an image
right = self.image.size[0] - (right / 100.0 * self.image.size[0])
bottom = self.image.size[1] - (bottom / 100.0 * self.image.size[1])
draw = ImageDraw.Draw(self.image)
draw.rectangle(
(left, top, right, bottom), fill=fill_color, outline=outline_color,
width=outline_width
)
return self.image
class TransformationFlip(BaseTransformation):
arguments = ()
label = _('Flip')
@@ -316,6 +524,8 @@ class TransformationZoom(BaseTransformation):
BaseTransformation.register(transformation=TransformationCrop)
BaseTransformation.register(transformation=TransformationDrawRectangle)
BaseTransformation.register(transformation=TransformationDrawRectanglePercent)
BaseTransformation.register(transformation=TransformationFlip)
BaseTransformation.register(transformation=TransformationGaussianBlur)
BaseTransformation.register(transformation=TransformationLineArt)

View File

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

View File

@@ -31,9 +31,10 @@ from .html_widgets import (
)
from .links import (
link_document_index_instance_list, link_document_type_index_templates,
link_index_instance_menu, link_index_template_setup,
link_index_template_create, link_index_template_document_types,
link_index_template_delete, link_index_template_edit, link_index_template_list,
link_index_instance_menu, link_index_instance_rebuild,
link_index_template_setup, link_index_template_create,
link_index_template_document_types, link_index_template_delete,
link_index_template_edit, link_index_template_list,
link_index_template_node_tree_view, link_index_instances_rebuild,
link_index_template_node_create, link_index_template_node_delete,
link_index_template_node_edit
@@ -100,17 +101,24 @@ class DocumentIndexingApp(MayanAppConfig):
model=IndexInstanceNode, related='index_template_node__index'
)
SourceColumn(
column_index_label = SourceColumn(
attribute='label', is_identifier=True, is_sortable=True,
source=Index
)
column_index_label.add_exclude(source=IndexInstance)
SourceColumn(
attribute='label', is_object_absolute_url=True, is_identifier=True,
is_sortable=True, source=IndexInstance
)
column_index_slug = SourceColumn(
attribute='slug', is_sortable=True, source=Index
)
SourceColumn(
column_index_slug.add_exclude(IndexInstance)
column_index_enabled = SourceColumn(
attribute='enabled', is_sortable=True, source=Index,
widget=TwoStateWidget
)
column_index_enabled.add_exclude(source=IndexInstance)
SourceColumn(
func=lambda context: context[
@@ -192,6 +200,7 @@ class DocumentIndexingApp(MayanAppConfig):
menu_object.bind_links(
links=(
link_index_template_delete, link_index_template_edit,
link_index_instance_rebuild
), sources=(Index,)
)
menu_object.bind_links(

View File

@@ -49,6 +49,12 @@ link_index_instances_rebuild = Link(
),
text=_('Rebuild indexes'), view='indexing:rebuild_index_instances'
)
link_index_instance_rebuild = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_indexing.icons.icon_index_instances_rebuild',
permissions=(permission_document_indexing_rebuild,),
text=_('Rebuild index'), view='indexing:index_setup_rebuild'
)
link_index_template_setup = Link(
condition=get_cascade_condition(

View File

@@ -50,3 +50,10 @@ class IndexViewTestMixin(object):
'label': TEST_INDEX_LABEL_EDITED, 'slug': TEST_INDEX_SLUG
}
)
def _request_test_index_rebuild_view(self):
return self.post(
viewname='indexing:index_setup_rebuild', kwargs={
'pk': self.test_index.pk
}
)

View File

@@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.documents.tests import GenericDocumentViewTestCase
from ..models import Index
from ..models import Index, IndexInstanceNode
from ..permissions import (
permission_document_indexing_create, permission_document_indexing_delete,
permission_document_indexing_edit,
@@ -10,7 +10,10 @@ from ..permissions import (
permission_document_indexing_rebuild
)
from .literals import TEST_INDEX_LABEL, TEST_INDEX_LABEL_EDITED
from .literals import (
TEST_INDEX_LABEL, TEST_INDEX_LABEL_EDITED,
TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION
)
from .mixins import IndexTestMixin, IndexViewTestMixin
@@ -76,6 +79,41 @@ class IndexViewTestCase(
self.test_index.refresh_from_db()
self.assertEqual(self.test_index.label, TEST_INDEX_LABEL_EDITED)
def test_index_rebuild_view_no_permission(self):
self.upload_document()
self._create_test_index()
self.test_index.node_templates.create(
parent=self.test_index.template_root,
expression=TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION,
link_documents=True
)
response = self._request_test_index_rebuild_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(IndexInstanceNode.objects.count(), 0)
def test_index_rebuild_view_with_access(self):
self.upload_document()
self._create_test_index()
self.test_index.node_templates.create(
parent=self.test_index.template_root,
expression=TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION,
link_documents=True
)
self.grant_access(
obj=self.test_index,
permission=permission_document_indexing_rebuild
)
response = self._request_test_index_rebuild_view()
self.assertEqual(response.status_code, 302)
self.assertNotEqual(IndexInstanceNode.objects.count(), 0)
class IndexInstanceViewTestCase(
IndexTestMixin, IndexViewTestMixin, GenericDocumentViewTestCase
@@ -108,9 +146,13 @@ class IndexInstanceViewTestCase(
)
self.assertContains(response, text=TEST_INDEX_LABEL, status_code=200)
class IndexToolsViewTestCase(
IndexTestMixin, IndexViewTestMixin, GenericDocumentViewTestCase
):
def _request_indexes_rebuild_get_view(self):
return self.get(
viewname='indexing:rebuild_index_instances',
viewname='indexing:rebuild_index_instances'
)
def _request_indexes_rebuild_post_view(self):

View File

@@ -11,8 +11,8 @@ from .views import (
DocumentIndexNodeListView, DocumentTypeIndexesView, IndexInstanceNodeView,
IndexListView, IndexesRebuildView, SetupIndexDocumentTypesView,
SetupIndexCreateView, SetupIndexDeleteView, SetupIndexEditView,
SetupIndexListView, SetupIndexTreeTemplateListView, TemplateNodeCreateView,
TemplateNodeDeleteView, TemplateNodeEditView
SetupIndexListView, SetupIndexRebuildView, SetupIndexTreeTemplateListView,
TemplateNodeCreateView, TemplateNodeDeleteView, TemplateNodeEditView
)
urlpatterns = [
@@ -46,6 +46,10 @@ urlpatterns = [
view=SetupIndexDocumentTypesView.as_view(),
name='index_setup_document_types'
),
url(
regex=r'^setup/index/(?P<pk>\d+)/rebuild/$',
view=SetupIndexRebuildView.as_view(), name='index_setup_rebuild'
),
url(
regex=r'^setup/template/node/(?P<pk>\d+)/create/child/$',
view=TemplateNodeCreateView.as_view(), name='template_node_create'

View File

@@ -9,8 +9,8 @@ from django.utils.translation import ugettext_lazy as _, ungettext
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.generics import (
AddRemoveView, FormView, SingleObjectCreateView, SingleObjectDeleteView,
SingleObjectEditView, SingleObjectListView
AddRemoveView, ConfirmView, FormView, SingleObjectCreateView,
SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView
)
from mayan.apps.documents.events import event_document_type_edited
from mayan.apps.documents.models import Document, DocumentType
@@ -32,7 +32,7 @@ from .permissions import (
permission_document_indexing_create, permission_document_indexing_delete,
permission_document_indexing_edit,
permission_document_indexing_instance_view,
permission_document_indexing_view
permission_document_indexing_rebuild, permission_document_indexing_view
)
from .tasks import task_rebuild_index
@@ -150,6 +150,36 @@ class SetupIndexListView(SingleObjectListView):
}
class SetupIndexRebuildView(ConfirmView):
post_action_redirect = reverse_lazy(
viewname='indexing:index_setup_list'
)
def get_extra_context(self):
return {
'object': self.get_object(),
'title': _('Rebuild index: %s') % self.get_object()
}
def get_object(self):
return get_object_or_404(klass=self.get_queryset(), pk=self.kwargs['pk'])
def get_queryset(self):
return AccessControlList.objects.restrict_queryset(
permission=permission_document_indexing_rebuild,
queryset=Index.objects.all(), user=self.request.user
)
def view_action(self):
task_rebuild_index.apply_async(
kwargs=dict(index_id=self.get_object().pk)
)
messages.success(
message='Index queued for rebuild.', request=self.request
)
class SetupIndexDocumentTypesView(AddRemoveView):
main_object_method_add = 'document_types_add'
main_object_method_remove = 'document_types_remove'
@@ -279,6 +309,7 @@ class IndexListView(SingleObjectListView):
def get_extra_context(self):
return {
'hide_links': True,
'hide_object': True,
'no_results_icon': icon_index,
'no_results_main_link': link_index_template_create.resolve(
context=RequestContext(request=self.request)

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.22 on 2019-07-11 05:44
from __future__ import unicode_literals
from django.db import migrations, models
import mayan.apps.document_signatures.models
import mayan.apps.storage.classes
class Migration(migrations.Migration):
dependencies = [
('document_signatures', '0008_auto_20180429_0759'),
]
operations = [
migrations.AlterField(
model_name='detachedsignature',
name='signature_file',
field=models.FileField(blank=True, null=True, storage=mayan.apps.storage.classes.FakeStorageSubclass(), upload_to=mayan.apps.document_signatures.models.upload_to, verbose_name='Signature file'),
),
]

View File

@@ -18,9 +18,9 @@ setting_storage_backend = namespace.add_setting(
)
setting_storage_backend_arguments = namespace.add_setting(
global_name='SIGNATURES_STORAGE_BACKEND_ARGUMENTS',
default='{{location: {}}}'.format(
os.path.join(settings.MEDIA_ROOT, 'document_signatures')
), quoted=True, help_text=_(
default={
'location': os.path.join(settings.MEDIA_ROOT, 'document_signatures')
}, help_text=_(
'Arguments to pass to the SIGNATURE_STORAGE_BACKEND. '
)
)

View File

@@ -1,23 +1,11 @@
from __future__ import unicode_literals
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django.utils.module_loading import import_string
from mayan.apps.storage.utils import get_storage_subclass
from .settings import (
setting_storage_backend, setting_storage_backend_arguments
)
storage_detachedsignature = import_string(
storage_detachedsignature = get_storage_subclass(
dotted_path=setting_storage_backend.value
)(
**yaml.load(
stream=setting_storage_backend_arguments.value or '{}',
Loader=SafeLoader
)
)
)(**setting_storage_backend_arguments.value)

View File

@@ -1,6 +1,8 @@
from __future__ import absolute_import, unicode_literals
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.cache import cache_control, patch_cache_control
from rest_framework import generics
@@ -10,6 +12,7 @@ from mayan.apps.documents.permissions import permission_document_type_view
from mayan.apps.rest_api.filters import MayanObjectPermissionsFilter
from mayan.apps.rest_api.permissions import MayanPermission
from .literals import WORKFLOW_IMAGE_TASK_TIMEOUT
from .models import Workflow
from .permissions import (
permission_workflow_create, permission_workflow_delete,
@@ -23,8 +26,12 @@ from .serializers import (
WritableWorkflowTransitionSerializer
)
from .settings import settings_workflow_image_cache_time
from .storages import storage_workflowimagecache
from .tasks import task_generate_workflow_image
class APIDocumentTypeWorkflowListView(generics.ListAPIView):
class APIDocumentTypeWorkflowRuntimeProxyListView(generics.ListAPIView):
"""
get: Returns a list of all the document type workflows.
"""
@@ -172,7 +179,42 @@ class APIWorkflowDocumentTypeView(generics.RetrieveDestroyAPIView):
self.get_workflow().document_types.remove(instance)
class APIWorkflowListView(generics.ListCreateAPIView):
class APIWorkflowImageView(generics.RetrieveAPIView):
"""
get: Returns an image representation of the selected workflow.
"""
filter_backends = (MayanObjectPermissionsFilter,)
mayan_object_permissions = {
'GET': (permission_workflow_view,),
}
queryset = Workflow.objects.all()
def get_serializer(self, *args, **kwargs):
return None
def get_serializer_class(self):
return None
@cache_control(private=True)
def retrieve(self, request, *args, **kwargs):
task = task_generate_workflow_image.apply_async(
kwargs=dict(
document_state_id=self.get_object().pk,
)
)
cache_filename = task.get(timeout=WORKFLOW_IMAGE_TASK_TIMEOUT)
with storage_workflowimagecache.open(cache_filename) as file_object:
response = HttpResponse(file_object.read(), content_type='image')
if '_hash' in request.GET:
patch_cache_control(
response,
max_age=settings_workflow_image_cache_time.value
)
return response
class APIWorkflowRuntimeProxyListView(generics.ListCreateAPIView):
"""
get: Returns a list of all the workflows.
post: Create a new workflow.
@@ -187,7 +229,7 @@ class APIWorkflowListView(generics.ListCreateAPIView):
if not self.request:
return None
return super(APIWorkflowListView, self).get_serializer(*args, **kwargs)
return super(APIWorkflowRuntimeProxyListView, self).get_serializer(*args, **kwargs)
def get_serializer_class(self):
if self.request.method == 'GET':

View File

@@ -27,30 +27,34 @@ from .dependencies import * # NOQA
from .handlers import (
handler_index_document, handler_launch_workflow, handler_trigger_transition
)
from .html_widgets import WorkflowLogExtraDataWidget, widget_transition_events
from .links import (
link_document_workflow_instance_list, link_setup_document_type_workflows,
link_setup_workflow_document_types, link_setup_workflow_create,
link_setup_workflow_delete, link_setup_workflow_edit,
link_setup_workflow_list, link_setup_workflow_states,
link_setup_workflow_state_action_delete,
link_setup_workflow_state_action_edit,
link_setup_workflow_state_action_list,
link_setup_workflow_state_action_selection,
link_setup_workflow_state_create, link_setup_workflow_state_delete,
link_setup_workflow_state_edit, link_setup_workflow_transitions,
link_setup_workflow_transition_create,
link_setup_workflow_transition_delete, link_setup_workflow_transition_edit,
link_tool_launch_all_workflows, link_workflow_instance_detail,
link_workflow_instance_list, link_document_type_workflow_templates,
link_workflow_template_document_types, link_workflow_template_create,
link_workflow_template_delete, link_workflow_template_edit,
link_workflow_template_list, link_workflow_template_state_list,
link_workflow_template_state_action_delete,
link_workflow_template_state_action_edit,
link_workflow_template_state_action_list,
link_workflow_template_state_action_selection,
link_workflow_template_state_create, link_workflow_template_state_delete,
link_workflow_template_state_edit, link_workflow_template_transition_list,
link_workflow_template_transition_create,
link_workflow_template_transition_delete, link_workflow_template_transition_edit,
link_workflow_template_transition_field_create,
link_workflow_template_transition_field_delete,
link_workflow_template_transition_field_edit,
link_workflow_template_transition_field_list,
link_tool_launch_workflows, link_workflow_instance_detail,
link_workflow_instance_transition, link_workflow_runtime_proxy_document_list,
link_workflow_runtime_proxy_list, link_workflow_preview,
link_workflow_runtime_proxy_list, link_workflow_template_preview,
link_workflow_runtime_proxy_state_document_list, link_workflow_runtime_proxy_state_list,
link_workflow_transition_events
link_workflow_template_transition_events
)
from .permissions import (
permission_workflow_delete, permission_workflow_edit,
permission_workflow_transition, permission_workflow_view
)
from .widgets import widget_transition_events
class DocumentStatesApp(MayanAppConfig):
@@ -86,6 +90,7 @@ class DocumentStatesApp(MayanAppConfig):
WorkflowStateAction = self.get_model('WorkflowStateAction')
WorkflowStateRuntimeProxy = self.get_model('WorkflowStateRuntimeProxy')
WorkflowTransition = self.get_model('WorkflowTransition')
WorkflowTransitionField = self.get_model('WorkflowTransitionField')
WorkflowTransitionTriggerEvent = self.get_model(
'WorkflowTransitionTriggerEvent'
)
@@ -152,6 +157,9 @@ class DocumentStatesApp(MayanAppConfig):
ModelPermission.register_inheritance(
model=WorkflowTransition, related='workflow',
)
ModelPermission.register_inheritance(
model=WorkflowTransitionField, related='transition',
)
ModelPermission.register_inheritance(
model=WorkflowTransitionTriggerEvent,
related='transition__workflow',
@@ -160,9 +168,10 @@ class DocumentStatesApp(MayanAppConfig):
SourceColumn(
attribute='label', is_sortable=True, source=Workflow
)
SourceColumn(
column_workflow_internal_name = SourceColumn(
attribute='internal_name', is_sortable=True, source=Workflow
)
column_workflow_internal_name.add_exclude(source=WorkflowRuntimeProxy)
SourceColumn(
attribute='get_initial_state', empty_value=_('None'),
source=Workflow
@@ -203,12 +212,25 @@ class DocumentStatesApp(MayanAppConfig):
source=WorkflowInstanceLogEntry, label=_('User'), attribute='user'
)
SourceColumn(
source=WorkflowInstanceLogEntry, label=_('Transition'),
attribute='transition'
source=WorkflowInstanceLogEntry,
attribute='transition__origin_state', is_sortable=True
)
SourceColumn(
source=WorkflowInstanceLogEntry, label=_('Comment'),
attribute='comment'
source=WorkflowInstanceLogEntry,
attribute='transition', is_sortable=True
)
SourceColumn(
source=WorkflowInstanceLogEntry,
attribute='transition__destination_state', is_sortable=True
)
SourceColumn(
source=WorkflowInstanceLogEntry,
attribute='comment', is_sortable=True
)
SourceColumn(
source=WorkflowInstanceLogEntry,
attribute='get_extra_data', label=_('Additional details'),
widget=WorkflowLogExtraDataWidget
)
SourceColumn(
@@ -256,45 +278,92 @@ class DocumentStatesApp(MayanAppConfig):
)
)
SourceColumn(
<<<<<<< HEAD
=======
attribute='name', is_identifier=True, is_sortable=True,
source=WorkflowTransitionField
)
SourceColumn(
attribute='label', is_sortable=True, source=WorkflowTransitionField
)
SourceColumn(
attribute='get_field_type_display', label=_('Type'),
source=WorkflowTransitionField
)
SourceColumn(
attribute='required', is_sortable=True,
source=WorkflowTransitionField, widget=TwoStateWidget
)
SourceColumn(
attribute='get_widget_display', label=_('Widget'),
is_sortable=False, source=WorkflowTransitionField
)
SourceColumn(
attribute='widget_kwargs', is_sortable=True,
source=WorkflowTransitionField
)
SourceColumn(
>>>>>>> versions/minor
source=WorkflowRuntimeProxy, label=_('Documents'),
func=lambda context: context['object'].get_document_count(
user=context['request'].user
), order=99
)
SourceColumn(
source=WorkflowStateRuntimeProxy, label=_('Documents'),
func=lambda context: context['object'].get_document_count(
user=context['request'].user
), order=99
)
menu_facet.bind_links(
links=(link_document_workflow_instance_list,), sources=(Document,)
links=(link_workflow_instance_list,), sources=(Document,)
)
menu_list_facet.bind_links(
links=(
link_acl_list, link_events_for_object,
link_object_event_types_user_subcriptions_list,
link_setup_workflow_document_types,
link_setup_workflow_states, link_setup_workflow_transitions,
link_workflow_preview
link_workflow_template_document_types,
link_workflow_template_state_list, link_workflow_template_transition_list,
link_workflow_template_preview
), sources=(Workflow,)
)
menu_list_facet.bind_links(
links=(
link_setup_document_type_workflows,
link_document_type_workflow_templates,
), sources=(DocumentType,)
)
menu_main.bind_links(links=(link_workflow_runtime_proxy_list,), position=10)
menu_object.bind_links(
links=(
link_setup_workflow_delete, link_setup_workflow_edit
link_workflow_template_delete, link_workflow_template_edit
), sources=(Workflow,)
)
menu_object.bind_links(
links=(
link_setup_workflow_state_edit,
link_setup_workflow_state_action_list,
link_setup_workflow_state_delete
link_workflow_template_state_edit,
link_workflow_template_state_action_list,
link_workflow_template_state_delete
), sources=(WorkflowState,)
)
menu_object.bind_links(
links=(
link_setup_workflow_transition_edit,
link_workflow_transition_events, link_acl_list,
link_setup_workflow_transition_delete
link_workflow_template_transition_edit,
link_workflow_template_transition_events,
link_workflow_template_transition_field_list, link_acl_list,
link_workflow_template_transition_delete
), sources=(WorkflowTransition,)
)
menu_object.bind_links(
links=(
link_workflow_template_transition_field_delete,
link_workflow_template_transition_field_edit
), sources=(WorkflowTransitionField,)
)
menu_object.bind_links(
links=(
link_workflow_instance_detail,
@@ -315,17 +384,23 @@ class DocumentStatesApp(MayanAppConfig):
)
menu_object.bind_links(
links=(
link_setup_workflow_state_action_edit,
link_workflow_template_state_action_edit,
link_object_error_list,
link_setup_workflow_state_action_delete,
link_workflow_template_state_action_delete,
), sources=(WorkflowStateAction,)
)
menu_secondary.bind_links(
links=(link_setup_workflow_list, link_setup_workflow_create),
links=(link_workflow_template_list, link_workflow_template_create),
sources=(
Workflow, 'document_states:setup_workflow_create',
'document_states:setup_workflow_list'
Workflow, 'document_states:workflow_template_create',
'document_states:workflow_template_list'
)
)
menu_secondary.bind_links(
links=(link_workflow_template_transition_field_create,),
sources=(
WorkflowTransition,
)
)
menu_secondary.bind_links(
@@ -335,31 +410,31 @@ class DocumentStatesApp(MayanAppConfig):
)
)
menu_secondary.bind_links(
links=(link_setup_workflow_state_action_selection,),
links=(link_workflow_template_state_action_selection,),
sources=(
WorkflowState,
)
)
menu_secondary.bind_links(
links=(
link_setup_workflow_transition_create,
link_workflow_template_transition_create,
), sources=(
WorkflowTransition,
'document_states:setup_workflow_transition_list',
'document_states:workflow_template_transition_list',
)
)
menu_secondary.bind_links(
links=(
link_setup_workflow_state_create,
link_workflow_template_state_create,
), sources=(
WorkflowState,
'document_states:setup_workflow_state_list',
'document_states:workflow_template_state_list',
)
)
menu_setup.bind_links(links=(link_setup_workflow_list,))
menu_setup.bind_links(links=(link_workflow_template_list,))
menu_tools.bind_links(links=(link_tool_launch_all_workflows,))
menu_tools.bind_links(links=(link_tool_launch_workflows,))
post_save.connect(
dispatch_uid='workflows_handler_launch_workflow',

View File

@@ -0,0 +1,9 @@
from __future__ import absolute_import, unicode_literals
from django import forms
from .widgets import WorkflowImageWidget
class WorfklowImageField(forms.fields.Field):
widget = WorkflowImageWidget

View File

@@ -12,10 +12,10 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.forms import DynamicModelForm
from .classes import WorkflowAction
from .fields import WorfklowImageField
from .models import (
Workflow, WorkflowState, WorkflowStateAction, WorkflowTransition
)
from .widgets import WorkflowImageWidget
class WorkflowActionSelectionForm(forms.Form):
@@ -165,32 +165,25 @@ WorkflowTransitionTriggerEventRelationshipFormSet = formset_factory(
)
class WorkflowInstanceTransitionForm(forms.Form):
class WorkflowInstanceTransitionSelectForm(forms.Form):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
workflow_instance = kwargs.pop('workflow_instance')
super(WorkflowInstanceTransitionForm, self).__init__(*args, **kwargs)
super(WorkflowInstanceTransitionSelectForm, self).__init__(*args, **kwargs)
self.fields[
'transition'
].queryset = workflow_instance.get_transition_choices(_user=user)
transition = forms.ModelChoiceField(
help_text=_('Select a transition to execute in the next step.'),
label=_('Transition'), queryset=WorkflowTransition.objects.none()
)
comment = forms.CharField(
help_text=_('Optional comment to attach to the transition.'),
label=_('Comment'), required=False, widget=forms.widgets.Textarea(
attrs={
'rows': 3
}
)
)
class WorkflowPreviewForm(forms.Form):
preview = forms.CharField(widget=WorkflowImageWidget())
workflow = WorfklowImageField()
def __init__(self, *args, **kwargs):
instance = kwargs.pop('instance', None)
super(WorkflowPreviewForm, self).__init__(*args, **kwargs)
self.fields['preview'].initial = instance
self.fields['workflow'].initial = instance

View File

@@ -0,0 +1,25 @@
from __future__ import unicode_literals
from django.template.loader import render_to_string
from django.utils.html import format_html_join
def widget_transition_events(transition):
return format_html_join(
sep='\n', format_string='<div class="">{}</div>', args_generator=(
(
transition_trigger.event_type.label,
) for transition_trigger in transition.trigger_events.all()
)
)
class WorkflowLogExtraDataWidget(object):
template_name = 'document_states/extra_data.html'
def render(self, name=None, value=None):
return render_to_string(
template_name=self.template_name, context={
'value': value
}
)

View File

@@ -3,43 +3,42 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon
from mayan.apps.documents.icons import icon_document, icon_document_type
icon_workflow = Icon(driver_name='fontawesome', symbol='sitemap')
icon_document_type_workflow_list = icon_workflow
icon_tool_launch_workflows = icon_workflow
icon_document_workflow_instance_list = Icon(
driver_name='fontawesome', symbol='sitemap'
)
icon_setup_workflow_list = Icon(driver_name='fontawesome', symbol='sitemap')
icon_tool_launch_all_workflows = Icon(
driver_name='fontawesome', symbol='sitemap'
)
icon_workflow_create = Icon(
icon_document_type_workflow_list = icon_workflow
icon_workflow_template_create = Icon(
driver_name='fontawesome-dual', primary_symbol='sitemap',
secondary_symbol='plus'
)
icon_workflow_delete = Icon(driver_name='fontawesome', symbol='times')
icon_workflow_document_type_list = icon_document_type
icon_workflow_edit = Icon(driver_name='fontawesome', symbol='pencil-alt')
icon_workflow_list = Icon(driver_name='fontawesome', symbol='sitemap')
icon_workflow_preview = Icon(driver_name='fontawesome', symbol='eye')
icon_workflow_template_delete = Icon(driver_name='fontawesome', symbol='times')
icon_workflow_template_document_type_list = icon_document_type
icon_workflow_template_edit = Icon(
driver_name='fontawesome', symbol='pencil-alt'
)
icon_workflow_template_list = icon_workflow
icon_workflow_template_preview = Icon(driver_name='fontawesome', symbol='eye')
# Workflow instances
icon_workflow_instance_detail = Icon(driver_name='fontawesome', symbol='sitemap')
icon_workflow_instance_detail = icon_workflow
icon_workflow_instance_list = icon_workflow
icon_workflow_instance_transition = Icon(
driver_name='fontawesome', symbol='arrows-alt-h'
)
# Workflow runtime proxies
icon_workflow_runtime_proxy_document_list = icon_document
icon_workflow_runtime_proxy_list = Icon(
driver_name='fontawesome', symbol='sitemap'
)
icon_workflow_runtime_proxy_list = icon_workflow
icon_workflow_runtime_proxy_state_document_list = icon_document
icon_workflow_runtime_proxy_state_list = Icon(
driver_name='fontawesome', symbol='circle'
)
# Workflow transition states
icon_workflow_state_action_delete = Icon(
driver_name='fontawesome', symbol='times'
)
@@ -57,14 +56,25 @@ icon_workflow_state_create = Icon(
icon_workflow_state_delete = Icon(driver_name='fontawesome', symbol='times')
icon_workflow_state_edit = Icon(driver_name='fontawesome', symbol='pencil-alt')
# Workflow transition state actions
icon_workflow_state_action = Icon(driver_name='fontawesome', symbol='code')
icon_workflow_state_action_delete = Icon(driver_name='fontawesome', symbol='times')
icon_workflow_state_action_edit = Icon(driver_name='fontawesome', symbol='pencil-alt')
icon_workflow_state_action_delete = Icon(
driver_name='fontawesome', symbol='times'
)
icon_workflow_state_action_edit = Icon(
driver_name='fontawesome', symbol='pencil-alt'
)
icon_workflow_state_action_selection = Icon(
driver_name='fontawesome-dual', primary_symbol='code',
secondary_symbol='plus'
)
icon_workflow_state_action_list = Icon(driver_name='fontawesome', symbol='code')
icon_workflow_state_action_list = Icon(
driver_name='fontawesome', symbol='code'
)
# Workflow transitions
icon_workflow_transition = Icon(
driver_name='fontawesome', symbol='arrows-alt-h'
)
@@ -72,10 +82,31 @@ icon_workflow_transition_create = Icon(
driver_name='fontawesome-dual', primary_symbol='arrows-alt-h',
secondary_symbol='plus'
)
icon_workflow_transition_delete = Icon(driver_name='fontawesome', symbol='times')
icon_workflow_transition_delete = Icon(
driver_name='fontawesome', symbol='times'
)
icon_workflow_transition_edit = Icon(
driver_name='fontawesome', symbol='pencil-alt'
)
# Workflow transition fields
icon_workflow_transition_field = Icon(
driver_name='fontawesome', symbol='table'
)
icon_workflow_transition_field_delete = Icon(
driver_name='fontawesome', symbol='times'
)
icon_workflow_transition_field_edit = Icon(
driver_name='fontawesome', symbol='pencil-alt'
)
icon_workflow_transition_field_create = Icon(
driver_name='fontawesome-dual', primary_symbol='table',
secondary_symbol='plus'
)
icon_workflow_transition_field_list = Icon(
driver_name='fontawesome', symbol='table'
)
icon_workflow_transition_triggers = Icon(
driver_name='fontawesome', symbol='bolt'
)

View File

@@ -11,180 +11,225 @@ from .permissions import (
permission_workflow_view,
)
link_setup_document_type_workflows = Link(
# Workflow templates
link_document_type_workflow_templates = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_document_type_workflow_list',
permissions=(permission_document_type_edit,), text=_('Workflows'),
view='document_states:document_type_workflows',
view='document_states:document_type_workflow_templates',
)
link_setup_workflow_create = Link(
icon_class_path='mayan.apps.document_states.icons.icon_workflow_create',
link_workflow_template_create = Link(
icon_class_path='mayan.apps.document_states.icons.icon_workflow_template_create',
permissions=(permission_workflow_create,),
text=_('Create workflow'), view='document_states:setup_workflow_create'
text=_('Create workflow'), view='document_states:workflow_template_create'
)
link_setup_workflow_delete = Link(
link_workflow_template_delete = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_delete',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_template_delete',
permissions=(permission_workflow_delete,),
tags='dangerous', text=_('Delete'),
view='document_states:setup_workflow_delete',
view='document_states:workflow_template_delete',
)
link_setup_workflow_document_types = Link(
link_workflow_template_document_types = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_document_type_list',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_template_document_type_list',
permissions=(permission_workflow_edit,), text=_('Document types'),
view='document_states:setup_workflow_document_types',
view='document_states:workflow_template_document_types',
)
link_setup_workflow_edit = Link(
link_workflow_template_edit = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_edit',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_template_edit',
permissions=(permission_workflow_edit,),
text=_('Edit'), view='document_states:setup_workflow_edit',
text=_('Edit'), view='document_states:workflow_template_edit',
)
link_setup_workflow_list = Link(
icon_class_path='mayan.apps.document_states.icons.icon_setup_workflow_list',
link_workflow_template_list = Link(
icon_class_path='mayan.apps.document_states.icons.icon_workflow_template_list',
permissions=(permission_workflow_view,), text=_('Workflows'),
view='document_states:setup_workflow_list'
view='document_states:workflow_template_list'
)
link_setup_workflow_state_action_delete = Link(
link_workflow_template_preview = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_template_preview',
permissions=(permission_workflow_view,),
text=_('Preview'), view='document_states:workflow_template_preview'
)
# Workflow template state actions
link_workflow_template_state_action_delete = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_state_action_delete',
permissions=(permission_workflow_edit,),
tags='dangerous', text=_('Delete'),
view='document_states:setup_workflow_state_action_delete',
view='document_states:workflow_template_state_action_delete',
)
link_setup_workflow_state_action_edit = Link(
link_workflow_template_state_action_edit = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_state_action_edit',
permissions=(permission_workflow_edit,),
text=_('Edit'), view='document_states:setup_workflow_state_action_edit',
text=_('Edit'), view='document_states:workflow_template_state_action_edit',
)
link_setup_workflow_state_action_list = Link(
link_workflow_template_state_action_list = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_state_action_list',
permissions=(permission_workflow_edit,),
text=_('Actions'),
view='document_states:setup_workflow_state_action_list',
view='document_states:workflow_template_state_action_list',
)
link_setup_workflow_state_action_selection = Link(
link_workflow_template_state_action_selection = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_state_action',
permissions=(permission_workflow_edit,), text=_('Create action'),
view='document_states:setup_workflow_state_action_selection',
view='document_states:workflow_template_state_action_selection',
)
link_setup_workflow_state_create = Link(
# Workflow template states
link_workflow_template_state_create = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_state_create',
permissions=(permission_workflow_edit,), text=_('Create state'),
view='document_states:setup_workflow_state_create',
view='document_states:workflow_template_state_create',
)
link_setup_workflow_state_delete = Link(
link_workflow_template_state_delete = Link(
args='object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_state_delete',
permissions=(permission_workflow_edit,),
tags='dangerous', text=_('Delete'),
view='document_states:setup_workflow_state_delete',
view='document_states:workflow_template_state_delete',
)
link_setup_workflow_state_edit = Link(
link_workflow_template_state_edit = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_state_edit',
permissions=(permission_workflow_edit,),
text=_('Edit'), view='document_states:setup_workflow_state_edit',
text=_('Edit'), view='document_states:workflow_template_state_edit',
)
link_setup_workflow_states = Link(
link_workflow_template_state_list = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_state',
permissions=(permission_workflow_view,), text=_('States'),
view='document_states:setup_workflow_state_list',
view='document_states:workflow_template_state_list',
)
link_setup_workflow_transition_create = Link(
# Workflow template transitions
link_workflow_template_transition_create = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_create',
permissions=(permission_workflow_edit,), text=_('Create transition'),
view='document_states:setup_workflow_transition_create',
view='document_states:workflow_template_transition_create',
)
link_setup_workflow_transition_delete = Link(
link_workflow_template_transition_delete = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_delete',
permissions=(permission_workflow_edit,),
tags='dangerous', text=_('Delete'),
view='document_states:setup_workflow_transition_delete',
view='document_states:workflow_template_transition_delete',
)
link_setup_workflow_transition_edit = Link(
link_workflow_template_transition_edit = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_edit',
permissions=(permission_workflow_edit,),
text=_('Edit'), view='document_states:setup_workflow_transition_edit',
text=_('Edit'), view='document_states:workflow_template_transition_edit',
)
link_setup_workflow_transitions = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition',
permissions=(permission_workflow_view,), text=_('Transitions'),
view='document_states:setup_workflow_transition_list',
)
link_workflow_transition_events = Link(
link_workflow_template_transition_events = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_triggers',
permissions=(permission_workflow_edit,),
text=_('Transition triggers'),
view='document_states:setup_workflow_transition_events'
view='document_states:workflow_template_transition_events'
)
link_workflow_preview = Link(
link_workflow_template_transition_list = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_preview',
permissions=(permission_workflow_view,),
text=_('Preview'), view='document_states:workflow_preview'
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition',
permissions=(permission_workflow_view,), text=_('Transitions'),
view='document_states:workflow_template_transition_list',
)
link_tool_launch_all_workflows = Link(
icon_class_path='mayan.apps.document_states.icons.icon_tool_launch_all_workflows',
permissions=(permission_workflow_tools,),
text=_('Launch all workflows'),
view='document_states:tool_launch_all_workflows'
# Workflow transition fields
link_workflow_template_transition_field_create = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_field',
permissions=(permission_workflow_edit,), text=_('Create field'),
view='document_states:workflow_template_transition_field_create',
)
link_workflow_template_transition_field_delete = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_field_delete',
permissions=(permission_workflow_edit,),
tags='dangerous', text=_('Delete'),
view='document_states:workflow_template_transition_field_delete',
)
link_workflow_template_transition_field_edit = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_field_edit',
permissions=(permission_workflow_edit,),
text=_('Edit'), view='document_states:workflow_template_transition_field_edit',
)
link_workflow_template_transition_field_list = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_field_list',
permissions=(permission_workflow_edit,),
text=_('Fields'),
view='document_states:workflow_template_transition_field_list',
)
# Document workflow instances
link_document_workflow_instance_list = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_document_workflow_instance_list',
permissions=(permission_workflow_view,), text=_('Workflows'),
view='document_states:document_workflow_instance_list',
)
link_workflow_instance_detail = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_instance_detail',
permissions=(permission_workflow_view,),
text=_('Detail'), view='document_states:workflow_instance_detail',
)
link_workflow_instance_list = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_instance_list',
permissions=(permission_workflow_view,), text=_('Workflows'),
view='document_states:workflow_instance_list',
)
link_workflow_instance_transition = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_instance_transition',
text=_('Transition'),
view='document_states:workflow_instance_transition',
view='document_states:workflow_instance_transition_selection',
)
# Runtime proxies
link_workflow_runtime_proxy_document_list = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_runtime_proxy_document_list',
permissions=(permission_workflow_view,),
text=_('Workflow documents'),
view='document_states:workflow_document_list',
view='document_states:workflow_runtime_proxy_document_list',
)
link_workflow_runtime_proxy_list = Link(
icon_class_path='mayan.apps.document_states.icons.icon_workflow_runtime_proxy_list',
permissions=(permission_workflow_view,),
text=_('Workflows'), view='document_states:workflow_list'
text=_('Workflows'), view='document_states:workflow_runtime_proxy_list'
)
link_workflow_runtime_proxy_state_document_list = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_runtime_proxy_state_document_list',
permissions=(permission_workflow_view,),
text=_('State documents'),
view='document_states:workflow_state_document_list',
view='document_states:workflow_runtime_proxy_state_document_list',
)
link_workflow_runtime_proxy_state_list = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_runtime_proxy_state_list',
permissions=(permission_workflow_view,),
text=_('States'), view='document_states:workflow_state_list',
text=_('States'), view='document_states:workflow_runtime_proxy_state_list',
)
# Tools
link_tool_launch_workflows = Link(
icon_class_path='mayan.apps.document_states.icons.icon_tool_launch_workflows',
permissions=(permission_workflow_tools,),
text=_('Launch all workflows'),
view='document_states:tool_launch_workflows'
)

View File

@@ -2,6 +2,27 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
FIELD_TYPE_CHOICE_CHAR = 1
FIELD_TYPE_CHOICE_INTEGER = 2
FIELD_TYPE_CHOICES = (
(FIELD_TYPE_CHOICE_CHAR, _('Character')),
(FIELD_TYPE_CHOICE_INTEGER, _('Number (Integer)')),
)
FIELD_TYPE_MAPPING = {
FIELD_TYPE_CHOICE_CHAR: 'django.forms.CharField',
FIELD_TYPE_CHOICE_INTEGER: 'django.forms.IntegerField',
}
WIDGET_CLASS_TEXTAREA = 1
WIDGET_CLASS_CHOICES = (
(WIDGET_CLASS_TEXTAREA, _('Text area')),
)
WIDGET_CLASS_MAPPING = {
WIDGET_CLASS_TEXTAREA: 'django.forms.widgets.Textarea',
}
WORKFLOW_ACTION_ON_ENTRY = 1
WORKFLOW_ACTION_ON_EXIT = 2
@@ -9,3 +30,4 @@ WORKFLOW_ACTION_WHEN_CHOICES = (
(WORKFLOW_ACTION_ON_ENTRY, _('On entry')),
(WORKFLOW_ACTION_ON_EXIT, _('On exit')),
)
WORKFLOW_IMAGE_TASK_TIMEOUT = 60

View File

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-01 04:54
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('document_states', '0013_auto_20190423_0810'),
]
operations = [
migrations.CreateModel(
name='WorkflowTransitionField',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('field_type', models.PositiveIntegerField(choices=[(1, 'Character'), (2, 'Number (Integer)')], verbose_name='Type')),
('name', models.CharField(help_text='The name that will be used to identify this field in other parts of the workflow system.', max_length=128, verbose_name='Internal name')),
('label', models.CharField(help_text='The field name that will be shown on the user interface.', max_length=128, verbose_name='Label')),
('help_text', models.TextField(blank=True, help_text='An optional message that will help users better understand the purpose of the field and data to provide.', verbose_name='Help text')),
('required', models.BooleanField(default=False, help_text='Whether this fields needs to be filled out or not to proceed.', verbose_name='Required')),
('transition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='document_states.WorkflowTransition', verbose_name='Transition')),
],
options={
'verbose_name': 'Workflow transition trigger event',
'verbose_name_plural': 'Workflow transitions trigger events',
},
),
migrations.AddField(
model_name='workflowinstance',
name='context',
field=models.TextField(blank=True, verbose_name='Backend data'),
),
migrations.AddField(
model_name='workflowinstancelogentry',
name='extra_data',
field=models.TextField(blank=True, verbose_name='Extra data'),
),
migrations.AlterUniqueTogether(
name='workflowtransitionfield',
unique_together=set([('transition', 'name')]),
),
]

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-01 13:11
from __future__ import unicode_literals
from django.db import migrations, models
import mayan.apps.common.validators
class Migration(migrations.Migration):
dependencies = [
('document_states', '0014_auto_20190701_0454'),
]
operations = [
migrations.AddField(
model_name='workflowtransitionfield',
name='widget',
field=models.PositiveIntegerField(blank=True, choices=[(1, 'Text area')], help_text='An optional class to change the default presentation of the field.', null=True, verbose_name='Widget class'),
),
migrations.AddField(
model_name='workflowtransitionfield',
name='widget_kwargs',
field=models.TextField(blank=True, help_text='A group of keyword arguments to customize the widget. Use YAML format.', validators=[mayan.apps.common.validators.YAMLValidator()], verbose_name='Widget keyword arguments'),
),
migrations.AlterField(
model_name='workflowinstance',
name='context',
field=models.TextField(blank=True, verbose_name='Context'),
),
]

View File

@@ -1,12 +1,21 @@
from __future__ import absolute_import, unicode_literals
import hashlib
import json
import logging
from furl import furl
from graphviz import Digraph
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django.conf import settings
from django.core import serializers
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.files.base import ContentFile
from django.db import IntegrityError, models, transaction
from django.db.models import F, Max, Q
from django.urls import reverse
@@ -15,18 +24,20 @@ from django.utils.module_loading import import_string
from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.validators import validate_internal_name
from mayan.apps.common.validators import YAMLValidator, validate_internal_name
from mayan.apps.documents.models import Document, DocumentType
from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.events.models import StoredEventType
from .error_logs import error_log_state_actions
from .events import event_workflow_created, event_workflow_edited
from .literals import (
WORKFLOW_ACTION_WHEN_CHOICES, WORKFLOW_ACTION_ON_ENTRY,
WORKFLOW_ACTION_ON_EXIT
FIELD_TYPE_CHOICES, WIDGET_CLASS_CHOICES, WORKFLOW_ACTION_WHEN_CHOICES,
WORKFLOW_ACTION_ON_ENTRY, WORKFLOW_ACTION_ON_EXIT
)
from .managers import WorkflowManager
from .permissions import permission_workflow_transition
from .storages import storage_workflowimagecache
logger = logging.getLogger(__name__)
@@ -63,9 +74,49 @@ class Workflow(models.Model):
def __str__(self):
return self.label
def generate_image(self):
cache_filename = '{}-{}'.format(self.id, self.get_hash())
image = self.render()
# Since open "wb+" doesn't create files, check if the file
# exists, if not then create it
if not storage_workflowimagecache.exists(cache_filename):
storage_workflowimagecache.save(
name=cache_filename, content=ContentFile(content='')
)
with storage_workflowimagecache.open(cache_filename, mode='wb+') as file_object:
file_object.write(image)
return cache_filename
def get_api_image_url(self, *args, **kwargs):
final_url = furl()
final_url.args = kwargs
final_url.path = reverse(
viewname='rest_api:workflow-image',
kwargs={'pk': self.pk}
)
final_url.args['_hash'] = self.get_hash()
return final_url.tostr()
def get_document_types_not_in_workflow(self):
return DocumentType.objects.exclude(pk__in=self.document_types.all())
def get_hash(self):
objects_lists = list(
Workflow.objects.filter(pk=self.pk)
) + list(
WorkflowState.objects.filter(workflow__pk=self.pk)
) + list(
WorkflowTransition.objects.filter(workflow__pk=self.pk)
)
return hashlib.sha256(
serializers.serialize('json', objects_lists)
).hexdigest()
def get_initial_state(self):
try:
return self.states.get(initial=True)
@@ -362,6 +413,61 @@ class WorkflowTransition(models.Model):
return self.label
@python_2_unicode_compatible
class WorkflowTransitionField(models.Model):
transition = models.ForeignKey(
on_delete=models.CASCADE, related_name='fields',
to=WorkflowTransition, verbose_name=_('Transition')
)
field_type = models.PositiveIntegerField(
choices=FIELD_TYPE_CHOICES, verbose_name=_('Type')
)
name = models.CharField(
help_text=_(
'The name that will be used to identify this field in other parts '
'of the workflow system.'
), max_length=128, verbose_name=_('Internal name')
)
label = models.CharField(
help_text=_(
'The field name that will be shown on the user interface.'
), max_length=128, verbose_name=_('Label'))
help_text = models.TextField(
blank=True, help_text=_(
'An optional message that will help users better understand the '
'purpose of the field and data to provide.'
), verbose_name=_('Help text')
)
required = models.BooleanField(
default=False, help_text=_(
'Whether this fields needs to be filled out or not to proceed.'
), verbose_name=_('Required')
)
widget = models.PositiveIntegerField(
blank=True, choices=WIDGET_CLASS_CHOICES, help_text=_(
'An optional class to change the default presentation of the field.'
), null=True, verbose_name=_('Widget class')
)
widget_kwargs = models.TextField(
blank=True, help_text=_(
'A group of keyword arguments to customize the widget. '
'Use YAML format.'
), validators=[YAMLValidator()],
verbose_name=_('Widget keyword arguments')
)
class Meta:
unique_together = ('transition', 'name')
verbose_name = _('Workflow transition trigger event')
verbose_name_plural = _('Workflow transitions trigger events')
def __str__(self):
return self.label
def get_widget_kwargs(self):
return yaml.load(stream=self.widget_kwargs, Loader=SafeLoader)
@python_2_unicode_compatible
class WorkflowTransitionTriggerEvent(models.Model):
transition = models.ForeignKey(
@@ -391,6 +497,9 @@ class WorkflowInstance(models.Model):
on_delete=models.CASCADE, related_name='workflows', to=Document,
verbose_name=_('Document')
)
context = models.TextField(
blank=True, verbose_name=_('Context')
)
class Meta:
ordering = ('workflow',)
@@ -401,16 +510,31 @@ class WorkflowInstance(models.Model):
def __str__(self):
return force_text(self.workflow)
def do_transition(self, transition, user=None, comment=None):
def do_transition(self, transition, extra_data=None, user=None, comment=None):
with transaction.atomic():
try:
if transition in self.get_current_state().origin_transitions.all():
if extra_data:
context = self.loads()
context.update(extra_data)
self.dumps(context=context)
self.log_entries.create(
comment=comment or '', transition=transition, user=user
comment=comment or '',
extra_data=json.dumps(extra_data or {}),
transition=transition, user=user
)
except AttributeError:
# No initial state has been set for this workflow
pass
def dumps(self, context):
"""
Serialize the context data.
"""
self.context = json.dumps(context)
self.save()
def get_absolute_url(self):
return reverse(
viewname='document_states:workflow_instance_detail', kwargs={
@@ -419,10 +543,12 @@ class WorkflowInstance(models.Model):
)
def get_context(self):
return {
context = {
'document': self.document, 'workflow': self.workflow,
'workflow_instance': self,
}
context['workflow_instance_context'] = self.loads()
return context
def get_current_state(self):
"""
@@ -488,6 +614,12 @@ class WorkflowInstance(models.Model):
"""
return WorkflowTransition.objects.none()
def loads(self):
"""
Deserialize the context data.
"""
return json.loads(self.context or '{}')
@python_2_unicode_compatible
class WorkflowInstanceLogEntry(models.Model):
@@ -514,6 +646,7 @@ class WorkflowInstanceLogEntry(models.Model):
to=settings.AUTH_USER_MODEL, verbose_name=_('User')
)
comment = models.TextField(blank=True, verbose_name=_('Comment'))
extra_data = models.TextField(blank=True, verbose_name=_('Extra data'))
class Meta:
ordering = ('datetime',)
@@ -527,7 +660,21 @@ class WorkflowInstanceLogEntry(models.Model):
if self.transition not in self.workflow_instance.get_transition_choices(_user=self.user):
raise ValidationError(_('Not a valid transition choice.'))
def get_extra_data(self):
result = {}
for key, value in self.loads().items():
result[self.transition.fields.get(name=key).label] = value
return result
def loads(self):
"""
Deserialize the context data.
"""
return json.loads(self.extra_data or '{}')
def save(self, *args, **kwargs):
with transaction.atomic():
result = super(WorkflowInstanceLogEntry, self).save(*args, **kwargs)
context = self.workflow_instance.get_context()
context.update(
@@ -561,9 +708,30 @@ class WorkflowRuntimeProxy(Workflow):
verbose_name = _('Workflow runtime proxy')
verbose_name_plural = _('Workflow runtime proxies')
def get_document_count(self, user):
"""
Return the numeric count of documents executing this workflow.
The count is filtered by access.
"""
return AccessControlList.objects.restrict_queryset(
permission=permission_document_view,
queryset=Document.objects.filter(workflows__workflow=self),
user=user
).count()
class WorkflowStateRuntimeProxy(WorkflowState):
class Meta:
proxy = True
verbose_name = _('Workflow state runtime proxy')
verbose_name_plural = _('Workflow state runtime proxies')
def get_document_count(self, user):
"""
Return the numeric count of documents at this workflow state.
The count is filtered by access.
"""
return AccessControlList.objects.restrict_queryset(
permission=permission_document_view, queryset=self.get_documents(),
user=user
).count()

View File

@@ -3,12 +3,21 @@ from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.task_manager.classes import CeleryQueue
from mayan.apps.task_manager.workers import worker_slow
from mayan.apps.task_manager.workers import worker_fast, worker_slow
queue_document_states = CeleryQueue(
name='document_states', label=_('Document states'), worker=worker_slow
label=_('Document states'), name='document_states', worker=worker_slow
)
queue_document_states_fast = CeleryQueue(
label=_('Document states fast'), name='document_states_fast',
worker=worker_fast
)
queue_document_states.add_task_type(
dotted_path='mayan.apps.document_states.tasks.task_launch_all_workflows',
label=_('Launch all workflows')
label=_('Launch all workflows'),
dotted_path='mayan.apps.document_states.tasks.task_launch_all_workflows'
)
queue_document_states_fast.add_task_type(
label=_('Generate workflow previews'),
dotted_path='mayan.apps.document_states.tasks.task_generate_workflow_image'
)

View File

@@ -0,0 +1,32 @@
from __future__ import unicode_literals
import os
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from mayan.apps.smart_settings.classes import Namespace
namespace = Namespace(label=_('Workflows'), name='document_states')
settings_workflow_image_cache_time = namespace.add_setting(
global_name='WORKFLOWS_IMAGE_CACHE_TIME', default='31556926',
help_text=_(
'Time in seconds that the browser should cache the supplied workflow '
'images. The default of 31559626 seconds corresponde to 1 year.'
)
)
setting_workflowimagecache_storage = namespace.add_setting(
global_name='WORKFLOWS_IMAGE_CACHE_STORAGE_BACKEND',
default='django.core.files.storage.FileSystemStorage', help_text=_(
'Path to the Storage subclass to use when storing the cached '
'workflow image files.'
)
)
setting_workflowimagecache_storage_arguments = namespace.add_setting(
global_name='WORKFLOWS_IMAGE_CACHE_STORAGE_BACKEND_ARGUMENTS',
default={'location': os.path.join(settings.MEDIA_ROOT, 'workflows')},
help_text=_(
'Arguments to pass to the WORKFLOWS_IMAGE_CACHE_STORAGE_BACKEND.'
)
)

View File

@@ -0,0 +1,12 @@
from __future__ import unicode_literals
from mayan.apps.storage.utils import get_storage_subclass
from .settings import (
setting_workflowimagecache_storage,
setting_workflowimagecache_storage_arguments
)
storage_workflowimagecache = get_storage_subclass(
dotted_path=setting_workflowimagecache_storage.value
)(**setting_workflowimagecache_storage_arguments.value)

View File

@@ -9,6 +9,17 @@ from mayan.celery import app
logger = logging.getLogger(__name__)
@app.task()
def task_generate_workflow_image(document_state_id):
Workflow = apps.get_model(
app_label='document_states', model_name='Workflow'
)
workflow = Workflow.objects.get(pk=document_state_id)
return workflow.generate_image()
@app.task(ignore_result=True)
def task_launch_all_workflows():
Document = apps.get_model(app_label='documents', model_name='Document')

View File

@@ -0,0 +1,8 @@
{% if value %}
<ul>
{% for key, value in value.items %}
<li>{{ key }}: {{ value }}</li>
{% endfor %}
</ul>
{% endif %}

View File

@@ -0,0 +1,2 @@
<img class="img-responsive" name="{{ widget.name }}" {% include "django/forms/widgets/attrs.html" %} src="{{ widget.value.get_api_image_url }}" style="margin:auto;" />

View File

@@ -1,5 +1,7 @@
from __future__ import unicode_literals
from ..literals import FIELD_TYPE_CHOICE_CHAR
TEST_INDEX_LABEL = 'test workflow index'
TEST_WORKFLOW_LABEL = 'test workflow label'
@@ -11,6 +13,10 @@ TEST_WORKFLOW_INSTANCE_LOG_ENTRY_COMMENT = 'test workflow instance log entry com
TEST_WORKFLOW_STATE_LABEL = 'test state label'
TEST_WORKFLOW_STATE_LABEL_EDITED = 'test state label edited'
TEST_WORKFLOW_STATE_COMPLETION = 66
TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT = 'test workflow transition field help test'
TEST_WORKFLOW_TRANSITION_FIELD_LABEL = 'test workflow transition field'
TEST_WORKFLOW_TRANSITION_FIELD_NAME = 'test_workflow_transition_field'
TEST_WORKFLOW_TRANSITION_FIELD_TYPE = FIELD_TYPE_CHOICE_CHAR
TEST_WORKFLOW_TRANSITION_LABEL = 'test transition label'
TEST_WORKFLOW_TRANSITION_LABEL_2 = 'test transition label 2'
TEST_WORKFLOW_TRANSITION_LABEL_EDITED = 'test transition label edited'

View File

@@ -38,19 +38,19 @@ class WorkflowStateViewTestMixin(object):
data.update(extra_data)
return self.post(
viewname='document_states:setup_workflow_state_create',
viewname='document_states:workflow_template_state_create',
kwargs={'pk': self.test_workflow.pk}, data=data
)
def _request_test_workflow_state_delete_view(self):
return self.post(
viewname='document_states:setup_workflow_state_delete',
viewname='document_states:workflow_template_state_delete',
kwargs={'pk': self.test_workflow_state_1.pk}
)
def _request_test_workflow_state_edit_view(self):
return self.post(
viewname='document_states:setup_workflow_state_edit',
viewname='document_states:workflow_template_state_edit',
kwargs={'pk': self.test_workflow_state_1.pk}, data={
'label': TEST_WORKFLOW_STATE_LABEL_EDITED
}
@@ -58,7 +58,7 @@ class WorkflowStateViewTestMixin(object):
def _request_test_workflow_state_list_view(self):
return self.get(
viewname='document_states:setup_workflow_state_list',
viewname='document_states:workflow_template_state_list',
kwargs={'pk': self.test_workflow.pk}
)
@@ -120,7 +120,7 @@ class WorkflowTestMixin(object):
class WorkflowTransitionViewTestMixin(object):
def _request_test_workflow_transition_create_view(self):
return self.post(
viewname='document_states:setup_workflow_transition_create',
viewname='document_states:workflow_template_transition_create',
kwargs={'pk': self.test_workflow.pk}, data={
'label': TEST_WORKFLOW_TRANSITION_LABEL,
'origin_state': self.test_workflow_state_1.pk,
@@ -130,13 +130,13 @@ class WorkflowTransitionViewTestMixin(object):
def _request_test_workflow_transition_delete_view(self):
return self.post(
viewname='document_states:setup_workflow_transition_delete',
viewname='document_states:workflow_template_transition_delete',
kwargs={'pk': self.test_workflow_transition.pk}
)
def _request_test_workflow_transition_edit_view(self):
return self.post(
viewname='document_states:setup_workflow_transition_edit',
viewname='document_states:workflow_template_transition_edit',
kwargs={'pk': self.test_workflow_transition.pk}, data={
'label': TEST_WORKFLOW_TRANSITION_LABEL_EDITED,
'origin_state': self.test_workflow_state_1.pk,
@@ -146,15 +146,16 @@ class WorkflowTransitionViewTestMixin(object):
def _request_test_workflow_transition_list_view(self):
return self.get(
viewname='document_states:setup_workflow_transition_list',
viewname='document_states:workflow_template_transition_list',
kwargs={'pk': self.test_workflow.pk}
)
def _request_test_workflow_transition(self):
return self.post(
viewname='document_states:workflow_instance_transition',
kwargs={'pk': self.test_workflow_instance.pk}, data={
'transition': self.test_workflow_transition.pk,
viewname='document_states:workflow_instance_transition_execute',
kwargs={
'workflow_instance_pk': self.test_workflow_instance.pk,
'workflow_transition_pk': self.test_workflow_transition.pk,
}
)
@@ -162,7 +163,7 @@ class WorkflowTransitionViewTestMixin(object):
class WorkflowViewTestMixin(object):
def _request_test_workflow_create_view(self):
return self.post(
viewname='document_states:setup_workflow_create', data={
viewname='document_states:workflow_template_create', data={
'label': TEST_WORKFLOW_LABEL,
'internal_name': TEST_WORKFLOW_INTERNAL_NAME,
}
@@ -170,14 +171,14 @@ class WorkflowViewTestMixin(object):
def _request_test_workflow_delete_view(self):
return self.post(
viewname='document_states:setup_workflow_delete', kwargs={
viewname='document_states:workflow_template_delete', kwargs={
'pk': self.test_workflow.pk
}
)
def _request_test_workflow_edit_view(self):
return self.post(
viewname='document_states:setup_workflow_edit', kwargs={
viewname='document_states:workflow_template_edit', kwargs={
'pk': self.test_workflow.pk,
}, data={
'label': TEST_WORKFLOW_LABEL_EDITED,
@@ -187,12 +188,12 @@ class WorkflowViewTestMixin(object):
def _request_test_workflow_list_view(self):
return self.get(
viewname='document_states:setup_workflow_list',
viewname='document_states:workflow_template_list',
)
def _request_test_workflow_preview_view(self):
def _request_test_workflow_template_preview_view(self):
return self.get(
viewname='document_states:workflow_preview', kwargs={
viewname='document_states:workflow_template_preview', kwargs={
'pk': self.test_workflow.pk,
}
)

View File

@@ -15,7 +15,7 @@ class WorkflowStateActionViewTestCase(WorkflowStateActionTestMixin, WorkflowTest
def _request_test_document_state_action_view(self):
return self.get(
viewname='document_states:setup_workflow_state_action_list',
viewname='document_states:workflow_template_state_action_list',
kwargs={'pk': self.test_workflow_state.pk}
)

View File

@@ -10,7 +10,10 @@ from ..permissions import (
)
from .literals import (
TEST_WORKFLOW_TRANSITION_LABEL, TEST_WORKFLOW_TRANSITION_LABEL_EDITED
TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT,
TEST_WORKFLOW_TRANSITION_FIELD_LABEL, TEST_WORKFLOW_TRANSITION_FIELD_NAME,
TEST_WORKFLOW_TRANSITION_FIELD_TYPE, TEST_WORKFLOW_TRANSITION_LABEL,
TEST_WORKFLOW_TRANSITION_LABEL_EDITED
)
from .mixins import (
WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin
@@ -160,7 +163,7 @@ class WorkflowTransitionDocumentViewTestCase(
permission.
"""
response = self._request_test_workflow_transition()
self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_code, 404)
# Workflow should remain in the same initial state
self.assertEqual(
@@ -209,7 +212,7 @@ class WorkflowTransitionEventViewTestCase(
):
def _request_test_workflow_transition_event_list_view(self):
return self.get(
viewname='document_states:setup_workflow_transition_events',
viewname='document_states:workflow_template_transition_events',
kwargs={'pk': self.test_workflow_transition.pk}
)
@@ -232,3 +235,125 @@ class WorkflowTransitionEventViewTestCase(
response = self._request_test_workflow_transition_event_list_view()
self.assertEqual(response.status_code, 200)
class WorkflowTransitionFieldViewTestCase(
WorkflowTestMixin, WorkflowTransitionViewTestMixin, GenericViewTestCase
):
def setUp(self):
super(WorkflowTransitionFieldViewTestCase, self).setUp()
self._create_test_workflow()
self._create_test_workflow_states()
self._create_test_workflow_transition()
def _create_test_workflow_transition_field(self):
self.test_workflow_transition_field = self.test_workflow_transition.fields.create(
field_type=TEST_WORKFLOW_TRANSITION_FIELD_TYPE,
name=TEST_WORKFLOW_TRANSITION_FIELD_NAME,
label=TEST_WORKFLOW_TRANSITION_FIELD_LABEL,
help_text=TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT
)
def _request_test_workflow_transition_field_list_view(self):
return self.get(
viewname='document_states:workflow_template_transition_field_list',
kwargs={'pk': self.test_workflow_transition.pk}
)
def test_workflow_transition_field_list_view_no_permission(self):
self._create_test_workflow_transition_field()
response = self._request_test_workflow_transition_field_list_view()
self.assertNotContains(
response=response,
text=self.test_workflow_transition_field.label,
status_code=404
)
def test_workflow_transition_field_list_view_with_access(self):
self._create_test_workflow_transition_field()
self.grant_access(
obj=self.test_workflow, permission=permission_workflow_edit
)
response = self._request_test_workflow_transition_field_list_view()
self.assertContains(
response=response,
text=self.test_workflow_transition_field.label,
status_code=200
)
def _request_workflow_transition_field_create_view(self):
return self.post(
viewname='document_states:workflow_template_transition_field_create',
kwargs={'pk': self.test_workflow_transition.pk},
data={
'field_type': TEST_WORKFLOW_TRANSITION_FIELD_TYPE,
'name': TEST_WORKFLOW_TRANSITION_FIELD_NAME,
'label': TEST_WORKFLOW_TRANSITION_FIELD_LABEL,
'help_text': TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT
}
)
def test_workflow_transition_field_create_view_no_permission(self):
workflow_transition_field_count = self.test_workflow_transition.fields.count()
response = self._request_workflow_transition_field_create_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(
self.test_workflow_transition.fields.count(),
workflow_transition_field_count
)
def test_workflow_transition_field_create_view_with_access(self):
workflow_transition_field_count = self.test_workflow_transition.fields.count()
self.grant_access(
obj=self.test_workflow, permission=permission_workflow_edit
)
response = self._request_workflow_transition_field_create_view()
self.assertEqual(response.status_code, 302)
self.assertEqual(
self.test_workflow_transition.fields.count(),
workflow_transition_field_count + 1
)
def _request_workflow_transition_field_delete_view(self):
return self.post(
viewname='document_states:workflow_template_transition_field_delete',
kwargs={'pk': self.test_workflow_transition_field.pk},
)
def test_workflow_transition_field_delete_view_no_permission(self):
self._create_test_workflow_transition_field()
workflow_transition_field_count = self.test_workflow_transition.fields.count()
response = self._request_workflow_transition_field_delete_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(
self.test_workflow_transition.fields.count(),
workflow_transition_field_count
)
def test_workflow_transition_field_delete_view_with_access(self):
self._create_test_workflow_transition_field()
workflow_transition_field_count = self.test_workflow_transition.fields.count()
self.grant_access(
obj=self.test_workflow, permission=permission_workflow_edit
)
response = self._request_workflow_transition_field_delete_view()
self.assertEqual(response.status_code, 302)
self.assertEqual(
self.test_workflow_transition.fields.count(),
workflow_transition_field_count - 1
)

View File

@@ -93,31 +93,31 @@ class WorkflowViewTestCase(
self.assertEqual(response.status_code, 200)
self.assertContains(response, text=self.test_workflow.label)
def test_workflow_preview_view_no_access(self):
def test_workflow_template_preview_view_no_access(self):
self._create_test_workflow()
response = self._request_test_workflow_preview_view()
response = self._request_test_workflow_template_preview_view()
self.assertEqual(response.status_code, 404)
self.assertTrue(self.test_workflow in Workflow.objects.all())
def test_workflow_preview_view_with_access(self):
def test_workflow_template_preview_view_with_access(self):
self._create_test_workflow()
self.grant_access(
obj=self.test_workflow, permission=permission_workflow_view
)
response = self._request_test_workflow_preview_view()
response = self._request_test_workflow_template_preview_view()
self.assertEqual(response.status_code, 200)
class WorkflowToolViewTestCase(WorkflowTestMixin, GenericDocumentViewTestCase):
def _request_workflow_launch_view(self):
return self.post(
viewname='document_states:tool_launch_all_workflows',
viewname='document_states:tool_launch_workflows',
)
def test_tool_launch_all_workflows_view_no_permission(self):
def test_tool_launch_workflows_view_no_permission(self):
self._create_test_workflow(add_document_type=True)
self._create_test_workflow_states()
self._create_test_workflow_transition()
@@ -129,7 +129,7 @@ class WorkflowToolViewTestCase(WorkflowTestMixin, GenericDocumentViewTestCase):
self.assertEqual(self.test_document.workflows.count(), 0)
def test_tool_launch_all_workflows_view_with_permission(self):
def test_tool_launch_workflows_view_with_permission(self):
self._create_test_workflow(add_document_type=True)
self._create_test_workflow_states()
self._create_test_workflow_transition()

View File

@@ -3,191 +3,247 @@ from __future__ import unicode_literals
from django.conf.urls import url
from .api_views import (
APIDocumentTypeWorkflowListView, APIWorkflowDocumentTypeList,
APIWorkflowDocumentTypeView, APIWorkflowInstanceListView,
APIWorkflowInstanceView, APIWorkflowInstanceLogEntryListView,
APIWorkflowListView, APIWorkflowStateListView, APIWorkflowStateView,
APIDocumentTypeWorkflowRuntimeProxyListView, APIWorkflowDocumentTypeList,
APIWorkflowDocumentTypeView, APIWorkflowImageView,
APIWorkflowInstanceListView, APIWorkflowInstanceView,
APIWorkflowInstanceLogEntryListView, APIWorkflowRuntimeProxyListView,
APIWorkflowStateListView, APIWorkflowStateView,
APIWorkflowTransitionListView, APIWorkflowTransitionView, APIWorkflowView
)
from .views import (
DocumentWorkflowInstanceListView, SetupWorkflowCreateView,
SetupWorkflowDeleteView, SetupWorkflowDocumentTypesView,
SetupWorkflowEditView, SetupWorkflowListView,
SetupWorkflowStateActionCreateView, SetupWorkflowStateActionDeleteView,
SetupWorkflowStateActionEditView, SetupWorkflowStateActionListView,
SetupWorkflowStateActionSelectionView, SetupWorkflowStateCreateView,
SetupWorkflowStateDeleteView, SetupWorkflowStateEditView,
SetupWorkflowStateListView, SetupWorkflowTransitionListView,
SetupWorkflowTransitionCreateView, SetupWorkflowTransitionDeleteView,
SetupWorkflowTransitionEditView,
SetupWorkflowTransitionTriggerEventListView, ToolLaunchAllWorkflows,
WorkflowDocumentListView, WorkflowInstanceDetailView,
WorkflowImageView, WorkflowInstanceTransitionView, WorkflowListView,
WorkflowPreviewView, WorkflowStateDocumentListView, WorkflowStateListView,
from .views.workflow_instance_views import (
WorkflowInstanceDetailView, WorkflowInstanceListView,
WorkflowInstanceTransitionSelectView,
WorkflowInstanceTransitionExecuteView
)
from .views.workflow_proxy_views import (
WorkflowRuntimeProxyDocumentListView,
WorkflowRuntimeProxyListView, WorkflowRuntimeProxyStateDocumentListView,
WorkflowRuntimeProxyStateListView
)
from .views.workflow_template_views import (
DocumentTypeWorkflowTemplatesView, ToolLaunchWorkflows,
WorkflowTemplateCreateView, WorkflowTemplateDeleteView,
WorkflowTemplateEditView, WorkflowTemplateListView,
WorkflowTemplatePreviewView, WorkflowTemplateDocumentTypesView
)
from .views.workflow_template_state_views import (
WorkflowTemplateStateActionCreateView,
WorkflowTemplateStateActionDeleteView, WorkflowTemplateStateActionEditView,
WorkflowTemplateStateActionListView,
WorkflowTemplateStateActionSelectionView, WorkflowTemplateStateCreateView,
WorkflowTemplateStateDeleteView, WorkflowTemplateStateEditView,
WorkflowTemplateStateListView
)
from .views.workflow_template_transition_views import (
WorkflowTemplateTransitionCreateView, WorkflowTemplateTransitionDeleteView,
WorkflowTemplateTransitionEditView, WorkflowTemplateTransitionListView,
WorkflowTemplateTransitionTriggerEventListView,
WorkflowTemplateTransitionFieldCreateView,
WorkflowTemplateTransitionFieldDeleteView,
WorkflowTemplateTransitionFieldEditView,
WorkflowTemplateTransitionFieldListView
)
from .views.workflow_views import SetupDocumentTypeWorkflowsView
urlpatterns_workflows = [
urlpatterns_workflow_instances = [
url(
regex=r'^document_type/(?P<pk>\d+)/workflows/$',
view=SetupDocumentTypeWorkflowsView.as_view(),
name='document_type_workflows'
regex=r'^documents/(?P<pk>\d+)/workflows/$',
view=WorkflowInstanceListView.as_view(),
name='workflow_instance_list'
),
url(
regex=r'^documents/workflows/(?P<pk>\d+)/$',
view=WorkflowInstanceDetailView.as_view(),
name='workflow_instance_detail'
),
url(
regex=r'^documents/workflows/(?P<pk>\d+)/transitions/select/$',
view=WorkflowInstanceTransitionSelectView.as_view(),
name='workflow_instance_transition_selection'
),
url(
regex=r'^documents/workflows/(?P<workflow_instance_pk>\d+)/transitions/(?P<workflow_transition_pk>\d+)/execute/$',
view=WorkflowInstanceTransitionExecuteView.as_view(),
name='workflow_instance_transition_execute'
),
]
urlpatterns_workflow_runtime_proxies = [
url(
regex=r'workflow_runtime_proxies/$',
view=WorkflowRuntimeProxyListView.as_view(),
name='workflow_runtime_proxy_list'
),
url(
regex=r'^workflow_runtime_proxies/(?P<pk>\d+)/documents/$',
view=WorkflowRuntimeProxyDocumentListView.as_view(),
name='workflow_runtime_proxy_document_list'
),
url(
regex=r'^workflow_runtime_proxies/(?P<pk>\d+)/states/$',
view=WorkflowRuntimeProxyStateListView.as_view(),
name='workflow_runtime_proxy_state_list'
),
url(
regex=r'^workflow_runtime_proxies/states/(?P<pk>\d+)/documents/$',
view=WorkflowRuntimeProxyStateDocumentListView.as_view(),
name='workflow_runtime_proxy_state_document_list'
),
]
urlpatterns_workflow_states = [
url(
regex=r'^workflow_templates/(?P<pk>\d+)/states/$',
view=WorkflowTemplateStateListView.as_view(),
name='workflow_template_state_list'
),
url(
regex=r'^workflow_templates/(?P<pk>\d+)/states/create/$',
view=WorkflowTemplateStateCreateView.as_view(),
name='workflow_template_state_create'
),
url(
regex=r'^workflow_templates/states/(?P<pk>\d+)/delete/$',
view=WorkflowTemplateStateDeleteView.as_view(),
name='workflow_template_state_delete'
),
url(
regex=r'^workflow_templates/states/(?P<pk>\d+)/edit/$',
view=WorkflowTemplateStateEditView.as_view(),
name='workflow_template_state_edit'
),
]
urlpatterns_workflow_state_actions = [
url(
regex=r'^workflow_templates/states/(?P<pk>\d+)/actions/$',
view=WorkflowTemplateStateActionListView.as_view(),
name='workflow_template_state_action_list'
),
url(
regex=r'^workflow_templates/states/(?P<pk>\d+)/actions/selection/$',
view=WorkflowTemplateStateActionSelectionView.as_view(),
name='workflow_template_state_action_selection'
),
url(
regex=r'^workflow_templates/states/(?P<pk>\d+)/actions/(?P<class_path>[a-zA-Z0-9_.]+)/create/$',
view=WorkflowTemplateStateActionCreateView.as_view(),
name='workflow_template_state_action_create'
),
url(
regex=r'^workflow_templates/states/actions/(?P<pk>\d+)/delete/$',
view=WorkflowTemplateStateActionDeleteView.as_view(),
name='workflow_template_state_action_delete'
),
url(
regex=r'^workflow_templates/states/actions/(?P<pk>\d+)/edit/$',
view=WorkflowTemplateStateActionEditView.as_view(),
name='workflow_template_state_action_edit'
),
]
urlpatterns_workflow_templates = [
url(
regex=r'^workflow_templates/$', view=WorkflowTemplateListView.as_view(),
name='workflow_template_list'
),
url(
regex=r'^workflow_templates/create/$', view=WorkflowTemplateCreateView.as_view(),
name='workflow_template_create'
),
url(
regex=r'^workflow_templates/(?P<pk>\d+)/delete/$',
view=WorkflowTemplateDeleteView.as_view(), name='workflow_template_delete'
),
url(
regex=r'^workflow_templates/(?P<pk>\d+)/document_types/$',
view=WorkflowTemplateDocumentTypesView.as_view(),
name='workflow_template_document_types'
),
url(
regex=r'^workflow_templates/(?P<pk>\d+)/edit/$',
view=WorkflowTemplateEditView.as_view(), name='workflow_template_edit'
),
url(
regex=r'^workflow_templates/(?P<pk>\d+)/preview/$',
view=WorkflowTemplatePreviewView.as_view(),
name='workflow_template_preview'
),
url(
regex=r'^document_types/(?P<pk>\d+)/workflow_templates/$',
view=DocumentTypeWorkflowTemplatesView.as_view(),
name='document_type_workflow_templates'
),
]
urlpatterns_workflow_transitions = [
url(
regex=r'^workflow_templates/(?P<pk>\d+)/transitions/$',
view=WorkflowTemplateTransitionListView.as_view(),
name='workflow_template_transition_list'
),
url(
regex=r'^workflow_templates/(?P<pk>\d+)/transitions/create/$',
view=WorkflowTemplateTransitionCreateView.as_view(),
name='workflow_template_transition_create'
),
url(
regex=r'^workflow_templates/(?P<pk>\d+)/transitions/events/$',
view=WorkflowTemplateTransitionTriggerEventListView.as_view(),
name='workflow_template_transition_events'
),
url(
regex=r'^workflow_templates/transitions/(?P<pk>\d+)/delete/$',
view=WorkflowTemplateTransitionDeleteView.as_view(),
name='workflow_template_transition_delete'
),
url(
regex=r'^workflow_templates/transitions/(?P<pk>\d+)/edit/$',
view=WorkflowTemplateTransitionEditView.as_view(),
name='workflow_template_transition_edit'
),
]
urlpatterns_workflow_transition_fields = [
url(
regex=r'^workflow_templates/transitions/(?P<pk>\d+)/fields/create/$',
view=WorkflowTemplateTransitionFieldCreateView.as_view(),
name='workflow_template_transition_field_create'
),
url(
regex=r'^workflow_templates/transitions/(?P<pk>\d+)/fields/$',
view=WorkflowTemplateTransitionFieldListView.as_view(),
name='workflow_template_transition_field_list'
),
url(
regex=r'^workflow_templates/transitions/fields/(?P<pk>\d+)/delete/$',
view=WorkflowTemplateTransitionFieldDeleteView.as_view(),
name='workflow_template_transition_field_delete'
),
url(
regex=r'^workflow_templates/transitions/fields/(?P<pk>\d+)/edit/$',
view=WorkflowTemplateTransitionFieldEditView.as_view(),
name='workflow_template_transition_field_edit'
),
]
urlpatterns = [
url(
regex=r'^document/(?P<pk>\d+)/workflows/$',
view=DocumentWorkflowInstanceListView.as_view(),
name='document_workflow_instance_list'
),
url(
regex=r'^document/workflows/(?P<pk>\d+)/$',
view=WorkflowInstanceDetailView.as_view(),
name='workflow_instance_detail'
),
url(
regex=r'^document/workflows/(?P<pk>\d+)/transition/$',
view=WorkflowInstanceTransitionView.as_view(),
name='workflow_instance_transition'
),
url(
regex=r'^setup/all/$', view=SetupWorkflowListView.as_view(),
name='setup_workflow_list'
),
url(
regex=r'^setup/create/$', view=SetupWorkflowCreateView.as_view(),
name='setup_workflow_create'
),
url(
regex=r'^setup/workflow/(?P<pk>\d+)/edit/$',
view=SetupWorkflowEditView.as_view(), name='setup_workflow_edit'
),
url(
regex=r'^setup/workflow/(?P<pk>\d+)/delete/$',
view=SetupWorkflowDeleteView.as_view(), name='setup_workflow_delete'
),
url(
regex=r'^setup/workflow/(?P<pk>\d+)/documents/$',
view=WorkflowDocumentListView.as_view(),
name='setup_workflow_document_list'
),
url(
regex=r'^setup/workflow/(?P<pk>\d+)/document_types/$',
view=SetupWorkflowDocumentTypesView.as_view(),
name='setup_workflow_document_types'
),
url(
regex=r'^setup/workflow/(?P<pk>\d+)/states/$',
view=SetupWorkflowStateListView.as_view(),
name='setup_workflow_state_list'
),
url(
regex=r'^setup/workflow/(?P<pk>\d+)/states/create/$',
view=SetupWorkflowStateCreateView.as_view(),
name='setup_workflow_state_create'
),
url(
regex=r'^setup/workflow/(?P<pk>\d+)/transitions/$',
view=SetupWorkflowTransitionListView.as_view(),
name='setup_workflow_transition_list'
),
url(
regex=r'^setup/workflow/(?P<pk>\d+)/transitions/create/$',
view=SetupWorkflowTransitionCreateView.as_view(),
name='setup_workflow_transition_create'
),
url(
regex=r'^setup/workflow/(?P<pk>\d+)/transitions/events/$',
view=SetupWorkflowTransitionTriggerEventListView.as_view(),
name='setup_workflow_transition_events'
),
url(
regex=r'^setup/workflow/state/(?P<pk>\d+)/delete/$',
view=SetupWorkflowStateDeleteView.as_view(),
name='setup_workflow_state_delete'
),
url(
regex=r'^setup/workflow/state/(?P<pk>\d+)/edit/$',
view=SetupWorkflowStateEditView.as_view(),
name='setup_workflow_state_edit'
),
url(
regex=r'^setup/workflow/state/(?P<pk>\d+)/actions/$',
view=SetupWorkflowStateActionListView.as_view(),
name='setup_workflow_state_action_list'
),
url(
regex=r'^setup/workflow/state/(?P<pk>\d+)/actions/selection/$',
view=SetupWorkflowStateActionSelectionView.as_view(),
name='setup_workflow_state_action_selection'
),
url(
regex=r'^setup/workflow/state/(?P<pk>\d+)/actions/(?P<class_path>[a-zA-Z0-9_.]+)/create/$',
view=SetupWorkflowStateActionCreateView.as_view(),
name='setup_workflow_state_action_create'
),
url(
regex=r'^setup/workflow/state/actions/(?P<pk>\d+)/delete/$',
view=SetupWorkflowStateActionDeleteView.as_view(),
name='setup_workflow_state_action_delete'
),
url(
regex=r'^setup/workflow/state/actions/(?P<pk>\d+)/edit/$',
view=SetupWorkflowStateActionEditView.as_view(),
name='setup_workflow_state_action_edit'
),
url(
regex=r'^setup/workflow/transitions/(?P<pk>\d+)/delete/$',
view=SetupWorkflowTransitionDeleteView.as_view(),
name='setup_workflow_transition_delete'
),
url(
regex=r'^setup/workflow/transitions/(?P<pk>\d+)/edit/$',
view=SetupWorkflowTransitionEditView.as_view(),
name='setup_workflow_transition_edit'
),
url(
regex=r'^tools/workflow/all/launch/$',
view=ToolLaunchAllWorkflows.as_view(),
name='tool_launch_all_workflows'
),
url(
regex=r'all/$',
view=WorkflowListView.as_view(),
name='workflow_list'
),
url(
regex=r'^(?P<pk>\d+)/documents/$',
view=WorkflowDocumentListView.as_view(),
name='workflow_document_list'
),
url(
regex=r'^(?P<pk>\d+)/states/$',
view=WorkflowStateListView.as_view(),
name='workflow_state_list'
),
url(
regex=r'^(?P<pk>\d+)/image/$',
view=WorkflowImageView.as_view(),
name='workflow_image'
),
url(
regex=r'^(?P<pk>\d+)/preview/$',
view=WorkflowPreviewView.as_view(),
name='workflow_preview'
),
url(
regex=r'^state/(?P<pk>\d+)/documents/$',
view=WorkflowStateDocumentListView.as_view(),
name='workflow_state_document_list'
regex=r'^tools/workflows/launch/$',
view=ToolLaunchWorkflows.as_view(),
name='tool_launch_workflows'
),
]
urlpatterns.extend(urlpatterns_workflows)
urlpatterns.extend(urlpatterns_workflow_instances)
urlpatterns.extend(urlpatterns_workflow_runtime_proxies)
urlpatterns.extend(urlpatterns_workflow_states)
urlpatterns.extend(urlpatterns_workflow_state_actions)
urlpatterns.extend(urlpatterns_workflow_templates)
urlpatterns.extend(urlpatterns_workflow_transitions)
urlpatterns.extend(urlpatterns_workflow_transition_fields)
api_urls = [
url(
regex=r'^workflows/$', view=APIWorkflowListView.as_view(),
regex=r'^workflows/$', view=APIWorkflowRuntimeProxyListView.as_view(),
name='workflow-list'
),
url(
@@ -204,6 +260,10 @@ api_urls = [
view=APIWorkflowDocumentTypeView.as_view(),
name='workflow-document-type-detail'
),
url(
regex=r'^workflows/(?P<pk>\d+)/image/$',
name='workflow-image', view=APIWorkflowImageView.as_view()
),
url(
regex=r'^workflows/(?P<pk>[0-9]+)/states/$',
view=APIWorkflowStateListView.as_view(), name='workflowstate-list'
@@ -239,7 +299,7 @@ api_urls = [
),
url(
regex=r'^document_types/(?P<pk>[0-9]+)/workflows/$',
view=APIDocumentTypeWorkflowListView.as_view(),
view=APIDocumentTypeWorkflowRuntimeProxyListView.as_view(),
name='documenttype-workflow-list'
),
]

View File

@@ -1,3 +1 @@
from .workflow_instance_views import * # NOQA
from .workflow_proxy_views import * # NOQA
from .workflow_views import * # NOQA

View File

@@ -4,25 +4,24 @@ from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.forms import DynamicForm
from mayan.apps.common.generics import FormView, SingleObjectListView
from mayan.apps.common.mixins import ExternalObjectMixin
from mayan.apps.documents.models import Document
from ..forms import WorkflowInstanceTransitionForm
from ..icons import icon_workflow_instance_detail, icon_workflow_list
from ..forms import WorkflowInstanceTransitionSelectForm
from ..icons import icon_workflow_instance_detail, icon_workflow_template_list
from ..links import link_workflow_instance_transition
from ..literals import FIELD_TYPE_MAPPING, WIDGET_CLASS_MAPPING
from ..models import WorkflowInstance
from ..permissions import permission_workflow_view
__all__ = (
'DocumentWorkflowInstanceListView', 'WorkflowInstanceDetailView',
'WorkflowInstanceTransitionView'
)
class DocumentWorkflowInstanceListView(SingleObjectListView):
class WorkflowInstanceListView(SingleObjectListView):
def dispatch(self, request, *args, **kwargs):
AccessControlList.objects.check_access(
obj=self.get_document(), permissions=(permission_workflow_view,),
@@ -30,7 +29,7 @@ class DocumentWorkflowInstanceListView(SingleObjectListView):
)
return super(
DocumentWorkflowInstanceListView, self
WorkflowInstanceListView, self
).dispatch(request, *args, **kwargs)
def get_document(self):
@@ -39,7 +38,7 @@ class DocumentWorkflowInstanceListView(SingleObjectListView):
def get_extra_context(self):
return {
'hide_link': True,
'no_results_icon': icon_workflow_list,
'no_results_icon': icon_workflow_template_list,
'no_results_text': _(
'Assign workflows to the document type of this document '
'to have this document execute those workflows. '
@@ -100,14 +99,17 @@ class WorkflowInstanceDetailView(SingleObjectListView):
return get_object_or_404(klass=WorkflowInstance, pk=self.kwargs['pk'])
class WorkflowInstanceTransitionView(FormView):
form_class = WorkflowInstanceTransitionForm
class WorkflowInstanceTransitionExecuteView(FormView):
form_class = DynamicForm
template_name = 'appearance/generic_form.html'
def form_valid(self, form):
form_data = form.cleaned_data
comment = form_data.pop('comment')
self.get_workflow_instance().do_transition(
comment=form.cleaned_data['comment'],
transition=form.cleaned_data['transition'], user=self.request.user
comment=comment, extra_data=form_data,
transition=self.get_workflow_transition(), user=self.request.user,
)
messages.success(
self.request, _(
@@ -122,19 +124,99 @@ class WorkflowInstanceTransitionView(FormView):
'object': self.get_workflow_instance().document,
'submit_label': _('Submit'),
'title': _(
'Do transition for workflow: %s'
) % self.get_workflow_instance(),
'Execute transition "%(transition)s" for workflow: %(workflow)s'
) % {
'transition': self.get_workflow_transition(),
'workflow': self.get_workflow_instance(),
},
'workflow_instance': self.get_workflow_instance(),
}
def get_form_extra_kwargs(self):
return {
'user': self.request.user,
'workflow_instance': self.get_workflow_instance()
schema = {
'fields': {
'comment': {
'label': _('Comment'),
'class': 'django.forms.CharField', 'kwargs': {
'help_text': _(
'Optional comment to attach to the transition.'
),
'required': False,
}
}
},
'widgets': {
'comment': {
'class': 'django.forms.widgets.Textarea',
'kwargs': {
'attrs': {
'rows': 3
}
}
}
}
}
for field in self.get_workflow_transition().fields.all():
schema['fields'][field.name] = {
'class': FIELD_TYPE_MAPPING[field.field_type],
'help_text': field.help_text,
'label': field.label,
'required': field.required,
}
if field.widget:
schema['widgets'][field.name] = {
'class': WIDGET_CLASS_MAPPING[field.widget],
'kwargs': field.get_widget_kwargs()
}
return {'schema': schema}
def get_success_url(self):
return self.get_workflow_instance().get_absolute_url()
def get_workflow_instance(self):
return get_object_or_404(klass=WorkflowInstance, pk=self.kwargs['pk'])
return get_object_or_404(
klass=WorkflowInstance, pk=self.kwargs['workflow_instance_pk']
)
def get_workflow_transition(self):
return get_object_or_404(
klass=self.get_workflow_instance().get_transition_choices(
_user=self.request.user
), pk=self.kwargs['workflow_transition_pk']
)
class WorkflowInstanceTransitionSelectView(ExternalObjectMixin, FormView):
external_object_class = WorkflowInstance
form_class = WorkflowInstanceTransitionSelectForm
template_name = 'appearance/generic_form.html'
def form_valid(self, form):
return HttpResponseRedirect(
redirect_to=reverse(
viewname='document_states:workflow_instance_transition_execute',
kwargs={
'workflow_instance_pk': self.external_object.pk,
'workflow_transition_pk': form.cleaned_data['transition'].pk
}
)
)
def get_extra_context(self):
return {
'navigation_object_list': ('object', 'workflow_instance'),
'object': self.external_object.document,
'submit_label': _('Select'),
'title': _(
'Select transition for workflow: %s'
) % self.external_object,
'workflow_instance': self.external_object,
}
def get_form_extra_kwargs(self):
return {
'user': self.request.user,
'workflow_instance': self.external_object
}

View File

@@ -9,18 +9,13 @@ from mayan.apps.common.generics import SingleObjectListView
from mayan.apps.documents.models import Document
from mayan.apps.documents.views import DocumentListView
from ..icons import icon_workflow_list
from ..links import link_setup_workflow_create, link_setup_workflow_state_create
from ..icons import icon_workflow_template_list
from ..links import link_workflow_template_create, link_workflow_template_state_create
from ..models import WorkflowRuntimeProxy, WorkflowStateRuntimeProxy
from ..permissions import permission_workflow_view
__all__ = (
'WorkflowDocumentListView', 'WorkflowListView',
'WorkflowStateDocumentListView', 'WorkflowStateListView'
)
class WorkflowDocumentListView(DocumentListView):
class WorkflowRuntimeProxyDocumentListView(DocumentListView):
def dispatch(self, request, *args, **kwargs):
self.workflow = get_object_or_404(
klass=WorkflowRuntimeProxy, pk=self.kwargs['pk']
@@ -32,14 +27,14 @@ class WorkflowDocumentListView(DocumentListView):
)
return super(
WorkflowDocumentListView, self
WorkflowRuntimeProxyDocumentListView, self
).dispatch(request, *args, **kwargs)
def get_document_queryset(self):
return Document.objects.filter(workflows__workflow=self.workflow)
def get_extra_context(self):
context = super(WorkflowDocumentListView, self).get_extra_context()
context = super(WorkflowRuntimeProxyDocumentListView, self).get_extra_context()
context.update(
{
'no_results_text': _(
@@ -56,14 +51,14 @@ class WorkflowDocumentListView(DocumentListView):
return context
class WorkflowListView(SingleObjectListView):
class WorkflowRuntimeProxyListView(SingleObjectListView):
object_permission = permission_workflow_view
def get_extra_context(self):
return {
'hide_object': True,
'no_results_icon': icon_workflow_list,
'no_results_main_link': link_setup_workflow_create.resolve(
'no_results_icon': icon_workflow_template_list,
'no_results_main_link': link_workflow_template_create.resolve(
context=RequestContext(request=self.request)
),
'no_results_text': _(
@@ -79,13 +74,13 @@ class WorkflowListView(SingleObjectListView):
return WorkflowRuntimeProxy.objects.all()
class WorkflowStateDocumentListView(DocumentListView):
class WorkflowRuntimeProxyStateDocumentListView(DocumentListView):
def get_document_queryset(self):
return self.get_workflow_state().get_documents()
def get_extra_context(self):
workflow_state = self.get_workflow_state()
context = super(WorkflowStateDocumentListView, self).get_extra_context()
context = super(WorkflowRuntimeProxyStateDocumentListView, self).get_extra_context()
context.update(
{
'object': workflow_state,
@@ -118,7 +113,7 @@ class WorkflowStateDocumentListView(DocumentListView):
return workflow_state
class WorkflowStateListView(SingleObjectListView):
class WorkflowRuntimeProxyStateListView(SingleObjectListView):
def dispatch(self, request, *args, **kwargs):
AccessControlList.objects.check_access(
obj=self.get_workflow(), permissions=(permission_workflow_view,),
@@ -126,14 +121,14 @@ class WorkflowStateListView(SingleObjectListView):
)
return super(
WorkflowStateListView, self
WorkflowRuntimeProxyStateListView, self
).dispatch(request, *args, **kwargs)
def get_extra_context(self):
return {
'hide_link': True,
'hide_object': True,
'no_results_main_link': link_setup_workflow_state_create.resolve(
'no_results_main_link': link_workflow_template_state_create.resolve(
context=RequestContext(
request=self.request, dict_={'object': self.get_workflow()}
)

View File

@@ -0,0 +1,326 @@
from __future__ import absolute_import, unicode_literals
from django.contrib import messages
from django.db import transaction
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.generics import (
AddRemoveView, ConfirmView, FormView, SingleObjectCreateView,
SingleObjectDeleteView, SingleObjectDetailView,
SingleObjectDynamicFormCreateView, SingleObjectDynamicFormEditView,
SingleObjectEditView, SingleObjectListView
)
from mayan.apps.common.mixins import ExternalObjectMixin
from mayan.apps.documents.events import event_document_type_edited
from mayan.apps.documents.models import DocumentType
from mayan.apps.documents.permissions import permission_document_type_edit
from mayan.apps.events.classes import EventType
from mayan.apps.events.models import StoredEventType
from ..classes import WorkflowAction
from ..events import event_workflow_edited
from ..forms import (
WorkflowActionSelectionForm, WorkflowForm, WorkflowPreviewForm,
WorkflowStateActionDynamicForm, WorkflowStateForm, WorkflowTransitionForm,
WorkflowTransitionTriggerEventRelationshipFormSet
)
from ..icons import (
icon_workflow_template_list, icon_workflow_state, icon_workflow_state_action,
icon_workflow_transition, icon_workflow_transition_field
)
from ..links import (
link_workflow_template_create, link_workflow_template_state_create,
link_workflow_template_state_action_selection,
link_workflow_template_transition_create,
link_workflow_template_transition_field_create,
)
from ..models import (
Workflow, WorkflowState, WorkflowStateAction, WorkflowTransition,
WorkflowTransitionField
)
from ..permissions import (
permission_workflow_create, permission_workflow_delete,
permission_workflow_edit, permission_workflow_tools,
permission_workflow_view,
)
from ..tasks import task_launch_all_workflows
class WorkflowTemplateStateActionCreateView(SingleObjectDynamicFormCreateView):
form_class = WorkflowStateActionDynamicForm
object_permission = permission_workflow_edit
def get_class(self):
try:
return WorkflowAction.get(name=self.kwargs['class_path'])
except KeyError:
raise Http404(
'{} class not found'.format(self.kwargs['class_path'])
)
def get_extra_context(self):
return {
'navigation_object_list': ('object', 'workflow'),
'object': self.get_object(),
'title': _(
'Create a "%s" workflow action'
) % self.get_class().label,
'workflow': self.get_object().workflow
}
def get_form_extra_kwargs(self):
return {
'request': self.request,
'action_path': self.kwargs['class_path']
}
def get_form_schema(self):
return self.get_class()().get_form_schema(request=self.request)
def get_instance_extra_data(self):
return {
'action_path': self.kwargs['class_path'],
'state': self.get_object()
}
def get_object(self):
return get_object_or_404(klass=WorkflowState, pk=self.kwargs['pk'])
def get_post_action_redirect(self):
return reverse(
viewname='document_states:workflow_template_state_action_list',
kwargs={'pk': self.get_object().pk}
)
class WorkflowTemplateStateActionDeleteView(SingleObjectDeleteView):
model = WorkflowStateAction
object_permission = permission_workflow_edit
def get_extra_context(self):
return {
'navigation_object_list': (
'object', 'workflow_state', 'workflow'
),
'object': self.get_object(),
'title': _('Delete workflow state action: %s') % self.get_object(),
'workflow': self.get_object().state.workflow,
'workflow_state': self.get_object().state,
}
def get_post_action_redirect(self):
return reverse(
viewname='document_states:workflow_template_state_action_list',
kwargs={'pk': self.get_object().state.pk}
)
class WorkflowTemplateStateActionEditView(SingleObjectDynamicFormEditView):
form_class = WorkflowStateActionDynamicForm
model = WorkflowStateAction
object_permission = permission_workflow_edit
def get_extra_context(self):
return {
'navigation_object_list': (
'object', 'workflow_state', 'workflow'
),
'object': self.get_object(),
'title': _('Edit workflow state action: %s') % self.get_object(),
'workflow': self.get_object().state.workflow,
'workflow_state': self.get_object().state,
}
def get_form_extra_kwargs(self):
return {
'request': self.request,
'action_path': self.get_object().action_path,
}
def get_form_schema(self):
return self.get_object().get_class_instance().get_form_schema(
request=self.request
)
def get_post_action_redirect(self):
return reverse(
viewname='document_states:workflow_template_state_action_list',
kwargs={'pk': self.get_object().state.pk}
)
class WorkflowTemplateStateActionListView(SingleObjectListView):
object_permission = permission_workflow_edit
def get_extra_context(self):
return {
'hide_object': True,
'navigation_object_list': ('object', 'workflow'),
'no_results_icon': icon_workflow_state_action,
'no_results_main_link': link_workflow_template_state_action_selection.resolve(
context=RequestContext(
request=self.request, dict_={
'object': self.get_workflow_state()
}
)
),
'no_results_text': _(
'Workflow state actions are macros that get executed when '
'documents enters or leaves the state in which they reside.'
),
'no_results_title': _(
'There are no actions for this workflow state'
),
'object': self.get_workflow_state(),
'title': _(
'Actions for workflow state: %s'
) % self.get_workflow_state(),
'workflow': self.get_workflow_state().workflow,
}
def get_form_schema(self):
return {'fields': self.get_class().fields}
def get_source_queryset(self):
return self.get_workflow_state().actions.all()
def get_workflow_state(self):
return get_object_or_404(klass=WorkflowState, pk=self.kwargs['pk'])
class WorkflowTemplateStateActionSelectionView(FormView):
form_class = WorkflowActionSelectionForm
view_permission = permission_workflow_edit
def form_valid(self, form):
klass = form.cleaned_data['klass']
return HttpResponseRedirect(
redirect_to=reverse(
viewname='document_states:workflow_template_state_action_create',
kwargs={'pk': self.get_object().pk, 'class_path': klass}
)
)
def get_extra_context(self):
return {
'navigation_object_list': (
'object', 'workflow'
),
'object': self.get_object(),
'title': _('New workflow state action selection'),
'workflow': self.get_object().workflow,
}
def get_object(self):
return get_object_or_404(klass=WorkflowState, pk=self.kwargs['pk'])
class WorkflowTemplateStateCreateView(ExternalObjectMixin, SingleObjectCreateView):
external_object_class = Workflow
external_object_permission = permission_workflow_edit
external_object_pk_url_kwarg = 'pk'
form_class = WorkflowStateForm
def get_extra_context(self):
return {
'object': self.get_workflow(),
'title': _(
'Create states for workflow: %s'
) % self.get_workflow()
}
def get_instance_extra_data(self):
return {'workflow': self.get_workflow()}
def get_source_queryset(self):
return self.get_workflow().states.all()
def get_success_url(self):
return reverse(
viewname='document_states:workflow_template_state_list',
kwargs={'pk': self.kwargs['pk']}
)
def get_workflow(self):
return self.external_object
class WorkflowTemplateStateDeleteView(SingleObjectDeleteView):
model = WorkflowState
object_permission = permission_workflow_edit
pk_url_kwarg = 'pk'
def get_extra_context(self):
return {
'navigation_object_list': ('object', 'workflow_instance'),
'object': self.get_object(),
'title': _(
'Delete workflow state: %s?'
) % self.object,
'workflow_instance': self.get_object().workflow,
}
def get_success_url(self):
return reverse(
viewname='document_states:workflow_template_state_list',
kwargs={'pk': self.get_object().workflow.pk}
)
class WorkflowTemplateStateEditView(SingleObjectEditView):
form_class = WorkflowStateForm
model = WorkflowState
object_permission = permission_workflow_edit
pk_url_kwarg = 'pk'
def get_extra_context(self):
return {
'navigation_object_list': ('object', 'workflow_instance'),
'object': self.get_object(),
'title': _(
'Edit workflow state: %s'
) % self.object,
'workflow_instance': self.get_object().workflow,
}
def get_success_url(self):
return reverse(
viewname='document_states:workflow_template_state_list',
kwargs={'pk': self.get_object().workflow.pk}
)
class WorkflowTemplateStateListView(ExternalObjectMixin, SingleObjectListView):
external_object_class = Workflow
external_object_permission = permission_workflow_view
external_object_pk_url_kwarg = 'pk'
object_permission = permission_workflow_view
def get_extra_context(self):
return {
'hide_object': True,
'no_results_icon': icon_workflow_state,
'no_results_main_link': link_workflow_template_state_create.resolve(
context=RequestContext(
self.request, {'object': self.get_workflow()}
)
),
'no_results_text': _(
'Create states and link them using transitions.'
),
'no_results_title': _(
'This workflow doesn\'t have any states'
),
'object': self.get_workflow(),
'title': _('States of workflow: %s') % self.get_workflow()
}
def get_source_queryset(self):
return self.get_workflow().states.all()
def get_workflow(self):
return self.external_object

View File

@@ -0,0 +1,372 @@
from __future__ import absolute_import, unicode_literals
from django.contrib import messages
from django.db import transaction
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.generics import (
AddRemoveView, ConfirmView, FormView, SingleObjectCreateView,
SingleObjectDeleteView, SingleObjectDetailView,
SingleObjectDynamicFormCreateView, SingleObjectDynamicFormEditView,
SingleObjectEditView, SingleObjectListView
)
from mayan.apps.common.mixins import ExternalObjectMixin
from mayan.apps.documents.events import event_document_type_edited
from mayan.apps.documents.models import DocumentType
from mayan.apps.documents.permissions import permission_document_type_edit
from mayan.apps.events.classes import EventType
from mayan.apps.events.models import StoredEventType
from ..classes import WorkflowAction
from ..events import event_workflow_edited
from ..forms import (
WorkflowActionSelectionForm, WorkflowForm, WorkflowPreviewForm,
WorkflowStateActionDynamicForm, WorkflowStateForm, WorkflowTransitionForm,
WorkflowTransitionTriggerEventRelationshipFormSet
)
from ..icons import (
icon_workflow_template_list, icon_workflow_state, icon_workflow_state_action,
icon_workflow_transition, icon_workflow_transition_field
)
from ..links import (
link_workflow_template_create, link_workflow_template_state_create,
link_workflow_template_state_action_selection,
link_workflow_template_transition_create,
link_workflow_template_transition_field_create,
)
from ..models import (
Workflow, WorkflowState, WorkflowStateAction, WorkflowTransition,
WorkflowTransitionField
)
from ..permissions import (
permission_workflow_create, permission_workflow_delete,
permission_workflow_edit, permission_workflow_tools,
permission_workflow_view,
)
from ..tasks import task_launch_all_workflows
class WorkflowTemplateTransitionCreateView(ExternalObjectMixin, SingleObjectCreateView):
external_object_class = Workflow
external_object_permission = permission_workflow_edit
external_object_pk_url_kwarg = 'pk'
form_class = WorkflowTransitionForm
def get_extra_context(self):
return {
'object': self.get_workflow(),
'title': _(
'Create transitions for workflow: %s'
) % self.get_workflow()
}
def get_form_kwargs(self):
kwargs = super(
WorkflowTemplateTransitionCreateView, self
).get_form_kwargs()
kwargs['workflow'] = self.get_workflow()
return kwargs
def get_instance_extra_data(self):
return {'workflow': self.get_workflow()}
def get_source_queryset(self):
return self.get_workflow().transitions.all()
def get_success_url(self):
return reverse(
viewname='document_states:workflow_template_transition_list',
kwargs={'pk': self.kwargs['pk']}
)
def get_workflow(self):
return self.external_object
class WorkflowTemplateTransitionDeleteView(SingleObjectDeleteView):
model = WorkflowTransition
object_permission = permission_workflow_edit
pk_url_kwarg = 'pk'
def get_extra_context(self):
return {
'object': self.get_object(),
'navigation_object_list': ('object', 'workflow_instance'),
'title': _(
'Delete workflow transition: %s?'
) % self.object,
'workflow_instance': self.get_object().workflow,
}
def get_success_url(self):
return reverse(
viewname='document_states:workflow_template_transition_list',
kwargs={'pk': self.get_object().workflow.pk}
)
class WorkflowTemplateTransitionEditView(SingleObjectEditView):
form_class = WorkflowTransitionForm
model = WorkflowTransition
object_permission = permission_workflow_edit
pk_url_kwarg = 'pk'
def get_extra_context(self):
return {
'navigation_object_list': ('object', 'workflow_instance'),
'object': self.get_object(),
'title': _(
'Edit workflow transition: %s'
) % self.object,
'workflow_instance': self.get_object().workflow,
}
def get_form_kwargs(self):
kwargs = super(
WorkflowTemplateTransitionEditView, self
).get_form_kwargs()
kwargs['workflow'] = self.get_object().workflow
return kwargs
def get_success_url(self):
return reverse(
viewname='document_states:workflow_template_transition_list',
kwargs={'pk': self.get_object().workflow.pk}
)
class WorkflowTemplateTransitionListView(ExternalObjectMixin, SingleObjectListView):
external_object_class = Workflow
external_object_permission = permission_workflow_view
external_object_pk_url_kwarg = 'pk'
object_permission = permission_workflow_view
def get_extra_context(self):
return {
'hide_object': True,
'no_results_icon': icon_workflow_transition,
'no_results_main_link': link_workflow_template_transition_create.resolve(
context=RequestContext(
self.request, {'object': self.get_workflow()}
)
),
'no_results_text': _(
'Create a transition and use it to move a workflow from '
' one state to another.'
),
'no_results_title': _(
'This workflow doesn\'t have any transitions'
),
'object': self.get_workflow(),
'title': _(
'Transitions of workflow: %s'
) % self.get_workflow()
}
def get_source_queryset(self):
return self.get_workflow().transitions.all()
def get_workflow(self):
return self.external_object
class WorkflowTemplateTransitionTriggerEventListView(ExternalObjectMixin, FormView):
external_object_class = WorkflowTransition
external_object_permission = permission_workflow_edit
external_object_pk_url_kwarg = 'pk'
form_class = WorkflowTransitionTriggerEventRelationshipFormSet
def dispatch(self, *args, **kwargs):
EventType.refresh()
return super(
WorkflowTemplateTransitionTriggerEventListView, self
).dispatch(*args, **kwargs)
def form_valid(self, form):
try:
for instance in form:
instance.save()
except Exception as exception:
messages.error(
message=_(
'Error updating workflow transition trigger events; %s'
) % exception, request=self.request
)
else:
messages.success(
message=_(
'Workflow transition trigger events updated successfully'
), request=self.request
)
return super(
WorkflowTemplateTransitionTriggerEventListView, self
).form_valid(form=form)
def get_extra_context(self):
return {
'form_display_mode_table': True,
'navigation_object_list': ('object', 'workflow'),
'object': self.get_object(),
'subtitle': _(
'Triggers are events that cause this transition to execute '
'automatically.'
),
'title': _(
'Workflow transition trigger events for: %s'
) % self.get_object(),
'workflow': self.get_object().workflow,
}
def get_initial(self):
obj = self.get_object()
initial = []
# Return the queryset by name from the sorted list of the class
event_type_ids = [event_type.id for event_type in EventType.all()]
event_type_queryset = StoredEventType.objects.filter(
name__in=event_type_ids
)
# Sort queryset in Python by namespace, then by label
event_type_queryset = sorted(
event_type_queryset, key=lambda x: (x.namespace, x.label)
)
for event_type in event_type_queryset:
initial.append({
'transition': obj,
'event_type': event_type,
})
return initial
def get_object(self):
return self.external_object
def get_post_action_redirect(self):
return reverse(
viewname='document_states:workflow_template_transition_list',
kwargs={'pk': self.get_object().workflow.pk}
)
class WorkflowTemplateTransitionFieldCreateView(ExternalObjectMixin, SingleObjectCreateView):
external_object_class = WorkflowTransition
external_object_permission = permission_workflow_edit
fields = (
'name', 'label', 'field_type', 'help_text', 'required', 'widget',
'widget_kwargs'
)
def get_extra_context(self):
return {
'navigation_object_list': ('transition', 'workflow'),
'transition': self.external_object,
'title': _(
'Create a field for workflow transition: %s'
) % self.external_object,
'workflow': self.external_object.workflow
}
def get_instance_extra_data(self):
return {
'transition': self.external_object,
}
def get_queryset(self):
return self.external_object.fields.all()
def get_post_action_redirect(self):
return reverse(
viewname='document_states:workflow_template_transition_field_list',
kwargs={'pk': self.external_object.pk}
)
class WorkflowTemplateTransitionFieldDeleteView(SingleObjectDeleteView):
model = WorkflowTransitionField
object_permission = permission_workflow_edit
def get_extra_context(self):
return {
'navigation_object_list': (
'object', 'workflow_transition', 'workflow'
),
'object': self.object,
'title': _('Delete workflow transition field: %s') % self.object,
'workflow': self.object.transition.workflow,
'workflow_transition': self.object.transition,
}
def get_post_action_redirect(self):
return reverse(
viewname='document_states:workflow_template_transition_field_list',
kwargs={'pk': self.object.transition.pk}
)
class WorkflowTemplateTransitionFieldEditView(SingleObjectEditView):
fields = (
'name', 'label', 'field_type', 'help_text', 'required', 'widget',
'widget_kwargs'
)
model = WorkflowTransitionField
object_permission = permission_workflow_edit
def get_extra_context(self):
return {
'navigation_object_list': (
'object', 'workflow_transition', 'workflow'
),
'object': self.object,
'title': _('Edit workflow transition field: %s') % self.object,
'workflow': self.object.transition.workflow,
'workflow_transition': self.object.transition,
}
def get_post_action_redirect(self):
return reverse(
viewname='document_states:workflow_template_transition_field_list',
kwargs={'pk': self.object.transition.pk}
)
class WorkflowTemplateTransitionFieldListView(ExternalObjectMixin, SingleObjectListView):
external_object_class = WorkflowTransition
external_object_permission = permission_workflow_edit
def get_extra_context(self):
return {
'hide_object': True,
'navigation_object_list': ('object', 'workflow'),
'no_results_icon': icon_workflow_transition_field,
'no_results_main_link': link_workflow_template_transition_field_create.resolve(
context=RequestContext(
request=self.request, dict_={
'object': self.external_object
}
)
),
'no_results_text': _(
'Workflow transition fields allow adding data to the '
'workflow\'s context. This additional context data can then '
'be used by other elements of the workflow system like the '
'workflow state actions.'
),
'no_results_title': _(
'There are no fields for this workflow transition'
),
'object': self.external_object,
'title': _(
'Fields for workflow transition: %s'
) % self.external_object,
'workflow': self.external_object.workflow,
}
def get_source_queryset(self):
return self.external_object.fields.all()

View File

@@ -0,0 +1,261 @@
from __future__ import absolute_import, unicode_literals
from django.contrib import messages
from django.db import transaction
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.generics import (
AddRemoveView, ConfirmView, FormView, SingleObjectCreateView,
SingleObjectDeleteView, SingleObjectDetailView,
SingleObjectDynamicFormCreateView, SingleObjectDynamicFormEditView,
SingleObjectEditView, SingleObjectListView
)
from mayan.apps.common.mixins import ExternalObjectMixin
from mayan.apps.documents.events import event_document_type_edited
from mayan.apps.documents.models import DocumentType
from mayan.apps.documents.permissions import permission_document_type_edit
from mayan.apps.events.classes import EventType
from mayan.apps.events.models import StoredEventType
from ..classes import WorkflowAction
from ..events import event_workflow_edited
from ..forms import (
WorkflowActionSelectionForm, WorkflowForm, WorkflowPreviewForm,
WorkflowStateActionDynamicForm, WorkflowStateForm, WorkflowTransitionForm,
WorkflowTransitionTriggerEventRelationshipFormSet
)
from ..icons import (
icon_workflow_template_list, icon_workflow_state, icon_workflow_state_action,
icon_workflow_transition, icon_workflow_transition_field
)
from ..links import (
link_workflow_template_create, link_workflow_template_state_create,
link_workflow_template_state_action_selection,
link_workflow_template_transition_create,
link_workflow_template_transition_field_create,
)
from ..models import (
Workflow, WorkflowState, WorkflowStateAction, WorkflowTransition,
WorkflowTransitionField
)
from ..permissions import (
permission_workflow_create, permission_workflow_delete,
permission_workflow_edit, permission_workflow_tools,
permission_workflow_view,
)
from ..tasks import task_launch_all_workflows
class DocumentTypeWorkflowTemplatesView(AddRemoveView):
main_object_permission = permission_document_type_edit
main_object_model = DocumentType
main_object_pk_url_kwarg = 'pk'
secondary_object_model = Workflow
secondary_object_permission = permission_workflow_edit
list_available_title = _('Available workflows')
list_added_title = _('Workflows assigned this document type')
related_field = 'workflows'
def get_actions_extra_kwargs(self):
return {'_user': self.request.user}
def get_extra_context(self):
return {
'object': self.main_object,
'subtitle': _(
'Removing a workflow from a document type will also '
'remove all running instances of that workflow.'
),
'title': _(
'Workflows assigned the document type: %s'
) % self.main_object,
}
def action_add(self, queryset, _user):
with transaction.atomic():
event_document_type_edited.commit(
actor=_user, target=self.main_object
)
for obj in queryset:
self.main_object.workflows.add(obj)
event_workflow_edited.commit(
action_object=self.main_object, actor=_user, target=obj
)
def action_remove(self, queryset, _user):
with transaction.atomic():
event_document_type_edited.commit(
actor=_user, target=self.main_object
)
for obj in queryset:
self.main_object.workflows.remove(obj)
event_workflow_edited.commit(
action_object=self.main_object, actor=_user,
target=obj
)
obj.instances.filter(
document__document_type=self.main_object
).delete()
class WorkflowTemplateListView(SingleObjectListView):
model = Workflow
object_permission = permission_workflow_view
def get_extra_context(self):
return {
'hide_object': True,
'no_results_icon': icon_workflow_template_list,
'no_results_main_link': link_workflow_template_create.resolve(
context=RequestContext(request=self.request)
),
'no_results_text': _(
'Workflows store a series of states and keep track of the '
'current state of a document. Transitions are used to change the '
'current state to a new one.'
),
'no_results_title': _(
'No workflows have been defined'
),
'title': _('Workflows'),
}
class WorkflowTemplateCreateView(SingleObjectCreateView):
extra_context = {'title': _('Create workflow')}
form_class = WorkflowForm
model = Workflow
post_action_redirect = reverse_lazy(
viewname='document_states:workflow_template_list'
)
view_permission = permission_workflow_create
def get_save_extra_data(self):
return {'_user': self.request.user}
class WorkflowTemplateDeleteView(SingleObjectDeleteView):
model = Workflow
object_permission = permission_workflow_delete
post_action_redirect = reverse_lazy(
viewname='document_states:workflow_template_list'
)
def get_extra_context(self):
return {
'title': _(
'Delete workflow: %s?'
) % self.object,
}
class WorkflowTemplateEditView(SingleObjectEditView):
form_class = WorkflowForm
model = Workflow
object_permission = permission_workflow_edit
post_action_redirect = reverse_lazy(
viewname='document_states:workflow_template_list'
)
def get_extra_context(self):
return {
'title': _(
'Edit workflow: %s'
) % self.object,
}
def get_save_extra_data(self):
return {'_user': self.request.user}
class WorkflowTemplateDocumentTypesView(AddRemoveView):
main_object_permission = permission_workflow_edit
main_object_model = Workflow
main_object_pk_url_kwarg = 'pk'
secondary_object_model = DocumentType
secondary_object_permission = permission_document_type_edit
list_available_title = _('Available document types')
list_added_title = _('Document types assigned this workflow')
related_field = 'document_types'
def get_actions_extra_kwargs(self):
return {'_user': self.request.user}
def get_extra_context(self):
return {
'object': self.main_object,
'subtitle': _(
'Removing a document type from a workflow will also '
'remove all running instances of that workflow for '
'documents of the document type just removed.'
),
'title': _(
'Document types assigned the workflow: %s'
) % self.main_object,
}
def action_add(self, queryset, _user):
with transaction.atomic():
event_workflow_edited.commit(
actor=_user, target=self.main_object
)
for obj in queryset:
self.main_object.document_types.add(obj)
event_document_type_edited.commit(
action_object=self.main_object, actor=_user, target=obj
)
def action_remove(self, queryset, _user):
with transaction.atomic():
event_workflow_edited.commit(
actor=_user, target=self.main_object
)
for obj in queryset:
self.main_object.document_types.remove(obj)
event_document_type_edited.commit(
action_object=self.main_object, actor=_user,
target=obj
)
self.main_object.instances.filter(
document__document_type=obj
).delete()
class WorkflowTemplatePreviewView(SingleObjectDetailView):
form_class = WorkflowPreviewForm
model = Workflow
object_permission = permission_workflow_view
pk_url_kwarg = 'pk'
def get_extra_context(self):
return {
'hide_labels': True,
'object': self.get_object(),
'title': _('Preview of: %s') % self.get_object()
}
class ToolLaunchWorkflows(ConfirmView):
extra_context = {
'title': _('Launch all workflows?'),
'subtitle': _(
'This will launch all workflows created after documents have '
'already been uploaded.'
)
}
view_permission = permission_workflow_tools
def view_action(self):
task_launch_all_workflows.apply_async()
messages.success(
message=_('Workflow launch queued successfully.'),
request=self.request
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +1,12 @@
from __future__ import unicode_literals
from django import forms
from django.urls import reverse
from django.utils.html import format_html_join, mark_safe
def widget_transition_events(transition):
return format_html_join(
sep='\n', format_string='<div class="">{}</div>', args_generator=(
(
transition_trigger.event_type.label,
) for transition_trigger in transition.trigger_events.all()
)
)
def widget_workflow_diagram(workflow):
return mark_safe(
'<img class="img-responsive" src="{}" style="margin:auto;">'.format(
reverse(
viewname='document_states:workflow_image', kwargs={
'pk': workflow.pk
}
)
)
)
class WorkflowImageWidget(forms.widgets.Widget):
def render(self, name, value, attrs=None):
if value:
output = []
output.append(widget_workflow_diagram(value))
return mark_safe(''.join(output))
else:
return ''
template_name = 'document_states/forms/widgets/workflow_image.html'
def format_value(self, value):
if value == '' or value is None:
return None
return value

View File

@@ -310,14 +310,6 @@ class DocumentsApp(MayanAppConfig):
attribute='label', is_identifier=True, is_sortable=True,
source=DeletedDocument
)
SourceColumn(
func=lambda context: document_page_thumbnail_widget.render(
instance=context['object']
), label=_('Thumbnail'), source=DeletedDocument
)
SourceColumn(
attribute='document_type', is_sortable=True, source=DeletedDocument
)
SourceColumn(
attribute='deleted_date_time', include_label=True, order=99,
source=DeletedDocument

View File

@@ -41,7 +41,8 @@ class DocumentTypeFilteredSelectForm(forms.Form):
self.fields['document_type'] = field_class(
help_text=help_text, label=_('Document type'),
queryset=queryset, required=True,
widget=widget_class(attrs={'size': 10}), **extra_kwargs
widget=widget_class(attrs={'class': 'select2', 'size': 10}),
**extra_kwargs
)

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