Compare commits
105 Commits
features/q
...
features/w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36b89cf0ea | ||
|
|
6bcf35bef5 | ||
|
|
7ef6102876 | ||
|
|
4363bba0fe | ||
|
|
e2f2181ebb | ||
|
|
d4f7e2cd16 | ||
|
|
058e36b4a9 | ||
|
|
1ddd5f26b1 | ||
|
|
44652d49fb | ||
|
|
119c1bde76 | ||
|
|
ed227b4111 | ||
|
|
c44090aca6 | ||
|
|
8a7da6a103 | ||
|
|
3e3b1f75a0 | ||
|
|
1ab7b7b9b1 | ||
|
|
3fab5c1427 | ||
|
|
516c3aeb2c | ||
|
|
3ac1000b46 | ||
|
|
4adeefc978 | ||
|
|
8bc4b6a95e | ||
|
|
37e85590e8 | ||
|
|
78a0189e1c | ||
|
|
91b0b2d9c3 | ||
|
|
8a54deba3d | ||
|
|
22da1e0a78 | ||
|
|
c9668d62e5 | ||
|
|
7a01a77c43 | ||
|
|
9564db398f | ||
|
|
7faa24eb7b | ||
|
|
51f278301b | ||
|
|
2cc35c3c61 | ||
|
|
8c73fda1ae | ||
|
|
8811c8269f | ||
|
|
f36f99c5fb | ||
|
|
0e972eff06 | ||
|
|
7913b5ddcc | ||
|
|
1c86ea5b5b | ||
|
|
ec6a3bd960 | ||
|
|
080553c797 | ||
|
|
08ee07e652 | ||
|
|
d7d77fcb55 | ||
|
|
601bff304f | ||
|
|
bb5324ef50 | ||
|
|
4c212f6ea4 | ||
|
|
941356ed69 | ||
|
|
97804b255b | ||
|
|
06c3ef6583 | ||
|
|
6cd857e2bf | ||
|
|
fbb0f0b9bd | ||
|
|
9e068c3e83 | ||
|
|
72a3807354 | ||
|
|
109fcba795 | ||
|
|
01380e0572 | ||
|
|
5146c6d202 | ||
|
|
300bdbfc8a | ||
|
|
a0331e0236 | ||
|
|
744bfefa5c | ||
|
|
850fb16c8c | ||
|
|
72ba805fbb | ||
|
|
3d7b40f029 | ||
|
|
2039a9f13b | ||
|
|
bb8f12dd7a | ||
|
|
40ab1f3665 | ||
|
|
fdef757fd0 | ||
|
|
3608ee1141 | ||
|
|
7fb3d61dff | ||
|
|
e9aa11673b | ||
|
|
03a7aa5daf | ||
|
|
755f20c5c4 | ||
|
|
64772e2e90 | ||
|
|
75a4a426e0 | ||
|
|
42a7ebeea2 | ||
|
|
3d22f48555 | ||
|
|
488e048d8f | ||
|
|
2f82559a5c | ||
|
|
7d5b7b9fc4 | ||
|
|
7aa68b8bbf | ||
|
|
aecde926f2 | ||
|
|
6b95628e56 | ||
|
|
56a1b97b46 | ||
|
|
34a5a54c8b | ||
|
|
0c17ab3f8a | ||
|
|
c967a25f82 | ||
|
|
7562588c42 | ||
|
|
a1a706b7b9 | ||
|
|
d623cb2df5 | ||
|
|
488ddcf1e1 | ||
|
|
3d39893f17 | ||
|
|
3694839d97 | ||
|
|
cce27aceca | ||
|
|
c73d251370 | ||
|
|
091f0d1cfd | ||
|
|
d2affdcf21 | ||
|
|
885d430b98 | ||
|
|
39eabe1c54 | ||
|
|
f6ad579829 | ||
|
|
6fc9e46882 | ||
|
|
2d326a679d | ||
|
|
aa8c2db446 | ||
|
|
925b55d76d | ||
|
|
5808d3653d | ||
|
|
bc072f7b7e | ||
|
|
b3d59eee39 | ||
|
|
7d379a52af | ||
|
|
499ab1f3e7 |
@@ -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
14
CHANGES_BC.rst
Normal 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.
|
||||
785
HISTORY.rst
785
HISTORY.rst
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
166
docs/releases/3.3.rst
Normal 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/
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.template.loader import get_template
|
||||
|
||||
|
||||
class IconDriver(object):
|
||||
context = {}
|
||||
_registry = {}
|
||||
|
||||
@classmethod
|
||||
@@ -14,6 +15,17 @@ class IconDriver(object):
|
||||
def register(cls, driver_class):
|
||||
cls._registry[driver_class.name] = driver_class
|
||||
|
||||
def get_context(self):
|
||||
return self.context
|
||||
|
||||
def render(self, extra_context=None):
|
||||
context = self.get_context()
|
||||
if extra_context:
|
||||
context.update(extra_context)
|
||||
return get_template(template_name=self.template_name).render(
|
||||
context=context
|
||||
)
|
||||
|
||||
|
||||
class FontAwesomeDriver(IconDriver):
|
||||
name = 'fontawesome'
|
||||
@@ -22,10 +34,8 @@ class FontAwesomeDriver(IconDriver):
|
||||
def __init__(self, symbol):
|
||||
self.symbol = symbol
|
||||
|
||||
def render(self):
|
||||
return get_template(template_name=self.template_name).render(
|
||||
context={'symbol': self.symbol}
|
||||
)
|
||||
def get_context(self):
|
||||
return {'symbol': self.symbol}
|
||||
|
||||
|
||||
class FontAwesomeDualDriver(IconDriver):
|
||||
@@ -36,23 +46,21 @@ class FontAwesomeDualDriver(IconDriver):
|
||||
self.primary_symbol = primary_symbol
|
||||
self.secondary_symbol = secondary_symbol
|
||||
|
||||
def render(self):
|
||||
return get_template(template_name=self.template_name).render(
|
||||
context={
|
||||
'data': (
|
||||
{
|
||||
'class': 'fas fa-circle',
|
||||
'transform': 'down-3 right-10',
|
||||
'mask': 'fas fa-{}'.format(self.primary_symbol)
|
||||
},
|
||||
{'class': 'far fa-circle', 'transform': 'down-3 right-10'},
|
||||
{
|
||||
'class': 'fas fa-{}'.format(self.secondary_symbol),
|
||||
'transform': 'shrink-4 down-3 right-10'
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
def get_context(self):
|
||||
return {
|
||||
'data': (
|
||||
{
|
||||
'class': 'fas fa-circle',
|
||||
'transform': 'down-3 right-10',
|
||||
'mask': 'fas fa-{}'.format(self.primary_symbol)
|
||||
},
|
||||
{'class': 'far fa-circle', 'transform': 'down-3 right-10'},
|
||||
{
|
||||
'class': 'fas fa-{}'.format(self.secondary_symbol),
|
||||
'transform': 'shrink-4 down-3 right-10'
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class FontAwesomeCSSDriver(IconDriver):
|
||||
@@ -62,10 +70,8 @@ class FontAwesomeCSSDriver(IconDriver):
|
||||
def __init__(self, css_classes):
|
||||
self.css_classes = css_classes
|
||||
|
||||
def render(self):
|
||||
return get_template(template_name=self.template_name).render(
|
||||
context={'css_classes': self.css_classes}
|
||||
)
|
||||
def get_context(self):
|
||||
return {'css_classes': self.css_classes}
|
||||
|
||||
|
||||
class FontAwesomeMasksDriver(IconDriver):
|
||||
@@ -75,23 +81,23 @@ class FontAwesomeMasksDriver(IconDriver):
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
def render(self):
|
||||
return get_template(template_name=self.template_name).render(
|
||||
context={'data': self.data}
|
||||
)
|
||||
def get_context(self):
|
||||
return {'data': self.data}
|
||||
|
||||
|
||||
class FontAwesomeLayersDriver(IconDriver):
|
||||
name = 'fontawesome-layers'
|
||||
template_name = 'appearance/icons/font_awesome_layers.html'
|
||||
|
||||
def __init__(self, data):
|
||||
def __init__(self, data, shadow_class=None):
|
||||
self.data = data
|
||||
self.shadow_class = shadow_class
|
||||
|
||||
def render(self):
|
||||
return get_template(template_name=self.template_name).render(
|
||||
context={'data': self.data}
|
||||
)
|
||||
def get_context(self):
|
||||
return {
|
||||
'data': self.data,
|
||||
'shadow_class': self.shadow_class,
|
||||
}
|
||||
|
||||
|
||||
class Icon(object):
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -112,12 +115,12 @@ a i {
|
||||
}
|
||||
|
||||
.dashboard-widget {
|
||||
box-shadow: 1px 1px 1px rgba(0,0,0,0.3);
|
||||
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.dashboard-widget .panel-heading i {
|
||||
text-shadow: 1px 1px 1px rgba(0,0,0,0.3);
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dashboard-widget-icon {
|
||||
@@ -170,7 +173,7 @@ a i {
|
||||
}
|
||||
.navbar-collapse {
|
||||
border-top: 1px solid transparent;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.1);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.navbar-fixed-top {
|
||||
top: 0;
|
||||
@@ -213,6 +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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<span class="fa-layers fa-fw" style="margin-right: 7px;">
|
||||
{% if enable_shadow %}
|
||||
<i class="{{ shadow_class }}" data-fa-transform="right-1 down-2" style="color:rgba(0, 0, 0, 0.3); stroke: rgba(255, 255, 255, 0.3); stroke-width: 20;"></i>
|
||||
{% endif %}
|
||||
{% for entry in data %}
|
||||
<i class="{{ entry.class }}" data-fa-transform="{{ entry.transform }}" data-fa-mask="{{ entry.mask }}"></i>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
<i class="fa fa-{{ symbol }}" style="padding-right: 5px; width: auto;"></i>
|
||||
{% if enable_shadow %}
|
||||
<span class="fa-layers fa-fw" >
|
||||
<i class="fa fa-{{ symbol }}" data-fa-transform="right-1 down-2" style="color:rgba(0, 0, 0, 0.3);stroke: rgba(255, 255, 255, 0.3); stroke-width: 20;"></i>
|
||||
<i class="fa fa-{{ symbol }}"></i>
|
||||
</span>
|
||||
{% else %}
|
||||
<i class="fa fa-{{ symbol }}" style="padding-right: 5px; width: auto;"></i>
|
||||
{% endif %}
|
||||
|
||||
70
mayan/apps/appearance/templates/appearance/menu_main.html
Normal file
70
mayan/apps/appearance/templates/appearance/menu_main.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% load navigation_tags %}
|
||||
{% load smart_settings_tags %}
|
||||
|
||||
{% load common_tags %}
|
||||
{% load navigation_tags %}
|
||||
|
||||
{% spaceless %}
|
||||
<div class="panel-group" id="accordion-sidebar" role="tablist" aria-multiselectable="true">
|
||||
{% navigation_resolve_menu name='main' as main_menus_results %}
|
||||
{% for main_menu_results in main_menus_results %}
|
||||
{% for link_group in main_menu_results.link_groups %}
|
||||
{% for link in link_group.links %}
|
||||
{% with 'active' as li_class_active %}
|
||||
{% with ' ' as link_classes %}
|
||||
{% if link|get_type == "<class 'mayan.apps.navigation.classes.Menu'>" %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="headingOne">
|
||||
<h4 class="panel-title">
|
||||
<a class="non-ajax collapsed" role="button" data-toggle="collapse" data-parent="#accordion-sidebar" href="#accordion-body-{{ forloop.counter }}" aria-expanded="false" aria-controls="collapseOne">
|
||||
<div class="pull-left">
|
||||
{% if link.icon %}
|
||||
<i class="hidden-xs hidden-sm hidden-md {{ link.icon }}"></i>
|
||||
{% endif %}
|
||||
{% if link.icon_class %}{{ link.icon_class.render }}{% endif %}
|
||||
{{ link.label }}
|
||||
</div>
|
||||
<div class="accordion-indicator pull-right"><span class="caret"></span></div>
|
||||
<div class="clearfix"></div>
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="accordion-body-{{ forloop.counter }}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingOne">
|
||||
<div class="panel-body">
|
||||
<ul class="list-unstyled">
|
||||
{% navigation_resolve_menu name=link.name as sub_menus_results %}
|
||||
{% for sub_menu_results in sub_menus_results %}
|
||||
{% for link_group in sub_menu_results.link_groups %}
|
||||
{% with '' as link_class_active %}
|
||||
{% with 'a-main-menu-accordion-link' as link_classes %}
|
||||
{% with 'true' as as_li %}
|
||||
{% with link_group.links as object_navigation_links %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="headingOne">
|
||||
<h4 class="panel-title">
|
||||
{% include 'navigation/generic_link_instance.html' %}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endspaceless %}
|
||||
@@ -3,10 +3,11 @@
|
||||
{% load navigation_tags %}
|
||||
{% load smart_settings_tags %}
|
||||
|
||||
{% spaceless %}
|
||||
<nav class="navbar navbar-default navbar-fixed-top">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
|
||||
<button aria-expanded="false" aria-controls="navbar" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" type="button">
|
||||
<span class="sr-only">{% trans 'Toggle navigation' %}</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
@@ -14,9 +15,10 @@
|
||||
</button>
|
||||
<a class="navbar-brand" href="{% url home_view %}">{% smart_setting 'COMMON_PROJECT_TITLE' %}</a>
|
||||
</div>
|
||||
|
||||
<div id="navbar" class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
{% navigation_resolve_menu name='main' as topbar_menus_results %}
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{% navigation_resolve_menu name='topbar' as topbar_menus_results %}
|
||||
{% for tobpar_menu_result in topbar_menus_results %}
|
||||
{% for link_group in tobpar_menu_result.link_groups %}
|
||||
{% for link in link_group.links %}
|
||||
@@ -34,24 +36,8 @@
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{% get_menu_links name='main' as menu_links %}
|
||||
{% for link_set in menu_links %}
|
||||
{% for link in link_set %}
|
||||
{% with 'true' as as_li %}
|
||||
{% with 'true' as hide_active_anchor %}
|
||||
{% with 'active' as li_class_active %}
|
||||
{% with 'first' as li_class_first %}
|
||||
{% with ' ' as link_classes %}
|
||||
{% include 'navigation/generic_subnavigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endspaceless %}
|
||||
@@ -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" %}'
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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=(
|
||||
|
||||
@@ -38,16 +38,26 @@ link_check_out_document = Link(
|
||||
args='object.pk', condition=is_not_checked_out,
|
||||
icon_class=icon_check_out_document,
|
||||
permissions=(permission_document_check_out,),
|
||||
text=_('Check out document'), view='checkouts:check_out_document',
|
||||
text=_('Check out document'), view='checkouts:check_out_document'
|
||||
)
|
||||
link_check_out_document_multiple = Link(
|
||||
icon_class=icon_check_out_document,
|
||||
permissions=(permission_document_check_out,), text=_('Check out'),
|
||||
view='checkouts:check_out_document_multiple'
|
||||
)
|
||||
link_check_in_document = Link(
|
||||
args='object.pk', icon_class=icon_check_in_document,
|
||||
condition=is_checked_out, permissions=(
|
||||
permission_document_check_in, permission_document_check_in_override
|
||||
), text=_('Check in document'), view='checkouts:check_in_document',
|
||||
), text=_('Check in document'), view='checkouts:check_in_document'
|
||||
)
|
||||
link_check_in_document_multiple = Link(
|
||||
icon_class=icon_check_in_document,
|
||||
permissions=(permission_document_check_in,), text=_('Check in'),
|
||||
view='checkouts:check_in_document_multiple'
|
||||
)
|
||||
link_check_out_info = Link(
|
||||
args='resolved_object.pk', icon_class=icon_check_out_info, permissions=(
|
||||
permission_document_check_out_detail_view,
|
||||
), text=_('Check in/out'), view='checkouts:check_out_info',
|
||||
), text=_('Check in/out'), view='checkouts:check_out_info'
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.apps import apps
|
||||
from django.db import models, transaction
|
||||
from django.utils.timezone import now
|
||||
|
||||
from mayan.apps.acls.models import AccessControlList
|
||||
from mayan.apps.documents.models import Document
|
||||
|
||||
from .events import (
|
||||
@@ -14,10 +15,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:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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'
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -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):
|
||||
def get_extra_context(self):
|
||||
document = self.get_object()
|
||||
|
||||
context = {
|
||||
'object': document,
|
||||
}
|
||||
|
||||
if document.get_check_out_info().user != self.request.user:
|
||||
context['title'] = _(
|
||||
'You didn\'t originally checked out this document. '
|
||||
'Forcefully check in the document: %s?'
|
||||
) % document
|
||||
else:
|
||||
context['title'] = _('Check in the document: %s?') % document
|
||||
|
||||
return context
|
||||
|
||||
def get_object(self):
|
||||
return get_object_or_404(klass=Document, pk=self.kwargs['pk'])
|
||||
|
||||
def get_post_action_redirect(self):
|
||||
return reverse(
|
||||
viewname='checkouts:check_out_info', kwargs={
|
||||
'pk': self.get_object().pk
|
||||
}
|
||||
)
|
||||
|
||||
def view_action(self):
|
||||
document = self.get_object()
|
||||
|
||||
if document.get_check_out_info().user == self.request.user:
|
||||
AccessControlList.objects.check_access(
|
||||
obj=document, permissions=(permission_document_check_in,),
|
||||
user=self.request.user
|
||||
)
|
||||
else:
|
||||
AccessControlList.objects.check_access(
|
||||
obj=document,
|
||||
permissions=(permission_document_check_in_override,),
|
||||
user=self.request.user
|
||||
)
|
||||
|
||||
try:
|
||||
document.check_in(user=self.request.user)
|
||||
except DocumentNotCheckedOut:
|
||||
messages.error(
|
||||
message=_('Document has not been checked out.'),
|
||||
request=self.request
|
||||
)
|
||||
else:
|
||||
messages.success(
|
||||
message=_(
|
||||
'Document "%s" checked in successfully.'
|
||||
) % document, request=self.request
|
||||
)
|
||||
|
||||
|
||||
class CheckoutDocumentView(SingleObjectCreateView):
|
||||
form_class = DocumentCheckoutForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.document = get_object_or_404(klass=Document, pk=self.kwargs['pk'])
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
obj=self.document, permissions=(permission_document_check_out,),
|
||||
user=request.user
|
||||
)
|
||||
|
||||
return super(
|
||||
CheckoutDocumentView, self
|
||||
).dispatch(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
try:
|
||||
instance = form.save(commit=False)
|
||||
instance.user = self.request.user
|
||||
instance.document = self.document
|
||||
instance.save()
|
||||
except DocumentAlreadyCheckedOut:
|
||||
messages.error(
|
||||
message=_('Document already checked out.'),
|
||||
request=self.request
|
||||
)
|
||||
else:
|
||||
messages.success(
|
||||
message=_(
|
||||
'Document "%s" checked out successfully.'
|
||||
) % self.document, request=self.request
|
||||
)
|
||||
|
||||
return HttpResponseRedirect(redirect_to=self.get_success_url())
|
||||
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):
|
||||
return {
|
||||
'object': self.document,
|
||||
'title': _('Check out document: %s') % self.document
|
||||
queryset = self.get_object_list()
|
||||
|
||||
result = {
|
||||
'title': ungettext(
|
||||
singular='Check in %(count)d document',
|
||||
plural='Check in %(count)d documents',
|
||||
number=queryset.count()
|
||||
) % {
|
||||
'count': queryset.count(),
|
||||
}
|
||||
}
|
||||
|
||||
def get_post_action_redirect(self):
|
||||
return reverse(
|
||||
viewname='checkouts:check_out_info', kwargs={
|
||||
'pk': self.document.pk
|
||||
}
|
||||
)
|
||||
if queryset.count() == 1:
|
||||
result.update(
|
||||
{
|
||||
'object': queryset.first(),
|
||||
'title': _(
|
||||
'Check in document: %s'
|
||||
) % queryset.first()
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
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(),
|
||||
def get_post_object_action_url(self):
|
||||
if self.action_count == 1:
|
||||
return reverse(
|
||||
viewname='checkouts:document_checkout_info',
|
||||
kwargs={'pk': self.action_id_list[0]}
|
||||
)
|
||||
else:
|
||||
super(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
|
||||
)
|
||||
|
||||
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'),
|
||||
}
|
||||
check_in_override_queryset = AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_check_in_override,
|
||||
queryset=source_queryset, user=self.request.user
|
||||
)
|
||||
|
||||
return check_in_queryset | check_in_override_queryset
|
||||
|
||||
def object_action(self, form, instance):
|
||||
DocumentCheckout.business_logic.check_in_document(
|
||||
document=instance, user=self.request.user
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class CheckoutDetailView(SingleObjectDetailView):
|
||||
class DocumentCheckoutView(MultipleObjectFormActionView):
|
||||
error_message = 'Unable to checkout document "%(instance)s". %(exception)s'
|
||||
form_class = DocumentCheckoutForm
|
||||
model = Document
|
||||
object_permission = permission_document_check_out
|
||||
pk_url_kwarg = 'pk'
|
||||
success_message_singular = '%(count)d document checked out.'
|
||||
success_message_plural = '%(count)d documents checked out.'
|
||||
|
||||
def get_extra_context(self):
|
||||
queryset = self.get_object_list()
|
||||
|
||||
result = {
|
||||
'title': ungettext(
|
||||
singular='Checkout %(count)d document',
|
||||
plural='Checkout %(count)d documents',
|
||||
number=queryset.count()
|
||||
) % {
|
||||
'count': queryset.count(),
|
||||
}
|
||||
}
|
||||
|
||||
if queryset.count() == 1:
|
||||
result.update(
|
||||
{
|
||||
'object': queryset.first(),
|
||||
'title': _(
|
||||
'Check out document: %s'
|
||||
) % queryset.first()
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def get_post_object_action_url(self):
|
||||
if self.action_count == 1:
|
||||
return reverse(
|
||||
viewname='checkouts:document_checkout_info',
|
||||
kwargs={'pk': self.action_id_list[0]}
|
||||
)
|
||||
else:
|
||||
super(DocumentCheckoutView, self).get_post_action_redirect()
|
||||
|
||||
def object_action(self, form, instance):
|
||||
DocumentCheckout.objects.check_out_document(
|
||||
block_new_version=form.cleaned_data['block_new_version'],
|
||||
document=instance,
|
||||
expiration_datetime=form.cleaned_data['expiration_datetime'],
|
||||
user=self.request.user,
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -35,8 +35,14 @@ icon_menu_about = Icon(
|
||||
icon_menu_user = Icon(
|
||||
driver_name='fontawesome', symbol='user-circle'
|
||||
)
|
||||
icon_object_error_list_with_icon = Icon(
|
||||
driver_name='fontawesome', symbol='lock'
|
||||
icon_object_errors = Icon(
|
||||
driver_name='fontawesome', symbol='exclamation-triangle'
|
||||
)
|
||||
icon_object_error_list = Icon(
|
||||
driver_name='fontawesome', symbol='exclamation-triangle'
|
||||
)
|
||||
icon_object_error_list_clear = Icon(
|
||||
driver_name='fontawesome', symbol='times'
|
||||
)
|
||||
icon_ok = Icon(
|
||||
driver_name='fontawesome', symbol='check'
|
||||
|
||||
@@ -8,8 +8,8 @@ from mayan.apps.navigation.classes import Link
|
||||
from .icons import (
|
||||
icon_about, icon_current_user_locale_profile_details,
|
||||
icon_current_user_locale_profile_edit, icon_documentation,
|
||||
icon_forum, icon_license, icon_object_error_list_with_icon,
|
||||
icon_setup, icon_source_code, icon_support, icon_tools
|
||||
icon_forum, icon_license, icon_setup, icon_source_code, icon_support,
|
||||
icon_tools
|
||||
)
|
||||
from .permissions_runtime import permission_error_log_view
|
||||
|
||||
@@ -50,21 +50,17 @@ link_documentation = Link(
|
||||
text=_('Documentation'), url='https://docs.mayan-edms.com'
|
||||
)
|
||||
link_object_error_list = Link(
|
||||
icon_class_path='mayan.apps.common.icons.icon_object_error_list',
|
||||
kwargs=get_kwargs_factory('resolved_object'),
|
||||
permissions=(permission_error_log_view,), text=_('Errors'),
|
||||
view='common:object_error_list',
|
||||
)
|
||||
link_object_error_list_clear = Link(
|
||||
icon_class_path='mayan.apps.common.icons.icon_object_error_list_clear',
|
||||
kwargs=get_kwargs_factory('resolved_object'),
|
||||
permissions=(permission_error_log_view,), text=_('Clear all'),
|
||||
view='common:object_error_list_clear',
|
||||
)
|
||||
link_object_error_list_with_icon = Link(
|
||||
kwargs=get_kwargs_factory('resolved_object'),
|
||||
icon_class=icon_object_error_list_with_icon,
|
||||
permissions=(permission_error_log_view,), text=_('Errors'),
|
||||
view='common:error_list',
|
||||
)
|
||||
link_forum = Link(
|
||||
icon_class=icon_forum, tags='new_window', text=_('Forum'),
|
||||
url='https://forum.mayan-edms.com'
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import errno
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from pathlib2 import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import management
|
||||
from django.core.management.base import CommandError
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mayan.apps.documents.models import DocumentType
|
||||
from mayan.apps.storage.utils import fs_cleanup
|
||||
|
||||
from ...literals import MESSAGE_DEPRECATION_WARNING
|
||||
from ...warnings import DeprecationWarning
|
||||
|
||||
CONVERTDB_FOLDER = 'convertdb'
|
||||
CONVERTDB_OUTPUT_FILENAME = 'migrate.json'
|
||||
|
||||
|
||||
class Command(management.BaseCommand):
|
||||
help = 'Convert from a database backend to another one.'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
category=DeprecationWarning,
|
||||
message=force_text(MESSAGE_DEPRECATION_WARNING)
|
||||
)
|
||||
|
||||
super(Command, self).__init__(*args, **kwargs)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'args', metavar='app_label[.ModelName]', nargs='*',
|
||||
help=_(
|
||||
'Restricts dumped data to the specified app_label or '
|
||||
'app_label.ModelName.'
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
'--from', action='store', default='default', dest='from',
|
||||
help=_(
|
||||
'The database from which data will be exported. If omitted '
|
||||
'the database named "default" will be used.'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--to', action='store', default='default', dest='to',
|
||||
help=_(
|
||||
'The database to which data will be imported. If omitted '
|
||||
'the database named "default" will be used.'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--force', action='store_true', dest='force',
|
||||
help=_(
|
||||
'Force the conversion of the database even if the receiving '
|
||||
'database is not empty.'
|
||||
),
|
||||
)
|
||||
|
||||
def handle(self, *app_labels, **options):
|
||||
# Create the media/convertdb folder
|
||||
convertdb_folder_path = force_text(
|
||||
Path(
|
||||
settings.MEDIA_ROOT, CONVERTDB_FOLDER
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
os.makedirs(convertdb_folder_path)
|
||||
except OSError as exception:
|
||||
if exception.errno == errno.EEXIST:
|
||||
pass
|
||||
|
||||
convertdb_file_path = force_text(
|
||||
Path(
|
||||
convertdb_folder_path, CONVERTDB_OUTPUT_FILENAME
|
||||
)
|
||||
)
|
||||
|
||||
management.call_command(command_name='purgeperiodictasks')
|
||||
|
||||
management.call_command(
|
||||
'dumpdata', *app_labels, all=True,
|
||||
database=options['from'], natural_primary=True,
|
||||
natural_foreign=True, output=convertdb_file_path,
|
||||
interactive=False, format='json'
|
||||
)
|
||||
|
||||
if DocumentType.objects.using(options['to']).count() and not options['force']:
|
||||
fs_cleanup(convertdb_file_path)
|
||||
raise CommandError(
|
||||
'There is existing data in the database that will be '
|
||||
'used for the import. If you proceed with the conversion '
|
||||
'you might lose data. Please check your settings.'
|
||||
)
|
||||
|
||||
management.call_command(
|
||||
'loaddata', convertdb_file_path, database=options['to'], interactive=False,
|
||||
verbosity=3
|
||||
)
|
||||
fs_cleanup(convertdb_file_path)
|
||||
@@ -17,6 +17,7 @@ menu_object = Menu(label=_('Actions'), name='object')
|
||||
menu_secondary = Menu(label=_('Secondary'), name='secondary')
|
||||
menu_setup = Menu(name='setup')
|
||||
menu_tools = Menu(name='tools')
|
||||
menu_topbar = Menu(name='topbar')
|
||||
menu_user = Menu(
|
||||
icon_class=icon_menu_user, name='user', label=_('User')
|
||||
)
|
||||
|
||||
32
mayan/apps/common/migrations/0012_auto_20190711_0548.py
Normal file
32
mayan/apps/common/migrations/0012_auto_20190711_0548.py
Normal file
File diff suppressed because one or more lines are too long
22
mayan/apps/common/serialization.py
Normal file
22
mayan/apps/common/serialization.py
Normal 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)
|
||||
@@ -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')}
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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, _(
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
_(
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
20
mayan/apps/converter/migrations/0014_auto_20190626_1904.py
Normal file
20
mayan/apps/converter/migrations/0014_auto_20190626_1904.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.'),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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. '
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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',
|
||||
|
||||
9
mayan/apps/document_states/fields.py
Normal file
9
mayan/apps/document_states/fields.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
25
mayan/apps/document_states/html_widgets.py
Normal file
25
mayan/apps/document_states/html_widgets.py
Normal 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
|
||||
}
|
||||
)
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')]),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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,15 +510,30 @@ class WorkflowInstance(models.Model):
|
||||
def __str__(self):
|
||||
return force_text(self.workflow)
|
||||
|
||||
def do_transition(self, transition, user=None, comment=None):
|
||||
try:
|
||||
if transition in self.get_current_state().origin_transitions.all():
|
||||
self.log_entries.create(
|
||||
comment=comment or '', transition=transition, user=user
|
||||
)
|
||||
except AttributeError:
|
||||
# No initial state has been set for this workflow
|
||||
pass
|
||||
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 '',
|
||||
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(
|
||||
@@ -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,33 +660,47 @@ 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 save(self, *args, **kwargs):
|
||||
result = super(WorkflowInstanceLogEntry, self).save(*args, **kwargs)
|
||||
context = self.workflow_instance.get_context()
|
||||
context.update(
|
||||
{
|
||||
'entry_log': self
|
||||
}
|
||||
)
|
||||
|
||||
for action in self.transition.origin_state.exit_actions.filter(enabled=True):
|
||||
context.update(
|
||||
{
|
||||
'action': action,
|
||||
}
|
||||
)
|
||||
action.execute(context=context)
|
||||
|
||||
for action in self.transition.destination_state.entry_actions.filter(enabled=True):
|
||||
context.update(
|
||||
{
|
||||
'action': action,
|
||||
}
|
||||
)
|
||||
action.execute(context=context)
|
||||
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(
|
||||
{
|
||||
'entry_log': self
|
||||
}
|
||||
)
|
||||
|
||||
for action in self.transition.origin_state.exit_actions.filter(enabled=True):
|
||||
context.update(
|
||||
{
|
||||
'action': action,
|
||||
}
|
||||
)
|
||||
action.execute(context=context)
|
||||
|
||||
for action in self.transition.destination_state.entry_actions.filter(enabled=True):
|
||||
context.update(
|
||||
{
|
||||
'action': action,
|
||||
}
|
||||
)
|
||||
action.execute(context=context)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class WorkflowRuntimeProxy(Workflow):
|
||||
class Meta:
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
32
mayan/apps/document_states/settings.py
Normal file
32
mayan/apps/document_states/settings.py
Normal 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.'
|
||||
)
|
||||
)
|
||||
12
mayan/apps/document_states/storages.py
Normal file
12
mayan/apps/document_states/storages.py
Normal 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)
|
||||
@@ -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')
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{% if value %}
|
||||
<ul>
|
||||
{% for key, value in value.items %}
|
||||
<li>{{ key }}: {{ value }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
@@ -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;" />
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
from .workflow_instance_views import * # NOQA
|
||||
from .workflow_proxy_views import * # NOQA
|
||||
from .workflow_views import * # NOQA
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
261
mayan/apps/document_states/views/workflow_template_views.py
Normal file
261
mayan/apps/document_states/views/workflow_template_views.py
Normal 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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user