Compare commits

..

13 Commits

Author SHA1 Message Date
Roberto Rosario
754b84b4d7 Merge branch 'versions/minor' into features/workflow_context 2019-07-05 21:38:00 -04:00
Roberto Rosario
572690e2bc Finish workflow context implementation
Improve workflow instance detail view.
Add workflow transition field widget support.
Fix workflow transition field required support.
Update tests.

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

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

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-30 09:51:22 -04:00
Roberto Rosario
42db8255d1 Merge branch 'versions/minor' into features/workflow_context
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-29 20:35:25 -04:00
Roberto Rosario
14d45cbe90 Use polylines for the edge splines
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 15:48:44 -04:00
Roberto Rosario
75be11bc96 Hightlight initial state
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 15:35:33 -04:00
Roberto Rosario
ebf29d0eed Add actions to workflow preview
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 15:35:27 -04:00
Roberto Rosario
a391d27b44 Add transition form comment help text
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 14:33:37 -04:00
Roberto Rosario
753c9b8b4b Merge branch 'versions/minor' into features/workflow_context 2019-06-28 14:08:58 -04:00
133 changed files with 4997 additions and 3968 deletions

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -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 installdependencies \
&& mayan-edms.py installjavascript \
&& MAYAN_STATIC_ROOT=${PROJECT_INSTALL_DIR}/static mayan-edms.py preparestatic --link --noinput
COPY --chown=mayan:mayan requirements/testing-base.txt "${PROJECT_INSTALL_DIR}"

View File

@@ -1 +1 @@
3.2.6
3.2.5

View File

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

View File

@@ -1,111 +0,0 @@
Version 3.2.6
=============
Released: July 10, 2019
Changes
-------
- Remove the smart settings app * import. Following MERC 0005.
- Encode settings YAML before hashing. Avoids unicode issues with Python 3.
- Fix document icon used in the workflow runtime links.
- Add trashed date time label.
- Fix thumbnail generation issue. GitLab issue #637.
Thanks to Giacomo Cariello (@giacomocariello) for the report
and the merge request fixing the issue.
Removals
--------
- None
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::
sudo -u mayan /opt/mayan-edms/bin/pip install mayan-edms==3.2.6
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_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf
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_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py 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
-----------------------------
- None
Bugs fixed or issues closed
---------------------------
- :gitlab-issue:`637` Thumbnail generation bug
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/

View File

@@ -8,69 +8,11 @@ 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
- None
Upgrading from a previous version
@@ -81,11 +23,11 @@ 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
$ curl https://gitlab.com/mayan-edms/mayan-edms/raw/master/removals.txt | pip uninstall -r /dev/stdin
Type in the console::
/opt/mayan-edms/bin/pip install mayan-edms==3.3
$ pip install mayan-edms==3.3
the requirements will also be updated automatically.
@@ -95,19 +37,19 @@ Using Git
If you installed Mayan EDMS by cloning the Git repository issue the commands::
git reset --hard HEAD
git pull
$ 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
$ pip uninstall -y -r removals.txt
Next upgrade/add the new requirements::
pip install --upgrade -r requirements.txt
$ pip install --upgrade -r requirements.txt
Common steps
@@ -124,8 +66,9 @@ 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 \
sudo MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf
Edit the supervisord configuration file and update any setting the template
@@ -135,11 +78,11 @@ generator missed::
Migrate existing database schema with::
sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media /opt/mayan-edms/bin/mayan-edms.py performupgrade
$ 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
$ mayan-edms.py preparestatic --noinput
The upgrade procedure is now complete.
@@ -147,20 +90,12 @@ 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
- None
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
- :gitlab-issue:`XX`
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/

View File

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

View File

@@ -1,9 +1,9 @@
from __future__ import unicode_literals
__title__ = 'Mayan EDMS'
__version__ = '3.2.6'
__build__ = 0x030206
__build_string__ = 'v3.2.6_Wed Jul 10 03:18:15 2019 -0400'
__version__ = '3.2.5'
__build__ = 0x030205
__build_string__ = 'v3.2.5_Fri Jul 5 16:39:17 2019 -0400'
__django_version__ = '1.11'
__author__ = 'Roberto Rosario'
__author_email__ = 'roberto.rosario@mayan-edms.com'

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.smart_settings import Namespace
from .literals import DEFAULT_MAXIMUM_TITLE_LENGTH

View File

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

View File

@@ -41,17 +41,6 @@ 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();
@@ -171,7 +160,6 @@ class MayanApp {
this.setupFullHeightResizing();
this.setupItemsSelector();
this.setupNavbarCollapse();
MayanApp.setupNavBarState();
this.setupNewWindowAnchor();
$.each(this.ajaxMenusOptions, function(index, value) {
value.app = self;

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.smart_settings import Namespace
from .literals import DEFAULT_LOGIN_METHOD, DEFAULT_MAXIMUM_SESSION_LENGTH

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.smart_settings import Namespace
from .literals import DEFAULT_EMAIL, DEFAULT_PASSWORD, DEFAULT_USERNAME

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,19 +4,13 @@ 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, document=None, user=None):
if not document:
document = self.test_document
def _check_out_test_document(self, user=None):
if not user:
user = self._test_case_user
@@ -25,61 +19,7 @@ class DocumentCheckoutTestMixin(object):
)
self.test_check_out = DocumentCheckout.objects.check_out_document(
block_new_version=True, document=document,
block_new_version=True, document=self.test_document,
expiration_datetime=self._check_out_expiration_datetime,
user=user
)
class DocumentCheckoutViewTestMixin(object):
def _request_test_document_check_in_get_view(self):
return self.get(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
def _request_test_document_check_in_post_view(self):
return self.post(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
def _request_test_document_multiple_check_in_post_view(self):
return self.post(
viewname='checkouts:check_in_document_multiple', data={
'id_list': as_id_list(items=self.test_documents)
}
)
def _request_test_document_check_out_view(self):
return self.post(
viewname='checkouts:check_out_document', kwargs={
'pk': self.test_document.pk
}, data={
'block_new_version': True,
'expiration_datetime_0': TIME_DELTA_UNIT_DAYS,
'expiration_datetime_1': 2
}
)
def _request_test_document_multiple_check_out_post_view(self):
return self.post(
viewname='checkouts:check_out_document_multiple', data={
'block_new_version': True,
'expiration_datetime_0': TIME_DELTA_UNIT_DAYS,
'expiration_datetime_1': 2,
'id_list': as_id_list(items=self.test_documents)
}
)
def _request_test_document_check_out_detail_view(self):
return self.get(
viewname='checkouts:check_out_info', kwargs={
'pk': self.test_document.pk
}
)
def _request_test_document_check_out_list_view(self):
return self.get(viewname='checkouts:check_out_list')

View File

@@ -65,7 +65,7 @@ class CheckoutsAPITestCase(DocumentCheckoutTestMixin, DocumentTestMixin, BaseAPI
force_text(self.test_document.uuid)
)
def _request_test_document_check_out_view(self):
def _request_document_checkout_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_test_document_check_out_view()
response = self._request_document_checkout_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_test_document_check_out_view()
response = self._request_document_checkout_view()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(

View File

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

View File

@@ -1,5 +1,6 @@
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
@@ -11,53 +12,64 @@ from ..permissions import (
permission_document_check_out, permission_document_check_out_detail_view
)
from .mixins import DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin
from .mixins import DocumentCheckoutTestMixin
class DocumentCheckoutViewTestCase(
DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin,
GenericDocumentViewTestCase
):
def test_document_check_in_get_view_no_permission(self):
self._check_out_test_document()
response = self._request_test_document_check_in_get_view()
self.assertNotContains(
response=response, text=self.test_document.label, status_code=404
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
}
)
self.assertTrue(self.test_document.is_checked_out())
def test_document_check_in_get_view_with_access(self):
def test_check_in_document_get_view_no_permission(self):
self._check_out_test_document()
self.grant_access(
obj=self.test_document, permission=permission_document_check_in
)
response = self._request_test_document_check_in_get_view()
response = self._request_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 test_document_check_in_post_view_no_permission(self):
self._check_out_test_document()
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_document_check_in_post_view_with_access(self):
def test_check_in_document_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_test_document_check_in_post_view()
response = self._request_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):
self._check_out_test_document()
response = self._request_document_check_in_post_view()
self.assertEqual(response.status_code, 403)
self.assertTrue(self.test_document.is_checked_out())
def test_check_in_document_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()
self.assertEqual(response.status_code, 302)
self.assertFalse(self.test_document.is_checked_out())
@@ -67,93 +79,24 @@ class DocumentCheckoutViewTestCase(
)
)
def test_document_multiple_check_in_post_view_no_permission(self):
# Upload second document
self.upload_document()
self._check_out_test_document(document=self.test_documents[0])
self._check_out_test_document(document=self.test_documents[1])
response = self._request_test_document_multiple_check_in_post_view()
self.assertEqual(response.status_code, 404)
self.assertTrue(self.test_documents[0].is_checked_out())
self.assertTrue(self.test_documents[1].is_checked_out())
self.assertTrue(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[0]
)
)
self.assertTrue(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[1]
)
def _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_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)
def test_check_out_document_view_no_permission(self):
response = self._request_document_checkout_view()
self.assertEqual(response.status_code, 403)
self.assertFalse(self.test_document.is_checked_out())
def test_document_check_out_view_with_access(self):
def test_check_out_document_view_with_access(self):
self.grant_access(
obj=self.test_document, permission=permission_document_check_out
)
@@ -162,117 +105,28 @@ class DocumentCheckoutViewTestCase(
permission=permission_document_check_out_detail_view
)
response = self._request_test_document_check_out_view()
response = self._request_document_checkout_view()
self.assertEqual(response.status_code, 302)
self.assertTrue(self.test_document.is_checked_out())
def test_document_multiple_check_out_post_view_no_permission(self):
# Upload second document
self.upload_document()
self.grant_access(
obj=self.test_documents[0],
permission=permission_document_check_out_detail_view
)
self.grant_access(
obj=self.test_documents[1],
permission=permission_document_check_out_detail_view
def _request_check_out_detail_view(self):
return self.get(
viewname='checkouts:check_out_info', kwargs={
'pk': self.test_document.pk
}
)
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):
def test_checkout_detail_view_no_permission(self):
self._check_out_test_document()
response = self._request_test_document_check_out_detail_view()
response = self._request_check_out_detail_view()
self.assertNotContains(
response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=404
)
def test_document_check_out_detail_view_with_access(self):
def test_checkout_detail_view_with_access(self):
self._check_out_test_document()
self.grant_access(
@@ -280,12 +134,15 @@ class DocumentCheckoutViewTestCase(
permission=permission_document_check_out_detail_view
)
response = self._request_test_document_check_out_detail_view()
response = self._request_check_out_detail_view()
self.assertContains(
response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=200
)
def test_document_checkout_list_view_no_permission(self):
def _request_check_out_list_view(self):
return self.get(viewname='checkouts:check_out_list')
def test_checkout_list_view_no_permission(self):
self._check_out_test_document()
self.grant_access(
@@ -293,12 +150,12 @@ class DocumentCheckoutViewTestCase(
permission=permission_document_view
)
response = self._request_test_document_check_out_list_view()
response = self._request_check_out_list_view()
self.assertNotContains(
response=response, text=self.test_document.label, status_code=200
)
def test_document_checkout_list_view_with_access(self):
def test_checkout_list_view_with_access(self):
self._check_out_test_document()
self.grant_access(
@@ -310,12 +167,12 @@ class DocumentCheckoutViewTestCase(
permission=permission_document_view
)
response = self._request_test_document_check_out_list_view()
response = self._request_check_out_list_view()
self.assertContains(
response=response, text=self.test_document.label, status_code=200
)
def test_document_check_out_new_version(self):
def test_document_new_version_after_check_out(self):
"""
Gitlab issue #231
User shown option to upload new version of a document even though it
@@ -352,13 +209,13 @@ class DocumentCheckoutViewTestCase(
self.assertEqual(resolved_link, None)
def test_document_check_in_forcefull_view_no_permission(self):
def test_forcefull_check_in_document_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_user()
self._check_out_test_document(user=self.test_user)
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
@@ -369,16 +226,21 @@ class DocumentCheckoutViewTestCase(
'pk': self.test_document.pk
}
)
self.assertEqual(response.status_code, 302)
self.assertContains(
response=response, text='Insufficient permissions', status_code=403
)
self.assertTrue(self.test_document.is_checked_out())
def test_document_check_in_forcefull_view_with_access(self):
self._create_test_user()
self._check_out_test_document(user=self.test_user)
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_override
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(
@@ -387,4 +249,5 @@ class DocumentCheckoutViewTestCase(
}
)
self.assertEqual(response.status_code, 302)
self.assertFalse(self.test_document.is_checked_out())

View File

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

View File

@@ -1,16 +1,20 @@
from __future__ import absolute_import, unicode_literals
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _, ungettext
from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.generics import (
MultipleObjectConfirmActionView, MultipleObjectFormActionView,
SingleObjectDetailView
ConfirmView, SingleObjectCreateView, SingleObjectDetailView
)
from mayan.apps.common.utils import encapsulate
from mayan.apps.documents.models import Document
from mayan.apps.documents.views import DocumentListView
from .exceptions import DocumentAlreadyCheckedOut, DocumentNotCheckedOut
from .forms import DocumentCheckoutForm, DocumentCheckoutDefailForm
from .icons import icon_check_out_info
from .models import DocumentCheckout
@@ -20,124 +24,159 @@ from .permissions import (
)
class DocumentCheckinView(MultipleObjectConfirmActionView):
error_message = 'Unable to check in document "%(instance)s". %(exception)s'
model = Document
pk_url_kwarg = 'pk'
success_message_singular = '%(count)d document checked in.'
success_message_plural = '%(count)d documents checked in.'
class DocumentCheckinView(ConfirmView):
def get_extra_context(self):
queryset = self.get_object_list()
document = self.get_object()
result = {
'title': ungettext(
singular='Check in %(count)d document',
plural='Check in %(count)d documents',
number=queryset.count()
) % {
'count': queryset.count(),
}
context = {
'object': document,
}
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Check in document: %s'
) % queryset.first()
}
)
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 result
return context
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]}
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:
super(DocumentCheckinView, self).get_post_action_redirect()
AccessControlList.objects.check_access(
obj=document,
permissions=(permission_document_check_in_override,),
user=self.request.user
)
def get_source_queryset(self):
# object_permission is None to disable restricting queryset mixin
# and restrict the queryset ourselves from two permissions
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
)
source_queryset = super(DocumentCheckinView, self).get_source_queryset()
check_in_queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_check_in, queryset=source_queryset,
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())
def get_extra_context(self):
return {
'object': self.document,
'title': _('Check out document: %s') % self.document
}
def get_post_action_redirect(self):
return reverse(
viewname='checkouts:check_out_info', kwargs={
'pk': self.document.pk
}
)
class CheckoutListView(DocumentListView):
def get_document_queryset(self):
return AccessControlList.objects.restrict_queryset(
permission=permission_document_check_out_detail_view,
queryset=DocumentCheckout.objects.checked_out_documents(),
user=self.request.user
)
check_in_override_queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_check_in_override,
queryset=source_queryset, user=self.request.user
)
return check_in_queryset | check_in_override_queryset
def object_action(self, form, instance):
DocumentCheckout.business_logic.check_in_document(
document=instance, user=self.request.user
)
class DocumentCheckoutView(MultipleObjectFormActionView):
error_message = 'Unable to checkout document "%(instance)s". %(exception)s'
form_class = DocumentCheckoutForm
model = Document
object_permission = permission_document_check_out
pk_url_kwarg = 'pk'
success_message_singular = '%(count)d document checked out.'
success_message_plural = '%(count)d documents checked out.'
def get_extra_context(self):
queryset = self.get_object_list()
result = {
'title': ungettext(
singular='Checkout %(count)d document',
plural='Checkout %(count)d documents',
number=queryset.count()
) % {
'count': queryset.count(),
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'),
}
}
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,
)
return context
class DocumentCheckoutDetailView(SingleObjectDetailView):
class CheckoutDetailView(SingleObjectDetailView):
form_class = DocumentCheckoutDefailForm
model = Document
object_permission = permission_document_check_out_detail_view
@@ -149,27 +188,3 @@ class DocumentCheckoutDetailView(SingleObjectDetailView):
'Check out details for document: %s'
) % self.object
}
class DocumentCheckoutListView(DocumentListView):
def get_document_queryset(self):
return AccessControlList.objects.restrict_queryset(
permission=permission_document_check_out_detail_view,
queryset=DocumentCheckout.objects.checked_out_documents(),
user=self.request.user
)
def get_extra_context(self):
context = super(DocumentCheckoutListView, self).get_extra_context()
context.update(
{
'no_results_icon': icon_check_out_info,
'no_results_text': _(
'Checking out a document, blocks certain operations '
'for a predetermined amount of time.'
),
'no_results_title': _('No documents have been checked out'),
'title': _('Checked out documents'),
}
)
return context

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -6,10 +6,11 @@ from django.conf import settings
from django.utils.translation import ugettext_lazy as _
import mayan
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.smart_settings import Namespace
from .literals import DEFAULT_COMMON_HOME_VIEW
namespace = Namespace(label=_('Common'), name='common')
setting_auto_logging = namespace.add_setting(
@@ -94,5 +95,322 @@ setting_shared_storage = namespace.add_setting(
)
setting_shared_storage_arguments = namespace.add_setting(
global_name='COMMON_SHARED_STORAGE_ARGUMENTS',
default={'location': os.path.join(settings.MEDIA_ROOT, 'shared_files')}
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'
)
)

View File

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

View File

@@ -7,7 +7,6 @@ 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,
@@ -22,7 +21,7 @@ class BaseTestCase(
SilenceLoggerTestCaseMixin, ConnectionsCheckTestCaseMixin,
RandomPrimaryKeyModelMonkeyPatchMixin, ACLTestCaseMixin,
ModelTestCaseMixin, OpenFileCheckTestCaseMixin,
TempfileCheckTestCasekMixin, UserTestMixin, TestCase
TempfileCheckTestCasekMixin, TestCase
):
"""
This is the most basic test case class any test in the project should use.

View File

@@ -1,10 +1,6 @@
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):
@@ -17,9 +13,3 @@ def mute_stdout():
sys.stdout = NullFile()
yield
sys.stdout = stdout_old
def as_id_list(items):
return ','.join(
[force_text(item.pk) for item in items]
)

View File

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

View File

@@ -7,6 +7,11 @@ 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 _
@@ -15,14 +20,16 @@ from mayan.apps.storage.utils import NamedTemporaryFile
from ..classes import ConverterBase
from ..exceptions import PageCountError
from ..settings import setting_graphics_backend_arguments
from ..settings import setting_graphics_backend_config
from ..literals import (
DEFAULT_PDFTOPPM_DPI, DEFAULT_PDFTOPPM_FORMAT, DEFAULT_PDFTOPPM_PATH,
DEFAULT_PDFINFO_PATH
)
pdftoppm_path = setting_graphics_backend_arguments.value.get(
pdftoppm_path = yaml.load(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
).get(
'pdftoppm_path', DEFAULT_PDFTOPPM_PATH
)
@@ -32,20 +39,26 @@ except sh.CommandNotFound:
pdftoppm = None
else:
pdftoppm_format = '-{}'.format(
setting_graphics_backend_arguments.value.get(
yaml.load(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
).get(
'pdftoppm_format', DEFAULT_PDFTOPPM_FORMAT
)
)
pdftoppm_dpi = format(
setting_graphics_backend_arguments.value.get(
yaml.load(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
).get(
'pdftoppm_dpi', DEFAULT_PDFTOPPM_DPI
)
)
pdftoppm = pdftoppm.bake(pdftoppm_format, '-r', pdftoppm_dpi)
pdfinfo_path = setting_graphics_backend_arguments.value.get(
pdfinfo_path = yaml.load(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
).get(
'pdfinfo_path', DEFAULT_PDFINFO_PATH
)

View File

@@ -7,10 +7,15 @@ 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 (
@@ -22,13 +27,15 @@ from .literals import (
CONVERTER_OFFICE_FILE_MIMETYPES, DEFAULT_LIBREOFFICE_PATH,
DEFAULT_PAGE_NUMBER, DEFAULT_PILLOW_FORMAT
)
from .settings import setting_graphics_backend_arguments
libreoffice_path = setting_graphics_backend_arguments.value.get(
'libreoffice_path', DEFAULT_LIBREOFFICE_PATH
)
from .settings import setting_graphics_backend_config
logger = logging.getLogger(__name__)
BACKEND_CONFIG = yaml.load(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
)
libreoffice_path = BACKEND_CONFIG.get(
'libreoffice_path', DEFAULT_LIBREOFFICE_PATH
)
class ConverterBase(object):
@@ -55,7 +62,9 @@ class ConverterBase(object):
pass
def get_page(self, output_format=None):
output_format = output_format or setting_graphics_backend_arguments.value.get(
output_format = output_format or yaml.load(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
).get(
'pillow_format', DEFAULT_PILLOW_FORMAT
)

View File

@@ -2,12 +2,15 @@ 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
@@ -18,7 +21,7 @@ class TransformationForm(forms.ModelForm):
def clean(self):
try:
yaml_load(stream=self.cleaned_data['arguments'])
yaml.load(stream=self.cleaned_data['arguments'], Loader=SafeLoader)
except yaml.YAMLError:
raise ValidationError(
_(

View File

@@ -2,11 +2,16 @@ 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__)
@@ -18,8 +23,8 @@ class TransformationManager(models.Manager):
self.create(
content_type=content_type, object_id=obj.pk,
name=transformation.name, arguments=yaml_dump(
data=arguments
name=transformation.name, arguments=yaml.dump(
data=arguments, Dumper=SafeDumper
)
)
@@ -91,8 +96,9 @@ class TransformationManager(models.Manager):
# Some transformations don't require arguments
# return an empty dictionary as ** doesn't allow None
if transformation.arguments:
kwargs = yaml_load(
kwargs = yaml.load(
stream=transformation.arguments,
Loader=SafeLoader
)
else:
kwargs = {}

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.smart_settings import Namespace
from .literals import (
DEFAULT_LIBREOFFICE_PATH, DEFAULT_PDFTOPPM_DPI, DEFAULT_PDFTOPPM_FORMAT,
@@ -16,15 +16,22 @@ setting_graphics_backend = namespace.add_setting(
help_text=_('Graphics conversion backend to use.'),
global_name='CONVERTER_GRAPHICS_BACKEND',
)
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=_(
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=_(
'Configuration options for the graphics conversion backend.'
), global_name='CONVERTER_GRAPHICS_BACKEND_ARGUMENTS'
), global_name='CONVERTER_GRAPHICS_BACKEND_CONFIG', quoted=True
)

View File

@@ -2,12 +2,15 @@ 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):
@@ -17,7 +20,7 @@ class YAMLValidator(object):
def __call__(self, value):
value = value.strip()
try:
yaml_load(stream=value)
yaml.load(stream=value, Loader=SafeLoader)
except yaml.error.YAMLError:
raise ValidationError(
_('Enter a valid YAML value.'),

View File

@@ -5,7 +5,7 @@ import os
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.smart_settings import Namespace
namespace = Namespace(label=_('Signatures'), name='django_gpg')

View File

@@ -101,24 +101,22 @@ class DocumentIndexingApp(MayanAppConfig):
model=IndexInstanceNode, related='index_template_node__index'
)
column_index_label = SourceColumn(
attribute='label', is_identifier=True, is_sortable=True,
source=Index
SourceColumn(
attribute='label', exclude=(IndexInstance,), 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(
attribute='slug', exclude=(IndexInstance,), is_sortable=True,
source=Index
)
column_index_slug.add_exclude(IndexInstance)
column_index_enabled = SourceColumn(
attribute='enabled', is_sortable=True, source=Index,
widget=TwoStateWidget
SourceColumn(
attribute='enabled', exclude=(IndexInstance,), is_sortable=True,
source=Index, widget=TwoStateWidget
)
column_index_enabled.add_exclude(source=IndexInstance)
SourceColumn(
func=lambda context: context[

View File

@@ -17,11 +17,7 @@ from .literals import (
from .mixins import IndexTestMixin, IndexViewTestMixin
class IndexViewTestCase(
IndexTestMixin, IndexViewTestMixin, GenericDocumentViewTestCase
):
auto_upload_document = False
class IndexViewTestCase(IndexTestMixin, IndexViewTestMixin, GenericDocumentViewTestCase):
def test_index_create_view_no_permission(self):
response = self._request_test_index_create_view()
self.assertEqual(response.status_code, 403)
@@ -79,45 +75,6 @@ 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
):
def _request_index_instance_node_view(self, index_instance_node):
return self.get(
viewname='indexing:index_instance_node_view', kwargs={
@@ -146,13 +103,9 @@ 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):
@@ -196,3 +149,36 @@ class IndexToolsViewTestCase(
# An instance root exists
self.assertTrue(self.test_index.instance_root.pk)
def test_index_rebuild_view_no_permission(self):
self._create_test_index()
self.test_index.node_templates.create(
parent=self.test_index.template_root,
expression=TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION,
link_documents=True
)
response = self._request_test_index_rebuild_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(IndexInstanceNode.objects.count(), 0)
def test_index_rebuild_view_with_access(self):
self._create_test_index()
self.test_index.node_templates.create(
parent=self.test_index.template_root,
expression=TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION,
link_documents=True
)
self.grant_access(
obj=self.test_index,
permission=permission_document_indexing_rebuild
)
response = self._request_test_index_rebuild_view()
self.assertEqual(response.status_code, 302)
self.assertNotEqual(IndexInstanceNode.objects.count(), 0)

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.smart_settings import Namespace
namespace = Namespace(label=_('Document parsing'), name='document_parsing')

View File

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

View File

@@ -5,7 +5,7 @@ import os
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.smart_settings import Namespace
namespace = Namespace(label=_('Document signatures'), name='signatures')
@@ -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': os.path.join(settings.MEDIA_ROOT, 'document_signatures')
}, help_text=_(
default='{{location: {}}}'.format(
os.path.join(settings.MEDIA_ROOT, 'document_signatures')
), quoted=True, help_text=_(
'Arguments to pass to the SIGNATURE_STORAGE_BACKEND. '
)
)

View File

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

View File

@@ -1,8 +1,6 @@
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
@@ -12,7 +10,6 @@ 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,
@@ -26,12 +23,8 @@ 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 APIDocumentTypeWorkflowRuntimeProxyListView(generics.ListAPIView):
class APIDocumentTypeWorkflowListView(generics.ListAPIView):
"""
get: Returns a list of all the document type workflows.
"""
@@ -179,42 +172,7 @@ class APIWorkflowDocumentTypeView(generics.RetrieveDestroyAPIView):
self.get_workflow().document_types.remove(instance)
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):
class APIWorkflowListView(generics.ListCreateAPIView):
"""
get: Returns a list of all the workflows.
post: Create a new workflow.
@@ -229,7 +187,7 @@ class APIWorkflowRuntimeProxyListView(generics.ListCreateAPIView):
if not self.request:
return None
return super(APIWorkflowRuntimeProxyListView, self).get_serializer(*args, **kwargs)
return super(APIWorkflowListView, self).get_serializer(*args, **kwargs)
def get_serializer_class(self):
if self.request.method == 'GET':

View File

@@ -27,29 +27,29 @@ 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 .html_widgets import widget_transition_events, WorkflowLogExtraDataWidget
from .links import (
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_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_setup_workflow_transition_field_create,
link_setup_workflow_transition_field_delete,
link_setup_workflow_transition_field_edit,
link_setup_workflow_transition_field_list,
link_tool_launch_all_workflows, link_workflow_instance_detail,
link_workflow_instance_transition, link_workflow_runtime_proxy_document_list,
link_workflow_runtime_proxy_list, link_workflow_template_preview,
link_workflow_runtime_proxy_list, link_workflow_preview,
link_workflow_runtime_proxy_state_document_list, link_workflow_runtime_proxy_state_list,
link_workflow_template_transition_events
link_workflow_transition_events
)
from .permissions import (
permission_workflow_delete, permission_workflow_edit,
@@ -168,10 +168,10 @@ class DocumentStatesApp(MayanAppConfig):
SourceColumn(
attribute='label', is_sortable=True, source=Workflow
)
column_workflow_internal_name = SourceColumn(
attribute='internal_name', is_sortable=True, source=Workflow
SourceColumn(
attribute='internal_name', exclude=(WorkflowRuntimeProxy,),
is_sortable=True, source=Workflow
)
column_workflow_internal_name.add_exclude(source=WorkflowRuntimeProxy)
SourceColumn(
attribute='get_initial_state', empty_value=_('None'),
source=Workflow
@@ -206,10 +206,10 @@ class DocumentStatesApp(MayanAppConfig):
SourceColumn(
source=WorkflowInstanceLogEntry, label=_('Date and time'),
attribute='datetime'
attribute='datetime', is_sortable=True
)
SourceColumn(
source=WorkflowInstanceLogEntry, label=_('User'), attribute='user'
source=WorkflowInstanceLogEntry, attribute='user', is_sortable=True
)
SourceColumn(
source=WorkflowInstanceLogEntry,
@@ -279,8 +279,6 @@ class DocumentStatesApp(MayanAppConfig):
)
SourceColumn(
<<<<<<< HEAD
=======
attribute='name', is_identifier=True, is_sortable=True,
source=WorkflowTransitionField
)
@@ -305,7 +303,6 @@ class DocumentStatesApp(MayanAppConfig):
)
SourceColumn(
>>>>>>> versions/minor
source=WorkflowRuntimeProxy, label=_('Documents'),
func=lambda context: context['object'].get_document_count(
user=context['request'].user
@@ -319,49 +316,50 @@ class DocumentStatesApp(MayanAppConfig):
)
menu_facet.bind_links(
links=(link_workflow_instance_list,), sources=(Document,)
links=(link_document_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_workflow_template_document_types,
link_workflow_template_state_list, link_workflow_template_transition_list,
link_workflow_template_preview
link_setup_workflow_document_types,
link_setup_workflow_states, link_setup_workflow_transitions,
link_workflow_preview
), sources=(Workflow,)
)
menu_list_facet.bind_links(
links=(
link_document_type_workflow_templates,
link_setup_document_type_workflows,
), sources=(DocumentType,)
)
menu_main.bind_links(links=(link_workflow_runtime_proxy_list,), position=10)
menu_object.bind_links(
links=(
link_workflow_template_delete, link_workflow_template_edit
link_setup_workflow_delete, link_setup_workflow_edit
), sources=(Workflow,)
)
menu_object.bind_links(
links=(
link_workflow_template_state_edit,
link_workflow_template_state_action_list,
link_workflow_template_state_delete
link_setup_workflow_state_edit,
link_setup_workflow_state_action_list,
link_setup_workflow_state_delete
), sources=(WorkflowState,)
)
menu_object.bind_links(
links=(
link_workflow_template_transition_edit,
link_workflow_template_transition_events,
link_workflow_template_transition_field_list, link_acl_list,
link_workflow_template_transition_delete
link_setup_workflow_transition_edit,
link_workflow_transition_events,
link_setup_workflow_transition_field_list,
link_acl_list,
link_setup_workflow_transition_delete
), sources=(WorkflowTransition,)
)
menu_object.bind_links(
links=(
link_workflow_template_transition_field_delete,
link_workflow_template_transition_field_edit
link_setup_workflow_transition_field_delete,
link_setup_workflow_transition_field_edit
), sources=(WorkflowTransitionField,)
)
menu_object.bind_links(
@@ -384,21 +382,21 @@ class DocumentStatesApp(MayanAppConfig):
)
menu_object.bind_links(
links=(
link_workflow_template_state_action_edit,
link_setup_workflow_state_action_edit,
link_object_error_list,
link_workflow_template_state_action_delete,
link_setup_workflow_state_action_delete,
), sources=(WorkflowStateAction,)
)
menu_secondary.bind_links(
links=(link_workflow_template_list, link_workflow_template_create),
links=(link_setup_workflow_list, link_setup_workflow_create),
sources=(
Workflow, 'document_states:workflow_template_create',
'document_states:workflow_template_list'
Workflow, 'document_states:setup_workflow_create',
'document_states:setup_workflow_list'
)
)
menu_secondary.bind_links(
links=(link_workflow_template_transition_field_create,),
links=(link_setup_workflow_transition_field_create,),
sources=(
WorkflowTransition,
)
@@ -410,31 +408,31 @@ class DocumentStatesApp(MayanAppConfig):
)
)
menu_secondary.bind_links(
links=(link_workflow_template_state_action_selection,),
links=(link_setup_workflow_state_action_selection,),
sources=(
WorkflowState,
)
)
menu_secondary.bind_links(
links=(
link_workflow_template_transition_create,
link_setup_workflow_transition_create,
), sources=(
WorkflowTransition,
'document_states:workflow_template_transition_list',
'document_states:setup_workflow_transition_list',
)
)
menu_secondary.bind_links(
links=(
link_workflow_template_state_create,
link_setup_workflow_state_create,
), sources=(
WorkflowState,
'document_states:workflow_template_state_list',
'document_states:setup_workflow_state_list',
)
)
menu_setup.bind_links(links=(link_workflow_template_list,))
menu_setup.bind_links(links=(link_setup_workflow_list,))
menu_tools.bind_links(links=(link_tool_launch_workflows,))
menu_tools.bind_links(links=(link_tool_launch_all_workflows,))
post_save.connect(
dispatch_uid='workflows_handler_launch_workflow',

View File

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

View File

@@ -12,10 +12,10 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.forms import DynamicModelForm
from .classes import WorkflowAction
from .fields import WorfklowImageField
from .models import (
Workflow, WorkflowState, WorkflowStateAction, WorkflowTransition
)
from .widgets import WorkflowImageWidget
class WorkflowActionSelectionForm(forms.Form):
@@ -181,9 +181,9 @@ class WorkflowInstanceTransitionSelectForm(forms.Form):
class WorkflowPreviewForm(forms.Form):
workflow = WorfklowImageField()
preview = forms.CharField(widget=WorkflowImageWidget())
def __init__(self, *args, **kwargs):
instance = kwargs.pop('instance', None)
super(WorkflowPreviewForm, self).__init__(*args, **kwargs)
self.fields['workflow'].initial = instance
self.fields['preview'].initial = instance

View File

@@ -1,7 +1,9 @@
from __future__ import unicode_literals
from django import forms
from django.template.loader import render_to_string
from django.utils.html import format_html_join
from django.urls import reverse
from django.utils.html import format_html_join, mark_safe
def widget_transition_events(transition):
@@ -14,6 +16,18 @@ def widget_transition_events(transition):
)
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 WorkflowLogExtraDataWidget(object):
template_name = 'document_states/extra_data.html'

View File

@@ -1,44 +1,46 @@
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
from mayan.apps.documents.icons import icon_document_type
icon_workflow = Icon(driver_name='fontawesome', symbol='sitemap')
icon_tool_launch_workflows = icon_workflow
icon_document_type_workflow_list = icon_workflow
icon_workflow_template_create = Icon(
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(
driver_name='fontawesome-dual', primary_symbol='sitemap',
secondary_symbol='plus'
)
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_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_instance_detail = Icon(
driver_name='fontawesome', symbol='sitemap'
)
icon_workflow_template_list = icon_workflow
icon_workflow_template_preview = Icon(driver_name='fontawesome', symbol='eye')
# Workflow instances
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_workflow
icon_workflow_runtime_proxy_state_document_list = icon_document
icon_workflow_runtime_proxy_document_list = icon_document_type
icon_workflow_runtime_proxy_list = Icon(
driver_name='fontawesome', symbol='sitemap'
)
icon_workflow_runtime_proxy_state_document_list = icon_document_type
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'
)
@@ -56,8 +58,6 @@ 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'
@@ -69,12 +69,7 @@ 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'
)
# Workflow transitions
icon_workflow_state_action_list = Icon(driver_name='fontawesome', symbol='code')
icon_workflow_transition = Icon(
driver_name='fontawesome', symbol='arrows-alt-h'
)
@@ -89,11 +84,7 @@ 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 = Icon(driver_name='fontawesome', symbol='table')
icon_workflow_transition_field_delete = Icon(
driver_name='fontawesome', symbol='times'
)
@@ -107,6 +98,7 @@ icon_workflow_transition_field_create = Icon(
icon_workflow_transition_field_list = Icon(
driver_name='fontawesome', symbol='table'
)
icon_workflow_transition_triggers = Icon(
driver_name='fontawesome', symbol='bolt'
)

View File

@@ -11,185 +11,179 @@ from .permissions import (
permission_workflow_view,
)
# Workflow templates
link_document_type_workflow_templates = Link(
link_setup_document_type_workflows = 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_workflow_templates',
view='document_states:document_type_workflows',
)
link_workflow_template_create = Link(
icon_class_path='mayan.apps.document_states.icons.icon_workflow_template_create',
link_setup_workflow_create = Link(
icon_class_path='mayan.apps.document_states.icons.icon_workflow_create',
permissions=(permission_workflow_create,),
text=_('Create workflow'), view='document_states:workflow_template_create'
text=_('Create workflow'), view='document_states:setup_workflow_create'
)
link_workflow_template_delete = Link(
link_setup_workflow_delete = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_template_delete',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_delete',
permissions=(permission_workflow_delete,),
tags='dangerous', text=_('Delete'),
view='document_states:workflow_template_delete',
view='document_states:setup_workflow_delete',
)
link_workflow_template_document_types = Link(
link_setup_workflow_document_types = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_template_document_type_list',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_document_type_list',
permissions=(permission_workflow_edit,), text=_('Document types'),
view='document_states:workflow_template_document_types',
view='document_states:setup_workflow_document_types',
)
link_workflow_template_edit = Link(
link_setup_workflow_edit = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_template_edit',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_edit',
permissions=(permission_workflow_edit,),
text=_('Edit'), view='document_states:workflow_template_edit',
text=_('Edit'), view='document_states:setup_workflow_edit',
)
link_workflow_template_list = Link(
icon_class_path='mayan.apps.document_states.icons.icon_workflow_template_list',
link_setup_workflow_list = Link(
icon_class_path='mayan.apps.document_states.icons.icon_setup_workflow_list',
permissions=(permission_workflow_view,), text=_('Workflows'),
view='document_states:workflow_template_list'
view='document_states:setup_workflow_list'
)
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(
link_setup_workflow_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:workflow_template_state_action_delete',
view='document_states:setup_workflow_state_action_delete',
)
link_workflow_template_state_action_edit = Link(
link_setup_workflow_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:workflow_template_state_action_edit',
text=_('Edit'), view='document_states:setup_workflow_state_action_edit',
)
link_workflow_template_state_action_list = Link(
link_setup_workflow_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:workflow_template_state_action_list',
view='document_states:setup_workflow_state_action_list',
)
link_workflow_template_state_action_selection = Link(
link_setup_workflow_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:workflow_template_state_action_selection',
view='document_states:setup_workflow_state_action_selection',
)
# Workflow template states
link_workflow_template_state_create = Link(
link_setup_workflow_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:workflow_template_state_create',
view='document_states:setup_workflow_state_create',
)
link_workflow_template_state_delete = Link(
link_setup_workflow_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:workflow_template_state_delete',
view='document_states:setup_workflow_state_delete',
)
link_workflow_template_state_edit = Link(
link_setup_workflow_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:workflow_template_state_edit',
text=_('Edit'), view='document_states:setup_workflow_state_edit',
)
link_workflow_template_state_list = Link(
link_setup_workflow_states = 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:workflow_template_state_list',
view='document_states:setup_workflow_state_list',
)
# Workflow template transitions
link_workflow_template_transition_create = Link(
link_setup_workflow_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:workflow_template_transition_create',
view='document_states:setup_workflow_transition_create',
)
link_workflow_template_transition_delete = Link(
link_setup_workflow_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:workflow_template_transition_delete',
view='document_states:setup_workflow_transition_delete',
)
link_workflow_template_transition_edit = Link(
link_setup_workflow_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:workflow_template_transition_edit',
text=_('Edit'), view='document_states:setup_workflow_transition_edit',
)
link_workflow_template_transition_events = Link(
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(
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:workflow_template_transition_events'
)
link_workflow_template_transition_list = 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:workflow_template_transition_list',
view='document_states:setup_workflow_transition_events'
)
# Workflow transition fields
link_workflow_template_transition_field_create = Link(
link_setup_workflow_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',
view='document_states:setup_workflow_transition_field_create',
)
link_workflow_template_transition_field_delete = Link(
link_setup_workflow_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',
view='document_states:setup_workflow_transition_field_delete',
)
link_workflow_template_transition_field_edit = Link(
link_setup_workflow_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',
text=_('Edit'), view='document_states:setup_workflow_transition_field_edit',
)
link_workflow_template_transition_field_list = Link(
link_setup_workflow_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',
view='document_states:setup_workflow_transition_field_list',
)
link_workflow_preview = 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'
)
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'
)
# 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',
@@ -198,38 +192,28 @@ link_workflow_instance_transition = Link(
)
# 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_runtime_proxy_document_list',
view='document_states:workflow_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_runtime_proxy_list'
text=_('Workflows'), view='document_states:workflow_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_runtime_proxy_state_document_list',
view='document_states:workflow_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_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'
text=_('States'), view='document_states:workflow_state_list',
)

View File

@@ -30,4 +30,3 @@ WORKFLOW_ACTION_WHEN_CHOICES = (
(WORKFLOW_ACTION_ON_ENTRY, _('On entry')),
(WORKFLOW_ACTION_ON_EXIT, _('On exit')),
)
WORKFLOW_IMAGE_TASK_TIMEOUT = 60

View File

@@ -1,10 +1,8 @@
from __future__ import absolute_import, unicode_literals
import hashlib
import json
import logging
from furl import furl
from graphviz import Digraph
import yaml
try:
@@ -13,9 +11,7 @@ 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
@@ -37,7 +33,6 @@ from .literals import (
)
from .managers import WorkflowManager
from .permissions import permission_workflow_transition
from .storages import storage_workflowimagecache
logger = logging.getLogger(__name__)
@@ -74,49 +69,9 @@ 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)
@@ -307,8 +262,8 @@ class WorkflowState(models.Model):
def save(self, *args, **kwargs):
# Solve issue #557 "Break workflows with invalid input"
# without using a migration.
# Remove blank=True, remove this, and create a migration in the next
# minor version.
# TODO: Remove blank=True, remove this, and create a migration in the
# next minor version.
try:
self.completion = int(self.completion)

View File

@@ -3,21 +3,12 @@ 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_fast, worker_slow
from mayan.apps.task_manager.workers import worker_slow
queue_document_states = CeleryQueue(
label=_('Document states'), name='document_states', worker=worker_slow
name='document_states', label=_('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(
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'
dotted_path='mayan.apps.document_states.tasks.task_launch_all_workflows',
label=_('Launch all workflows')
)

View File

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

View File

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

View File

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

View File

@@ -5,4 +5,3 @@
{% endfor %}
</ul>
{% endif %}

View File

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

View File

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

View File

@@ -38,19 +38,19 @@ class WorkflowStateViewTestMixin(object):
data.update(extra_data)
return self.post(
viewname='document_states:workflow_template_state_create',
viewname='document_states:setup_workflow_state_create',
kwargs={'pk': self.test_workflow.pk}, data=data
)
def _request_test_workflow_state_delete_view(self):
return self.post(
viewname='document_states:workflow_template_state_delete',
viewname='document_states:setup_workflow_state_delete',
kwargs={'pk': self.test_workflow_state_1.pk}
)
def _request_test_workflow_state_edit_view(self):
return self.post(
viewname='document_states:workflow_template_state_edit',
viewname='document_states:setup_workflow_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:workflow_template_state_list',
viewname='document_states:setup_workflow_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:workflow_template_transition_create',
viewname='document_states:setup_workflow_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:workflow_template_transition_delete',
viewname='document_states:setup_workflow_transition_delete',
kwargs={'pk': self.test_workflow_transition.pk}
)
def _request_test_workflow_transition_edit_view(self):
return self.post(
viewname='document_states:workflow_template_transition_edit',
viewname='document_states:setup_workflow_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,7 +146,7 @@ class WorkflowTransitionViewTestMixin(object):
def _request_test_workflow_transition_list_view(self):
return self.get(
viewname='document_states:workflow_template_transition_list',
viewname='document_states:setup_workflow_transition_list',
kwargs={'pk': self.test_workflow.pk}
)
@@ -163,7 +163,7 @@ class WorkflowTransitionViewTestMixin(object):
class WorkflowViewTestMixin(object):
def _request_test_workflow_create_view(self):
return self.post(
viewname='document_states:workflow_template_create', data={
viewname='document_states:setup_workflow_create', data={
'label': TEST_WORKFLOW_LABEL,
'internal_name': TEST_WORKFLOW_INTERNAL_NAME,
}
@@ -171,14 +171,14 @@ class WorkflowViewTestMixin(object):
def _request_test_workflow_delete_view(self):
return self.post(
viewname='document_states:workflow_template_delete', kwargs={
viewname='document_states:setup_workflow_delete', kwargs={
'pk': self.test_workflow.pk
}
)
def _request_test_workflow_edit_view(self):
return self.post(
viewname='document_states:workflow_template_edit', kwargs={
viewname='document_states:setup_workflow_edit', kwargs={
'pk': self.test_workflow.pk,
}, data={
'label': TEST_WORKFLOW_LABEL_EDITED,
@@ -188,12 +188,12 @@ class WorkflowViewTestMixin(object):
def _request_test_workflow_list_view(self):
return self.get(
viewname='document_states:workflow_template_list',
viewname='document_states:setup_workflow_list',
)
def _request_test_workflow_template_preview_view(self):
def _request_test_workflow_preview_view(self):
return self.get(
viewname='document_states:workflow_template_preview', kwargs={
viewname='document_states:workflow_preview', kwargs={
'pk': self.test_workflow.pk,
}
)

View File

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

View File

@@ -3,6 +3,7 @@ from __future__ import unicode_literals
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.documents.tests import GenericDocumentViewTestCase
from ..literals import FIELD_TYPE_CHOICE_CHAR
from ..models import WorkflowTransition
from ..permissions import (
permission_workflow_edit, permission_workflow_view,
@@ -10,15 +11,17 @@ from ..permissions import (
)
from .literals import (
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
TEST_WORKFLOW_TRANSITION_LABEL, TEST_WORKFLOW_TRANSITION_LABEL_EDITED
)
from .mixins import (
WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin
)
TEST_WORKFLOW_TRANSITION_FIELD_NAME = 'test_workflow_transition_field'
TEST_WORKFLOW_TRANSITION_FIELD_LABEL = 'test workflow transition field'
TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT = 'test workflow transition field help test'
TEST_WORKFLOW_TRANSITION_FIELD_TYPE = FIELD_TYPE_CHOICE_CHAR
class WorkflowTransitionViewTestCase(
WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin,
@@ -212,7 +215,7 @@ class WorkflowTransitionEventViewTestCase(
):
def _request_test_workflow_transition_event_list_view(self):
return self.get(
viewname='document_states:workflow_template_transition_events',
viewname='document_states:setup_workflow_transition_events',
kwargs={'pk': self.test_workflow_transition.pk}
)
@@ -256,7 +259,7 @@ class WorkflowTransitionFieldViewTestCase(
def _request_test_workflow_transition_field_list_view(self):
return self.get(
viewname='document_states:workflow_template_transition_field_list',
viewname='document_states:setup_workflow_transition_field_list',
kwargs={'pk': self.test_workflow_transition.pk}
)
@@ -286,7 +289,7 @@ class WorkflowTransitionFieldViewTestCase(
def _request_workflow_transition_field_create_view(self):
return self.post(
viewname='document_states:workflow_template_transition_field_create',
viewname='document_states:setup_workflow_transition_field_create',
kwargs={'pk': self.test_workflow_transition.pk},
data={
'field_type': TEST_WORKFLOW_TRANSITION_FIELD_TYPE,
@@ -324,7 +327,7 @@ class WorkflowTransitionFieldViewTestCase(
def _request_workflow_transition_field_delete_view(self):
return self.post(
viewname='document_states:workflow_template_transition_field_delete',
viewname='document_states:setup_workflow_transition_field_delete',
kwargs={'pk': self.test_workflow_transition_field.pk},
)

View File

@@ -93,31 +93,31 @@ class WorkflowViewTestCase(
self.assertEqual(response.status_code, 200)
self.assertContains(response, text=self.test_workflow.label)
def test_workflow_template_preview_view_no_access(self):
def test_workflow_preview_view_no_access(self):
self._create_test_workflow()
response = self._request_test_workflow_template_preview_view()
response = self._request_test_workflow_preview_view()
self.assertEqual(response.status_code, 404)
self.assertTrue(self.test_workflow in Workflow.objects.all())
def test_workflow_template_preview_view_with_access(self):
def test_workflow_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_template_preview_view()
response = self._request_test_workflow_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_workflows',
viewname='document_states:tool_launch_all_workflows',
)
def test_tool_launch_workflows_view_no_permission(self):
def test_tool_launch_all_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_workflows_view_with_permission(self):
def test_tool_launch_all_workflows_view_with_permission(self):
self._create_test_workflow(add_document_type=True)
self._create_test_workflow_states()
self._create_test_workflow_transition()

View File

@@ -3,247 +3,229 @@ from __future__ import unicode_literals
from django.conf.urls import url
from .api_views import (
APIDocumentTypeWorkflowRuntimeProxyListView, APIWorkflowDocumentTypeList,
APIWorkflowDocumentTypeView, APIWorkflowImageView,
APIWorkflowInstanceListView, APIWorkflowInstanceView,
APIWorkflowInstanceLogEntryListView, APIWorkflowRuntimeProxyListView,
APIWorkflowStateListView, APIWorkflowStateView,
APIDocumentTypeWorkflowListView, APIWorkflowDocumentTypeList,
APIWorkflowDocumentTypeView, APIWorkflowInstanceListView,
APIWorkflowInstanceView, APIWorkflowInstanceLogEntryListView,
APIWorkflowListView, APIWorkflowStateListView, APIWorkflowStateView,
APIWorkflowTransitionListView, APIWorkflowTransitionView, APIWorkflowView
)
from .views.workflow_instance_views import (
WorkflowInstanceDetailView, WorkflowInstanceListView,
WorkflowInstanceTransitionSelectView,
WorkflowInstanceTransitionExecuteView
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, WorkflowInstanceTransitionExecuteView,
WorkflowInstanceTransitionSelectView, WorkflowListView,
WorkflowPreviewView, WorkflowStateDocumentListView, WorkflowStateListView,
)
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, SetupWorkflowTransitionFieldCreateView,
SetupWorkflowTransitionFieldDeleteView,
SetupWorkflowTransitionFieldEditView, SetupWorkflowTransitionFieldListView
)
urlpatterns_workflow_instances = [
urlpatterns_workflows = [
url(
regex=r'^documents/(?P<pk>\d+)/workflows/$',
view=WorkflowInstanceListView.as_view(),
name='workflow_instance_list'
regex=r'^setup/workflows/$', view=SetupWorkflowListView.as_view(),
name='setup_workflow_list'
),
url(
regex=r'^documents/workflows/(?P<pk>\d+)/$',
view=WorkflowInstanceDetailView.as_view(),
name='workflow_instance_detail'
regex=r'^setup/workflows/create/$', view=SetupWorkflowCreateView.as_view(),
name='setup_workflow_create'
),
url(
regex=r'^documents/workflows/(?P<pk>\d+)/transitions/select/$',
view=WorkflowInstanceTransitionSelectView.as_view(),
name='workflow_instance_transition_selection'
regex=r'^setup/workflows/(?P<pk>\d+)/delete/$',
view=SetupWorkflowDeleteView.as_view(), name='setup_workflow_delete'
),
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'
regex=r'^setup/workflows/(?P<pk>\d+)/edit/$',
view=SetupWorkflowEditView.as_view(), name='setup_workflow_edit'
),
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'
regex=r'^setup/document_types/(?P<pk>\d+)/workflows/$',
view=SetupDocumentTypeWorkflowsView.as_view(),
name='document_type_workflows'
),
]
urlpatterns_workflow_states = [
url(
regex=r'^workflow_templates/(?P<pk>\d+)/states/$',
view=WorkflowTemplateStateListView.as_view(),
name='workflow_template_state_list'
regex=r'^setup/workflow/(?P<pk>\d+)/states/$',
view=SetupWorkflowStateListView.as_view(),
name='setup_workflow_state_list'
),
url(
regex=r'^workflow_templates/(?P<pk>\d+)/states/create/$',
view=WorkflowTemplateStateCreateView.as_view(),
name='workflow_template_state_create'
regex=r'^setup/workflow/(?P<pk>\d+)/states/create/$',
view=SetupWorkflowStateCreateView.as_view(),
name='setup_workflow_state_create'
),
url(
regex=r'^workflow_templates/states/(?P<pk>\d+)/delete/$',
view=WorkflowTemplateStateDeleteView.as_view(),
name='workflow_template_state_delete'
regex=r'^setup/workflow/state/(?P<pk>\d+)/delete/$',
view=SetupWorkflowStateDeleteView.as_view(),
name='setup_workflow_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'
regex=r'^setup/workflow/state/(?P<pk>\d+)/edit/$',
view=SetupWorkflowStateEditView.as_view(),
name='setup_workflow_state_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'
regex=r'^setup/workflows/transitions/(?P<pk>\d+)/fields/create/$',
view=SetupWorkflowTransitionFieldCreateView.as_view(),
name='setup_workflow_transition_field_create'
),
url(
regex=r'^workflow_templates/transitions/(?P<pk>\d+)/fields/$',
view=WorkflowTemplateTransitionFieldListView.as_view(),
name='workflow_template_transition_field_list'
regex=r'^setup/workflows/transitions/(?P<pk>\d+)/fields/$',
view=SetupWorkflowTransitionFieldListView.as_view(),
name='setup_workflow_transition_field_list'
),
url(
regex=r'^workflow_templates/transitions/fields/(?P<pk>\d+)/delete/$',
view=WorkflowTemplateTransitionFieldDeleteView.as_view(),
name='workflow_template_transition_field_delete'
regex=r'^setup/workflows/transitions/fields/(?P<pk>\d+)/delete/$',
view=SetupWorkflowTransitionFieldDeleteView.as_view(),
name='setup_workflow_transition_field_delete'
),
url(
regex=r'^workflow_templates/transitions/fields/(?P<pk>\d+)/edit/$',
view=WorkflowTemplateTransitionFieldEditView.as_view(),
name='workflow_template_transition_field_edit'
regex=r'^setup/workflows/transitions/fields/(?P<pk>\d+)/edit/$',
view=SetupWorkflowTransitionFieldEditView.as_view(),
name='setup_workflow_transition_field_edit'
),
]
urlpatterns = [
url(
regex=r'^tools/workflows/launch/$',
view=ToolLaunchWorkflows.as_view(),
name='tool_launch_workflows'
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+)/transitions/select/$',
view=WorkflowInstanceTransitionSelectView.as_view(),
name='workflow_instance_transition_selection'
),
url(
regex=r'^document/workflows/(?P<workflow_instance_pk>\d+)/transitions/(?P<workflow_transition_pk>\d+)/execute/$',
view=WorkflowInstanceTransitionExecuteView.as_view(),
name='workflow_instance_transition_execute'
),
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+)/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/transitions/(?P<pk>\d+)/events/$',
view=SetupWorkflowTransitionTriggerEventListView.as_view(),
name='setup_workflow_transition_events'
),
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'
),
]
urlpatterns.extend(urlpatterns_workflow_instances)
urlpatterns.extend(urlpatterns_workflow_runtime_proxies)
urlpatterns.extend(urlpatterns_workflows)
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=APIWorkflowRuntimeProxyListView.as_view(),
regex=r'^workflows/$', view=APIWorkflowListView.as_view(),
name='workflow-list'
),
url(
@@ -260,10 +242,6 @@ 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'
@@ -299,7 +277,7 @@ api_urls = [
),
url(
regex=r'^document_types/(?P<pk>[0-9]+)/workflows/$',
view=APIDocumentTypeWorkflowRuntimeProxyListView.as_view(),
view=APIDocumentTypeWorkflowListView.as_view(),
name='documenttype-workflow-list'
),
]

View File

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

View File

@@ -14,14 +14,20 @@ from mayan.apps.common.mixins import ExternalObjectMixin
from mayan.apps.documents.models import Document
from ..forms import WorkflowInstanceTransitionSelectForm
from ..icons import icon_workflow_instance_detail, icon_workflow_template_list
from ..icons import icon_workflow_instance_detail, icon_workflow_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',
'WorkflowInstanceTransitionSelectView',
'WorkflowInstanceTransitionExecuteView'
)
class WorkflowInstanceListView(SingleObjectListView):
class DocumentWorkflowInstanceListView(SingleObjectListView):
def dispatch(self, request, *args, **kwargs):
AccessControlList.objects.check_access(
obj=self.get_document(), permissions=(permission_workflow_view,),
@@ -29,7 +35,7 @@ class WorkflowInstanceListView(SingleObjectListView):
)
return super(
WorkflowInstanceListView, self
DocumentWorkflowInstanceListView, self
).dispatch(request, *args, **kwargs)
def get_document(self):
@@ -38,7 +44,7 @@ class WorkflowInstanceListView(SingleObjectListView):
def get_extra_context(self):
return {
'hide_link': True,
'no_results_icon': icon_workflow_template_list,
'no_results_icon': icon_workflow_list,
'no_results_text': _(
'Assign workflows to the document type of this document '
'to have this document execute those workflows. '

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,37 @@
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):
template_name = 'document_states/forms/widgets/workflow_image.html'
def format_value(self, value):
if value == '' or value is None:
return None
return value
def render(self, name, value, attrs=None):
if value:
output = []
output.append(widget_workflow_diagram(value))
return mark_safe(''.join(output))
else:
return ''

View File

@@ -41,8 +41,7 @@ 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={'class': 'select2', 'size': 10}),
**extra_kwargs
widget=widget_class(attrs={'class': 'select2', 'size': 10}), **extra_kwargs
)

View File

@@ -1,22 +0,0 @@
# -*- 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.documents.models.document_version_models
import mayan.apps.storage.classes
class Migration(migrations.Migration):
dependencies = [
('documents', '0047_auto_20180917_0737'),
]
operations = [
migrations.AlterField(
model_name='documentversion',
name='file',
field=models.FileField(storage=mayan.apps.storage.classes.FakeStorageSubclass(), upload_to=mayan.apps.documents.models.document_version_models.UUID_FUNCTION, verbose_name='File'),
),
]

View File

@@ -229,7 +229,7 @@ class DocumentPage(models.Model):
for transformation in transformations:
converter.transform(transformation=transformation)
return converter.get_page()
return page_image
except Exception as exception:
# Cleanup in case of error
logger.error(

View File

@@ -5,7 +5,7 @@ import os
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.smart_settings import Namespace
from .literals import (
DEFAULT_DOCUMENTS_HASH_BLOCK_SIZE, DEFAULT_LANGUAGE, DEFAULT_LANGUAGE_CODES
@@ -18,14 +18,15 @@ setting_documentimagecache_storage = namespace.add_setting(
default='django.core.files.storage.FileSystemStorage', help_text=_(
'Path to the Storage subclass to use when storing the cached '
'document image files.'
)
), quoted=True
)
setting_documentimagecache_storage_arguments = namespace.add_setting(
global_name='DOCUMENTS_CACHE_STORAGE_BACKEND_ARGUMENTS',
default={'location': os.path.join(settings.MEDIA_ROOT, 'document_cache')},
help_text=_(
default='{{location: {}}}'.format(
os.path.join(settings.MEDIA_ROOT, 'document_cache')
), help_text=_(
'Arguments to pass to the DOCUMENT_CACHE_STORAGE_BACKEND.'
),
), quoted=True,
)
setting_disable_base_image_cache = namespace.add_setting(
global_name='DOCUMENTS_DISABLE_BASE_IMAGE_CACHE', default=False,
@@ -126,8 +127,9 @@ setting_storage_backend = namespace.add_setting(
)
setting_storage_backend_arguments = namespace.add_setting(
global_name='DOCUMENTS_STORAGE_BACKEND_ARGUMENTS',
default={'location': os.path.join(settings.MEDIA_ROOT, 'document_storage')},
help_text=_('Arguments to pass to the DOCUMENT_STORAGE_BACKEND.')
default='{{location: {}}}'.format(
os.path.join(settings.MEDIA_ROOT, 'document_storage')
), help_text=_('Arguments to pass to the DOCUMENT_STORAGE_BACKEND.')
)
setting_thumbnail_height = namespace.add_setting(
global_name='DOCUMENTS_THUMBNAIL_HEIGHT', default='', help_text=_(

View File

@@ -1,6 +1,13 @@
from __future__ import unicode_literals
from mayan.apps.storage.utils import get_storage_subclass
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django.utils.module_loading import import_string
from .settings import (
setting_documentimagecache_storage,
@@ -8,10 +15,20 @@ from .settings import (
setting_storage_backend, setting_storage_backend_arguments
)
storage_documentversion = get_storage_subclass(
storage_documentversion = import_string(
dotted_path=setting_storage_backend.value
)(**setting_storage_backend_arguments.value)
)(
**yaml.load(
stream=setting_storage_backend_arguments.value or '{}',
Loader=SafeLoader
)
)
storage_documentimagecache = get_storage_subclass(
storage_documentimagecache = import_string(
dotted_path=setting_documentimagecache_storage.value
)(**setting_documentimagecache_storage_arguments.value)
)(
**yaml.load(
stream=setting_documentimagecache_storage_arguments.value or '{}',
Loader=SafeLoader
)
)

View File

@@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.apps import MayanAppConfig
from mayan.apps.common.html_widgets import TwoStateWidget
from mayan.apps.common.menus import (
menu_object, menu_secondary, menu_tools, menu_topbar, menu_user
menu_main, menu_object, menu_secondary, menu_tools, menu_user
)
from mayan.apps.navigation.classes import SourceColumn
@@ -85,7 +85,7 @@ class EventsApp(MayanAppConfig):
source=Notification, widget=TwoStateWidget
)
menu_topbar.bind_links(
menu_main.bind_links(
links=(link_user_notifications_list,), position=99
)
menu_object.bind_links(

View File

@@ -4,6 +4,12 @@ import json
import logging
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 _
@@ -51,7 +57,11 @@ class EXIFToolDriver(FileMetadataDriver):
)
def read_settings(self):
self.exiftool_path = setting_drivers_arguments.value.get(
driver_arguments = yaml.load(
stream=setting_drivers_arguments.value, Loader=SafeLoader
)
self.exiftool_path = driver_arguments.get(
'exif_driver', {}
).get('exiftool_path', DEFAULT_EXIF_PATH)

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.smart_settings import Namespace
from .literals import DEFAULT_EXIF_PATH
@@ -16,7 +16,12 @@ setting_auto_process = namespace.add_setting(
)
)
setting_drivers_arguments = namespace.add_setting(
default={'exif_driver': {'exiftool_path': DEFAULT_EXIF_PATH}}, help_text=_(
default='''
{{
exif_driver: {{exiftool_path: {}}},
}}
'''.replace('\n', '').format(DEFAULT_EXIF_PATH), help_text=_(
'Arguments to pass to the drivers.'
), global_name='FILE_METADATA_DRIVERS_ARGUMENTS'
), global_name='FILE_METADATA_DRIVERS_ARGUMENTS', quoted=True
)

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.smart_settings import Namespace
from .literals import DEFAULT_BACKEND, DEFAULT_LOCK_TIMEOUT_VALUE

View File

@@ -8,11 +8,11 @@ DEFAULT_DOCUMENT_BODY_TEMPLATE = _(
'--------\n '
'This email has been sent from %(project_title)s (%(project_website)s)'
)
DEFAULT_DOCUMENT_SUBJECT_TEMPLATE = _('Document: {{ document }}')
DEFAULT_LINK_BODY_TEMPLATE = _(
'To access this document click on the following link: '
'{{ link }}\n\n--------\n '
'This email has been sent from %(project_title)s (%(project_website)s)'
)
DEFAULT_LINK_SUBJECT_TEMPLATE = _('Link for document: {{ document }}')
EMAIL_SEPARATORS = (',', ';')

View File

@@ -2,32 +2,31 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.smart_settings import Namespace
from .literals import (
DEFAULT_DOCUMENT_BODY_TEMPLATE, DEFAULT_DOCUMENT_SUBJECT_TEMPLATE,
DEFAULT_LINK_BODY_TEMPLATE, DEFAULT_LINK_SUBJECT_TEMPLATE
DEFAULT_DOCUMENT_BODY_TEMPLATE, DEFAULT_LINK_BODY_TEMPLATE
)
namespace = Namespace(label=_('Mailing'), name='mailer')
setting_document_subject_template = namespace.add_setting(
default=DEFAULT_DOCUMENT_SUBJECT_TEMPLATE,
help_text=_('Template for the document email form subject line.'),
global_name='MAILER_DOCUMENT_SUBJECT_TEMPLATE'
)
setting_document_body_template = namespace.add_setting(
default=DEFAULT_DOCUMENT_BODY_TEMPLATE,
help_text=_('Template for the document email form body text. Can include HTML.'),
global_name='MAILER_DOCUMENT_BODY_TEMPLATE'
)
setting_link_subject_template = namespace.add_setting(
default=DEFAULT_LINK_SUBJECT_TEMPLATE,
default=_('Link for document: {{ document }}'),
help_text=_('Template for the document link email form subject line.'),
global_name='MAILER_LINK_SUBJECT_TEMPLATE'
global_name='MAILER_LINK_SUBJECT_TEMPLATE', quoted=True
)
setting_link_body_template = namespace.add_setting(
default=DEFAULT_LINK_BODY_TEMPLATE,
help_text=_('Template for the document link email form body text. Can include HTML.'),
global_name='MAILER_LINK_BODY_TEMPLATE'
global_name='MAILER_LINK_BODY_TEMPLATE', quoted=True
)
setting_document_subject_template = namespace.add_setting(
default=_('Document: {{ document }}'),
help_text=_('Template for the document email form subject line.'),
global_name='MAILER_DOCUMENT_SUBJECT_TEMPLATE', quoted=True
)
setting_document_body_template = namespace.add_setting(
default=DEFAULT_DOCUMENT_BODY_TEMPLATE,
help_text=_('Template for the document email form body text. Can include HTML.'),
global_name='MAILER_DOCUMENT_BODY_TEMPLATE', quoted=True
)

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