diff --git a/.tx/config b/.tx/config index 158957b39c..2a6a1da730 100644 --- a/.tx/config +++ b/.tx/config @@ -115,6 +115,12 @@ source_lang = en source_file = mayan/apps/events/locale/en/LC_MESSAGES/django.po type = PO +[mayan-edms.file_caching-3-0] +file_filter = mayan/apps/file_caching/locale//LC_MESSAGES/django.po +source_lang = en +source_file = mayan/apps/file_caching/locale/en/LC_MESSAGES/django.po +type = PO + [mayan-edms.file_metadata-3-0] file_filter = mayan/apps/file_metadata/locale//LC_MESSAGES/django.po source_lang = en @@ -222,3 +228,10 @@ file_filter = mayan/apps/user_management/locale//LC_MESSAGES/django.po source_lang = en source_file = mayan/apps/user_management/locale/en/LC_MESSAGES/django.po type = PO + +[mayan-edms.weblink-3-0] +file_filter = mayan/apps/weblinks/locale//LC_MESSAGES/django.po +source_lang = en +source_file = mayan/apps/weblinks/locale/en/LC_MESSAGES/django.po +type = PO + diff --git a/HISTORY.rst b/HISTORY.rst index a2152d5e41..aea00e8a61 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,3 +1,84 @@ +3.3 (2019-XX-XX) +================ +- Add support for icon shadows. +- Add icons and no-result template to the object error log view and + links. +- Use Select2 widget for the document type selection form. +- Backport the vertical main menu update. +- Backport workflow preview refactor. GitLab issue #532. +- Add support for source column inheritance. +- Add support for source column exclusion. +- Backport workflow context support. +- Backport workflow transitions field support. +- Backport workflow email action. +- Backport individual index rebuild support. +- Rename the installjavascript command to installdependencies. +- Remove database conversion command. +- Remove support for quoted configuration entries. Support unquoted, + nested dictionaries in the configuration. Requires manual + update of existing config.yml files. +- Support user specified locations for the configuration file with the + CONFIGURATION_FILEPATH (MAYAN_CONFIGURATION_FILEPATH environment variable), + and CONFIGURATION_LAST_GOOD_FILEPATH + (MAYAN_CONFIGURATION_LAST_GOOD_FILEPATH environment variable) settings. +- Move bootstrapped settings code to their own module in the smart_settings + apps. +- Remove individual database configuration options. All database + configuration is now done using MAYAN_DATABASES to mirror Django way of + doing atabase etup. +- Added support for YAML encoded environment variables to the platform + templates apps. +- Move YAML code to its own module. +- Move Django and Celery settings. +- Backport FakeStorageSubclass from versions/next. +- Remove django-environ. +- Support checking in and out multiple documents. +- Remove encapsulate helper. +- Add support for menu inheritance. +- Emphasize source column labels. +- Backport file cache manager app. +- Convert document image cache to use file cache manager app. + Add setting DOCUMENTS_CACHE_MAXIMUM_SIZE defaults to 500 MB. +- Replace djcelery and replace it with django-celery-beat. +- Update Celery to version 4.3.0 + Thanks to Jakob Haufe (@sur5r) and Jesaja Everling (@jeverling) + for much of the research and code updates. +- Support wildcard MIME type associations for the file metadata drivers. +- Rename MAYAN_GUID to MAYAN_GID +- Update Gunicorn to use sync workers. +- Include devpi-server as a development dependency. +- Update default Docker stack file. +- Remove Redis from the Docker image. +- Add Celery flower to the Docker image. +- Allow PIP proxying to the Docker image during build. +- Default Celery worker concurrency to 0 (auto). +- Set DJANGO_SETTINGS_MODULE environment variable to make it + available to sub processes. +- Add entrypoint commands to run single workers, single gunicorn + or single celery commands like "flower". +- Add platform template to return queues for a worker. +- Update the EXIFTOOL driver to run for all documents + regardless of MIME type. +- Remove task inspection from task manager app. +- Move pagination navigation inside the toolbar. +- Remove document image clear link and view. + This is now handled by the file caching app. +- Add web links app. +- Add support to display column help text + as a tooltip. +- Update numeric dashboard widget to display + thousand commas. +- Add support for disabling document pages. +- Add support for converter layers. +- Add redactions app. +- Unify all line endings to be Linux style. +- Add support for changing the system messages position. + GitLab issue #640. Thanks to Matthias Urhahn (@d4rken). +- Update Docker deploy script. Use alpine postgres version. + Support Docker networks and make it the default. + Delete the containers to allow the script to be idempotent. + Deploy a Redis container. + 3.2.8 (2019-10-01) ================== - Fix error when accessing some API entry points without diff --git a/Makefile b/Makefile index bffe667715..e5de22ab31 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ clean-pyc: ## Remove Python artifacts. find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + - + find . -name '__pycache__' -exec rm -R -f {} + # Testing @@ -234,10 +234,10 @@ generate-requirements: ## Generate all requirements files from the project deped # Dev server runserver: ## Run the development server. - ./manage.py runserver --settings=mayan.settings.development $(ADDRPORT) + ./manage.py runserver --nothreading --settings=mayan.settings.development $(ADDRPORT) runserver_plus: ## Run the Django extension's development server. - ./manage.py runserver_plus --settings=mayan.settings.development $(ADDRPORT) + ./manage.py runserver_plus --nothreading --settings=mayan.settings.development $(ADDRPORT) shell_plus: ## Run the shell_plus command. ./manage.py shell_plus --settings=mayan.settings.development @@ -258,7 +258,7 @@ test-with-docker-frontend: ## Launch a front end instance that uses the producti ./manage.py runserver --settings=mayan.settings.staging.docker test-with-docker-worker: ## Launch a worker instance that uses the production-like services. - ./manage.py celery worker --settings=mayan.settings.staging.docker -B -l INFO -O fair + DJANGO_SETTINGS_MODULE=mayan.settings.staging.docker ./manage.py celery worker -A mayan -B -l INFO -O fair docker-mysql-on: ## Launch and initialize a MySQL Docker container. docker run -d --name mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=True -e MYSQL_DATABASE=mayan_edms mysql diff --git a/contrib/scripts/install/development.sh b/contrib/scripts/install/development.sh deleted file mode 100644 index a8c9497c46..0000000000 --- a/contrib/scripts/install/development.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env bash - -INSTALLATION_DIRECTORY=/home/vagrant/mayan-edms/ -DB_NAME=mayan_edms -DB_PASSWORD=test123 - -cat << EOF | sudo tee -a /etc/motd.tail -**********************************sudo apt - -Mayan EDMS Vagrant Development Box - -********************************** -EOF - -# Update sources -echo -e "\n -> Running apt-get update & upgrade \n" -sudo apt-get -qq update -sudo apt-get -y upgrade - -echo -e "\n -> Installing core binaries \n" -sudo apt-get -y install git-core python-virtualenv gcc python-dev libjpeg-dev libpng-dev libtiff-dev tesseract-ocr poppler-utils libreoffice - -echo -e "\n -> Cloning development branch of repository \n" -git clone /mayan-edms-repository/ $INSTALLATION_DIRECTORY -cd $INSTALLATION_DIRECTORY -git checkout development -git reset HEAD --hard - -echo -e "\n -> Setting up virtual env \n" -virtualenv venv -source venv/bin/activate - -echo -e "\n -> Installing python dependencies \n" -pip install -r requirements.txt - -echo -e "\n -> Running Mayan EDMS initial setup \n" -./manage.py initialsetup - -echo -e "\n -> Installing Redis server \n" -sudo apt-get install -y redis-server -pip install redis - -echo -e "\n -> Installing testing software \n" -pip install coverage - -echo -e "\n -> Installing MySQL \n" -sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password password '$DB_PASSWORD -sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password '$DB_PASSWORD -sudo apt-get install -y mysql-server libmysqlclient-dev -# Create a passwordless root and travis users -mysql -u root -p$DB_PASSWORD -e "SET PASSWORD = PASSWORD('');" -mysql -u root -e "CREATE USER 'travis'@'localhost' IDENTIFIED BY '';GRANT ALL PRIVILEGES ON * . * TO 'travis'@'localhost';FLUSH PRIVILEGES;" -mysql -u travis -e "CREATE DATABASE $DB_NAME;" -pip install mysql-python - -echo -e "\n -> Installing PostgreSQL \n" -sudo apt-get install -y postgresql postgresql-server-dev-all -sudo -u postgres psql -c 'create database mayan_edms;' -U postgres -sudo cat > /etc/postgresql/9.3/main/pg_hba.conf << EOF -local all postgres trust - -# TYPE DATABASE USER ADDRESS METHOD - -# "local" is for Unix domain socket connections only -local all all peer -# IPv4 local connections: -host all all 127.0.0.1/32 md5 -# IPv6 local connections: -host all all ::1/128 md5 -EOF - -pip install -q psycopg2 diff --git a/contrib/scripts/install/dialog.sh b/contrib/scripts/install/dialog.sh deleted file mode 100644 index e7d929639c..0000000000 --- a/contrib/scripts/install/dialog.sh +++ /dev/null @@ -1,1733 +0,0 @@ -#!/bin/bash -# -# Copyright (c) 2017 Igor Pecovnik, igor.pecovnik@gma**.com -# -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. - -# Functions: -# check_status -# choose_webserver -# server_conf -# install_packet -# alive_port -# alive_process -# install_basic -# create_ispconfig_configuration -# install_cups -# install_samba -# install_omv -# install_tvheadend -# install_urbackup -# install_transmission -# install_transmission_seed_armbian_torrents -# install_syncthing -# install_vpn_server -# install_vpn_client -# install_DashNTP -# install_MySQL -# install_MySQLDovecot -# install_Virus -# install_hhvm -# install_phpmyadmin -# install_apache -# install_nginx -# install_PureFTPD -# install_Bind -# install_Stats -# install_Jailkit -# install_Fail2BanDovecot -# install_Fail2BanRulesDovecot -# install_ISPConfig -# check_if_installed - -# -# load functions, local first -# -if [[ -f debian-config-jobs ]]; then source debian-config-jobs; - elif [[ -f /usr/lib/armbian-config/jobs.sh ]]; then source /usr/lib/armbian-config/jobs.sh; - else exit 1; -fi - -if [[ -f debian-config-submenu ]]; then source debian-config-submenu; - elif [[ -f /usr/lib/armbian-config/submenu.sh ]]; then source /usr/lib/armbian-config/submenu.sh; - else exit 1; -fi - -if [[ -f debian-config-functions ]]; then source debian-config-functions; - elif [[ -f /usr/lib/armbian-config/functions.sh ]]; then source /usr/lib/armbian-config/functions.sh; - else exit 1; -fi - -if [[ -f debian-config-functions-network ]]; then source debian-config-functions-network; - elif [[ -f /usr/lib/armbian-config/functions-network.sh ]]; then source /usr/lib/armbian-config/functions-network.sh; - else exit 1; -fi - - - - -# -# not sure if needed -# -export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - - - - -function check_status -{ -# -# Check if service is already installed and show it's status -# - -dialog --backtitle "$BACKTITLE" --title "Please wait" --infobox "\nLoading install info ... " 5 28 -LIST=() -LIST_CONST=3 - -# Samba -SAMBA_STATUS="$(check_if_installed samba && echo "on" || echo "off" )" -alive_port "Windows compatible file sharing" "445" "boolean" -LIST+=( "Samba" "$DESCRIPTION" "$SAMBA_STATUS" ) - -# CUPS -CUPS_STATUS="$(check_if_installed cups && echo "on" || echo "off" )" -alive_port "Common UNIX Printing System (CUPS)" "631" "boolean" -LIST+=( "CUPS" "$DESCRIPTION" "$CUPS_STATUS" ) - -# TV headend -TVHEADEND_STATUS="$(check_if_installed tvheadend && echo "on" || echo "off" )" -alive_port "TV streaming server" "9981" -LIST+=( "TV headend" "$DESCRIPTION" "$TVHEADEND_STATUS" ) - -# Synthing -SYNCTHING_STATUS="$([[ -d /usr/bin/syncthing ]] && echo "on" || echo "off" )" -alive_port "Personal cloud @syncthing.net" "8384" "boolean" -LIST+=( "Syncthing" "$DESCRIPTION" "$SYNCTHING_STATUS" ) - -# Mayan EDMS -MAYAN_STATUS="$( [[ -d /opt/mayan ]] && echo "on" || echo "off" )" -alive_port "Electronic Document Management System" "8000" "boolean" -LIST+=( "Mayan EDMS" "$DESCRIPTION" "$MAYAN_STATUS" ) - -# Exagear -if [[ "$(check_if_installed xserver-xorg && echo "on")" == "on" && "$family" == "Ubuntu" ]]; then - EXAGEAR_STATUS="$(check_if_installed exagear-armbian && echo "on" || echo "off" )" - LIST+=( "ExaGear" "32bit x86 Linux/Windows emulator trial" "$EXAGEAR_STATUS" ) -fi - -if [[ "$(dpkg --print-architecture)" == "armhf" || "$(dpkg --print-architecture)" == "amd64" ]]; then - LIST_CONST=2 - # vpn server - VPN_SERVER_STATUS="$([[ -d /usr/local/vpnserver ]] && echo "on" || echo "off" )" - LIST+=( "VPN server" "Softether VPN server" "$VPN_SERVER_STATUS" ) - # vpn client - VPN_CLIENT_STATUS="$([[ -d /usr/local/vpnclient ]] && echo "on" || echo "off" )" - LIST+=( "VPN client" "Softether VPN client" "$VPN_CLIENT_STATUS" ) -fi -# NCP -NCP_STATUS="$( [[ -d /var/www/nextcloud ]] && echo "on" || echo "off" )" -[[ "$family" != "Ubuntu" ]] && LIST+=( "NCP" "Nextcloud personal cloud" "$NCP_STATUS" ) -# OMV -OMV_STATUS="$(check_if_installed openmediavault && echo "on" || echo "off" )" -[[ "$family" != "Ubuntu" ]] && LIST+=( "OMV" "OpenMediaVault NAS solution" "$OMV_STATUS" ) && LIST_CONST=3 - -# Plex media server -PLEX_STATUS="$((check_if_installed plexmediaserver || check_if_installed plexmediaserver-installer) && echo "on" || echo "off" )" -alive_port "Plex media server" "32400" -LIST+=( "Plex" "$DESCRIPTION" "$PLEX_STATUS" ) - -# Radarr -RADARR_STATUS="$([[ -d /opt/Radarr ]] && echo "on" || echo "off" )" -alive_port "Movies downloading server" "7878" -LIST+=( "Radarr" "$DESCRIPTION" "$RADARR_STATUS" ) - -# Sonarr -SONARR_STATUS="$([[ -d /opt/NzbDrone ]] && echo "on" || echo "off" )" -alive_port "TV shows downloading server" "8989" -LIST+=( "Sonarr" "$DESCRIPTION" "$SONARR_STATUS" ) - -# MINIdlna -MINIDLNA_STATUS="$(check_if_installed minidlna && echo "on" || echo "off" )" -alive_port "Lightweight DLNA/UPnP-AV server" "8200" "boolean" -LIST+=( "Minidlna" "$DESCRIPTION" "$MINIDLNA_STATUS" ) - -# Pi hole -PI_HOLE_STATUS="$([[ -d /etc/pihole ]] && echo "on" || echo "off" )" -alive_process "Ad blocker" "pihole-FTL" -LIST+=( "Pi hole" "$DESCRIPTION" "$PI_HOLE_STATUS" ) - -# Transmission -TRANSMISSION_STATUS="$(check_if_installed transmission-daemon && echo "on" || echo "off" )" -alive_port "Torrent download server" "9091" "boolean" -LIST+=( "Transmission" "$DESCRIPTION" "$TRANSMISSION_STATUS" ) - - -# UrBackup -URBACKUP_STATUS="$((check_if_installed urbackup-server || check_if_installed urbackup-server-dbg) && echo "on" || echo "off" )" -alive_port "Client/server backup system" "51413" "boolean" -LIST+=( "UrBackup" "$DESCRIPTION" "$URBACKUP_STATUS" ) - - -# ISPconfig -ISPCONFIG_STATUS="$([[ -d /usr/local/ispconfig ]] && echo "on" || echo "off" )" -LIST+=( "ISPConfig" "SMTP mail, IMAP, POP3 & LAMP/LEMP web server" "$ISPCONFIG_STATUS" ) -} - - - - -function choose_webserver -{ -# -# Target web server selection -# -check_if_installed openmediavault -case $? in - 0) - # OMV installed, prevent switching from nginx to apache which would trash OMV installation - server="nginx" - ;; - *) - dialog --title "Choose a webserver" --backtitle "$BACKTITLE" --yes-label "Apache" --no-label "Nginx" \ - --yesno "\nChoose a web server which you are familiar with. They both work almost the same." 8 70 - response=$? - case $response in - 0) server="apache";; - 1) server="nginx";; - 255) exit;; - esac - ;; -esac -} - - - - -function server_conf -{ -# -# Add some reqired date for installation -# -exec 3>&1 -dialog --title "Server configuration" --separate-widget $'\n' --ok-label "Install" --backtitle "$BACKTITLE" \ ---form "\nPlease fill out this form:\n " \ -12 70 0 \ -"Your FQDN for $serverip:" 1 1 "$hostnamefqdn" 1 31 32 0 \ -"Mysql root password:" 2 1 "$mysql_pass" 2 31 32 0 \ -2>&1 1>&3 | { - -read -r hostnamefqdn -read -r mysql_pass -echo $mysql_pass > ${TEMP_DIR}/mysql_pass -echo $hostnamefqdn > ${TEMP_DIR}/hostnamefqdn -# end -} -exec 3>&- -# read variables back -read MYSQL_PASS < ${TEMP_DIR}/mysql_pass -read HOSTNAMEFQDN < ${TEMP_DIR}/hostnamefqdn -} - - - - -install_packet () -{ -# -# Install missing packets -# -i=0 -j=1 -IFS=" " -declare -a PACKETS=($1) -#skupaj=$(apt-get -s -y -qq install $1 | wc -l) -skupaj=${#PACKETS[@]} -while [[ $i -lt $skupaj ]]; do -procent=$(echo "scale=2;($j/$skupaj)*100"|bc) - x=${PACKETS[$i]} - if [ $(dpkg-query -W -f='${Status}' $x 2>/dev/null | grep -c "ok installed") -eq 0 ]; then - printf '%.0f\n' $procent | dialog \ - --backtitle "$BACKTITLE" \ - --title "Installing" \ - --gauge "\n$2\n\n$x" 10 70 - if [ "$(DEBIAN_FRONTEND=noninteractive apt-get -qq -y install $x >${TEMP_DIR}/install.log 2>&1 || echo 'Installation failed' \ - | grep 'Installation failed')" != "" ]; then - echo -e "[\e[0;31m error \x1B[0m] Installation failed" - tail ${TEMP_DIR}/install.log - exit - fi - fi - i=$[$i+1] - j=$[$j+1] -done -echo "" -} - - -alive_port () -{ -# -# Displays URL to the service $1 on port $2 or just that is active if $3 = boolean -# -DEFAULT_ADAPTER=$(ip -4 route ls | grep default | grep -Po '(?<=dev )(\S+)') -LOCALIPADD=$(ip -4 addr show dev $DEFAULT_ADAPTER | awk '/inet/ {print $2}' | cut -d'/' -f1) -if [[ -n $(netstat -lnt | awk '$6 == "LISTEN" && $4 ~ ".'$2'"') ]]; then - if [[ $3 == boolean ]]; then - DESCRIPTION="$1 is \Z1active\Z0"; - else - DESCRIPTION="Active on http://${LOCALIPADD}:\Z1$2\Z0"; - fi -else -DESCRIPTION="$1"; -fi -} - - - -alive_process () -{ -# -# check if process name $2 is running. Display it's name $1 or $1 is active if active -# -if pgrep -x "$2" > /dev/null 2>&1; then DESCRIPTION="$1 is \Z1active\Z0"; else DESCRIPTION="$1"; fi -} - - - - - - - -install_basic (){ -# -# Set hostname, FQDN, add to sources list -# -IFS=" " -set ${HOSTNAMEFQDN//./ } -HOSTNAMESHORT="$1" -cp /etc/hosts /etc/hosts.backup -cp /etc/hostname /etc/hostname.backup -# create new -echo "127.0.0.1 localhost.localdomain localhost" > /etc/hosts -echo "${serverIP} ${HOSTNAMEFQDN} ${HOSTNAMESHORT} #ispconfig " >> /etc/hosts -echo "$HOSTNAMESHORT" > /etc/hostname -/etc/init.d/hostname.sh start >/dev/null 2>&1 -hostnamectl set-hostname $HOSTNAMESHORT -if [[ $family == "Ubuntu" ]]; then - # set hostname in Ubuntu - hostnamectl set-hostname $HOSTNAMESHORT - # disable AppArmor - if [[ -n $(service apparmor status | grep -w active | grep -w running) ]]; then - service apparmor stop - update-rc.d -f apparmor remove - apt-get -y -qq remove apparmor apparmor-utils - fi -else - grep -q "contrib" /etc/apt/sources.list || sed -i 's|main|main contrib|' /etc/apt/sources.list - grep -q "non-free" /etc/apt/sources.list || sed -i 's|contrib|contrib non-free|' /etc/apt/sources.list - grep -q "deb http://ftp.debian.org/debian jessie-backports main" /etc/apt/sources.list || echo "deb http://ftp.debian.org/debian jessie-backports main" >> /etc/apt/sources.list - debconf-apt-progress -- apt-get update -fi -} - - - - -create_ispconfig_configuration (){ -# -# ISPConfig autoconfiguration -# -cat > ${TEMP_DIR}/isp.conf.php < -EOF -} - - - -install_cups () -{ -# -# Install printer system -# -debconf-apt-progress -- apt-get -y install cups lpr cups-filters -# cups-filters if jessie -sed -e 's/Listen localhost:631/Listen 631/g' -i /etc/cups/cupsd.conf -sed -e 's//\nallow $SUBNET/g' -i /etc/cups/cupsd.conf -sed -e 's//\nallow $SUBNET/g' -i /etc/cups/cupsd.conf -sed -e 's//\nallow $SUBNET/g' -i /etc/cups/cupsd.conf -service cups restart -service samba restart | service smbd restart >/dev/null 2>&1 -} - - - - -install_samba () -{ -# -# install Samba file sharing -# -local SECTION="Samba" -SMBUSER=$(whiptail --inputbox "What is your samba username?" 8 78 $SMBUSER --title "$SECTION" 3>&1 1>&2 2>&3) -exitstatus=$?; if [ $exitstatus = 1 ]; then exit 1; fi -SMBPASS=$(whiptail --inputbox "What is your samba password?" 8 78 $SMBPASS --title "$SECTION" 3>&1 1>&2 2>&3) -exitstatus=$?; if [ $exitstatus = 1 ]; then exit 1; fi -SMBGROUP=$(whiptail --inputbox "What is your samba group?" 8 78 $SMBGROUP --title "$SECTION" 3>&1 1>&2 2>&3) -exitstatus=$?; if [ $exitstatus = 1 ]; then exit 1; fi -# -debconf-apt-progress -- apt-get -y install samba samba-common-bin samba-vfs-modules -useradd $SMBUSER -echo -ne "$SMBPASS\n$SMBPASS\n" | passwd $SMBUSER >/dev/null 2>&1 -echo -ne "$SMBPASS\n$SMBPASS\n" | smbpasswd -a -s $SMBUSER >/dev/null 2>&1 -service samba stop | service smbd stop >/dev/null 2>&1 -cp /etc/samba/smb.conf /etc/samba/smb.conf.stock -cat > /etc/samba/smb.conf.tmp << EOF -[global] - workgroup = SMBGROUP - server string = %h server - hosts allow = SUBNET - log file = /var/log/samba/log.%m - max log size = 1000 - syslog = 0 - panic action = /usr/share/samba/panic-action %d - load printers = yes - printing = cups - printcap name = cups - min receivefile size = 16384 - write cache size = 524288 - getwd cache = yes - socket options = TCP_NODELAY IPTOS_LOWDELAY - -[printers] - comment = All Printers - path = /var/spool/samba - browseable = no - public = yes - guest ok = yes - writable = no - printable = yes - printer admin = SMBUSER - -[print$] - comment = Printer Drivers - path = /etc/samba/drivers - browseable = yes - guest ok = no - read only = yes - write list = SMBUSER - -[ext] - comment = Storage - path = /ext - writable = yes - public = no - valid users = SMBUSER - force create mode = 0644 -EOF -sed -i "s/SMBGROUP/$SMBGROUP/" /etc/samba/smb.conf.tmp -sed -i "s/SMBUSER/$SMBUSER/" /etc/samba/smb.conf.tmp -sed -i "s/SUBNET/$SUBNET/" /etc/samba/smb.conf.tmp -dialog --backtitle "$BACKTITLE" --title "Review samba configuration" --no-collapse --editbox /etc/samba/smb.conf.tmp 30 0 2> /etc/samba/smb.conf.tmp.out -if [[ $? = 0 ]]; then - mv /etc/samba/smb.conf.tmp.out /etc/samba/smb.conf - install -m 755 -g $SMBUSER -o $SMBUSER -d /ext - service service smbd stop >/dev/null 2>&1 - sleep 3 - service service smbd start >/dev/null 2>&1 -fi -} - -install_ncp (){ - curl -sSL https://raw.githubusercontent.com/nextcloud/nextcloudpi/master/install.sh | bash -} - -install_omv (){ -# -# On Debian install OpenMediaVault 3 (Jessie) or 4 (Stretch) -# -# TODO: Some OMV packages lack authentication - -if [[ "$family" == "Ubuntu" ]]; then - dialog --backtitle "$BACKTITLE" --title "Dependencies not met" --msgbox "\nOpenMediaVault can only be installed on Debian." 7 52 - sleep 5 - exit 1 -fi - -case $distribution in - jessie) - OMV_Name="erasmus" - OMV_EXTRAS_URL="https://github.com/OpenMediaVault-Plugin-Developers/packages/raw/master/openmediavault-omvextrasorg_latest_all3.deb" - ;; - stretch) - OMV_Name="arrakis" - OMV_EXTRAS_URL="https://github.com/OpenMediaVault-Plugin-Developers/packages/raw/master/openmediavault-omvextrasorg_latest_all4.deb" - ;; -esac - -systemctl status log2ram >/dev/null 2>&1 && (systemctl stop log2ram ; systemctl disable log2ram >/dev/null 2>&1; rm /etc/cron.daily/log2ram) -export APT_LISTCHANGES_FRONTEND=none -if [ -f /etc/armbian-release ]; then - . /etc/armbian-release -else - sed -i "s/^# en_US.UTF-8/en_US.UTF-8/" /etc/locale.gen - locale-gen -fi - -# preserve cpufrequtils settings: -if [ -f /etc/default/cpufrequtils ]; then - . /etc/default/cpufrequtils -fi - -cat > /etc/apt/sources.list.d/openmediavault.list << EOF -deb https://openmediavault.github.io/packages/ ${OMV_Name} main - -## Uncomment the following line to add software from the proposed repository. -deb https://openmediavault.github.io/packages/ ${OMV_Name}-proposed main - -## This software is not part of OpenMediaVault, but is offered by third-party -## developers as a service to OpenMediaVault users. -# deb https://openmediavault.github.io/packages/ ${OMV_Name} partner -EOF - -debconf-apt-progress -- apt-get update - -read HOSTNAME /dev/null | awk -F" " '/additional disk space will be used/ {print $4}') -SPACE_AVAIL=$(df -k / | awk -F" " '/\/$/ {printf ("%0.0f",$4/1200); }') -if [ ${SPACE_AVAIL} -lt ${SPACE_NEEDED} ]; then - dialog --backtitle "$BACKTITLE" --title "No space left on device" --msgbox "\nOpenMediaVault needs ${SPACE_NEEDED} MB for installation while only ${SPACE_AVAIL} MB are available." 7 52 - exit 1 -fi -apt-get --allow-unauthenticated install openmediavault-keyring -apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 7AA630A1EDEE7D73 -debconf-apt-progress -- apt-get -y --allow-unauthenticated --fix-missing --no-install-recommends \ - -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install openmediavault postfix dirmngr -FILE="${TEMP_DIR}/omv_extras.deb"; wget "$OMV_EXTRAS_URL" -qO $FILE && dpkg -i $FILE ; rm $FILE -# /usr/sbin/omv-update -debconf-apt-progress -- apt-get update -debconf-apt-progress -- apt-get --yes --force-yes --fix-missing --auto-remove --allow-unauthenticated \ - --show-upgraded --option DPkg::Options::="--force-confold" dist-upgrade - -# Install flashmemory plugin and netatalk by default, use nice logo for the latter, -# disable OMV monitoring by default -. /usr/share/openmediavault/scripts/helper-functions -debconf-apt-progress -- apt-get -y --fix-missing --no-install-recommends --auto-remove install openmediavault-flashmemory openmediavault-netatalk -AFP_Options="mimic model = Macmini" -SMB_Options="min receivefile size = 16384\nwrite cache size = 524288\ngetwd cache = yes\nsocket options = TCP_NODELAY IPTOS_LOWDELAY" -xmlstarlet ed -L -u "/config/services/afp/extraoptions" -v "$(echo -e "${AFP_Options}")" ${OMV_CONFIG_FILE} -xmlstarlet ed -L -u "/config/services/smb/extraoptions" -v "$(echo -e "${SMB_Options}")" ${OMV_CONFIG_FILE} -xmlstarlet ed -L -u "/config/services/flashmemory/enable" -v "1" ${OMV_CONFIG_FILE} -xmlstarlet ed -L -u "/config/services/ssh/enable" -v "1" ${OMV_CONFIG_FILE} -xmlstarlet ed -L -u "/config/services/ssh/permitrootlogin" -v "1" ${OMV_CONFIG_FILE} -xmlstarlet ed -L -u "/config/system/time/ntp/enable" -v "1" ${OMV_CONFIG_FILE} -xmlstarlet ed -L -u "/config/system/time/timezone" -v "${TZ}" ${OMV_CONFIG_FILE} -xmlstarlet ed -L -u "/config/system/network/dns/hostname" -v "${HOSTNAME}" ${OMV_CONFIG_FILE} -/usr/sbin/omv-rpc -u admin "perfstats" "set" '{"enable":false}' -/usr/sbin/omv-rpc -u admin "config" "applyChanges" '{ "modules": ["monit","rrdcached","collectd"],"force": true }' -sed -i 's|-j /var/lib/rrdcached/journal/ ||' /etc/init.d/rrdcached -/sbin/folder2ram -enablesystemd 2>/dev/null - -# Prevent accidentally destroying board performance by clicking around in OMV UI since -# OMV sets 'powersave' governor when touching 'Power Management' settings. -if [ ! -f /etc/default/cpufrequtils ]; then - DEFAULT_GOV="$(zgrep "^CONFIG_CPU_FREQ_DEFAULT_GOV_" /proc/config.gz 2>/dev/null | sed 's/CONFIG_CPU_FREQ_DEFAULT_GOV_//')" - if [ -n "${DEFAULT_GOV}" ]; then - GOVERNOR=$(cut -f1 -d= <<<"${DEFAULT_GOV}" | tr '[:upper:]' '[:lower:]') - else - GOVERNOR=ondemand - fi - MIN_SPEED="0" - MAX_SPEED="0" -fi -echo -e "OMV_CPUFREQUTILS_GOVERNOR=${GOVERNOR}" >>/etc/default/openmediavault -echo -e "OMV_CPUFREQUTILS_MINSPEED=${MIN_SPEED}" >>/etc/default/openmediavault -echo -e "OMV_CPUFREQUTILS_MAXSPEED=${MAX_SPEED}" >>/etc/default/openmediavault -for i in netatalk samba flashmemory ssh ntp timezone monit rrdcached collectd cpufrequtils ; do - /usr/sbin/omv-mkconf $i -done - -# Hardkernel Cloudshell 1 and 2 fixes, read the whole thread for details: -# https://forum.openmediavault.org/index.php/Thread/17855 -lsusb | grep -q -i "05e3:0735" && sed -i "/exit 0/i echo 20 > /sys/class/block/sda/queue/max_sectors_kb" /etc/rc.local -if [ "X${BOARD}" = "Xodroidxu4" ]; then - HMP_Fix='; taskset -c -p 4-7 $i ' - apt install -y i2c-tools - /usr/sbin/i2cdetect -y 1 | grep -q "60: 60" - if [ $? -eq 0 ]; then - add-apt-repository -y ppa:kyle1117/ppa - sed -i 's/jessie/xenial/' /etc/apt/sources.list.d/kyle1117-ppa-jessie.list - apt install -y -q cloudshell-lcd odroid-cloudshell cloudshell2-fan & - lsusb -v | awk -F"__" '/RANDOM_/ {print $2}' | head -n1 | while read ; do - echo "ATTRS{idVendor}==\"152d\", ATTRS{idProduct}==\"0561\", KERNEL==\"sd*\", ENV{DEVTYPE}==\"disk\", SYMLINK=\"disk/by-id/\$env{ID_BUS}-CloudShell2-${REPLY}-\$env{ID_MODEL}\"" >> /etc/udev/rules.d/99-cloudshell2.rules - echo "ATTRS{idVendor}==\"152d\", ATTRS{idProduct}==\"0561\", KERNEL==\"sd*\", ENV{DEVTYPE}==\"partition\", SYMLINK=\"disk/by-id/\$env{ID_BUS}-CloudShell2-${REPLY}-\$env{ID_MODEL}-part%n\"" >> /etc/udev/rules.d/99-cloudshell2.rules - done - fi -fi - -# Add a cron job to make NAS processes more snappy -systemctl status rsyslog >/dev/null 2>&1 -if [ $? -eq 0 ]; then - echo ':msg, contains, "do ionice -c1" ~' >/etc/rsyslog.d/omv-armbian.conf - systemctl restart rsyslog -fi -echo "* * * * * root for i in \`pgrep \"ftpd|nfsiod|smbd|afpd|cnid\"\` ; do ionice -c1 -p \$i ${HMP_Fix}; done >/dev/null 2>&1" >/etc/cron.d/make_nas_processes_faster -chmod 600 /etc/cron.d/make_nas_processes_faster - -/usr/sbin/omv-initsystem -} - - - - -install_tvheadend () -{ -# -# TVheadend https://tvheadend.org/ unofficial port https://tvheadend.org/boards/5/topics/21528 -# -if [ ! -f /etc/apt/sources.list.d/tvheadend.list ]; then - echo "deb https://dl.bintray.com/tvheadend/deb xenial release-4.2" >> /etc/apt/sources.list.d/tvheadend.list - apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 379CE192D401AB61 >/dev/null 2>&1 -fi - -if [[ $distribution == "stretch" ]]; then - URL="http://security.debian.org/debian-security/pool/updates/main/o/openssl/libssl1.0.0_1.0.1t-1+deb8u8_"$(dpkg --print-architecture)".deb" - fancy_wget "$URL" "-O ${TEMP_DIR}/package.deb" - dpkg -i ${TEMP_DIR}/package.deb >/dev/null 2>&1 - local pkglist="libssl-doc zlib1g-dev tvheadend xmltv-util" -else - local pkglist="libssl-doc libssl1.0.0 zlib1g-dev tvheadend xmltv-util" -fi - -debconf-apt-progress -- apt-get update -debconf-apt-progress -- apt-get -y install $pkglist -} - - - - -install_urbackup () -{ -# -# Client/server backup system https://www.urbackup.org/ -# -if [ "$(dpkg --print-architecture | grep arm64)" == "arm64" ]; then local arch=armhf; else local arch=$(dpkg --print-architecture); fi -PREFIX="http://hndl.urbackup.org/Server/latest/" -URL="http://hndl.urbackup.org/Server/latest/"$(wget -q $PREFIX -O - | html2text -width 120 | grep deb | awk ' { print $3 }' | grep $arch) -fancy_wget "$URL" "-O ${TEMP_DIR}/package.deb" -dpkg -i ${TEMP_DIR}/package.deb >/dev/null 2>&1 -apt-get -yy -f install -} - - - - -install_transmission () -{ -# -# transmission -# -install_packet "debconf-utils unzip build-essential html2text apt-transport-https" "Downloading dependencies" -install_packet "transmission-cli transmission-common transmission-daemon" "Install torrent server" -# systemd workaround -# https://forum.armbian.com/index.php?/topic/4017-programs-does-not-start-automatically-at-boot/ -sed -e 's/exit 0//g' -i /etc/rc.local - cat >> /etc/rc.local <<"EOF" -service transmission-daemon restart -exit 0 -EOF -} - - - -install_transmission_seed_armbian_torrents () -{ -# -# seed our torrents -# -# adjust network buffers if necessary -rmem_recommended=4194304 -wmem_recommended=1048576 -rmem_actual=$(sysctl net.core.rmem_max | awk -F" " '{print $3}') -if [ ${rmem_actual} -lt ${rmem_recommended} ]; then - grep -q net.core.rmem_max /etc/sysctl.conf && \ - sed -i "s/net.core.rmem_max =.*/net.core.rmem_max = ${rmem_recommended}/" /etc/sysctl.conf || \ - echo "net.core.rmem_max = ${rmem_recommended}" >> /etc/sysctl.conf -fi -wmem_actual=$(sysctl net.core.wmem_max | awk -F" " '{print $3}') -if [ ${wmem_actual} -lt ${wmem_recommended} ]; then - grep -q net.core.wmem_max /etc/sysctl.conf && \ - sed -i "s/net.core.wmem_max =.*/net.core.wmem_max = ${wmem_recommended}/" /etc/sysctl.conf || \ - echo "net.core.wmem_max = ${wmem_recommended}" >> /etc/sysctl.conf -fi -/sbin/sysctl -p -# create cron job for daily sync with official Armbian torrents -cat > /etc/cron.daily/seed-armbian-torrent <<"EOF" -#!/bin/bash -# -# armbian torrents auto update -# -# download latest torrent pack -wget -qO- -O ${TEMP_DIR}/armbian-torrents.zip https://dl.armbian.com/torrent/all-torrents.zip -# test zip for corruption -unzip -t ${TEMP_DIR}/armbian-torrents.zip >/dev/null 2>&1 -[[ $? -ne 0 ]] && echo "Error in zip" && exit -# extract zip -unzip -o ${TEMP_DIR}/armbian-torrents.zip -d ${TEMP_DIR}/torrent-tmp >/dev/null 2>&1 -# create list of current active torrents -transmission-remote -n 'transmission:transmission' -l | sed '1d; $d' > ${TEMP_DIR}/torrent-tmp/active.torrents -# loop and add/update torrent files -for f in ${TEMP_DIR}/torrent-tmp/*.torrent; do - transmission-remote -n 'transmission:transmission' -a $f > /dev/null 2>&1 - # remove added from the list - pattern="${f//.torrent}"; pattern="${pattern##*/}"; - sed -i "/$pattern/d" ${TEMP_DIR}/torrent-tmp/active.torrents -done -# remove old armbian torrents -while read i; do - [[ $i == *Armbian_* ]] && transmission-remote -n 'transmission:transmission' -t $(echo "$i" | awk '{print $1}';) --remove-and-delete -done < ${TEMP_DIR}/torrent-tmp/active.torrents -# remove temporally files and direcotories -EOF -chmod +x /etc/cron.daily/seed-armbian-torrent -/etc/cron.daily/seed-armbian-torrent & -} - - - - -install_syncthing () -{ -# -# Install Personal cloud https://syncthing.net/ -# - -if [ "$(dpkg --print-architecture | grep armhf)" == "armhf" ]; then - local filename="linux-arm" -elif [ "$(dpkg --print-architecture | grep arm64)" == "arm64" ]; then - local filename="linux-arm64" -else - local filename="linux-amd64" -fi -mkdir -p /usr/bin/syncthing -wgeturl=$(curl -s "https://api.github.com/repos/syncthing/syncthing/releases" | grep $filename | grep 'browser_download_url' | head -1 | cut -d \" -f 4) -fancy_wget "$wgeturl" "-O ${TEMP_DIR}/syncthing.tgz" -wgeturl=$(curl -s "https://api.github.com/repos/syncthing/syncthing-inotify/releases" | grep $filename | grep 'browser_download_url' | head -1 | cut -d \" -f 4) -fancy_wget "$wgeturl" "-O ${TEMP_DIR}/syncthing-inotify.tgz" -tar xf ${TEMP_DIR}/syncthing.tgz -C ${TEMP_DIR} -tar xf ${TEMP_DIR}/syncthing-inotify.tgz -C /usr/bin -cp -R ${TEMP_DIR}/syncthing-*/syncthing /usr/bin -cp ${TEMP_DIR}/syncthing-*/etc/linux-systemd/system/syncthing* /etc/systemd/system/ -cp /etc/systemd/system/syncthing@.service /etc/systemd/system/syncthing-inotify@.service - -# adjust service for inotify -sed -i "s/^Description=.*/Description=Syncthing Inotify File Watcher/" /etc/systemd/system/syncthing-inotify@.service -sed -i "s/^After=.*/After=network.target syncthing.service/" /etc/systemd/system/syncthing-inotify@.service -sed -i "s/^ExecStart=.*/ExecStart=\/usr\/bin\/syncthing-inotify -logfile=\/var\/log\/syncthing-inotify.log -logflags=3/" /etc/systemd/system/syncthing-inotify@.service -sed -i "/^\[Install\]/a Requires=syncthing.service" /etc/systemd/system/syncthing-inotify@.service - -# increase open file limit -if !(grep -qs "fs.inotify.max_user_watches=204800" "/etc/sysctl.conf");then - echo -e "fs.inotify.max_user_watches=204800" | tee -a /etc/sysctl.conf -fi -add_choose_user -systemctl enable syncthing@${CHOSEN_USER}.service >/dev/null 2>&1 -systemctl start syncthing@${CHOSEN_USER}.service >/dev/null 2>&1 -systemctl enable syncthing-inotify@${CHOSEN_USER}.service >/dev/null 2>&1 -systemctl start syncthing-inotify@${CHOSEN_USER}.service >/dev/null 2>&1 -} - - - - -install_plex_media_server () -{ -# -# Media server -# -if [ "$(dpkg --print-architecture | grep armhf)" == "armhf" ]; then - echo -e "deb [arch=armhf] http://dev2day.de/pms/ stretch main" > /etc/apt/sources.list.d/plex.list - wget -q -O - http://dev2day.de/pms/dev2day-pms.gpg.key | apt-key add - >/dev/null 2>&1 - debconf-apt-progress -- apt-get update - debconf-apt-progress -- apt-get -y install plexmediaserver-installer -elif [ "$(dpkg --print-architecture | grep arm64)" == "arm64" ]; then - echo -e "deb [arch=armhf] http://dev2day.de/pms/ stretch main" > /etc/apt/sources.list.d/plex.list - wget -q -O - http://dev2day.de/pms/dev2day-pms.gpg.key | apt-key add - >/dev/null 2>&1 - debconf-apt-progress -- apt-get update - debconf-apt-progress -- apt-get -y install binutils:armhf plexmediaserver-installer:armhf -else - fancy_wget "https://downloads.plex.tv/plex-media-server/1.12.3.4973-215c28d86/plexmediaserver_1.12.3.4973-215c28d86_amd64.deb" "-O ${TEMP_DIR}/package.deb" - dpkg -i ${TEMP_DIR}/package.deb >/dev/null 2>&1 -fi -} - - - - -install_radarr () -{ -# -# Automatically downloading movies -# -debconf-apt-progress -- apt-get update -debconf-apt-progress -- apt-get -y install mono-devel mediainfo libmono-cil-dev -wgeturl=$(curl -s "https://api.github.com/repos/Radarr/Radarr/releases" | grep 'linux.tar.gz' | grep 'browser_download_url' | head -1 | cut -d \" -f 4) -fancy_wget "$wgeturl" "-O ${TEMP_DIR}/radarr.tgz" -tar xf ${TEMP_DIR}/radarr.tgz -C /opt -cat << _EOF_ > /etc/systemd/system/radarr.service -[Unit] -Description=Radarr Daemon -After=network.target -[Service] -User=root -Type=simple -ExecStart=/usr/bin/mono --debug /opt/Radarr/Radarr.exe -nobrowser -[Install] -WantedBy=multi-user.target -_EOF_ -systemctl enable radarr >/dev/null 2>&1 -systemctl start radarr -} - - - - -install_sonarr () -{ -# -# Automatically downloading TV shows -# -if [ "$(dpkg --print-architecture | grep arm64)" == "arm64" ]; then - debconf-apt-progress -- apt-get update - debconf-apt-progress -- apt-get -y install mono-complete mediainfo - fancy_wget "http://update.sonarr.tv/v2/develop/mono/NzbDrone.develop.tar.gz" "-O ${TEMP_DIR}/sonarr.tgz" - tar xf ${TEMP_DIR}/sonarr.tgz -C /opt -else - apt-key adv --keyserver keyserver.ubuntu.com --recv-keys FDA5DFFC >/dev/null 2>&1 - echo -e "deb https://apt.sonarr.tv/ develop main" > /etc/apt/sources.list.d/sonarr.list - debconf-apt-progress -- apt-get update - debconf-apt-progress -- apt-get -y install nzbdrone -fi -cat << _EOF_ > /etc/systemd/system/sonarr.service -[Unit] -Description=Sonarr (NzbDrone) Daemon -After=network.target -[Service] -User=root -Type=simple -ExecStart=/usr/bin/mono --debug /opt/NzbDrone/NzbDrone.exe -nobrowser -[Install] -WantedBy=multi-user.target -_EOF_ -systemctl enable sonarr >/dev/null 2>&1 -systemctl start sonarr -} - - - - -install_vpn_server () -{ -# -# Script downloads latest stable -# -cd ${TEMP_DIR} -PREFIX="http://www.softether-download.com/files/softether/" -install_packet "debconf-utils unzip build-essential html2text apt-transport-https" "Downloading basic packages" -URL=$(wget -q $PREFIX -O - | html2text | grep rtm | awk ' { print $(NF) }' | tail -1) -SUFIX="${URL/-tree/}" -if [ "$(dpkg --print-architecture | grep armhf)" != "" ]; then -DLURL=$PREFIX$URL"/Linux/SoftEther_VPN_Server/32bit_-_ARM_EABI/softether-vpnserver-$SUFIX-linux-arm_eabi-32bit.tar.gz" -else -install_packet "gcc-multilib" "Install libraries" -DLURL=$PREFIX$URL"/Linux/SoftEther_VPN_Server/32bit_-_Intel_x86/softether-vpnserver-$SUFIX-linux-x86-32bit.tar.gz" -fi -wget -q $DLURL -O - | tar -xz -cd vpnserver -make i_read_and_agree_the_license_agreement | dialog --backtitle "$BACKTITLE" --title "Compiling SoftEther VPN" --progressbox $TTY_Y $TTY_X -cd .. -cp -R vpnserver /usr/local -cd /usr/local/vpnserver/ -chmod 600 * -chmod 700 vpncmd -chmod 700 vpnserver -if [[ -d /run/systemd/system/ ]]; then -cat </lib/systemd/system/ethervpn.service -[Unit] -Description=VPN service - -[Service] -Type=oneshot -ExecStart=/usr/local/vpnserver/vpnserver start -ExecStop=/usr/local/vpnserver/vpnserver stop -RemainAfterExit=yes - -[Install] -WantedBy=multi-user.target -EOT -systemctl enable ethervpn.service -service ethervpn start - -else - -cat < /etc/init.d/vpnserver -#!/bin/sh -### BEGIN INIT INFO -# Provides: vpnserver -# Required-Start: \$remote_fs \$syslog -# Required-Stop: \$remote_fs \$syslog -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: Start daemon at boot time -# Description: Enable Softether by daemon. -### END INIT INFO -DAEMON=/usr/local/vpnserver/vpnserver -LOCK=/var/lock/vpnserver -test -x $DAEMON || exit 0 -case "\$1" in -start) -\$DAEMON start -touch \$LOCK -;; -stop) -\$DAEMON stop -rm \$LOCK -;; -restart) -\$DAEMON stop -sleep 3 -\$DAEMON start -;; -*) -echo "Usage: \$0 {start|stop|restart}" -exit 1 -esac -exit 0 -EOT -chmod 755 /etc/init.d/vpnserver -mkdir /var/lock/subsys -update-rc.d vpnserver defaults >> $logfile -/etc/init.d/vpnserver start -fi -} - - - - -install_vpn_client () -{ -# -# Script downloads latest stable -# -cd ${TEMP_DIR} -PREFIX="http://www.softether-download.com/files/softether/" -install_packet "debconf-utils unzip build-essential html2text apt-transport-https" "Downloading basic packages" -URL=$(wget -q $PREFIX -O - | html2text | grep rtm | awk ' { print $(NF) }' | tail -1) -SUFIX="${URL/-tree/}" -if [ "$(dpkg --print-architecture | grep armhf)" != "" ]; then -DLURL=$PREFIX$URL"/Linux/SoftEther_VPN_Client/32bit_-_ARM_EABI/softether-vpnclient-$SUFIX-linux-arm_eabi-32bit.tar.gz" -else -install_packet "gcc-multilib" "Install libraries" -DLURL=$PREFIX$URL"/Linux/SoftEther_VPN_Client/32bit_-_Intel_x86/softether-vpnclient-$SUFIX-linux-x86-32bit.tar.gz" -fi -wget -q $DLURL -O - | tar -xz -cd vpnclient -make i_read_and_agree_the_license_agreement | dialog --backtitle "$BACKTITLE" --title "Compiling SoftEther VPN vpnclient" --progressbox $TTY_Y $TTY_X -cd .. -cp -R vpnclient /usr/local -cd /usr/local/vpnclient/ -chmod 600 * -chmod 700 vpncmd -chmod 700 vpnclient -} - - - - -install_DashNTP () -{ -# -# Install DASH and ntp service -# -echo "dash dash/sh boolean false" | debconf-set-selections -dpkg-reconfigure -f noninteractive dash > /dev/null 2>&1 -install_packet "ntp ntpdate" "Install DASH and ntp service" -} - - - - -install_MySQL () -{ -# -# Maria SQL -# -install_packet "mariadb-client mariadb-server" "SQL client and server" -#Allow MySQL to listen on all interfaces -cp /etc/mysql/my.cnf /etc/mysql/my.cnf.backup -[[ -f /etc/mysql/my.cnf ]] && sed -i 's|bind-address.*|#bind-address = 127.0.0.1|' /etc/mysql/my.cnf -[[ -f /etc/mysql/mariadb.conf.d/50-server.cnf ]] && sed -i 's|bind-address.*|#bind-address = 127.0.0.1|' /etc/mysql/mariadb.conf.d/50-server.cnf -SECURE_MYSQL=$(expect -c " -set timeout 3 -spawn mysql_secure_installation -expect \"Enter current password for root (enter for none):\" -send \"\r\" -expect \"root password?\" -send \"y\r\" -expect \"New password:\" -send \"$MYSQL_PASS\r\" -expect \"Re-enter new password:\" -send \"$MYSQL_PASS\r\" -expect \"Remove anonymous users?\" -send \"y\r\" -expect \"Disallow root login remotely?\" -send \"y\r\" -expect \"Remove test database and access to it?\" -send \"y\r\" -expect \"Reload privilege tables now?\" -send \"y\r\" -expect eof -") -# -# Execution mysql_secure_installation -# -echo "${SECURE_MYSQL}" >> /dev/null -# ISP config exception -mkdir -p /etc/mysql/mariadb.conf.d/ -cat > /etc/mysql/mariadb.conf.d/99-ispconfig.cnf<<"EOF" -[mysqld] -sql-mode="NO_ENGINE_SUBSTITUTION" -EOF -service mysql restart >> /dev/null -} - - - - -install_MySQLDovecot () -{ -# -# Install Postfix, Dovecot, Saslauthd, rkhunter, binutils -# -echo "postfix postfix/main_mailer_type select Internet Site" | debconf-set-selections -echo "postfix postfix/mailname string $HOSTNAMEFQDN" | debconf-set-selections -install_packet "postfix postfix-mysql postfix-doc openssl getmail4 rkhunter binutils dovecot-imapd dovecot-pop3d dovecot-mysql \ -dovecot-sieve sudo libsasl2-modules" "postfix, dovecot, saslauthd, rkhunter, binutils" -#Uncommenting some Postfix configuration files -cp /etc/postfix/master.cf /etc/postfix/master.cf.backup -sed -i 's|#submission inet n - - - - smtpd|submission inet n - - - - smtpd|' /etc/postfix/master.cf -sed -i 's|# -o syslog_name=postfix/submission| -o syslog_name=postfix/submission|' /etc/postfix/master.cf -sed -i 's|# -o smtpd_tls_security_level=encrypt| -o smtpd_tls_security_level=encrypt|' /etc/postfix/master.cf -sed -i 's|# -o smtpd_sasl_auth_enable=yes| -o smtpd_sasl_auth_enable=yes|' /etc/postfix/master.cf -sed -i 's|# -o smtpd_client_restrictions=permit_sasl_authenticated,reject| -o smtpd_client_restrictions=permit_sasl_authenticated,reject|' /etc/postfix/master.cf -sed -i 's|# -o smtpd_sasl_auth_enable=yes| -o smtpd_sasl_auth_enable=yes|' /etc/postfix/master.cf -sed -i 's|# -o smtpd_sasl_auth_enable=yes| -o smtpd_sasl_auth_enable=yes|' /etc/postfix/master.cf -sed -i 's|# -o smtpd_sasl_auth_enable=yes| -o smtpd_sasl_auth_enable=yes|' /etc/postfix/master.cf -sed -i 's|#smtps inet n - - - - smtpd|smtps inet n - - - - smtpd|' /etc/postfix/master.cf -sed -i 's|# -o syslog_name=postfix/smtps| -o syslog_name=postfix/smtps|' /etc/postfix/master.cf -sed -i 's|# -o smtpd_tls_wrappermode=yes| -o smtpd_tls_wrappermode=yes|' /etc/postfix/master.cf -service postfix restart >> /dev/null -} - - - - -install_Virus () -{ -# -# Install Amavisd-new, SpamAssassin, And Clamav -# -install_packet "amavisd-new spamassassin clamav clamav-daemon zoo unzip bzip2 arj p7zip unrar-free ripole rpm nomarch lzop \ -cabextract apt-listchanges libnet-ldap-perl libauthen-sasl-perl clamav-docs daemon libio-string-perl libio-socket-ssl-perl \ -libnet-ident-perl zip libnet-dns-perl postgrey" "amavisd, spamassassin, clamav" -sed -i "s/^AllowSupplementaryGroups.*/AllowSupplementaryGroups true/" /etc/clamav/clamd.conf -service spamassassin stop >/dev/null 2>&1 -systemctl disable spamassassin >/dev/null 2>&1 -} - - - - -install_hhvm () -{ -# -# Install HipHop Virtual Machine -# -apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0xB4112585D386EB94 >/dev/null 2>&1 -add-apt-repository https://dl.hhvm.com/"${family,,}" >/dev/null 2>&1 -debconf-apt-progress -- apt-get update -install_packet "hhvm" "HipHop Virtual Machine" -} - - - - -install_phpmyadmin () -{ -# -# Phpmyadmin unattended installation -# -if [[ "$family" != "Ubuntu" ]]; then -DEBIAN_FRONTEND=noninteractive debconf-apt-progress -- apt-get -y install phpmyadmin -else -debconf-set-selections <<< "phpmyadmin phpmyadmin/internal/skip-preseed boolean true" -debconf-set-selections <<< "phpmyadmin phpmyadmin/reconfigure-webserver multiselect true" -debconf-set-selections <<< "phpmyadmin phpmyadmin/dbconfig-install boolean false" -echo "phpmyadmin phpmyadmin/internal/skip-preseed boolean true" | debconf-set-selections -echo "phpmyadmin phpmyadmin/reconfigure-webserver multiselect" | debconf-set-selections -echo "phpmyadmin phpmyadmin/dbconfig-install boolean false" | debconf-set-selections -debconf-apt-progress -- apt-get install -y phpmyadmin -fi -} - - - - -install_apache () -{ -# -# Install Apache2, PHP5, FCGI, suExec, Pear and mcrypt -# - -local pkg="apache2 apache2-doc apache2-utils libapache2-mod-fcgid php-pear mcrypt imagemagick libruby libapache2-mod-python memcached" - -local pkg_xenial="libapache2-mod-php php7.0 php7.0-common php7.0-gd php7.0-mysql php7.0-imap php7.0-cli php7.0-cgi \ -apache2-suexec-pristine php-auth php7.0-mcrypt php7.0-curl php7.0-intl php7.0-pspell php7.0-recode php7.0-sqlite3 php7.0-tidy \ -php7.0-xmlrpc php7.0-xsl php-memcache php-imagick php-gettext php7.0-zip php7.0-mbstring php7.0-opcache php-apcu \ -libapache2-mod-fastcgi php7.0-fpm letsencrypt" - -local pkg_stretch="libapache2-mod-php php7.0 php7.0-common php7.0-gd php7.0-mysql php7.0-imap php7.0-cli php7.0-cgi libapache2-mod-fcgid \ -apache2-suexec-pristine php7.0-mcrypt libapache2-mod-python php7.0-curl php7.0-intl php7.0-pspell php7.0-recode php7.0-sqlite3 \ -php7.0-tidy php7.0-xmlrpc php7.0-xsl php-memcache php-imagick php-gettext php7.0-zip php7.0-mbstring libapache2-mod-passenger \ -php7.0-soap php7.0-fpm php7.0-opcache php-apcu certbot" - -local pkg_jessie="apache2.2-common apache2-mpm-prefork libexpat1 ssl-cert libapache2-mod-php5 php5 php5-common php5-gd php5-mysql \ -php5-imap php5-cli php5-cgi libapache2-mod-fcgid apache2-suexec php-pear php-auth php5-mcrypt mcrypt php5-imagick libapache2-mod-python \ -php5-curl php5-intl php5-memcache php5-memcached php5-pspell php5-recode php5-sqlite php5-tidy php5-xmlrpc php5-xsl \ -libapache2-mod-passenger php5-xcache libapache2-mod-fastcgi php5-fpm" - -local temp="pkg_${distribution}" -install_packet "${pkg} ${!temp}" "Apache for $family $distribution" -# fix HTTPOXY vulnerability -cat < /etc/apache2/conf-available/httpoxy.conf - - RequestHeader unset Proxy early - - -EOT - -a2enmod actions proxy_fcgi setenvif fastcgi alias httpoxy suexec rewrite ssl actions include dav_fs dav auth_digest cgi headers >/dev/null 2>&1 -a2enconf php7.0-fpm >/dev/null 2>&1 -service apache2 restart >> /dev/null -} - - - - -install_nginx () -{ -# -# Install NginX, PHP5, FCGI, suExec, Pear, And mcrypt -# -local pkg="nginx php-pear memcached fcgiwrap" - -local pkg_xenial="php7.0-fpm php7.0-opcache php7.0-fpm php7.0 php7.0-common php7.0-gd php7.0-mysql php7.0-imap php7.0-cli php7.0-cgi \ -php7.0-mcrypt mcrypt imagemagick libruby php7.0-curl php7.0-intl php7.0-pspell php7.0-recode php7.0-sqlite3 php7.0-tidy \ -php7.0-xmlrpc php7.0-xsl php-memcache php-imagick php-gettext php7.0-zip php7.0-mbstring php-apcu" - -local pkg_stretch="php7.0-fpm php7.0-opcache php7.0-fpm php7.0 php7.0-common php7.0-gd php7.0-mysql php7.0-imap php7.0-cli php7.0-cgi \ -php7.0-mcrypt mcrypt imagemagick libruby php7.0-curl php7.0-intl php7.0-pspell php7.0-recode php7.0-sqlite3 php7.0-tidy \ -php7.0-xmlrpc php7.0-xsl php-memcache php-imagick php-gettext php7.0-zip php7.0-mbstring php-apcu" - -local pkg_jessie="php5-fpm php5-mysql php5-curl php5-gd php5-intl php5-imagick php5-imap php5-mcrypt php5-memcache \ -php5-memcached php5-pspell php5-recode php5-snmp php5-sqlite php5-tidy php5-xmlrpc php5-xsl php-apc" - -local temp="pkg_${distribution}" -install_packet "${pkg} ${!temp}" "Nginx for $family $distribution" - -phpenmod mcrypt mbstring - -if [[ -f /etc/php/7.0/fpm/php.ini ]]; then - tz=$(cat /etc/timezone | sed 's/\//\\\//g') - sed -i "s/^cgi.fix_pathinfo=.*/cgi.fix_pathinfo=0/" /etc/php/7.0/fpm/php.ini - sed -i "s/^date.timezone=.*/date.timezone=""$tz""/" /etc/php/7.0/fpm/php.ini - service php7.0-fpm reload >> /dev/null -else - debconf-apt-progress -- apt-get install -y python-certbot -t jessie-backports - service php5-fpm reload >> /dev/null -fi -} - - - - -install_PureFTPD () -{ -# -# Install PureFTPd and Quota -# -install_packet "pure-ftpd-common pure-ftpd-mysql quota quotatool" "pureFTPd and Quota" - -sed -i 's/VIRTUALCHROOT=false/VIRTUALCHROOT=true/' /etc/default/pure-ftpd-common -echo 1 > /etc/pure-ftpd/conf/TLS -mkdir -p /etc/ssl/private/ -openssl req -x509 -nodes -days 7300 -newkey rsa:2048 -subj "/C=GB/ST=GB/L=GB/O=GB/OU=GB/CN=$(hostname -f)/emailAddress=joe@joe.com" -keyout /etc/ssl/private/pure-ftpd.pem -out /etc/ssl/private/pure-ftpd.pem >/dev/null 2>&1 -chmod 600 /etc/ssl/private/pure-ftpd.pem -/etc/init.d/pure-ftpd-mysql restart >/dev/null 2>&1 -local temp=$(cat /etc/fstab | grep "/ " | tail -1 | awk '{print $4}') -sed -i "s/$temp/$temp,usrjquota=quota.user,grpjquota=quota.group,jqfmt=vfsv0/" /etc/fstab -mount -o remount / >/dev/null 2>&1 -quotacheck -avugm >/dev/null 2>&1 -quotaon -avug >/dev/null 2>&1 -} - - - - -install_Bind () -{ -# -# Install BIND DNS Server -# -install_packet "bind9 dnsutils" "Install BIND DNS Server" -} - - - - -install_Stats () -{ -# -# Install Vlogger, Webalizer, And AWstats -# -install_packet "vlogger webalizer awstats geoip-database libclass-dbi-mysql-perl" "vlogger, webalizer, awstats" -sed -i "s/*/10 * * * * www-data/#*/10 * * * * www-data/" /etc/cron.d/awstats -sed -i "s/10 03 * * * www-data/#10 03 * * * www-data/" /etc/cron.d/awstats -} - - - - -install_Jailkit() -{ -# -debconf-apt-progress -- apt-get install -y build-essential autoconf automake libtool flex bison debhelper binutils -cd ${TEMP_DIR} -wget -q http://olivier.sessink.nl/jailkit/jailkit-2.19.tar.gz -O - | tar -xz && cd jailkit-2.19 -echo 5 > debian/compat -./debian/rules binary > /dev/null 2>&1 -dpkg -i ../jailkit_2.19-1_*.deb > /dev/null 2>&1 -} - - - - -install_Fail2BanDovecot() -{ -# -# Install fail2ban -# -install_packet "fail2ban ufw" "Install fail2ban and UFW Firewall" -if [[ $distribution == "stretch" ]]; then -cat > /etc/fail2ban/jail.local <<"EOF" -[pure-ftpd] -enabled = true -port = ftp -filter = pure-ftpd -logpath = /var/log/syslog -maxretry = 3 - -[dovecot] -enabled = true -filter = dovecot -logpath = /var/log/mail.log -maxretry = 5 - -[postfix-sasl] -enabled = true -port = smtp -filter = postfix-sasl -logpath = /var/log/mail.log -maxretry = 3 -EOF -else -cat > /etc/fail2ban/jail.local <<"EOF" -[pureftpd] -enabled = true -port = ftp -filter = pureftpd -logpath = /var/log/syslog -maxretry = 3 - -[dovecot-pop3imap] -enabled = true -filter = dovecot-pop3imap -action = iptables-multiport[name=dovecot-pop3imap, port="pop3,pop3s,imap,imaps", protocol=tcp] -logpath = /var/log/mail.log -maxretry = 5 - -[sasl] -enabled = true -port = smtp -filter = postfix-sasl -logpath = /var/log/mail.log -maxretry = 3 -EOF -fi -} - - - - -install_Fail2BanRulesDovecot() -{ -# -# Dovecot rules -# -cat > /etc/fail2ban/filter.d/pureftpd.conf <<"EOF" -[Definition] -failregex = .*pure-ftpd: \(.*@\) \[WARNING\] Authentication failed for user.* -ignoreregex = -EOF - -cat > /etc/fail2ban/filter.d/dovecot-pop3imap.conf <<"EOF" -[Definition] -failregex = (?: pop3-login|imap-login): .*(?:Authentication failure|Aborted login \(auth failed|Aborted login \(tried to use disabled|Disconnected \(auth failed|Aborted login \(\d+ authentication attempts).*rip=(?P\S*),.* -ignoreregex = -EOF -# Add the missing ignoreregex line -echo "ignoreregex =" >> /etc/fail2ban/filter.d/postfix-sasl.conf -service fail2ban restart >> /dev/null -} - - - - -install_ISPConfig (){ -#------------------------------------------------------------------------------------------------------------------------------------------ -# Install ISPConfig 3 -#------------------------------------------------------------------------------------------------------------------------------------------ -cd ${TEMP_DIR} -wget -q http://www.ispconfig.org/downloads/ISPConfig-3-stable.tar.gz -O - | tar -xz -cd ${TEMP_DIR}/ispconfig3_install/install/ -#apt-get -y install php5-cli php5-mysql -php -q install.php --autoinstall=${TEMP_DIR}/isp.conf.php -echo "Admin panel: https://$serverIP:8080" -echo "PHPmyadmin: http://$serverIP:8081/phpmyadmin" -} - - -install_mayan_edms (){ -# -# Install Mayan EDMS -# - -# Default values -MAYAN_DATABASE_PASSWORD="mayandbpass" -MAYAN_INSTALLATION_FOLDER="/opt/mayan-edms" -MAYAN_MEDIA_ROOT="/opt/mayan-edms-data" - -# User interaction -exec 3>&1 -dialog --title "Server configuration" --separate-widget $'\n' \ ---ok-label "Install" --backtitle "$BACKTITLE" \ ---form "\nPlease fill out this form:\n " 13 70 0 \ -"Ddatabase password:" 1 1 "$MAYAN_DATABASE_PASSWORD" 1 31 32 0 \ -"Installation folder:" 2 1 "$MAYAN_INSTALLATION_FOLDER" 2 31 32 0 \ -"Data folder:" 3 1 "$MAYAN_MEDIA_ROOT" 3 31 32 0 \ -2>&1 1>&3 | { -read -r MAYAN_DATABASE_PASSWORD -read -r MAYAN_MEDIA_ROOT -read -r MAYAN_INSTALLATION_FOLDER -echo $MAYAN_DATABASE_PASSWORD > ${TEMP_DIR}/MAYAN_DATABASE_PASSWORD -echo $MAYAN_MEDIA_ROOT > ${TEMP_DIR}/MAYAN_MEDIA_ROOT -echo $MAYAN_INSTALLATION_FOLDER > ${TEMP_DIR}/MAYAN_INSTALLATION_FOLDER -} -exec 3>&- -read MAYAN_DATABASE_PASSWORD < ${TEMP_DIR}/MAYAN_DATABASE_PASSWORD -read MAYAN_MEDIA_ROOT < ${TEMP_DIR}/MAYAN_MEDIA_ROOT -read MAYAN_INSTALLATION_FOLDER < ${TEMP_DIR}/MAYAN_INSTALLATION_FOLDER - -# OS dependencies -install_packet "g++ gcc ghostscript gnupg1 graphviz libffi-dev libjpeg-dev libmagic1 libpq-dev libpng-dev libreoffice libssl-dev libtiff-dev poppler-utils postgresql python-dev python-pip python-virtualenv redis-server sane-utils supervisor tesseract-ocr zlib1g-dev" "Installing dependencies" - -# Mayan OS user account -dialog --infobox "Adding Mayan EDMS user account" 3 70 -adduser mayan --disabled-password --disabled-login --no-create-home --gecos "" >/dev/null 2>&1 -sleep 1 - -# Create installtion and data folders -mkdir -p "${MAYAN_INSTALLATION_FOLDER}" -mkdir -p "${MAYAN_MEDIA_ROOT}" - -# Create the Python virtualenv to isolate Python dependencies of Mayan -dialog --infobox "Creating Python virtual environment" 3 70 -python /usr/lib/python2.7/dist-packages/virtualenv.py $MAYAN_INSTALLATION_FOLDER > /dev/null - -# Give ownership to the Mayan OS user -chown mayan:mayan "${MAYAN_INSTALLATION_FOLDER}" -R -chown mayan:mayan "${MAYAN_MEDIA_ROOT}" -R - -# Pillow can't find zlib or libjpeg on aarch64 (ODROID C2) -if [ "$(uname -m)" = "aarch64" ]; then \ - ln -s /usr/lib/aarch64-linux-gnu/libz.so /usr/lib/ && \ - ln -s /usr/lib/aarch64-linux-gnu/libjpeg.so /usr/lib/ \ -; fi - -# Pillow can't find zlib or libjpeg on armv7l (ODROID HC1) -if [ "$(uname -m)" = "armv7l" ]; then \ - ln -s /usr/lib/arm-linux-gnueabihf/libz.so /usr/lib/ && \ - ln -s /usr/lib/arm-linux-gnueabihf/libjpeg.so /usr/lib/ \ -; fi - -# Install Mayan from the web and all its Python dependencies -MAYAN_PIP=$MAYAN_INSTALLATION_FOLDER/bin/pip -dialog --infobox "Installing Mayan EDMS Python package (Takes several minutes)" 3 70 -sudo -u mayan $MAYAN_PIP install --no-cache-dir mayan-edms > /dev/null 2>&1 - -# Python Postgres driver -dialog --infobox "Installing PostgreSQL database driver" 3 70 -sudo -u mayan $MAYAN_PIP install --no-cache-dir psycopg2==2.7.3.2 > /dev/null - -# Python Redis driver -dialog --infobox "Installing Redis driver" 3 70 -sudo -u mayan $MAYAN_PIP install --no-cache-dir redis==2.10.6 > /dev/null - -# Create postgres Mayan user and database -MAYAN_BIN=$MAYAN_INSTALLATION_FOLDER/bin/mayan-edms.py -dialog --infobox "Creating and initializing database (Takes several minutes)" 3 70 -sudo -u postgres psql -c "CREATE USER mayan WITH password '$MAYAN_DATABASE_PASSWORD';" -sudo -u postgres createdb -O mayan mayan - -# Execute initialsetup command. Migrate DB, create base files, downloads Javascript libraries -sudo -u mayan \ - MAYAN_DATABASE_ENGINE=django.db.backends.postgresql \ - MAYAN_DATABASE_NAME=mayan \ - MAYAN_DATABASE_USER=mayan \ - MAYAN_DATABASE_HOST=127.0.0.1 \ - MAYAN_MEDIA_ROOT=$MAYAN_MEDIA_ROOT \ - MAYAN_DATABASE_PASSWORD=$MAYAN_DATABASE_PASSWORD \ - $MAYAN_BIN initialsetup --force > /dev/null - -# Compress and merge Javascript, CSS for web serving -dialog --infobox "Preparing static files" 3 70 -sudo -u mayan \ - MAYAN_MEDIA_ROOT=$MAYAN_MEDIA_ROOT \ - $MAYAN_BIN preparestatic --noinput > /dev/null - -# Create supervisor file for gunicorn (frontend), 3 background workers, and the scheduler for periodic tasks -cat > /etc/supervisor/conf.d/mayan.conf <> /etc/redis/redis.conf - -# This starts all of Mayan's processes -dialog --infobox "Starting service" 3 70 -systemctl restart supervisor.service - -# Installation report -dialog --msgbox "Installation complete.\nInstallation folder: $MAYAN_INSTALLATION_FOLDER\nData folder: $MAYAN_MEDIA_ROOT\nPort: 8000" 10 70 -} - - -#------------------------------------------------------------------------------------------------------------------------------------------ -# Main choices -#------------------------------------------------------------------------------------------------------------------------------------------ - -# check for root -# -if [[ $EUID != 0 ]]; then - dialog --title "Warning" --infobox "\nThis script requires root privileges.\n\nExiting ..." 7 41 - sleep 3 - exit -fi - -# nameserver backup -if [ -d /etc/resolvconf/resolv.conf.d ]; then - echo 'nameserver 8.8.8.8' > /etc/resolvconf/resolv.conf.d/head - resolvconf -u -fi - -# Create a safe temporary directory -TEMP_DIR=$(mktemp -d || exit 1) -chmod 700 ${TEMP_DIR} -trap "rm -rf \"${TEMP_DIR}\" ; exit 0" 0 1 2 3 15 - -# Install basic stuff, we have to wait for other apt tasks to finish -# (eg unattended-upgrades) -i=0 -tput sc -while fuser /var/lib/dpkg/lock >/dev/null 2>&1 ; do - case $(($i % 4)) in - 0 ) j="-" ;; - 1 ) j="\\" ;; - 2 ) j="|" ;; - 3 ) j="/" ;; - esac - tput rc - echo -en "\r[$j] Waiting for other software managers to finish..." - sleep 0.5 - ((i=i+1)) -done - -apt-get -qq -y --no-install-recommends install debconf-utils html2text apt-transport-https dialog whiptail lsb-release bc expect > /dev/null - -# gather some info -# -TTY_X=$(($(stty size | awk '{print $2}')-6)) # determine terminal width -TTY_Y=$(($(stty size | awk '{print $1}')-6)) # determine terminal height -distribution=$(lsb_release -cs) -family=$(lsb_release -is) -serverIP=$(ip route get 8.8.8.8 | awk '{ print $NF; exit }') -set ${serverIP//./ } -SUBNET="$1.$2.$3." -hostnamefqdn=$(hostname -f) -mysql_pass="" -BACKTITLE="Softy - Armbian post deployment scripts, http://www.armbian.com" -SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -#check_status - -# main dialog routine -# -DIALOG_CANCEL=1 -DIALOG_ESC=255 - -while true; do - - # prepare menu items - check_status - LISTLENGHT="$((${#LIST[@]}/2))" - exec 3>&1 - selection=$(dialog --backtitle "$BACKTITLE" --title "Installing to $family $distribution" --colors --clear --cancel-label \ - "Exit" --checklist "\nChoose what you want to install:\n " $(($LISTLENGHT+$LIST_CONST)) 70 15 "${LIST[@]}" 2>&1 1>&3) - exit_status=$? - exec 3>&- - case $exit_status in - $DIALOG_ESC | $DIALOG_CANCEL) - clear - exit 1 - ;; - esac - - # cycle trought all install options - i=0 - - while [ "$i" -lt "$LISTLENGHT" ]; do - - if [[ "$selection" == *Samba* && "$SAMBA_STATUS" != "on" ]]; then - install_samba - selection=${selection//Samba/} - fi - - if [[ "$selection" == *CUPS* && "$CUPS_STATUS" != "on" ]]; then - install_cups - selection=${selection//CUPS/} - fi - - if [[ "$selection" == *headend* && "$TVHEADEND_STATUS" != "on" ]]; then - install_tvheadend - selection=${selection//\"TV headend\"/} - fi - - if [[ "$selection" == *Minidlna* && "$MINIDLNA_STATUS" != "on" ]]; then - install_packet "minidlna" "Install lightweight DLNA/UPnP-AV server" - selection=${selection//Minidlna/} - fi - - if [[ "$selection" == *ISPConfig* && "$ISPCONFIG_STATUS" != "on" ]]; then - server_conf - if [[ "$MYSQL_PASS" == "" ]]; then - dialog --msgbox "Mysql password can't be blank. Exiting..." 7 70 - exit - fi - if [[ "$(echo $HOSTNAMEFQDN | grep -P '(?=^.{1,254}$)(^(?>(?!\d+\.)[a-zA-Z0-9_\-]{1,63}\.?)+(?:[a-zA-Z]{2,})$)')" == "" ]]; then - dialog --msgbox "Invalid FQDN. Exiting..." 7 70 - exit - fi - choose_webserver; install_basic; install_DashNTP; install_MySQL; install_MySQLDovecot; install_Virus; install_$server; - install_phpmyadmin - [[ -z "$(dpkg --print-architecture | grep arm)" ]] && install_hhvm - create_ispconfig_configuration;install_PureFTPD; - install_Jailkit; install_Fail2BanDovecot; install_Fail2BanRulesDovecot; - install_ISPConfig - read -n 1 -s -p "Press any key to continue" - selection=${selection//ISPConfig/} - fi - - if [[ "$selection" == *Syncthing* && "$SYNCTHING_STATUS" != "on" ]]; then - install_syncthing - selection=${selection//Syncthing/} - fi - - if [[ "$selection" == *ExaGear* && "$EXAGEAR_STATUS" != "on" ]]; then - debconf-apt-progress -- apt-get update - debconf-apt-progress -- apt-get -y install exagear-armbian exagear-desktop exagear-dsound-server exagear-guest-ubuntu-1604 - selection=${selection//ExaGear/} - fi - - if [[ "$selection" == *server* && "$VPN_SERVER_STATUS" != "on" ]]; then - install_vpn_server - selection=${selection//\"VPN server\"/} - fi - - if [[ "$selection" == *client* && "$VPN_CLIENT_STATUS" != "on" ]]; then - install_vpn_client - selection=${selection//\"VPN client\"/} - fi - if [[ "$selection" == *NCP* && "$NCP_STATUS" != "on" ]]; then - install_ncp - selection=${selection//NCP/} - fi - - if [[ "$selection" == *OMV* && "$OMV_STATUS" != "on" ]]; then - install_omv - selection=${selection//OMV/} - fi - - if [[ "$selection" == *Plex* && "$PLEX_STATUS" != "on" ]]; then - install_plex_media_server - selection=${selection//Plex/} - fi - - if [[ "$selection" == *Radarr* && "$RADARR_STATUS" != "on" ]]; then - install_radarr - selection=${selection//Radarr/} - fi - - if [[ "$selection" == *Sonarr* && "$SONARR_STATUS" != "on" ]]; then - install_sonarr - selection=${selection//Sonarr/} - fi - - if [[ "$selection" == *hole* && "$PI_HOLE_STATUS" != "on" ]]; then - curl -L "https://install.pi-hole.net" | bash - selection=${selection//\"Pi hole\"/} - fi - - if [[ "$selection" == *Transmission* && "$TRANSMISSION_STATUS" != "on" ]]; then - install_transmission - selection=${selection//Transmission/} - dialog --title "Seed Armbian torrents" --backtitle "$BACKTITLE" --yes-label "Yes" --no-label "Cancel" --yesno "\ - \nDo you want to help community and seed armbian torrent files? It will ensure faster download for everyone.\ - \n\nWe need around 80Gb of your space." 11 44 - if [[ $? = 0 ]]; then - install_transmission_seed_armbian_torrents - fi - fi - - if [[ "$selection" == *UrBackup* && "$URBACKUP_STATUS" != "on" ]]; then - install_urbackup - selection=${selection//UrBackup/} - fi - - if [[ "$selection" == *Mayan* && "$MAYAN_STATUS" != "on" ]]; then - install_mayan_edms - selection=${selection//\"Mayan EDMS\"/} - fi - - i=$[$i+1] - done - # reread statuses - check_status -done - - - diff --git a/contrib/scripts/install/docker.sh b/contrib/scripts/install/docker.sh index 8e851725ca..af81926b8f 100755 --- a/contrib/scripts/install/docker.sh +++ b/contrib/scripts/install/docker.sh @@ -5,21 +5,25 @@ set -e # $ curl -fsSL get.mayan-edms.com -o get-mayan-edms.sh # $ sh get-mayan-edms.sh # -# NOTE: Make sure to verify the contents of the script +# NOTE: Before executing, make sure to verify the contents of the script # you downloaded matches the contents of docker.sh # located at https://gitlab.com/mayan-edms/mayan-edms/blob/master/contrib/scripts/install/docker.sh -# before executing. : ${VERBOSE:=true} : ${INSTALL_DOCKER:=false} : ${DELETE_VOLUMES:=false} +: ${USE_DOCKER_NETWORK:=true} +: ${DOCKER_NETWORK_NAME:=mayan} : ${DATABASE_USER:=mayan} : ${DATABASE_NAME:=mayan} : ${DATABASE_PASSWORD:=mayanuserpass} -: ${DOCKER_POSTGRES_IMAGE:=postgres:9.6} +: ${DOCKER_POSTGRES_IMAGE:=postgres:9.6-alpine} : ${DOCKER_POSTGRES_CONTAINER:=mayan-edms-postgres} : ${DOCKER_POSTGRES_VOLUME:=/docker-volumes/mayan-edms/postgres} : ${DOCKER_POSTGRES_PORT:=5432} +: ${DOCKER_REDIS_IMAGE:=redis:5.0-alpine} +: ${DOCKER_REDIS_CONTAINER:=mayan-edms-redis} +: ${DOCKER_REDIS_PORT:=6379} : ${DOCKER_MAYAN_IMAGE:=mayanedms/mayanedms:latest} : ${DOCKER_MAYAN_CONTAINER:=mayan-edms} : ${DOCKER_MAYAN_VOLUME:=/docker-volumes/mayan-edms/media} @@ -44,6 +48,8 @@ echo "Variable values to be used:" echo "---------------------------" echo "INSTALL_DOCKER: $INSTALL_DOCKER" echo "DELETE_VOLUMES: $DELETE_VOLUMES" +echo "USE_DOCKER_NETWORK: $USE_DOCKER_NETWORK" +echo "DOCKER_NETWORK_NAME: $DOCKER_NETWORK_NAME" echo "DATABASE_USER: $DATABASE_USER" echo "DATABASE_NAME: $DATABASE_NAME" echo "DATABASE_PASSWORD: $DATABASE_PASSWORD" @@ -51,10 +57,17 @@ echo "DOCKER_POSTGRES_IMAGE: $DOCKER_POSTGRES_IMAGE" echo "DOCKER_POSTGRES_CONTAINER: $DOCKER_POSTGRES_CONTAINER" echo "DOCKER_POSTGRES_VOLUME: $DOCKER_POSTGRES_VOLUME" echo "DOCKER_POSTGRES_PORT: $DOCKER_POSTGRES_PORT" +echo "DOCKER_REDIS_IMAGE: $DOCKER_REDIS_IMAGE" +echo "DOCKER_REDIS_CONTAINER: $DOCKER_REDIS_CONTAINER" +echo "DOCKER_REDIS_PORT: $DOCKER_REDIS_PORT" echo "DOCKER_MAYAN_IMAGE: $DOCKER_MAYAN_IMAGE" echo "DOCKER_MAYAN_CONTAINER: $DOCKER_MAYAN_CONTAINER" echo "DOCKER_MAYAN_VOLUME: $DOCKER_MAYAN_VOLUME" -echo "\nStarting in 10 seconds." +echo +echo "Override any of them by setting them before the script. " +echo "Example: INSTALL_DOCKER=true sh get-mayan-edms.sh" + +echo "\nStarting in 10 seconds. Press CTRL+C to cancel." sleep 10 fi @@ -72,33 +85,62 @@ if [ -z `which docker` ]; then fi echo -n "* Removing existing Mayan EDMS and PostgreSQL containers (no data will be lost)..." -true || docker stop $DOCKER_MAYAN_CONTAINER >/dev/null 2>&1 -true || docker rm $DOCKER_MAYAN_CONTAINER >/dev/null 2>&1 -true || docker stop $DOCKER_POSTGRES_CONTAINER >/dev/null 2>&1 -true || docker rm $DOCKER_POSTGRES_CONTAINER >/dev/null 2>&1 +docker rm -f $DOCKER_REDIS_CONTAINER >/dev/null 2>&1 || true +docker rm -f $DOCKER_POSTGRES_CONTAINER >/dev/null 2>&1 || true +docker rm -f $DOCKER_MAYAN_CONTAINER >/dev/null 2>&1 || true echo "Done" if [ "$DELETE_VOLUMES" = true ]; then -echo -n "* Deleting Docker volumes in 5 seconds (warning: this delete all document data)..." +echo -n "* Deleting Docker volumes in 5 seconds (warning: this will delete all document data). Press CTRL+C to cancel..." sleep 5 -true || rm DOCKER_MAYAN_VOLUME -Rf -true || rm DOCKER_POSTGRES_VOLUME -Rf +rm DOCKER_MAYAN_VOLUME -Rf || true +rm DOCKER_POSTGRES_VOLUME -Rf || true echo "Done" fi -echo -n "* Pulling (downloading) the Mayan EDMS Docker image..." -docker pull $DOCKER_MAYAN_IMAGE >/dev/null +echo -n "* Pulling (downloading) the Redis Docker image..." +docker pull $DOCKER_REDIS_IMAGE > /dev/null echo "Done" echo -n "* Pulling (downloading) the PostgreSQL Docker image..." docker pull $DOCKER_POSTGRES_IMAGE > /dev/null echo "Done" +echo -n "* Pulling (downloading) the Mayan EDMS Docker image..." +docker pull $DOCKER_MAYAN_IMAGE >/dev/null +echo "Done" + +if [ "$USE_DOCKER_NETWORK" = true ]; then + echo -n "* Creating Docker network..." + docker network create $DOCKER_NETWORK_NAME 2> /dev/null || true + # Ignore error if the network already exists + echo "Done" +fi + +if [ "$USE_DOCKER_NETWORK" = true ]; then + NETWORK_ARGUMENT="--network=$DOCKER_NETWORK_NAME" + POSTGRES_PORT_ARGUMENT="" + REDIS_PORT_ARGUMENT="" + MAYAN_DATABASE_PORT_ARGUMENT="" + MAYAN_DATABASE_HOST_ARGUMENT="-e MAYAN_DATABASE_HOST=$DOCKER_POSTGRES_CONTAINER" + MAYAN_BROKER_URL_ARGUMENT="-e MAYAN_BROKER_URL=redis://$DOCKER_REDIS_CONTAINER:6379/0" + MAYAN_CELERY_RESULT_BACKEND_ARGUMENT="-e MAYAN_CELERY_RESULT_BACKEND=redis://$DOCKER_REDIS_CONTAINER:6379/1" +else + NETWORK_ARGUMENT="" + POSTGRES_PORT_ARGUMENT="-e $DOCKER_POSTGRES_PORT:5432" + REDIS_PORT_ARGUMENT="-e $DOCKER_REDIS_PORT:6379" + MAYAN_DATABASE_PORT_ARGUMENT="-e MAYAN_DATABASE_PORT=$DOCKER_POSTGRES_PORT" + MAYAN_DATABASE_HOST_ARGUMENT="-e MAYAN_DATABASE_HOST=172.17.0.1" + MAYAN_BROKER_URL_ARGUMENT="-e MAYAN_BROKER_URL=redis://172.17.0.1:6379/0" + MAYAN_CELERY_RESULT_BACKEND_ARGUMENT="-e MAYAN_CELERY_RESULT_BACKEND=redis://172.17.0.1:6379/1" +fi + echo -n "* Deploying the PostgreSQL container..." docker run -d \ --name $DOCKER_POSTGRES_CONTAINER \ +$NETWORK_ARGUMENT \ --restart=always \ --p $DOCKER_POSTGRES_PORT:5432 \ +$POSTGRES_PORT_ARGUMENT \ -e POSTGRES_USER=$DATABASE_USER \ -e POSTGRES_DB=$DATABASE_NAME \ -e POSTGRES_PASSWORD=$DATABASE_PASSWORD \ @@ -106,6 +148,24 @@ docker run -d \ $DOCKER_POSTGRES_IMAGE >/dev/null echo "Done" +echo -n "* Deploying the Redis container..." +docker run -d \ +--name $DOCKER_REDIS_CONTAINER \ +$NETWORK_ARGUMENT \ +--restart=always \ +$REDIS_PORT_ARGUMENT \ +$DOCKER_REDIS_IMAGE \ +redis-server \ +--appendonly no \ +--databases 2 \ +--maxmemory 100mb \ +--maxmemory-policy allkeys-lru \ +--maxclients 500 \ +--save "" \ +--tcp-backlog 256 \ +>/dev/null +echo "Done" + echo -n "* Waiting for the PostgreSQL container to be ready (10 seconds)..." sleep 10 echo "Done" @@ -113,15 +173,18 @@ echo "Done" echo -n "* Deploying Mayan EDMS container..." docker run -d \ --name $DOCKER_MAYAN_CONTAINER \ +$NETWORK_ARGUMENT \ --restart=always \ -p 80:8000 \ -e MAYAN_DATABASE_ENGINE=django.db.backends.postgresql \ --e MAYAN_DATABASE_HOST=172.17.0.1 \ +$MAYAN_DATABASE_HOST_ARGUMENT \ +$MAYAN_DATABASE_PORT_ARGUMENT \ -e MAYAN_DATABASE_NAME=$DATABASE_NAME \ -e MAYAN_DATABASE_PASSWORD=$DATABASE_PASSWORD \ -e MAYAN_DATABASE_USER=$DATABASE_USER \ --e MAYAN_DATABASE_PORT=$DOCKER_POSTGRES_PORT \ -e MAYAN_DATABASE_CONN_MAX_AGE=0 \ +$MAYAN_BROKER_URL_ARGUMENT \ +$MAYAN_CELERY_RESULT_BACKEND_ARGUMENT \ -v $DOCKER_MAYAN_VOLUME:/var/lib/mayan \ $DOCKER_MAYAN_IMAGE >/dev/null echo "Done" diff --git a/contrib/scripts/install/production.sh b/contrib/scripts/install/production.sh deleted file mode 100644 index 2f70812fec..0000000000 --- a/contrib/scripts/install/production.sh +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env bash - -# ====== CONFIG ====== -INSTALLATION_DIRECTORY=/usr/share/mayan-edms/ -DB_NAME=mayan_edms -DB_USERNAME=mayan -DB_PASSWORD=test123 -# ==== END CONFIG ==== - -cat << EOF | tee -a /etc/motd.tail -********************************** - -Mayan EDMS Vagrant Production Box - -********************************** -EOF - -echo -e "\n -> Running apt-get update & upgrade \n" -apt-get -qq update -apt-get -y upgrade - -echo -e "\n -> Installing core binaries \n" -apt-get install nginx supervisor redis-server postgresql libpq-dev libjpeg-dev libmagic1 libpng-dev libreoffice libtiff-dev gcc ghostscript gpgv python-dev python-virtualenv tesseract-ocr poppler-utils -y - -echo -e "\n -> Setting up virtualenv \n" -rm -f ${INSTALLATION_DIRECTORY} -virtualenv ${INSTALLATION_DIRECTORY} -source ${INSTALLATION_DIRECTORY}bin/activate - -echo -e "\n -> Installing Mayan EDMS from PyPI \n" -pip install mayan-edms - -echo -e "\n -> Installing Python client for PostgreSQL, Redis, and uWSGI \n" -pip install psycopg2 redis uwsgi - -echo -e "\n -> Creating the database for the installation \n" -echo "CREATE USER mayan WITH PASSWORD '$DB_PASSWORD';" | sudo -u postgres psql -sudo -u postgres createdb -O $DB_USERNAME $DB_NAME - -echo -e "\n -> Creating the directories for the logs \n" -mkdir /var/log/mayan - -echo -e "\n -> Making a convenience symlink \n" -cd ${INSTALLATION_DIRECTORY} -ln -s lib/python2.7/site-packages/mayan . - -echo -e "\n -> Creating an initial settings file \n" -mayan-edms.py createsettings - -echo -e "\n -> Updating the mayan/settings/local.py file \n" -cat >> mayan/settings/local.py << EOF -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': '$DB_NAME', - 'USER': '$DB_USERNAME', - 'PASSWORD': '$DB_PASSWORD', - 'HOST': 'localhost', - 'PORT': '5432', - } -} - -BROKER_URL = 'redis://127.0.0.1:6379/0' -CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0' -EOF - -echo -e "\n -> Migrating the database or initialize the project \n" -mayan-edms.py initialsetup - -echo -e "\n -> Disabling the default NGINX site \n" -rm -f /etc/nginx/sites-enabled/default - -echo -e "\n -> Creating a uwsgi.ini file \n" -cat > uwsgi.ini << EOF -[uwsgi] -chdir = ${INSTALLATION_DIRECTORY}lib/python2.7/site-packages/mayan -chmod-socket = 664 -chown-socket = www-data:www-data -env = DJANGO_SETTINGS_MODULE=mayan.settings.production -gid = www-data -logto = /var/log/uwsgi/%n.log -pythonpath = ${INSTALLATION_DIRECTORY}lib/python2.7/site-packages -master = True -max-requests = 5000 -socket = ${INSTALLATION_DIRECTORY}uwsgi.sock -uid = www-data -vacuum = True -wsgi-file = ${INSTALLATION_DIRECTORY}lib/python2.7/site-packages/mayan/wsgi.py -EOF - -echo -e "\n -> Creating the directory for the uWSGI log files \n" -mkdir -p /var/log/uwsgi - -echo -e "\n -> Creating the NGINX site file for Mayan EDMS, /etc/nginx/sites-available/mayan \n" -cat > /etc/nginx/sites-available/mayan << EOF -server { - listen 80; - server_name localhost; - - location / { - include uwsgi_params; - uwsgi_pass unix:${INSTALLATION_DIRECTORY}uwsgi.sock; - - client_max_body_size 30M; # Increse if your plan to upload bigger documents - proxy_read_timeout 30s; # Increase if your document uploads take more than 30 seconds - } - - location /static { - alias ${INSTALLATION_DIRECTORY}mayan/media/static; - expires 1h; - } - - location /favicon.ico { - alias ${INSTALLATION_DIRECTORY}mayan/media/static/appearance/images/favicon.ico; - expires 1h; - } -} -EOF - -echo -e "\n -> Enabling the NGINX site for Mayan EDMS \n" -ln -s /etc/nginx/sites-available/mayan /etc/nginx/sites-enabled/ - -echo -e "\n -> Creating the supervisor file for the uWSGI process, /etc/supervisor/conf.d/mayan-uwsgi.conf \n" -cat > /etc/supervisor/conf.d/mayan-uwsgi.conf << EOF -[program:mayan-uwsgi] -command = ${INSTALLATION_DIRECTORY}bin/uwsgi --ini ${INSTALLATION_DIRECTORY}uwsgi.ini -user = root -autostart = true -autorestart = true -redirect_stderr = true -EOF - -echo -e "\n -> Creating the supervisor file for the Celery worker, /etc/supervisor/conf.d/mayan-celery.conf \n" -cat > /etc/supervisor/conf.d/mayan-celery.conf << EOF -[program:mayan-worker] -command = ${INSTALLATION_DIRECTORY}bin/python ${INSTALLATION_DIRECTORY}bin/mayan-edms.py celery --settings=mayan.settings.production worker -Ofair -l ERROR -directory = ${INSTALLATION_DIRECTORY} -user = www-data -stdout_logfile = /var/log/mayan/worker-stdout.log -stderr_logfile = /var/log/mayan/worker-stderr.log -autostart = true -autorestart = true -startsecs = 10 -stopwaitsecs = 10 -killasgroup = true -priority = 998 - -[program:mayan-beat] -command = ${INSTALLATION_DIRECTORY}bin/python ${INSTALLATION_DIRECTORY}bin/mayan-edms.py celery --settings=mayan.settings.production beat -l ERROR -directory = ${INSTALLATION_DIRECTORY} -user = www-data -numprocs = 1 -stdout_logfile = /var/log/mayan/beat-stdout.log -stderr_logfile = /var/log/mayan/beat-stderr.log -autostart = true -autorestart = true -startsecs = 10 -stopwaitsecs = 1 -killasgroup = true -priority = 998 -EOF - -echo -e "\n -> Collecting the static files \n" -mayan-edms.py preparestatic --noinput - -echo -e "\n -> Making the installation directory readable and writable by the webserver user \n" -chown www-data:www-data ${INSTALLATION_DIRECTORY} -R - -echo -e "\n -> Restarting the services \n" -/etc/init.d/nginx restart -/etc/init.d/supervisor restart diff --git a/contrib/scripts/process_messages.py b/contrib/scripts/process_messages.py index f473f050b6..132cb50eac 100755 --- a/contrib/scripts/process_messages.py +++ b/contrib/scripts/process_messages.py @@ -13,11 +13,12 @@ APP_LIST = ( 'checkouts', 'common', 'converter', 'dashboards', 'dependencies', 'django_gpg', 'document_comments', 'document_indexing', 'document_parsing', 'document_signatures', 'document_states', - 'documents', 'dynamic_search', 'events', 'file_metadata', 'linking', - 'lock_manager', 'mayan_statistics', 'mailer', 'metadata', 'mirroring', - 'motd', 'navigation', 'ocr', 'permissions', 'platform', 'rest_api', - 'smart_settings', 'sources', 'storage', 'tags', 'task_manager', - 'user_management' + 'documents', 'dynamic_search', 'events', 'file_caching', + 'file_metadata', 'linking', 'lock_manager', 'mailer', + 'mayan_statistics', 'metadata', 'mirroring', 'motd', 'navigation', + 'ocr', 'permissions', 'platform', 'rest_api', 'smart_settings', + 'sources', 'storage', 'tags', 'task_manager', 'user_management', + 'weblinks' ) LANGUAGE_LIST = ( diff --git a/contrib/scripts/start_gunicorn.sh b/contrib/scripts/start_gunicorn.sh deleted file mode 100644 index 9e52a959c4..0000000000 --- a/contrib/scripts/start_gunicorn.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -NAME="mayan-edms" -DJANGODIR=/usr/share/mayan-edms -SOCKFILE=/var/tmp/filesystem.sock -USER=www-data -GROUP=www-data -NUM_WORKERS=3 -DJANGO_SETTINGS_MODULE=mayan.settings.production -DJANGO_WSGI_MODULE=mayan.wsgi -TIMEOUT=600 - -echo "Starting $NAME as `whoami`" - -# Activate the virtual environment -cd $DJANGODIR -source bin/activate -export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE -export PYTHONPATH=$DJANGODIR:$PYTHONPATH - -# Create the run directory if it doesn't exist -RUNDIR=$(dirname $SOCKFILE) -test -d $RUNDIR || mkdir -p $RUNDIR - -# Start your Django Unicorn -# Programs meant to be run under supervisor should not daemonize themselves (do not use --daemon) -exec bin/gunicorn ${DJANGO_WSGI_MODULE}:application \ - --name $NAME \ - --workers $NUM_WORKERS \ - --user=$USER --group=$GROUP \ - --log-level=debug \ - --bind=unix:$SOCKFILE \ - --timeout=$TIMEOUT - - diff --git a/docker/Dockerfile b/docker/Dockerfile index bd503964c5..cb193e6ad2 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,7 +4,7 @@ # BASE_IMAGE - Bare bones image with the base packages needed to run Mayan EDMS #### -FROM debian:9.8-slim as BASE_IMAGE +FROM debian:10.0-slim as BASE_IMAGE LABEL maintainer="Roberto Rosario roberto.rosario@mayan-edms.com" @@ -22,6 +22,7 @@ RUN set -x \ && DEBIAN_FRONTEND=noninteractive \ apt-get update \ && apt-get install -y --no-install-recommends \ + ca-certificates \ exiftool \ fonts-arphic-uming \ fonts-arphic-ukai \ @@ -31,11 +32,11 @@ apt-get update \ graphviz \ libfuse2 \ libmagic1 \ - libmariadbclient18 \ + libmariadb3 \ libreoffice \ libpq5 \ poppler-utils \ - redis-server \ + python3-distutils \ sane-utils \ sudo \ supervisor \ @@ -54,21 +55,20 @@ apt-get update \ && if [ "$(uname -m)" = "armv7l" ]; then \ ln -s /usr/lib/arm-linux-gnueabihf/libz.so /usr/lib/ \ && ln -s /usr/lib/arm-linux-gnueabihf/libjpeg.so /usr/lib/ \ -; fi \ -# Discard data when Redis runs out of memory -&& echo "maxmemory-policy allkeys-lru" >> /etc/redis/redis.conf \ -# Disable saving the Redis database -echo "save \"\"" >> /etc/redis/redis.conf \ -# Only provision 1 database -&& echo "databases 1" >> /etc/redis/redis.conf +; fi #### -# BUILDER_IMAGE - This image buildS the Python package and is discarded afterwards +# BUILDER_IMAGE - This image builds the Python package and is discarded afterwards +# only the build artifact is carried over to the next image. #### # Reuse image FROM BASE_IMAGE as BUILDER_IMAGE +# Python libraries caching +ARG PIP_INDEX_URL +ARG PIP_TRUSTED_HOST + WORKDIR /src # Copy the source files needed to build the Python package @@ -97,39 +97,40 @@ apt-get install -y --no-install-recommends \ libssl-dev \ g++ \ gcc \ - python-dev \ - python-virtualenv \ + python3-dev \ + python3-venv \ && mkdir -p "${PROJECT_INSTALL_DIR}" \ && chown -R mayan:mayan "${PROJECT_INSTALL_DIR}" \ && chown -R mayan:mayan /src USER mayan -RUN python -m virtualenv "${PROJECT_INSTALL_DIR}" \ +RUN python3 -m venv "${PROJECT_INSTALL_DIR}" \ && . "${PROJECT_INSTALL_DIR}/bin/activate" \ -&& pip install --no-cache-dir --no-use-pep517 \ - librabbitmq==1.6.1 \ - mysql-python==1.2.5 \ - psycopg2==2.7.3.2 \ - redis==2.10.6 \ +&& pip install --no-cache-dir \ + librabbitmq==2.0.0 \ + mysqlclient==1.4.2.post1 \ + psycopg2==2.8.3 \ + redis==3.2.1 \ + flower==0.9.3 \ # psutil is needed by ARM builds otherwise gevent and gunicorn fail to start && UNAME=`uname -m` && if [ "${UNAME#*arm}" != $UNAME ]; then \ - pip install --no-cache-dir --no-use-pep517 \ + pip install --no-cache-dir \ psutil==5.6.2 \ ; fi \ # Install the Python packages needed to build Mayan EDMS -&& pip install --no-cache-dir --no-use-pep517 -r /src/requirements/build.txt \ +&& pip install --no-cache-dir -r /src/requirements/build.txt \ # Build Mayan EDMS -&& python setup.py sdist \ +&& python3 setup.py sdist \ # Install the built Mayan EDMS package -&& pip install --no-cache-dir --no-use-pep517 dist/mayan* \ +&& pip install --no-cache-dir dist/mayan* \ # Install the static content -&& mayan-edms.py installjavascript \ +&& mayan-edms.py installdependencies \ && MAYAN_STATIC_ROOT=${PROJECT_INSTALL_DIR}/static mayan-edms.py preparestatic --link --noinput COPY --chown=mayan:mayan requirements/testing-base.txt "${PROJECT_INSTALL_DIR}" #### -# Final image - BASE_IMAGE + Mayan install directory from the builder image +# Final image - BASE_IMAGE + BUILDER_IMAGE artifact (Mayan install directory) #### FROM BASE_IMAGE @@ -145,7 +146,7 @@ VOLUME ["/var/lib/mayan"] ENTRYPOINT ["entrypoint.sh"] EXPOSE 8000 -CMD ["mayan"] +CMD ["run_all"] RUN ${PROJECT_INSTALL_DIR}/bin/mayan-edms.py platformtemplate supervisord_docker > /etc/supervisor/conf.d/mayan.conf \ && apt-get clean autoclean \ diff --git a/docker/Makefile b/docker/Makefile index bd2743759b..cbb25827b9 100755 --- a/docker/Makefile +++ b/docker/Makefile @@ -1,4 +1,9 @@ -APT_PROXY ?= `/sbin/ip route|awk '/docker0/ { print $$9 }'`:3142 +HOST_IP = `/sbin/ip route|awk '/docker0/ { print $$9 }'` + +APT_PROXY ?= $(HOST_IP):3142 +PIP_INDEX_URL ?= http://$(HOST_IP):3141/root/pypi/+simple/ +PIP_TRUSTED_HOST ?= $(HOST_IP) + IMAGE_VERSION ?= `cat docker/rootfs/version` CONSOLE_COLUMNS ?= `echo $$(tput cols)` CONSOLE_LINES ?= `echo $$(tput lines)` @@ -7,7 +12,7 @@ docker-build: ## Build a new image locally. docker build -t mayanedms/mayanedms:$(IMAGE_VERSION) -f docker/Dockerfile . docker-build-with-proxy: ## Build a new image locally using an APT proxy as APT_PROXY. - docker build -t mayanedms/mayanedms:$(IMAGE_VERSION) -f docker/Dockerfile --build-arg APT_PROXY=$(APT_PROXY) . + docker build -t mayanedms/mayanedms:$(IMAGE_VERSION) -f docker/Dockerfile --build-arg APT_PROXY=$(APT_PROXY) --build-arg PIP_INDEX_URL=$(PIP_INDEX_URL) --build-arg PIP_TRUSTED_HOST=$(PIP_TRUSTED_HOST) --build-arg HTTP_PROXY=$(HTTP_PROXY) --build-arg HTTPS_PROXY=$(HTTPS_PROXY) . docker-shell: ## Launch a bash instance inside a running container. Pass the container name via DOCKER_CONTAINER. docker exec -e TERM=$(TERM) -e "COLUMNS=$(CONSOLE_COLUMNS)" -e "LINES=$(CONSOLE_LINES)" -it $(DOCKER_CONTAINER) /bin/bash @@ -23,3 +28,13 @@ docker-test-cleanup: ## Delete the test container and the test volume. docker-test-all: ## Build and executed the test suite in a test container. docker-test-all: docker-build-with-proxy docker run --rm run-tests + +docker-compose-build: + docker-compose -f docker/docker-compose.yml -p mayan-edms build + +docker-compose-build-with-proxy: + docker-compose -f docker/docker-compose.yml -p mayan-edms build --build-arg APT_PROXY=$(APT_PROXY) --build-arg PIP_INDEX_URL=$(PIP_INDEX_URL) --build-arg PIP_TRUSTED_HOST=$(PIP_TRUSTED_HOST) --build-arg HTTP_PROXY=$(HTTP_PROXY) --build-arg HTTPS_PROXY=$(HTTPS_PROXY) + +docker-compose-up: + docker-compose -f docker/docker-compose.yml -p mayan-edms up + diff --git a/docker/docker-compose-development.yml b/docker/docker-compose-development.yml deleted file mode 100755 index 48c71afe26..0000000000 --- a/docker/docker-compose-development.yml +++ /dev/null @@ -1,72 +0,0 @@ -version: '2.1' - -volumes: - broker: - driver: local - app: - driver: local - db: - driver: local - results: - driver: local - -services: - broker: - container_name: mayan-edms-broker - image: healthcheck/rabbitmq - environment: - RABBITMQ_DEFAULT_USER: mayan - RABBITMQ_DEFAULT_PASS: mayan - RABBITMQ_DEFAULT_VHOST: mayan - volumes: - - broker:/var/lib/rabbitmq - results: - container_name: mayan-edms-results - image: healthcheck/redis - volumes: - - results:/data - #db: - # container_name: mayan-edms-db - # image: healthcheck/mysql - # environment: - # MYSQL_DATABASE: mayan - # MYSQL_PASSWORD: mayan-password - # MYSQL_ROOT_PASSWORD: root-password - # MYSQL_USER: mayan - # volumes: - # - db:/var/lib/mysql - db: - container_name: mayan-edms-db - image: healthcheck/postgres - environment: - POSTGRES_DB: mayan - POSTGRES_PASSWORD: mayan-password - POSTGRES_USER: mayan - volumes: - - db:/var/lib/postgresql/data - mayan-edms: - container_name: mayan-edms-app - image: mayan-edms/next - build: - context: ./ - args: - - APT_PROXY=172.18.0.1:3142 - depends_on: - broker: - condition: service_healthy - db: - condition: service_healthy - results: - condition: service_healthy - environment: - MAYAN_BROKER_URL: amqp://mayan:mayan@broker:5672/mayan - MAYAN_CELERY_RESULT_BACKEND: redis://results:6379/0 - MAYAN_DATABASE_ENGINE: django.db.backends.postgresql - MAYAN_DATABASE_HOST: db - MAYAN_DATABASE_NAME: mayan - MAYAN_DATABASE_PASSWORD: mayan-password - MAYAN_DATABASE_USER: mayan - ports: - - "80:80" - volumes: - - app:/var/lib/mayan diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 978b417148..79fa4d7f82 100755 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,58 +1,130 @@ -version: '2.1' +version: '3.7' -volumes: - broker: - driver: local - app: - driver: local - db: - driver: local - results: - driver: local +networks: + mayan-bridge: + driver: bridge services: - broker: - container_name: mayan-edms-broker - image: healthcheck/rabbitmq - environment: - RABBITMQ_DEFAULT_USER: mayan - RABBITMQ_DEFAULT_PASS: mayan - RABBITMQ_DEFAULT_VHOST: mayan - volumes: - - broker:/var/lib/rabbitmq - results: - container_name: mayan-edms-results - image: healthcheck/redis - volumes: - - results:/data - db: - container_name: mayan-edms-db - image: healthcheck/postgres - environment: - POSTGRES_DB: mayan - POSTGRES_PASSWORD: mayan-password - POSTGRES_USER: mayan - volumes: - - db:/var/lib/postgresql/data - mayan-edms: - container_name: mayan-edms-app - image: mayanedms/mayanedms:latest + app: + build: + context: .. + dockerfile: ./docker/Dockerfile depends_on: - broker: - condition: service_healthy - db: - condition: service_healthy - results: - condition: service_healthy - environment: - MAYAN_BROKER_URL: amqp://mayan:mayan@broker:5672/mayan - MAYAN_CELERY_RESULT_BACKEND: redis://results:6379/0 - MAYAN_DATABASE_ENGINE: django.db.backends.postgresql - MAYAN_DATABASE_HOST: db - MAYAN_DATABASE_NAME: mayan - MAYAN_DATABASE_PASSWORD: mayan-password - MAYAN_DATABASE_USER: mayan + - postgresql + - redis + # Enable to use RabbitMQ + #- rabbitmq + environment: &mayan_env + # Enable to use RabbitMQ + # MAYAN_CELERY_BROKER_URL: amqp://mayan:mayanrabbitpass@broker:5672/mayan + # Disable Redis Broker to use RabbitMQ as Broker + MAYAN_CELERY_BROKER_URL: redis://redis:6379/1 + MAYAN_CELERY_RESULT_BACKEND: redis://redis:6379/0 + MAYAN_DATABASES: "{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayandbpass','USER':'mayan','HOST':'postgresql'}}" + image: mayanedms/mayanedms:3.2.6 + networks: + - mayan-bridge ports: - "80:8000" + restart: unless-stopped volumes: - - app:/var/lib/mayan + - /docker-volumes/mayan-edms/media:/var/lib/mayan + + postgresql: + environment: + POSTGRES_DB: mayan + POSTGRES_PASSWORD: mayandbpass + POSTGRES_USER: mayan + image: postgres:9.6 + networks: + - mayan-bridge + restart: unless-stopped + volumes: + - /docker-volumes/mayan-edms/postgres:/var/lib/postgresql/data + + redis: + command: + - redis-server + - --databases + - "2" + - --maxmemory-policy + - allkeys-lru + - --save + - "" + image: redis:5.0 + networks: + - mayan-bridge + restart: unless-stopped + + # Optional services + + # celery_flower: + # command: + # - run_celery + # - flower + # depends_on: + # - postgresql + # - redis + # # Enable to use RabbitMQ + # # - rabbitmq + # environment: + # <<: *mayan_env + # image: mayanedms/mayanedms:3.2.6 + # networks: + # - mayan-bridge + # ports: + # - "5555:5555" + # restart: unless-stopped + + # Enable to use RabbitMQ + # rabbitmq: + # container_name: mayan-edms-rabbitmq + # image: healthcheck/rabbitmq + # environment: + # RABBITMQ_DEFAULT_USER: mayan + # RABBITMQ_DEFAULT_PASS: mayanrabbitpass + # RABBITMQ_DEFAULT_VHOST: mayan + # networks: + # - mayan-bridge + # restart: unless-stopped + # volumes: + # - /docker-volumes/mayan-edms/rabbitmq:/var/lib/rabbitmq + + # Enable to run stand alone workers + # worker_fast: + # command: + # - run_worker + # - fast + # depends_on: + # - postgresql + # - redis + # # Enable to use RabbitMQ + # # - rabbitmq + # environment: + # <<: *mayan_env + # image: mayanedms/mayanedms:3.2.6 + # networks: + # - mayan-bridge + # restart: unless-stopped + # volumes: + # - /docker-volumes/mayan-edms/media:/var/lib/mayan + + # Enable to run stand frontend gunicorn + # frontend: + # command: + # - run_frontend + # depends_on: + # - postgresql + # - redis + # # Enable to use RabbitMQ + # # - rabbitmq + # environment: + # <<: *mayan_env + # image: mayanedms/mayanedms:3.2.6 + # networks: + # - mayan-bridge + # ports: + # - "81:8000" + # restart: unless-stopped + # volumes: + # - /docker-volumes/mayan-edms/media:/var/lib/mayan diff --git a/docker/rootfs/usr/local/bin/entrypoint.sh b/docker/rootfs/usr/local/bin/entrypoint.sh index 18b0aa7e78..a877af7dcc 100755 --- a/docker/rootfs/usr/local/bin/entrypoint.sh +++ b/docker/rootfs/usr/local/bin/entrypoint.sh @@ -1,4 +1,7 @@ -#!/bin/sh +#!/bin/bash + +# Use bash and not sh to support argument slicing "${@:2}" +# sh defaults to dash instead of bash. set -e echo "mayan: starting entrypoint.sh" @@ -11,17 +14,13 @@ DEFAULT_USER_GID=1000 MAYAN_USER_UID=${MAYAN_USER_UID:-${DEFAULT_USER_UID}} MAYAN_USER_GID=${MAYAN_USER_GID:-${DEFAULT_USER_GID}} -export MAYAN_DEFAULT_BROKER_URL=redis://127.0.0.1:6379/0 -export MAYAN_DEFAULT_CELERY_RESULT_BACKEND=redis://127.0.0.1:6379/0 - export MAYAN_ALLOWED_HOSTS='["*"]' export MAYAN_BIN=/opt/mayan-edms/bin/mayan-edms.py -export MAYAN_BROKER_URL=${MAYAN_BROKER_URL:-${MAYAN_DEFAULT_BROKER_URL}} -export MAYAN_CELERY_RESULT_BACKEND=${MAYAN_CELERY_RESULT_BACKEND:-${MAYAN_DEFAULT_CELERY_RESULT_BACKEND}} export MAYAN_INSTALL_DIR=/opt/mayan-edms export MAYAN_PYTHON_BIN_DIR=/opt/mayan-edms/bin/ export MAYAN_MEDIA_ROOT=/var/lib/mayan export MAYAN_SETTINGS_MODULE=${MAYAN_SETTINGS_MODULE:-mayan.settings.production} +export DJANGO_SETTINGS_MODULE=${MAYAN_SETTINGS_MODULE} export MAYAN_GUNICORN_BIN=${MAYAN_PYTHON_BIN_DIR}gunicorn export MAYAN_GUNICORN_WORKERS=${MAYAN_GUNICORN_WORKERS:-2} @@ -29,8 +28,8 @@ export MAYAN_GUNICORN_TIMEOUT=${MAYAN_GUNICORN_TIMEOUT:-120} export MAYAN_PIP_BIN=${MAYAN_PYTHON_BIN_DIR}pip export MAYAN_STATIC_ROOT=${MAYAN_INSTALL_DIR}/static -MAYAN_WORKER_FAST_CONCURRENCY=${MAYAN_WORKER_FAST_CONCURRENCY:-1} -MAYAN_WORKER_MEDIUM_CONCURRENCY=${MAYAN_WORKER_MEDIUM_CONCURRENCY:-1} +MAYAN_WORKER_FAST_CONCURRENCY=${MAYAN_WORKER_FAST_CONCURRENCY:-0} +MAYAN_WORKER_MEDIUM_CONCURRENCY=${MAYAN_WORKER_MEDIUM_CONCURRENCY:-0} MAYAN_WORKER_SLOW_CONCURRENCY=${MAYAN_WORKER_SLOW_CONCURRENCY:-1} update_uid_gid() { @@ -67,11 +66,9 @@ else fi export MAYAN_WORKER_SLOW_CONCURRENCY -export CELERY_ALWAYS_EAGER=False +# Allow importing of user setting modules export PYTHONPATH=$PYTHONPATH:$MAYAN_MEDIA_ROOT -chown mayan:mayan /var/lib/mayan -R - apt_get_install() { apt-get -q update apt-get install -y --force-yes --no-install-recommends --auto-remove "$@" @@ -79,9 +76,9 @@ apt_get_install() { rm -rf /var/lib/apt/lists/* } -initialize() { - echo "mayan: initialize()" - su mayan -c "${MAYAN_BIN} initialsetup --force --no-javascript" +initialsetup() { + echo "mayan: initialsetup()" + su mayan -c "${MAYAN_BIN} initialsetup --force --no-dependencies" } os_package_installs() { @@ -98,43 +95,71 @@ pip_installs() { fi } -start() { +run_all() { echo "mayan: start()" rm -rf /var/run/supervisor.sock exec /usr/bin/supervisord -nc /etc/supervisor/supervisord.conf } -upgrade() { - echo "mayan: upgrade()" - su mayan -c "${MAYAN_BIN} performupgrade --no-javascript" +performupgrade() { + echo "mayan: performupgrade()" + su mayan -c "${MAYAN_BIN} performupgrade --no-dependencies" +} + +make_ready() { + # Check if this is a new install, otherwise try to upgrade the existing + # installation on subsequent starts + if [ ! -f $INSTALL_FLAG ]; then + initialsetup + else + performupgrade + fi +} + +set_uid_guid() { + echo "mayan: changing uid/guid" + usermod mayan -u ${MAYAN_USER_UID:-${DEFAULT_USER_UID}} + groupmod mayan -g ${MAYAN_USER_GID:-${DEFAULT_USER_GID}} } os_package_installs || true pip_installs || true +chown mayan:mayan /var/lib/mayan -R case "$1" in -mayan) # Check if this is a new install, otherwise try to upgrade the existing - # installation on subsequent starts - if [ ! -f $INSTALL_FLAG ]; then - initialize - else - upgrade - fi - start - ;; +run_initialsetup) + initialsetup + ;; -run-tests) # Check if this is a new install, otherwise try to upgrade the existing - # installation on subsequent starts - if [ ! -f $INSTALL_FLAG ]; then - initialize - else - upgrade - fi - run-tests.sh - ;; +run_performupgrade) + performupgrade + ;; -*) su mayan -c "$@"; - ;; +run_all) + make_ready + run_all + ;; + +run_celery) + run_celery.sh "${@:2}" + ;; + +run_frontend) + run_frontend.sh + ;; + +run_tests) + make_ready + run_tests.sh + ;; + +run_worker) + run_worker.sh "${@:2}" + ;; + +*) + su mayan -c "$@" + ;; esac diff --git a/docker/rootfs/usr/local/bin/run_celery.sh b/docker/rootfs/usr/local/bin/run_celery.sh new file mode 100755 index 0000000000..a4aba0c8a2 --- /dev/null +++ b/docker/rootfs/usr/local/bin/run_celery.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +# Use -A and not --app. Both are the same but behave differently +# -A can be located before the command while --app cannot. +su mayan -c "${MAYAN_PYTHON_BIN_DIR}celery -A mayan $@" diff --git a/docker/rootfs/usr/local/bin/run_frontend.sh b/docker/rootfs/usr/local/bin/run_frontend.sh new file mode 100755 index 0000000000..f3c3967ffb --- /dev/null +++ b/docker/rootfs/usr/local/bin/run_frontend.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +MAYAN_GUNICORN_MAX_REQUESTS=${MAYAN_GUNICORN_MAX_REQUESTS:-500} +MAYAN_GUNICORN_MAX_REQUESTS_JITTERS=${MAYAN_GUNICORN_MAX_REQUESTS_JITTERS:-50} +MAYAN_GUNICORN_WORKER_CLASS=${MAYAN_GUNICORN_WORKER_CLASS:-sync} + +su mayan -c "${MAYAN_PYTHON_BIN_DIR}gunicorn -w ${MAYAN_GUNICORN_WORKERS} mayan.wsgi --max-requests ${MAYAN_GUNICORN_MAX_REQUESTS} --max-requests-jitter ${MAYAN_GUNICORN_MAX_REQUESTS_JITTERS} --worker-class ${MAYAN_GUNICORN_WORKER_CLASS} --bind 0.0.0.0:8000 --timeout ${MAYAN_GUNICORN_TIMEOUT}" diff --git a/docker/rootfs/usr/local/bin/run-tests.sh b/docker/rootfs/usr/local/bin/run_tests.sh similarity index 100% rename from docker/rootfs/usr/local/bin/run-tests.sh rename to docker/rootfs/usr/local/bin/run_tests.sh diff --git a/docker/rootfs/usr/local/bin/run_worker.sh b/docker/rootfs/usr/local/bin/run_worker.sh new file mode 100755 index 0000000000..dd5db6068d --- /dev/null +++ b/docker/rootfs/usr/local/bin/run_worker.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +QUEUE_LIST=`MAYAN_WORKER_NAME=$1 su mayan -c "${MAYAN_PYTHON_BIN_DIR}mayan-edms.py platformtemplate worker_queues"` + +# Use -A and not --app. Both are the same but behave differently +# -A can be located before the command while --app cannot. +# Pass ${@:2} to allow overriding the defaults arguments +su mayan -c "${MAYAN_PYTHON_BIN_DIR}celery -A mayan worker -Ofair -l ERROR -Q $QUEUE_LIST ${@:2}" diff --git a/docker/stack.yml b/docker/stack.yml index c8a7b243bd..95bff43909 100644 --- a/docker/stack.yml +++ b/docker/stack.yml @@ -9,24 +9,32 @@ volumes: services: db: - image: postgres environment: POSTGRES_DB: mayan - POSTGRES_PASSWORD: mayan-password + POSTGRES_PASSWORD: mayandbpass POSTGRES_USER: mayan + image: postgres volumes: - db:/var/lib/postgresql/data app: + environment: + MAYAN_CELERY_BROKER_URL: redis://redis:6379/1 + MAYAN_CELERY_RESULT_BACKEND: redis://redis:6379/0 + MAYAN_DATABASES: "{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayandbpass','USER':'mayan','HOST':'db'}}" image: mayanedms/mayanedms:latest ports: - 80:8000 - environment: - MAYAN_DATABASE_ENGINE: django.db.backends.postgresql - MAYAN_DATABASE_HOST: db - MAYAN_DATABASE_NAME: mayan - MAYAN_DATABASE_PASSWORD: mayan-password - MAYAN_DATABASE_USER: mayan - MAYAN_DATABASE_CONN_MAX_AGE: 0 volumes: - app:/var/lib/mayan + + redis: + command: + - redis-server + - --databases + - "2" + - --maxmemory-policy + - allkeys-lru + - --save + - "" + image: redis:5.0 diff --git a/docs/chapters/deploying.rst b/docs/chapters/deploying.rst index 425f3274f9..7bbc889a17 100644 --- a/docs/chapters/deploying.rst +++ b/docs/chapters/deploying.rst @@ -127,9 +127,8 @@ For another setup that offers more performance and scalability refer to the :: - sudo -u mayan MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \ - MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \ - MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \ + sudo -u mayan MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \ + MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \ /opt/mayan-edms/bin/mayan-edms.py initialsetup @@ -148,9 +147,8 @@ For another setup that offers more performance and scalability refer to the ------------------------------------------------------------------------ :: - sudo MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \ - MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \ - MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \ + sudo mayan MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \ + MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \ /opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf @@ -222,11 +220,11 @@ of a restart or power failure. The Gunicorn workers are increased to 3. --------------------------------------------------------------------- Replace (paying attention to the comma at the end):: - MAYAN_BROKER_URL="redis://127.0.0.1:6379/0", + MAYAN_CELERY_BROKER_URL="redis://127.0.0.1:6379/0", with:: - MAYAN_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan", + MAYAN_CELERY_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan", increase the number of Gunicorn workers to 3 in the line (``-w 2`` section):: diff --git a/docs/chapters/docker.rst b/docs/chapters/docker.rst index b2e0c5f4c6..9862154e19 100644 --- a/docs/chapters/docker.rst +++ b/docs/chapters/docker.rst @@ -49,12 +49,7 @@ Finally create and run a Mayan EDMS container:: --name mayan-edms \ --restart=always \ -p 80:8000 \ - -e MAYAN_DATABASE_ENGINE=django.db.backends.postgresql \ - -e MAYAN_DATABASE_HOST=172.17.0.1 \ - -e MAYAN_DATABASE_NAME=mayan \ - -e MAYAN_DATABASE_PASSWORD=mayanuserpass \ - -e MAYAN_DATABASE_USER=mayan \ - -e MAYAN_DATABASE_CONN_MAX_AGE=0 \ + -e MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'172.17.0.1'}}" \ -v /docker-volumes/mayan-edms/media:/var/lib/mayan \ mayanedms/mayanedms: @@ -108,12 +103,7 @@ instead of the IP address of the Docker host (``172.17.0.1``):: --network=mayan \ --restart=always \ -p 80:8000 \ - -e MAYAN_DATABASE_ENGINE=django.db.backends.postgresql \ - -e MAYAN_DATABASE_HOST=mayan-edms-postgres \ - -e MAYAN_DATABASE_NAME=mayan \ - -e MAYAN_DATABASE_PASSWORD=mayanuserpass \ - -e MAYAN_DATABASE_USER=mayan \ - -e MAYAN_DATABASE_CONN_MAX_AGE=0 \ + -e MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'mayan-edms-postgres'}}" \ -v /docker-volumes/mayan-edms/media:/var/lib/mayan \ mayanedms/mayanedms: @@ -137,101 +127,14 @@ To start the container again:: Environment Variables --------------------- -The Mayan EDMS image can be configure via environment variables. - -``MAYAN_DATABASE_ENGINE`` - -Defaults to ``None``. This environment variable configures the database -backend to use. If left unset, SQLite will be used. The database backends -supported by this Docker image are: - -- ``'django.db.backends.postgresql'`` -- ``'django.db.backends.mysql'`` -- ``'django.db.backends.sqlite3'`` - -When using the SQLite backend, the database file will be saved in the Docker -volume. The SQLite database as used by Mayan EDMS is meant only for development -or testing, never use it in production. - -``MAYAN_DATABASE_NAME`` - -Defaults to 'mayan'. This optional environment variable can be used to define -the database name that Mayan EDMS will connect to. For more information read -the pertinent Django documentation page: -:django-docs:`Connecting to the database ` - -``MAYAN_DATABASE_USER`` - -Defaults to 'mayan'. This optional environment variable is used to set the -username that will be used to connect to the database. For more information -read the pertinent Django documentation page: -:django-docs:`Settings, USER ` - -``MAYAN_DATABASE_PASSWORD`` - -Defaults to ''. This optional environment variable is used to set the -password that will be used to connect to the database. For more information -read the pertinent Django documentation page: -:django-docs:`Settings, PASSWORD ` - -``MAYAN_DATABASE_HOST`` - -Defaults to `None`. This optional environment variable is used to set the -hostname that will be used to connect to the database. This can be the -hostname of another container or an IP address. For more information read -the pertinent Django documentation page: -:django-docs:`Settings, HOST ` - -``MAYAN_DATABASE_PORT`` - -Defaults to `None`. This optional environment variable is used to set the -port number to use when connecting to the database. An empty string means -the default port. Not used with SQLite. For more information read the -pertinent Django documentation page: -:django-docs:`Settings, PORT ` - -``MAYAN_BROKER_URL`` - -This optional environment variable determines the broker that Celery will use -to relay task messages between the frontend code and the background workers. -For more information read the pertinent Celery Kombu documentation page: `Broker URL`_ - -.. _Broker URL: http://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls - -This Docker image supports using Redis and RabbitMQ as brokers. - -Caveat: If the `MAYAN_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment -variables are specified, the built-in Redis server inside the container will -be disabled. - -``MAYAN_CELERY_RESULT_BACKEND`` - -This optional environment variable determines the results backend that Celery -will use to relay result messages from the background workers to the frontend -code. For more information read the pertinent Celery Kombu documentation page: -`Task result backend settings`_ - -.. _Task result backend settings: http://docs.celeryproject.org/en/3.1/configuration.html#celery-result-backend - -This Docker image supports using Redis and RabbitMQ as result backends. - -Caveat: If the `MAYAN_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment -variables are specified, the built-in Redis server inside the container will -be disabled. +The common set of settings can also be modified via environment variables when +using the Docker image. In addition to the common set of settings, some Docker +image specific environment variables are available. ``MAYAN_SETTINGS_MODULE`` Optional. Allows loading an alternate settings file. -``MAYAN_DATABASE_CONN_MAX_AGE`` - -Amount in seconds to keep a database connection alive. Allow reuse of database -connections. For more information read the pertinent Django documentation -page: :django-docs:`Settings, CONN_MAX_AGE ` -According to new information Gunicorn's microthreads don't share connections -and will exhaust the available Postgres connections available if a number -other than 0 is used. Reference: https://serverfault.com/questions/635100/django-conn-max-age-persists-connections-but-doesnt-reuse-them-with-postgresq -and https://github.com/benoitc/gunicorn/issues/996 ``MAYAN_GUNICORN_TIMEOUT`` @@ -281,6 +184,15 @@ Optional. Changes the GID of the ``mayan`` user internal to the Docker container. Defaults to 1000. +Included drivers +---------------- + +The Docker image supports using Redis and RabbitMQ as result backends. For +databases, the image includes support for PostgreSQL and MySQL/MariaDB. +Support for additional brokers or databases may be added using the +``MAYAN_APT_INSTALL`` environment variable. + + .. _docker-accessing-outside-data: Accessing outside data @@ -448,6 +360,7 @@ These are: Nightly images ============== + The continuous integration pipeline used for testing development builds also produces a resulting Docker image. These are build automatically and their stability is not guaranteed. They should never be used in production. diff --git a/docs/chapters/scaling_up.rst b/docs/chapters/scaling_up.rst index 01a59cecb3..de03468b83 100644 --- a/docs/chapters/scaling_up.rst +++ b/docs/chapters/scaling_up.rst @@ -94,11 +94,11 @@ For the Docker image, launch a separate RabbitMQ container docker run -d --name mayan-edms-rabbitmq -e RABBITMQ_DEFAULT_USER=mayan -e RABBITMQ_DEFAULT_PASS=mayanrabbitmqpassword -e RABBITMQ_DEFAULT_VHOST=mayan rabbitmq:3 -Pass the MAYAN_BROKER_URL environment variable (https://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls) +Pass the MAYAN_CELERY_BROKER_URL environment variable (https://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls) to the Mayan EDMS container so that it uses the RabbitMQ container the message broker:: - -e MAYAN_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan", + -e MAYAN_CELERY_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan", When tasks finish, they leave behind a return status or the result of a calculation, these are stored for a while so that whoever requested the diff --git a/docs/releases/3.3.rst b/docs/releases/3.3.rst new file mode 100644 index 0000000000..17ff89a513 --- /dev/null +++ b/docs/releases/3.3.rst @@ -0,0 +1,213 @@ +Version 3.3 +=========== + +Released: XX XX, 2019 + + +Changes +------- + +- Add support for icon shadows. +- Add icons and no-result template to the object error log view and + links. +- Use Select2 widget for the document type selection form. +- Backport the vertical main menu update. This update splits the previous + main menu into a new menu in the same location as the previous one + now called the top bar, and a new vertical main menu on the left side. + The vertical menu remain open even when clicking on items and upon + a browser refresh will also restore its state to match the selected + view. +- Backport workflow preview refactor. GitLab issue #532. +- Add support for source column inheritance. +- Add support for source column exclusion. +- Backport workflow context support. +- Backport workflow transitions field support. +- Backport workflow email action. +- Backport individual index rebuild support. +- Rename the installjavascript command to installdependencies. +- Remove database conversion command. +- Remove support for quoted configuration entries. Support unquoted, + nested dictionaries in the configuration. Requires manual + update of existing config.yml files. +- Support user specified locations for the configuration file with the + CONFIGURATION_FILEPATH (MAYAN_CONFIGURATION_FILEPATH environment variable), and + CONFIGURATION_LAST_GOOD_FILEPATH + (MAYAN_CONFIGURATION_LAST_GOOD_FILEPATH environment variable) settings. +- Move bootstrapped settings code to their own module in the smart_settings apps. +- Remove individual database configuration options. All database configuration + is now done using MAYAN_DATABASES to mirror Django way of doing database setup. +- Added support for YAML encoded environment variables to the platform + templates apps. +- Move YAML code to its own module. Code now resides in common.serialization + in the form of two new functions: yaml_load and yaml_dump. +- Move Django and Celery settings. Django settings now reside in the smart + settings app. Celery settings now reside in the task manager app. +- Backport FakeStorageSubclass from versions/next. Placeholder class to allow + serializing the real storage subclass to support migrations. + Used by all configurable storages. +- Support checking in and out multiple documents. +- Remove encapsulate helper. +- Add support for menu inheritance. +- Emphasize source column labels. +- Backport file cache manager app. +- Convert document image cache to use file cache manager app. + Add setting DOCUMENTS_CACHE_MAXIMUM_SIZE defaults to 500 MB. +- Update Celery to version 4.3.0. Settings changed: + MAYAN_BROKER_URL to MAYAN_CELERY_BROKER_URL, + MAYAN_CELERY_ALWAYS_EAGER to MAYAN_CELERY_TASK_ALWAYS_EAGER. +- Replace djcelery and replace it with django-celery-beat. +- Update Celery to version 4.3.0 with 55e9b2263cbdb9b449361412fd18d8ee0a442dd3 + from versions/next, code from GitLab issue #594 and GitLab merge request !55. + Thanks to Jakob Haufe (@sur5r) and Jesaja Everling (@jeverling) + for much of the research and code updates. +- Support wildcard MIME type associations for the file metadata drivers. +- Rename MAYAN_GUID to MAYAN_GID +- Update Gunicorn to use sync workers. +- Include devpi-server as a development dependency. +- Update default Docker stack file. +- Remove Redis from the Docker image. +- Add Celery flower to the Docker image. +- Allow PIP proxying to the Docker image during build. +- Default Celery worker concurrency to 0 (auto). +- Set DJANGO_SETTINGS_MODULE environment variable to make it + available to sub processes. +- Add entrypoint commands to run single workers, single gunicorn + or single celery commands like "flower". +- Add platform template to return queues for a worker. +- Remove task inspection from task manager app. +- Move pagination navigation inside the toolbar. +- Remove document image clear link and view. + This is now handled by the file caching app. +- Add web links app. +- Add support to display column help text + as a tooltip. +- Update numeric dashboard widget to display + thousand commas. +- Add support for disabling document pages. +- Add support for converter layers. +- Add redactions app. +- Unify all line endings to be Linux style. +- Add support for changing the system messages position. + GitLab issue #640. Thanks to Matthias Urhahn (@d4rken). + +Removals +-------- + +- Database conversion. Reason for removal. The database conversions support + provided by this feature (SQLite to PostgreSQL) was being confused with + database migrations and upgrades. + + Database upgrades are the responsibility of the app and the framework. + Database conversions however are not the responsibility of the app (Mayan), + they are the responsibility of the framework. + + Database conversion is outside the scope of what Mayan does but we added + the code, management command, instructions and testing setup to provide + this to our users until the framework (Django) decided to add this + themselves (like they did with migrations). + + Continued confusion about the purpose of the feature and confusion about + how errors with this feature were a reflexion of the code quality of + Mayannecessitated the removal of the database conversion feature. + +- Django environ + + +Upgrading from a previous version +--------------------------------- + +If installed via Python's PIP +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Remove deprecated requirements:: + + sudo -u mayan curl https://gitlab.com/mayan-edms/mayan-edms/raw/master/removals.txt -o /tmp/removals.txt && sudo -u mayan /opt/mayan-edms/bin/pip uninstall -y -r /tmp/removals.txt + +Type in the console:: + + /opt/mayan-edms/bin/pip install mayan-edms==3.3 + +the requirements will also be updated automatically. + + +Using Git +^^^^^^^^^ + +If you installed Mayan EDMS by cloning the Git repository issue the commands:: + + git reset --hard HEAD + git pull + +otherwise download the compressed archived and uncompress it overriding the +existing installation. + +Remove deprecated requirements:: + + pip uninstall -y -r removals.txt + +Next upgrade/add the new requirements:: + + pip install --upgrade -r requirements.txt + + +Common steps +^^^^^^^^^^^^ + +Perform these steps after updating the code from either step above. + +Make a backup of your supervisord file:: + + sudo cp /etc/supervisor/conf.d/mayan.conf /etc/supervisor/conf.d/mayan.conf.bck + +Update the supervisord configuration file. Replace the environment +variables values show here with your respective settings. This step will refresh +the supervisord configuration file with the new queues and the latest +recommended layout:: + + sudo MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \ + MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \ + /opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf + +Edit the supervisord configuration file and update any setting the template +generator missed:: + + sudo vi /etc/supervisor/conf.d/mayan.conf + +Migrate existing database schema with:: + + sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media /opt/mayan-edms/bin/mayan-edms.py performupgrade + +Add new static media:: + + sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media /opt/mayan-edms/bin/mayan-edms.py preparestatic --noinput + +The upgrade procedure is now complete. + + +Backward incompatible changes +----------------------------- + +- Update quoted settings to be unquoted: + + - COMMON_SHARED_STORAGE_ARGUMENTS + - CONVERTER_GRAPHICS_BACKEND_ARGUMENTS + - DOCUMENTS_CACHE_STORAGE_BACKEND_ARGUMENTS + - DOCUMENTS_STORAGE_BACKEND_ARGUMENTS + - FILE_METADATA_DRIVERS_ARGUMENTS + - SIGNATURES_STORAGE_BACKEND_ARGUMENTS + + +Bugs fixed or issues closed +--------------------------- + +- :gitlab-issue:`526` RuntimeWarning: Never call result.get() within a task! +- :gitlab-issue:`532` Workflow preview isn't updated right after transitions are modified +- :gitlab-issue:`540` hint-outdated/update documentation +- :gitlab-issue:`594` 3.2b1: Unable to install/run under Python 3.5/3.6/3.7 +- :gitlab-issue:`634` Failing docker entrypoint when using secret config +- :gitlab-issue:`635` Build a docker image for Python3 +- :gitlab-issue:`640` UX: "Toast" Popup position prevents access to actions +- :gitlab-issue:`644` Update sane-utils package in docker image. + + +.. _PyPI: https://pypi.python.org/pypi/mayan-edms/ diff --git a/docs/releases/index.rst b/docs/releases/index.rst index 0b619a9906..e254e418dc 100644 --- a/docs/releases/index.rst +++ b/docs/releases/index.rst @@ -20,6 +20,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 3.3 3.2.8 3.2.7 3.2.6 diff --git a/mayan/apps/acls/classes.py b/mayan/apps/acls/classes.py index 1c06e0d05e..8e8b8c5db6 100644 --- a/mayan/apps/acls/classes.py +++ b/mayan/apps/acls/classes.py @@ -12,6 +12,7 @@ logger = logging.getLogger(__name__) class ModelPermission(object): _functions = {} _inheritances = {} + _manager_names = {} _registry = {} @classmethod @@ -20,22 +21,6 @@ class ModelPermission(object): # TODO: Find method to revert the add_to_class('acls'...) # delattr doesn't work. - @classmethod - def register(cls, model, permissions): - from django.contrib.contenttypes.fields import GenericRelation - - cls._registry.setdefault(model, []) - for permission in permissions: - cls._registry[model].append(permission) - - AccessControlList = apps.get_model( - app_label='acls', model_name='AccessControlList' - ) - - model.add_to_class( - name='acls', value=GenericRelation(AccessControlList) - ) - @classmethod def get_classes(cls, as_content_type=False): ContentType = apps.get_model( @@ -97,6 +82,40 @@ class ModelPermission(object): def get_inheritance(cls, model): return cls._inheritances[model] + @classmethod + def get_manager(cls, model): + try: + manager_name = cls.get_manager_name(model=model) + except KeyError: + manager_name = None + + if manager_name: + manager = getattr(model, manager_name) + else: + manager = model._meta.default_manager + + return manager + + @classmethod + def get_manager_name(cls, model): + return cls._manager_names[model] + + @classmethod + def register(cls, model, permissions): + from django.contrib.contenttypes.fields import GenericRelation + + cls._registry.setdefault(model, []) + for permission in permissions: + cls._registry[model].append(permission) + + AccessControlList = apps.get_model( + app_label='acls', model_name='AccessControlList' + ) + + model.add_to_class( + name='acls', value=GenericRelation(AccessControlList) + ) + @classmethod def register_function(cls, model, function): cls._functions[model] = function @@ -104,3 +123,7 @@ class ModelPermission(object): @classmethod def register_inheritance(cls, model, related): cls._inheritances[model] = related + + @classmethod + def register_manager(cls, model, manager_name): + cls._manager_names[model] = manager_name diff --git a/mayan/apps/acls/managers.py b/mayan/apps/acls/managers.py index 0cb07b5987..5bab247470 100644 --- a/mayan/apps/acls/managers.py +++ b/mayan/apps/acls/managers.py @@ -45,8 +45,8 @@ class AccessControlListManager(models.Manager): # 4: No related field, but has an inherited related field, solved by # recursion, branches to #2 or #3. # 5: Inherited field of a related field - # -- Not addressed yet -- # 6: Inherited field of a related field that is Generic Foreign Key + # -- Not addressed yet -- # 7: Has a related function result = [] @@ -58,10 +58,28 @@ class AccessControlListManager(models.Manager): if isinstance(related_field, GenericForeignKey): # Case 3: Generic Foreign Key, multiple ContentTypes + object # id combinations + # Also handles case #6 using the parent related field + # reference template. + + # Craft a double underscore reference to a previous related + # field in the case where multiple related fields are + # associated. + # Example: object_layer__content_type + recuisive_related_reference = '__'.join(related_field_name.split('__')[0:-1]) + + # If there is at least one parent related field we add a + # double underscore to make it a valid filter template. + if recuisive_related_reference: + recuisive_related_reference = '{}__'.format(recuisive_related_reference) + content_type_object_id_queryset = queryset.annotate( ct_fk_combination=Concat( - related_field.ct_field, Value('-'), - related_field.fk_field, output_field=CharField() + '{}{}'.format( + recuisive_related_reference, related_field.ct_field + ), Value('-'), + '{}{}'.format( + recuisive_related_reference, related_field.fk_field + ), output_field=CharField() ) ).values('ct_fk_combination') @@ -75,8 +93,7 @@ class AccessControlListManager(models.Manager): ct_fk_combination__in=content_type_object_id_queryset ).values('object_id') - field_lookup = 'object_id__in' - + field_lookup = '{}object_id__in'.format(recuisive_related_reference) result.append(Q(**{field_lookup: acl_filter})) else: # Case 2: Related field of a single type, single ContentType, @@ -97,6 +114,7 @@ class AccessControlListManager(models.Manager): # Case 5: Related field, has an inherited related field itself # Bubble up permssion check + # Recurse and reduce # TODO: Add relationship support: OR or AND # TODO: OR for document pages, version, doc, and types # TODO: AND for new cabinet levels ACLs @@ -200,28 +218,26 @@ class AccessControlListManager(models.Manager): return result - def check_access(self, obj, permissions, user, manager=None): + def check_access(self, obj, permissions, user): # Allow specific managers for models that have more than one # for example the Document model when checking for access for a trashed # document. - if manager: - source_queryset = manager.all() + meta = getattr(obj, '_meta', None) + + if not meta: + logger.debug( + ugettext( + 'Object "%s" is not a model and cannot be checked for ' + 'access.' + ) % force_text(obj) + ) + return True else: - meta = getattr(obj, '_meta', None) + manager = ModelPermission.get_manager(model=obj._meta.model) + source_queryset = manager.all() - if not meta: - logger.debug( - ugettext( - 'Object "%s" is not a model and cannot be checked for ' - 'access.' - ) % force_text(obj) - ) - return True - else: - source_queryset = obj._meta.default_manager.all() - - restricted_queryset = obj._meta.default_manager.none() + restricted_queryset = manager.none() for permission in permissions: # Default relationship betweens permissions is OR # TODO: Add support for AND relationship diff --git a/mayan/apps/acls/tests/test_api.py b/mayan/apps/acls/tests/test_api.py index 5292414fd1..81dfe34c24 100644 --- a/mayan/apps/acls/tests/test_api.py +++ b/mayan/apps/acls/tests/test_api.py @@ -3,7 +3,7 @@ from __future__ import absolute_import, unicode_literals from rest_framework import status from mayan.apps.permissions.tests.literals import TEST_ROLE_LABEL -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..models import AccessControlList from ..permissions import permission_acl_edit, permission_acl_view diff --git a/mayan/apps/acls/tests/test_classes.py b/mayan/apps/acls/tests/test_classes.py index f1ab062709..5b75d2916b 100644 --- a/mayan/apps/acls/tests/test_classes.py +++ b/mayan/apps/acls/tests/test_classes.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, unicode_literals -from mayan.apps.common.tests import BaseTestCase +from mayan.apps.common.tests.base import BaseTestCase from ..classes import ModelPermission diff --git a/mayan/apps/acls/tests/test_links.py b/mayan/apps/acls/tests/test_links.py index fd45f8f253..ae6926acff 100644 --- a/mayan/apps/acls/tests/test_links.py +++ b/mayan/apps/acls/tests/test_links.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.urls import reverse -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from ..links import ( link_acl_delete, link_acl_list, link_acl_create, link_acl_permissions diff --git a/mayan/apps/acls/tests/test_models.py b/mayan/apps/acls/tests/test_models.py index 7b60c1bbdd..a420039045 100644 --- a/mayan/apps/acls/tests/test_models.py +++ b/mayan/apps/acls/tests/test_models.py @@ -3,7 +3,7 @@ from __future__ import absolute_import, unicode_literals from django.core.exceptions import PermissionDenied from django.db import models -from mayan.apps.common.tests import BaseTestCase +from mayan.apps.common.tests.base import BaseTestCase from ..classes import ModelPermission from ..models import AccessControlList diff --git a/mayan/apps/acls/tests/test_views.py b/mayan/apps/acls/tests/test_views.py index e338a99109..72b3a7d19c 100644 --- a/mayan/apps/acls/tests/test_views.py +++ b/mayan/apps/acls/tests/test_views.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, unicode_literals -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from ..models import AccessControlList from ..permissions import permission_acl_edit, permission_acl_view diff --git a/mayan/apps/acls/workflow_actions.py b/mayan/apps/acls/workflow_actions.py index da916aa780..ee0e1638a7 100644 --- a/mayan/apps/acls/workflow_actions.py +++ b/mayan/apps/acls/workflow_actions.py @@ -16,7 +16,6 @@ from mayan.apps.permissions.models import Role from .classes import ModelPermission from .permissions import permission_acl_edit -__all__ = ('GrantAccessAction', 'RevokeAccessAction') logger = logging.getLogger(__name__) @@ -57,7 +56,7 @@ class GrantAccessAction(WorkflowAction): } } field_order = ('content_type', 'object_id', 'roles', 'permissions') - label = _('Grant access') + label = _('Grant object access') widgets = { 'content_type': { 'class': 'django.forms.widgets.Select', 'kwargs': { @@ -140,7 +139,7 @@ class GrantAccessAction(WorkflowAction): class RevokeAccessAction(GrantAccessAction): - label = _('Revoke access') + label = _('Revoke object access') def execute(self, context): self.get_execute_data() diff --git a/mayan/apps/appearance/classes.py b/mayan/apps/appearance/classes.py index 9b05a39fc3..7f1ad604ec 100644 --- a/mayan/apps/appearance/classes.py +++ b/mayan/apps/appearance/classes.py @@ -4,6 +4,7 @@ from django.template.loader import get_template class IconDriver(object): + context = {} _registry = {} @classmethod @@ -14,6 +15,17 @@ class IconDriver(object): def register(cls, driver_class): cls._registry[driver_class.name] = driver_class + def get_context(self): + return self.context + + def render(self, extra_context=None): + context = self.get_context() + if extra_context: + context.update(extra_context) + return get_template(template_name=self.template_name).render( + context=context + ) + class FontAwesomeDriver(IconDriver): name = 'fontawesome' @@ -22,10 +34,8 @@ class FontAwesomeDriver(IconDriver): def __init__(self, symbol): self.symbol = symbol - def render(self): - return get_template(template_name=self.template_name).render( - context={'symbol': self.symbol} - ) + def get_context(self): + return {'symbol': self.symbol} class FontAwesomeDualDriver(IconDriver): @@ -36,23 +46,21 @@ class FontAwesomeDualDriver(IconDriver): self.primary_symbol = primary_symbol self.secondary_symbol = secondary_symbol - def render(self): - return get_template(template_name=self.template_name).render( - context={ - 'data': ( - { - 'class': 'fas fa-circle', - 'transform': 'down-3 right-10', - 'mask': 'fas fa-{}'.format(self.primary_symbol) - }, - {'class': 'far fa-circle', 'transform': 'down-3 right-10'}, - { - 'class': 'fas fa-{}'.format(self.secondary_symbol), - 'transform': 'shrink-4 down-3 right-10' - }, - ) - } - ) + def get_context(self): + return { + 'data': ( + { + 'class': 'fas fa-circle', + 'transform': 'down-3 right-10', + 'mask': 'fas fa-{}'.format(self.primary_symbol) + }, + {'class': 'far fa-circle', 'transform': 'down-3 right-10'}, + { + 'class': 'fas fa-{}'.format(self.secondary_symbol), + 'transform': 'shrink-4 down-3 right-10' + }, + ) + } class FontAwesomeCSSDriver(IconDriver): @@ -62,10 +70,8 @@ class FontAwesomeCSSDriver(IconDriver): def __init__(self, css_classes): self.css_classes = css_classes - def render(self): - return get_template(template_name=self.template_name).render( - context={'css_classes': self.css_classes} - ) + def get_context(self): + return {'css_classes': self.css_classes} class FontAwesomeMasksDriver(IconDriver): @@ -75,23 +81,23 @@ class FontAwesomeMasksDriver(IconDriver): def __init__(self, data): self.data = data - def render(self): - return get_template(template_name=self.template_name).render( - context={'data': self.data} - ) + def get_context(self): + return {'data': self.data} class FontAwesomeLayersDriver(IconDriver): name = 'fontawesome-layers' template_name = 'appearance/icons/font_awesome_layers.html' - def __init__(self, data): + def __init__(self, data, shadow_class=None): self.data = data + self.shadow_class = shadow_class - def render(self): - return get_template(template_name=self.template_name).render( - context={'data': self.data} - ) + def get_context(self): + return { + 'data': self.data, + 'shadow_class': self.shadow_class, + } class Icon(object): diff --git a/mayan/apps/appearance/literals.py b/mayan/apps/appearance/literals.py index 87ce87d1f8..fcf23b3c75 100644 --- a/mayan/apps/appearance/literals.py +++ b/mayan/apps/appearance/literals.py @@ -1 +1,2 @@ DEFAULT_MAXIMUM_TITLE_LENGTH = 120 +DEFAULT_MESSAGE_POSITION = 'top-right' diff --git a/mayan/apps/appearance/settings.py b/mayan/apps/appearance/settings.py index ad0927530a..990f3e168e 100644 --- a/mayan/apps/appearance/settings.py +++ b/mayan/apps/appearance/settings.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.smart_settings.classes import Namespace -from .literals import DEFAULT_MAXIMUM_TITLE_LENGTH +from .literals import DEFAULT_MAXIMUM_TITLE_LENGTH, DEFAULT_MESSAGE_POSITION namespace = Namespace(label=_('Appearance'), name='appearance') @@ -15,3 +15,11 @@ setting_max_title_length = namespace.add_setting( 'title.' ) ) +setting_message_position = namespace.add_setting( + default=DEFAULT_MESSAGE_POSITION, + global_name='APPEARANCE_MESSAGE_POSITION', help_text=_( + 'Position where the system message will be displayed. Options are: ' + 'top-left, top-center, top-right, bottom-left, bottom-center, ' + 'bottom-right.' + ) +) diff --git a/mayan/apps/appearance/static/appearance/css/base.css b/mayan/apps/appearance/static/appearance/css/base.css index ad14cf3326..982c544ea3 100644 --- a/mayan/apps/appearance/static/appearance/css/base.css +++ b/mayan/apps/appearance/static/appearance/css/base.css @@ -12,7 +12,7 @@ } body { - padding-top: 70px; + padding-top: 60px; } .navbar-brand { @@ -70,7 +70,8 @@ img.lazy-load-carousel { } .label-tag { - text-shadow: 0px 0px 2px #000 + text-shadow: 0px 0px 2px #000; + box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.5); } .fancybox-nav span { @@ -88,19 +89,17 @@ hr { } .btn-block { + border-top: 2px solid rgba(255, 255, 255, 0.7); + border-left: 2px solid rgba(255, 255, 255, 0.7); + border-right: 2px solid rgba(0, 0, 0, 0.7); + border-bottom: 2px solid rgba(0, 0, 0, 0.7); + box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5); margin-bottom: 15px; - white-space: normal; min-height: 120px; - padding-top: 20px; padding-bottom: 1px; -} - -.btn-block .fa { - text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3); -} - -.btn-block { - text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3); + padding-top: 20px; + text-shadow: 1px 1px 3px rgba(0, 0, 0, 1); + white-space: normal; } .radio ul li { @@ -112,14 +111,10 @@ a i { } .dashboard-widget { - box-shadow: 1px 1px 1px rgba(0,0,0,0.3); + box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.7); border: 1px solid black; } -.dashboard-widget .panel-heading i { - text-shadow: 1px 1px 1px rgba(0,0,0,0.3); -} - .dashboard-widget-icon { font-size: 200%; } @@ -170,7 +165,7 @@ a i { } .navbar-collapse { border-top: 1px solid transparent; - box-shadow: inset 0 1px 0 rgba(255,255,255,0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); } .navbar-fixed-top { top: 0; @@ -213,6 +208,22 @@ a i { font-weight: bold; } +.source-column-label { + font-weight: bold; +} + +.panel-highlighted { + box-shadow: 0px 0px 3px #18bc9c, 10px 10px 20px #000000; +} + +.panel-highlighted:hover { + box-shadow: 0px 0px 3px #18bc9c, 10px 10px 20px #000000, 0px 0px 8px #000000; +} + +.panel-item:not(.panel-highlighted):hover { + box-shadow: 0px 0px 8px #000000; +} + /* Content */ @media (min-width:1200px) { .container-fluid { @@ -242,14 +253,6 @@ a i { margin: auto; } -.thin_border { - border: 1px solid black; - display: block; - margin-left: auto; - margin-right: auto; -} - - .thin_border-thumbnail { display: block; max-width: 100%; @@ -259,10 +262,18 @@ a i { margin: auto; } +/* Must go after .thin_border-thumbnail */ +.thin_border { + border: 1px solid black; + display: inline; + margin-left: 0px; + margin-right: 0px; +} + #ajax-spinner { position: fixed; - top: 12px; - right: 10px; + top: 16px; + left: 10px; z-index: 9999; width: 25px; height: 25px; @@ -328,7 +339,7 @@ a i { .main { padding-right: 0px; padding-left: 0px; - /*margin-left: 210px;*/ + margin-left: 210px; } } @@ -411,6 +422,141 @@ a i { margin-bottom: 2px; } +/* + * Top navigation + * Hide default border to remove 1px line. + */ +.navbar-fixed-top { + border: 0; +} + + +/* menu_main */ +/* Hide for mobile, show later */ + +#menu-main { + display: none; + background-color: #2c3e50; + border-right: 1px solid #18bc9c; + bottom: 0; + left: 0; + overflow-x: hidden; + overflow-y: auto; + padding-top: 10px; + position: fixed; + top: 51px; + width: 210px; + z-index: 1000; +} + +@media (min-width: 768px) { + #menu-main { + display: block; + } + + .navbar-brand { + text-align: center; + width: 210px; + } +} + +.main .page-header { + margin-top: 0; +} + +.navbar-brand { +} + +.navbar-brand { + outline: none; +} + +.container-fluid { + margin-right: 0px; + margin-left: 0px; + width: 100%; +} + +#accordion-sidebar a { + padding: 10px 15px; +} + +#accordion-sidebar a[aria-expanded="true"] { + background: #1a242f; +} + +#accordion-sidebar .panel { + border: 0px; +} + +#accordion-sidebar a { + text-decoration: none; + outline: none; + position: relative; + display: block; +} + +#accordion-sidebar .panel-heading { + background-color: #2c3e50; + color: white; + padding: 0px; +} + +#accordion-sidebar .panel-heading:hover { + background-color: #517394; +} + +#accordion-sidebar > .panel > div > .panel-body > ul > li > a:hover { + background-color: #517394; +} + +#accordion-sidebar > .panel > div > .panel-body > ul > li.active { + background: #1a242f; +} + +#accordion-sidebar .panel-title { + font-size: 15px; +} + +#accordion-sidebar .panel-body { + font-size: 13px; + border: 0px; + background-color: #2c3e50; + padding-top: 5px; + padding-left: 20px; + padding-right: 0px; + padding-bottom: 0px; +} + +#accordion-sidebar .panel-body li { + padding: 0px; +} + +#accordion-sidebar .panel-body a { + color: white; + text-decoration: none; + padding: 9px; +} + +.navbar-fixed-top { + box-shadow: 0px 3px 3px rgba(0, 0, 0, 0.4); +} + +.toolbar { + border: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 1px 1px 2px rgba(0, 0, 0, .3); + margin-bottom: 10px; + padding-bottom: 8px; + padding-left: 12px; + padding-right: 15px; + padding-top: 8px; +} + +#body-plain { + padding-top: 0px; + margin-top: 10px; +} + /* jstree - cabinets */ #jstree { max-width: 100%; diff --git a/mayan/apps/appearance/static/appearance/js/base.js b/mayan/apps/appearance/static/appearance/js/base.js index 244ac0298a..ff87e82871 100644 --- a/mayan/apps/appearance/static/appearance/js/base.js +++ b/mayan/apps/appearance/static/appearance/js/base.js @@ -6,7 +6,8 @@ var MayanAppClass = MayanApp; var partialNavigation = new PartialNavigation({ initialURL: initialURL, - disabledAnchorClasses: ['disabled'], + disabledAnchorClasses: [ + 'btn-multi-item-action', 'disabled', 'pagination-disabled' + ], excludeAnchorClasses: ['fancybox', 'new_window', 'non-ajax'], - formBeforeSerializeCallbacks: [MayanApp.MultiObjectFormProcess], }); diff --git a/mayan/apps/appearance/static/appearance/js/mayan_app.js b/mayan/apps/appearance/static/appearance/js/mayan_app.js index fd65b1a6e6..ed8515162f 100644 --- a/mayan/apps/appearance/static/appearance/js/mayan_app.js +++ b/mayan/apps/appearance/static/appearance/js/mayan_app.js @@ -4,7 +4,7 @@ class MayanApp { constructor (options) { var self = this; - options = options || { + this.options = options || { ajaxMenusOptions: [] } @@ -17,28 +17,44 @@ class MayanApp { // Class methods and variables - static MultiObjectFormProcess ($form, options) { - /* - * ajaxForm callback to add the external item checkboxes to the - * submitted form - */ + static countChecked() { + var checkCount = $('.check-all-slave:checked').length; - if ($form.hasClass('form-multi-object-action')) { - // Turn form data into an object - var formArray = $form.serializeArray().reduce(function (obj, item) { - obj[item.name] = item.value; - return obj; - }, {}); + if (checkCount) { + $('#multi-item-title').hide(); + $('#multi-item-actions').show(); + } else { + $('#multi-item-title').show(); + $('#multi-item-actions').hide(); + } + } - // Add all checked checkboxes to the form data - $('.form-multi-object-action-checkbox:checked').each(function() { - var $this = $(this); - formArray[$this.attr('name')] = $this.attr('value'); + static setupMultiItemActions () { + $('body').on('change', '.check-all-slave', function () { + MayanApp.countChecked(); + }); + + $('body').on('click', '.btn-multi-item-action', function (event) { + var id_list = []; + $('.check-all-slave:checked').each(function (index, value) { + //Split the name (ie:"pk_200") and extract only the ID + id_list.push(value.name.split('_')[1]); + }); + event.preventDefault(); + partialNavigation.setLocation( + $(this).attr('href') + '?id_list=' + id_list.join(',') + ); + }); + } + + static setupNavBarState () { + $('body').on('click', '.a-main-menu-accordion-link', function (event) { + $('.a-main-menu-accordion-link').each(function (index, value) { + $(this).parent().removeClass('active'); }); - // Set the form data as the data to send - options.data = formArray; - } + $(this).parent().addClass('active'); + }); } static updateNavbarState () { @@ -46,8 +62,10 @@ class MayanApp { var uriFragment = uri.fragment(); $('.a-main-menu-accordion-link').each(function (index, value) { if (value.pathname === uriFragment) { - $(this).closest('.collapse').addClass('in').parent().find('.collapsed').removeClass('collapsed').attr('aria-expanded', 'true'); - $(this).parent().addClass('active'); + var $this = $(this); + + $this.closest('.collapse').addClass('in').parent().find('.collapsed').removeClass('collapsed').attr('aria-expanded', 'true'); + $this.parent().addClass('active'); } }); } @@ -92,7 +110,7 @@ class MayanApp { 'closeButton': true, 'debug': false, 'newestOnTop': true, - 'positionClass': 'toast-top-right', + 'positionClass': 'toast-' + this.options.messagePosition, 'preventDuplicates': false, 'onclick': null, 'showDuration': '300', @@ -162,17 +180,19 @@ class MayanApp { var self = this; this.setupAJAXSpinner(); - this.setupAutoSubmit(); this.setupBodyAdjust(); this.setupFormHotkeys(); this.setupFullHeightResizing(); this.setupItemsSelector(); + MayanApp.setupMultiItemActions(); this.setupNavbarCollapse(); + MayanApp.setupNavBarState(); this.setupNewWindowAnchor(); $.each(this.ajaxMenusOptions, function(index, value) { value.app = self; app.doRefreshAJAXMenu(value); }); + this.setupPanelSelection(); partialNavigation.initialize(); } @@ -196,14 +216,6 @@ class MayanApp { }); } - setupAutoSubmit () { - $('body').on('change', '.select-auto-submit', function () { - if ($(this).val()) { - $(this.form).trigger('submit'); - } - }); - } - setupBodyAdjust () { var self = this; @@ -242,9 +254,22 @@ class MayanApp { app.lastChecked = null; $('body').on('click', '.check-all', function (event) { + var $this = $(this); var checked = $(event.target).prop('checked'); var $checkBoxes = $('.check-all-slave'); + if (checked === undefined) { + checked = $this.data('checked'); + checked = !checked; + $this.data('checked', checked); + + if (checked) { + $this.find('[data-fa-i2svg]').addClass($this.data('icon-checked')).removeClass($this.data('icon-unchecked')); + } else { + $this.find('[data-fa-i2svg]').addClass($this.data('icon-unchecked')).removeClass($this.data('icon-checked')); + } + } + $checkBoxes.prop('checked', checked); $checkBoxes.trigger('change'); }); @@ -290,6 +315,58 @@ class MayanApp { }); } + setupPanelSelection () { + var app = this; + + // Setup panel highlighting on check + $('body').on('change', '.check-all-slave', function (event) { + var checked = $(event.target).prop('checked'); + if (checked) { + $(this).closest('.panel-item').addClass('panel-highlighted'); + } else { + $(this).closest('.panel-item').removeClass('panel-highlighted'); + } + }); + + $('body').on('click', '.panel-item', function (event) { + var $this = $(this); + var targetSrc = $(event.target).prop('src'); + var targetHref = $(event.target).prop('href'); + var targetIsButton = event.target.tagName === 'BUTTON'; + var lastChecked = null; + + if ((targetSrc === undefined) && (targetHref === undefined) && (targetIsButton === false)) { + var $checkbox = $this.find('.check-all-slave'); + var checked = $checkbox.prop('checked'); + + if (checked) { + $checkbox.prop('checked', ''); + $checkbox.trigger('change'); + } else { + $checkbox.prop('checked', 'checked'); + $checkbox.trigger('change'); + } + + if(!app.lastChecked) { + app.lastChecked = $checkbox; + } + + if (event.shiftKey) { + var $checkBoxes = $('.check-all-slave'); + + var start = $checkBoxes.index($checkbox); + var end = $checkBoxes.index(app.lastChecked); + + $checkBoxes.slice( + Math.min(start, end), Math.max(start, end) + 1 + ).prop('checked', app.lastChecked.prop('checked')).trigger('change'); + } + app.lastChecked = $checkbox; + window.getSelection().removeAllRanges(); + } + }); + } + setupScrollView () { $('.scrollable').scrollview(); } diff --git a/mayan/apps/appearance/templates/appearance/base.html b/mayan/apps/appearance/templates/appearance/base.html index 46056cf83a..e742b2c995 100644 --- a/mayan/apps/appearance/templates/appearance/base.html +++ b/mayan/apps/appearance/templates/appearance/base.html @@ -23,6 +23,7 @@ {% block content_plain %}{% endblock %} {% else %}
+ {% navigation_resolve_menus names='facet,list facet' sort_results=True as facet_menus_link_results %}
@@ -43,7 +44,7 @@ {% if settings_changed %}
-

{% trans 'Warning' %} {% trans 'Settings updated, restart your installation for changes to take proper effect.' %}

+

{% trans 'Warning' %} {% trans 'Settings updated, restart your installation and refresh your browser for changes to take effect.' %}

{% endif %}
@@ -138,6 +139,9 @@ }, {% endfor %} ]; + $(function () { + $('[data-toggle="tooltip"]').tooltip(); + }) {% block javascript %}{% endblock %} diff --git a/mayan/apps/appearance/templates/appearance/base_plain.html b/mayan/apps/appearance/templates/appearance/base_plain.html index 7a5b8e38a6..e4ea07764d 100644 --- a/mayan/apps/appearance/templates/appearance/base_plain.html +++ b/mayan/apps/appearance/templates/appearance/base_plain.html @@ -32,7 +32,7 @@ } - + {% block content_plain %}{% endblock %} diff --git a/mayan/apps/appearance/templates/appearance/generic_form_instance.html b/mayan/apps/appearance/templates/appearance/generic_form_instance.html index a30a8eb1db..d8aa93755b 100644 --- a/mayan/apps/appearance/templates/appearance/generic_form_instance.html +++ b/mayan/apps/appearance/templates/appearance/generic_form_instance.html @@ -45,7 +45,7 @@ {{ field }} {% endfor %} {% for field in form.visible_fields %} -
+
{# We display the label then the field for all except checkboxes #} {% if field|widget_type != 'checkboxinput' and not field.field.widget.attrs.hidden %} {% if not hide_labels %}{{ field.label_tag }}{% if field.field.required and not read_only %} ({% trans 'required' %}){% endif %}{% endif %} diff --git a/mayan/apps/appearance/templates/appearance/generic_list_items_subtemplate.html b/mayan/apps/appearance/templates/appearance/generic_list_items_subtemplate.html index 991162c0c0..97436d3aa6 100644 --- a/mayan/apps/appearance/templates/appearance/generic_list_items_subtemplate.html +++ b/mayan/apps/appearance/templates/appearance/generic_list_items_subtemplate.html @@ -11,41 +11,9 @@ {% include 'appearance/no_results.html' %}
{% else %} -

- {% if page_obj %} - {% if page_obj.paginator.num_pages != 1 %} - {% blocktrans with page_obj.start_index as start and page_obj.end_index as end and page_obj.paginator.object_list|length as total and page_obj.number as page_number and page_obj.paginator.num_pages as total_pages %}Total ({{ start }} - {{ end }} out of {{ total }}) (Page {{ page_number }} of {{ total_pages }}){% endblocktrans %} - {% else %} - {% blocktrans with page_obj.paginator.object_list|length as total %}Total: {{ total }}{% endblocktrans %} - {% endif %} - {% else %} - {% blocktrans with object_list|length as total %}Total: {{ total }}{% endblocktrans %} - {% endif %} -

-
+ {% include "appearance/list_header.html" %} + {% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
-
-
-
- {% if object_list %} - {% if not hide_multi_item_actions %} - {% get_multi_item_links_form object_list %} - {% endif %} - {% if multi_item_actions %} -
-   - {{ multi_item_form }} -
- {% endif %} - {% endif %} -
-
-
- - {% if object_list %} -
- {% endif %} -
{% for object in object_list %}
@@ -53,9 +21,9 @@
-
- {% if not hide_columns %} {% navigation_get_source_columns source=object exclude_identifier=True as source_columns %} {% for column in source_columns %} -
{% navigation_source_column_resolve column=column as column_value %}{% if column_value != '' %}{% if column.include_label %}{{ column.label }}: {% endif %}{{ column_value }}{% endif %}
+
{% navigation_source_column_resolve column=column as column_value %}{% if column_value != '' %}{% if column.include_label %}{{ column.label }}: {% endif %}{{ column_value }}{% endif %}
{% endfor %} {% endif %} @@ -136,7 +98,6 @@
{% endfor %}
- {% include 'pagination/pagination.html' %}
{% endif %}
diff --git a/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html b/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html index d0aa3d6b88..5d2ea6b952 100644 --- a/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html +++ b/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html @@ -1,6 +1,7 @@ {% load i18n %} {% load static %} +{% load appearance_tags %} {% load common_tags %} {% load navigation_tags %} @@ -11,44 +12,16 @@ {% include 'appearance/no_results.html' %}
{% else %} -

- {% if page_obj %} - {% if page_obj.paginator.num_pages != 1 %} - {% blocktrans with page_obj.start_index as start and page_obj.end_index as end and page_obj.paginator.object_list|length as total and page_obj.number as page_number and page_obj.paginator.num_pages as total_pages %}Total ({{ start }} - {{ end }} out of {{ total }}) (Page {{ page_number }} of {{ total_pages }}){% endblocktrans %} - {% else %} - {% blocktrans with page_obj.paginator.object_list|length as total %}Total: {{ total }}{% endblocktrans %} - {% endif %} - {% else %} - {% blocktrans with object_list|length as total %}Total: {{ total }}{% endblocktrans %} - {% endif %} -

-
- + {% include "appearance/list_header.html" %} + {% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
-
-
-
- {% if object_list %} - {% if not hide_multi_item_actions %} - {% get_multi_item_links_form object_list %} - {% endif %} - {% if multi_item_actions %} -
- {{ multi_item_form }} -
- {% endif %} - {% endif %} -
-
-
-
{% if not hide_header %} - {% if multi_item_actions %} - + {% if links_multi_menus_results %} + {% endif %} {% if not hide_object %} @@ -57,32 +30,46 @@ {% navigation_get_source_columns source=object_list only_identifier=True as source_column %} {% if source_column %} {% endif %} {% endif %} {% if not hide_columns %} {% navigation_get_source_columns source=object_list exclude_identifier=True as source_columns %} - {% for column in source_columns %} + {% for source_column in source_columns %} {% endfor %} {% endif %} @@ -99,9 +86,9 @@ {% for object in object_list %} - {% if multi_item_actions %} + {% if links_multi_menus_results %} {% endif %} @@ -112,11 +99,7 @@ {% navigation_source_column_resolve column=source_column as column_value %} {% if column_value %} {% endif %} {% endif %} @@ -170,7 +153,6 @@
- {% if source_column.is_sortable %} - {{ source_column.label }} - {% if source_column.get_sort_field == sort_field %} - {% if icon_sort %}{{ icon_sort.render }}{% endif %} + + {% if source_column.is_sortable %} + {{ source_column.label }} + {% if source_column.get_sort_field == sort_field %} + {% if icon_sort %}{{ icon_sort.render }}{% endif %} + {% endif %} + {% else %} + {{ source_column.label }} {% endif %} - - {% else %} - {{ source_column.label }} - {% endif %} + + {% if source_column.help_text %} + + {% get_icon icon_path='mayan.apps.navigation.icons.icon_source_column_help_text' %} + + {% endif %} + - {% if column.is_sortable %} - {{ column.label }} - {% if column.get_sort_field == sort_field %} - {% if icon_sort %}{{ icon_sort.render }}{% endif %} + + {% if source_column.is_sortable %} + {{ source_column.label }} + {% if source_column.get_sort_field == sort_field %} + {% if icon_sort %}{{ icon_sort.render }}{% endif %} + {% endif %} + {% else %} + {{ source_column.label }} {% endif %} - - {% else %} - {{ column.label }} - {% endif %} + + {% if source_column.help_text %} + + {% get_icon icon_path='mayan.apps.navigation.icons.icon_source_column_help_text' %} + + {% endif %} +
- + - {% if source_column.is_attribute_absolute_url or source_column.is_object_absolute_url %} - {{ column_value }} - {% else %} - {{ column_value }} - {% endif %} + {{ column_value }}
- {% include 'pagination/pagination.html' %}
{% endif %}
diff --git a/mayan/apps/appearance/templates/appearance/icons/font_awesome_layers.html b/mayan/apps/appearance/templates/appearance/icons/font_awesome_layers.html index 4356ad89e1..8a8b454966 100644 --- a/mayan/apps/appearance/templates/appearance/icons/font_awesome_layers.html +++ b/mayan/apps/appearance/templates/appearance/icons/font_awesome_layers.html @@ -1,4 +1,7 @@ + {% if enable_shadow %} + + {% endif %} {% for entry in data %} {% endfor %} diff --git a/mayan/apps/appearance/templates/appearance/icons/font_awesome_symbol.html b/mayan/apps/appearance/templates/appearance/icons/font_awesome_symbol.html index fac6aeca4d..84e7b87eb8 100644 --- a/mayan/apps/appearance/templates/appearance/icons/font_awesome_symbol.html +++ b/mayan/apps/appearance/templates/appearance/icons/font_awesome_symbol.html @@ -1 +1,8 @@ - +{% if enable_shadow %} + + + + +{% else %} + +{% endif %} diff --git a/mayan/apps/appearance/templates/appearance/list_header.html b/mayan/apps/appearance/templates/appearance/list_header.html new file mode 100644 index 0000000000..ce310e8419 --- /dev/null +++ b/mayan/apps/appearance/templates/appearance/list_header.html @@ -0,0 +1,28 @@ +{% load i18n %} +{% load static %} + +{% load common_tags %} +{% load navigation_tags %} + +{% if object_list %} +

+ {% if page_obj %} + {% if page_obj.paginator.num_pages != 1 %} + {% blocktrans with page_obj.start_index as start and page_obj.end_index as end and page_obj.paginator.object_list|length as total and page_obj.number as page_number and page_obj.paginator.num_pages as total_pages %}Total ({{ start }} - {{ end }} out of {{ total }}) (Page {{ page_number }} of {{ total_pages }}){% endblocktrans %} + {% else %} + {% blocktrans with page_obj.paginator.object_list|length as total %}Total: {{ total }}{% endblocktrans %} + {% endif %} + {% else %} + {% blocktrans with object_list|length as total %}Total: {{ total }}{% endblocktrans %} + {% endif %} +

+
+ + {% if not hide_multi_item_actions %} + {% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %} + {% endif %} +{% endif %} + +
+ {% include 'appearance/list_toolbar.html' %} +
diff --git a/mayan/apps/appearance/templates/appearance/list_toolbar.html b/mayan/apps/appearance/templates/appearance/list_toolbar.html new file mode 100644 index 0000000000..72a4148390 --- /dev/null +++ b/mayan/apps/appearance/templates/appearance/list_toolbar.html @@ -0,0 +1,90 @@ +{% load i18n %} + +{% load common_tags %} +{% load navigation_tags %} + +{% if is_paginated or links_multi_menus_results %} +
+{% endif %} + + {% if links_multi_menus_results %} +
+ +
+ {% endif %} + + {% if is_paginated %} +
+ +
+ {% endif %} + + {% if links_multi_menus_results %} +

{% trans 'Select items to activate bulk actions. Use Shift + click to select many.' %}

+ + + {% endif %} + +{% if is_paginated or links_multi_menus_results %} +
+
+{% endif %} diff --git a/mayan/apps/appearance/templates/appearance/menu_main.html b/mayan/apps/appearance/templates/appearance/menu_main.html new file mode 100644 index 0000000000..d9e356607a --- /dev/null +++ b/mayan/apps/appearance/templates/appearance/menu_main.html @@ -0,0 +1,70 @@ +{% load i18n %} + +{% load navigation_tags %} +{% load smart_settings_tags %} + +{% load common_tags %} +{% load navigation_tags %} + +{% spaceless %} +
+ {% 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 == "" %} +
+ +
+
+
    + {% 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 %} +
+
+
+
+ {% else %} +
+ +
+ {% endif %} + {% endwith %} + {% endwith %} + {% endfor %} + {% endfor %} + {% endfor %} +
+{% endspaceless %} diff --git a/mayan/apps/appearance/templates/appearance/main_menu.html b/mayan/apps/appearance/templates/appearance/menu_topbar.html similarity index 59% rename from mayan/apps/appearance/templates/appearance/main_menu.html rename to mayan/apps/appearance/templates/appearance/menu_topbar.html index 5d41c7ee1b..b9401818a1 100644 --- a/mayan/apps/appearance/templates/appearance/main_menu.html +++ b/mayan/apps/appearance/templates/appearance/menu_topbar.html @@ -3,10 +3,11 @@ {% load navigation_tags %} {% load smart_settings_tags %} +{% spaceless %} +{% endspaceless %} diff --git a/mayan/apps/appearance/templates/appearance/root.html b/mayan/apps/appearance/templates/appearance/root.html index 35a29851f1..a261bf50a1 100644 --- a/mayan/apps/appearance/templates/appearance/root.html +++ b/mayan/apps/appearance/templates/appearance/root.html @@ -31,8 +31,11 @@ {% if appearance_type == 'plain' %} {% block content_plain %}{% endblock %} {% else %} +
@@ -102,6 +105,7 @@ ajaxMenusOptions: [ { callback: function (options) { + MayanApp.updateNavbarState(); options.app.doBodyAdjust(); }, interval: 5000, @@ -109,7 +113,14 @@ name: 'menu_main', url: '{% url "rest_api:template-detail" "menu_main" %}' }, - ] + { + interval: 5000, + menuSelector: '#menu-topbar', + name: 'menu_topbar', + url: '{% url "rest_api:template-detail" "menu_topbar" %}' + }, + ], + messagePosition: '{% smart_setting "APPEARANCE_MESSAGE_POSITION" %}' }); var afterBaseLoad = function () { diff --git a/mayan/apps/appearance/templates/pagination/pagination.html b/mayan/apps/appearance/templates/pagination/pagination.html index a1cdc9bfa0..829a6f958b 100644 --- a/mayan/apps/appearance/templates/pagination/pagination.html +++ b/mayan/apps/appearance/templates/pagination/pagination.html @@ -11,7 +11,7 @@ {% if page %} {% ifequal page page_obj.number %} -
  • {{ page }}
  • +
  • {{ page }}
  • {% else %}
  • {{ page }}
  • {% endifequal %} diff --git a/mayan/apps/appearance/templatetags/appearance_tags.py b/mayan/apps/appearance/templatetags/appearance_tags.py index 05c3ba043d..eb9cf1bc02 100644 --- a/mayan/apps/appearance/templatetags/appearance_tags.py +++ b/mayan/apps/appearance/templatetags/appearance_tags.py @@ -7,6 +7,11 @@ from django.utils.translation import ugettext_lazy as _ register = Library() +@register.simple_tag +def appearance_icon_render(icon_class, enable_shadow=False): + return icon_class.render(extra_context={'enable_shadow': enable_shadow}) + + @register.filter def get_choice_value(field): try: diff --git a/mayan/apps/authentication/tests/test_views.py b/mayan/apps/authentication/tests/test_views.py index f0c02e9c04..841bdee3ee 100644 --- a/mayan/apps/authentication/tests/test_views.py +++ b/mayan/apps/authentication/tests/test_views.py @@ -11,7 +11,7 @@ from django.test import override_settings from django.urls import reverse from django.utils.http import urlunquote_plus -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from mayan.apps.smart_settings.classes import Namespace from mayan.apps.user_management.permissions import permission_user_edit from mayan.apps.user_management.tests.literals import TEST_USER_PASSWORD_EDITED diff --git a/mayan/apps/autoadmin/templates/autoadmin/credentials.html b/mayan/apps/autoadmin/templates/autoadmin/credentials.html index 3a258c8034..3db5b857c0 100755 --- a/mayan/apps/autoadmin/templates/autoadmin/credentials.html +++ b/mayan/apps/autoadmin/templates/autoadmin/credentials.html @@ -4,21 +4,23 @@ {% if autoadmin_properties.account %}
    -
    +

    -

    {% trans 'First time login' %}

    +

    {% trans 'Automatic credentials' %}

    diff --git a/mayan/apps/autoadmin/tests/literals.py b/mayan/apps/autoadmin/tests/literals.py index c217b05800..fa6ab9456b 100644 --- a/mayan/apps/autoadmin/tests/literals.py +++ b/mayan/apps/autoadmin/tests/literals.py @@ -3,5 +3,5 @@ from __future__ import unicode_literals TEST_ADMIN_USER_EMAIL = 'testemail@example.com' TEST_ADMIN_USER_PASSWORD = 'test admin user password' TEST_ADMIN_USER_USERNAME = 'test_admin_user_username' -TEST_FIRST_TIME_LOGIN_TEXT = 'First time login' +TEST_FIRST_TIME_LOGIN_TEXT = 'Automatic credentials' TEST_MOCK_VIEW_TEXT = 'mock view text' diff --git a/mayan/apps/autoadmin/tests/test_management_commands.py b/mayan/apps/autoadmin/tests/test_management_commands.py index 9de71acd1f..1ddc6e7835 100644 --- a/mayan/apps/autoadmin/tests/test_management_commands.py +++ b/mayan/apps/autoadmin/tests/test_management_commands.py @@ -2,20 +2,24 @@ from __future__ import unicode_literals from django.contrib.auth import get_user_model from django.core import management -from django.test import TestCase +from mayan.apps.common.tests.base import BaseTestCase from mayan.apps.common.tests.utils import mute_stdout from ..models import AutoAdminSingleton -class AutoAdminManagementCommandTestCase(TestCase): +class AutoAdminManagementCommandTestCase(BaseTestCase): + create_test_case_user = False + def setUp(self): + super(AutoAdminManagementCommandTestCase, self).setUp() with mute_stdout(): management.call_command('createautoadmin') def tearDown(self): AutoAdminSingleton.objects.all().delete() + super(AutoAdminManagementCommandTestCase, self).tearDown() def test_autoadmin_creation(self): autoadmin = AutoAdminSingleton.objects.get() diff --git a/mayan/apps/autoadmin/tests/test_models.py b/mayan/apps/autoadmin/tests/test_models.py index f45f5ede31..b1f74ccef7 100644 --- a/mayan/apps/autoadmin/tests/test_models.py +++ b/mayan/apps/autoadmin/tests/test_models.py @@ -2,8 +2,7 @@ from __future__ import unicode_literals import logging -from django.test import TestCase - +from mayan.apps.common.tests.base import BaseTestCase from mayan.apps.common.tests.utils import mute_stdout from ..models import AutoAdminSingleton @@ -12,7 +11,7 @@ from ..settings import setting_username from .literals import TEST_ADMIN_USER_PASSWORD -class AutoAdminHandlerTestCase(TestCase): +class AutoAdminHandlerTestCase(BaseTestCase): def test_post_admin_creation(self): logging.disable(logging.INFO) diff --git a/mayan/apps/autoadmin/tests/test_views.py b/mayan/apps/autoadmin/tests/test_views.py index a5a90aa977..5d49ec3eef 100644 --- a/mayan/apps/autoadmin/tests/test_views.py +++ b/mayan/apps/autoadmin/tests/test_views.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from mayan.apps.common.settings import setting_home_view -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from mayan.apps.common.tests.utils import mute_stdout from ..models import AutoAdminSingleton diff --git a/mayan/apps/cabinets/migrations/0002_auto_20190729_0236.py b/mayan/apps/cabinets/migrations/0002_auto_20190729_0236.py new file mode 100644 index 0000000000..415e3eb0fa --- /dev/null +++ b/mayan/apps/cabinets/migrations/0002_auto_20190729_0236.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-07-29 02:36 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cabinets', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='cabinet', + name='label', + field=models.CharField(help_text='A short text used to identify the cabinet.', max_length=128, verbose_name='Label'), + ), + ] diff --git a/mayan/apps/cabinets/models.py b/mayan/apps/cabinets/models.py index b59e637157..e5fc8d623e 100644 --- a/mayan/apps/cabinets/models.py +++ b/mayan/apps/cabinets/models.py @@ -32,7 +32,10 @@ class Cabinet(MPTTModel): blank=True, db_index=True, null=True, on_delete=models.CASCADE, related_name='children', to='self' ) - label = models.CharField(max_length=128, verbose_name=_('Label')) + label = models.CharField( + help_text=_('A short text used to identify the cabinet.'), + max_length=128, verbose_name=_('Label') + ) documents = models.ManyToManyField( blank=True, related_name='cabinets', to=Document, verbose_name=_('Documents') diff --git a/mayan/apps/cabinets/tests/test_api.py b/mayan/apps/cabinets/tests/test_api.py index 4fe870c221..8e71ce2e0e 100644 --- a/mayan/apps/cabinets/tests/test_api.py +++ b/mayan/apps/cabinets/tests/test_api.py @@ -5,8 +5,8 @@ from django.utils.encoding import force_text from rest_framework import status from mayan.apps.documents.permissions import permission_document_view -from mayan.apps.documents.tests import DocumentTestMixin -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.documents.tests.mixins import DocumentTestMixin +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..models import Cabinet from ..permissions import ( diff --git a/mayan/apps/cabinets/tests/test_events.py b/mayan/apps/cabinets/tests/test_events.py index a0a7961deb..f29eaf511c 100644 --- a/mayan/apps/cabinets/tests/test_events.py +++ b/mayan/apps/cabinets/tests/test_events.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from actstream.models import Action -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from mayan.apps.documents.tests.test_models import GenericDocumentTestCase from ..events import ( diff --git a/mayan/apps/cabinets/tests/test_models.py b/mayan/apps/cabinets/tests/test_models.py index 51305515ab..b18b3eb93f 100644 --- a/mayan/apps/cabinets/tests/test_models.py +++ b/mayan/apps/cabinets/tests/test_models.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals from django.core.exceptions import ValidationError -from mayan.apps.common.tests import BaseTestCase -from mayan.apps.documents.tests import DocumentTestMixin +from mayan.apps.common.tests.base import BaseTestCase +from mayan.apps.documents.tests.mixins import DocumentTestMixin from ..models import Cabinet diff --git a/mayan/apps/cabinets/tests/test_views.py b/mayan/apps/cabinets/tests/test_views.py index 894a01d3eb..44bf735705 100644 --- a/mayan/apps/cabinets/tests/test_views.py +++ b/mayan/apps/cabinets/tests/test_views.py @@ -1,8 +1,8 @@ from __future__ import absolute_import, unicode_literals -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from mayan.apps.documents.permissions import permission_document_view -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from ..models import Cabinet from ..permissions import ( diff --git a/mayan/apps/cabinets/tests/test_wizard_steps.py b/mayan/apps/cabinets/tests/test_wizard_steps.py index bcdc7f30ce..84f7fabd82 100644 --- a/mayan/apps/cabinets/tests/test_wizard_steps.py +++ b/mayan/apps/cabinets/tests/test_wizard_steps.py @@ -2,9 +2,8 @@ from __future__ import unicode_literals from mayan.apps.documents.models import Document from mayan.apps.documents.permissions import permission_document_create -from mayan.apps.documents.tests import ( - GenericDocumentViewTestCase, TEST_SMALL_DOCUMENT_PATH, -) +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase +from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH from mayan.apps.sources.models import WebFormSource from mayan.apps.sources.tests.literals import ( TEST_SOURCE_LABEL, TEST_SOURCE_UNCOMPRESS_N diff --git a/mayan/apps/cabinets/tests/test_workflow_actions.py b/mayan/apps/cabinets/tests/test_workflow_actions.py index eb010b55c5..88c8697411 100644 --- a/mayan/apps/cabinets/tests/test_workflow_actions.py +++ b/mayan/apps/cabinets/tests/test_workflow_actions.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from mayan.apps.document_states.tests.mixins import WorkflowTestMixin from mayan.apps.document_states.tests.test_workflow_actions import ActionTestCase @@ -46,7 +46,7 @@ class CabinetWorkflowActionViewTestCase( self._create_test_workflow_state() response = self.get( - viewname='document_states:setup_workflow_state_action_create', + viewname='document_states:workflow_template_state_action_create', kwargs={ 'pk': self.test_workflow_state.pk, 'class_path': 'mayan.apps.cabinets.workflow_actions.CabinetAddAction' @@ -61,7 +61,7 @@ class CabinetWorkflowActionViewTestCase( self._create_test_cabinet() response = self.get( - viewname='document_states:setup_workflow_state_action_create', + viewname='document_states:workflow_template_state_action_create', kwargs={ 'pk': self.test_workflow_state.pk, 'class_path': 'mayan.apps.cabinets.workflow_actions.CabinetRemoveAction' diff --git a/mayan/apps/cabinets/urls.py b/mayan/apps/cabinets/urls.py index 4703ddeab7..ee87035a40 100644 --- a/mayan/apps/cabinets/urls.py +++ b/mayan/apps/cabinets/urls.py @@ -12,55 +12,62 @@ from .views import ( CabinetDeleteView, CabinetDetailView, CabinetEditView, CabinetListView, ) -urlpatterns = [ +urlpatterns_cabinets = [ url( - regex=r'^list/$', view=CabinetListView.as_view(), name='cabinet_list' + regex=r'^cabinets/$', view=CabinetListView.as_view(), name='cabinet_list' ), url( - regex=r'^(?P\d+)/child/add/$', view=CabinetChildAddView.as_view(), - name='cabinet_child_add' - ), - url( - regex=r'^create/$', view=CabinetCreateView.as_view(), + regex=r'^cabinets/create/$', view=CabinetCreateView.as_view(), name='cabinet_create' ), url( - regex=r'^(?P\d+)/edit/$', view=CabinetEditView.as_view(), - name='cabinet_edit' + regex=r'^cabinets/(?P\d+)/children/add/$', view=CabinetChildAddView.as_view(), + name='cabinet_child_add' ), url( - regex=r'^(?P\d+)/delete/$', view=CabinetDeleteView.as_view(), + regex=r'^cabinets/(?P\d+)/delete/$', view=CabinetDeleteView.as_view(), name='cabinet_delete' ), url( - regex=r'^(?P\d+)/$', view=CabinetDetailView.as_view(), - name='cabinet_view' + regex=r'^cabinets/(?P\d+)/edit/$', view=CabinetEditView.as_view(), + name='cabinet_edit' ), url( - regex=r'^document/(?P\d+)/cabinet/add/$', + regex=r'^cabinets/(?P\d+)/$', view=CabinetDetailView.as_view(), + name='cabinet_view' + ), +] + +urlpatterns_documents_cabinets = [ + url( + regex=r'^documents/(?P\d+)/cabinets/add/$', view=DocumentAddToCabinetView.as_view(), name='document_cabinet_add' ), url( - regex=r'^document/multiple/cabinet/add/$', + regex=r'^documents/multiple/cabinets/add/$', view=DocumentAddToCabinetView.as_view(), name='document_multiple_cabinet_add' ), url( - regex=r'^document/(?P\d+)/cabinet/remove/$', + regex=r'^documents/(?P\d+)/cabinets/remove/$', view=DocumentRemoveFromCabinetView.as_view(), name='document_cabinet_remove' ), url( - regex=r'^document/multiple/cabinet/remove/$', + regex=r'^documents/multiple/cabinets/remove/$', view=DocumentRemoveFromCabinetView.as_view(), name='multiple_document_cabinet_remove' ), url( - regex=r'^document/(?P\d+)/cabinet/list/$', + regex=r'^documents/(?P\d+)/cabinets/$', view=DocumentCabinetListView.as_view(), name='document_cabinet_list' ), ] +urlpatterns = [] +urlpatterns.extend(urlpatterns_cabinets) +urlpatterns.extend(urlpatterns_documents_cabinets) + api_urls = [ url( regex=r'^cabinets/(?P[0-9]+)/documents/(?P[0-9]+)/$', diff --git a/mayan/apps/checkouts/apps.py b/mayan/apps/checkouts/apps.py index 6f9e5b7c00..7c067edb0b 100644 --- a/mayan/apps/checkouts/apps.py +++ b/mayan/apps/checkouts/apps.py @@ -6,9 +6,12 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.acls.classes import ModelPermission from mayan.apps.common.apps import MayanAppConfig -from mayan.apps.common.menus import menu_facet, menu_main, menu_secondary +from mayan.apps.common.menus import ( + menu_facet, menu_main, menu_multi_item, menu_secondary +) from mayan.apps.dashboards.dashboards import dashboard_main from mayan.apps.events.classes import ModelEventType +from mayan.apps.navigation.classes import SourceColumn from .dashboard_widgets import DashboardWidgetTotalCheckouts from .events import ( @@ -17,8 +20,9 @@ from .events import ( ) from .handlers import handler_check_new_version_creation from .links import ( - link_check_in_document, link_check_out_document, link_check_out_info, - link_check_out_list + link_check_in_document, link_check_in_document_multiple, + link_check_out_document, link_check_out_document_multiple, + link_check_out_info, link_check_out_list ) from .methods import ( method_check_in, method_get_check_out_info, method_get_check_out_state, @@ -43,6 +47,8 @@ class CheckoutsApp(MayanAppConfig): def ready(self): super(CheckoutsApp, self).ready() + CheckedOutDocument = self.get_model(model_name='CheckedOutDocument') + DocumentCheckout = self.get_model(model_name='DocumentCheckout') Document = apps.get_model( app_label='documents', model_name='Document' ) @@ -76,6 +82,22 @@ class CheckoutsApp(MayanAppConfig): permission_document_check_out_detail_view ) ) + ModelPermission.register_inheritance( + model=DocumentCheckout, related='document' + ) + + SourceColumn( + attribute='get_user_display', include_label=True, order=99, + source=CheckedOutDocument + ) + SourceColumn( + attribute='get_checkout_datetime', include_label=True, order=99, + source=CheckedOutDocument + ) + SourceColumn( + attribute='get_checkout_expiration', include_label=True, order=99, + source=CheckedOutDocument + ) dashboard_main.add_widget( widget=DashboardWidgetTotalCheckouts, order=-1 @@ -85,6 +107,22 @@ class CheckoutsApp(MayanAppConfig): links=(link_check_out_info,), sources=(Document,) ) menu_main.bind_links(links=(link_check_out_list,), position=98) + menu_multi_item.bind_links( + links=( + link_check_in_document_multiple, + ), sources=(CheckedOutDocument,) + ) + menu_multi_item.bind_links( + links=( + link_check_in_document_multiple, + link_check_out_document_multiple, + ), sources=(Document,) + ) + menu_multi_item.unbind_links( + links=( + link_check_out_document_multiple, + ), sources=(CheckedOutDocument,) + ) menu_secondary.bind_links( links=(link_check_out_document, link_check_in_document), sources=( diff --git a/mayan/apps/checkouts/forms.py b/mayan/apps/checkouts/forms.py index 9375dcb00f..9939f75d17 100644 --- a/mayan/apps/checkouts/forms.py +++ b/mayan/apps/checkouts/forms.py @@ -10,7 +10,7 @@ from .models import DocumentCheckout from .widgets import SplitTimeDeltaWidget -class DocumentCheckoutForm(forms.ModelForm): +class DocumentCheckOutForm(forms.ModelForm): class Meta: fields = ('expiration_datetime', 'block_new_version') model = DocumentCheckout @@ -19,7 +19,7 @@ class DocumentCheckoutForm(forms.ModelForm): } -class DocumentCheckoutDefailForm(DetailForm): +class DocumentCheckOutDetailForm(DetailForm): def __init__(self, *args, **kwargs): instance = kwargs['instance'] @@ -56,7 +56,7 @@ class DocumentCheckoutDefailForm(DetailForm): ) kwargs['extra_fields'] = extra_fields - super(DocumentCheckoutDefailForm, self).__init__(*args, **kwargs) + super(DocumentCheckOutDetailForm, self).__init__(*args, **kwargs) class Meta: fields = () diff --git a/mayan/apps/checkouts/links.py b/mayan/apps/checkouts/links.py index d03d54c23d..c8012653c9 100644 --- a/mayan/apps/checkouts/links.py +++ b/mayan/apps/checkouts/links.py @@ -38,16 +38,26 @@ link_check_out_document = Link( args='object.pk', condition=is_not_checked_out, icon_class=icon_check_out_document, permissions=(permission_document_check_out,), - text=_('Check out document'), view='checkouts:check_out_document', + text=_('Check out document'), view='checkouts:check_out_document' +) +link_check_out_document_multiple = Link( + icon_class=icon_check_out_document, + permissions=(permission_document_check_out,), text=_('Check out'), + view='checkouts:check_out_document_multiple' ) link_check_in_document = Link( args='object.pk', icon_class=icon_check_in_document, condition=is_checked_out, permissions=( permission_document_check_in, permission_document_check_in_override - ), text=_('Check in document'), view='checkouts:check_in_document', + ), text=_('Check in document'), view='checkouts:check_in_document' +) +link_check_in_document_multiple = Link( + icon_class=icon_check_in_document, + permissions=(permission_document_check_in,), text=_('Check in'), + view='checkouts:check_in_document_multiple' ) link_check_out_info = Link( args='resolved_object.pk', icon_class=icon_check_out_info, permissions=( permission_document_check_out_detail_view, - ), text=_('Check in/out'), view='checkouts:check_out_info', + ), text=_('Check in/out'), view='checkouts:check_out_info' ) diff --git a/mayan/apps/checkouts/managers.py b/mayan/apps/checkouts/managers.py index 674a77257e..6fbebfc5e1 100644 --- a/mayan/apps/checkouts/managers.py +++ b/mayan/apps/checkouts/managers.py @@ -6,6 +6,7 @@ from django.apps import apps from django.db import models, transaction from django.utils.timezone import now +from mayan.apps.acls.models import AccessControlList from mayan.apps.documents.models import Document from .events import ( @@ -14,10 +15,58 @@ from .events import ( ) from .exceptions import DocumentNotCheckedOut from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN +from .permissions import ( + permission_document_check_in, permission_document_check_in_override +) logger = logging.getLogger(__name__) +class DocumentCheckoutBusinessLogicManager(models.Manager): + def check_in_document(self, document, user=None): + # Convert any document submodel to the parent model class + queryset = document._meta.default_manager.filter(pk=document.pk) + + if not self.filter(document__pk__in=queryset).exists(): + raise DocumentNotCheckedOut + + return self.check_in_documents(queryset=queryset, user=user) + + def check_in_documents(self, queryset, user=None): + if user: + user_document_checkouts = AccessControlList.objects.restrict_queryset( + permission=permission_document_check_in, + queryset=self.filter(user_id=user.pk, document__in=queryset), + user=user + ) + + others_document_checkouts = AccessControlList.objects.restrict_queryset( + permission=permission_document_check_in_override, + queryset=self.exclude(user_id=user.pk, document__in=queryset), + user=user + ) + + with transaction.atomic(): + if user: + for checkout in user_document_checkouts: + event_document_check_in.commit( + actor=user, target=checkout.document + ) + checkout.delete() + + for checkout in others_document_checkouts: + event_document_forceful_check_in.commit( + actor=user, target=checkout.document + ) + checkout.delete() + else: + for checkout in self.filter(document__in=queryset): + event_document_auto_check_in.commit( + target=checkout.document + ) + checkout.delete() + + class DocumentCheckoutManager(models.Manager): def are_document_new_versions_allowed(self, document, user=None): try: @@ -27,25 +76,6 @@ class DocumentCheckoutManager(models.Manager): else: return not check_out_info.block_new_version - def check_in_document(self, document, user=None): - try: - document_check_out = self.model.objects.get(document=document) - except self.model.DoesNotExist: - raise DocumentNotCheckedOut - else: - with transaction.atomic(): - if user: - if self.get_check_out_info(document=document).user != user: - event_document_forceful_check_in.commit( - actor=user, target=document - ) - else: - event_document_check_in.commit(actor=user, target=document) - else: - event_document_auto_check_in.commit(target=document) - - document_check_out.delete() - def check_in_expired_check_outs(self): for document in self.expired_check_outs(): document.check_in() @@ -57,7 +87,11 @@ class DocumentCheckoutManager(models.Manager): ) def checked_out_documents(self): - return Document.objects.filter( + CheckedOutDocument = apps.get_model( + app_label='checkouts', model_name='CheckedOutDocument' + ) + + return CheckedOutDocument.objects.filter( pk__in=self.model.objects.values('document__id') ) @@ -74,7 +108,11 @@ class DocumentCheckoutManager(models.Manager): return STATE_CHECKED_IN def expired_check_outs(self): - expired_list = Document.objects.filter( + CheckedOutDocument = apps.get_model( + app_label='checkouts', model_name='CheckedOutDocument' + ) + + expired_list = CheckedOutDocument.objects.filter( pk__in=self.model.objects.filter( expiration_datetime__lte=now() ).values_list('document__pk', flat=True) @@ -83,9 +121,6 @@ class DocumentCheckoutManager(models.Manager): return expired_list def get_by_natural_key(self, document_natural_key): - Document = apps.get_model( - app_label='documents', model_name='Document' - ) try: document = Document.objects.get_by_natural_key(document_natural_key) except Document.DoesNotExist: diff --git a/mayan/apps/checkouts/methods.py b/mayan/apps/checkouts/methods.py index 680f6cf4c7..3972807a8f 100644 --- a/mayan/apps/checkouts/methods.py +++ b/mayan/apps/checkouts/methods.py @@ -8,7 +8,7 @@ def method_check_in(self, user=None): app_label='checkouts', model_name='DocumentCheckout' ) - return DocumentCheckout.objects.check_in_document( + return DocumentCheckout.business_logic.check_in_document( document=self, user=user ) diff --git a/mayan/apps/checkouts/migrations/0008_checkedoutdocument.py b/mayan/apps/checkouts/migrations/0008_checkedoutdocument.py new file mode 100644 index 0000000000..903a01d1a6 --- /dev/null +++ b/mayan/apps/checkouts/migrations/0008_checkedoutdocument.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-07-25 04:52 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0050_auto_20190725_0451'), + ('checkouts', '0007_auto_20180310_1715'), + ] + + operations = [ + migrations.CreateModel( + name='CheckedOutDocument', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + }, + bases=('documents.document',), + ), + ] diff --git a/mayan/apps/checkouts/models.py b/mayan/apps/checkouts/models.py index c724e21db8..5efc322dff 100644 --- a/mayan/apps/checkouts/models.py +++ b/mayan/apps/checkouts/models.py @@ -14,7 +14,10 @@ from mayan.apps.documents.models import Document from .events import event_document_check_out from .exceptions import DocumentAlreadyCheckedOut -from .managers import DocumentCheckoutManager, NewVersionBlockManager +from .managers import ( + DocumentCheckoutBusinessLogicManager, DocumentCheckoutManager, + NewVersionBlockManager +) logger = logging.getLogger(__name__) @@ -49,6 +52,7 @@ class DocumentCheckout(models.Model): ) objects = DocumentCheckoutManager() + business_logic = DocumentCheckoutBusinessLogicManager() class Meta: ordering = ('pk',) @@ -81,13 +85,13 @@ class DocumentCheckout(models.Model): natural_key.dependencies = ['documents.Document'] def save(self, *args, **kwargs): - new_checkout = not self.pk - if not new_checkout or self.document.is_checked_out(): + is_new = not self.pk + if not is_new or self.document.is_checked_out(): raise DocumentAlreadyCheckedOut with transaction.atomic(): result = super(DocumentCheckout, self).save(*args, **kwargs) - if new_checkout: + if is_new: event_document_check_out.commit( actor=self.user, target=self.document ) @@ -119,3 +123,24 @@ class NewVersionBlock(models.Model): def natural_key(self): return self.document.natural_key() natural_key.dependencies = ['documents.Document'] + + +class CheckedOutDocument(Document): + class Meta: + proxy = True + + def get_user_display(self): + check_out_info = self.get_check_out_info() + return check_out_info.user.get_full_name() or check_out_info.user + + get_user_display.short_description = _('User') + + def get_checkout_datetime(self): + return self.get_check_out_info().checkout_datetime + + get_checkout_datetime.short_description = _('Checkout time and date') + + def get_checkout_expiration(self): + return self.get_check_out_info().expiration_datetime + + get_checkout_expiration.short_description = _('Checkout expiration') diff --git a/mayan/apps/checkouts/tests/mixins.py b/mayan/apps/checkouts/tests/mixins.py index 2e7d34adb8..5fca8e56a0 100644 --- a/mayan/apps/checkouts/tests/mixins.py +++ b/mayan/apps/checkouts/tests/mixins.py @@ -5,6 +5,7 @@ import datetime from django.utils.timezone import now from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS +from mayan.apps.common.tests.utils import as_id_list from ..models import DocumentCheckout @@ -64,16 +65,13 @@ class DocumentCheckoutViewTestMixin(object): } ) - def _request_test_document_check_out_detail_view(self): - return self.get( - viewname='checkouts:check_out_info', kwargs={ - 'pk': self.test_document.pk + def _request_test_document_multiple_check_in_post_view(self): + return self.post( + viewname='checkouts:check_in_document_multiple', data={ + 'id_list': as_id_list(items=self.test_documents) } ) - def _request_test_document_check_out_list_view(self): - return self.get(viewname='checkouts:check_out_list') - def _request_test_document_check_out_view(self): return self.post( viewname='checkouts:check_out_document', kwargs={ @@ -84,3 +82,23 @@ class DocumentCheckoutViewTestMixin(object): 'block_new_version': True } ) + + def _request_test_document_multiple_check_out_post_view(self): + return self.post( + viewname='checkouts:check_out_document_multiple', data={ + 'block_new_version': True, + 'expiration_datetime_unit': TIME_DELTA_UNIT_DAYS, + 'expiration_datetime_amount': 99, + 'id_list': as_id_list(items=self.test_documents) + } + ) + + def _request_test_document_check_out_detail_view(self): + return self.get( + viewname='checkouts:check_out_info', kwargs={ + 'pk': self.test_document.pk + } + ) + + def _request_test_document_check_out_list_view(self): + return self.get(viewname='checkouts:check_out_list') diff --git a/mayan/apps/checkouts/tests/test_api.py b/mayan/apps/checkouts/tests/test_api.py index ad475f5c4d..9266f6398e 100644 --- a/mayan/apps/checkouts/tests/test_api.py +++ b/mayan/apps/checkouts/tests/test_api.py @@ -4,9 +4,9 @@ from django.utils.encoding import force_text from rest_framework import status -from mayan.apps.documents.tests import DocumentTestMixin from mayan.apps.documents.permissions import permission_document_view -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.documents.tests.mixins import DocumentTestMixin +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..models import DocumentCheckout from ..permissions import ( diff --git a/mayan/apps/checkouts/tests/test_links.py b/mayan/apps/checkouts/tests/test_links.py index 1debe57e29..1fd88ad8aa 100644 --- a/mayan/apps/checkouts/tests/test_links.py +++ b/mayan/apps/checkouts/tests/test_links.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from ..links import link_check_out_document, link_check_out_info from ..permissions import ( diff --git a/mayan/apps/checkouts/tests/test_models.py b/mayan/apps/checkouts/tests/test_models.py index b6015d0016..b449c654ac 100644 --- a/mayan/apps/checkouts/tests/test_models.py +++ b/mayan/apps/checkouts/tests/test_models.py @@ -2,11 +2,10 @@ from __future__ import unicode_literals import time -from mayan.apps.common.tests import BaseTestCase -from mayan.apps.documents.tests import ( - GenericDocumentTestCase, DocumentTestMixin -) +from mayan.apps.common.tests.base import BaseTestCase +from mayan.apps.documents.tests.base import GenericDocumentTestCase from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH +from mayan.apps.documents.tests.mixins import DocumentTestMixin from ..exceptions import ( DocumentAlreadyCheckedOut, DocumentNotCheckedOut, @@ -53,7 +52,7 @@ class DocumentCheckoutTestCase( block_new_version=True ) - def test_document_checkin_without_checkout(self): + def test_document_check_in_without_check_out(self): with self.assertRaises(DocumentNotCheckedOut): self.test_document.check_in() diff --git a/mayan/apps/checkouts/tests/test_views.py b/mayan/apps/checkouts/tests/test_views.py index 8cae22a1f4..128e1643e0 100644 --- a/mayan/apps/checkouts/tests/test_views.py +++ b/mayan/apps/checkouts/tests/test_views.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from mayan.apps.documents.permissions import permission_document_view -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from mayan.apps.sources.links import link_document_version_upload from ..literals import STATE_CHECKED_OUT, STATE_LABELS @@ -22,8 +22,8 @@ class DocumentCheckoutViewTestCase( self._check_out_test_document() response = self._request_test_document_check_in_get_view() - self.assertContains( - response=response, text=self.test_document.label, status_code=200 + self.assertNotContains( + response=response, text=self.test_document.label, status_code=404 ) self.assertTrue(self.test_document.is_checked_out()) @@ -46,7 +46,7 @@ class DocumentCheckoutViewTestCase( self._check_out_test_document() response = self._request_test_document_check_in_post_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.assertTrue(self.test_document.is_checked_out()) @@ -67,9 +67,89 @@ class DocumentCheckoutViewTestCase( ) ) + def test_document_multiple_check_in_post_view_no_permission(self): + # Upload second document + self.upload_document() + + self._check_out_test_document(document=self.test_documents[0]) + self._check_out_test_document(document=self.test_documents[1]) + + response = self._request_test_document_multiple_check_in_post_view() + self.assertEqual(response.status_code, 404) + + self.assertTrue(self.test_documents[0].is_checked_out()) + self.assertTrue(self.test_documents[1].is_checked_out()) + self.assertTrue( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[0] + ) + ) + self.assertTrue( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[1] + ) + ) + + def test_document_multiple_check_in_post_view_with_document_0_access(self): + # Upload second document + self.upload_document() + + self._check_out_test_document(document=self.test_documents[0]) + self._check_out_test_document(document=self.test_documents[1]) + + self.grant_access( + obj=self.test_documents[0], permission=permission_document_check_in + ) + + response = self._request_test_document_multiple_check_in_post_view() + self.assertEqual(response.status_code, 302) + + self.assertFalse(self.test_documents[0].is_checked_out()) + self.assertTrue(self.test_documents[1].is_checked_out()) + self.assertFalse( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[0] + ) + ) + self.assertTrue( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[1] + ) + ) + + def test_document_multiple_check_in_post_view_with_access(self): + # Upload second document + self.upload_document() + + self._check_out_test_document(document=self.test_documents[0]) + self._check_out_test_document(document=self.test_documents[1]) + + self.grant_access( + obj=self.test_documents[0], permission=permission_document_check_in + ) + self.grant_access( + obj=self.test_documents[1], permission=permission_document_check_in + ) + + response = self._request_test_document_multiple_check_in_post_view() + self.assertEqual(response.status_code, 302) + + self.assertFalse(self.test_documents[0].is_checked_out()) + self.assertFalse(self.test_documents[1].is_checked_out()) + self.assertFalse( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[0] + ) + ) + self.assertFalse( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[1] + ) + ) + def test_document_check_out_view_no_permission(self): response = self._request_test_document_check_out_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.assertFalse(self.test_document.is_checked_out()) @@ -87,6 +167,102 @@ class DocumentCheckoutViewTestCase( self.assertTrue(self.test_document.is_checked_out()) + def test_document_multiple_check_out_post_view_no_permission(self): + # Upload second document + self.upload_document() + + self.grant_access( + obj=self.test_documents[0], + permission=permission_document_check_out_detail_view + ) + self.grant_access( + obj=self.test_documents[1], + permission=permission_document_check_out_detail_view + ) + + response = self._request_test_document_multiple_check_out_post_view() + self.assertEqual(response.status_code, 404) + + self.assertFalse(self.test_documents[0].is_checked_out()) + self.assertFalse(self.test_documents[1].is_checked_out()) + self.assertFalse( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[0] + ) + ) + self.assertFalse( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[1] + ) + ) + + def test_document_multiple_check_out_post_view_with_document_access(self): + # Upload second document + self.upload_document() + + self.grant_access( + obj=self.test_documents[0], permission=permission_document_check_out + ) + self.grant_access( + obj=self.test_documents[0], + permission=permission_document_check_out_detail_view + ) + self.grant_access( + obj=self.test_documents[1], + permission=permission_document_check_out_detail_view + ) + + response = self._request_test_document_multiple_check_out_post_view() + self.assertEqual(response.status_code, 302) + + self.assertTrue(self.test_documents[0].is_checked_out()) + self.assertFalse(self.test_documents[1].is_checked_out()) + self.assertTrue( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[0] + ) + ) + self.assertFalse( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[1] + ) + ) + + def test_document_multiple_check_out_post_view_with_access(self): + # Upload second document + self.upload_document() + + self.grant_access( + obj=self.test_documents[0], permission=permission_document_check_out + ) + self.grant_access( + obj=self.test_documents[1], permission=permission_document_check_out + ) + self.grant_access( + obj=self.test_documents[0], + permission=permission_document_check_out_detail_view + ) + self.grant_access( + obj=self.test_documents[1], + permission=permission_document_check_out_detail_view + ) + + response = self._request_test_document_multiple_check_out_post_view() + self.assertEqual(response.status_code, 302) + + self.assertTrue(self.test_documents[0].is_checked_out()) + self.assertTrue(self.test_documents[1].is_checked_out()) + self.assertTrue( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[0] + ) + ) + self.assertTrue( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[1] + ) + ) + def test_document_check_out_detail_view_no_permission(self): self._check_out_test_document() @@ -156,19 +332,18 @@ class DocumentCheckoutViewTestCase( 'pk': self.test_document.pk } ) - self.assertContains( - response=response, text='Insufficient permissions', status_code=403 - ) + self.assertEqual(response.status_code, 302) self.assertTrue(self.test_document.is_checked_out()) - def test_document_check_in_forcefull_view_with_permission(self): + def test_document_check_in_forcefull_view_with_access(self): self._create_test_user() # Check out document as test_user self._check_out_test_document(user=self.test_user) self.grant_access( - obj=self.test_document, permission=permission_document_check_in_override + obj=self.test_document, + permission=permission_document_check_in_override ) # Check in document as test_case_user @@ -219,6 +394,8 @@ class NewVersionBlockViewTestCase( # Needed by the url view resolver response.context.current_app = None - resolved_link = link_document_version_upload.resolve(context=response.context) + resolved_link = link_document_version_upload.resolve( + context=response.context + ) self.assertEqual(resolved_link, None) diff --git a/mayan/apps/checkouts/urls.py b/mayan/apps/checkouts/urls.py index 87dfff5c73..f79579bd51 100644 --- a/mayan/apps/checkouts/urls.py +++ b/mayan/apps/checkouts/urls.py @@ -4,8 +4,8 @@ from django.conf.urls import url from .api_views import APICheckedoutDocumentListView, APICheckedoutDocumentView from .views import ( - DocumentCheckOutView, DocumentCheckOutDetailView, DocumentCheckOutListView, - DocumentCheckInView + DocumentCheckInView, DocumentCheckOutDetailView, DocumentCheckOutListView, + DocumentCheckOutView ) urlpatterns = [ @@ -14,16 +14,25 @@ urlpatterns = [ name='check_out_list' ), url( - regex=r'^documents/(?P\d+)/check/out/$', view=DocumentCheckOutView.as_view(), - name='check_out_document' + regex=r'^documents/(?P\d+)/check/in/$', + view=DocumentCheckInView.as_view(), name='check_in_document' ), url( - regex=r'^documents/(?P\d+)/check/in/$', view=DocumentCheckInView.as_view(), - name='check_in_document' + regex=r'^documents/multiple/check/in/$', + name='check_in_document_multiple', view=DocumentCheckInView.as_view() ), url( - regex=r'^documents/(?P\d+)/check/info/$', view=DocumentCheckOutDetailView.as_view(), - name='check_out_info' + regex=r'^documents/(?P\d+)/check/out/$', + view=DocumentCheckOutView.as_view(), name='check_out_document' + ), + url( + regex=r'^documents/multiple/check/out/$', + name='check_out_document_multiple', + view=DocumentCheckOutView.as_view() + ), + url( + regex=r'^documents/(?P\d+)/checkout/info/$', + view=DocumentCheckOutDetailView.as_view(), name='check_out_info' ), ] diff --git a/mayan/apps/checkouts/views.py b/mayan/apps/checkouts/views.py index 4bab799063..784cc3544c 100644 --- a/mayan/apps/checkouts/views.py +++ b/mayan/apps/checkouts/views.py @@ -1,21 +1,17 @@ from __future__ import absolute_import, unicode_literals -from django.contrib import messages -from django.http import HttpResponseRedirect -from django.shortcuts import get_object_or_404 from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _, ungettext from mayan.apps.acls.models import AccessControlList from mayan.apps.common.generics import ( - ConfirmView, SingleObjectCreateView, SingleObjectDetailView + MultipleObjectConfirmActionView, MultipleObjectFormActionView, + SingleObjectDetailView ) -from mayan.apps.common.utils import encapsulate from mayan.apps.documents.models import Document from mayan.apps.documents.views import DocumentListView -from .exceptions import DocumentAlreadyCheckedOut, DocumentNotCheckedOut -from .forms import DocumentCheckoutForm, DocumentCheckoutDefailForm +from .forms import DocumentCheckOutForm, DocumentCheckOutDetailForm from .icons import icon_check_out_info from .models import DocumentCheckout from .permissions import ( @@ -24,66 +20,125 @@ from .permissions import ( ) -class DocumentCheckInView(ConfirmView): - def get_extra_context(self): - document = self.get_object() +class DocumentCheckInView(MultipleObjectConfirmActionView): + error_message = 'Unable to check in document "%(instance)s". %(exception)s' + model = Document + pk_url_kwarg = 'pk' + success_message_singular = '%(count)d document checked in.' + success_message_plural = '%(count)d documents checked in.' - context = { - 'object': document, + def get_extra_context(self): + queryset = self.get_object_list() + + result = { + 'title': ungettext( + singular='Check in %(count)d document', + plural='Check in %(count)d documents', + number=queryset.count() + ) % { + 'count': queryset.count(), + } } - if document.get_check_out_info().user != self.request.user: - context['title'] = _( - 'You didn\'t originally checked out this document. ' - 'Forcefully check in the document: %s?' - ) % document + if queryset.count() == 1: + result.update( + { + 'object': queryset.first(), + 'title': _( + 'Check in document: %s' + ) % queryset.first() + } + ) + + return result + + def get_post_object_action_url(self): + if self.action_count == 1: + return reverse( + viewname='checkouts:document_checkout_info', + kwargs={'pk': self.action_id_list[0]} + ) else: - context['title'] = _('Check in the document: %s?') % document + super(DocumentCheckInView, self).get_post_action_redirect() - return context + def get_source_queryset(self): + # object_permission is None to disable restricting queryset mixin + # and restrict the queryset ourselves from two permissions - def get_object(self): - return get_object_or_404(klass=Document, pk=self.kwargs['pk']) + source_queryset = super(DocumentCheckInView, self).get_source_queryset() - def get_post_action_redirect(self): - return reverse( - viewname='checkouts:check_out_info', kwargs={ - 'pk': self.get_object().pk - } + check_in_queryset = AccessControlList.objects.restrict_queryset( + permission=permission_document_check_in, queryset=source_queryset, + user=self.request.user ) - def view_action(self): - document = self.get_object() + check_in_override_queryset = AccessControlList.objects.restrict_queryset( + permission=permission_document_check_in_override, + queryset=source_queryset, user=self.request.user + ) - if document.get_check_out_info().user == self.request.user: - AccessControlList.objects.check_access( - obj=document, permissions=(permission_document_check_in,), - user=self.request.user - ) - else: - AccessControlList.objects.check_access( - obj=document, - permissions=(permission_document_check_in_override,), - user=self.request.user + return check_in_queryset | check_in_override_queryset + + def object_action(self, form, instance): + DocumentCheckout.business_logic.check_in_document( + document=instance, user=self.request.user + ) + + +class DocumentCheckOutView(MultipleObjectFormActionView): + error_message = 'Unable to checkout document "%(instance)s". %(exception)s' + form_class = DocumentCheckOutForm + model = Document + object_permission = permission_document_check_out + pk_url_kwarg = 'pk' + success_message_singular = '%(count)d document checked out.' + success_message_plural = '%(count)d documents checked out.' + + def get_extra_context(self): + queryset = self.get_object_list() + + result = { + 'title': ungettext( + singular='Checkout %(count)d document', + plural='Checkout %(count)d documents', + number=queryset.count() + ) % { + 'count': queryset.count(), + } + } + + if queryset.count() == 1: + result.update( + { + 'object': queryset.first(), + 'title': _( + 'Check out document: %s' + ) % queryset.first() + } ) - try: - document.check_in(user=self.request.user) - except DocumentNotCheckedOut: - messages.error( - message=_('Document has not been checked out.'), - request=self.request + return result + + def get_post_object_action_url(self): + if self.action_count == 1: + return reverse( + viewname='checkouts:document_checkout_info', + kwargs={'pk': self.action_id_list[0]} ) else: - messages.success( - message=_( - 'Document "%s" checked in successfully.' - ) % document, request=self.request - ) + super(DocumentCheckOutView, self).get_post_action_redirect() + + def object_action(self, form, instance): + DocumentCheckout.objects.check_out_document( + block_new_version=form.cleaned_data['block_new_version'], + document=instance, + expiration_datetime=form.cleaned_data['expiration_datetime'], + user=self.request.user, + ) class DocumentCheckOutDetailView(SingleObjectDetailView): - form_class = DocumentCheckoutDefailForm + form_class = DocumentCheckOutDetailForm model = Document object_permission = permission_document_check_out_detail_view @@ -96,55 +151,6 @@ class DocumentCheckOutDetailView(SingleObjectDetailView): } -class DocumentCheckOutView(SingleObjectCreateView): - form_class = DocumentCheckoutForm - - def dispatch(self, request, *args, **kwargs): - self.document = get_object_or_404(klass=Document, pk=self.kwargs['pk']) - - AccessControlList.objects.check_access( - obj=self.document, permissions=(permission_document_check_out,), - user=request.user - ) - - return super( - DocumentCheckOutView, self - ).dispatch(request, *args, **kwargs) - - def form_valid(self, form): - try: - instance = form.save(commit=False) - instance.user = self.request.user - instance.document = self.document - instance.save() - except DocumentAlreadyCheckedOut: - messages.error( - message=_('Document already checked out.'), - request=self.request - ) - else: - messages.success( - message=_( - 'Document "%s" checked out successfully.' - ) % self.document, request=self.request - ) - - return HttpResponseRedirect(redirect_to=self.get_success_url()) - - def get_extra_context(self): - return { - 'object': self.document, - 'title': _('Check out document: %s') % self.document - } - - def get_post_action_redirect(self): - return reverse( - viewname='checkouts:check_out_info', kwargs={ - 'pk': self.document.pk - } - ) - - class DocumentCheckOutListView(DocumentListView): def get_document_queryset(self): return AccessControlList.objects.restrict_queryset( @@ -157,34 +163,13 @@ class DocumentCheckOutListView(DocumentListView): context = super(DocumentCheckOutListView, self).get_extra_context() context.update( { - 'extra_columns': ( - { - 'name': _('User'), - 'attribute': encapsulate( - lambda document: document.get_check_out_info().user.get_full_name() or document.get_check_out_info().user - ) - }, - { - 'name': _('Checkout time and date'), - 'attribute': encapsulate( - lambda document: document.get_check_out_info().checkout_datetime - ) - }, - { - 'name': _('Checkout expiration'), - 'attribute': encapsulate( - lambda document: document.get_check_out_info().expiration_datetime - ) - }, - ), 'no_results_icon': icon_check_out_info, 'no_results_text': _( - 'Checking out a document blocks certain document ' - 'operations for a predetermined amount of ' - 'time.' + 'Checking out a document, blocks certain operations ' + 'for a predetermined amount of time.' ), 'no_results_title': _('No documents have been checked out'), - 'title': _('Documents checked out'), + 'title': _('Checked out documents'), } ) return context diff --git a/mayan/apps/common/apps.py b/mayan/apps/common/apps.py index 3d7f633675..324f33dc4e 100644 --- a/mayan/apps/common/apps.py +++ b/mayan/apps/common/apps.py @@ -27,9 +27,7 @@ from .links import ( ) from .literals import MESSAGE_SQLITE_WARNING -from .menus import ( - menu_about, menu_main, menu_secondary, menu_user -) +from .menus import menu_about, menu_secondary, menu_topbar, menu_user from .settings import ( setting_auto_logging, setting_production_error_log_path, setting_production_error_logging @@ -97,7 +95,10 @@ class CommonApp(MayanAppConfig): ) Template( - name='menu_main', template_name='appearance/main_menu.html' + name='menu_main', template_name='appearance/menu_main.html' + ) + Template( + name='menu_topbar', template_name='appearance/menu_topbar.html' ) menu_user.bind_links( @@ -112,7 +113,7 @@ class CommonApp(MayanAppConfig): ) ) - menu_main.bind_links(links=(menu_about, menu_user,), position=99) + menu_topbar.bind_links(links=(menu_about, menu_user,), position=99) menu_secondary.bind_links( links=(link_object_error_list_clear,), sources=( 'common:object_error_list', diff --git a/mayan/apps/common/dependencies.py b/mayan/apps/common/dependencies.py index 97bfb1b0d4..90cec8cf47 100644 --- a/mayan/apps/common/dependencies.py +++ b/mayan/apps/common/dependencies.py @@ -61,102 +61,9 @@ PythonDependency( SOFTWARE. ''', module=__name__, name='PyYAML', version_string='==5.1.1' ) -PythonDependency( - copyright_text=''' - Copyright (c) 2015 Ask Solem & contributors. All rights reserved. - Copyright (c) 2012-2014 GoPivotal, Inc. All rights reserved. - Copyright (c) 2009, 2010, 2011, 2012 Ask Solem, and individual contributors. All rights reserved. - - Celery is licensed under The BSD License (3 Clause, also known as - the new BSD license). The license is an OSI approved Open Source - license and is GPL-compatible(1). - - The license text can also be found here: - http://www.opensource.org/licenses/BSD-3-Clause - - License - ======= - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of Ask Solem, nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, - THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Ask Solem OR CONTRIBUTORS - BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - POSSIBILITY OF SUCH DAMAGE. - - Documentation License - ===================== - - The documentation portion of Celery (the rendered contents of the - "docs" directory of a software distribution or checkout) is supplied - under the Creative Commons Attribution-Noncommercial-Share Alike 3.0 - United States License as described by - http://creativecommons.org/licenses/by-nc-sa/3.0/us/ - - Footnotes - ========= - (1) A GPL-compatible license makes it possible to - combine Celery with other software that is released - under the GPL, it does not mean that we're distributing - Celery under the GPL license. The BSD license, unlike the GPL, - let you distribute a modified version without making your - changes open source. - ''', module=__name__, name='celery', version_string='==3.1.24' -) -PythonDependency( - copyright_text=''' - Copyright (c) 2012-2013 GoPivotal, Inc. All Rights Reserved. - Copyright (c) 2009-2012 Ask Solem. All Rights Reserved. - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - Neither the name of Ask Solem nor the names of its contributors may be used - to endorse or promote products derived from this software without specific - prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, - THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS - BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - POSSIBILITY OF SUCH DAMAGE. - ''', module=__name__, name='django-celery', version_string='==3.2.1' -) PythonDependency( module=__name__, name='django-downloadview', version_string='==1.9' ) -PythonDependency( - module=__name__, name='django-environ', version_string='==0.4.5' -) PythonDependency( module=__name__, name='django-formtools', version_string='==2.1' ) @@ -383,6 +290,10 @@ PythonDependency( module=__name__, environment=environment_development, name='Werkzeug', version_string='==0.15.4' ) +PythonDependency( + module=__name__, environment=environment_development, name='devpi-server', + version_string='==5.0.0' +) PythonDependency( environment=environment_development, module=__name__, name='django-debug-toolbar', version_string='==1.11' diff --git a/mayan/apps/common/http.py b/mayan/apps/common/http.py index f353dfb99a..ba507523d2 100644 --- a/mayan/apps/common/http.py +++ b/mayan/apps/common/http.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.http import QueryDict from django.utils.encoding import force_bytes +from django.utils.six import PY3 class URL(object): @@ -20,9 +21,7 @@ class URL(object): def to_string(self): if self._args.keys(): - query = force_bytes( - '?{}'.format(self._args.urlencode()) - ) + query = '?{}'.format(self._args.urlencode()) else: query = '' @@ -31,6 +30,9 @@ class URL(object): else: path = '' - result = force_bytes('{}{}'.format(path, query)) + result = '{}{}'.format(path, query) - return result + if PY3: + return result + else: + return force_bytes(result) diff --git a/mayan/apps/common/icons.py b/mayan/apps/common/icons.py index ad021e2500..0c8919ec0c 100644 --- a/mayan/apps/common/icons.py +++ b/mayan/apps/common/icons.py @@ -41,6 +41,9 @@ 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' ) diff --git a/mayan/apps/common/links.py b/mayan/apps/common/links.py index dc58f19ca4..78b3a5dcb6 100644 --- a/mayan/apps/common/links.py +++ b/mayan/apps/common/links.py @@ -50,12 +50,14 @@ 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', diff --git a/mayan/apps/common/management/commands/convertdb.py b/mayan/apps/common/management/commands/convertdb.py deleted file mode 100644 index 30bc2d0e67..0000000000 --- a/mayan/apps/common/management/commands/convertdb.py +++ /dev/null @@ -1,107 +0,0 @@ -from __future__ import unicode_literals - -import errno -import os -import warnings - -from pathlib2 import Path - -from django.conf import settings -from django.core import management -from django.core.management.base import CommandError -from django.utils.encoding import force_text -from django.utils.translation import ugettext_lazy as _ - -from mayan.apps.documents.models import DocumentType -from mayan.apps.storage.utils import fs_cleanup - -from ...literals import MESSAGE_DEPRECATION_WARNING -from ...warnings import DeprecationWarning - -CONVERTDB_FOLDER = 'convertdb' -CONVERTDB_OUTPUT_FILENAME = 'migrate.json' - - -class Command(management.BaseCommand): - help = 'Convert from a database backend to another one.' - - def __init__(self, *args, **kwargs): - warnings.warn( - category=DeprecationWarning, - message=force_text(MESSAGE_DEPRECATION_WARNING) - ) - - super(Command, self).__init__(*args, **kwargs) - - def add_arguments(self, parser): - parser.add_argument( - 'args', metavar='app_label[.ModelName]', nargs='*', - help=_( - 'Restricts dumped data to the specified app_label or ' - 'app_label.ModelName.' - ) - ) - parser.add_argument( - '--from', action='store', default='default', dest='from', - help=_( - 'The database from which data will be exported. If omitted ' - 'the database named "default" will be used.' - ), - ) - parser.add_argument( - '--to', action='store', default='default', dest='to', - help=_( - 'The database to which data will be imported. If omitted ' - 'the database named "default" will be used.' - ), - ) - parser.add_argument( - '--force', action='store_true', dest='force', - help=_( - 'Force the conversion of the database even if the receiving ' - 'database is not empty.' - ), - ) - - def handle(self, *app_labels, **options): - # Create the media/convertdb folder - convertdb_folder_path = force_text( - Path( - settings.MEDIA_ROOT, CONVERTDB_FOLDER - ) - ) - - try: - os.makedirs(convertdb_folder_path) - except OSError as exception: - if exception.errno == errno.EEXIST: - pass - - convertdb_file_path = force_text( - Path( - convertdb_folder_path, CONVERTDB_OUTPUT_FILENAME - ) - ) - - management.call_command(command_name='purgeperiodictasks') - - management.call_command( - 'dumpdata', *app_labels, all=True, - database=options['from'], natural_primary=True, - natural_foreign=True, output=convertdb_file_path, - interactive=False, format='json' - ) - - if DocumentType.objects.using(options['to']).count() and not options['force']: - fs_cleanup(convertdb_file_path) - raise CommandError( - 'There is existing data in the database that will be ' - 'used for the import. If you proceed with the conversion ' - 'you might lose data. Please check your settings.' - ) - - management.call_command( - 'loaddata', convertdb_file_path, database=options['to'], interactive=False, - verbosity=3 - ) - fs_cleanup(convertdb_file_path) diff --git a/mayan/apps/common/management/commands/initialsetup.py b/mayan/apps/common/management/commands/initialsetup.py index 0b9755f145..1b68b737f5 100644 --- a/mayan/apps/common/management/commands/initialsetup.py +++ b/mayan/apps/common/management/commands/initialsetup.py @@ -28,8 +28,8 @@ class Command(management.BaseCommand): ) parser.add_argument( - '--no-javascript', action='store_true', dest='no_javascript', - help='Don\'t download the JavaScript dependencies.', + '--no-dependencies', action='store_true', dest='no_dependencies', + help='Don\'t download dependencies.', ) def initialize_system(self, force=False): @@ -88,9 +88,9 @@ class Command(management.BaseCommand): self.initialize_system(force=options.get('force', False)) pre_initial_setup.send(sender=self) - if not options.get('no_javascript', False): + if not options.get('no_dependencies', False): management.call_command( - command_name='installjavascript', interactive=False + command_name='installdependencies', interactive=False ) management.call_command( diff --git a/mayan/apps/common/management/commands/literals.py b/mayan/apps/common/management/commands/literals.py index 7b3864fbac..e69de29bb2 100644 --- a/mayan/apps/common/management/commands/literals.py +++ b/mayan/apps/common/management/commands/literals.py @@ -1,10 +0,0 @@ -from __future__ import unicode_literals - -SETTING_FILE_TEMPLATE = ''' -from __future__ import absolute_import, unicode_literals - -from .base import * - -SECRET_KEY = '{0}' - -''' diff --git a/mayan/apps/common/management/commands/performupgrade.py b/mayan/apps/common/management/commands/performupgrade.py index a0240b2bdb..21ec2b37f8 100644 --- a/mayan/apps/common/management/commands/performupgrade.py +++ b/mayan/apps/common/management/commands/performupgrade.py @@ -11,8 +11,8 @@ class Command(management.BaseCommand): def add_arguments(self, parser): parser.add_argument( - '--no-javascript', action='store_true', dest='no_javascript', - help='Don\'t download the JavaScript dependencies.', + '--no-dependencies', action='store_true', dest='no_dependencies', + help='Don\'t download dependencies.', ) def handle(self, *args, **options): @@ -25,9 +25,9 @@ class Command(management.BaseCommand): ) ) - if not options.get('no_javascript', False): + if not options.get('no_dependencies', False): management.call_command( - command_name='installjavascript', interactive=False + command_name='installdependencies', interactive=False ) try: diff --git a/mayan/apps/common/management/commands/purgeperiodictasks.py b/mayan/apps/common/management/commands/purgeperiodictasks.py index 835e47440d..e750cf2d47 100644 --- a/mayan/apps/common/management/commands/purgeperiodictasks.py +++ b/mayan/apps/common/management/commands/purgeperiodictasks.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.core import management -from djcelery.models import IntervalSchedule, PeriodicTask +from django_celery_beat.models import IntervalSchedule, PeriodicTask class Command(management.BaseCommand): diff --git a/mayan/apps/common/menus.py b/mayan/apps/common/menus.py index c25064cb40..d1c39545a5 100644 --- a/mayan/apps/common/menus.py +++ b/mayan/apps/common/menus.py @@ -17,6 +17,7 @@ menu_object = Menu(label=_('Actions'), name='object') menu_secondary = Menu(label=_('Secondary'), name='secondary') menu_setup = Menu(name='setup') menu_tools = Menu(name='tools') +menu_topbar = Menu(name='topbar') menu_user = Menu( icon_class=icon_menu_user, name='user', label=_('User') ) diff --git a/mayan/apps/common/migrations/0012_auto_20190711_0548.py b/mayan/apps/common/migrations/0012_auto_20190711_0548.py new file mode 100644 index 0000000000..9915d05e2c --- /dev/null +++ b/mayan/apps/common/migrations/0012_auto_20190711_0548.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-07-11 05:48 +from __future__ import unicode_literals + +from django.db import migrations, models +import mayan.apps.common.models +import mayan.apps.storage.classes + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0011_auto_20180429_0758'), + ] + + operations = [ + migrations.AlterField( + model_name='shareduploadedfile', + name='file', + field=models.FileField(storage=mayan.apps.storage.classes.FakeStorageSubclass(), upload_to=mayan.apps.common.models.upload_to, verbose_name='File'), + ), + migrations.AlterField( + model_name='userlocaleprofile', + name='language', + field=models.CharField(choices=[('ar', 'Arabic'), ('bg', 'Bulgarian'), ('bs', 'Bosnian'), ('cs', 'Czech'), ('da', 'Danish'), ('de', 'German'), ('el', 'Greek'), ('en', 'English'), ('es', 'Spanish'), ('fa', 'Persian'), ('fr', 'French'), ('hu', 'Hungarian'), ('id', 'Indonesian'), ('it', 'Italian'), ('lv', 'Latvian'), ('nl', 'Dutch'), ('pl', 'Polish'), ('pt', 'Portuguese'), ('pt-br', 'Portuguese (Brazil)'), ('ro', 'Romanian'), ('ru', 'Russian'), ('sl', 'Slovenian'), ('tr', 'Turkish'), ('vi', 'Vietnamese'), ('zh', 'Chinese')], max_length=8, verbose_name='Language'), + ), + migrations.AlterField( + model_name='userlocaleprofile', + name='timezone', + field=models.CharField(choices=[(b'Africa/Abidjan', b'Africa/Abidjan'), (b'Africa/Accra', b'Africa/Accra'), (b'Africa/Addis_Ababa', b'Africa/Addis_Ababa'), (b'Africa/Algiers', b'Africa/Algiers'), (b'Africa/Asmara', b'Africa/Asmara'), (b'Africa/Bamako', b'Africa/Bamako'), (b'Africa/Bangui', b'Africa/Bangui'), (b'Africa/Banjul', b'Africa/Banjul'), (b'Africa/Bissau', b'Africa/Bissau'), (b'Africa/Blantyre', b'Africa/Blantyre'), (b'Africa/Brazzaville', b'Africa/Brazzaville'), (b'Africa/Bujumbura', b'Africa/Bujumbura'), (b'Africa/Cairo', b'Africa/Cairo'), (b'Africa/Casablanca', b'Africa/Casablanca'), (b'Africa/Ceuta', b'Africa/Ceuta'), (b'Africa/Conakry', b'Africa/Conakry'), (b'Africa/Dakar', b'Africa/Dakar'), (b'Africa/Dar_es_Salaam', b'Africa/Dar_es_Salaam'), (b'Africa/Djibouti', b'Africa/Djibouti'), (b'Africa/Douala', b'Africa/Douala'), (b'Africa/El_Aaiun', b'Africa/El_Aaiun'), (b'Africa/Freetown', b'Africa/Freetown'), (b'Africa/Gaborone', b'Africa/Gaborone'), (b'Africa/Harare', b'Africa/Harare'), (b'Africa/Johannesburg', b'Africa/Johannesburg'), (b'Africa/Juba', b'Africa/Juba'), (b'Africa/Kampala', b'Africa/Kampala'), (b'Africa/Khartoum', b'Africa/Khartoum'), (b'Africa/Kigali', b'Africa/Kigali'), (b'Africa/Kinshasa', b'Africa/Kinshasa'), (b'Africa/Lagos', b'Africa/Lagos'), (b'Africa/Libreville', b'Africa/Libreville'), (b'Africa/Lome', b'Africa/Lome'), (b'Africa/Luanda', b'Africa/Luanda'), (b'Africa/Lubumbashi', b'Africa/Lubumbashi'), (b'Africa/Lusaka', b'Africa/Lusaka'), (b'Africa/Malabo', b'Africa/Malabo'), (b'Africa/Maputo', b'Africa/Maputo'), (b'Africa/Maseru', b'Africa/Maseru'), (b'Africa/Mbabane', b'Africa/Mbabane'), (b'Africa/Mogadishu', b'Africa/Mogadishu'), (b'Africa/Monrovia', b'Africa/Monrovia'), (b'Africa/Nairobi', b'Africa/Nairobi'), (b'Africa/Ndjamena', b'Africa/Ndjamena'), (b'Africa/Niamey', b'Africa/Niamey'), (b'Africa/Nouakchott', b'Africa/Nouakchott'), (b'Africa/Ouagadougou', b'Africa/Ouagadougou'), (b'Africa/Porto-Novo', b'Africa/Porto-Novo'), (b'Africa/Sao_Tome', b'Africa/Sao_Tome'), (b'Africa/Tripoli', b'Africa/Tripoli'), (b'Africa/Tunis', b'Africa/Tunis'), (b'Africa/Windhoek', b'Africa/Windhoek'), (b'America/Adak', b'America/Adak'), (b'America/Anchorage', b'America/Anchorage'), (b'America/Anguilla', b'America/Anguilla'), (b'America/Antigua', b'America/Antigua'), (b'America/Araguaina', b'America/Araguaina'), (b'America/Argentina/Buenos_Aires', b'America/Argentina/Buenos_Aires'), (b'America/Argentina/Catamarca', b'America/Argentina/Catamarca'), (b'America/Argentina/Cordoba', b'America/Argentina/Cordoba'), (b'America/Argentina/Jujuy', b'America/Argentina/Jujuy'), (b'America/Argentina/La_Rioja', b'America/Argentina/La_Rioja'), (b'America/Argentina/Mendoza', b'America/Argentina/Mendoza'), (b'America/Argentina/Rio_Gallegos', b'America/Argentina/Rio_Gallegos'), (b'America/Argentina/Salta', b'America/Argentina/Salta'), (b'America/Argentina/San_Juan', b'America/Argentina/San_Juan'), (b'America/Argentina/San_Luis', b'America/Argentina/San_Luis'), (b'America/Argentina/Tucuman', b'America/Argentina/Tucuman'), (b'America/Argentina/Ushuaia', b'America/Argentina/Ushuaia'), (b'America/Aruba', b'America/Aruba'), (b'America/Asuncion', b'America/Asuncion'), (b'America/Atikokan', b'America/Atikokan'), (b'America/Bahia', b'America/Bahia'), (b'America/Bahia_Banderas', b'America/Bahia_Banderas'), (b'America/Barbados', b'America/Barbados'), (b'America/Belem', b'America/Belem'), (b'America/Belize', b'America/Belize'), (b'America/Blanc-Sablon', b'America/Blanc-Sablon'), (b'America/Boa_Vista', b'America/Boa_Vista'), (b'America/Bogota', b'America/Bogota'), (b'America/Boise', b'America/Boise'), (b'America/Cambridge_Bay', b'America/Cambridge_Bay'), (b'America/Campo_Grande', b'America/Campo_Grande'), (b'America/Cancun', b'America/Cancun'), (b'America/Caracas', b'America/Caracas'), (b'America/Cayenne', b'America/Cayenne'), (b'America/Cayman', b'America/Cayman'), (b'America/Chicago', b'America/Chicago'), (b'America/Chihuahua', b'America/Chihuahua'), (b'America/Costa_Rica', b'America/Costa_Rica'), (b'America/Creston', b'America/Creston'), (b'America/Cuiaba', b'America/Cuiaba'), (b'America/Curacao', b'America/Curacao'), (b'America/Danmarkshavn', b'America/Danmarkshavn'), (b'America/Dawson', b'America/Dawson'), (b'America/Dawson_Creek', b'America/Dawson_Creek'), (b'America/Denver', b'America/Denver'), (b'America/Detroit', b'America/Detroit'), (b'America/Dominica', b'America/Dominica'), (b'America/Edmonton', b'America/Edmonton'), (b'America/Eirunepe', b'America/Eirunepe'), (b'America/El_Salvador', b'America/El_Salvador'), (b'America/Fort_Nelson', b'America/Fort_Nelson'), (b'America/Fortaleza', b'America/Fortaleza'), (b'America/Glace_Bay', b'America/Glace_Bay'), (b'America/Godthab', b'America/Godthab'), (b'America/Goose_Bay', b'America/Goose_Bay'), (b'America/Grand_Turk', b'America/Grand_Turk'), (b'America/Grenada', b'America/Grenada'), (b'America/Guadeloupe', b'America/Guadeloupe'), (b'America/Guatemala', b'America/Guatemala'), (b'America/Guayaquil', b'America/Guayaquil'), (b'America/Guyana', b'America/Guyana'), (b'America/Halifax', b'America/Halifax'), (b'America/Havana', b'America/Havana'), (b'America/Hermosillo', b'America/Hermosillo'), (b'America/Indiana/Indianapolis', b'America/Indiana/Indianapolis'), (b'America/Indiana/Knox', b'America/Indiana/Knox'), (b'America/Indiana/Marengo', b'America/Indiana/Marengo'), (b'America/Indiana/Petersburg', b'America/Indiana/Petersburg'), (b'America/Indiana/Tell_City', b'America/Indiana/Tell_City'), (b'America/Indiana/Vevay', b'America/Indiana/Vevay'), (b'America/Indiana/Vincennes', b'America/Indiana/Vincennes'), (b'America/Indiana/Winamac', b'America/Indiana/Winamac'), (b'America/Inuvik', b'America/Inuvik'), (b'America/Iqaluit', b'America/Iqaluit'), (b'America/Jamaica', b'America/Jamaica'), (b'America/Juneau', b'America/Juneau'), (b'America/Kentucky/Louisville', b'America/Kentucky/Louisville'), (b'America/Kentucky/Monticello', b'America/Kentucky/Monticello'), (b'America/Kralendijk', b'America/Kralendijk'), (b'America/La_Paz', b'America/La_Paz'), (b'America/Lima', b'America/Lima'), (b'America/Los_Angeles', b'America/Los_Angeles'), (b'America/Lower_Princes', b'America/Lower_Princes'), (b'America/Maceio', b'America/Maceio'), (b'America/Managua', b'America/Managua'), (b'America/Manaus', b'America/Manaus'), (b'America/Marigot', b'America/Marigot'), (b'America/Martinique', b'America/Martinique'), (b'America/Matamoros', b'America/Matamoros'), (b'America/Mazatlan', b'America/Mazatlan'), (b'America/Menominee', b'America/Menominee'), (b'America/Merida', b'America/Merida'), (b'America/Metlakatla', b'America/Metlakatla'), (b'America/Mexico_City', b'America/Mexico_City'), (b'America/Miquelon', b'America/Miquelon'), (b'America/Moncton', b'America/Moncton'), (b'America/Monterrey', b'America/Monterrey'), (b'America/Montevideo', b'America/Montevideo'), (b'America/Montserrat', b'America/Montserrat'), (b'America/Nassau', b'America/Nassau'), (b'America/New_York', b'America/New_York'), (b'America/Nipigon', b'America/Nipigon'), (b'America/Nome', b'America/Nome'), (b'America/Noronha', b'America/Noronha'), (b'America/North_Dakota/Beulah', b'America/North_Dakota/Beulah'), (b'America/North_Dakota/Center', b'America/North_Dakota/Center'), (b'America/North_Dakota/New_Salem', b'America/North_Dakota/New_Salem'), (b'America/Ojinaga', b'America/Ojinaga'), (b'America/Panama', b'America/Panama'), (b'America/Pangnirtung', b'America/Pangnirtung'), (b'America/Paramaribo', b'America/Paramaribo'), (b'America/Phoenix', b'America/Phoenix'), (b'America/Port-au-Prince', b'America/Port-au-Prince'), (b'America/Port_of_Spain', b'America/Port_of_Spain'), (b'America/Porto_Velho', b'America/Porto_Velho'), (b'America/Puerto_Rico', b'America/Puerto_Rico'), (b'America/Punta_Arenas', b'America/Punta_Arenas'), (b'America/Rainy_River', b'America/Rainy_River'), (b'America/Rankin_Inlet', b'America/Rankin_Inlet'), (b'America/Recife', b'America/Recife'), (b'America/Regina', b'America/Regina'), (b'America/Resolute', b'America/Resolute'), (b'America/Rio_Branco', b'America/Rio_Branco'), (b'America/Santarem', b'America/Santarem'), (b'America/Santiago', b'America/Santiago'), (b'America/Santo_Domingo', b'America/Santo_Domingo'), (b'America/Sao_Paulo', b'America/Sao_Paulo'), (b'America/Scoresbysund', b'America/Scoresbysund'), (b'America/Sitka', b'America/Sitka'), (b'America/St_Barthelemy', b'America/St_Barthelemy'), (b'America/St_Johns', b'America/St_Johns'), (b'America/St_Kitts', b'America/St_Kitts'), (b'America/St_Lucia', b'America/St_Lucia'), (b'America/St_Thomas', b'America/St_Thomas'), (b'America/St_Vincent', b'America/St_Vincent'), (b'America/Swift_Current', b'America/Swift_Current'), (b'America/Tegucigalpa', b'America/Tegucigalpa'), (b'America/Thule', b'America/Thule'), (b'America/Thunder_Bay', b'America/Thunder_Bay'), (b'America/Tijuana', b'America/Tijuana'), (b'America/Toronto', b'America/Toronto'), (b'America/Tortola', b'America/Tortola'), (b'America/Vancouver', b'America/Vancouver'), (b'America/Whitehorse', b'America/Whitehorse'), (b'America/Winnipeg', b'America/Winnipeg'), (b'America/Yakutat', b'America/Yakutat'), (b'America/Yellowknife', b'America/Yellowknife'), (b'Antarctica/Casey', b'Antarctica/Casey'), (b'Antarctica/Davis', b'Antarctica/Davis'), (b'Antarctica/DumontDUrville', b'Antarctica/DumontDUrville'), (b'Antarctica/Macquarie', b'Antarctica/Macquarie'), (b'Antarctica/Mawson', b'Antarctica/Mawson'), (b'Antarctica/McMurdo', b'Antarctica/McMurdo'), (b'Antarctica/Palmer', b'Antarctica/Palmer'), (b'Antarctica/Rothera', b'Antarctica/Rothera'), (b'Antarctica/Syowa', b'Antarctica/Syowa'), (b'Antarctica/Troll', b'Antarctica/Troll'), (b'Antarctica/Vostok', b'Antarctica/Vostok'), (b'Arctic/Longyearbyen', b'Arctic/Longyearbyen'), (b'Asia/Aden', b'Asia/Aden'), (b'Asia/Almaty', b'Asia/Almaty'), (b'Asia/Amman', b'Asia/Amman'), (b'Asia/Anadyr', b'Asia/Anadyr'), (b'Asia/Aqtau', b'Asia/Aqtau'), (b'Asia/Aqtobe', b'Asia/Aqtobe'), (b'Asia/Ashgabat', b'Asia/Ashgabat'), (b'Asia/Atyrau', b'Asia/Atyrau'), (b'Asia/Baghdad', b'Asia/Baghdad'), (b'Asia/Bahrain', b'Asia/Bahrain'), (b'Asia/Baku', b'Asia/Baku'), (b'Asia/Bangkok', b'Asia/Bangkok'), (b'Asia/Barnaul', b'Asia/Barnaul'), (b'Asia/Beirut', b'Asia/Beirut'), (b'Asia/Bishkek', b'Asia/Bishkek'), (b'Asia/Brunei', b'Asia/Brunei'), (b'Asia/Chita', b'Asia/Chita'), (b'Asia/Choibalsan', b'Asia/Choibalsan'), (b'Asia/Colombo', b'Asia/Colombo'), (b'Asia/Damascus', b'Asia/Damascus'), (b'Asia/Dhaka', b'Asia/Dhaka'), (b'Asia/Dili', b'Asia/Dili'), (b'Asia/Dubai', b'Asia/Dubai'), (b'Asia/Dushanbe', b'Asia/Dushanbe'), (b'Asia/Famagusta', b'Asia/Famagusta'), (b'Asia/Gaza', b'Asia/Gaza'), (b'Asia/Hebron', b'Asia/Hebron'), (b'Asia/Ho_Chi_Minh', b'Asia/Ho_Chi_Minh'), (b'Asia/Hong_Kong', b'Asia/Hong_Kong'), (b'Asia/Hovd', b'Asia/Hovd'), (b'Asia/Irkutsk', b'Asia/Irkutsk'), (b'Asia/Jakarta', b'Asia/Jakarta'), (b'Asia/Jayapura', b'Asia/Jayapura'), (b'Asia/Jerusalem', b'Asia/Jerusalem'), (b'Asia/Kabul', b'Asia/Kabul'), (b'Asia/Kamchatka', b'Asia/Kamchatka'), (b'Asia/Karachi', b'Asia/Karachi'), (b'Asia/Kathmandu', b'Asia/Kathmandu'), (b'Asia/Khandyga', b'Asia/Khandyga'), (b'Asia/Kolkata', b'Asia/Kolkata'), (b'Asia/Krasnoyarsk', b'Asia/Krasnoyarsk'), (b'Asia/Kuala_Lumpur', b'Asia/Kuala_Lumpur'), (b'Asia/Kuching', b'Asia/Kuching'), (b'Asia/Kuwait', b'Asia/Kuwait'), (b'Asia/Macau', b'Asia/Macau'), (b'Asia/Magadan', b'Asia/Magadan'), (b'Asia/Makassar', b'Asia/Makassar'), (b'Asia/Manila', b'Asia/Manila'), (b'Asia/Muscat', b'Asia/Muscat'), (b'Asia/Nicosia', b'Asia/Nicosia'), (b'Asia/Novokuznetsk', b'Asia/Novokuznetsk'), (b'Asia/Novosibirsk', b'Asia/Novosibirsk'), (b'Asia/Omsk', b'Asia/Omsk'), (b'Asia/Oral', b'Asia/Oral'), (b'Asia/Phnom_Penh', b'Asia/Phnom_Penh'), (b'Asia/Pontianak', b'Asia/Pontianak'), (b'Asia/Pyongyang', b'Asia/Pyongyang'), (b'Asia/Qatar', b'Asia/Qatar'), (b'Asia/Qostanay', b'Asia/Qostanay'), (b'Asia/Qyzylorda', b'Asia/Qyzylorda'), (b'Asia/Riyadh', b'Asia/Riyadh'), (b'Asia/Sakhalin', b'Asia/Sakhalin'), (b'Asia/Samarkand', b'Asia/Samarkand'), (b'Asia/Seoul', b'Asia/Seoul'), (b'Asia/Shanghai', b'Asia/Shanghai'), (b'Asia/Singapore', b'Asia/Singapore'), (b'Asia/Srednekolymsk', b'Asia/Srednekolymsk'), (b'Asia/Taipei', b'Asia/Taipei'), (b'Asia/Tashkent', b'Asia/Tashkent'), (b'Asia/Tbilisi', b'Asia/Tbilisi'), (b'Asia/Tehran', b'Asia/Tehran'), (b'Asia/Thimphu', b'Asia/Thimphu'), (b'Asia/Tokyo', b'Asia/Tokyo'), (b'Asia/Tomsk', b'Asia/Tomsk'), (b'Asia/Ulaanbaatar', b'Asia/Ulaanbaatar'), (b'Asia/Urumqi', b'Asia/Urumqi'), (b'Asia/Ust-Nera', b'Asia/Ust-Nera'), (b'Asia/Vientiane', b'Asia/Vientiane'), (b'Asia/Vladivostok', b'Asia/Vladivostok'), (b'Asia/Yakutsk', b'Asia/Yakutsk'), (b'Asia/Yangon', b'Asia/Yangon'), (b'Asia/Yekaterinburg', b'Asia/Yekaterinburg'), (b'Asia/Yerevan', b'Asia/Yerevan'), (b'Atlantic/Azores', b'Atlantic/Azores'), (b'Atlantic/Bermuda', b'Atlantic/Bermuda'), (b'Atlantic/Canary', b'Atlantic/Canary'), (b'Atlantic/Cape_Verde', b'Atlantic/Cape_Verde'), (b'Atlantic/Faroe', b'Atlantic/Faroe'), (b'Atlantic/Madeira', b'Atlantic/Madeira'), (b'Atlantic/Reykjavik', b'Atlantic/Reykjavik'), (b'Atlantic/South_Georgia', b'Atlantic/South_Georgia'), (b'Atlantic/St_Helena', b'Atlantic/St_Helena'), (b'Atlantic/Stanley', b'Atlantic/Stanley'), (b'Australia/Adelaide', b'Australia/Adelaide'), (b'Australia/Brisbane', b'Australia/Brisbane'), (b'Australia/Broken_Hill', b'Australia/Broken_Hill'), (b'Australia/Currie', b'Australia/Currie'), (b'Australia/Darwin', b'Australia/Darwin'), (b'Australia/Eucla', b'Australia/Eucla'), (b'Australia/Hobart', b'Australia/Hobart'), (b'Australia/Lindeman', b'Australia/Lindeman'), (b'Australia/Lord_Howe', b'Australia/Lord_Howe'), (b'Australia/Melbourne', b'Australia/Melbourne'), (b'Australia/Perth', b'Australia/Perth'), (b'Australia/Sydney', b'Australia/Sydney'), (b'Canada/Atlantic', b'Canada/Atlantic'), (b'Canada/Central', b'Canada/Central'), (b'Canada/Eastern', b'Canada/Eastern'), (b'Canada/Mountain', b'Canada/Mountain'), (b'Canada/Newfoundland', b'Canada/Newfoundland'), (b'Canada/Pacific', b'Canada/Pacific'), (b'Europe/Amsterdam', b'Europe/Amsterdam'), (b'Europe/Andorra', b'Europe/Andorra'), (b'Europe/Astrakhan', b'Europe/Astrakhan'), (b'Europe/Athens', b'Europe/Athens'), (b'Europe/Belgrade', b'Europe/Belgrade'), (b'Europe/Berlin', b'Europe/Berlin'), (b'Europe/Bratislava', b'Europe/Bratislava'), (b'Europe/Brussels', b'Europe/Brussels'), (b'Europe/Bucharest', b'Europe/Bucharest'), (b'Europe/Budapest', b'Europe/Budapest'), (b'Europe/Busingen', b'Europe/Busingen'), (b'Europe/Chisinau', b'Europe/Chisinau'), (b'Europe/Copenhagen', b'Europe/Copenhagen'), (b'Europe/Dublin', b'Europe/Dublin'), (b'Europe/Gibraltar', b'Europe/Gibraltar'), (b'Europe/Guernsey', b'Europe/Guernsey'), (b'Europe/Helsinki', b'Europe/Helsinki'), (b'Europe/Isle_of_Man', b'Europe/Isle_of_Man'), (b'Europe/Istanbul', b'Europe/Istanbul'), (b'Europe/Jersey', b'Europe/Jersey'), (b'Europe/Kaliningrad', b'Europe/Kaliningrad'), (b'Europe/Kiev', b'Europe/Kiev'), (b'Europe/Kirov', b'Europe/Kirov'), (b'Europe/Lisbon', b'Europe/Lisbon'), (b'Europe/Ljubljana', b'Europe/Ljubljana'), (b'Europe/London', b'Europe/London'), (b'Europe/Luxembourg', b'Europe/Luxembourg'), (b'Europe/Madrid', b'Europe/Madrid'), (b'Europe/Malta', b'Europe/Malta'), (b'Europe/Mariehamn', b'Europe/Mariehamn'), (b'Europe/Minsk', b'Europe/Minsk'), (b'Europe/Monaco', b'Europe/Monaco'), (b'Europe/Moscow', b'Europe/Moscow'), (b'Europe/Oslo', b'Europe/Oslo'), (b'Europe/Paris', b'Europe/Paris'), (b'Europe/Podgorica', b'Europe/Podgorica'), (b'Europe/Prague', b'Europe/Prague'), (b'Europe/Riga', b'Europe/Riga'), (b'Europe/Rome', b'Europe/Rome'), (b'Europe/Samara', b'Europe/Samara'), (b'Europe/San_Marino', b'Europe/San_Marino'), (b'Europe/Sarajevo', b'Europe/Sarajevo'), (b'Europe/Saratov', b'Europe/Saratov'), (b'Europe/Simferopol', b'Europe/Simferopol'), (b'Europe/Skopje', b'Europe/Skopje'), (b'Europe/Sofia', b'Europe/Sofia'), (b'Europe/Stockholm', b'Europe/Stockholm'), (b'Europe/Tallinn', b'Europe/Tallinn'), (b'Europe/Tirane', b'Europe/Tirane'), (b'Europe/Ulyanovsk', b'Europe/Ulyanovsk'), (b'Europe/Uzhgorod', b'Europe/Uzhgorod'), (b'Europe/Vaduz', b'Europe/Vaduz'), (b'Europe/Vatican', b'Europe/Vatican'), (b'Europe/Vienna', b'Europe/Vienna'), (b'Europe/Vilnius', b'Europe/Vilnius'), (b'Europe/Volgograd', b'Europe/Volgograd'), (b'Europe/Warsaw', b'Europe/Warsaw'), (b'Europe/Zagreb', b'Europe/Zagreb'), (b'Europe/Zaporozhye', b'Europe/Zaporozhye'), (b'Europe/Zurich', b'Europe/Zurich'), (b'GMT', b'GMT'), (b'Indian/Antananarivo', b'Indian/Antananarivo'), (b'Indian/Chagos', b'Indian/Chagos'), (b'Indian/Christmas', b'Indian/Christmas'), (b'Indian/Cocos', b'Indian/Cocos'), (b'Indian/Comoro', b'Indian/Comoro'), (b'Indian/Kerguelen', b'Indian/Kerguelen'), (b'Indian/Mahe', b'Indian/Mahe'), (b'Indian/Maldives', b'Indian/Maldives'), (b'Indian/Mauritius', b'Indian/Mauritius'), (b'Indian/Mayotte', b'Indian/Mayotte'), (b'Indian/Reunion', b'Indian/Reunion'), (b'Pacific/Apia', b'Pacific/Apia'), (b'Pacific/Auckland', b'Pacific/Auckland'), (b'Pacific/Bougainville', b'Pacific/Bougainville'), (b'Pacific/Chatham', b'Pacific/Chatham'), (b'Pacific/Chuuk', b'Pacific/Chuuk'), (b'Pacific/Easter', b'Pacific/Easter'), (b'Pacific/Efate', b'Pacific/Efate'), (b'Pacific/Enderbury', b'Pacific/Enderbury'), (b'Pacific/Fakaofo', b'Pacific/Fakaofo'), (b'Pacific/Fiji', b'Pacific/Fiji'), (b'Pacific/Funafuti', b'Pacific/Funafuti'), (b'Pacific/Galapagos', b'Pacific/Galapagos'), (b'Pacific/Gambier', b'Pacific/Gambier'), (b'Pacific/Guadalcanal', b'Pacific/Guadalcanal'), (b'Pacific/Guam', b'Pacific/Guam'), (b'Pacific/Honolulu', b'Pacific/Honolulu'), (b'Pacific/Kiritimati', b'Pacific/Kiritimati'), (b'Pacific/Kosrae', b'Pacific/Kosrae'), (b'Pacific/Kwajalein', b'Pacific/Kwajalein'), (b'Pacific/Majuro', b'Pacific/Majuro'), (b'Pacific/Marquesas', b'Pacific/Marquesas'), (b'Pacific/Midway', b'Pacific/Midway'), (b'Pacific/Nauru', b'Pacific/Nauru'), (b'Pacific/Niue', b'Pacific/Niue'), (b'Pacific/Norfolk', b'Pacific/Norfolk'), (b'Pacific/Noumea', b'Pacific/Noumea'), (b'Pacific/Pago_Pago', b'Pacific/Pago_Pago'), (b'Pacific/Palau', b'Pacific/Palau'), (b'Pacific/Pitcairn', b'Pacific/Pitcairn'), (b'Pacific/Pohnpei', b'Pacific/Pohnpei'), (b'Pacific/Port_Moresby', b'Pacific/Port_Moresby'), (b'Pacific/Rarotonga', b'Pacific/Rarotonga'), (b'Pacific/Saipan', b'Pacific/Saipan'), (b'Pacific/Tahiti', b'Pacific/Tahiti'), (b'Pacific/Tarawa', b'Pacific/Tarawa'), (b'Pacific/Tongatapu', b'Pacific/Tongatapu'), (b'Pacific/Wake', b'Pacific/Wake'), (b'Pacific/Wallis', b'Pacific/Wallis'), (b'US/Alaska', b'US/Alaska'), (b'US/Arizona', b'US/Arizona'), (b'US/Central', b'US/Central'), (b'US/Eastern', b'US/Eastern'), (b'US/Hawaii', b'US/Hawaii'), (b'US/Mountain', b'US/Mountain'), (b'US/Pacific', b'US/Pacific'), (b'UTC', b'UTC')], max_length=48, verbose_name='Timezone'), + ), + ] diff --git a/mayan/apps/common/migrations/0013_auto_20190725_0452.py b/mayan/apps/common/migrations/0013_auto_20190725_0452.py new file mode 100644 index 0000000000..1a8d0b92cd --- /dev/null +++ b/mayan/apps/common/migrations/0013_auto_20190725_0452.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-07-25 04:52 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0012_auto_20190711_0548'), + ] + + operations = [ + migrations.AlterField( + model_name='userlocaleprofile', + name='timezone', + field=models.CharField(choices=[('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmara', 'Africa/Asmara'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'America/Argentina/Catamarca'), ('America/Argentina/Cordoba', 'America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Atikokan', 'America/Atikokan'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Godthab', 'America/Godthab'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Louisville', 'America/Kentucky/Louisville'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nipigon', 'America/Nipigon'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Pangnirtung', 'America/Pangnirtung'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rainy_River', 'America/Rainy_River'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Thunder_Bay', 'America/Thunder_Bay'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('America/Yellowknife', 'America/Yellowknife'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Choibalsan', 'Asia/Choibalsan'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Ho_Chi_Minh', 'Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Kathmandu', 'Asia/Kathmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Kolkata', 'Asia/Kolkata'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yangon', 'Asia/Yangon'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faroe', 'Atlantic/Faroe'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Currie', 'Australia/Currie'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Sydney', 'Australia/Sydney'), ('Canada/Atlantic', 'Canada/Atlantic'), ('Canada/Central', 'Canada/Central'), ('Canada/Eastern', 'Canada/Eastern'), ('Canada/Mountain', 'Canada/Mountain'), ('Canada/Newfoundland', 'Canada/Newfoundland'), ('Canada/Pacific', 'Canada/Pacific'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kiev', 'Europe/Kiev'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Uzhgorod', 'Europe/Uzhgorod'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zaporozhye', 'Europe/Zaporozhye'), ('Europe/Zurich', 'Europe/Zurich'), ('GMT', 'GMT'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Chuuk', 'Pacific/Chuuk'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Enderbury', 'Pacific/Enderbury'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Pohnpei', 'Pacific/Pohnpei'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis'), ('US/Alaska', 'US/Alaska'), ('US/Arizona', 'US/Arizona'), ('US/Central', 'US/Central'), ('US/Eastern', 'US/Eastern'), ('US/Hawaii', 'US/Hawaii'), ('US/Mountain', 'US/Mountain'), ('US/Pacific', 'US/Pacific'), ('UTC', 'UTC')], max_length=48, verbose_name='Timezone'), + ), + ] diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index a014542bc1..861b75df93 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from django.contrib import messages +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.http import Http404, HttpResponseRedirect from django.shortcuts import get_object_or_404 @@ -8,6 +9,7 @@ from django.urls import reverse from django.utils.translation import ungettext, ugettext_lazy as _ from django.views.generic.detail import SingleObjectMixin +from mayan.apps.acls.classes import ModelPermission from mayan.apps.acls.models import AccessControlList from mayan.apps.permissions import Permission @@ -17,6 +19,28 @@ from .literals import PK_LIST_SEPARATOR from .settings import setting_home_view +class ContentTypeViewMixin(object): + """ + This mixin makes it easier for views to retrieve a content type from + the URL pattern. + """ + content_type_url_kw_args = { + 'app_label': 'app_label', + 'model_name': 'model' + } + + def get_content_type(self): + return get_object_or_404( + klass=ContentType, + app_label=self.kwargs[ + self.content_type_url_kw_args['app_label'] + ], + model=self.kwargs[ + self.content_type_url_kw_args['model_name'] + ] + ) + + class DeleteExtraDataMixin(object): """ Mixin to populate the extra data needed for delete views @@ -103,7 +127,15 @@ class ExternalObjectMixin(object): 'get_external_object_queryset() method.' ) - return self.external_object_queryset or self.external_object_class.objects.all() + queryset = self.external_object_queryset + + if not queryset: + manager = ModelPermission.get_manager( + model=self.external_object_class + ) + queryset = manager.all() + + return queryset def get_external_object_queryset_filtered(self): queryset = self.get_external_object_queryset() @@ -118,6 +150,20 @@ class ExternalObjectMixin(object): return queryset +class ExternalContentTypeObjectMixin(ContentTypeViewMixin, ExternalObjectMixin): + """ + Mixin to retrieve an external object by content type from the URL pattern. + """ + external_object_pk_url_kwarg = 'object_id' + + def get_external_object_queryset(self): + content_type = self.get_content_type() + self.external_object_class = content_type.model_class() + return super( + ExternalContentTypeObjectMixin, self + ).get_external_object_queryset() + + class FormExtraKwargsMixin(object): """ Mixin that allows a view to pass extra keyword arguments to forms @@ -250,9 +296,9 @@ class ObjectActionMixin(object): def get_success_message(self, count): return ungettext( - self.success_message, - self.success_message_plural, - count + singular=self.success_message, + plural=self.success_message_plural, + number=count ) % { 'count': count, } @@ -271,14 +317,15 @@ class ObjectActionMixin(object): pass except ActionError: messages.error( - self.request, self.error_message % {'instance': instance} + message=self.error_message % {'instance': instance}, + request=self.request ) else: self.action_count += 1 messages.success( - self.request, - self.get_success_message(count=self.action_count) + message=self.get_success_message(count=self.action_count), + request=self.request ) @@ -383,6 +430,21 @@ class RestrictedQuerysetMixin(object): object_permission = None source_queryset = None + def get_object_permission(self): + return self.object_permission + + def get_queryset(self): + queryset = self.get_source_queryset() + object_permission = self.get_object_permission() + + if object_permission: + queryset = AccessControlList.objects.restrict_queryset( + permission=object_permission, queryset=queryset, + user=self.request.user + ) + + return queryset + def get_source_queryset(self): if self.source_queryset is None: if self.model: @@ -398,17 +460,6 @@ class RestrictedQuerysetMixin(object): return self.source_queryset.all() - def get_queryset(self): - queryset = self.get_source_queryset() - - if self.object_permission: - queryset = AccessControlList.objects.restrict_queryset( - permission=self.object_permission, queryset=queryset, - user=self.request.user - ) - - return queryset - class ViewPermissionCheckMixin(object): """ @@ -420,11 +471,16 @@ class ViewPermissionCheckMixin(object): view_permission = None def dispatch(self, request, *args, **kwargs): - if self.view_permission: + view_permission = self.get_view_permission() + if view_permission: Permission.check_user_permissions( - permissions=(self.view_permission,), user=self.request.user + permissions=(view_permission,), + user=self.request.user ) return super( ViewPermissionCheckMixin, self ).dispatch(request, *args, **kwargs) + + def get_view_permission(self): + return self.view_permission diff --git a/mayan/apps/common/serialization.py b/mayan/apps/common/serialization.py new file mode 100644 index 0000000000..27f9b52c06 --- /dev/null +++ b/mayan/apps/common/serialization.py @@ -0,0 +1,22 @@ +from __future__ import unicode_literals + +import yaml + +try: + from yaml import CSafeLoader as SafeLoader, CSafeDumper as SafeDumper +except ImportError: + from yaml import SafeLoader, SafeDumper + + +def yaml_dump(*args, **kwargs): + defaults = {'Dumper': SafeDumper} + defaults.update(kwargs) + + return yaml.dump(*args, **defaults) + + +def yaml_load(*args, **kwargs): + defaults = {'Loader': SafeLoader} + defaults.update(kwargs) + + return yaml.load(*args, **defaults) diff --git a/mayan/apps/common/settings.py b/mayan/apps/common/settings.py index 84009fe170..c845cd0a6e 100644 --- a/mayan/apps/common/settings.py +++ b/mayan/apps/common/settings.py @@ -10,7 +10,6 @@ from mayan.apps.smart_settings.classes import Namespace from .literals import DEFAULT_COMMON_HOME_VIEW - namespace = Namespace(label=_('Common'), name='common') setting_auto_logging = namespace.add_setting( @@ -95,322 +94,5 @@ setting_shared_storage = namespace.add_setting( ) setting_shared_storage_arguments = namespace.add_setting( global_name='COMMON_SHARED_STORAGE_ARGUMENTS', - default='{{location: {}}}'.format( - os.path.join(settings.MEDIA_ROOT, 'shared_files') - ), quoted=True -) - -namespace = Namespace(label=_('Django'), name='django') - -setting_django_allowed_hosts = namespace.add_setting( - global_name='ALLOWED_HOSTS', default=settings.ALLOWED_HOSTS, - help_text=_( - 'A list of strings representing the host/domain names that this site ' - 'can serve. This is a security measure to prevent HTTP Host header ' - 'attacks, which are possible even under many seemingly-safe web ' - 'server configurations. Values in this list can be ' - 'fully qualified names (e.g. \'www.example.com\'), in which case ' - 'they will be matched against the request\'s Host header exactly ' - '(case-insensitive, not including port). A value beginning with a ' - 'period can be used as a subdomain wildcard: \'.example.com\' will ' - 'match example.com, www.example.com, and any other subdomain of ' - 'example.com. A value of \'*\' will match anything; in this case you ' - 'are responsible to provide your own validation of the Host header ' - '(perhaps in a middleware; if so this middleware must be listed ' - 'first in MIDDLEWARE).' - ), -) -setting_django_append_slash = namespace.add_setting( - global_name='APPEND_SLASH', default=settings.APPEND_SLASH, - help_text=_( - 'When set to True, if the request URL does not match any of the ' - 'patterns in the URLconf and it doesn\'t end in a slash, an HTTP ' - 'redirect is issued to the same URL with a slash appended. Note ' - 'that the redirect may cause any data submitted in a POST request ' - 'to be lost. The APPEND_SLASH setting is only used if ' - 'CommonMiddleware is installed (see Middleware). See also ' - 'PREPEND_WWW.' - ) -) -setting_django_auth_password_validators = namespace.add_setting( - global_name='AUTH_PASSWORD_VALIDATORS', - default=settings.AUTH_PASSWORD_VALIDATORS, - help_text=_( - 'The list of validators that are used to check the strength of ' - 'user\'s passwords.' - ) -) -setting_django_databases = namespace.add_setting( - global_name='DATABASES', default=settings.DATABASES, - help_text=_( - 'A dictionary containing the settings for all databases to be used ' - 'with Django. It is a nested dictionary whose contents map a ' - 'database alias to a dictionary containing the options for an ' - 'individual database. The DATABASES setting must configure a ' - 'default database; any number of additional databases may also ' - 'be specified.' - ), -) -setting_django_data_upload_max_memory_size = namespace.add_setting( - global_name='DATA_UPLOAD_MAX_MEMORY_SIZE', - default=settings.DATA_UPLOAD_MAX_MEMORY_SIZE, - help_text=_( - 'Default: 2621440 (i.e. 2.5 MB). The maximum size in bytes that a ' - 'request body may be before a SuspiciousOperation ' - '(RequestDataTooBig) is raised. The check is done when accessing ' - 'request.body or request.POST and is calculated against the total ' - 'request size excluding any file upload data. You can set this to ' - 'None to disable the check. Applications that are expected to ' - 'receive unusually large form posts should tune this setting. The ' - 'amount of request data is correlated to the amount of memory ' - 'needed to process the request and populate the GET and POST ' - 'dictionaries. Large requests could be used as a ' - 'denial-of-service attack vector if left unchecked. Since web ' - 'servers don\'t typically perform deep request inspection, it\'s ' - 'not possible to perform a similar check at that level. See also ' - 'FILE_UPLOAD_MAX_MEMORY_SIZE.' - ), -) -setting_django_default_from_email = namespace.add_setting( - global_name='DEFAULT_FROM_EMAIL', - default=settings.DEFAULT_FROM_EMAIL, - help_text=_( - 'Default: \'webmaster@localhost\' ' - 'Default email address to use for various automated correspondence ' - 'from the site manager(s). This doesn\'t include error messages sent ' - 'to ADMINS and MANAGERS; for that, see SERVER_EMAIL.' - ), -) -setting_django_disallowed_user_agents = namespace.add_setting( - global_name='DISALLOWED_USER_AGENTS', - default=settings.DISALLOWED_USER_AGENTS, - help_text=_( - 'Default: [] (Empty list). List of compiled regular expression ' - 'objects representing User-Agent strings that are not allowed to ' - 'visit any page, systemwide. Use this for bad robots/crawlers. ' - 'This is only used if CommonMiddleware is installed ' - '(see Middleware).' - ), -) -setting_django_email_backend = namespace.add_setting( - global_name='EMAIL_BACKEND', - default=settings.EMAIL_BACKEND, - help_text=_( - 'Default: \'django.core.mail.backends.smtp.EmailBackend\'. The ' - 'backend to use for sending emails.' - ), -) -setting_django_email_host = namespace.add_setting( - global_name='EMAIL_HOST', - default=settings.EMAIL_HOST, - help_text=_( - 'Default: \'localhost\'. The host to use for sending email.' - ), -) -setting_django_email_host_password = namespace.add_setting( - global_name='EMAIL_HOST_PASSWORD', - default=settings.EMAIL_HOST_PASSWORD, - help_text=_( - 'Default: \'\' (Empty string). Password to use for the SMTP ' - 'server defined in EMAIL_HOST. This setting is used in ' - 'conjunction with EMAIL_HOST_USER when authenticating to the ' - 'SMTP server. If either of these settings is empty, ' - 'Django won\'t attempt authentication.' - ), -) -setting_django_email_host_user = namespace.add_setting( - global_name='EMAIL_HOST_USER', - default=settings.EMAIL_HOST_USER, - help_text=_( - 'Default: \'\' (Empty string). Username to use for the SMTP ' - 'server defined in EMAIL_HOST. If empty, Django won\'t attempt ' - 'authentication.' - ), -) -setting_django_email_port = namespace.add_setting( - global_name='EMAIL_PORT', - default=settings.EMAIL_PORT, - help_text=_( - 'Default: 25. Port to use for the SMTP server defined in EMAIL_HOST.' - ), -) -setting_django_email_timeout = namespace.add_setting( - global_name='EMAIL_TIMEOUT', - default=settings.EMAIL_TIMEOUT, - help_text=_( - 'Default: None. Specifies a timeout in seconds for blocking ' - 'operations like the connection attempt.' - ), -) -setting_django_email_user_tls = namespace.add_setting( - global_name='EMAIL_USE_TLS', - default=settings.EMAIL_USE_TLS, - help_text=_( - 'Default: False. Whether to use a TLS (secure) connection when ' - 'talking to the SMTP server. This is used for explicit TLS ' - 'connections, generally on port 587. If you are experiencing ' - 'hanging connections, see the implicit TLS setting EMAIL_USE_SSL.' - ), -) -setting_django_email_user_ssl = namespace.add_setting( - global_name='EMAIL_USE_SSL', - default=settings.EMAIL_USE_SSL, - help_text=_( - 'Default: False. Whether to use an implicit TLS (secure) connection ' - 'when talking to the SMTP server. In most email documentation this ' - 'type of TLS connection is referred to as SSL. It is generally used ' - 'on port 465. If you are experiencing problems, see the explicit ' - 'TLS setting EMAIL_USE_TLS. Note that EMAIL_USE_TLS/EMAIL_USE_SSL ' - 'are mutually exclusive, so only set one of those settings to True.' - ), -) -setting_django_file_upload_max_memory_size = namespace.add_setting( - global_name='FILE_UPLOAD_MAX_MEMORY_SIZE', - default=settings.FILE_UPLOAD_MAX_MEMORY_SIZE, - help_text=_( - 'Default: 2621440 (i.e. 2.5 MB). The maximum size (in bytes) ' - 'that an upload will be before it gets streamed to the file ' - 'system. See Managing files for details. See also ' - 'DATA_UPLOAD_MAX_MEMORY_SIZE.' - ), -) -setting_django_login_url = namespace.add_setting( - global_name='LOGIN_URL', - default=settings.LOGIN_URL, - help_text=_( - 'Default: \'/accounts/login/\' The URL where requests are ' - 'redirected for login, especially when using the login_required() ' - 'decorator. This setting also accepts named URL patterns which ' - 'can be used to reduce configuration duplication since you ' - 'don\'t have to define the URL in two places (settings ' - 'and URLconf).' - ) -) -setting_django_login_redirect_url = namespace.add_setting( - global_name='LOGIN_REDIRECT_URL', - default=settings.LOGIN_REDIRECT_URL, - help_text=_( - 'Default: \'/accounts/profile/\' The URL where requests are ' - 'redirected after login when the contrib.auth.login view gets no ' - 'next parameter. This is used by the login_required() decorator, ' - 'for example. This setting also accepts named URL patterns which ' - 'can be used to reduce configuration duplication since you don\'t ' - 'have to define the URL in two places (settings and URLconf).' - ), -) -setting_django_logout_redirect_url = namespace.add_setting( - global_name='LOGOUT_REDIRECT_URL', - default=settings.LOGOUT_REDIRECT_URL, - help_text=_( - 'Default: None. The URL where requests are redirected after a user ' - 'logs out using LogoutView (if the view doesn\'t get a next_page ' - 'argument). If None, no redirect will be performed and the logout ' - 'view will be rendered. This setting also accepts named URL ' - 'patterns which can be used to reduce configuration duplication ' - 'since you don\'t have to define the URL in two places (settings ' - 'and URLconf).' - ) -) -setting_django_internal_ips = namespace.add_setting( - global_name='INTERNAL_IPS', - default=settings.INTERNAL_IPS, - help_text=_( - 'A list of IP addresses, as strings, that: Allow the debug() ' - 'context processor to add some variables to the template context. ' - 'Can use the admindocs bookmarklets even if not logged in as a ' - 'staff user. Are marked as "internal" (as opposed to "EXTERNAL") ' - 'in AdminEmailHandler emails.' - ), -) -setting_django_languages = namespace.add_setting( - global_name='LANGUAGES', - default=settings.LANGUAGES, - help_text=_( - 'A list of all available languages. The list is a list of ' - 'two-tuples in the format (language code, language name) ' - 'for example, (\'ja\', \'Japanese\'). This specifies which ' - 'languages are available for language selection. ' - 'Generally, the default value should suffice. Only set this ' - 'setting if you want to restrict language selection to a ' - 'subset of the Django-provided languages. ' - ), -) -setting_django_language_code = namespace.add_setting( - global_name='LANGUAGE_CODE', - default=settings.LANGUAGE_CODE, - help_text=_( - 'A string representing the language code for this installation. ' - 'This should be in standard language ID format. For example, U.S. ' - 'English is "en-us". It serves two purposes: If the locale ' - 'middleware isn\'t in use, it decides which translation is served ' - 'to all users. If the locale middleware is active, it provides a ' - 'fallback language in case the user\'s preferred language can\'t ' - 'be determined or is not supported by the website. It also provides ' - 'the fallback translation when a translation for a given literal ' - 'doesn\'t exist for the user\'s preferred language.' - ), -) -setting_django_static_url = namespace.add_setting( - global_name='STATIC_URL', - default=settings.STATIC_URL, - help_text=_( - 'URL to use when referring to static files located in STATIC_ROOT. ' - 'Example: "/static/" or "http://static.example.com/" ' - 'If not None, this will be used as the base path for asset ' - 'definitions (the Media class) and the staticfiles app. ' - 'It must end in a slash if set to a non-empty value.' - ), -) -setting_django_staticfiles_storage = namespace.add_setting( - global_name='STATICFILES_STORAGE', - default=settings.STATICFILES_STORAGE, - help_text=_( - 'The file storage engine to use when collecting static files with ' - 'the collectstatic management command. A ready-to-use instance of ' - 'the storage backend defined in this setting can be found at ' - 'django.contrib.staticfiles.storage.staticfiles_storage.' - ), -) -setting_django_time_zone = namespace.add_setting( - global_name='TIME_ZONE', - default=settings.TIME_ZONE, - help_text=_( - 'A string representing the time zone for this installation. ' - 'Note that this isn\'t necessarily the time zone of the server. ' - 'For example, one server may serve multiple Django-powered sites, ' - 'each with a separate time zone setting.' - ), -) -setting_django_wsgi_application = namespace.add_setting( - global_name='WSGI_APPLICATION', - default=settings.WSGI_APPLICATION, - help_text=_( - 'The full Python path of the WSGI application object that Django\'s ' - 'built-in servers (e.g. runserver) will use. The django-admin ' - 'startproject management command will create a simple wsgi.py ' - 'file with an application callable in it, and point this setting ' - 'to that application.' - ), -) - -namespace = Namespace(label=_('Celery'), name='celery') - -setting_celery_broker_url = namespace.add_setting( - global_name='BROKER_URL', default=settings.BROKER_URL, - help_text=_( - 'Default: "amqp://". Default broker URL. This must be a URL in ' - 'the form of: transport://userid:password@hostname:port/virtual_host ' - 'Only the scheme part (transport://) is required, the rest is ' - 'optional, and defaults to the specific transports default values.' - ), -) -setting_celery_result_backend = namespace.add_setting( - global_name='CELERY_RESULT_BACKEND', - default=settings.CELERY_RESULT_BACKEND, - help_text=_( - 'Default: No result backend enabled by default. The backend used ' - 'to store task results (tombstones). Refer to ' - 'http://docs.celeryproject.org/en/v4.1.0/userguide/configuration.' - 'html#result-backend' - ) + default={'location': os.path.join(settings.MEDIA_ROOT, 'shared_files')} ) diff --git a/mayan/apps/common/storages.py b/mayan/apps/common/storages.py index ded073d4e8..422b20682a 100644 --- a/mayan/apps/common/storages.py +++ b/mayan/apps/common/storages.py @@ -1,23 +1,11 @@ from __future__ import unicode_literals -import yaml - -try: - from yaml import CSafeLoader as SafeLoader -except ImportError: - from yaml import SafeLoader - -from django.utils.module_loading import import_string +from mayan.apps.storage.utils import get_storage_subclass from .settings import ( setting_shared_storage, setting_shared_storage_arguments ) -storage_sharedupload = import_string( +storage_sharedupload = get_storage_subclass( dotted_path=setting_shared_storage.value -)( - **yaml.load( - stream=setting_shared_storage_arguments.value or '{}', - Loader=SafeLoader - ) -) +)(**setting_shared_storage_arguments.value) diff --git a/mayan/apps/common/tests/__init__.py b/mayan/apps/common/tests/__init__.py index 94b5479a25..e69de29bb2 100644 --- a/mayan/apps/common/tests/__init__.py +++ b/mayan/apps/common/tests/__init__.py @@ -1,2 +0,0 @@ -from .base import BaseTestCase, GenericViewTestCase # NOQA -from .decorators import skip_file_descriptor_check # NOQA diff --git a/mayan/apps/common/tests/test_api.py b/mayan/apps/common/tests/test_api.py index 8484f565b4..8c5ae7a0aa 100644 --- a/mayan/apps/common/tests/test_api.py +++ b/mayan/apps/common/tests/test_api.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.test import override_settings -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..classes import Template diff --git a/mayan/apps/common/tests/test_compressed_files.py b/mayan/apps/common/tests/test_compressed_files.py index cc78d7f7ac..18d2071a17 100644 --- a/mayan/apps/common/tests/test_compressed_files.py +++ b/mayan/apps/common/tests/test_compressed_files.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import BaseTestCase +from mayan.apps.common.tests.base import BaseTestCase from ..compressed_files import Archive, TarArchive, ZipArchive diff --git a/mayan/apps/common/urls.py b/mayan/apps/common/urls.py index 13fd7e3fac..26d1c317e4 100644 --- a/mayan/apps/common/urls.py +++ b/mayan/apps/common/urls.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from django.conf.urls import url -from django.views.i18n import JavaScriptCatalog, set_language +from django.views.i18n import JavaScriptCatalog from .api_views import ( APIContentTypeList, APITemplateDetailView, APITemplateListView @@ -10,30 +10,10 @@ from .views import ( AboutView, CurrentUserLocaleProfileDetailsView, CurrentUserLocaleProfileEditView, FaviconRedirectView, HomeView, LicenseView, ObjectErrorLogEntryListClearView, ObjectErrorLogEntryListView, - RootView, SetupListView, ToolsListView, multi_object_action_view + RootView, SetupListView, ToolsListView ) -urlpatterns = [ - url(regex=r'^$', view=RootView.as_view(), name='root'), - url(regex=r'^home/$', view=HomeView.as_view(), name='home'), - url(regex=r'^about/$', view=AboutView.as_view(), name='about_view'), - url(regex=r'^license/$', view=LicenseView.as_view(), name='license_view'), - url( - regex=r'^object/multiple/action/$', view=multi_object_action_view, - name='multi_object_action_view' - ), - url(regex=r'^setup/$', view=SetupListView.as_view(), name='setup_list'), - url(regex=r'^tools/$', view=ToolsListView.as_view(), name='tools_list'), - url( - regex=r'^user/locale/$', - view=CurrentUserLocaleProfileDetailsView.as_view(), - name='current_user_locale_profile_details' - ), - url( - regex=r'^user/locale/edit/$', - view=CurrentUserLocaleProfileEditView.as_view(), - name='current_user_locale_profile_edit' - ), +urlpatterns_error_logs = [ url( regex=r'^object/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/errors/$', view=ObjectErrorLogEntryListView.as_view(), name='object_error_list' @@ -45,7 +25,20 @@ urlpatterns = [ ), ] -urlpatterns += [ +urlpatterns_user_locale = [ + url( + regex=r'^user/locale/$', + view=CurrentUserLocaleProfileDetailsView.as_view(), + name='current_user_locale_profile_details' + ), + url( + regex=r'^user/locale/edit/$', + view=CurrentUserLocaleProfileEditView.as_view(), + name='current_user_locale_profile_edit' + ), +] + +urlpatterns_misc = [ url( regex=r'^favicon\.ico$', view=FaviconRedirectView.as_view() ), @@ -53,11 +46,21 @@ urlpatterns += [ regex=r'^jsi18n/(?P\S+?)/$', view=JavaScriptCatalog.as_view(), name='javascript_catalog' ), - url( - regex=r'^set_language/$', view=set_language, name='set_language' - ), ] +urlpatterns = [ + url(regex=r'^$', view=RootView.as_view(), name='root'), + url(regex=r'^home/$', view=HomeView.as_view(), name='home'), + url(regex=r'^about/$', view=AboutView.as_view(), name='about_view'), + url(regex=r'^license/$', view=LicenseView.as_view(), name='license_view'), + url(regex=r'^setup/$', view=SetupListView.as_view(), name='setup_list'), + url(regex=r'^tools/$', view=ToolsListView.as_view(), name='tools_list'), +] + +urlpatterns.extend(urlpatterns_error_logs) +urlpatterns.extend(urlpatterns_misc) +urlpatterns.extend(urlpatterns_user_locale) + api_urls = [ url( regex=r'^content_types/$', view=APIContentTypeList.as_view(), diff --git a/mayan/apps/common/utils.py b/mayan/apps/common/utils.py index 2d76f17d43..85447e2813 100644 --- a/mayan/apps/common/utils.py +++ b/mayan/apps/common/utils.py @@ -21,14 +21,6 @@ def check_for_sqlite(): return settings.DATABASES['default']['ENGINE'] == DJANGO_SQLITE_BACKEND and settings.DEBUG is False -def encapsulate(function): - # Workaround Django ticket 15791 - # Changeset 16045 - # http://stackoverflow.com/questions/6861601/ - # cannot-resolve-callable-context-variable/6955045#6955045 - return lambda: function - - def get_related_field(model, related_field_name): try: local_field_name, remaining_field_path = related_field_name.split( diff --git a/mayan/apps/common/views.py b/mayan/apps/common/views.py index 30f16cc0f4..b78a0483c8 100644 --- a/mayan/apps/common/views.py +++ b/mayan/apps/common/views.py @@ -1,15 +1,11 @@ from __future__ import absolute_import, unicode_literals -from json import dumps - from django.conf import settings from django.contrib import messages from django.contrib.contenttypes.models import ContentType -from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 -from django.urls import reverse, reverse_lazy +from django.urls import reverse_lazy from django.utils import timezone, translation -from django.utils.http import urlencode from django.utils.translation import ugettext_lazy as _ from django.views.generic import RedirectView @@ -220,67 +216,3 @@ class ToolsListView(SimpleView): 'These modules are used to do system maintenance.' ) } - - -def multi_object_action_view(request): - """ - Proxy view called first when using a multi object action, which - then redirects to the appropriate specialized view - """ - next = request.POST.get( - 'next', request.GET.get( - 'next', request.META.get( - 'HTTP_REFERER', reverse(setting_home_view.value) - ) - ) - ) - - action = request.GET.get('action', None) - id_list = ','.join( - [key[3:] for key in request.GET.keys() if key.startswith('pk_')] - ) - items_property_list = [ - (key[11:]) for key in request.GET.keys() if key.startswith('properties_') - ] - - if not action: - messages.error( - message=_('No action selected.'), request=request - ) - return HttpResponseRedirect( - redirect_to=request.META.get( - 'HTTP_REFERER', reverse(setting_home_view.value) - ) - ) - - if not id_list and not items_property_list: - messages.error( - message=_('Must select at least one item.'), - request=request - ) - return HttpResponseRedirect( - redirect_to=request.META.get( - 'HTTP_REFERER', reverse(setting_home_view.value) - ) - ) - - # Separate redirects to keep backwards compatibility with older - # functions that don't expect a properties_list parameter - if items_property_list: - return HttpResponseRedirect( - redirect_to='%s?%s' % ( - action, - urlencode( - { - 'items_property_list': dumps(items_property_list), - 'next': next - } - ) - ) - ) - else: - return HttpResponseRedirect( - redirect_to='%s?%s' % ( - action, urlencode({'id_list': id_list, 'next': next}) - ) - ) diff --git a/mayan/apps/converter/apps.py b/mayan/apps/converter/apps.py index af8a668c6f..03e366c9e9 100644 --- a/mayan/apps/converter/apps.py +++ b/mayan/apps/converter/apps.py @@ -3,14 +3,15 @@ from __future__ import unicode_literals from django.utils.encoding import force_text 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_object, menu_secondary from mayan.apps.navigation.classes import SourceColumn from .dependencies import * # NOQA from .links import ( - link_transformation_create, link_transformation_delete, - link_transformation_edit + link_transformation_delete, link_transformation_edit, + link_transformation_select ) @@ -24,26 +25,31 @@ class ConverterApp(MayanAppConfig): def ready(self): super(ConverterApp, self).ready() - Transformation = self.get_model(model_name='Transformation') + LayerTransformation = self.get_model(model_name='LayerTransformation') - SourceColumn(attribute='order', source=Transformation) + ModelPermission.register_inheritance( + model=LayerTransformation, + related='object_layer__content_object', + ) + + SourceColumn(attribute='order', source=LayerTransformation) SourceColumn( - source=Transformation, label=_('Transformation'), + source=LayerTransformation, label=_('Transformation'), func=lambda context: force_text(context['object']) ) SourceColumn( - attribute='arguments', source=Transformation + attribute='arguments', source=LayerTransformation ) menu_object.bind_links( links=(link_transformation_edit, link_transformation_delete), - sources=(Transformation,) + sources=(LayerTransformation,) ) menu_secondary.bind_links( - links=(link_transformation_create,), sources=(Transformation,) + links=(link_transformation_select,), sources=(LayerTransformation,) ) menu_secondary.bind_links( - links=(link_transformation_create,), + links=(link_transformation_select,), sources=( 'converter:transformation_create', 'converter:transformation_list' diff --git a/mayan/apps/converter/backends/python.py b/mayan/apps/converter/backends/python.py index 672997f879..700ad2a381 100644 --- a/mayan/apps/converter/backends/python.py +++ b/mayan/apps/converter/backends/python.py @@ -7,11 +7,6 @@ import shutil from PIL import Image import PyPDF2 import sh -import yaml -try: - from yaml import CSafeLoader as SafeLoader -except ImportError: - from yaml import SafeLoader from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ @@ -20,16 +15,14 @@ from mayan.apps.storage.utils import NamedTemporaryFile from ..classes import ConverterBase from ..exceptions import PageCountError -from ..settings import setting_graphics_backend_config +from ..settings import setting_graphics_backend_arguments from ..literals import ( DEFAULT_PDFTOPPM_DPI, DEFAULT_PDFTOPPM_FORMAT, DEFAULT_PDFTOPPM_PATH, DEFAULT_PDFINFO_PATH ) -pdftoppm_path = yaml.load( - stream=setting_graphics_backend_config.value, Loader=SafeLoader -).get( +pdftoppm_path = setting_graphics_backend_arguments.value.get( 'pdftoppm_path', DEFAULT_PDFTOPPM_PATH ) @@ -39,26 +32,20 @@ except sh.CommandNotFound: pdftoppm = None else: pdftoppm_format = '-{}'.format( - yaml.load( - stream=setting_graphics_backend_config.value, Loader=SafeLoader - ).get( + setting_graphics_backend_arguments.value.get( 'pdftoppm_format', DEFAULT_PDFTOPPM_FORMAT ) ) pdftoppm_dpi = format( - yaml.load( - stream=setting_graphics_backend_config.value, Loader=SafeLoader - ).get( + setting_graphics_backend_arguments.value.get( 'pdftoppm_dpi', DEFAULT_PDFTOPPM_DPI ) ) pdftoppm = pdftoppm.bake(pdftoppm_format, '-r', pdftoppm_dpi) -pdfinfo_path = yaml.load( - stream=setting_graphics_backend_config.value, Loader=SafeLoader -).get( +pdfinfo_path = setting_graphics_backend_arguments.value.get( 'pdfinfo_path', DEFAULT_PDFINFO_PATH ) diff --git a/mayan/apps/converter/classes.py b/mayan/apps/converter/classes.py index 517b04ac51..e99b464438 100644 --- a/mayan/apps/converter/classes.py +++ b/mayan/apps/converter/classes.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import copy from io import BytesIO import logging import os @@ -7,16 +8,17 @@ 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.apps import apps +from django.core.exceptions import ImproperlyConfigured +from django.db import transaction +from django.utils.encoding import force_text, python_2_unicode_compatible +from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ +from mayan.apps.appearance.classes import Icon from mayan.apps.mimetype.api import get_mimetype +from mayan.apps.navigation.classes import Link from mayan.apps.storage.settings import setting_temporary_directory from mayan.apps.storage.utils import ( NamedTemporaryFile, fs_cleanup, mkdtemp @@ -27,16 +29,14 @@ from .literals import ( CONVERTER_OFFICE_FILE_MIMETYPES, DEFAULT_LIBREOFFICE_PATH, DEFAULT_PAGE_NUMBER, DEFAULT_PILLOW_FORMAT ) -from .settings import setting_graphics_backend_config +from .settings import setting_graphics_backend_arguments -logger = logging.getLogger(__name__) -BACKEND_CONFIG = yaml.load( - stream=setting_graphics_backend_config.value, Loader=SafeLoader -) -libreoffice_path = BACKEND_CONFIG.get( +libreoffice_path = setting_graphics_backend_arguments.value.get( 'libreoffice_path', DEFAULT_LIBREOFFICE_PATH ) +logger = logging.getLogger(__name__) + class ConverterBase(object): def __init__(self, file_object, mime_type=None): @@ -62,9 +62,7 @@ class ConverterBase(object): pass def get_page(self, output_format=None): - output_format = output_format or yaml.load( - stream=setting_graphics_backend_config.value, Loader=SafeLoader - ).get( + output_format = output_format or setting_graphics_backend_arguments.value.get( 'pillow_format', DEFAULT_PILLOW_FORMAT ) @@ -156,7 +154,7 @@ class ConverterBase(object): logger.error('Exception launching Libre Office; %s', exception) raise finally: - fs_cleanup(libreoffice_home_directory) + fs_cleanup(filename=libreoffice_home_directory) # LibreOffice return a PDF file with the same name as the input # provided but with the .pdf extension. @@ -190,7 +188,7 @@ class ConverterBase(object): shutil.copyfileobj( fsrc=converted_file_object, fdst=temporary_converted_file_object ) - fs_cleanup(converted_file_path) + fs_cleanup(filename=converted_file_path) temporary_converted_file_object.seek(0) return temporary_converted_file_object @@ -212,3 +210,228 @@ class ConverterBase(object): for transformation in transformations: self.image = transformation.execute_on(image=self.image) + + +@python_2_unicode_compatible +class Layer(object): + _registry = {} + + @classmethod + def all(cls): + return cls._registry.values() + + @classmethod + def get(cls, name): + return cls._registry[name] + + @classmethod + def get_by_value(cls, key, value): + for name, layer in cls._registry.items(): + if getattr(layer, key) == value: + return layer + + @classmethod + def invalidate_cache(cls): + for layer in cls.all(): + layer.__dict__.pop('stored_layer', None) + + @classmethod + def update(cls): + for layer in cls.all(): + layer.stored_layer + + def __init__( + self, label, name, order, permissions, default=False, + empty_results_text=None, symbol=None, + ): + """ + access_permission is the permission necessary to view the layer. + exclude_permission is the permission necessary to discard the layer. + """ + self.default = default + self.empty_results_text = empty_results_text + self.label = label + self.name = name + self.order = order + self.permissions = permissions + self.symbol = symbol + + # Check order + layer = self.__class__.get_by_value(key='order', value=self.order) + + if layer: + raise ImproperlyConfigured( + 'Layer "{}" already has order "{}" requested by layer "{}"'.format( + layer.name, order, self.name + ) + ) + + # Check default + if default: + layer = self.__class__.get_by_value(key='default', value=True) + if layer: + raise ImproperlyConfigured( + 'Layer "{}" is already the default layer; "{}"'.format( + layer.name, self.name + ) + ) + + self.__class__._registry[name] = self + + def get_permission(self, name): + return self.permissions.get(name, None) + + def __str__(self): + return force_text(self.label) + + def add_transformation_to(self, obj, transformation_class, arguments=None): + ContentType = apps.get_model( + app_label='contenttypes', model_name='ContentType' + ) + content_type = ContentType.objects.get_for_model(model=obj) + object_layer, created = self.stored_layer.object_layers.get_or_create( + content_type=content_type, object_id=obj.pk + ) + object_layer.transformations.create( + name=transformation_class.name, arguments=arguments + ) + + def copy_transformations(self, source, targets): + """ + Copy transformation from source to all targets + """ + ContentType = apps.get_model( + app_label='contenttypes', model_name='ContentType' + ) + + transformations = self.get_transformations_for(obj=source) + + with transaction.atomic(): + for target in targets: + content_type = ContentType.objects.get_for_model(model=target) + object_layer, created = self.stored_layer.object_layers.get_or_create( + content_type=content_type, object_id=target.pk + ) + for transformation in transformations: + object_layer.transformations.create( + order=transformation.order, + name=transformation.name, + arguments=transformation.arguments, + ) + + def get_empty_results_text(self): + if self.empty_results_text: + return self.empty_results_text + else: + return _( + 'Transformations allow changing the visual appearance ' + 'of documents without making permanent changes to the ' + 'document file themselves.' + ) + + def get_icon(self): + return Icon(driver_name='fontawesome', symbol=self.symbol) + + def get_model_instance(self): + StoredLayer = apps.get_model( + app_label='converter', model_name='StoredLayer' + ) + stored_layer, created = StoredLayer.objects.update_or_create( + name=self.name, defaults={'order': self.order} + ) + + return stored_layer + + def get_transformations_for(self, obj, as_classes=False): + """ + as_classes == True returns the transformation classes from .classes + ready to be feed to the converter class + """ + LayerTransformation = apps.get_model( + app_label='converter', model_name='LayerTransformation' + ) + + return LayerTransformation.objects.get_for_object( + obj=obj, as_classes=as_classes, + only_stored_layer=self.stored_layer + ) + + @cached_property + def stored_layer(self): + return self.get_model_instance() + + +class LayerLink(Link): + @staticmethod + def set_icon(instance, layer): + if instance.action == 'list': + if layer.symbol: + instance.icon_class = layer.get_icon() + + def __init__(self, action, layer, object_name=None, **kwargs): + super(LayerLink, self).__init__(**kwargs) + self.action = action + self.layer = layer + self.object_name = object_name or _('transformation') + + permission = layer.permissions.get(action, None) + if permission: + self.permissions = (permission,) + + if action == 'list': + self.kwargs = LayerLinkKwargsFactory( + layer_name=layer.name + ).get_kwargs_function() + + if action in ('create', 'select'): + self.kwargs = LayerLinkKwargsFactory().get_kwargs_function() + + LayerLink.set_icon(instance=self, layer=layer) + + def copy(self, layer): + result = copy.copy(self) + result.kwargs = LayerLinkKwargsFactory( + layer_name=layer.name + ).get_kwargs_function() + result._layer_name = layer.name + + LayerLink.set_icon(instance=result, layer=layer) + + return result + + @cached_property + def layer_name(self): + return getattr( + self, '_layer_name', Layer.get_by_value( + key='default', value=True + ).name + ) + + +class LayerLinkKwargsFactory(object): + def __init__(self, layer_name=None, variable_name='resolved_object'): + self.layer_name = layer_name + self.variable_name = variable_name + + def get_kwargs_function(self): + def get_kwargs(context): + ContentType = apps.get_model( + app_label='contenttypes', model_name='ContentType' + ) + + content_type = ContentType.objects.get_for_model( + context[self.variable_name] + ) + default_layer = Layer.get_by_value(key='default', value=True) + return { + 'app_label': '"{}"'.format(content_type.app_label), + 'model': '"{}"'.format(content_type.model), + 'object_id': '{}.pk'.format(self.variable_name), + 'layer_name': '"{}"'.format( + self.layer_name or context.get( + 'layer_name', default_layer.name + ) + ) + } + + return get_kwargs diff --git a/mayan/apps/converter/forms.py b/mayan/apps/converter/forms.py index 73749d2897..72de022c1a 100644 --- a/mayan/apps/converter/forms.py +++ b/mayan/apps/converter/forms.py @@ -2,26 +2,59 @@ 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 .models import Transformation +from mayan.apps.common.serialization import yaml_load + +from .models import LayerTransformation +from .transformations import BaseTransformation -class TransformationForm(forms.ModelForm): +class LayerTransformationSelectForm(forms.Form): + def __init__(self, *args, **kwargs): + layer = kwargs.pop('layer') + super(LayerTransformationSelectForm, self).__init__(*args, **kwargs) + self.fields[ + 'transformation' + ].choices = BaseTransformation.get_transformation_choices(layer=layer) + + transformation = forms.ChoiceField( + choices=(), help_text=_('Available transformations for this layer.'), + label=_('Transformation'), + ) + + +class LayerTransformationForm(forms.ModelForm): class Meta: - fields = ('name', 'arguments', 'order') - model = Transformation + fields = ('arguments', 'order') + model = LayerTransformation + + def __init__(self, *args, **kwargs): + transformation_name = kwargs.pop('transformation_name', None) + super(LayerTransformationForm, self).__init__(*args, **kwargs) + + if not transformation_name: + # Get the template name when the transformation is being edited. + template_name = getattr( + self.instance.get_transformation_class(), 'template_name', + None + ) + else: + # Get the template name when the transformation is being created + template_name = getattr( + BaseTransformation.get(name=transformation_name), + 'template_name', None + ) + + if template_name: + self.fields['arguments'].widget.attrs['class'] = 'hidden' + self.fields['order'].widget.attrs['class'] = 'hidden' def clean(self): try: - yaml.load(stream=self.cleaned_data['arguments'], Loader=SafeLoader) + yaml_load(stream=self.cleaned_data['arguments']) except yaml.YAMLError: raise ValidationError( _( diff --git a/mayan/apps/converter/icons.py b/mayan/apps/converter/icons.py index f91d1a9d13..9eaa5d928e 100644 --- a/mayan/apps/converter/icons.py +++ b/mayan/apps/converter/icons.py @@ -4,10 +4,7 @@ from mayan.apps.appearance.classes import Icon icon_transformations = Icon(driver_name='fontawesome', symbol='crop') -icon_transformation_create = Icon( - driver_name='fontawesome-dual', primary_symbol='crop', - secondary_symbol='plus' -) icon_transformation_delete = Icon(driver_name='fontawesome', symbol='times') icon_transformation_edit = Icon(driver_name='fontawesome', symbol='pencil-alt') icon_transformation_list = icon_transformations +icon_transformation_select = Icon(driver_name='fontawesome', symbol='plus') diff --git a/mayan/apps/converter/layers.py b/mayan/apps/converter/layers.py new file mode 100644 index 0000000000..892b00c15c --- /dev/null +++ b/mayan/apps/converter/layers.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from .classes import Layer +from .permissions import ( + permission_transformation_create, permission_transformation_delete, + permission_transformation_edit, permission_transformation_view +) + +layer_saved_transformations = Layer( + default=True, label=_('Saved transformations'), + name='saved_transformations', order=100, permissions={ + 'create': permission_transformation_create, + 'delete': permission_transformation_delete, + 'edit': permission_transformation_edit, + 'select': permission_transformation_create, + 'view': permission_transformation_view, + }, symbol='crop' +) diff --git a/mayan/apps/converter/links.py b/mayan/apps/converter/links.py index b69074fd9f..f325dd269b 100644 --- a/mayan/apps/converter/links.py +++ b/mayan/apps/converter/links.py @@ -1,55 +1,37 @@ from __future__ import unicode_literals -from django.apps import apps from django.utils.translation import ugettext_lazy as _ -from mayan.apps.navigation.classes import Link - -from .permissions import ( - permission_transformation_create, permission_transformation_delete, - permission_transformation_edit, permission_transformation_view -) +from .classes import LayerLink +from .layers import layer_saved_transformations -def get_kwargs_factory(variable_name): - def get_kwargs(context): - ContentType = apps.get_model( - app_label='contenttypes', model_name='ContentType' - ) - - content_type = ContentType.objects.get_for_model( - context[variable_name] - ) - return { - 'app_label': '"{}"'.format(content_type.app_label), - 'model': '"{}"'.format(content_type.model), - 'object_id': '{}.pk'.format(variable_name) - } - - return get_kwargs +def conditional_active(context, resolved_link): + return resolved_link.link.view == resolved_link.current_view_name and context.get('layer_name', None) == resolved_link.link.layer_name -link_transformation_create = Link( - icon_class_path='mayan.apps.converter.icons.icon_transformation_create', - kwargs=get_kwargs_factory('content_object'), - permissions=(permission_transformation_create,), - text=_('Create new transformation'), view='converter:transformation_create' -) -link_transformation_delete = Link( - args='resolved_object.pk', +link_transformation_delete = LayerLink( + action='delete', + kwargs={'layer_name': 'layer_name', 'pk': 'resolved_object.pk'}, icon_class_path='mayan.apps.converter.icons.icon_transformation_delete', - permissions=(permission_transformation_delete,), + layer=layer_saved_transformations, tags='dangerous', text=_('Delete'), view='converter:transformation_delete' ) -link_transformation_edit = Link( - args='resolved_object.pk', +link_transformation_edit = LayerLink( + action='edit', + kwargs={'layer_name': 'layer_name', 'pk': 'resolved_object.pk'}, icon_class_path='mayan.apps.converter.icons.icon_transformation_edit', - permissions=(permission_transformation_edit,), + layer=layer_saved_transformations, text=_('Edit'), view='converter:transformation_edit' ) -link_transformation_list = Link( - icon_class_path='mayan.apps.converter.icons.icon_transformation_list', - kwargs=get_kwargs_factory('resolved_object'), - permissions=(permission_transformation_view,), text=_('Transformations'), +link_transformation_list = LayerLink( + action='list', conditional_active=conditional_active, + layer=layer_saved_transformations, text=_('Transformations'), view='converter:transformation_list' ) +link_transformation_select = LayerLink( + action='select', + icon_class_path='mayan.apps.converter.icons.icon_transformation_select', + layer=layer_saved_transformations, text=_('Select new transformation'), + view='converter:transformation_select' +) diff --git a/mayan/apps/converter/managers.py b/mayan/apps/converter/managers.py index 5856231203..e0fd3fa65c 100644 --- a/mayan/apps/converter/managers.py +++ b/mayan/apps/converter/managers.py @@ -2,80 +2,89 @@ 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.apps import apps from django.contrib.contenttypes.models import ContentType -from django.db import models, transaction +from django.core.exceptions import PermissionDenied +from django.db import models +from mayan.apps.acls.models import AccessControlList +from mayan.apps.common.serialization import yaml_load + +from .classes import Layer from .transformations import BaseTransformation logger = logging.getLogger(__name__) -class TransformationManager(models.Manager): - def add_to_object(self, obj, transformation, arguments=None): - content_type = ContentType.objects.get_for_model(model=obj) - - self.create( - content_type=content_type, object_id=obj.pk, - name=transformation.name, arguments=yaml.dump( - data=arguments, Dumper=SafeDumper - ) - ) - - def copy(self, source, targets): - """ - Copy transformation from source to all targets - """ - content_type = ContentType.objects.get_for_model(model=source) - - # Get transformations - transformations = self.filter( - content_type=content_type, object_id=source.pk - ).values('name', 'arguments', 'order') - logger.debug('source transformations: %s', transformations) - - # Get all targets from target QS - targets_dict = map( - lambda entry: { - 'content_type': entry[0], 'object_id': entry[1] - }, zip( - ContentType.objects.get_for_models(models=targets).values(), - targets.values_list('pk', flat=True) - ) - ) - logger.debug('targets: %s', targets_dict) - - # Combine the two - results = [] - for instance in targets_dict: - for transformation in transformations: - result = instance.copy() - result.update(transformation) - results.append(dict(result)) - - logger.debug('results: %s', results) - - # Bulk create for a single DB query - with transaction.atomic(): - self.bulk_create( - map(lambda entry: self.model(**entry), results), - ) - - def get_for_object(self, obj, as_classes=False): +class LayerTransformationManager(models.Manager): + def get_for_object( + self, obj, as_classes=False, maximum_layer_order=None, + only_stored_layer=None, user=None + ): """ as_classes == True returns the transformation classes from .classes ready to be feed to the converter class """ + Layer.update() + + StoredLayer = apps.get_model( + app_label='converter', model_name='StoredLayer' + ) content_type = ContentType.objects.get_for_model(model=obj) transformations = self.filter( - content_type=content_type, object_id=obj.pk + enabled=True, object_layer__content_type=content_type, + object_layer__object_id=obj.pk, object_layer__enabled=True + ) + + access_layers = StoredLayer.objects.all() + exclude_layers = StoredLayer.objects.none() + + if maximum_layer_order: + access_layers = StoredLayer.objects.filter( + order__lte=maximum_layer_order + ) + exclude_layers = StoredLayer.objects.filter( + order__gt=maximum_layer_order + ) + + for stored_layer in access_layers: + access_permission = stored_layer.get_layer().permissions.get( + 'access_permission', None + ) + if access_permission: + try: + AccessControlList.objects.check_access( + obj=obj, permissions=(access_permission,), user=user + ) + except PermissionDenied: + access_layers = access_layers.exclude(pk=stored_layer.pk) + + for stored_layer in exclude_layers: + exclude_permission = stored_layer.get_layer().permissions.get( + 'exclude_permission', None + ) + if exclude_permission: + try: + AccessControlList.objects.check_access( + obj=obj, permissions=(exclude_permission,), user=user + ) + except PermissionDenied: + pass + else: + exclude_layers = exclude_layers.exclude(pk=stored_layer.pk) + + if only_stored_layer: + transformations = transformations.filter( + object_layer__stored_layer=only_stored_layer + ) + + transformations = transformations.filter( + object_layer__stored_layer__in=access_layers + ) + + transformations = transformations.exclude( + object_layer__stored_layer__in=exclude_layers ) if as_classes: @@ -96,9 +105,8 @@ class TransformationManager(models.Manager): # Some transformations don't require arguments # return an empty dictionary as ** doesn't allow None if transformation.arguments: - kwargs = yaml.load( + kwargs = yaml_load( stream=transformation.arguments, - Loader=SafeLoader ) else: kwargs = {} diff --git a/mayan/apps/converter/migrations/0014_auto_20190814_0013.py b/mayan/apps/converter/migrations/0014_auto_20190814_0013.py new file mode 100644 index 0000000000..a1221ebf4e --- /dev/null +++ b/mayan/apps/converter/migrations/0014_auto_20190814_0013.py @@ -0,0 +1,81 @@ +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import mayan.apps.converter.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('converter', '0013_auto_20180823_2353'), + ] + + operations = [ + migrations.CreateModel( + name='LayerTransformation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.PositiveIntegerField(blank=True, db_index=True, default=0, help_text='Order in which the transformations will be executed. If left unchanged, an automatic order value will be assigned.', verbose_name='Order')), + ('name', models.CharField(choices=[('crop', 'Crop: left, top, right, bottom'), ('draw_rectangle', 'Draw rectangle: left, top, right, bottom, fillcolor, outlinecolor, outlinewidth'), ('draw_rectangle_percent', 'Draw rectangle (percents coordinates): left, top, right, bottom, fillcolor, outlinecolor, outlinewidth'), ('flip', 'Flip'), ('gaussianblur', 'Gaussian blur: radius'), ('lineart', 'Line art'), ('mirror', 'Mirror'), ('redaction_percent', 'Redaction: left, top, right, bottom'), ('resize', 'Resize: width, height'), ('rotate', 'Rotate: degrees, fillcolor'), ('rotate180', 'Rotate 180 degrees'), ('rotate270', 'Rotate 270 degrees'), ('rotate90', 'Rotate 90 degrees'), ('unsharpmask', 'Unsharp masking: radius, percent, threshold'), ('zoom', 'Zoom: percent')], max_length=128, verbose_name='Name')), + ('arguments', models.TextField(blank=True, help_text='Enter the arguments for the transformation as a YAML dictionary. ie: {"degrees": 180}', validators=[mayan.apps.converter.validators.YAMLValidator()], verbose_name='Arguments')), + ('enabled', models.BooleanField(default=True, verbose_name='Enabled')), + ], + options={ + 'ordering': ('object_layer__stored_layer__order', 'order'), + 'verbose_name': 'Layer transformation', + 'verbose_name_plural': 'Layer transformations', + }, + ), + migrations.CreateModel( + name='ObjectLayer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('enabled', models.BooleanField(default=True, verbose_name='Enabled')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + options={ + 'ordering': ('stored_layer__order',), + 'verbose_name': 'Object layer', + 'verbose_name_plural': 'Object layers', + }, + ), + migrations.CreateModel( + name='StoredLayer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=64, unique=True, verbose_name='Name')), + ('order', models.PositiveIntegerField(db_index=True, unique=True, verbose_name='Order')), + ], + options={ + 'ordering': ('order',), + 'verbose_name': 'Stored layer', + 'verbose_name_plural': 'Stored layers', + }, + ), + migrations.AlterField( + model_name='transformation', + name='name', + field=models.CharField(choices=[('crop', 'Crop: left, top, right, bottom'), ('draw_rectangle', 'Draw rectangle: left, top, right, bottom, fillcolor, outlinecolor, outlinewidth'), ('draw_rectangle_percent', 'Draw rectangle (percents coordinates): left, top, right, bottom, fillcolor, outlinecolor, outlinewidth'), ('flip', 'Flip'), ('gaussianblur', 'Gaussian blur: radius'), ('lineart', 'Line art'), ('mirror', 'Mirror'), ('redaction_percent', 'Redaction: left, top, right, bottom'), ('resize', 'Resize: width, height'), ('rotate', 'Rotate: degrees, fillcolor'), ('rotate180', 'Rotate 180 degrees'), ('rotate270', 'Rotate 270 degrees'), ('rotate90', 'Rotate 90 degrees'), ('unsharpmask', 'Unsharp masking: radius, percent, threshold'), ('zoom', 'Zoom: percent')], max_length=128, verbose_name='Name'), + ), + migrations.AddField( + model_name='objectlayer', + name='stored_layer', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='object_layers', to='converter.StoredLayer', verbose_name='Stored layer'), + ), + migrations.AddField( + model_name='layertransformation', + name='object_layer', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transformations', to='converter.ObjectLayer', verbose_name='Object layer'), + ), + migrations.AlterUniqueTogether( + name='objectlayer', + unique_together=set([('content_type', 'object_id', 'stored_layer')]), + ), + migrations.AlterUniqueTogether( + name='layertransformation', + unique_together=set([('object_layer', 'order')]), + ), + ] diff --git a/mayan/apps/converter/migrations/0015_auto_20190814_0014.py b/mayan/apps/converter/migrations/0015_auto_20190814_0014.py new file mode 100644 index 0000000000..4704120e34 --- /dev/null +++ b/mayan/apps/converter/migrations/0015_auto_20190814_0014.py @@ -0,0 +1,76 @@ +from __future__ import unicode_literals + +from django.db import migrations + +from ..layers import layer_saved_transformations + + +def code_copy_transformations(apps, schema_editor): + ObjectLayer = apps.get_model( + app_label='converter', model_name='ObjectLayer' + ) + StoredLayer = apps.get_model( + app_label='converter', model_name='StoredLayer' + ) + Transformation = apps.get_model( + app_label='converter', model_name='Transformation' + ) + + stored_layer, created = StoredLayer.objects.using(schema_editor.connection.alias).update_or_create( + name=layer_saved_transformations.name, defaults={'order': layer_saved_transformations.order} + ) + + for transformation in Transformation.objects.using(schema_editor.connection.alias).all(): + object_layer, created = ObjectLayer.objects.get_or_create( + content_type=transformation.content_type, + object_id=transformation.object_id, + stored_layer=stored_layer + ) + + object_layer.transformations.create( + order=transformation.order, name=transformation.name, + arguments=transformation.arguments + ) + + +def code_copy_transformations_reverse(apps, schema_editor): + LayerTransformation = apps.get_model( + app_label='converter', model_name='LayerTransformation' + ) + ObjectLayer = apps.get_model( + app_label='converter', model_name='ObjectLayer' + ) + StoredLayer = apps.get_model( + app_label='converter', model_name='StoredLayer' + ) + Transformation = apps.get_model( + app_label='converter', model_name='Transformation' + ) + + stored_layer, created = StoredLayer.objects.using(schema_editor.connection.alias).update_or_create( + name=layer_saved_transformations.name, defaults={'order': layer_saved_transformations.order} + ) + + for object_layer in ObjectLayer.objects.using(schema_editor.connection.alias).filter(stored_layer=stored_layer): + for layer_transformation in LayerTransformation.objects.using(schema_editor.connection.alias).filter(object_layer=object_layer): + Transformation.objects.using(schema_editor.connection.alias).create( + content_type=object_layer.content_type, + object_id=object_layer.object_id, + order=layer_transformation.order, + name=layer_transformation.name, + arguments=layer_transformation.arguments + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('converter', '0014_auto_20190814_0013'), + ] + + operations = [ + migrations.RunPython( + code=code_copy_transformations, + reverse_code=code_copy_transformations_reverse + ) + ] diff --git a/mayan/apps/converter/migrations/0016_auto_20190814_0510.py b/mayan/apps/converter/migrations/0016_auto_20190814_0510.py new file mode 100644 index 0000000000..aae3540b27 --- /dev/null +++ b/mayan/apps/converter/migrations/0016_auto_20190814_0510.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-08-14 05:10 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('converter', '0015_auto_20190814_0014'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='transformation', + unique_together=set([]), + ), + migrations.RemoveField( + model_name='transformation', + name='content_type', + ), + migrations.DeleteModel( + name='Transformation', + ), + ] diff --git a/mayan/apps/converter/models.py b/mayan/apps/converter/models.py index 73df0efade..bb263bc2d4 100644 --- a/mayan/apps/converter/models.py +++ b/mayan/apps/converter/models.py @@ -9,7 +9,8 @@ from django.db.models import Max from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ -from .managers import TransformationManager +from .classes import Layer +from .managers import LayerTransformationManager from .transformations import BaseTransformation from .validators import YAMLValidator @@ -17,7 +18,47 @@ logger = logging.getLogger(__name__) @python_2_unicode_compatible -class Transformation(models.Model): +class StoredLayer(models.Model): + name = models.CharField( + max_length=64, unique=True, verbose_name=_('Name') + ) + order = models.PositiveIntegerField( + db_index=True, unique=True, verbose_name=_('Order') + ) + + def __str__(self): + return self.name + + class Meta: + ordering = ('order',) + verbose_name = _('Stored layer') + verbose_name_plural = _('Stored layers') + + def get_layer(self): + return Layer.get(name=self.name) + + +class ObjectLayer(models.Model): + content_type = models.ForeignKey(on_delete=models.CASCADE, to=ContentType) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey( + ct_field='content_type', fk_field='object_id' + ) + enabled = models.BooleanField(default=True, verbose_name=_('Enabled')) + stored_layer = models.ForeignKey( + on_delete=models.CASCADE, related_name='object_layers', to=StoredLayer, + verbose_name=_('Stored layer') + ) + + class Meta: + ordering = ('stored_layer__order',) + unique_together = ('content_type', 'object_id', 'stored_layer') + verbose_name = _('Object layer') + verbose_name_plural = _('Object layers') + + +@python_2_unicode_compatible +class LayerTransformation(models.Model): """ Model that stores the transformation and transformation arguments for a given object @@ -29,9 +70,10 @@ class Transformation(models.Model): transformation argument. Example: if a page is rotated with the Rotation transformation, this field will show by how many degrees it was rotated. """ - content_type = models.ForeignKey(on_delete=models.CASCADE, to=ContentType) - object_id = models.PositiveIntegerField() - content_object = GenericForeignKey('content_type', 'object_id') + object_layer = models.ForeignKey( + on_delete=models.CASCADE, related_name='transformations', + to=ObjectLayer, verbose_name=_('Object layer') + ) order = models.PositiveIntegerField( blank=True, db_index=True, default=0, help_text=_( 'Order in which the transformations will be executed. If left ' @@ -48,23 +90,27 @@ class Transformation(models.Model): 'dictionary. ie: {"degrees": 180}' ), validators=[YAMLValidator()], verbose_name=_('Arguments') ) + enabled = models.BooleanField(default=True, verbose_name=_('Enabled')) - objects = TransformationManager() + objects = LayerTransformationManager() class Meta: - ordering = ('order',) - unique_together = ('content_type', 'object_id', 'order') - verbose_name = _('Transformation') - verbose_name_plural = _('Transformations') + ordering = ('object_layer__stored_layer__order', 'order',) + unique_together = ('object_layer', 'order') + verbose_name = _('Layer transformation') + verbose_name_plural = _('Layer transformations') def __str__(self): return self.get_name_display() + def get_transformation_class(self): + return BaseTransformation.get(name=self.name) + def save(self, *args, **kwargs): if not self.order: - last_order = Transformation.objects.filter( - content_type=self.content_type, object_id=self.object_id + last_order = LayerTransformation.objects.filter( + object_layer=self.object_layer ).aggregate(Max('order'))['order__max'] if last_order is not None: self.order = last_order + 1 - super(Transformation, self).save(*args, **kwargs) + super(LayerTransformation, self).save(*args, **kwargs) diff --git a/mayan/apps/converter/settings.py b/mayan/apps/converter/settings.py index b680fa6d47..0d30648fc2 100644 --- a/mayan/apps/converter/settings.py +++ b/mayan/apps/converter/settings.py @@ -16,22 +16,15 @@ setting_graphics_backend = namespace.add_setting( help_text=_('Graphics conversion backend to use.'), global_name='CONVERTER_GRAPHICS_BACKEND', ) -setting_graphics_backend_config = namespace.add_setting( - default=''' - {{ - libreoffice_path: {}, - pdftoppm_dpi: {}, - pdftoppm_format: {}, - pdftoppm_path: {}, - pdfinfo_path: {}, - pillow_format: {} - - }} - '''.replace('\n', '').format( - DEFAULT_LIBREOFFICE_PATH, DEFAULT_PDFTOPPM_DPI, - DEFAULT_PDFTOPPM_FORMAT, DEFAULT_PDFTOPPM_PATH, DEFAULT_PDFINFO_PATH, - DEFAULT_PILLOW_FORMAT - ), help_text=_( +setting_graphics_backend_arguments = namespace.add_setting( + default={ + 'libreoffice_path': DEFAULT_LIBREOFFICE_PATH, + 'pdftoppm_dpi': DEFAULT_PDFTOPPM_DPI, + 'pdftoppm_format': DEFAULT_PDFTOPPM_FORMAT, + 'pdftoppm_path': DEFAULT_PDFTOPPM_PATH, + 'pdfinfo_path': DEFAULT_PDFINFO_PATH, + 'pillow_format': DEFAULT_PILLOW_FORMAT, + }, help_text=_( 'Configuration options for the graphics conversion backend.' - ), global_name='CONVERTER_GRAPHICS_BACKEND_CONFIG', quoted=True + ), global_name='CONVERTER_GRAPHICS_BACKEND_ARGUMENTS' ) diff --git a/mayan/apps/converter/tests/literals.py b/mayan/apps/converter/tests/literals.py index fbad176ee9..e40d463e40 100644 --- a/mayan/apps/converter/tests/literals.py +++ b/mayan/apps/converter/tests/literals.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals TEST_TRANSFORMATION_NAME = 'rotate' TEST_TRANSFORMATION_ARGUMENT = 'degrees: 180' +TEST_TRANSFORMATION_ARGUMENT_EDITED = 'degrees: 270' TEST_TRANSFORMATION_COMBINED_CACHE_HASH = '384bf78014d2aed7255d9e548a0694c70af0b22545653214bcceb1ac6286b5f7' TEST_TRANSFORMATION_RESIZE_CACHE_HASH = b'4aa319f5a6950985a19380a1f279a66769d04138bd1583844270fe8c269260fc' TEST_TRANSFORMATION_RESIZE_CACHE_HASH_2 = b'cc8d220d40e810b995181c0c69b44b7a61c3bb039c0be96a5465fcaf698ca99a' diff --git a/mayan/apps/converter/tests/mixins.py b/mayan/apps/converter/tests/mixins.py new file mode 100644 index 0000000000..b5a914a232 --- /dev/null +++ b/mayan/apps/converter/tests/mixins.py @@ -0,0 +1,97 @@ +from __future__ import unicode_literals + +from django.contrib.contenttypes.models import ContentType + +from mayan.apps.acls.classes import ModelPermission +from mayan.apps.permissions.tests.mixins import PermissionTestMixin + +from ..classes import Layer +from ..models import ObjectLayer + +from .literals import ( + TEST_TRANSFORMATION_NAME, TEST_TRANSFORMATION_ARGUMENT, + TEST_TRANSFORMATION_ARGUMENT_EDITED +) + + +class LayerTestMixin(PermissionTestMixin): + test_layer = Layer( + label='Test layer', name='test_layer', order=1000, + permissions={} + ) + + def setUp(self): + super(LayerTestMixin, self).setUp() + self._create_test_permission() + + self.test_layer_permission = self.test_permission + ModelPermission.register( + model=self.test_document._meta.model, permissions=( + self.test_permission, + ) + ) + + self.test_layer.permissions = { + 'create': self.test_layer_permission, + 'delete': self.test_layer_permission, + 'edit': self.test_layer_permission, + 'select': self.test_layer_permission, + 'view': self.test_layer_permission, + } + Layer.invalidate_cache() + Layer.update() + + +class TransformationTestMixin(LayerTestMixin): + def _create_test_transformation(self): + content_type = ContentType.objects.get_for_model(model=self.test_document) + object_layer, created = ObjectLayer.objects.get_or_create( + content_type=content_type, object_id=self.test_document.pk, + stored_layer=self.test_layer.stored_layer + ) + + self.test_transformation = object_layer.transformations.create( + name=TEST_TRANSFORMATION_NAME, + arguments=TEST_TRANSFORMATION_ARGUMENT + ) + + +class TransformationViewsTestMixin(object): + def _request_transformation_create_view(self): + return self.post( + viewname='converter:transformation_create', kwargs={ + 'app_label': 'documents', 'model': 'document', + 'object_id': self.test_document.pk, + 'layer_name': self.test_layer.name, + 'transformation_name': TEST_TRANSFORMATION_NAME, + }, data={ + 'arguments': TEST_TRANSFORMATION_ARGUMENT + } + ) + + def _request_transformation_delete_view(self): + return self.post( + viewname='converter:transformation_delete', kwargs={ + 'layer_name': self.test_layer.name, + 'pk': self.test_transformation.pk + } + ) + + def _request_transformation_edit_view(self): + return self.post( + viewname='converter:transformation_edit', kwargs={ + 'layer_name': self.test_layer.name, + 'pk': self.test_transformation.pk + }, data={ + 'arguments': TEST_TRANSFORMATION_ARGUMENT_EDITED + } + ) + + def _request_transformation_list_view(self): + return self.get( + viewname='converter:transformation_list', kwargs={ + 'app_label': 'documents', 'model': 'document', + 'object_id': self.test_document.pk, + 'layer_name': self.test_layer.name + } + ) diff --git a/mayan/apps/converter/tests/test_transformations.py b/mayan/apps/converter/tests/test_transformations.py index c596abfa27..80c5b49509 100644 --- a/mayan/apps/converter/tests/test_transformations.py +++ b/mayan/apps/converter/tests/test_transformations.py @@ -2,9 +2,8 @@ from __future__ import unicode_literals from django.test import TestCase -from mayan.apps.documents.tests import GenericDocumentTestCase +from mayan.apps.documents.tests.base import GenericDocumentTestCase -from ..models import Transformation from ..transformations import ( BaseTransformation, TransformationCrop, TransformationLineArt, TransformationResize, TransformationRotate, TransformationRotate90, @@ -24,6 +23,7 @@ from .literals import ( TEST_TRANSFORMATION_ZOOM_CACHE_HASH, TEST_TRANSFORMATION_ZOOM_PERCENT, ) +from .mixins import LayerTestMixin class TransformationBaseTestCase(TestCase): @@ -110,90 +110,89 @@ class TransformationBaseTestCase(TestCase): ) -class TransformationTestCase(GenericDocumentTestCase): +class TransformationTestCase(LayerTestMixin, GenericDocumentTestCase): def test_crop_transformation_optional_arguments(self): self._silence_logger(name='mayan.apps.converter.managers') document_page = self.test_document.pages.first() - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationCrop, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationCrop, arguments={'top': '10'} ) - self.assertTrue(document_page.generate_image().startswith('page')) + self.assertTrue(document_page.generate_image()) def test_crop_transformation_invalid_arguments(self): self._silence_logger(name='mayan.apps.converter.managers') document_page = self.test_document.pages.first() - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationCrop, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationCrop, arguments={'top': 'x', 'left': '-'} ) - - self.assertTrue(document_page.generate_image().startswith('page')) + self.assertTrue(document_page.generate_image()) def test_crop_transformation_non_valid_range_arguments(self): self._silence_logger(name='mayan.apps.converter.managers') document_page = self.test_document.pages.first() - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationCrop, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationCrop, arguments={'top': '-1000', 'bottom': '100000000'} ) - self.assertTrue(document_page.generate_image().startswith('page')) + self.assertTrue(document_page.generate_image()) def test_crop_transformation_overlapping_ranges_arguments(self): self._silence_logger(name='mayan.apps.converter.managers') document_page = self.test_document.pages.first() - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationCrop, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationCrop, arguments={'top': '1000', 'bottom': '1000'} ) - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationCrop, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationCrop, arguments={'left': '1000', 'right': '10000'} ) - self.assertTrue(document_page.generate_image().startswith('page')) + self.assertTrue(document_page.generate_image()) def test_lineart_transformations(self): document_page = self.test_document.pages.first() - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationLineArt, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationLineArt, arguments={} ) - self.assertTrue(document_page.generate_image().startswith('page')) + self.assertTrue(document_page.generate_image()) def test_rotate_transformations(self): document_page = self.test_document.pages.first() - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationRotate90, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationRotate90, arguments={} ) - self.assertTrue(document_page.generate_image().startswith('page')) + self.assertTrue(document_page.generate_image()) - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationRotate180, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationRotate180, arguments={} ) - self.assertTrue(document_page.generate_image().startswith('page')) + self.assertTrue(document_page.generate_image()) - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationRotate270, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationRotate270, arguments={} ) - self.assertTrue(document_page.generate_image().startswith('page')) + self.assertTrue(document_page.generate_image()) diff --git a/mayan/apps/converter/tests/test_views.py b/mayan/apps/converter/tests/test_views.py index faf33c8259..c07152b47e 100644 --- a/mayan/apps/converter/tests/test_views.py +++ b/mayan/apps/converter/tests/test_views.py @@ -1,110 +1,123 @@ from __future__ import unicode_literals -from django.contrib.contenttypes.models import ContentType +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from ..models import LayerTransformation -from ..models import Transformation -from ..permissions import ( - permission_transformation_create, permission_transformation_delete, - permission_transformation_view -) - -from .literals import TEST_TRANSFORMATION_NAME, TEST_TRANSFORMATION_ARGUMENT +from .mixins import TransformationTestMixin, TransformationViewsTestMixin -class TransformationViewsTestCase(GenericDocumentViewTestCase): - def _transformation_create_view(self): - return self.post( - viewname='converter:transformation_create', kwargs={ - 'app_label': 'documents', 'model': 'document', - 'object_id': self.test_document.pk - }, data={ - 'name': TEST_TRANSFORMATION_NAME, - 'arguments': TEST_TRANSFORMATION_ARGUMENT - } - ) +class TransformationViewsTestCase( + TransformationTestMixin, TransformationViewsTestMixin, + GenericDocumentViewTestCase +): + def test_transformation_create_view_no_permission(self): + transformation_count = LayerTransformation.objects.count() - def test_transformation_create_view_no_permissions(self): - transformation_count = Transformation.objects.count() - - response = self._transformation_create_view() - self.assertEqual(response.status_code, 403) - - self.assertEqual(Transformation.objects.count(), transformation_count) - - def test_transformation_create_view_with_permissions(self): - self.grant_permission(permission=permission_transformation_create) - - transformation_count = Transformation.objects.count() - - response = self._transformation_create_view() - self.assertEqual(response.status_code, 302) + response = self._request_transformation_create_view() + self.assertEqual(response.status_code, 404) self.assertEqual( - Transformation.objects.count(), transformation_count + 1 + LayerTransformation.objects.count(), transformation_count ) - def _request_transformation_delete_view(self): - return self.post( - viewname='converter:transformation_delete', kwargs={ - 'pk': self.test_transformation.pk - } - ) - - def _create_test_transformation(self): - content_type = ContentType.objects.get_for_model(model=self.test_document) - - self.test_transformation = Transformation.objects.create( - content_type=content_type, object_id=self.test_document.pk, - name=TEST_TRANSFORMATION_NAME, - arguments=TEST_TRANSFORMATION_ARGUMENT - ) - - def test_transformation_delete_view_no_permissions(self): - self._create_test_transformation() - - transformation_count = Transformation.objects.count() - - response = self._request_transformation_delete_view() - self.assertEqual(response.status_code, 403) - - self.assertEqual( - Transformation.objects.count(), transformation_count - ) - - def test_transformation_delete_view_with_permissions(self): - self._create_test_transformation() - - self.grant_permission(permission=permission_transformation_delete) - - transformation_count = Transformation.objects.count() - - response = self._request_transformation_delete_view() - self.assertEqual(response.status_code, 302) - - self.assertEqual( - Transformation.objects.count(), transformation_count - 1 - ) - - def _transformation_list_view(self): - return self.get( - viewname='converter:transformation_list', kwargs={ - 'app_label': 'documents', 'model': 'document', - 'object_id': self.test_document.pk - } - ) - - def test_transformation_list_view_no_permissions(self): - response = self._transformation_list_view() - self.assertEqual(response.status_code, 403) - - def test_transformation_list_view_with_permissions(self): + def test_transformation_create_view_with_permission(self): self.grant_access( - obj=self.test_document, permission=permission_transformation_view + obj=self.test_document, permission=self.test_permission ) - response = self._transformation_list_view() + transformation_count = LayerTransformation.objects.count() + + response = self._request_transformation_create_view() + self.assertEqual(response.status_code, 302) + + self.assertEqual( + LayerTransformation.objects.count(), transformation_count + 1 + ) + + def test_transformation_delete_view_no_permission(self): + self._create_test_transformation() + + transformation_count = LayerTransformation.objects.count() + + response = self._request_transformation_delete_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual( + LayerTransformation.objects.count(), transformation_count + ) + + def test_transformation_delete_view_with_access(self): + self._create_test_transformation() + + self.grant_access( + obj=self.test_document, permission=self.test_layer_permission + ) + + transformation_count = LayerTransformation.objects.count() + + response = self._request_transformation_delete_view() + self.assertEqual(response.status_code, 302) + + self.assertEqual( + LayerTransformation.objects.count(), transformation_count - 1 + ) + + def test_transformation_edit_view_no_permission(self): + self._create_test_transformation() + + transformation_arguments = self.test_transformation.arguments + + response = self._request_transformation_edit_view() + self.assertEqual(response.status_code, 404) + + self.test_transformation.refresh_from_db() + self.assertEqual( + transformation_arguments, self.test_transformation.arguments + ) + + def test_transformation_edit_view_with_access(self): + self._create_test_transformation() + + self.grant_access( + obj=self.test_document, permission=self.test_layer_permission + ) + + transformation_arguments = self.test_transformation.arguments + response = self._request_transformation_edit_view() + self.assertEqual(response.status_code, 302) + + self.test_transformation.refresh_from_db() + self.assertNotEqual( + transformation_arguments, self.test_transformation.arguments + ) + + def test_transformation_list_view_no_permission(self): + self._create_test_transformation() + + response = self._request_transformation_list_view() + self.assertNotContains( + response=response, text=self.test_document.label, status_code=404 + ) + self.assertNotContains( + response=response, + text=self.test_transformation.get_transformation_class().label, + status_code=404 + ) + + def test_transformation_list_view_with_access(self): + self._create_test_transformation() + + self.grant_access( + obj=self.test_document, permission=self.test_permission + ) + + response = self._request_transformation_list_view() self.assertContains( response=response, text=self.test_document.label, status_code=200 ) + self.assertContains( + response=response, + text=self.test_transformation.get_transformation_class().label, + status_code=200 + ) diff --git a/mayan/apps/converter/transformations.py b/mayan/apps/converter/transformations.py index fd3e822d92..4c02599615 100644 --- a/mayan/apps/converter/transformations.py +++ b/mayan/apps/converter/transformations.py @@ -5,12 +5,19 @@ import logging from PIL import Image, ImageColor, ImageDraw, ImageFilter +from django.utils.encoding import force_bytes, force_text from django.utils.translation import string_concat, ugettext_lazy as _ -from django.utils.encoding import force_bytes + +from .layers import layer_saved_transformations logger = logging.getLogger(__name__) +class BaseTransformationType(type): + def __str__(self): + return force_text(self.label) + + class BaseTransformation(object): """ Transformation can modify the appearance of the document's page preview. @@ -18,7 +25,9 @@ class BaseTransformation(object): """ arguments = () name = 'base_transformation' + _layer_transformations = {} _registry = {} + __metaclass__ = BaseTransformationType @staticmethod def combine(transformations): @@ -44,16 +53,25 @@ class BaseTransformation(object): return cls.label @classmethod - def get_transformation_choices(cls): + def get_transformation_choices(cls, layer=None): + if layer: + transformation_list = [ + (transformation.name, transformation) for transformation in cls._layer_transformations[layer] + ] + else: + transformation_list = cls._registry.items() + return sorted( [ - (name, klass.get_label()) for name, klass in cls._registry.items() + (name, klass.get_label()) for name, klass in transformation_list ] ) @classmethod - def register(cls, transformation): + def register(cls, layer, transformation): cls._registry[transformation.name] = transformation + cls._layer_transformations.setdefault(layer, []) + cls._layer_transformations[layer].append(transformation) def __init__(self, **kwargs): self.kwargs = {} @@ -308,12 +326,6 @@ class TransformationDrawRectanglePercent(BaseTransformation): if bottom > 100: bottom = 100 - #if left > right: - # left, right = right, left - - #if top > bottom: - # top, bottom = bottom, top - logger.debug( 'left: %f, top: %f, right: %f, bottom: %f', left, top, right, bottom @@ -523,17 +535,19 @@ class TransformationZoom(BaseTransformation): ) -BaseTransformation.register(transformation=TransformationCrop) -BaseTransformation.register(transformation=TransformationDrawRectangle) -BaseTransformation.register(transformation=TransformationDrawRectanglePercent) -BaseTransformation.register(transformation=TransformationFlip) -BaseTransformation.register(transformation=TransformationGaussianBlur) -BaseTransformation.register(transformation=TransformationLineArt) -BaseTransformation.register(transformation=TransformationMirror) -BaseTransformation.register(transformation=TransformationResize) -BaseTransformation.register(transformation=TransformationRotate) -BaseTransformation.register(transformation=TransformationRotate90) -BaseTransformation.register(transformation=TransformationRotate180) -BaseTransformation.register(transformation=TransformationRotate270) -BaseTransformation.register(transformation=TransformationUnsharpMask) -BaseTransformation.register(transformation=TransformationZoom) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationCrop) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationDrawRectangle) +BaseTransformation.register( + layer=layer_saved_transformations, transformation=TransformationDrawRectanglePercent +) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationFlip) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationGaussianBlur) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationLineArt) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationMirror) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationResize) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationRotate) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationRotate90) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationRotate180) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationRotate270) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationUnsharpMask) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationZoom) diff --git a/mayan/apps/converter/urls.py b/mayan/apps/converter/urls.py index a43042decd..8a0cf36167 100644 --- a/mayan/apps/converter/urls.py +++ b/mayan/apps/converter/urls.py @@ -3,25 +3,29 @@ from __future__ import unicode_literals from django.conf.urls import url from .views import ( - TransformationCreateView, TransformationDeleteView, TransformationEditView, - TransformationListView + TransformationCreateView, TransformationDeleteView, + TransformationEditView, TransformationListView, TransformationSelectView ) urlpatterns = [ url( - regex=r'^create_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', - view=TransformationCreateView.as_view(), name='transformation_create' - ), - url( - regex=r'^list_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', + regex=r'^object/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/layers/(?P[-_\w]+)/transformations/$', view=TransformationListView.as_view(), name='transformation_list' ), url( - regex=r'^delete/(?P\d+)/$', view=TransformationDeleteView.as_view(), - name='transformation_delete' + regex=r'^object/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/layers/(?P[-_\w]+)/transformations/select/$', + view=TransformationSelectView.as_view(), name='transformation_select' ), url( - regex=r'^edit/(?P\d+)/$', view=TransformationEditView.as_view(), - name='transformation_edit' + regex=r'^object/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/layers/(?P[-_\w]+)/transformations/(?P[-_\w]+)/create/$', + view=TransformationCreateView.as_view(), name='transformation_create' + ), + url( + regex=r'^layers/(?P[-_\w]+)/transformations/(?P\d+)/delete/$', + view=TransformationDeleteView.as_view(), name='transformation_delete' + ), + url( + regex=r'^layers/(?P[-_\w]+)/transformations/(?P\d+)/edit/$', + view=TransformationEditView.as_view(), name='transformation_edit' ), ] diff --git a/mayan/apps/converter/validators.py b/mayan/apps/converter/validators.py index 49c45d0cae..dd8562a945 100644 --- a/mayan/apps/converter/validators.py +++ b/mayan/apps/converter/validators.py @@ -2,15 +2,12 @@ from __future__ import unicode_literals import yaml -try: - from yaml import CSafeLoader as SafeLoader -except ImportError: - from yaml import SafeLoader - from django.core.exceptions import ValidationError from django.utils.deconstruct import deconstructible from django.utils.translation import ugettext_lazy as _ +from mayan.apps.common.serialization import yaml_load + @deconstructible class YAMLValidator(object): @@ -20,7 +17,7 @@ class YAMLValidator(object): def __call__(self, value): value = value.strip() try: - yaml.load(stream=value, Loader=SafeLoader) + yaml_load(stream=value) except yaml.error.YAMLError: raise ValidationError( _('Enter a valid YAML value.'), diff --git a/mayan/apps/converter/views.py b/mayan/apps/converter/views.py index fe4c042069..3f7178ca5e 100644 --- a/mayan/apps/converter/views.py +++ b/mayan/apps/converter/views.py @@ -2,59 +2,56 @@ from __future__ import absolute_import, unicode_literals import logging -from django.contrib.contenttypes.models import ContentType -from django.http import Http404 -from django.shortcuts import get_object_or_404 +from django.http import HttpResponseRedirect from django.template import RequestContext from django.urls import reverse from django.utils.translation import ugettext_lazy as _ -from mayan.apps.acls.models import AccessControlList from mayan.apps.common.generics import ( - SingleObjectCreateView, SingleObjectDeleteView, SingleObjectEditView, - SingleObjectListView + FormView, SingleObjectCreateView, SingleObjectDeleteView, + SingleObjectEditView, SingleObjectListView ) +from mayan.apps.common.mixins import ExternalContentTypeObjectMixin -from .forms import TransformationForm -from .icons import icon_transformation_list -from .links import link_transformation_create -from .models import Transformation -from .permissions import ( - permission_transformation_create, permission_transformation_delete, - permission_transformation_edit, permission_transformation_view -) +from .classes import Layer +from .forms import LayerTransformationForm, LayerTransformationSelectForm +from .links import link_transformation_select +from .models import LayerTransformation, ObjectLayer +from .transformations import BaseTransformation logger = logging.getLogger(__name__) -class TransformationCreateView(SingleObjectCreateView): - form_class = TransformationForm - +class LayerViewMixin(object): def dispatch(self, request, *args, **kwargs): - content_type = get_object_or_404( - klass=ContentType, app_label=self.kwargs['app_label'], - model=self.kwargs['model'] + self.layer = self.get_layer() + return super(LayerViewMixin, self).dispatch( + request=request, *args, **kwargs ) - try: - self.content_object = content_type.get_object_for_this_type( - pk=self.kwargs['object_id'] - ) - except content_type.model_class().DoesNotExist: - raise Http404 - - AccessControlList.objects.check_access( - obj=self.content_object, - permissions=(permission_transformation_create,), user=request.user + def get_layer(self): + return Layer.get( + name=self.kwargs['layer_name'] ) - return super(TransformationCreateView, self).dispatch( - request, *args, **kwargs - ) + +class TransformationCreateView( + LayerViewMixin, ExternalContentTypeObjectMixin, SingleObjectCreateView +): + form_class = LayerTransformationForm def form_valid(self, form): + layer = self.layer + content_type = self.get_content_type() + object_layer, created = ObjectLayer.objects.get_or_create( + content_type=content_type, object_id=self.external_object.pk, + stored_layer=layer.stored_layer + ) + instance = form.save(commit=False) - instance.content_object = self.content_object + instance.content_object = self.external_object + instance.name = self.kwargs['transformation_name'] + instance.object_layer = object_layer try: instance.full_clean() instance.save() @@ -66,91 +63,101 @@ class TransformationCreateView(SingleObjectCreateView): def get_extra_context(self): return { - 'content_object': self.content_object, + 'content_object': self.external_object, + 'form_field_css_classes': 'hidden' if hasattr( + self.get_transformation_class(), 'template_name' + ) else '', + 'layer': self.layer, + 'layer_name': self.layer.name, 'navigation_object_list': ('content_object',), 'title': _( - 'Create new transformation for: %s' - ) % self.content_object, + 'Create layer "%(layer)s" transformation ' + '"%(transformation)s" for: %(object)s' + ) % { + 'layer': self.layer, + 'transformation': self.get_transformation_class(), + 'object': self.external_object, + } } + def get_form_extra_kwargs(self): + return { + 'transformation_name': self.kwargs['transformation_name'] + } + + def get_external_object_permission(self): + return self.layer.permissions.get('create', None) + def get_post_action_redirect(self): return reverse( viewname='converter:transformation_list', kwargs={ 'app_label': self.kwargs['app_label'], 'model': self.kwargs['model'], - 'object_id': self.kwargs['object_id'] + 'object_id': self.kwargs['object_id'], + 'layer_name': self.kwargs['layer_name'] } ) def get_queryset(self): - return Transformation.objects.get_for_object(obj=self.content_object) - - -class TransformationDeleteView(SingleObjectDeleteView): - model = Transformation - - def dispatch(self, request, *args, **kwargs): - self.transformation = get_object_or_404( - klass=Transformation, pk=self.kwargs['pk'] + return self.layer.get_transformations_for( + obj=self.content_object ) - AccessControlList.objects.check_access( - obj=self.transformation.content_object, - permissions=(permission_transformation_delete,), user=request.user - ) + def get_template_names(self): + return [ + getattr( + self.get_transformation_class(), 'template_name', + self.template_name + ) + ] - return super(TransformationDeleteView, self).dispatch( - request, *args, **kwargs - ) + def get_transformation_class(self): + return BaseTransformation.get(name=self.kwargs['transformation_name']) - def get_post_action_redirect(self): - return reverse( - viewname='converter:transformation_list', kwargs={ - 'app_label': self.transformation.content_type.app_label, - 'model': self.transformation.content_type.model, - 'object_id': self.transformation.object_id - } - ) + +class TransformationDeleteView(LayerViewMixin, SingleObjectDeleteView): + model = LayerTransformation def get_extra_context(self): return { - 'content_object': self.transformation.content_object, + 'content_object': self.object.object_layer.content_object, + 'layer_name': self.layer.name, 'navigation_object_list': ('content_object', 'transformation'), 'previous': reverse( viewname='converter:transformation_list', kwargs={ - 'app_label': self.transformation.content_type.app_label, - 'model': self.transformation.content_type.model, - 'object_id': self.transformation.object_id + 'app_label': self.object.object_layer.content_type.app_label, + 'model': self.object.object_layer.content_type.model, + 'object_id': self.object.object_layer.object_id, + 'layer_name': self.object.object_layer.stored_layer.name } ), 'title': _( 'Delete transformation "%(transformation)s" for: ' '%(content_object)s?' ) % { - 'transformation': self.transformation, - 'content_object': self.transformation.content_object + 'transformation': self.object, + 'content_object': self.object.object_layer.content_object }, - 'transformation': self.transformation, + 'transformation': self.object, } + def get_object_permission(self): + return self.layer.permissions.get('delete', None) -class TransformationEditView(SingleObjectEditView): - form_class = TransformationForm - model = Transformation - - def dispatch(self, request, *args, **kwargs): - self.transformation = get_object_or_404( - klass=Transformation, pk=self.kwargs['pk'] + def get_post_action_redirect(self): + return reverse( + viewname='converter:transformation_list', kwargs={ + 'app_label': self.object.object_layer.content_type.app_label, + 'model': self.object.object_layer.content_type.model, + 'object_id': self.object.object_layer.object_id, + 'layer_name': self.object.object_layer.stored_layer.name + } ) - AccessControlList.objects.check_access( - obj=self.transformation.content_object, - permissions=(permission_transformation_edit,), user=request.user - ) - return super(TransformationEditView, self).dispatch( - request, *args, **kwargs - ) +class TransformationEditView(LayerViewMixin, SingleObjectEditView): + form_class = LayerTransformationForm + model = LayerTransformation def form_valid(self, form): instance = form.save(commit=False) @@ -165,72 +172,121 @@ class TransformationEditView(SingleObjectEditView): def get_extra_context(self): return { - 'content_object': self.transformation.content_object, + 'content_object': self.object.object_layer.content_object, + 'form_field_css_classes': 'hidden' if hasattr( + self.object.get_transformation_class(), 'template_name' + ) else '', + 'layer': self.layer, + 'layer_name': self.layer.name, 'navigation_object_list': ('content_object', 'transformation'), 'title': _( - 'Edit transformation "%(transformation)s" for: %(content_object)s' + 'Edit transformation "%(transformation)s" ' + 'for: %(content_object)s' ) % { - 'transformation': self.transformation, - 'content_object': self.transformation.content_object + 'transformation': self.object, + 'content_object': self.object.object_layer.content_object }, - 'transformation': self.transformation, + 'transformation': self.object, } + def get_object_permission(self): + return self.layer.permissions.get('edit', None) + def get_post_action_redirect(self): return reverse( viewname='converter:transformation_list', kwargs={ - 'app_label': self.transformation.content_type.app_label, - 'model': self.transformation.content_type.model, - 'object_id': self.transformation.object_id + 'app_label': self.object.object_layer.content_type.app_label, + 'model': self.object.object_layer.content_type.model, + 'object_id': self.object.object_layer.object_id, + 'layer_name': self.object.object_layer.stored_layer.name } ) - -class TransformationListView(SingleObjectListView): - def dispatch(self, request, *args, **kwargs): - content_type = get_object_or_404( - klass=ContentType, app_label=self.kwargs['app_label'], - model=self.kwargs['model'] - ) - - try: - self.content_object = content_type.get_object_for_this_type( - pk=self.kwargs['object_id'] + def get_template_names(self): + return [ + getattr( + self.object.get_transformation_class(), 'template_name', + self.template_name ) - except content_type.model_class().DoesNotExist: - raise Http404 + ] - AccessControlList.objects.check_access( - obj=self.content_object, - permissions=(permission_transformation_view,), user=request.user - ) - return super(TransformationListView, self).dispatch( - request, *args, **kwargs +class TransformationListView( + LayerViewMixin, ExternalContentTypeObjectMixin, SingleObjectListView +): + def get_external_object_permission(self): + return self.layer.permissions.get('view', None) + + def get_extra_context(self): + return { + 'object': self.external_object, + 'hide_link': True, + 'hide_object': True, + 'layer_name': self.layer.name, + 'no_results_icon': self.layer.get_icon(), + 'no_results_main_link': link_transformation_select.resolve( + context=RequestContext( + request=self.request, dict_={ + 'resolved_object': self.external_object, + 'layer_name': self.kwargs['layer_name'], + } + ) + ), + 'no_results_text': self.layer.get_empty_results_text(), + 'no_results_title': _( + 'There are no entries for layer "%(layer_name)s"' + ) % {'layer_name': self.layer.label}, + 'title': _( + 'Layer "%(layer)s" transformations for: %(object)s' + ) % { + 'layer': self.layer, + 'object': self.external_object, + } + } + + def get_source_queryset(self): + return self.layer.get_transformations_for(obj=self.external_object) + + +class TransformationSelectView( + ExternalContentTypeObjectMixin, LayerViewMixin, FormView +): + form_class = LayerTransformationSelectForm + template_name = 'appearance/generic_form.html' + + def form_valid(self, form): + return HttpResponseRedirect( + redirect_to=reverse( + viewname='converter:transformation_create', + kwargs={ + 'app_label': self.kwargs['app_label'], + 'model': self.kwargs['model'], + 'object_id': self.kwargs['object_id'], + 'layer_name': self.kwargs['layer_name'], + 'transformation_name': form.cleaned_data[ + 'transformation' + ] + } + ) ) def get_extra_context(self): return { - 'content_object': self.content_object, - 'hide_link': True, - 'hide_object': True, + 'layer': self.layer, + 'layer_name': self.kwargs['layer_name'], 'navigation_object_list': ('content_object',), - 'no_results_icon': icon_transformation_list, - 'no_results_main_link': link_transformation_create.resolve( - context=RequestContext( - request=self.request, dict_={ - 'content_object': self.content_object - } - ) - ), - 'no_results_text': _( - 'Transformations allow changing the visual appearance ' - 'of documents without making permanent changes to the ' - 'document file themselves.' - ), - 'no_results_title': _('No transformations'), - 'title': _('Transformations for: %s') % self.content_object, + 'content_object': self.external_object, + 'submit_label': _('Select'), + 'title': _( + 'Select new layer "%(layer)s" transformation ' + 'for: %(object)s' + ) % { + 'layer': self.layer, + 'object': self.external_object, + } } - def get_source_queryset(self): - return Transformation.objects.get_for_object(obj=self.content_object) + def get_form_extra_kwargs(self): + return { + 'layer': self.layer + } diff --git a/mayan/apps/dashboards/classes.py b/mayan/apps/dashboards/classes.py index c1e4a6d478..e3869556cf 100644 --- a/mayan/apps/dashboards/classes.py +++ b/mayan/apps/dashboards/classes.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from django.contrib.humanize.templatetags.humanize import intcomma from django.template import loader @@ -85,7 +86,8 @@ class DashboardWidgetNumeric(BaseDashboardWidget): def get_context(self): return { - 'count': self.count, + 'count': intcomma(value=self.count), + 'count_raw': self.count, 'icon_class': self.icon_class, 'label': self.label, 'link': self.link, diff --git a/mayan/apps/dashboards/templates/dashboards/numeric_widget.html b/mayan/apps/dashboards/templates/dashboards/numeric_widget.html index 9a41cce301..4aaf8481dd 100644 --- a/mayan/apps/dashboards/templates/dashboards/numeric_widget.html +++ b/mayan/apps/dashboards/templates/dashboards/numeric_widget.html @@ -1,5 +1,7 @@ {% load i18n %} +{% load appearance_tags %} +
    @@ -9,7 +11,7 @@ {% elif icon_class %}
    - {{ icon_class.render }} + {% appearance_icon_render icon_class enable_shadow=True %}
    {% endif %}
    diff --git a/mayan/apps/dependencies/management/commands/installjavascript.py b/mayan/apps/dependencies/management/commands/installdependencies.py similarity index 100% rename from mayan/apps/dependencies/management/commands/installjavascript.py rename to mayan/apps/dependencies/management/commands/installdependencies.py diff --git a/mayan/apps/dependencies/tests/test_classes.py b/mayan/apps/dependencies/tests/test_classes.py index dbc9f6ff87..e786979f5d 100644 --- a/mayan/apps/dependencies/tests/test_classes.py +++ b/mayan/apps/dependencies/tests/test_classes.py @@ -3,7 +3,7 @@ from __future__ import print_function, unicode_literals from pathlib2 import Path import shutil -from mayan.apps.common.tests import BaseTestCase +from mayan.apps.common.tests.base import BaseTestCase from mayan.apps.storage.utils import mkdtemp from ..classes import Dependency, Provider diff --git a/mayan/apps/django_gpg/tests/test_api.py b/mayan/apps/django_gpg/tests/test_api.py index 1f976e1107..6e9ab76b8f 100644 --- a/mayan/apps/django_gpg/tests/test_api.py +++ b/mayan/apps/django_gpg/tests/test_api.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from rest_framework import status -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..models import Key from ..permissions import ( diff --git a/mayan/apps/django_gpg/tests/test_models.py b/mayan/apps/django_gpg/tests/test_models.py index 5b805e5468..3e03ab8ee8 100644 --- a/mayan/apps/django_gpg/tests/test_models.py +++ b/mayan/apps/django_gpg/tests/test_models.py @@ -7,7 +7,7 @@ import mock from django.utils.encoding import force_bytes -from mayan.apps.common.tests import BaseTestCase +from mayan.apps.common.tests.base import BaseTestCase from mayan.apps.storage.utils import TemporaryFile from ..exceptions import ( diff --git a/mayan/apps/django_gpg/tests/test_views.py b/mayan/apps/django_gpg/tests/test_views.py index 5bc46a0b70..b0f05d88cb 100644 --- a/mayan/apps/django_gpg/tests/test_views.py +++ b/mayan/apps/django_gpg/tests/test_views.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals from django_downloadview.test import assert_download_response -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from ..models import Key from ..permissions import permission_key_download, permission_key_upload diff --git a/mayan/apps/django_gpg/urls.py b/mayan/apps/django_gpg/urls.py index a3f4ba10ad..d44b7e1bcb 100644 --- a/mayan/apps/django_gpg/urls.py +++ b/mayan/apps/django_gpg/urls.py @@ -11,35 +11,35 @@ from .views import ( urlpatterns = [ url( - regex=r'^(?P\d+)/$', view=KeyDetailView.as_view(), + regex=r'^keys/(?P\d+)/$', view=KeyDetailView.as_view(), name='key_detail' ), url( - regex=r'^(?P\d+)/delete/$', view=KeyDeleteView.as_view(), + regex=r'^keys/(?P\d+)/delete/$', view=KeyDeleteView.as_view(), name='key_delete' ), url( - regex=r'^(?P\d+)/download/$', view=KeyDownloadView.as_view(), + regex=r'^keys/(?P\d+)/download/$', view=KeyDownloadView.as_view(), name='key_download' ), url( - regex=r'^list/private/$', view=PrivateKeyListView.as_view(), + regex=r'^keys/private/$', view=PrivateKeyListView.as_view(), name='key_private_list' ), url( - regex=r'^list/public/$', view=PublicKeyListView.as_view(), + regex=r'^keys/public/$', view=PublicKeyListView.as_view(), name='key_public_list' ), url( - regex=r'^upload/$', view=KeyUploadView.as_view(), name='key_upload' + regex=r'^keys/upload/$', view=KeyUploadView.as_view(), name='key_upload' ), - url(regex=r'^query/$', view=KeyQueryView.as_view(), name='key_query'), + url(regex=r'^keys/query/$', view=KeyQueryView.as_view(), name='key_query'), url( - regex=r'^query/results/$', view=KeyQueryResultView.as_view(), + regex=r'^keys/query/results/$', view=KeyQueryResultView.as_view(), name='key_query_results' ), url( - regex=r'^receive/(?P.+)/$', view=KeyReceive.as_view(), + regex=r'^keys/receive/(?P.+)/$', view=KeyReceive.as_view(), name='key_receive' ), ] diff --git a/mayan/apps/document_comments/tests/test_api.py b/mayan/apps/document_comments/tests/test_api.py index 8ad608baa2..7b75aefcbb 100644 --- a/mayan/apps/document_comments/tests/test_api.py +++ b/mayan/apps/document_comments/tests/test_api.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals from rest_framework import status -from mayan.apps.documents.tests import DocumentTestMixin -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.documents.tests.mixins import DocumentTestMixin +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..models import Comment from ..permissions import ( diff --git a/mayan/apps/document_comments/tests/test_events.py b/mayan/apps/document_comments/tests/test_events.py index 5a4d4c942b..4d85152bfa 100644 --- a/mayan/apps/document_comments/tests/test_events.py +++ b/mayan/apps/document_comments/tests/test_events.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from actstream.models import Action -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from ..events import ( event_document_comment_created, event_document_comment_deleted, diff --git a/mayan/apps/document_comments/tests/test_views.py b/mayan/apps/document_comments/tests/test_views.py index 3351af99c2..1714ad4d9b 100644 --- a/mayan/apps/document_comments/tests/test_views.py +++ b/mayan/apps/document_comments/tests/test_views.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from ..models import Comment from ..permissions import ( diff --git a/mayan/apps/document_comments/urls.py b/mayan/apps/document_comments/urls.py index 3250e3ffa7..256776f8a5 100644 --- a/mayan/apps/document_comments/urls.py +++ b/mayan/apps/document_comments/urls.py @@ -11,25 +11,25 @@ from .views import ( urlpatterns = [ url( - regex=r'^(?P\d+)/comment/add/$', + regex=r'^documents/(?P\d+)/comments/$', + view=DocumentCommentListView.as_view(), name='comments_for_document' + ), + url( + regex=r'^documents/(?P\d+)/comments/add/$', view=DocumentCommentCreateView.as_view(), name='comment_add' ), url( - regex=r'^comment/(?P\d+)/delete/$', + regex=r'^comments/(?P\d+)/delete/$', view=DocumentCommentDeleteView.as_view(), name='comment_delete' ), url( - regex=r'^comment/(?P\d+)/$', + regex=r'^comments/(?P\d+)/$', view=DocumentCommentDetailView.as_view(), name='comment_details' ), url( - regex=r'^comment/(?P\d+)/edit/$', + regex=r'^comments/(?P\d+)/edit/$', view=DocumentCommentEditView.as_view(), name='comment_edit' ), - url( - regex=r'^(?P\d+)/comment/list/$', - view=DocumentCommentListView.as_view(), name='comments_for_document' - ), ] api_urls = [ diff --git a/mayan/apps/document_indexing/apps.py b/mayan/apps/document_indexing/apps.py index 2c0a770dc7..560b4cacfc 100644 --- a/mayan/apps/document_indexing/apps.py +++ b/mayan/apps/document_indexing/apps.py @@ -101,22 +101,24 @@ class DocumentIndexingApp(MayanAppConfig): model=IndexInstanceNode, related='index_template_node__index' ) - SourceColumn( - attribute='label', exclude=(IndexInstance,), is_identifier=True, - is_sortable=True, source=Index + column_index_label = SourceColumn( + attribute='label', is_identifier=True, is_sortable=True, + source=Index ) + column_index_label.add_exclude(source=IndexInstance) SourceColumn( attribute='label', is_object_absolute_url=True, is_identifier=True, is_sortable=True, source=IndexInstance ) - SourceColumn( - attribute='slug', exclude=(IndexInstance,), is_sortable=True, - source=Index + column_index_slug = SourceColumn( + attribute='slug', is_sortable=True, source=Index ) - SourceColumn( - attribute='enabled', exclude=(IndexInstance,), is_sortable=True, - source=Index, widget=TwoStateWidget + column_index_slug.add_exclude(IndexInstance) + column_index_enabled = SourceColumn( + attribute='enabled', is_sortable=True, source=Index, + widget=TwoStateWidget ) + column_index_enabled.add_exclude(source=IndexInstance) SourceColumn( func=lambda context: context[ diff --git a/mayan/apps/document_indexing/migrations/0016_auto_20191005_0647.py b/mayan/apps/document_indexing/migrations/0016_auto_20191005_0647.py new file mode 100644 index 0000000000..28c6c3fe34 --- /dev/null +++ b/mayan/apps/document_indexing/migrations/0016_auto_20191005_0647.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-10-05 06:47 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('document_indexing', '0015_auto_20190429_1922'), + ] + + operations = [ + migrations.AlterField( + model_name='index', + name='label', + field=models.CharField(help_text='Short description of this index.', max_length=128, unique=True, verbose_name='Label'), + ), + ] diff --git a/mayan/apps/document_indexing/models.py b/mayan/apps/document_indexing/models.py index e356bf6ff9..c734717eb3 100644 --- a/mayan/apps/document_indexing/models.py +++ b/mayan/apps/document_indexing/models.py @@ -33,6 +33,7 @@ class Index(models.Model): template and instance when resolved. """ label = models.CharField( + help_text=_('Short description of this index.'), max_length=128, unique=True, verbose_name=_('Label') ) slug = models.SlugField( @@ -49,7 +50,8 @@ class Index(models.Model): verbose_name=_('Enabled') ) document_types = models.ManyToManyField( - related_name='indexes', to=DocumentType, verbose_name=_('Document types') + related_name='indexes', to=DocumentType, + verbose_name=_('Document types') ) objects = IndexManager() diff --git a/mayan/apps/document_indexing/tests/__init__.py b/mayan/apps/document_indexing/tests/__init__.py index 82d60c8416..e69de29bb2 100644 --- a/mayan/apps/document_indexing/tests/__init__.py +++ b/mayan/apps/document_indexing/tests/__init__.py @@ -1 +0,0 @@ -from .mixins import * # NOQA diff --git a/mayan/apps/document_indexing/tests/test_api.py b/mayan/apps/document_indexing/tests/test_api.py index 7220151422..ad72f1ea63 100644 --- a/mayan/apps/document_indexing/tests/test_api.py +++ b/mayan/apps/document_indexing/tests/test_api.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals from rest_framework import status -from mayan.apps.documents.tests import DocumentTestMixin -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.documents.tests.mixins import DocumentTestMixin +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..models import Index from ..permissions import ( diff --git a/mayan/apps/document_indexing/tests/test_events.py b/mayan/apps/document_indexing/tests/test_events.py index eb826100c4..c5374899cd 100644 --- a/mayan/apps/document_indexing/tests/test_events.py +++ b/mayan/apps/document_indexing/tests/test_events.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals from actstream.models import Action -from mayan.apps.common.tests import GenericViewTestCase -from mayan.apps.documents.tests import DocumentTestMixin +from mayan.apps.common.tests.base import GenericViewTestCase +from mayan.apps.documents.tests.mixins import DocumentTestMixin from ..permissions import ( permission_document_indexing_create, permission_document_indexing_edit, diff --git a/mayan/apps/document_indexing/tests/test_models.py b/mayan/apps/document_indexing/tests/test_models.py index d458e69625..77281c007e 100644 --- a/mayan/apps/document_indexing/tests/test_models.py +++ b/mayan/apps/document_indexing/tests/test_models.py @@ -2,13 +2,11 @@ from __future__ import unicode_literals from django.utils.encoding import force_text -from mayan.apps.common.tests import BaseTestCase -from mayan.apps.documents.tests import ( - DocumentTestMixin, TEST_SMALL_DOCUMENT_PATH -) +from mayan.apps.common.tests.base import BaseTestCase +from mayan.apps.documents.tests.base import DocumentTestMixin from mayan.apps.documents.tests.literals import ( TEST_DOCUMENT_DESCRIPTION, TEST_DOCUMENT_DESCRIPTION_EDITED, - TEST_DOCUMENT_LABEL_EDITED + TEST_DOCUMENT_LABEL_EDITED, TEST_SMALL_DOCUMENT_PATH ) from mayan.apps.metadata.models import MetadataType, DocumentTypeMetadataType diff --git a/mayan/apps/document_indexing/tests/test_views.py b/mayan/apps/document_indexing/tests/test_views.py index 9bd515e9a9..d654bc691a 100644 --- a/mayan/apps/document_indexing/tests/test_views.py +++ b/mayan/apps/document_indexing/tests/test_views.py @@ -1,7 +1,8 @@ from __future__ import absolute_import, unicode_literals -from mayan.apps.common.tests import GenericViewTestCase -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from mayan.apps.documents.tests.base import ( + GenericDocumentViewTestCase, GenericViewTestCase +) from ..models import Index, IndexInstanceNode from ..permissions import ( diff --git a/mayan/apps/document_indexing/urls.py b/mayan/apps/document_indexing/urls.py index 6b363dbf5f..0cdefeeb6c 100644 --- a/mayan/apps/document_indexing/urls.py +++ b/mayan/apps/document_indexing/urls.py @@ -15,7 +15,6 @@ from .views import ( TemplateNodeCreateView, TemplateNodeDeleteView, TemplateNodeEditView ) - urlpatterns_templates = [ url( regex=r'^document_types/(?P\d+)/index_templates/$', diff --git a/mayan/apps/document_parsing/apps.py b/mayan/apps/document_parsing/apps.py index f481252e8b..ea4355c5a8 100644 --- a/mayan/apps/document_parsing/apps.py +++ b/mayan/apps/document_parsing/apps.py @@ -101,7 +101,7 @@ class DocumentParsingApp(MayanAppConfig): ) ModelField( - model=Document, name='versions__pages__content__content' + model=Document, name='versions__version_pages__content__content' ) ModelPermission.register( @@ -133,7 +133,7 @@ class DocumentParsingApp(MayanAppConfig): ) document_search.add_model_field( - field='versions__pages__content__content', label=_('Content') + field='versions__version_pages__content__content', label=_('Content') ) document_page_search.add_model_field( diff --git a/mayan/apps/document_parsing/links.py b/mayan/apps/document_parsing/links.py index 4fc81bca4d..2cfe95fab9 100644 --- a/mayan/apps/document_parsing/links.py +++ b/mayan/apps/document_parsing/links.py @@ -10,6 +10,11 @@ from .permissions import ( permission_parse_document ) + +def is_document_page_disabled(context): + return not context['resolved_object'].enabled + + link_document_content = Link( args='resolved_object.id', icon_class_path='mayan.apps.document_parsing.icons.icon_document_content', @@ -28,7 +33,7 @@ link_document_content_delete_multiple = Link( view='document_parsing:document_content_delete_multiple', ) link_document_page_content = Link( - args='resolved_object.id', + args='resolved_object.id', conditional_disable=is_document_page_disabled, icon_class_path='mayan.apps.document_parsing.icons.icon_document_content', permissions=(permission_content_view,), text=_('Content'), view='document_parsing:document_page_content' diff --git a/mayan/apps/document_parsing/tests/test_api.py b/mayan/apps/document_parsing/tests/test_api.py index 4d1b0d5825..ed72feb7b3 100644 --- a/mayan/apps/document_parsing/tests/test_api.py +++ b/mayan/apps/document_parsing/tests/test_api.py @@ -4,8 +4,9 @@ from django.test import override_settings from rest_framework import status -from mayan.apps.documents.tests import DocumentTestMixin, TEST_HYBRID_DOCUMENT -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.documents.tests.literals import TEST_HYBRID_DOCUMENT +from mayan.apps.documents.tests.mixins import DocumentTestMixin +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..permissions import permission_content_view diff --git a/mayan/apps/document_parsing/tests/test_indexing.py b/mayan/apps/document_parsing/tests/test_indexing.py index 4936cf4352..96e8d0dbde 100644 --- a/mayan/apps/document_parsing/tests/test_indexing.py +++ b/mayan/apps/document_parsing/tests/test_indexing.py @@ -1,7 +1,8 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import BaseTestCase -from mayan.apps.documents.tests import DocumentTestMixin, TEST_HYBRID_DOCUMENT +from mayan.apps.common.tests.base import BaseTestCase +from mayan.apps.documents.tests.literals import TEST_HYBRID_DOCUMENT +from mayan.apps.documents.tests.mixins import DocumentTestMixin from mayan.apps.document_indexing.models import Index, IndexInstanceNode from mayan.apps.document_indexing.tests.literals import TEST_INDEX_LABEL diff --git a/mayan/apps/document_parsing/tests/test_models.py b/mayan/apps/document_parsing/tests/test_models.py index ee5ccb55a3..72db48398c 100644 --- a/mayan/apps/document_parsing/tests/test_models.py +++ b/mayan/apps/document_parsing/tests/test_models.py @@ -2,7 +2,8 @@ from __future__ import unicode_literals from django.test import override_settings -from mayan.apps.documents.tests import GenericDocumentTestCase, TEST_HYBRID_DOCUMENT +from mayan.apps.documents.tests.base import GenericDocumentTestCase +from mayan.apps.documents.tests.literals import TEST_HYBRID_DOCUMENT from .literals import TEST_DOCUMENT_CONTENT diff --git a/mayan/apps/document_parsing/tests/test_parsers.py b/mayan/apps/document_parsing/tests/test_parsers.py index 1a4f731503..237bccc567 100644 --- a/mayan/apps/document_parsing/tests/test_parsers.py +++ b/mayan/apps/document_parsing/tests/test_parsers.py @@ -1,7 +1,8 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import BaseTestCase -from mayan.apps.documents.tests import DocumentTestMixin, TEST_HYBRID_DOCUMENT +from mayan.apps.common.tests.base import BaseTestCase +from mayan.apps.documents.tests.literals import TEST_HYBRID_DOCUMENT +from mayan.apps.documents.tests.mixins import DocumentTestMixin from ..parsers import PopplerParser diff --git a/mayan/apps/document_parsing/tests/test_views.py b/mayan/apps/document_parsing/tests/test_views.py index e7792dfd70..bb88c1817b 100644 --- a/mayan/apps/document_parsing/tests/test_views.py +++ b/mayan/apps/document_parsing/tests/test_views.py @@ -2,9 +2,8 @@ from __future__ import unicode_literals from django.test import override_settings -from mayan.apps.documents.tests import ( - GenericDocumentViewTestCase, TEST_HYBRID_DOCUMENT -) +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase +from mayan.apps.documents.tests.literals import TEST_HYBRID_DOCUMENT from ..models import DocumentPageContent from ..permissions import ( diff --git a/mayan/apps/document_signatures/migrations/0009_auto_20190711_0544.py b/mayan/apps/document_signatures/migrations/0009_auto_20190711_0544.py new file mode 100644 index 0000000000..b77b3ae66e --- /dev/null +++ b/mayan/apps/document_signatures/migrations/0009_auto_20190711_0544.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-07-11 05:44 +from __future__ import unicode_literals + +from django.db import migrations, models +import mayan.apps.document_signatures.models +import mayan.apps.storage.classes + + +class Migration(migrations.Migration): + + dependencies = [ + ('document_signatures', '0008_auto_20180429_0759'), + ] + + operations = [ + migrations.AlterField( + model_name='detachedsignature', + name='signature_file', + field=models.FileField(blank=True, null=True, storage=mayan.apps.storage.classes.FakeStorageSubclass(), upload_to=mayan.apps.document_signatures.models.upload_to, verbose_name='Signature file'), + ), + ] diff --git a/mayan/apps/document_signatures/settings.py b/mayan/apps/document_signatures/settings.py index e146ec667a..7a5e9dcfe1 100644 --- a/mayan/apps/document_signatures/settings.py +++ b/mayan/apps/document_signatures/settings.py @@ -18,9 +18,9 @@ setting_storage_backend = namespace.add_setting( ) setting_storage_backend_arguments = namespace.add_setting( global_name='SIGNATURES_STORAGE_BACKEND_ARGUMENTS', - default='{{location: {}}}'.format( - os.path.join(settings.MEDIA_ROOT, 'document_signatures') - ), quoted=True, help_text=_( + default={ + 'location': os.path.join(settings.MEDIA_ROOT, 'document_signatures') + }, help_text=_( 'Arguments to pass to the SIGNATURE_STORAGE_BACKEND. ' ) ) diff --git a/mayan/apps/document_signatures/storages.py b/mayan/apps/document_signatures/storages.py index 45d5d63d5e..423f11c1e5 100644 --- a/mayan/apps/document_signatures/storages.py +++ b/mayan/apps/document_signatures/storages.py @@ -1,23 +1,11 @@ from __future__ import unicode_literals -import yaml - -try: - from yaml import CSafeLoader as SafeLoader -except ImportError: - from yaml import SafeLoader - -from django.utils.module_loading import import_string +from mayan.apps.storage.utils import get_storage_subclass from .settings import ( setting_storage_backend, setting_storage_backend_arguments ) -storage_detachedsignature = import_string( +storage_detachedsignature = get_storage_subclass( dotted_path=setting_storage_backend.value -)( - **yaml.load( - stream=setting_storage_backend_arguments.value or '{}', - Loader=SafeLoader - ) -) +)(**setting_storage_backend_arguments.value) diff --git a/mayan/apps/document_signatures/tests/test_links.py b/mayan/apps/document_signatures/tests/test_links.py index 5f2b4678c3..8ad2d010c2 100644 --- a/mayan/apps/document_signatures/tests/test_links.py +++ b/mayan/apps/document_signatures/tests/test_links.py @@ -2,9 +2,8 @@ from __future__ import unicode_literals from django.urls import reverse -from mayan.apps.documents.tests import ( - GenericDocumentViewTestCase, TEST_DOCUMENT_PATH -) +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase +from mayan.apps.documents.tests.literals import TEST_DOCUMENT_PATH from ..links import ( link_document_version_signature_delete, diff --git a/mayan/apps/document_signatures/tests/test_models.py b/mayan/apps/document_signatures/tests/test_models.py index 957b87a159..2ac5c03ab4 100644 --- a/mayan/apps/document_signatures/tests/test_models.py +++ b/mayan/apps/document_signatures/tests/test_models.py @@ -8,9 +8,8 @@ from mayan.apps.django_gpg.tests.literals import ( TEST_KEY_DATA, TEST_KEY_PASSPHRASE ) from mayan.apps.documents.models import DocumentVersion -from mayan.apps.documents.tests import ( - GenericDocumentTestCase, TEST_DOCUMENT_PATH -) +from mayan.apps.documents.tests.base import GenericDocumentTestCase +from mayan.apps.documents.tests.literals import TEST_DOCUMENT_PATH from ..models import DetachedSignature, EmbeddedSignature from ..tasks import task_verify_missing_embedded_signature diff --git a/mayan/apps/document_signatures/tests/test_views.py b/mayan/apps/document_signatures/tests/test_views.py index 07c75aa6dd..dda0ec0639 100644 --- a/mayan/apps/document_signatures/tests/test_views.py +++ b/mayan/apps/document_signatures/tests/test_views.py @@ -3,9 +3,8 @@ from __future__ import absolute_import, unicode_literals from django_downloadview.test import assert_download_response from mayan.apps.documents.models import DocumentVersion -from mayan.apps.documents.tests import ( - GenericDocumentViewTestCase, TEST_DOCUMENT_PATH -) +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase +from mayan.apps.documents.tests.literals import TEST_DOCUMENT_PATH from ..models import DetachedSignature, EmbeddedSignature from ..permissions import ( diff --git a/mayan/apps/document_signatures/urls.py b/mayan/apps/document_signatures/urls.py index a70ec1c85c..cbc1896df5 100644 --- a/mayan/apps/document_signatures/urls.py +++ b/mayan/apps/document_signatures/urls.py @@ -12,37 +12,37 @@ from .views import ( urlpatterns = [ url( - regex=r'^(?P\d+)/details/$', + regex=r'^signatures/(?P\d+)/details/$', view=DocumentVersionSignatureDetailView.as_view(), name='document_version_signature_details' ), url( - regex=r'^signature/(?P\d+)/download/$', + regex=r'^signatures/(?P\d+)/download/$', view=DocumentVersionSignatureDownloadView.as_view(), name='document_version_signature_download' ), url( - regex=r'^document/version/(?P\d+)/signatures/list/$', + regex=r'^documents/versions/(?P\d+)/signatures/$', view=DocumentVersionSignatureListView.as_view(), name='document_version_signature_list' ), url( - regex=r'^documents/version/(?P\d+)/signature/detached/upload/$', + regex=r'^documents/versions/(?P\d+)/signatures/detached/upload/$', view=DocumentVersionSignatureUploadView.as_view(), name='document_version_signature_upload' ), url( - regex=r'^documents/version/(?P\d+)/signature/detached/create/$', + regex=r'^documents/versions/(?P\d+)/signatures/detached/create/$', view=DocumentVersionDetachedSignatureCreateView.as_view(), name='document_version_signature_detached_create' ), url( - regex=r'^documents/version/(?P\d+)/signature/embedded/create/$', + regex=r'^documents/versions/(?P\d+)/signatures/embedded/create/$', view=DocumentVersionEmbeddedSignatureCreateView.as_view(), name='document_version_signature_embedded_create' ), url( - regex=r'^signature/(?P\d+)/delete/$', + regex=r'^signatures/(?P\d+)/delete/$', view=DocumentVersionSignatureDeleteView.as_view(), name='document_version_signature_delete' ), diff --git a/mayan/apps/document_states/api_views.py b/mayan/apps/document_states/api_views.py index 78e4e31893..29ba509667 100644 --- a/mayan/apps/document_states/api_views.py +++ b/mayan/apps/document_states/api_views.py @@ -1,6 +1,8 @@ from __future__ import absolute_import, unicode_literals +from django.http import HttpResponse from django.shortcuts import get_object_or_404 +from django.views.decorators.cache import cache_control, patch_cache_control from rest_framework import generics @@ -10,6 +12,7 @@ from mayan.apps.documents.permissions import permission_document_type_view from mayan.apps.rest_api.filters import MayanObjectPermissionsFilter from mayan.apps.rest_api.permissions import MayanPermission +from .literals import WORKFLOW_IMAGE_TASK_TIMEOUT from .models import Workflow from .permissions import ( permission_workflow_create, permission_workflow_delete, @@ -23,8 +26,11 @@ from .serializers import ( WritableWorkflowTransitionSerializer ) +from .settings import settings_workflow_image_cache_time +from .tasks import task_generate_workflow_image -class APIDocumentTypeWorkflowListView(generics.ListAPIView): + +class APIDocumentTypeWorkflowRuntimeProxyListView(generics.ListAPIView): """ get: Returns a list of all the document type workflows. """ @@ -172,7 +178,43 @@ class APIWorkflowDocumentTypeView(generics.RetrieveDestroyAPIView): self.get_workflow().document_types.remove(instance) -class APIWorkflowListView(generics.ListCreateAPIView): +class APIWorkflowImageView(generics.RetrieveAPIView): + """ + get: Returns an image representation of the selected workflow. + """ + filter_backends = (MayanObjectPermissionsFilter,) + mayan_object_permissions = { + 'GET': (permission_workflow_view,), + } + queryset = Workflow.objects.all() + + def get_serializer(self, *args, **kwargs): + return None + + def get_serializer_class(self): + return None + + @cache_control(private=True) + def retrieve(self, request, *args, **kwargs): + task = task_generate_workflow_image.apply_async( + kwargs=dict( + document_state_id=self.get_object().pk, + ) + ) + + cache_filename = task.get(timeout=WORKFLOW_IMAGE_TASK_TIMEOUT) + cache_file = self.get_object().cache_partition.get_file(filename=cache_filename) + with cache_file.open() as file_object: + response = HttpResponse(file_object.read(), content_type='image') + if '_hash' in request.GET: + patch_cache_control( + response, + max_age=settings_workflow_image_cache_time.value + ) + return response + + +class APIWorkflowRuntimeProxyListView(generics.ListCreateAPIView): """ get: Returns a list of all the workflows. post: Create a new workflow. @@ -187,7 +229,7 @@ class APIWorkflowListView(generics.ListCreateAPIView): if not self.request: return None - return super(APIWorkflowListView, self).get_serializer(*args, **kwargs) + return super(APIWorkflowRuntimeProxyListView, self).get_serializer(*args, **kwargs) def get_serializer_class(self): if self.request.method == 'GET': diff --git a/mayan/apps/document_states/apps.py b/mayan/apps/document_states/apps.py index d5e4cdae26..e3dc71ed91 100644 --- a/mayan/apps/document_states/apps.py +++ b/mayan/apps/document_states/apps.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from django.apps import apps -from django.db.models.signals import post_save +from django.db.models.signals import post_migrate, post_save from django.utils.translation import ugettext_lazy as _ from mayan.apps.acls.classes import ModelPermission @@ -25,31 +25,32 @@ from .classes import DocumentStateHelper, WorkflowAction from .events import event_workflow_edited from .dependencies import * # NOQA from .handlers import ( - handler_index_document, handler_launch_workflow, handler_trigger_transition + handler_create_workflow_image_cache, handler_index_document, + handler_launch_workflow, handler_trigger_transition ) -from .html_widgets import widget_transition_events, WorkflowLogExtraDataWidget +from .html_widgets import WorkflowLogExtraDataWidget, widget_transition_events from .links import ( - link_document_workflow_instance_list, link_setup_document_type_workflows, - link_setup_workflow_document_types, link_setup_workflow_create, - link_setup_workflow_delete, link_setup_workflow_edit, - link_setup_workflow_list, link_setup_workflow_states, - link_setup_workflow_state_action_delete, - link_setup_workflow_state_action_edit, - link_setup_workflow_state_action_list, - link_setup_workflow_state_action_selection, - link_setup_workflow_state_create, link_setup_workflow_state_delete, - link_setup_workflow_state_edit, link_setup_workflow_transitions, - link_setup_workflow_transition_create, - link_setup_workflow_transition_delete, link_setup_workflow_transition_edit, - link_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_list, link_document_type_workflow_templates, + link_workflow_template_document_types, link_workflow_template_create, + link_workflow_template_delete, link_workflow_template_edit, + link_workflow_template_list, link_workflow_template_state_list, + link_workflow_template_state_action_delete, + link_workflow_template_state_action_edit, + link_workflow_template_state_action_list, + link_workflow_template_state_action_selection, + link_workflow_template_state_create, link_workflow_template_state_delete, + link_workflow_template_state_edit, link_workflow_template_transition_list, + link_workflow_template_transition_create, + link_workflow_template_transition_delete, link_workflow_template_transition_edit, + link_workflow_template_transition_field_create, + link_workflow_template_transition_field_delete, + link_workflow_template_transition_field_edit, + link_workflow_template_transition_field_list, + link_tool_launch_workflows, link_workflow_instance_detail, link_workflow_instance_transition, link_workflow_runtime_proxy_document_list, - link_workflow_runtime_proxy_list, link_workflow_preview, + link_workflow_runtime_proxy_list, link_workflow_template_preview, link_workflow_runtime_proxy_state_document_list, link_workflow_runtime_proxy_state_list, - link_workflow_transition_events + link_workflow_template_transition_events ) from .permissions import ( permission_workflow_delete, permission_workflow_edit, @@ -166,10 +167,10 @@ class DocumentStatesApp(MayanAppConfig): SourceColumn( attribute='label', is_sortable=True, source=Workflow ) - SourceColumn( - attribute='internal_name', exclude=(WorkflowRuntimeProxy,), - is_sortable=True, source=Workflow + column_workflow_internal_name = SourceColumn( + attribute='internal_name', is_sortable=True, source=Workflow ) + column_workflow_internal_name.add_exclude(source=WorkflowRuntimeProxy) SourceColumn( attribute='get_initial_state', empty_value=_('None'), source=Workflow @@ -314,50 +315,60 @@ class DocumentStatesApp(MayanAppConfig): ) menu_facet.bind_links( - links=(link_document_workflow_instance_list,), sources=(Document,) + links=(link_workflow_instance_list,), sources=(Document,) ) menu_list_facet.bind_links( links=( link_acl_list, link_events_for_object, link_object_event_types_user_subcriptions_list, - link_setup_workflow_document_types, - link_setup_workflow_states, link_setup_workflow_transitions, - link_workflow_preview + link_workflow_template_document_types, + link_workflow_template_state_list, link_workflow_template_transition_list, + link_workflow_template_preview ), sources=(Workflow,) ) + + menu_list_facet.unbind_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 + ), sources=(WorkflowRuntimeProxy,) + ) + menu_list_facet.bind_links( links=( - link_setup_document_type_workflows, + link_document_type_workflow_templates, ), sources=(DocumentType,) ) menu_main.bind_links(links=(link_workflow_runtime_proxy_list,), position=10) menu_object.bind_links( links=( - link_setup_workflow_delete, link_setup_workflow_edit + link_workflow_template_delete, link_workflow_template_edit ), sources=(Workflow,) ) menu_object.bind_links( links=( - link_setup_workflow_state_edit, - link_setup_workflow_state_action_list, - link_setup_workflow_state_delete + link_workflow_template_state_edit, + link_workflow_template_state_action_list, + link_workflow_template_state_delete ), sources=(WorkflowState,) ) menu_object.bind_links( links=( - link_setup_workflow_transition_edit, - link_workflow_transition_events, - link_setup_workflow_transition_field_list, - link_acl_list, - link_setup_workflow_transition_delete + link_workflow_template_transition_edit, + link_workflow_template_transition_events, + link_workflow_template_transition_field_list, link_acl_list, + link_workflow_template_transition_delete ), sources=(WorkflowTransition,) ) menu_object.bind_links( links=( - link_setup_workflow_transition_field_delete, - link_setup_workflow_transition_field_edit + link_workflow_template_transition_field_delete, + link_workflow_template_transition_field_edit ), sources=(WorkflowTransitionField,) ) menu_object.bind_links( @@ -380,17 +391,23 @@ class DocumentStatesApp(MayanAppConfig): ) menu_object.bind_links( links=( - link_setup_workflow_state_action_edit, + link_workflow_template_state_action_edit, link_object_error_list, - link_setup_workflow_state_action_delete, + link_workflow_template_state_action_delete, ), sources=(WorkflowStateAction,) ) menu_secondary.bind_links( - links=(link_setup_workflow_list, link_setup_workflow_create), + links=(link_workflow_template_list, link_workflow_template_create), sources=( - Workflow, 'document_states:setup_workflow_create', - 'document_states:setup_workflow_list' + Workflow, 'document_states:workflow_template_create', + 'document_states:workflow_template_list' + ) + ) + menu_secondary.bind_links( + links=(link_workflow_template_transition_field_create,), + sources=( + WorkflowTransition, ) ) menu_secondary.bind_links( @@ -406,31 +423,31 @@ class DocumentStatesApp(MayanAppConfig): ) ) menu_secondary.bind_links( - links=(link_setup_workflow_state_action_selection,), + links=(link_workflow_template_state_action_selection,), sources=( WorkflowState, ) ) menu_secondary.bind_links( links=( - link_setup_workflow_transition_create, + link_workflow_template_transition_create, ), sources=( WorkflowTransition, - 'document_states:setup_workflow_transition_list', + 'document_states:workflow_template_transition_list', ) ) menu_secondary.bind_links( links=( - link_setup_workflow_state_create, + link_workflow_template_state_create, ), sources=( WorkflowState, - 'document_states:setup_workflow_state_list', + 'document_states:workflow_template_state_list', ) ) - menu_setup.bind_links(links=(link_setup_workflow_list,)) + menu_setup.bind_links(links=(link_workflow_template_list,)) - menu_tools.bind_links(links=(link_tool_launch_all_workflows,)) + menu_tools.bind_links(links=(link_tool_launch_workflows,)) post_save.connect( dispatch_uid='workflows_handler_launch_workflow', @@ -440,6 +457,10 @@ class DocumentStatesApp(MayanAppConfig): # Index updating + post_migrate.connect( + dispatch_uid='workflows_handler_create_workflow_image_cache', + receiver=handler_create_workflow_image_cache, + ) post_save.connect( dispatch_uid='workflows_handler_index_document_save', receiver=handler_index_document, diff --git a/mayan/apps/document_states/fields.py b/mayan/apps/document_states/fields.py new file mode 100644 index 0000000000..75a3e7b958 --- /dev/null +++ b/mayan/apps/document_states/fields.py @@ -0,0 +1,9 @@ +from __future__ import absolute_import, unicode_literals + +from django import forms + +from .widgets import WorkflowImageWidget + + +class WorfklowImageField(forms.fields.Field): + widget = WorkflowImageWidget diff --git a/mayan/apps/document_states/forms.py b/mayan/apps/document_states/forms.py index 1e10ea9a14..e2320e2b46 100644 --- a/mayan/apps/document_states/forms.py +++ b/mayan/apps/document_states/forms.py @@ -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): - preview = forms.CharField(widget=WorkflowImageWidget()) + workflow = WorfklowImageField() def __init__(self, *args, **kwargs): instance = kwargs.pop('instance', None) super(WorkflowPreviewForm, self).__init__(*args, **kwargs) - self.fields['preview'].initial = instance + self.fields['workflow'].initial = instance diff --git a/mayan/apps/document_states/handlers.py b/mayan/apps/document_states/handlers.py index a0e3ee6899..8fc226b59b 100644 --- a/mayan/apps/document_states/handlers.py +++ b/mayan/apps/document_states/handlers.py @@ -6,6 +6,22 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.document_indexing.tasks import task_index_document from mayan.apps.events.classes import EventType +from .literals import ( + WORKFLOW_IMAGE_CACHE_STORAGE_INSTANCE_PATH, WORKFLOW_IMAGE_CACHE_NAME +) +from .settings import setting_workflow_image_cache_maximum_size + + +def handler_create_workflow_image_cache(sender, **kwargs): + Cache = apps.get_model(app_label='file_caching', model_name='Cache') + Cache.objects.update_or_create( + defaults={ + 'label': _('Workflow images'), + 'storage_instance_path': WORKFLOW_IMAGE_CACHE_STORAGE_INSTANCE_PATH, + 'maximum_size': setting_workflow_image_cache_maximum_size.value, + }, name=WORKFLOW_IMAGE_CACHE_NAME, + ) + def handler_index_document(sender, **kwargs): task_index_document.apply_async( diff --git a/mayan/apps/document_states/html_widgets.py b/mayan/apps/document_states/html_widgets.py index ac518160a5..58d3da1773 100644 --- a/mayan/apps/document_states/html_widgets.py +++ b/mayan/apps/document_states/html_widgets.py @@ -1,9 +1,7 @@ from __future__ import unicode_literals -from django import forms from django.template.loader import render_to_string -from django.urls import reverse -from django.utils.html import format_html_join, mark_safe +from django.utils.html import format_html_join def widget_transition_events(transition): @@ -16,18 +14,6 @@ def widget_transition_events(transition): ) -def widget_workflow_diagram(workflow): - return mark_safe( - ''.format( - reverse( - viewname='document_states:workflow_image', kwargs={ - 'pk': workflow.pk - } - ) - ) - ) - - class WorkflowLogExtraDataWidget(object): template_name = 'document_states/extra_data.html' diff --git a/mayan/apps/document_states/icons.py b/mayan/apps/document_states/icons.py index 21df02a9b5..3115901935 100644 --- a/mayan/apps/document_states/icons.py +++ b/mayan/apps/document_states/icons.py @@ -5,42 +5,40 @@ from mayan.apps.documents.icons import icon_document, icon_document_type icon_workflow = Icon(driver_name='fontawesome', symbol='sitemap') -icon_document_type_workflow_list = icon_workflow +icon_tool_launch_workflows = icon_workflow -icon_document_workflow_instance_list = Icon( - driver_name='fontawesome', symbol='sitemap' -) -icon_setup_workflow_list = Icon(driver_name='fontawesome', symbol='sitemap') -icon_tool_launch_all_workflows = Icon( - driver_name='fontawesome', symbol='sitemap' -) -icon_workflow_create = Icon( +icon_document_type_workflow_list = icon_workflow +icon_workflow_template_create = Icon( driver_name='fontawesome-dual', primary_symbol='sitemap', secondary_symbol='plus' ) -icon_workflow_delete = Icon(driver_name='fontawesome', symbol='times') -icon_workflow_document_type_list = icon_document_type -icon_workflow_edit = Icon(driver_name='fontawesome', symbol='pencil-alt') -icon_workflow_list = Icon(driver_name='fontawesome', symbol='sitemap') -icon_workflow_preview = Icon(driver_name='fontawesome', symbol='eye') - - -icon_workflow_instance_detail = Icon( - driver_name='fontawesome', symbol='sitemap' +icon_workflow_template_delete = Icon(driver_name='fontawesome', symbol='times') +icon_workflow_template_document_type_list = icon_document_type +icon_workflow_template_edit = Icon( + driver_name='fontawesome', symbol='pencil-alt' ) +icon_workflow_template_list = icon_workflow +icon_workflow_template_preview = Icon(driver_name='fontawesome', symbol='eye') + +# Workflow instances + +icon_workflow_instance_detail = icon_workflow +icon_workflow_instance_list = icon_workflow icon_workflow_instance_transition = Icon( driver_name='fontawesome', symbol='arrows-alt-h' ) +# Workflow runtime proxies + icon_workflow_runtime_proxy_document_list = icon_document -icon_workflow_runtime_proxy_list = Icon( - driver_name='fontawesome', symbol='sitemap' -) +icon_workflow_runtime_proxy_list = icon_workflow icon_workflow_runtime_proxy_state_document_list = icon_document icon_workflow_runtime_proxy_state_list = Icon( driver_name='fontawesome', symbol='circle' ) +# Workflow transition states + icon_workflow_state_action_delete = Icon( driver_name='fontawesome', symbol='times' ) @@ -58,6 +56,8 @@ 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,7 +69,12 @@ icon_workflow_state_action_selection = Icon( driver_name='fontawesome-dual', primary_symbol='code', secondary_symbol='plus' ) -icon_workflow_state_action_list = Icon(driver_name='fontawesome', symbol='code') +icon_workflow_state_action_list = Icon( + driver_name='fontawesome', symbol='code' +) + +# Workflow transitions + icon_workflow_transition = Icon( driver_name='fontawesome', symbol='arrows-alt-h' ) @@ -84,7 +89,11 @@ icon_workflow_transition_edit = Icon( driver_name='fontawesome', symbol='pencil-alt' ) -icon_workflow_transition_field = Icon(driver_name='fontawesome', symbol='table') +# Workflow transition fields + +icon_workflow_transition_field = Icon( + driver_name='fontawesome', symbol='table' +) icon_workflow_transition_field_delete = Icon( driver_name='fontawesome', symbol='times' ) @@ -98,7 +107,6 @@ 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' ) diff --git a/mayan/apps/document_states/links.py b/mayan/apps/document_states/links.py index 1404ff2644..7becc0da3a 100644 --- a/mayan/apps/document_states/links.py +++ b/mayan/apps/document_states/links.py @@ -11,179 +11,186 @@ from .permissions import ( permission_workflow_view, ) -link_setup_document_type_workflows = Link( +# Workflow templates + +link_document_type_workflow_templates = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_document_type_workflow_list', permissions=(permission_document_type_edit,), text=_('Workflows'), - view='document_states:document_type_workflows', + view='document_states:document_type_workflow_templates', ) -link_setup_workflow_create = Link( - icon_class_path='mayan.apps.document_states.icons.icon_workflow_create', +link_workflow_template_create = Link( + icon_class_path='mayan.apps.document_states.icons.icon_workflow_template_create', permissions=(permission_workflow_create,), - text=_('Create workflow'), view='document_states:setup_workflow_create' + text=_('Create workflow'), view='document_states:workflow_template_create' ) -link_setup_workflow_delete = Link( +link_workflow_template_delete = Link( args='resolved_object.pk', - icon_class_path='mayan.apps.document_states.icons.icon_workflow_delete', + icon_class_path='mayan.apps.document_states.icons.icon_workflow_template_delete', permissions=(permission_workflow_delete,), tags='dangerous', text=_('Delete'), - view='document_states:setup_workflow_delete', + view='document_states:workflow_template_delete', ) -link_setup_workflow_document_types = Link( +link_workflow_template_document_types = Link( args='resolved_object.pk', - icon_class_path='mayan.apps.document_states.icons.icon_workflow_document_type_list', + icon_class_path='mayan.apps.document_states.icons.icon_workflow_template_document_type_list', permissions=(permission_workflow_edit,), text=_('Document types'), - view='document_states:setup_workflow_document_types', + view='document_states:workflow_template_document_types', ) -link_setup_workflow_edit = Link( +link_workflow_template_edit = Link( args='resolved_object.pk', - icon_class_path='mayan.apps.document_states.icons.icon_workflow_edit', + icon_class_path='mayan.apps.document_states.icons.icon_workflow_template_edit', permissions=(permission_workflow_edit,), - text=_('Edit'), view='document_states:setup_workflow_edit', + text=_('Edit'), view='document_states:workflow_template_edit', ) -link_setup_workflow_list = Link( - icon_class_path='mayan.apps.document_states.icons.icon_setup_workflow_list', +link_workflow_template_list = Link( + icon_class_path='mayan.apps.document_states.icons.icon_workflow_template_list', permissions=(permission_workflow_view,), text=_('Workflows'), - view='document_states:setup_workflow_list' + view='document_states:workflow_template_list' ) -link_setup_workflow_state_action_delete = Link( +link_workflow_template_preview = Link( + args='resolved_object.pk', + icon_class_path='mayan.apps.document_states.icons.icon_workflow_template_preview', + permissions=(permission_workflow_view,), + text=_('Preview'), view='document_states:workflow_template_preview' +) + +# Workflow template state actions + +link_workflow_template_state_action_delete = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_state_action_delete', permissions=(permission_workflow_edit,), tags='dangerous', text=_('Delete'), - view='document_states:setup_workflow_state_action_delete', + view='document_states:workflow_template_state_action_delete', ) -link_setup_workflow_state_action_edit = Link( +link_workflow_template_state_action_edit = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_state_action_edit', permissions=(permission_workflow_edit,), - text=_('Edit'), view='document_states:setup_workflow_state_action_edit', + text=_('Edit'), view='document_states:workflow_template_state_action_edit', ) -link_setup_workflow_state_action_list = Link( +link_workflow_template_state_action_list = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_state_action_list', permissions=(permission_workflow_edit,), text=_('Actions'), - view='document_states:setup_workflow_state_action_list', + view='document_states:workflow_template_state_action_list', ) -link_setup_workflow_state_action_selection = Link( +link_workflow_template_state_action_selection = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_state_action', permissions=(permission_workflow_edit,), text=_('Create action'), - view='document_states:setup_workflow_state_action_selection', + view='document_states:workflow_template_state_action_selection', ) -link_setup_workflow_state_create = Link( + +# Workflow template states + +link_workflow_template_state_create = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_state_create', permissions=(permission_workflow_edit,), text=_('Create state'), - view='document_states:setup_workflow_state_create', + view='document_states:workflow_template_state_create', ) -link_setup_workflow_state_delete = Link( +link_workflow_template_state_delete = Link( args='object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_state_delete', permissions=(permission_workflow_edit,), tags='dangerous', text=_('Delete'), - view='document_states:setup_workflow_state_delete', + view='document_states:workflow_template_state_delete', ) -link_setup_workflow_state_edit = Link( +link_workflow_template_state_edit = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_state_edit', permissions=(permission_workflow_edit,), - text=_('Edit'), view='document_states:setup_workflow_state_edit', + text=_('Edit'), view='document_states:workflow_template_state_edit', ) -link_setup_workflow_states = Link( +link_workflow_template_state_list = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_state', permissions=(permission_workflow_view,), text=_('States'), - view='document_states:setup_workflow_state_list', + view='document_states:workflow_template_state_list', ) -link_setup_workflow_transition_create = Link( + +# Workflow template transitions + +link_workflow_template_transition_create = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_create', permissions=(permission_workflow_edit,), text=_('Create transition'), - view='document_states:setup_workflow_transition_create', + view='document_states:workflow_template_transition_create', ) -link_setup_workflow_transition_delete = Link( +link_workflow_template_transition_delete = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_delete', permissions=(permission_workflow_edit,), tags='dangerous', text=_('Delete'), - view='document_states:setup_workflow_transition_delete', + view='document_states:workflow_template_transition_delete', ) -link_setup_workflow_transition_edit = Link( +link_workflow_template_transition_edit = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_edit', permissions=(permission_workflow_edit,), - text=_('Edit'), view='document_states:setup_workflow_transition_edit', + text=_('Edit'), view='document_states:workflow_template_transition_edit', ) -link_setup_workflow_transitions = Link( - args='resolved_object.pk', - icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition', - permissions=(permission_workflow_view,), text=_('Transitions'), - view='document_states:setup_workflow_transition_list', -) -link_workflow_transition_events = Link( +link_workflow_template_transition_events = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_triggers', permissions=(permission_workflow_edit,), text=_('Transition triggers'), - view='document_states:setup_workflow_transition_events' + view='document_states:workflow_template_transition_events' +) +link_workflow_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', ) # Workflow transition fields -link_setup_workflow_transition_field_create = Link( + +link_workflow_template_transition_field_create = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_field', permissions=(permission_workflow_edit,), text=_('Create field'), - view='document_states:setup_workflow_transition_field_create', + view='document_states:workflow_template_transition_field_create', ) -link_setup_workflow_transition_field_delete = Link( +link_workflow_template_transition_field_delete = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_field_delete', permissions=(permission_workflow_edit,), tags='dangerous', text=_('Delete'), - view='document_states:setup_workflow_transition_field_delete', + view='document_states:workflow_template_transition_field_delete', ) -link_setup_workflow_transition_field_edit = Link( + +link_workflow_template_transition_field_edit = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_field_edit', permissions=(permission_workflow_edit,), - text=_('Edit'), view='document_states:setup_workflow_transition_field_edit', + text=_('Edit'), view='document_states:workflow_template_transition_field_edit', ) -link_setup_workflow_transition_field_list = Link( +link_workflow_template_transition_field_list = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_field_list', permissions=(permission_workflow_edit,), text=_('Fields'), - view='document_states: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' + view='document_states:workflow_template_transition_field_list', ) # Document workflow instances -link_document_workflow_instance_list = Link( - args='resolved_object.pk', - icon_class_path='mayan.apps.document_states.icons.icon_document_workflow_instance_list', - permissions=(permission_workflow_view,), text=_('Workflows'), - view='document_states:document_workflow_instance_list', -) + link_workflow_instance_detail = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_instance_detail', permissions=(permission_workflow_view,), text=_('Detail'), view='document_states:workflow_instance_detail', ) +link_workflow_instance_list = Link( + args='resolved_object.pk', + icon_class_path='mayan.apps.document_states.icons.icon_workflow_instance_list', + permissions=(permission_workflow_view,), text=_('Workflows'), + view='document_states:workflow_instance_list', +) link_workflow_instance_transition = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_instance_transition', @@ -192,28 +199,38 @@ 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_document_list', + view='document_states:workflow_runtime_proxy_document_list', ) link_workflow_runtime_proxy_list = Link( icon_class_path='mayan.apps.document_states.icons.icon_workflow_runtime_proxy_list', permissions=(permission_workflow_view,), - text=_('Workflows'), view='document_states:workflow_list' + text=_('Workflows'), view='document_states:workflow_runtime_proxy_list' ) link_workflow_runtime_proxy_state_document_list = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_runtime_proxy_state_document_list', permissions=(permission_workflow_view,), text=_('State documents'), - view='document_states:workflow_state_document_list', + view='document_states:workflow_runtime_proxy_state_document_list', ) link_workflow_runtime_proxy_state_list = Link( args='resolved_object.pk', icon_class_path='mayan.apps.document_states.icons.icon_workflow_runtime_proxy_state_list', permissions=(permission_workflow_view,), - text=_('States'), view='document_states:workflow_state_list', + text=_('States'), view='document_states:workflow_runtime_proxy_state_list', +) + +# Tools + +link_tool_launch_workflows = Link( + icon_class_path='mayan.apps.document_states.icons.icon_tool_launch_workflows', + permissions=(permission_workflow_tools,), + text=_('Launch all workflows'), + view='document_states:tool_launch_workflows' ) diff --git a/mayan/apps/document_states/literals.py b/mayan/apps/document_states/literals.py index 84242f9ab0..ee4d5a085e 100644 --- a/mayan/apps/document_states/literals.py +++ b/mayan/apps/document_states/literals.py @@ -2,6 +2,8 @@ from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ +DEFAULT_WORKFLOW_IMAGE_CACHE_MAXIMUM_SIZE = 50 * 2 ** 20 # 50 Megabytes + FIELD_TYPE_CHOICE_CHAR = 1 FIELD_TYPE_CHOICE_INTEGER = 2 FIELD_TYPE_CHOICES = ( @@ -30,3 +32,6 @@ WORKFLOW_ACTION_WHEN_CHOICES = ( (WORKFLOW_ACTION_ON_ENTRY, _('On entry')), (WORKFLOW_ACTION_ON_EXIT, _('On exit')), ) +WORKFLOW_IMAGE_CACHE_NAME = 'workflow_images' +WORKFLOW_IMAGE_CACHE_STORAGE_INSTANCE_PATH = 'mayan.apps.document_states.storages.storage_workflowimagecache' +WORKFLOW_IMAGE_TASK_TIMEOUT = 60 diff --git a/mayan/apps/document_states/models.py b/mayan/apps/document_states/models.py index 87628d470a..62de81badc 100644 --- a/mayan/apps/document_states/models.py +++ b/mayan/apps/document_states/models.py @@ -1,8 +1,10 @@ from __future__ import absolute_import, unicode_literals +import hashlib import json import logging +from furl import furl from graphviz import Digraph import yaml try: @@ -10,16 +12,22 @@ try: except ImportError: from yaml import SafeLoader +from django.apps import apps from django.conf import settings +from django.core import serializers from django.core.exceptions import PermissionDenied, ValidationError from django.db import IntegrityError, models, transaction from django.db.models import F, Max, Q from django.urls import reverse -from django.utils.encoding import force_text, python_2_unicode_compatible +from django.utils.encoding import ( + force_bytes, force_text, python_2_unicode_compatible +) +from django.utils.functional import cached_property from django.utils.module_loading import import_string from django.utils.translation import ugettext_lazy as _ from mayan.apps.acls.models import AccessControlList +from mayan.apps.common.serialization import yaml_load from mayan.apps.common.validators import YAMLValidator, validate_internal_name from mayan.apps.documents.models import Document, DocumentType from mayan.apps.documents.permissions import permission_document_view @@ -29,7 +37,8 @@ from .error_logs import error_log_state_actions from .events import event_workflow_created, event_workflow_edited from .literals import ( FIELD_TYPE_CHOICES, WIDGET_CLASS_CHOICES, WORKFLOW_ACTION_WHEN_CHOICES, - WORKFLOW_ACTION_ON_ENTRY, WORKFLOW_ACTION_ON_EXIT + WORKFLOW_ACTION_ON_ENTRY, WORKFLOW_ACTION_ON_EXIT, + WORKFLOW_IMAGE_CACHE_NAME ) from .managers import WorkflowManager from .permissions import permission_workflow_transition @@ -69,9 +78,71 @@ class Workflow(models.Model): def __str__(self): return self.label + @cached_property + def cache(self): + Cache = apps.get_model(app_label='file_caching', model_name='Cache') + return Cache.objects.get(name=WORKFLOW_IMAGE_CACHE_NAME) + + @cached_property + def cache_partition(self): + partition, created = self.cache.partitions.get_or_create( + name='{}'.format(self.pk) + ) + return partition + + def delete(self, *args, **kwargs): + self.cache_partition.delete() + return super(Workflow, self).delete(*args, **kwargs) + + def generate_image(self): + cache_filename = '{}'.format(self.get_hash()) + + if self.cache_partition.get_file(filename=cache_filename): + logger.debug( + 'workflow cache file "%s" found', cache_filename + ) + else: + logger.debug( + 'workflow cache file "%s" not found', cache_filename + ) + + image = self.render() + with self.cache_partition.create_file(filename=cache_filename) 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( + WorkflowStateAction.objects.filter(state__workflow__pk=self.pk) + ) + list( + WorkflowTransition.objects.filter(workflow__pk=self.pk) + ) + + return hashlib.sha256( + force_bytes( + serializers.serialize('json', objects_lists) + ) + ).hexdigest() + def get_initial_state(self): try: return self.states.get(initial=True) @@ -420,7 +491,7 @@ class WorkflowTransitionField(models.Model): return self.label def get_widget_kwargs(self): - return yaml.load(stream=self.widget_kwargs, Loader=SafeLoader) + return yaml_load(stream=self.widget_kwargs) @python_2_unicode_compatible diff --git a/mayan/apps/document_states/queues.py b/mayan/apps/document_states/queues.py index 6b269d5b99..b68354e8b2 100644 --- a/mayan/apps/document_states/queues.py +++ b/mayan/apps/document_states/queues.py @@ -3,12 +3,21 @@ from __future__ import absolute_import, unicode_literals from django.utils.translation import ugettext_lazy as _ from mayan.apps.task_manager.classes import CeleryQueue -from mayan.apps.task_manager.workers import worker_slow +from mayan.apps.task_manager.workers import worker_fast, worker_slow queue_document_states = CeleryQueue( - name='document_states', label=_('Document states'), worker=worker_slow + label=_('Document states'), name='document_states', worker=worker_slow ) +queue_document_states_fast = CeleryQueue( + label=_('Document states fast'), name='document_states_fast', + worker=worker_fast +) + queue_document_states.add_task_type( - dotted_path='mayan.apps.document_states.tasks.task_launch_all_workflows', - label=_('Launch all workflows') + label=_('Launch all workflows'), + dotted_path='mayan.apps.document_states.tasks.task_launch_all_workflows' +) +queue_document_states_fast.add_task_type( + label=_('Generate workflow previews'), + dotted_path='mayan.apps.document_states.tasks.task_generate_workflow_image' ) diff --git a/mayan/apps/document_states/settings.py b/mayan/apps/document_states/settings.py new file mode 100644 index 0000000000..657754856b --- /dev/null +++ b/mayan/apps/document_states/settings.py @@ -0,0 +1,44 @@ +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 + +from .literals import DEFAULT_WORKFLOW_IMAGE_CACHE_MAXIMUM_SIZE +from .utils import callback_update_workflow_image_cache_size + +namespace = Namespace(label=_('Workflows'), name='document_states') + +setting_workflow_image_cache_maximum_size = namespace.add_setting( + global_name='WORKFLOW_IMAGE_CACHE_MAXIMUM_SIZE', + default=DEFAULT_WORKFLOW_IMAGE_CACHE_MAXIMUM_SIZE, + help_text=_( + 'The threshold at which the WORKFLOW_IMAGE_CACHE_STORAGE_BACKEND will ' + 'start deleting the oldest workflow image cache files. Specify the ' + 'size in bytes.' + ), post_edit_function=callback_update_workflow_image_cache_size +) +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.' + ) +) diff --git a/mayan/apps/document_states/storages.py b/mayan/apps/document_states/storages.py new file mode 100644 index 0000000000..d4ab8de524 --- /dev/null +++ b/mayan/apps/document_states/storages.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals + +from mayan.apps.storage.utils import get_storage_subclass + +from .settings import ( + setting_workflowimagecache_storage, + setting_workflowimagecache_storage_arguments +) + +storage_workflowimagecache = get_storage_subclass( + dotted_path=setting_workflowimagecache_storage.value +)(**setting_workflowimagecache_storage_arguments.value) diff --git a/mayan/apps/document_states/tasks.py b/mayan/apps/document_states/tasks.py index 2156a45ead..3ec8d90acb 100644 --- a/mayan/apps/document_states/tasks.py +++ b/mayan/apps/document_states/tasks.py @@ -9,6 +9,17 @@ from mayan.celery import app logger = logging.getLogger(__name__) +@app.task() +def task_generate_workflow_image(document_state_id): + Workflow = apps.get_model( + app_label='document_states', model_name='Workflow' + ) + + workflow = Workflow.objects.get(pk=document_state_id) + + return workflow.generate_image() + + @app.task(ignore_result=True) def task_launch_all_workflows(): Document = apps.get_model(app_label='documents', model_name='Document') diff --git a/mayan/apps/document_states/templates/document_states/forms/widgets/workflow_image.html b/mayan/apps/document_states/templates/document_states/forms/widgets/workflow_image.html new file mode 100644 index 0000000000..8b2dc419cb --- /dev/null +++ b/mayan/apps/document_states/templates/document_states/forms/widgets/workflow_image.html @@ -0,0 +1,3 @@ +{% load i18n %} +{% trans 'Workflow preview' %} + diff --git a/mayan/apps/document_states/tests/literals.py b/mayan/apps/document_states/tests/literals.py index 212427d2d3..63d758efc3 100644 --- a/mayan/apps/document_states/tests/literals.py +++ b/mayan/apps/document_states/tests/literals.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +from ..literals import FIELD_TYPE_CHOICE_CHAR + TEST_INDEX_LABEL = 'test workflow index' TEST_WORKFLOW_LABEL = 'test workflow label' @@ -11,6 +13,10 @@ TEST_WORKFLOW_INSTANCE_LOG_ENTRY_COMMENT = 'test workflow instance log entry com TEST_WORKFLOW_STATE_LABEL = 'test state label' TEST_WORKFLOW_STATE_LABEL_EDITED = 'test state label edited' TEST_WORKFLOW_STATE_COMPLETION = 66 +TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT = 'test workflow transition field help test' +TEST_WORKFLOW_TRANSITION_FIELD_LABEL = 'test workflow transition field' +TEST_WORKFLOW_TRANSITION_FIELD_NAME = 'test_workflow_transition_field' +TEST_WORKFLOW_TRANSITION_FIELD_TYPE = FIELD_TYPE_CHOICE_CHAR TEST_WORKFLOW_TRANSITION_LABEL = 'test transition label' TEST_WORKFLOW_TRANSITION_LABEL_2 = 'test transition label 2' TEST_WORKFLOW_TRANSITION_LABEL_EDITED = 'test transition label edited' diff --git a/mayan/apps/document_states/tests/mixins.py b/mayan/apps/document_states/tests/mixins.py index f8ea985a1c..ff09844425 100644 --- a/mayan/apps/document_states/tests/mixins.py +++ b/mayan/apps/document_states/tests/mixins.py @@ -38,19 +38,19 @@ class WorkflowStateViewTestMixin(object): data.update(extra_data) return self.post( - viewname='document_states:setup_workflow_state_create', + viewname='document_states:workflow_template_state_create', kwargs={'pk': self.test_workflow.pk}, data=data ) def _request_test_workflow_state_delete_view(self): return self.post( - viewname='document_states:setup_workflow_state_delete', + viewname='document_states:workflow_template_state_delete', kwargs={'pk': self.test_workflow_state_1.pk} ) def _request_test_workflow_state_edit_view(self): return self.post( - viewname='document_states:setup_workflow_state_edit', + viewname='document_states:workflow_template_state_edit', kwargs={'pk': self.test_workflow_state_1.pk}, data={ 'label': TEST_WORKFLOW_STATE_LABEL_EDITED } @@ -58,7 +58,7 @@ class WorkflowStateViewTestMixin(object): def _request_test_workflow_state_list_view(self): return self.get( - viewname='document_states:setup_workflow_state_list', + viewname='document_states:workflow_template_state_list', kwargs={'pk': self.test_workflow.pk} ) @@ -120,7 +120,7 @@ class WorkflowTestMixin(object): class WorkflowTransitionViewTestMixin(object): def _request_test_workflow_transition_create_view(self): return self.post( - viewname='document_states:setup_workflow_transition_create', + viewname='document_states:workflow_template_transition_create', kwargs={'pk': self.test_workflow.pk}, data={ 'label': TEST_WORKFLOW_TRANSITION_LABEL, 'origin_state': self.test_workflow_state_1.pk, @@ -130,13 +130,13 @@ class WorkflowTransitionViewTestMixin(object): def _request_test_workflow_transition_delete_view(self): return self.post( - viewname='document_states:setup_workflow_transition_delete', + viewname='document_states:workflow_template_transition_delete', kwargs={'pk': self.test_workflow_transition.pk} ) def _request_test_workflow_transition_edit_view(self): return self.post( - viewname='document_states:setup_workflow_transition_edit', + viewname='document_states:workflow_template_transition_edit', kwargs={'pk': self.test_workflow_transition.pk}, data={ 'label': TEST_WORKFLOW_TRANSITION_LABEL_EDITED, 'origin_state': self.test_workflow_state_1.pk, @@ -146,7 +146,7 @@ class WorkflowTransitionViewTestMixin(object): def _request_test_workflow_transition_list_view(self): return self.get( - viewname='document_states:setup_workflow_transition_list', + viewname='document_states:workflow_template_transition_list', kwargs={'pk': self.test_workflow.pk} ) @@ -163,7 +163,7 @@ class WorkflowTransitionViewTestMixin(object): class WorkflowViewTestMixin(object): def _request_test_workflow_create_view(self): return self.post( - viewname='document_states:setup_workflow_create', data={ + viewname='document_states:workflow_template_create', data={ 'label': TEST_WORKFLOW_LABEL, 'internal_name': TEST_WORKFLOW_INTERNAL_NAME, } @@ -171,14 +171,14 @@ class WorkflowViewTestMixin(object): def _request_test_workflow_delete_view(self): return self.post( - viewname='document_states:setup_workflow_delete', kwargs={ + viewname='document_states:workflow_template_delete', kwargs={ 'pk': self.test_workflow.pk } ) def _request_test_workflow_edit_view(self): return self.post( - viewname='document_states:setup_workflow_edit', kwargs={ + viewname='document_states:workflow_template_edit', kwargs={ 'pk': self.test_workflow.pk, }, data={ 'label': TEST_WORKFLOW_LABEL_EDITED, @@ -188,12 +188,12 @@ class WorkflowViewTestMixin(object): def _request_test_workflow_list_view(self): return self.get( - viewname='document_states:setup_workflow_list', + viewname='document_states:workflow_template_list', ) - def _request_test_workflow_preview_view(self): + def _request_test_workflow_template_preview_view(self): return self.get( - viewname='document_states:workflow_preview', kwargs={ + viewname='document_states:workflow_template_preview', kwargs={ 'pk': self.test_workflow.pk, } ) diff --git a/mayan/apps/document_states/tests/test_api.py b/mayan/apps/document_states/tests/test_api.py index 84c55394f0..c7bb35eb9f 100644 --- a/mayan/apps/document_states/tests/test_api.py +++ b/mayan/apps/document_states/tests/test_api.py @@ -4,7 +4,7 @@ from rest_framework import status from mayan.apps.documents.permissions import permission_document_type_view from mayan.apps.documents.tests.mixins import DocumentTestMixin -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..models import Workflow from ..permissions import ( diff --git a/mayan/apps/document_states/tests/test_events.py b/mayan/apps/document_states/tests/test_events.py index 9998331665..e0d7bd7301 100644 --- a/mayan/apps/document_states/tests/test_events.py +++ b/mayan/apps/document_states/tests/test_events.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from actstream.models import Action -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from ..events import event_workflow_created, event_workflow_edited from ..models import Workflow diff --git a/mayan/apps/document_states/tests/test_indexing.py b/mayan/apps/document_states/tests/test_indexing.py index 179988ef5f..5154397c86 100644 --- a/mayan/apps/document_states/tests/test_indexing.py +++ b/mayan/apps/document_states/tests/test_indexing.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.documents.tests import GenericDocumentTestCase +from mayan.apps.documents.tests.base import GenericDocumentTestCase from mayan.apps.document_indexing.models import Index, IndexInstanceNode from ..models import Workflow diff --git a/mayan/apps/document_states/tests/test_models.py b/mayan/apps/document_states/tests/test_models.py new file mode 100644 index 0000000000..462483bea5 --- /dev/null +++ b/mayan/apps/document_states/tests/test_models.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals + +from mayan.apps.common.tests.base import BaseTestCase + +from .mixins import WorkflowTestMixin + + +class WorkflowModelTestCase(WorkflowTestMixin, BaseTestCase): + def test_workflow_template_preview(self): + self._create_test_workflow() + self.assertTrue(self.test_workflow.get_api_image_url()) diff --git a/mayan/apps/document_states/tests/test_workflow_state_action_views.py b/mayan/apps/document_states/tests/test_workflow_state_action_views.py index e79853fd90..f72ab4cdf2 100644 --- a/mayan/apps/document_states/tests/test_workflow_state_action_views.py +++ b/mayan/apps/document_states/tests/test_workflow_state_action_views.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from ..permissions import permission_workflow_edit @@ -15,7 +15,7 @@ class WorkflowStateActionViewTestCase(WorkflowStateActionTestMixin, WorkflowTest def _request_test_document_state_action_view(self): return self.get( - viewname='document_states:setup_workflow_state_action_list', + viewname='document_states:workflow_template_state_action_list', kwargs={'pk': self.test_workflow_state.pk} ) diff --git a/mayan/apps/document_states/tests/test_workflow_state_views.py b/mayan/apps/document_states/tests/test_workflow_state_views.py index 30fcb5dedf..1c2027da23 100644 --- a/mayan/apps/document_states/tests/test_workflow_state_views.py +++ b/mayan/apps/document_states/tests/test_workflow_state_views.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from ..models import WorkflowState from ..permissions import permission_workflow_edit, permission_workflow_view diff --git a/mayan/apps/document_states/tests/test_workflow_transition_views.py b/mayan/apps/document_states/tests/test_workflow_transition_views.py index 2446cecd9b..c651b1b057 100644 --- a/mayan/apps/document_states/tests/test_workflow_transition_views.py +++ b/mayan/apps/document_states/tests/test_workflow_transition_views.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import GenericViewTestCase -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from ..literals import FIELD_TYPE_CHOICE_CHAR from ..models import WorkflowTransition @@ -11,7 +11,10 @@ from ..permissions import ( ) from .literals import ( - TEST_WORKFLOW_TRANSITION_LABEL, TEST_WORKFLOW_TRANSITION_LABEL_EDITED + TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT, + TEST_WORKFLOW_TRANSITION_FIELD_LABEL, TEST_WORKFLOW_TRANSITION_FIELD_NAME, + TEST_WORKFLOW_TRANSITION_FIELD_TYPE, TEST_WORKFLOW_TRANSITION_LABEL, + TEST_WORKFLOW_TRANSITION_LABEL_EDITED ) from .mixins import ( WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin @@ -215,7 +218,7 @@ class WorkflowTransitionEventViewTestCase( ): def _request_test_workflow_transition_event_list_view(self): return self.get( - viewname='document_states:setup_workflow_transition_events', + viewname='document_states:workflow_template_transition_events', kwargs={'pk': self.test_workflow_transition.pk} ) @@ -259,7 +262,7 @@ class WorkflowTransitionFieldViewTestCase( def _request_test_workflow_transition_field_list_view(self): return self.get( - viewname='document_states:setup_workflow_transition_field_list', + viewname='document_states:workflow_template_transition_field_list', kwargs={'pk': self.test_workflow_transition.pk} ) @@ -289,7 +292,7 @@ class WorkflowTransitionFieldViewTestCase( def _request_workflow_transition_field_create_view(self): return self.post( - viewname='document_states:setup_workflow_transition_field_create', + viewname='document_states:workflow_template_transition_field_create', kwargs={'pk': self.test_workflow_transition.pk}, data={ 'field_type': TEST_WORKFLOW_TRANSITION_FIELD_TYPE, @@ -327,7 +330,7 @@ class WorkflowTransitionFieldViewTestCase( def _request_workflow_transition_field_delete_view(self): return self.post( - viewname='document_states:setup_workflow_transition_field_delete', + viewname='document_states:workflow_template_transition_field_delete', kwargs={'pk': self.test_workflow_transition_field.pk}, ) diff --git a/mayan/apps/document_states/tests/test_workflow_views.py b/mayan/apps/document_states/tests/test_workflow_views.py index 24df83a095..07af93b6fe 100644 --- a/mayan/apps/document_states/tests/test_workflow_views.py +++ b/mayan/apps/document_states/tests/test_workflow_views.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import GenericViewTestCase -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from ..models import Workflow from ..permissions import ( @@ -93,31 +93,31 @@ class WorkflowViewTestCase( self.assertEqual(response.status_code, 200) self.assertContains(response, text=self.test_workflow.label) - def test_workflow_preview_view_no_access(self): + def test_workflow_template_preview_view_no_access(self): self._create_test_workflow() - response = self._request_test_workflow_preview_view() + response = self._request_test_workflow_template_preview_view() self.assertEqual(response.status_code, 404) self.assertTrue(self.test_workflow in Workflow.objects.all()) - def test_workflow_preview_view_with_access(self): + def test_workflow_template_preview_view_with_access(self): self._create_test_workflow() self.grant_access( obj=self.test_workflow, permission=permission_workflow_view ) - response = self._request_test_workflow_preview_view() + response = self._request_test_workflow_template_preview_view() self.assertEqual(response.status_code, 200) class WorkflowToolViewTestCase(WorkflowTestMixin, GenericDocumentViewTestCase): def _request_workflow_launch_view(self): return self.post( - viewname='document_states:tool_launch_all_workflows', + viewname='document_states:tool_launch_workflows', ) - def test_tool_launch_all_workflows_view_no_permission(self): + def test_tool_launch_workflows_view_no_permission(self): self._create_test_workflow(add_document_type=True) self._create_test_workflow_states() self._create_test_workflow_transition() @@ -129,7 +129,7 @@ class WorkflowToolViewTestCase(WorkflowTestMixin, GenericDocumentViewTestCase): self.assertEqual(self.test_document.workflows.count(), 0) - def test_tool_launch_all_workflows_view_with_permission(self): + def test_tool_launch_workflows_view_with_permission(self): self._create_test_workflow(add_document_type=True) self._create_test_workflow_states() self._create_test_workflow_transition() diff --git a/mayan/apps/document_states/urls.py b/mayan/apps/document_states/urls.py index 49ccd4f19c..e184830e70 100644 --- a/mayan/apps/document_states/urls.py +++ b/mayan/apps/document_states/urls.py @@ -3,229 +3,249 @@ from __future__ import unicode_literals from django.conf.urls import url from .api_views import ( - APIDocumentTypeWorkflowListView, APIWorkflowDocumentTypeList, - APIWorkflowDocumentTypeView, APIWorkflowInstanceListView, - APIWorkflowInstanceView, APIWorkflowInstanceLogEntryListView, - APIWorkflowListView, APIWorkflowStateListView, APIWorkflowStateView, + APIDocumentTypeWorkflowRuntimeProxyListView, APIWorkflowDocumentTypeList, + APIWorkflowDocumentTypeView, APIWorkflowImageView, + APIWorkflowInstanceListView, APIWorkflowInstanceView, + APIWorkflowInstanceLogEntryListView, APIWorkflowRuntimeProxyListView, + APIWorkflowStateListView, APIWorkflowStateView, APIWorkflowTransitionListView, APIWorkflowTransitionView, APIWorkflowView ) -from .views import ( - DocumentWorkflowInstanceListView, SetupWorkflowCreateView, - SetupWorkflowDeleteView, SetupWorkflowDocumentTypesView, - SetupWorkflowEditView, SetupWorkflowListView, - SetupWorkflowStateActionCreateView, SetupWorkflowStateActionDeleteView, - SetupWorkflowStateActionEditView, SetupWorkflowStateActionListView, - SetupWorkflowStateActionSelectionView, SetupWorkflowStateCreateView, - SetupWorkflowStateDeleteView, SetupWorkflowStateEditView, - SetupWorkflowStateListView, SetupWorkflowTransitionListView, - SetupWorkflowTransitionCreateView, SetupWorkflowTransitionDeleteView, - SetupWorkflowTransitionEditView, - SetupWorkflowTransitionTriggerEventListView, ToolLaunchAllWorkflows, - WorkflowDocumentListView, WorkflowInstanceDetailView, - WorkflowImageView, WorkflowInstanceTransitionExecuteView, - WorkflowInstanceTransitionSelectView, WorkflowListView, - WorkflowPreviewView, WorkflowStateDocumentListView, WorkflowStateListView, +from .views.workflow_instance_views import ( + WorkflowInstanceDetailView, WorkflowInstanceListView, + WorkflowInstanceTransitionSelectView, + WorkflowInstanceTransitionExecuteView ) -from .views.workflow_views import ( - SetupDocumentTypeWorkflowsView, SetupWorkflowTransitionFieldCreateView, - SetupWorkflowTransitionFieldDeleteView, - SetupWorkflowTransitionFieldEditView, SetupWorkflowTransitionFieldListView +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 ) -urlpatterns_workflows = [ +urlpatterns_workflow_instances = [ url( - regex=r'^setup/workflows/$', view=SetupWorkflowListView.as_view(), - name='setup_workflow_list' + regex=r'^documents/(?P\d+)/workflows/$', + view=WorkflowInstanceListView.as_view(), + name='workflow_instance_list' ), url( - regex=r'^setup/workflows/create/$', view=SetupWorkflowCreateView.as_view(), - name='setup_workflow_create' + regex=r'^documents/workflows/(?P\d+)/$', + view=WorkflowInstanceDetailView.as_view(), + name='workflow_instance_detail' ), url( - regex=r'^setup/workflows/(?P\d+)/delete/$', - view=SetupWorkflowDeleteView.as_view(), name='setup_workflow_delete' + regex=r'^documents/workflows/(?P\d+)/transitions/select/$', + view=WorkflowInstanceTransitionSelectView.as_view(), + name='workflow_instance_transition_selection' ), url( - regex=r'^setup/workflows/(?P\d+)/edit/$', - view=SetupWorkflowEditView.as_view(), name='setup_workflow_edit' + regex=r'^documents/workflows/(?P\d+)/transitions/(?P\d+)/execute/$', + view=WorkflowInstanceTransitionExecuteView.as_view(), + name='workflow_instance_transition_execute' + ), +] + +urlpatterns_workflow_runtime_proxies = [ + url( + regex=r'workflow_runtime_proxies/$', + view=WorkflowRuntimeProxyListView.as_view(), + name='workflow_runtime_proxy_list' ), url( - regex=r'^setup/document_types/(?P\d+)/workflows/$', - view=SetupDocumentTypeWorkflowsView.as_view(), - name='document_type_workflows' + regex=r'^workflow_runtime_proxies/(?P\d+)/documents/$', + view=WorkflowRuntimeProxyDocumentListView.as_view(), + name='workflow_runtime_proxy_document_list' + ), + url( + regex=r'^workflow_runtime_proxies/(?P\d+)/states/$', + view=WorkflowRuntimeProxyStateListView.as_view(), + name='workflow_runtime_proxy_state_list' + ), + url( + regex=r'^workflow_runtime_proxies/states/(?P\d+)/documents/$', + view=WorkflowRuntimeProxyStateDocumentListView.as_view(), + name='workflow_runtime_proxy_state_document_list' ), ] urlpatterns_workflow_states = [ url( - regex=r'^setup/workflow/(?P\d+)/states/$', - view=SetupWorkflowStateListView.as_view(), - name='setup_workflow_state_list' + regex=r'^workflow_templates/(?P\d+)/states/$', + view=WorkflowTemplateStateListView.as_view(), + name='workflow_template_state_list' ), url( - regex=r'^setup/workflow/(?P\d+)/states/create/$', - view=SetupWorkflowStateCreateView.as_view(), - name='setup_workflow_state_create' + regex=r'^workflow_templates/(?P\d+)/states/create/$', + view=WorkflowTemplateStateCreateView.as_view(), + name='workflow_template_state_create' ), url( - regex=r'^setup/workflow/state/(?P\d+)/delete/$', - view=SetupWorkflowStateDeleteView.as_view(), - name='setup_workflow_state_delete' + regex=r'^workflow_templates/states/(?P\d+)/delete/$', + view=WorkflowTemplateStateDeleteView.as_view(), + name='workflow_template_state_delete' ), url( - regex=r'^setup/workflow/state/(?P\d+)/edit/$', - view=SetupWorkflowStateEditView.as_view(), - name='setup_workflow_state_edit' + regex=r'^workflow_templates/states/(?P\d+)/edit/$', + view=WorkflowTemplateStateEditView.as_view(), + name='workflow_template_state_edit' + ), +] + +urlpatterns_workflow_state_actions = [ + url( + regex=r'^workflow_templates/states/(?P\d+)/actions/$', + view=WorkflowTemplateStateActionListView.as_view(), + name='workflow_template_state_action_list' + ), + url( + regex=r'^workflow_templates/states/(?P\d+)/actions/selection/$', + view=WorkflowTemplateStateActionSelectionView.as_view(), + name='workflow_template_state_action_selection' + ), + url( + regex=r'^workflow_templates/states/(?P\d+)/actions/(?P[a-zA-Z0-9_.]+)/create/$', + view=WorkflowTemplateStateActionCreateView.as_view(), + name='workflow_template_state_action_create' + ), + url( + regex=r'^workflow_templates/states/actions/(?P\d+)/delete/$', + view=WorkflowTemplateStateActionDeleteView.as_view(), + name='workflow_template_state_action_delete' + ), + url( + regex=r'^workflow_templates/states/actions/(?P\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\d+)/delete/$', + view=WorkflowTemplateDeleteView.as_view(), name='workflow_template_delete' + ), + url( + regex=r'^workflow_templates/(?P\d+)/document_types/$', + view=WorkflowTemplateDocumentTypesView.as_view(), + name='workflow_template_document_types' + ), + url( + regex=r'^workflow_templates/(?P\d+)/edit/$', + view=WorkflowTemplateEditView.as_view(), name='workflow_template_edit' + ), + url( + regex=r'^workflow_templates/(?P\d+)/preview/$', + view=WorkflowTemplatePreviewView.as_view(), + name='workflow_template_preview' + ), + url( + regex=r'^document_types/(?P\d+)/workflow_templates/$', + view=DocumentTypeWorkflowTemplatesView.as_view(), + name='document_type_workflow_templates' + ), +] +urlpatterns_workflow_transitions = [ + url( + regex=r'^workflow_templates/(?P\d+)/transitions/$', + view=WorkflowTemplateTransitionListView.as_view(), + name='workflow_template_transition_list' + ), + url( + regex=r'^workflow_templates/(?P\d+)/transitions/create/$', + view=WorkflowTemplateTransitionCreateView.as_view(), + name='workflow_template_transition_create' + ), + url( + regex=r'^workflow_templates/(?P\d+)/transitions/events/$', + view=WorkflowTemplateTransitionTriggerEventListView.as_view(), + name='workflow_template_transition_events' + ), + url( + regex=r'^workflow_templates/transitions/(?P\d+)/delete/$', + view=WorkflowTemplateTransitionDeleteView.as_view(), + name='workflow_template_transition_delete' + ), + url( + regex=r'^workflow_templates/transitions/(?P\d+)/edit/$', + view=WorkflowTemplateTransitionEditView.as_view(), + name='workflow_template_transition_edit' ), ] urlpatterns_workflow_transition_fields = [ url( - regex=r'^setup/workflows/transitions/(?P\d+)/fields/create/$', - view=SetupWorkflowTransitionFieldCreateView.as_view(), - name='setup_workflow_transition_field_create' + regex=r'^workflow_templates/transitions/(?P\d+)/fields/create/$', + view=WorkflowTemplateTransitionFieldCreateView.as_view(), + name='workflow_template_transition_field_create' ), url( - regex=r'^setup/workflows/transitions/(?P\d+)/fields/$', - view=SetupWorkflowTransitionFieldListView.as_view(), - name='setup_workflow_transition_field_list' + regex=r'^workflow_templates/transitions/(?P\d+)/fields/$', + view=WorkflowTemplateTransitionFieldListView.as_view(), + name='workflow_template_transition_field_list' ), url( - regex=r'^setup/workflows/transitions/fields/(?P\d+)/delete/$', - view=SetupWorkflowTransitionFieldDeleteView.as_view(), - name='setup_workflow_transition_field_delete' + regex=r'^workflow_templates/transitions/fields/(?P\d+)/delete/$', + view=WorkflowTemplateTransitionFieldDeleteView.as_view(), + name='workflow_template_transition_field_delete' ), url( - regex=r'^setup/workflows/transitions/fields/(?P\d+)/edit/$', - view=SetupWorkflowTransitionFieldEditView.as_view(), - name='setup_workflow_transition_field_edit' + regex=r'^workflow_templates/transitions/fields/(?P\d+)/edit/$', + view=WorkflowTemplateTransitionFieldEditView.as_view(), + name='workflow_template_transition_field_edit' ), ] -urlpatterns = [ +urlpatterns_tools = [ url( - regex=r'^document/(?P\d+)/workflows/$', - view=DocumentWorkflowInstanceListView.as_view(), - name='document_workflow_instance_list' - ), - url( - regex=r'^document/workflows/(?P\d+)/$', - view=WorkflowInstanceDetailView.as_view(), - name='workflow_instance_detail' - ), - url( - regex=r'^document/workflows/(?P\d+)/transitions/select/$', - view=WorkflowInstanceTransitionSelectView.as_view(), - name='workflow_instance_transition_selection' - ), - url( - regex=r'^document/workflows/(?P\d+)/transitions/(?P\d+)/execute/$', - view=WorkflowInstanceTransitionExecuteView.as_view(), - name='workflow_instance_transition_execute' - ), - url( - regex=r'^setup/workflow/(?P\d+)/documents/$', - view=WorkflowDocumentListView.as_view(), - name='setup_workflow_document_list' - ), - url( - regex=r'^setup/workflow/(?P\d+)/document_types/$', - view=SetupWorkflowDocumentTypesView.as_view(), - name='setup_workflow_document_types' - ), - url( - regex=r'^setup/workflow/(?P\d+)/transitions/$', - view=SetupWorkflowTransitionListView.as_view(), - name='setup_workflow_transition_list' - ), - url( - regex=r'^setup/workflow/(?P\d+)/transitions/create/$', - view=SetupWorkflowTransitionCreateView.as_view(), - name='setup_workflow_transition_create' - ), - url( - regex=r'^setup/workflow/transitions/(?P\d+)/events/$', - view=SetupWorkflowTransitionTriggerEventListView.as_view(), - name='setup_workflow_transition_events' - ), - url( - regex=r'^setup/workflow/state/(?P\d+)/actions/$', - view=SetupWorkflowStateActionListView.as_view(), - name='setup_workflow_state_action_list' - ), - url( - regex=r'^setup/workflow/state/(?P\d+)/actions/selection/$', - view=SetupWorkflowStateActionSelectionView.as_view(), - name='setup_workflow_state_action_selection' - ), - url( - regex=r'^setup/workflow/state/(?P\d+)/actions/(?P[a-zA-Z0-9_.]+)/create/$', - view=SetupWorkflowStateActionCreateView.as_view(), - name='setup_workflow_state_action_create' - ), - url( - regex=r'^setup/workflow/state/actions/(?P\d+)/delete/$', - view=SetupWorkflowStateActionDeleteView.as_view(), - name='setup_workflow_state_action_delete' - ), - url( - regex=r'^setup/workflow/state/actions/(?P\d+)/edit/$', - view=SetupWorkflowStateActionEditView.as_view(), - name='setup_workflow_state_action_edit' - ), - url( - regex=r'^setup/workflow/transitions/(?P\d+)/delete/$', - view=SetupWorkflowTransitionDeleteView.as_view(), - name='setup_workflow_transition_delete' - ), - url( - regex=r'^setup/workflow/transitions/(?P\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\d+)/documents/$', - view=WorkflowDocumentListView.as_view(), - name='workflow_document_list' - ), - url( - regex=r'^(?P\d+)/states/$', - view=WorkflowStateListView.as_view(), - name='workflow_state_list' - ), - url( - regex=r'^(?P\d+)/image/$', - view=WorkflowImageView.as_view(), - name='workflow_image' - ), - url( - regex=r'^(?P\d+)/preview/$', - view=WorkflowPreviewView.as_view(), - name='workflow_preview' - ), - url( - regex=r'^state/(?P\d+)/documents/$', - view=WorkflowStateDocumentListView.as_view(), - name='workflow_state_document_list' + regex=r'^tools/workflows/launch/$', + view=ToolLaunchWorkflows.as_view(), + name='tool_launch_workflows' ), ] -urlpatterns.extend(urlpatterns_workflows) + +urlpatterns = [] +urlpatterns.extend(urlpatterns_tools) +urlpatterns.extend(urlpatterns_workflow_instances) +urlpatterns.extend(urlpatterns_workflow_runtime_proxies) urlpatterns.extend(urlpatterns_workflow_states) +urlpatterns.extend(urlpatterns_workflow_state_actions) +urlpatterns.extend(urlpatterns_workflow_templates) +urlpatterns.extend(urlpatterns_workflow_transitions) urlpatterns.extend(urlpatterns_workflow_transition_fields) api_urls = [ url( - regex=r'^workflows/$', view=APIWorkflowListView.as_view(), + regex=r'^workflows/$', view=APIWorkflowRuntimeProxyListView.as_view(), name='workflow-list' ), url( @@ -242,6 +262,10 @@ api_urls = [ view=APIWorkflowDocumentTypeView.as_view(), name='workflow-document-type-detail' ), + url( + regex=r'^workflows/(?P\d+)/image/$', + name='workflow-image', view=APIWorkflowImageView.as_view() + ), url( regex=r'^workflows/(?P[0-9]+)/states/$', view=APIWorkflowStateListView.as_view(), name='workflowstate-list' @@ -277,7 +301,7 @@ api_urls = [ ), url( regex=r'^document_types/(?P[0-9]+)/workflows/$', - view=APIDocumentTypeWorkflowListView.as_view(), + view=APIDocumentTypeWorkflowRuntimeProxyListView.as_view(), name='documenttype-workflow-list' ), ] diff --git a/mayan/apps/document_states/utils.py b/mayan/apps/document_states/utils.py new file mode 100644 index 0000000000..252ffd5165 --- /dev/null +++ b/mayan/apps/document_states/utils.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals + +from django.apps import apps + +from .literals import WORKFLOW_IMAGE_CACHE_NAME + + +def callback_update_workflow_image_cache_size(setting): + Cache = apps.get_model(app_label='file_caching', model_name='Cache') + cache = Cache.objects.get(name=WORKFLOW_IMAGE_CACHE_NAME) + cache.maximum_size = setting.value + cache.save() diff --git a/mayan/apps/document_states/views/__init__.py b/mayan/apps/document_states/views/__init__.py index d6e36751b2..e69de29bb2 100644 --- a/mayan/apps/document_states/views/__init__.py +++ b/mayan/apps/document_states/views/__init__.py @@ -1,3 +0,0 @@ -from .workflow_instance_views import * # NOQA -from .workflow_proxy_views import * # NOQA -from .workflow_views import * # NOQA diff --git a/mayan/apps/document_states/views/workflow_instance_views.py b/mayan/apps/document_states/views/workflow_instance_views.py index bed57fdcbc..6fcfd38fbd 100644 --- a/mayan/apps/document_states/views/workflow_instance_views.py +++ b/mayan/apps/document_states/views/workflow_instance_views.py @@ -14,20 +14,14 @@ 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_list +from ..icons import icon_workflow_instance_detail, icon_workflow_template_list from ..links import link_workflow_instance_transition from ..literals import FIELD_TYPE_MAPPING, WIDGET_CLASS_MAPPING from ..models import WorkflowInstance from ..permissions import permission_workflow_view -__all__ = ( - 'DocumentWorkflowInstanceListView', 'WorkflowInstanceDetailView', - 'WorkflowInstanceTransitionSelectView', - 'WorkflowInstanceTransitionExecuteView' -) - -class DocumentWorkflowInstanceListView(SingleObjectListView): +class WorkflowInstanceListView(SingleObjectListView): def dispatch(self, request, *args, **kwargs): AccessControlList.objects.check_access( obj=self.get_document(), permissions=(permission_workflow_view,), @@ -35,7 +29,7 @@ class DocumentWorkflowInstanceListView(SingleObjectListView): ) return super( - DocumentWorkflowInstanceListView, self + WorkflowInstanceListView, self ).dispatch(request, *args, **kwargs) def get_document(self): @@ -44,7 +38,7 @@ class DocumentWorkflowInstanceListView(SingleObjectListView): def get_extra_context(self): return { 'hide_link': True, - 'no_results_icon': icon_workflow_list, + 'no_results_icon': icon_workflow_template_list, 'no_results_text': _( 'Assign workflows to the document type of this document ' 'to have this document execute those workflows. ' diff --git a/mayan/apps/document_states/views/workflow_proxy_views.py b/mayan/apps/document_states/views/workflow_proxy_views.py index 21efa2bb9e..77bd864ea4 100644 --- a/mayan/apps/document_states/views/workflow_proxy_views.py +++ b/mayan/apps/document_states/views/workflow_proxy_views.py @@ -9,18 +9,13 @@ from mayan.apps.common.generics import SingleObjectListView from mayan.apps.documents.models import Document from mayan.apps.documents.views import DocumentListView -from ..icons import icon_workflow_list -from ..links import link_setup_workflow_create, link_setup_workflow_state_create +from ..icons import icon_workflow_template_list +from ..links import link_workflow_template_create, link_workflow_template_state_create from ..models import WorkflowRuntimeProxy, WorkflowStateRuntimeProxy from ..permissions import permission_workflow_view -__all__ = ( - 'WorkflowDocumentListView', 'WorkflowListView', - 'WorkflowStateDocumentListView', 'WorkflowStateListView' -) - -class WorkflowDocumentListView(DocumentListView): +class WorkflowRuntimeProxyDocumentListView(DocumentListView): def dispatch(self, request, *args, **kwargs): self.workflow = get_object_or_404( klass=WorkflowRuntimeProxy, pk=self.kwargs['pk'] @@ -32,14 +27,14 @@ class WorkflowDocumentListView(DocumentListView): ) return super( - WorkflowDocumentListView, self + WorkflowRuntimeProxyDocumentListView, self ).dispatch(request, *args, **kwargs) def get_document_queryset(self): return Document.objects.filter(workflows__workflow=self.workflow) def get_extra_context(self): - context = super(WorkflowDocumentListView, self).get_extra_context() + context = super(WorkflowRuntimeProxyDocumentListView, self).get_extra_context() context.update( { 'no_results_text': _( @@ -56,14 +51,14 @@ class WorkflowDocumentListView(DocumentListView): return context -class WorkflowListView(SingleObjectListView): +class WorkflowRuntimeProxyListView(SingleObjectListView): object_permission = permission_workflow_view def get_extra_context(self): return { 'hide_object': True, - 'no_results_icon': icon_workflow_list, - 'no_results_main_link': link_setup_workflow_create.resolve( + 'no_results_icon': icon_workflow_template_list, + 'no_results_main_link': link_workflow_template_create.resolve( context=RequestContext(request=self.request) ), 'no_results_text': _( @@ -79,13 +74,13 @@ class WorkflowListView(SingleObjectListView): return WorkflowRuntimeProxy.objects.all() -class WorkflowStateDocumentListView(DocumentListView): +class WorkflowRuntimeProxyStateDocumentListView(DocumentListView): def get_document_queryset(self): return self.get_workflow_state().get_documents() def get_extra_context(self): workflow_state = self.get_workflow_state() - context = super(WorkflowStateDocumentListView, self).get_extra_context() + context = super(WorkflowRuntimeProxyStateDocumentListView, self).get_extra_context() context.update( { 'object': workflow_state, @@ -118,7 +113,7 @@ class WorkflowStateDocumentListView(DocumentListView): return workflow_state -class WorkflowStateListView(SingleObjectListView): +class WorkflowRuntimeProxyStateListView(SingleObjectListView): def dispatch(self, request, *args, **kwargs): AccessControlList.objects.check_access( obj=self.get_workflow(), permissions=(permission_workflow_view,), @@ -126,14 +121,14 @@ class WorkflowStateListView(SingleObjectListView): ) return super( - WorkflowStateListView, self + WorkflowRuntimeProxyStateListView, self ).dispatch(request, *args, **kwargs) def get_extra_context(self): return { 'hide_link': True, 'hide_object': True, - 'no_results_main_link': link_setup_workflow_state_create.resolve( + 'no_results_main_link': link_workflow_template_state_create.resolve( context=RequestContext( request=self.request, dict_={'object': self.get_workflow()} ) diff --git a/mayan/apps/document_states/views/workflow_template_state_views.py b/mayan/apps/document_states/views/workflow_template_state_views.py new file mode 100644 index 0000000000..ab846c15b2 --- /dev/null +++ b/mayan/apps/document_states/views/workflow_template_state_views.py @@ -0,0 +1,303 @@ +from __future__ import absolute_import, unicode_literals + +from django.http import Http404, HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.template import RequestContext +from django.urls import reverse +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.common.generics import ( + FormView, SingleObjectCreateView, SingleObjectDeleteView, + SingleObjectDynamicFormCreateView, SingleObjectDynamicFormEditView, + SingleObjectEditView, SingleObjectListView +) +from mayan.apps.common.mixins import ExternalObjectMixin + +from ..classes import WorkflowAction +from ..forms import ( + WorkflowActionSelectionForm, WorkflowStateActionDynamicForm, + WorkflowStateForm +) +from ..icons import icon_workflow_state, icon_workflow_state_action +from ..links import ( + link_workflow_template_state_create, + link_workflow_template_state_action_selection, +) +from ..models import Workflow, WorkflowState, WorkflowStateAction +from ..permissions import permission_workflow_edit, permission_workflow_view + + +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 diff --git a/mayan/apps/document_states/views/workflow_template_transition_views.py b/mayan/apps/document_states/views/workflow_template_transition_views.py new file mode 100644 index 0000000000..f33444751e --- /dev/null +++ b/mayan/apps/document_states/views/workflow_template_transition_views.py @@ -0,0 +1,347 @@ +from __future__ import absolute_import, unicode_literals + +from django.contrib import messages +from django.template import RequestContext +from django.urls import reverse +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.common.generics import ( + FormView, SingleObjectCreateView, SingleObjectDeleteView, + SingleObjectEditView, SingleObjectListView +) +from mayan.apps.common.mixins import ExternalObjectMixin +from mayan.apps.events.classes import EventType +from mayan.apps.events.models import StoredEventType + +from ..forms import ( + WorkflowTransitionForm, WorkflowTransitionTriggerEventRelationshipFormSet +) +from ..icons import icon_workflow_transition, icon_workflow_transition_field +from ..links import ( + link_workflow_template_transition_create, + link_workflow_template_transition_field_create, +) +from ..models import Workflow, WorkflowTransition, WorkflowTransitionField +from ..permissions import permission_workflow_edit, permission_workflow_view + + +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() diff --git a/mayan/apps/document_states/views/workflow_template_views.py b/mayan/apps/document_states/views/workflow_template_views.py new file mode 100644 index 0000000000..71f61245d9 --- /dev/null +++ b/mayan/apps/document_states/views/workflow_template_views.py @@ -0,0 +1,238 @@ +from __future__ import absolute_import, unicode_literals + +from django.contrib import messages +from django.db import transaction +from django.template import RequestContext +from django.urls import reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.common.generics import ( + AddRemoveView, ConfirmView, SingleObjectCreateView, SingleObjectDeleteView, + SingleObjectDetailView, SingleObjectEditView, SingleObjectListView +) +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 ..events import event_workflow_edited +from ..forms import WorkflowForm, WorkflowPreviewForm +from ..icons import icon_workflow_template_list +from ..links import link_workflow_template_create +from ..models import Workflow +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 + ) diff --git a/mayan/apps/document_states/views/workflow_views.py b/mayan/apps/document_states/views/workflow_views.py deleted file mode 100644 index d69c0ad774..0000000000 --- a/mayan/apps/document_states/views/workflow_views.py +++ /dev/null @@ -1,895 +0,0 @@ -from __future__ import absolute_import, unicode_literals - -from django.contrib import messages -from django.core.files.base import ContentFile -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, - SingleObjectDownloadView, 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_list, icon_workflow_state, icon_workflow_state_action, - icon_workflow_transition, icon_workflow_transition_field -) -from ..links import ( - link_setup_workflow_create, link_setup_workflow_state_create, - link_setup_workflow_state_action_selection, - link_setup_workflow_transition_create, - link_setup_workflow_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 - -__all__ = ( - 'WorkflowImageView', 'WorkflowPreviewView', - 'SetupWorkflowListView', 'SetupWorkflowCreateView', 'SetupWorkflowEditView', - 'SetupWorkflowDeleteView', 'SetupWorkflowDocumentTypesView', - 'SetupWorkflowStateActionCreateView', 'SetupWorkflowStateActionDeleteView', - 'SetupWorkflowStateActionEditView', 'SetupWorkflowStateActionListView', - 'SetupWorkflowStateActionSelectionView', 'SetupWorkflowStateCreateView', - 'SetupWorkflowStateDeleteView', 'SetupWorkflowStateEditView', - 'SetupWorkflowStateListView', 'SetupWorkflowTransitionCreateView', - 'SetupWorkflowTransitionDeleteView', 'SetupWorkflowTransitionEditView', - 'SetupWorkflowTransitionListView', - 'SetupWorkflowTransitionTriggerEventListView', 'ToolLaunchAllWorkflows' -) - - -class SetupDocumentTypeWorkflowsView(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 SetupWorkflowListView(SingleObjectListView): - model = Workflow - object_permission = permission_workflow_view - - def get_extra_context(self): - return { - 'hide_object': True, - 'no_results_icon': icon_workflow_list, - 'no_results_main_link': link_setup_workflow_create.resolve( - 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 SetupWorkflowCreateView(SingleObjectCreateView): - extra_context = {'title': _('Create workflow')} - form_class = WorkflowForm - model = Workflow - post_action_redirect = reverse_lazy( - viewname='document_states:setup_workflow_list' - ) - view_permission = permission_workflow_create - - def get_save_extra_data(self): - return {'_user': self.request.user} - - -class SetupWorkflowDeleteView(SingleObjectDeleteView): - model = Workflow - object_permission = permission_workflow_delete - post_action_redirect = reverse_lazy( - viewname='document_states:setup_workflow_list' - ) - - def get_extra_context(self): - return { - 'title': _( - 'Delete workflow: %s?' - ) % self.object, - } - - -class SetupWorkflowEditView(SingleObjectEditView): - form_class = WorkflowForm - model = Workflow - object_permission = permission_workflow_edit - post_action_redirect = reverse_lazy( - viewname='document_states:setup_workflow_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 SetupWorkflowDocumentTypesView(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() - - -# Workflow state actions - - -class SetupWorkflowStateActionCreateView(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:setup_workflow_state_action_list', - kwargs={'pk': self.get_object().pk} - ) - - -class SetupWorkflowStateActionDeleteView(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:setup_workflow_state_action_list', - kwargs={'pk': self.get_object().state.pk} - ) - - -class SetupWorkflowStateActionEditView(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:setup_workflow_state_action_list', - kwargs={'pk': self.get_object().state.pk} - ) - - -class SetupWorkflowStateActionListView(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_setup_workflow_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 SetupWorkflowStateActionSelectionView(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:setup_workflow_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']) - - -# Workflow states - - -class SetupWorkflowStateCreateView(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:setup_workflow_state_list', - kwargs={'pk': self.kwargs['pk']} - ) - - def get_workflow(self): - return self.external_object - - -class SetupWorkflowStateDeleteView(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:setup_workflow_state_list', - kwargs={'pk': self.get_object().workflow.pk} - ) - - -class SetupWorkflowStateEditView(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:setup_workflow_state_list', - kwargs={'pk': self.get_object().workflow.pk} - ) - - -class SetupWorkflowStateListView(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_setup_workflow_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 - - -# Transitions - - -class SetupWorkflowTransitionCreateView(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( - SetupWorkflowTransitionCreateView, 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:setup_workflow_transition_list', - kwargs={'pk': self.kwargs['pk']} - ) - - def get_workflow(self): - return self.external_object - - -class SetupWorkflowTransitionDeleteView(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:setup_workflow_transition_list', - kwargs={'pk': self.get_object().workflow.pk} - ) - - -class SetupWorkflowTransitionEditView(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( - SetupWorkflowTransitionEditView, self - ).get_form_kwargs() - kwargs['workflow'] = self.get_object().workflow - return kwargs - - def get_success_url(self): - return reverse( - viewname='document_states:setup_workflow_transition_list', - kwargs={'pk': self.get_object().workflow.pk} - ) - - -class SetupWorkflowTransitionListView(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_setup_workflow_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 SetupWorkflowTransitionTriggerEventListView(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( - SetupWorkflowTransitionTriggerEventListView, 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( - SetupWorkflowTransitionTriggerEventListView, 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:setup_workflow_transition_list', - kwargs={'pk': self.get_object().workflow.pk} - ) - - -# Transition fields - -class SetupWorkflowTransitionFieldCreateView(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:setup_workflow_transition_field_list', - kwargs={'pk': self.external_object.pk} - ) - - -class SetupWorkflowTransitionFieldDeleteView(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:setup_workflow_transition_field_list', - kwargs={'pk': self.object.transition.pk} - ) - - -class SetupWorkflowTransitionFieldEditView(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:setup_workflow_transition_field_list', - kwargs={'pk': self.object.transition.pk} - ) - - -class SetupWorkflowTransitionFieldListView(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_setup_workflow_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() - - -class ToolLaunchAllWorkflows(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 - ) - - -class WorkflowImageView(SingleObjectDownloadView): - attachment = False - model = Workflow - object_permission = permission_workflow_view - - def get_file(self): - workflow = self.get_object() - return ContentFile(workflow.render(), name=workflow.label) - - def get_mimetype(self): - return 'image' - - -class WorkflowPreviewView(SingleObjectDetailView): - form_class = WorkflowPreviewForm - model = Workflow - object_permission = permission_workflow_view - - def get_extra_context(self): - return { - 'hide_labels': True, - 'title': _('Preview of: %s') % self.get_object() - } diff --git a/mayan/apps/document_states/widgets.py b/mayan/apps/document_states/widgets.py index 87ae0a5f4b..a1ccdb57d7 100644 --- a/mayan/apps/document_states/widgets.py +++ b/mayan/apps/document_states/widgets.py @@ -1,38 +1,12 @@ from __future__ import unicode_literals from django import forms -from django.urls import reverse -from django.utils.html import format_html_join, mark_safe -from django.utils.translation import ugettext_lazy as _ - - -def widget_transition_events(transition): - return format_html_join( - sep='\n', format_string='
    {}
    ', args_generator=( - ( - transition_trigger.event_type.label, - ) for transition_trigger in transition.trigger_events.all() - ) - ) - - -def widget_workflow_diagram(workflow): - return mark_safe( - '{}'.format( - _('Workflow preview'), reverse( - viewname='document_states:workflow_image', kwargs={ - 'pk': workflow.pk - } - ) - ) - ) class WorkflowImageWidget(forms.widgets.Widget): - def render(self, name, value, attrs=None): - if value: - output = [] - output.append(widget_workflow_diagram(value)) - return mark_safe(''.join(output)) - else: - return '' + template_name = 'document_states/forms/widgets/workflow_image.html' + + def format_value(self, value): + if value == '' or value is None: + return None + return value diff --git a/mayan/apps/document_states/workflow_actions.py b/mayan/apps/document_states/workflow_actions.py index 1e1eb5fe01..98e69978eb 100644 --- a/mayan/apps/document_states/workflow_actions.py +++ b/mayan/apps/document_states/workflow_actions.py @@ -11,7 +11,6 @@ from django.utils.translation import ugettext_lazy as _ from .classes import WorkflowAction from .exceptions import WorkflowStateActionError -__all__ = ('DocumentPropertiesEditAction', 'HTTPPostAction',) logger = logging.getLogger(__name__) DEFAULT_TIMEOUT = 4 # 4 seconds @@ -37,7 +36,7 @@ class DocumentPropertiesEditAction(WorkflowAction): }, } field_order = ('document_label', 'document_description') - label = _('Modify the properties of the document') + label = _('Modify document properties') widgets = { 'document_description': { 'class': 'django.forms.widgets.Textarea', 'kwargs': { diff --git a/mayan/apps/documents/api_views.py b/mayan/apps/documents/api_views.py index c42a8d50c2..4d17038722 100644 --- a/mayan/apps/documents/api_views.py +++ b/mayan/apps/documents/api_views.py @@ -36,7 +36,6 @@ from .serializers import ( WritableDocumentTypeSerializer, WritableDocumentVersionSerializer ) from .settings import settings_document_page_image_cache_time -from .storages import storage_documentimagecache from .tasks import task_generate_document_page_image logger = logging.getLogger(__name__) @@ -165,7 +164,7 @@ class APIDocumentPageImageView(generics.RetrieveAPIView): AccessControlList.objects.check_access( obj=document, permissions=(permission_required,), - user=self.request.user, manager=Document.passthrough + user=self.request.user ) return document @@ -175,7 +174,7 @@ class APIDocumentPageImageView(generics.RetrieveAPIView): ) def get_queryset(self): - return self.get_document_version().pages.all() + return self.get_document_version().pages_all.all() def get_serializer(self, *args, **kwargs): return None @@ -197,19 +196,27 @@ class APIDocumentPageImageView(generics.RetrieveAPIView): if rotation: rotation = int(rotation) + maximum_layer_order = request.GET.get('maximum_layer_order') + if maximum_layer_order: + maximum_layer_order = int(maximum_layer_order) + task = task_generate_document_page_image.apply_async( kwargs=dict( document_page_id=self.get_object().pk, width=width, - height=height, zoom=zoom, rotation=rotation + height=height, zoom=zoom, rotation=rotation, + maximum_layer_order=maximum_layer_order, + user_id=request.user.pk ) ) cache_filename = task.get(timeout=DOCUMENT_IMAGE_TASK_TIMEOUT) - with storage_documentimagecache.open(cache_filename) as file_object: + cache_file = self.get_object().cache_partition.get_file(filename=cache_filename) + with cache_file.open() as file_object: response = HttpResponse(file_object.read(), content_type='image') if '_hash' in request.GET: patch_cache_control( - response, max_age=settings_document_page_image_cache_time.value + response=response, + max_age=settings_document_page_image_cache_time.value ) return response diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py index 0032127a92..09e85928c7 100644 --- a/mayan/apps/documents/apps.py +++ b/mayan/apps/documents/apps.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, unicode_literals -from django.db.models.signals import post_delete +from django.db.models.signals import post_delete, post_migrate from django.utils.translation import ugettext_lazy as _ from mayan.apps.acls.classes import ModelPermission @@ -43,11 +43,11 @@ from .events import ( event_document_view ) from .handlers import ( - handler_create_default_document_type, handler_remove_empty_duplicates_lists, - handler_scan_duplicates_for, + handler_create_default_document_type, handler_create_document_cache, + handler_remove_empty_duplicates_lists, handler_scan_duplicates_for ) from .links import ( - link_clear_image_cache, link_document_clear_transformations, + link_document_clear_transformations, link_document_clone_transformations, link_document_delete, link_document_document_type_edit, link_document_download, link_document_duplicates_list, link_document_edit, @@ -60,6 +60,8 @@ from .links import ( link_document_multiple_download, link_document_multiple_favorites_add, link_document_multiple_favorites_remove, link_document_multiple_restore, link_document_multiple_trash, link_document_multiple_update_page_count, + link_document_page_disable, link_document_page_multiple_disable, + link_document_page_enable, link_document_page_multiple_enable, link_document_page_navigation_first, link_document_page_navigation_last, link_document_page_navigation_next, link_document_page_navigation_previous, link_document_page_return, link_document_page_rotate_left, @@ -100,6 +102,10 @@ from .widgets import ( ) +def is_document_page_enabled(context): + return context['object'].enabled + + class DocumentsApp(MayanAppConfig): app_namespace = 'documents' app_url = 'documents' @@ -214,12 +220,21 @@ class DocumentsApp(MayanAppConfig): ModelPermission.register_inheritance( model=Document, related='document_type', ) + ModelPermission.register_manager( + model=Document, manager_name='passthrough' + ) ModelPermission.register_inheritance( model=DocumentPage, related='document_version__document', ) + ModelPermission.register_manager( + model=DocumentPage, manager_name='passthrough' + ) ModelPermission.register_inheritance( model=DocumentPageResult, related='document_version__document', ) + ModelPermission.register_manager( + model=DocumentPageResult, manager_name='passthrough' + ) ModelPermission.register_inheritance( model=DocumentTypeFilename, related='document_type', ) @@ -262,13 +277,21 @@ class DocumentsApp(MayanAppConfig): # DocumentPage SourceColumn( attribute='get_label', is_identifier=True, - is_object_absolute_url=True, source=DocumentPage + is_object_absolute_url=True, source=DocumentPage, + widget_condition=is_document_page_enabled ) SourceColumn( func=lambda context: document_page_thumbnail_widget.render( instance=context['object'] ), label=_('Thumbnail'), source=DocumentPage ) + SourceColumn( + attribute='enabled', include_label=True, source=DocumentPage, + widget=TwoStateWidget + ) + SourceColumn( + attribute='page_number', include_label=True, source=DocumentPage + ) SourceColumn( attribute='get_label', is_identifier=True, @@ -377,7 +400,7 @@ class DocumentsApp(MayanAppConfig): menu_setup.bind_links(links=(link_document_type_setup,)) menu_tools.bind_links( - links=(link_clear_image_cache, link_duplicated_document_scan) + links=(link_duplicated_document_scan,) ) # Document type links @@ -503,6 +526,16 @@ class DocumentsApp(MayanAppConfig): link_document_page_navigation_last ), sources=(DocumentPage,) ) + menu_multi_item.bind_links( + links=( + link_document_page_multiple_disable, + link_document_page_multiple_enable + ), sources=(DocumentPage,) + ) + menu_object.bind_links( + links=(link_document_page_disable, link_document_page_enable), + sources=(DocumentPage,) + ) menu_list_facet.bind_links( links=(link_transformation_list,), sources=(DocumentPage,) ) @@ -527,6 +560,10 @@ class DocumentsApp(MayanAppConfig): dispatch_uid='handler_create_default_document_type', receiver=handler_create_default_document_type ) + post_migrate.connect( + dispatch_uid='documents_handler_create_document_cache', + receiver=handler_create_document_cache, + ) post_version_upload.connect( dispatch_uid='handler_scan_duplicates_for', receiver=handler_scan_duplicates_for diff --git a/mayan/apps/documents/forms/document_type_forms.py b/mayan/apps/documents/forms/document_type_forms.py index e2d75f8d61..7484cf23c3 100644 --- a/mayan/apps/documents/forms/document_type_forms.py +++ b/mayan/apps/documents/forms/document_type_forms.py @@ -41,7 +41,8 @@ class DocumentTypeFilteredSelectForm(forms.Form): self.fields['document_type'] = field_class( help_text=help_text, label=_('Document type'), queryset=queryset, required=True, - widget=widget_class(attrs={'class': 'select2', 'size': 10}), **extra_kwargs + widget=widget_class(attrs={'class': 'select2', 'size': 10}), + **extra_kwargs ) diff --git a/mayan/apps/documents/handlers.py b/mayan/apps/documents/handlers.py index 597a38c40d..a033ec609f 100644 --- a/mayan/apps/documents/handlers.py +++ b/mayan/apps/documents/handlers.py @@ -1,8 +1,13 @@ from __future__ import unicode_literals from django.apps import apps +from django.utils.translation import ugettext_lazy as _ -from .literals import DEFAULT_DOCUMENT_TYPE_LABEL +from .literals import ( + DEFAULT_DOCUMENT_TYPE_LABEL, DOCUMENT_CACHE_STORAGE_INSTANCE_PATH, + DOCUMENT_IMAGES_CACHE_NAME +) +from .settings import setting_document_cache_maximum_size from .signals import post_initial_document_type from .tasks import task_clean_empty_duplicate_lists, task_scan_duplicates_for @@ -21,6 +26,17 @@ def handler_create_default_document_type(sender, **kwargs): ) +def handler_create_document_cache(sender, **kwargs): + Cache = apps.get_model(app_label='file_caching', model_name='Cache') + Cache.objects.update_or_create( + defaults={ + 'label': _('Document images'), + 'storage_instance_path': DOCUMENT_CACHE_STORAGE_INSTANCE_PATH, + 'maximum_size': setting_document_cache_maximum_size.value, + }, name=DOCUMENT_IMAGES_CACHE_NAME, + ) + + def handler_scan_duplicates_for(sender, instance, **kwargs): task_scan_duplicates_for.apply_async( kwargs={'document_id': instance.document.pk} diff --git a/mayan/apps/documents/icons.py b/mayan/apps/documents/icons.py index 7d411a30f0..fb891fd585 100644 --- a/mayan/apps/documents/icons.py +++ b/mayan/apps/documents/icons.py @@ -7,13 +7,11 @@ icon_document_type = Icon( driver_name='fontawesome-layers', data=[ {'class': 'fas fa-circle', 'transform': 'shrink-12 up-2'}, {'class': 'fas fa-cog', 'transform': 'shrink-6 up-2', 'mask': 'fas fa-torah'} - ] + ], shadow_class='fas fa-torah' ) icon_menu_documents = Icon(driver_name='fontawesome', symbol='book') -icon_clear_image_cache = Icon(driver_name='fontawesome', symbol='file-image') - icon_dashboard_document_types = icon_document_type icon_dashboard_documents_in_trash = Icon( driver_name='fontawesome', symbol='trash-alt' @@ -27,8 +25,6 @@ icon_dashboard_new_documents_this_month = Icon( icon_dashboard_total_document = Icon( driver_name='fontawesome', symbol='book' ) - - icon_document_quick_download = Icon( driver_name='fontawesome', symbol='download' ) @@ -106,6 +102,14 @@ icon_favorite_document_remove = Icon( secondary_symbol='minus' ) +# Document pages + +icon_document_page_disable = Icon( + driver_name='fontawesomecss', css_classes='far fa-eye-slash' +) +icon_document_page_enable = Icon( + driver_name='fontawesomecss', css_classes='far fa-eye' +) icon_document_page_navigation_first = Icon( driver_name='fontawesome', symbol='step-backward' ) diff --git a/mayan/apps/documents/links.py b/mayan/apps/documents/links.py index 98920e1dc1..271aa16579 100644 --- a/mayan/apps/documents/links.py +++ b/mayan/apps/documents/links.py @@ -8,7 +8,7 @@ from mayan.apps.converter.permissions import ( from mayan.apps.navigation.classes import Link from .icons import ( - icon_clear_image_cache, icon_document_list_recent_access, + icon_document_list_recent_access, icon_recent_added_document_list, icon_document_page_navigation_first, icon_document_page_navigation_last, icon_document_page_navigation_next, icon_document_page_navigation_previous, icon_document_page_return, @@ -19,14 +19,14 @@ from .icons import ( icon_duplicated_document_list, icon_duplicated_document_scan ) from .permissions import ( - permission_document_delete, permission_document_download, - permission_document_properties_edit, permission_document_print, - permission_document_restore, permission_document_tools, - permission_document_version_revert, permission_document_view, - permission_document_trash, permission_document_type_create, - permission_document_type_delete, permission_document_type_edit, - permission_document_type_view, permission_empty_trash, - permission_document_version_view + permission_document_delete, permission_document_edit, + permission_document_download, permission_document_properties_edit, + permission_document_print, permission_document_restore, + permission_document_tools, permission_document_version_revert, + permission_document_view, permission_document_trash, + permission_document_type_create, permission_document_type_delete, + permission_document_type_edit, permission_document_type_view, + permission_empty_trash, permission_document_version_view ) from .settings import setting_zoom_max_level, setting_zoom_min_level @@ -43,11 +43,11 @@ def is_not_current_version(context): def is_first_page(context): - return context['resolved_object'].page_number <= 1 + return context['resolved_object'].siblings.first() == context['resolved_object'] def is_last_page(context): - return context['resolved_object'].page_number >= context['resolved_object'].document_version.pages.count() + return context['resolved_object'].siblings.last() == context['resolved_object'] def is_max_zoom(context): @@ -58,6 +58,14 @@ def is_min_zoom(context): return context['zoom'] <= setting_zoom_min_level.value +def is_document_page_enabled(context): + return context['resolved_object'].enabled + + +def is_document_page_disabled(context): + return not context['resolved_object'].enabled + + # Facet link_document_preview = Link( args='resolved_object.id', @@ -264,22 +272,37 @@ link_document_list_deleted = Link( text=_('Trash can'), view='documents:document_list_deleted' ) -# Tools -link_clear_image_cache = Link( - icon_class=icon_clear_image_cache, - description=_( - 'Clear the graphics representations used to speed up the documents\' ' - 'display and interactive transformations results.' - ), permissions=(permission_document_tools,), - text=_('Clear document image cache'), - view='documents:document_clear_image_cache' -) link_trash_can_empty = Link( permissions=(permission_empty_trash,), text=_('Empty trash'), view='documents:trash_can_empty' ) # Document pages + +link_document_page_disable = Link( + condition=is_document_page_enabled, + icon_class_path='mayan.apps.documents.icons.icon_document_page_disable', + kwargs={'pk': 'resolved_object.id'}, + permissions=(permission_document_edit,), text=_('Disable page'), + view='documents:document_page_disable' +) +link_document_page_multiple_disable = Link( + icon_class_path='mayan.apps.documents.icons.icon_document_page_disable', + text=_('Disable pages'), + view='documents:document_page_multiple_disable' +) +link_document_page_enable = Link( + condition=is_document_page_disabled, + icon_class_path='mayan.apps.documents.icons.icon_document_page_enable', + kwargs={'pk': 'resolved_object.id'}, + permissions=(permission_document_edit,), text=_('Enable page'), + view='documents:document_page_enable' +) +link_document_page_multiple_enable = Link( + icon_class_path='mayan.apps.documents.icons.icon_document_page_enable', + text=_('Enable pages'), + view='documents:document_page_multiple_enable' +) link_document_page_navigation_first = Link( args='resolved_object.pk', conditional_disable=is_first_page, icon_class=icon_document_page_navigation_first, @@ -323,6 +346,7 @@ link_document_page_rotate_right = Link( text=_('Rotate right'), view='documents:document_page_rotate_right', ) link_document_page_view = Link( + conditional_disable=is_document_page_disabled, icon_class_path='mayan.apps.documents.icons.icon_document_page_view', permissions=(permission_document_view,), text=_('Page image'), view='documents:document_page_view', args='resolved_object.pk' diff --git a/mayan/apps/documents/literals.py b/mayan/apps/documents/literals.py index 0ae213fac4..d639d1980f 100644 --- a/mayan/apps/documents/literals.py +++ b/mayan/apps/documents/literals.py @@ -9,6 +9,7 @@ CHECK_TRASH_PERIOD_INTERVAL = 60 DELETE_STALE_STUBS_INTERVAL = 60 * 10 # 10 minutes DEFAULT_DELETE_PERIOD = 30 DEFAULT_DELETE_TIME_UNIT = TIME_DELTA_UNIT_DAYS +DEFAULT_DOCUMENTS_CACHE_MAXIMUM_SIZE = 500 * 2 ** 20 # 500 Megabytes DEFAULT_DOCUMENTS_HASH_BLOCK_SIZE = 65535 DEFAULT_LANGUAGE = 'eng' DEFAULT_LANGUAGE_CODES = ( @@ -30,6 +31,8 @@ DEFAULT_LANGUAGE_CODES = ( DEFAULT_ZIP_FILENAME = 'document_bundle.zip' DEFAULT_DOCUMENT_TYPE_LABEL = _('Default') DOCUMENT_IMAGE_TASK_TIMEOUT = 120 +DOCUMENT_IMAGES_CACHE_NAME = 'document_images' +DOCUMENT_CACHE_STORAGE_INSTANCE_PATH = 'mayan.apps.documents.storages.storage_documentimagecache' STUB_EXPIRATION_INTERVAL = 60 * 60 * 24 # 24 hours UPDATE_PAGE_COUNT_RETRY_DELAY = 10 UPLOAD_NEW_DOCUMENT_RETRY_DELAY = 10 diff --git a/mayan/apps/documents/managers.py b/mayan/apps/documents/managers.py index b047b9d5e9..6c9dd92640 100644 --- a/mayan/apps/documents/managers.py +++ b/mayan/apps/documents/managers.py @@ -22,28 +22,9 @@ class DocumentManager(models.Manager): def get_queryset(self): return TrashCanQuerySet( - self.model, using=self._db + model=self.model, using=self._db ).filter(in_trash=False).filter(is_stub=False) - def invalidate_cache(self): - for document in self.model.objects.all(): - document.invalidate_cache() - - -class DocumentPageCachedImage(models.Manager): - def get_by_natural_key(self, filename, document_page_natural_key): - DocumentPage = apps.get_model( - app_label='documents', model_name='DocumentPage' - ) - try: - document_page = DocumentPage.objects.get_by_natural_key( - *document_page_natural_key - ) - except DocumentPage.DoesNotExist: - raise self.model.DoesNotExist - - return self.get(document_page__pk=document_page.pk, filename=filename) - class DocumentPageManager(models.Manager): def get_by_natural_key(self, page_number, document_version_natural_key): @@ -57,6 +38,11 @@ class DocumentPageManager(models.Manager): return self.get(document_version__pk=document_version.pk, page_number=page_number) + def get_queryset(self): + return models.QuerySet( + model=self.model, using=self._db + ).filter(enabled=True) + class DocumentTypeManager(models.Manager): def check_delete_periods(self): diff --git a/mayan/apps/documents/migrations/0048_auto_20190711_0544.py b/mayan/apps/documents/migrations/0048_auto_20190711_0544.py new file mode 100644 index 0000000000..627329af90 --- /dev/null +++ b/mayan/apps/documents/migrations/0048_auto_20190711_0544.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-07-11 05:44 +from __future__ import unicode_literals + +from django.db import migrations, models +import mayan.apps.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'), + ), + ] diff --git a/mayan/apps/documents/migrations/0049_auto_20190715_0454.py b/mayan/apps/documents/migrations/0049_auto_20190715_0454.py new file mode 100644 index 0000000000..ccba51c786 --- /dev/null +++ b/mayan/apps/documents/migrations/0049_auto_20190715_0454.py @@ -0,0 +1,37 @@ +from __future__ import unicode_literals + +from django.db import migrations + +from ..storages import storage_documentimagecache + + +def operation_clear_old_cache(apps, schema_editor): + DocumentPageCachedImage = apps.get_model( + app_label='documents', model_name='DocumentPageCachedImage' + ) + + for cached_image in DocumentPageCachedImage.objects.using(schema_editor.connection.alias).all(): + # Delete each cached image directly since the model doesn't exists and + # will not trigger the physical deletion of the stored file + storage_documentimagecache.delete(cached_image.filename) + cached_image.delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('documents', '0048_auto_20190711_0544'), + ] + + operations = [ + migrations.RunPython( + code=operation_clear_old_cache, + reverse_code=migrations.RunPython.noop + ), + migrations.RemoveField( + model_name='documentpagecachedimage', + name='document_page', + ), + migrations.DeleteModel( + name='DocumentPageCachedImage', + ), + ] diff --git a/mayan/apps/documents/migrations/0050_auto_20190725_0451.py b/mayan/apps/documents/migrations/0050_auto_20190725_0451.py new file mode 100644 index 0000000000..cfb54c92c2 --- /dev/null +++ b/mayan/apps/documents/migrations/0050_auto_20190725_0451.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-07-25 04:51 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0049_auto_20190715_0454'), + ] + + operations = [ + migrations.AlterField( + model_name='document', + name='language', + field=models.CharField(blank=True, default='eng', help_text='The dominant language in the document.', max_length=8, verbose_name='Language'), + ), + ] diff --git a/mayan/apps/documents/migrations/0051_documentpage_enabled.py b/mayan/apps/documents/migrations/0051_documentpage_enabled.py new file mode 100644 index 0000000000..0c74c9cc00 --- /dev/null +++ b/mayan/apps/documents/migrations/0051_documentpage_enabled.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-07-29 07:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0050_auto_20190725_0451'), + ] + + operations = [ + migrations.AddField( + model_name='documentpage', + name='enabled', + field=models.BooleanField(default=True, verbose_name='Enabled'), + ), + ] diff --git a/mayan/apps/documents/models/document_models.py b/mayan/apps/documents/models/document_models.py index 9f205226c8..8b6fef2400 100644 --- a/mayan/apps/documents/models/document_models.py +++ b/mayan/apps/documents/models/document_models.py @@ -136,10 +136,6 @@ class Document(models.Model): if latest_version: return latest_version.get_api_image_url(*args, **kwargs) - def invalidate_cache(self): - for document_version in self.versions.all(): - document_version.invalidate_cache() - @property def is_in_trash(self): return self.in_trash @@ -240,6 +236,18 @@ class Document(models.Model): def page_count(self): return self.latest_version.page_count + @property + def pages_all(self): + try: + return self.latest_version.pages_all + except AttributeError: + # Document has no version yet + DocumentPage = apps.get_model( + app_label='documents', model_name='DocumentPage' + ) + + return DocumentPage.objects.none() + @property def pages(self): try: diff --git a/mayan/apps/documents/models/document_page_models.py b/mayan/apps/documents/models/document_page_models.py index afbf34cef3..00be66ab4b 100644 --- a/mayan/apps/documents/models/document_page_models.py +++ b/mayan/apps/documents/models/document_page_models.py @@ -4,31 +4,31 @@ import logging from furl import furl -from django.core.files.base import ContentFile from django.db import models from django.urls import reverse from django.utils.encoding import force_text, python_2_unicode_compatible +from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from mayan.apps.converter.literals import DEFAULT_ZOOM_LEVEL, DEFAULT_ROTATION -from mayan.apps.converter.models import Transformation + +from mayan.apps.converter.models import LayerTransformation from mayan.apps.converter.transformations import ( BaseTransformation, TransformationResize, TransformationRotate, TransformationZoom ) from mayan.apps.converter.utils import get_converter_class -from ..managers import DocumentPageCachedImage, DocumentPageManager +from ..managers import DocumentPageManager from ..settings import ( setting_disable_base_image_cache, setting_disable_transformed_image_cache, setting_display_width, setting_display_height, setting_zoom_max_level, setting_zoom_min_level ) -from ..storages import storage_documentimagecache from .document_version_models import DocumentVersion -__all__ = ('DocumentPage', 'DocumentPageCachedImage', 'DocumentPageResult') +__all__ = ('DocumentPage', 'DocumentPageResult') logger = logging.getLogger(__name__) @@ -38,15 +38,17 @@ class DocumentPage(models.Model): Model that describes a document version page """ document_version = models.ForeignKey( - on_delete=models.CASCADE, related_name='pages', to=DocumentVersion, + on_delete=models.CASCADE, related_name='version_pages', to=DocumentVersion, verbose_name=_('Document version') ) + enabled = models.BooleanField(default=True, verbose_name=_('Enabled')) page_number = models.PositiveIntegerField( db_index=True, default=1, editable=False, verbose_name=_('Page number') ) objects = DocumentPageManager() + passthrough = models.Manager() class Meta: ordering = ('page_number',) @@ -56,12 +58,15 @@ class DocumentPage(models.Model): def __str__(self): return self.get_label() - @property - def cache_filename(self): - return 'page-cache-{}'.format(self.uuid) + @cached_property + def cache_partition(self): + partition, created = self.document_version.cache.partitions.get_or_create( + name=self.uuid + ) + return partition def delete(self, *args, **kwargs): - self.invalidate_cache() + self.cache_partition.delete() super(DocumentPage, self).delete(*args, **kwargs) def detect_orientation(self): @@ -78,31 +83,26 @@ class DocumentPage(models.Model): def document(self): return self.document_version.document - def generate_image(self, *args, **kwargs): - transformation_list = self.get_combined_transformation_list(*args, **kwargs) - - cache_filename = '{}-{}'.format( - self.cache_filename, BaseTransformation.combine(transformation_list) - ) + def generate_image(self, user=None, **kwargs): + transformation_list = self.get_combined_transformation_list(user=user, **kwargs) + combined_cache_filename = BaseTransformation.combine(transformation_list) # Check is transformed image is available - logger.debug('transformations cache filename: %s', cache_filename) + logger.debug('transformations cache filename: %s', combined_cache_filename) - if not setting_disable_transformed_image_cache.value and storage_documentimagecache.exists(cache_filename): + if not setting_disable_transformed_image_cache.value and self.cache_partition.get_file(filename=combined_cache_filename): logger.debug( - 'transformations cache file "%s" found', cache_filename + 'transformations cache file "%s" found', combined_cache_filename ) else: logger.debug( - 'transformations cache file "%s" not found', cache_filename + 'transformations cache file "%s" not found', combined_cache_filename ) image = self.get_image(transformations=transformation_list) - with storage_documentimagecache.open(cache_filename, 'wb+') as file_object: + with self.cache_partition.create_file(filename=combined_cache_filename) as file_object: file_object.write(image.getvalue()) - self.cached_images.create(filename=cache_filename) - - return cache_filename + return combined_cache_filename def get_absolute_url(self): return reverse( @@ -136,7 +136,7 @@ class DocumentPage(models.Model): return final_url.tostr() - def get_combined_transformation_list(self, *args, **kwargs): + def get_combined_transformation_list(self, user=None, *args, **kwargs): """ Return a list of transformation containing the server side document page transformation as well as tranformations created @@ -159,11 +159,15 @@ class DocumentPage(models.Model): zoom_level = setting_zoom_max_level.value # Generate transformation hash - transformation_list = [] + maximum_layer_order = kwargs.get('maximum_layer_order', None) + # Stored transformations first - for stored_transformation in Transformation.objects.get_for_object(self, as_classes=True): + for stored_transformation in LayerTransformation.objects.get_for_object( + self, maximum_layer_order=maximum_layer_order, as_classes=True, + user=user + ): transformation_list.append(stored_transformation) # Interactive transformations second @@ -186,13 +190,15 @@ class DocumentPage(models.Model): return transformation_list def get_image(self, transformations=None): - cache_filename = self.cache_filename + cache_filename = 'base_image' logger.debug('Page cache filename: %s', cache_filename) - if not setting_disable_base_image_cache.value and storage_documentimagecache.exists(cache_filename): + cache_file = self.cache_partition.get_file(filename=cache_filename) + + if not setting_disable_base_image_cache.value and cache_file: logger.debug('Page cache file "%s" found', cache_filename) - with storage_documentimagecache.open(cache_filename) as file_object: + with cache_file.open() as file_object: converter = get_converter_class()( file_object=file_object ) @@ -200,8 +206,8 @@ class DocumentPage(models.Model): converter.seek_page(page_number=0) # This code is also repeated below to allow using a context - # manager with storage_documentimagecache.open and close it - # automatically. + # manager with cache_file.open and close it automatically. + # Apply runtime transformations for transformation in transformations: converter.transform(transformation=transformation) @@ -218,14 +224,11 @@ class DocumentPage(models.Model): page_image = converter.get_page() - # Since open "wb+" doesn't create files, check if the file - # exists, if not then create it - if not storage_documentimagecache.exists(cache_filename): - storage_documentimagecache.save(name=cache_filename, content=ContentFile(content='')) - - with storage_documentimagecache.open(cache_filename, 'wb+') as file_object: + # Since open "wb+" doesn't create files, create it explicitly + with self.cache_partition.create_file(filename=cache_filename) as file_object: file_object.write(page_image.getvalue()) + # Apply runtime transformations for transformation in transformations: converter.transform(transformation=transformation) @@ -236,14 +239,8 @@ class DocumentPage(models.Model): 'Error creating page cache file "%s"; %s', cache_filename, exception ) - storage_documentimagecache.delete(cache_filename) raise - def invalidate_cache(self): - storage_documentimagecache.delete(self.cache_filename) - for cached_image in self.cached_images.all(): - cached_image.delete() - @property def is_in_trash(self): return self.document.is_in_trash @@ -254,7 +251,7 @@ class DocumentPage(models.Model): ) % { 'document': force_text(self.document), 'page_num': self.page_number, - 'total_pages': self.document_version.pages.count() + 'total_pages': self.document_version.pages_all.count() } get_label.short_description = _('Label') @@ -277,38 +274,6 @@ class DocumentPage(models.Model): return '{}-{}'.format(self.document_version.uuid, self.pk) -class DocumentPageCachedImage(models.Model): - document_page = models.ForeignKey( - on_delete=models.CASCADE, related_name='cached_images', - to=DocumentPage, verbose_name=_('Document page') - ) - datetime = models.DateTimeField( - auto_now_add=True, db_index=True, verbose_name=_('Date time') - ) - filename = models.CharField(max_length=128, verbose_name=_('Filename')) - file_size = models.PositiveIntegerField( - db_index=True, default=0, verbose_name=_('File size') - ) - - objects = DocumentPageCachedImage() - - class Meta: - verbose_name = _('Document page cached image') - verbose_name_plural = _('Document page cached images') - - def delete(self, *args, **kwargs): - storage_documentimagecache.delete(self.filename) - return super(DocumentPageCachedImage, self).delete(*args, **kwargs) - - def natural_key(self): - return (self.filename, self.document_page.natural_key()) - natural_key.dependencies = ['documents.DocumentPage'] - - def save(self, *args, **kwargs): - self.file_size = storage_documentimagecache.size(self.filename) - return super(DocumentPageCachedImage, self).save(*args, **kwargs) - - class DocumentPageResult(DocumentPage): class Meta: ordering = ('document_version__document', 'page_number') diff --git a/mayan/apps/documents/models/document_version_models.py b/mayan/apps/documents/models/document_version_models.py index 78b837860d..1c24fbfeda 100644 --- a/mayan/apps/documents/models/document_version_models.py +++ b/mayan/apps/documents/models/document_version_models.py @@ -7,24 +7,25 @@ import shutil import uuid from django.apps import apps -from django.core.files.base import ContentFile from django.db import models, transaction from django.template import Template, Context from django.urls import reverse from django.utils.encoding import force_text, python_2_unicode_compatible +from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from mayan.apps.converter.exceptions import InvalidOfficeFormat, PageCountError -from mayan.apps.converter.models import Transformation +from mayan.apps.converter.layers import layer_saved_transformations from mayan.apps.converter.transformations import TransformationRotate from mayan.apps.converter.utils import get_converter_class from mayan.apps.mimetype.api import get_mimetype from ..events import event_document_new_version, event_document_version_revert +from ..literals import DOCUMENT_IMAGES_CACHE_NAME from ..managers import DocumentVersionManager from ..settings import setting_fix_orientation, setting_hash_block_size from ..signals import post_document_created, post_version_upload -from ..storages import storage_documentversion, storage_documentimagecache +from ..storages import storage_documentversion from .document_models import Document @@ -61,14 +62,6 @@ class DocumentVersion(models.Model): _pre_open_hooks = {} _post_save_hooks = {} - @classmethod - def register_pre_open_hook(cls, order, func): - cls._pre_open_hooks[order] = func - - @classmethod - def register_post_save_hook(cls, order, func): - cls._post_save_hooks[order] = func - document = models.ForeignKey( on_delete=models.CASCADE, related_name='versions', to=Document, verbose_name=_('Document') @@ -118,18 +111,35 @@ class DocumentVersion(models.Model): objects = DocumentVersionManager() + @classmethod + def register_pre_open_hook(cls, order, func): + cls._pre_open_hooks[order] = func + + @classmethod + def register_post_save_hook(cls, order, func): + cls._post_save_hooks[order] = func + def __str__(self): return self.get_rendered_string() - @property - def cache_filename(self): - return 'document-version-{}'.format(self.uuid) + @cached_property + def cache(self): + Cache = apps.get_model(app_label='file_caching', model_name='Cache') + return Cache.objects.get(name=DOCUMENT_IMAGES_CACHE_NAME) + + @cached_property + def cache_partition(self): + partition, created = self.cache.partitions.get_or_create( + name='version-{}'.format(self.uuid) + ) + return partition def delete(self, *args, **kwargs): for page in self.pages.all(): page.delete() self.file.storage.delete(self.file.name) + self.cache_partition.delete() return super(DocumentVersion, self).delete(*args, **kwargs) @@ -146,7 +156,7 @@ class DocumentVersion(models.Model): for page in self.pages.all(): degrees = page.detect_orientation() if degrees: - Transformation.objects.add_to_object( + layer_saved_transformations.add_to_object( obj=page, transformation=TransformationRotate, arguments='{{"degrees": {}}}'.format(360 - degrees) ) @@ -164,43 +174,36 @@ class DocumentVersion(models.Model): return first_page.get_api_image_url(*args, **kwargs) def get_intermediate_file(self): - cache_filename = self.cache_filename - logger.debug('Intermidiate filename: %s', cache_filename) - - if storage_documentimagecache.exists(cache_filename): - logger.debug('Intermidiate file "%s" found.', cache_filename) - - return storage_documentimagecache.open(cache_filename) + cache_filename = 'intermediate_file' + cache_file = self.cache_partition.get_file(filename=cache_filename) + if cache_file: + logger.debug('Intermidiate file found.') + return cache_file.open() else: - logger.debug('Intermidiate file "%s" not found.', cache_filename) + logger.debug('Intermidiate file not found.') try: with self.open() as version_file_object: - converter = get_converter_class()(file_object=version_file_object) + converter = get_converter_class()( + file_object=version_file_object + ) with converter.to_pdf() as pdf_file_object: - - # Since open "wb+" doesn't create files, check if the file - # exists, if not then create it - if not storage_documentimagecache.exists(cache_filename): - storage_documentimagecache.save( - name=cache_filename, content=ContentFile(content='') - ) - - with storage_documentimagecache.open(cache_filename, mode='wb+') as file_object: + with self.cache_partition.create_file(filename=cache_filename) as file_object: shutil.copyfileobj( fsrc=pdf_file_object, fdst=file_object ) - return storage_documentimagecache.open(cache_filename) + return self.cache_partition.get_file(filename=cache_filename).open() except InvalidOfficeFormat: return self.open() except Exception as exception: - # Cleanup in case of error logger.error( 'Error creating intermediate file "%s"; %s.', cache_filename, exception ) - storage_documentimagecache.delete(cache_filename) + cache_file = self.cache_partition.get_file(filename=cache_filename) + if cache_file: + cache_file.delete() raise def get_rendered_string(self, preserve_extension=False): @@ -223,11 +226,6 @@ class DocumentVersion(models.Model): return (self.checksum, self.document.natural_key()) natural_key.dependencies = ['documents.Document'] - def invalidate_cache(self): - storage_documentimagecache.delete(self.cache_filename) - for page in self.pages.all(): - page.invalidate_cache() - @property def is_in_trash(self): return self.document.is_in_trash @@ -248,6 +246,17 @@ class DocumentVersion(models.Model): return result + @property + def pages_all(self): + DocumentPage = apps.get_model( + app_label='documents', model_name='DocumentPage' + ) + return DocumentPage.passthrough.filter(document_version=self) + + @property + def pages(self): + return self.version_pages.all() + @property def page_count(self): """ diff --git a/mayan/apps/documents/queues.py b/mayan/apps/documents/queues.py index 50840203c5..11280f17d8 100644 --- a/mayan/apps/documents/queues.py +++ b/mayan/apps/documents/queues.py @@ -61,10 +61,6 @@ queue_documents_periodic.add_task_type( schedule=timedelta(seconds=DELETE_STALE_STUBS_INTERVAL), ) -queue_tools.add_task_type( - dotted_path='mayan.apps.documents.tasks.task_clear_image_cache', - label=_('Clear image cache') -) queue_tools.add_task_type( dotted_path='mayan.apps.documents.tasks.task_scan_duplicates_all', label=_('Duplicated document scan') diff --git a/mayan/apps/documents/settings.py b/mayan/apps/documents/settings.py index ca27496599..8a666c8f11 100644 --- a/mayan/apps/documents/settings.py +++ b/mayan/apps/documents/settings.py @@ -8,25 +8,35 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.smart_settings.classes import Namespace from .literals import ( - DEFAULT_DOCUMENTS_HASH_BLOCK_SIZE, DEFAULT_LANGUAGE, DEFAULT_LANGUAGE_CODES + DEFAULT_DOCUMENTS_CACHE_MAXIMUM_SIZE, DEFAULT_DOCUMENTS_HASH_BLOCK_SIZE, + DEFAULT_LANGUAGE, DEFAULT_LANGUAGE_CODES ) +from .utils import callback_update_cache_size namespace = Namespace(label=_('Documents'), name='documents') +setting_document_cache_maximum_size = namespace.add_setting( + global_name='DOCUMENTS_CACHE_MAXIMUM_SIZE', + default=DEFAULT_DOCUMENTS_CACHE_MAXIMUM_SIZE, + help_text=_( + 'The threshold at which the DOCUMENT_CACHE_STORAGE_BACKEND will start ' + 'deleting the oldest document image cache files. Specify the size in ' + 'bytes.' + ), post_edit_function=callback_update_cache_size +) setting_documentimagecache_storage = namespace.add_setting( global_name='DOCUMENTS_CACHE_STORAGE_BACKEND', 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: {}}}'.format( - os.path.join(settings.MEDIA_ROOT, 'document_cache') - ), help_text=_( + default={'location': 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, @@ -127,9 +137,8 @@ setting_storage_backend = namespace.add_setting( ) setting_storage_backend_arguments = namespace.add_setting( global_name='DOCUMENTS_STORAGE_BACKEND_ARGUMENTS', - default='{{location: {}}}'.format( - os.path.join(settings.MEDIA_ROOT, 'document_storage') - ), help_text=_('Arguments to pass to the DOCUMENT_STORAGE_BACKEND.') + default={'location': 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=_( diff --git a/mayan/apps/documents/storages.py b/mayan/apps/documents/storages.py index 95405e9242..a4e9132fde 100644 --- a/mayan/apps/documents/storages.py +++ b/mayan/apps/documents/storages.py @@ -1,13 +1,6 @@ from __future__ import unicode_literals -import yaml - -try: - from yaml import CSafeLoader as SafeLoader -except ImportError: - from yaml import SafeLoader - -from django.utils.module_loading import import_string +from mayan.apps.storage.utils import get_storage_subclass from .settings import ( setting_documentimagecache_storage, @@ -15,20 +8,10 @@ from .settings import ( setting_storage_backend, setting_storage_backend_arguments ) -storage_documentversion = import_string( +storage_documentversion = get_storage_subclass( dotted_path=setting_storage_backend.value -)( - **yaml.load( - stream=setting_storage_backend_arguments.value or '{}', - Loader=SafeLoader - ) -) +)(**setting_storage_backend_arguments.value) -storage_documentimagecache = import_string( +storage_documentimagecache = get_storage_subclass( dotted_path=setting_documentimagecache_storage.value -)( - **yaml.load( - stream=setting_documentimagecache_storage_arguments.value or '{}', - Loader=SafeLoader - ) -) +)(**setting_documentimagecache_storage_arguments.value) diff --git a/mayan/apps/documents/tasks.py b/mayan/apps/documents/tasks.py index f161fc7973..34956086bc 100644 --- a/mayan/apps/documents/tasks.py +++ b/mayan/apps/documents/tasks.py @@ -42,17 +42,6 @@ def task_check_trash_periods(): DocumentType.objects.check_trash_periods() -@app.task(ignore_result=True) -def task_clear_image_cache(): - Document = apps.get_model( - app_label='documents', model_name='Document' - ) - - logger.info('Starting document cache invalidation') - Document.objects.invalidate_cache() - logger.info('Finished document cache invalidation') - - @app.task(ignore_result=True) def task_delete_document(trashed_document_id): DeletedDocument = apps.get_model( @@ -77,14 +66,19 @@ def task_delete_stubs(): @app.task() -def task_generate_document_page_image(document_page_id, *args, **kwargs): +def task_generate_document_page_image(document_page_id, user_id=None, **kwargs): DocumentPage = apps.get_model( app_label='documents', model_name='DocumentPage' ) + User = get_user_model() - document_page = DocumentPage.objects.get(pk=document_page_id) + if user_id: + user = User.objects.get(pk=user_id) + else: + user = None - return document_page.generate_image(*args, **kwargs) + document_page = DocumentPage.passthrough.get(pk=document_page_id) + return document_page.generate_image(user=user, **kwargs) @app.task(ignore_result=True) diff --git a/mayan/apps/documents/tests/__init__.py b/mayan/apps/documents/tests/__init__.py index c1f93fb9de..e69de29bb2 100644 --- a/mayan/apps/documents/tests/__init__.py +++ b/mayan/apps/documents/tests/__init__.py @@ -1,3 +0,0 @@ -from .base import GenericDocumentTestCase, GenericDocumentViewTestCase # NOQA -from .literals import * # NOQA -from .mixins import * # NOQA diff --git a/mayan/apps/documents/tests/base.py b/mayan/apps/documents/tests/base.py index 54014d919d..644e6e4f42 100644 --- a/mayan/apps/documents/tests/base.py +++ b/mayan/apps/documents/tests/base.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import BaseTestCase, GenericViewTestCase +from mayan.apps.common.tests.base import BaseTestCase, GenericViewTestCase from .mixins import DocumentTestMixin diff --git a/mayan/apps/documents/tests/literals.py b/mayan/apps/documents/tests/literals.py index 12e1b85686..4e89bd23fd 100644 --- a/mayan/apps/documents/tests/literals.py +++ b/mayan/apps/documents/tests/literals.py @@ -6,6 +6,7 @@ import os from django.conf import settings from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS +from mayan.apps.converter.transformations import TransformationRotate __all__ = ( 'TEST_COMPRESSED_DOCUMENTS_FILENAME', 'TEST_COMPRESSED_DOCUMENT_PATH', @@ -49,6 +50,7 @@ TEST_SMALL_DOCUMENT_CHECKSUM = 'efa10e6cc21f83078aaa94d5cbe51de67b51af706143b\ afc7fd6d4c02124879a' TEST_SMALL_DOCUMENT_MIMETYPE = 'image/png' TEST_SMALL_DOCUMENT_SIZE = 17436 +TEST_TRANSFORMATION_CLASS = TransformationRotate TEST_TRANSFORMATION_NAME = 'rotate' TEST_TRANSFORMATION_ARGUMENT = 'degrees: 180' diff --git a/mayan/apps/documents/tests/mixins.py b/mayan/apps/documents/tests/mixins.py index cb692a1f10..bb996c6cde 100644 --- a/mayan/apps/documents/tests/mixins.py +++ b/mayan/apps/documents/tests/mixins.py @@ -4,6 +4,8 @@ import os from django.conf import settings +from mayan.apps.converter.classes import Layer + from ..literals import PAGE_RANGE_ALL from ..models import DocumentType @@ -26,6 +28,8 @@ class DocumentTestMixin(object): def setUp(self): super(DocumentTestMixin, self).setUp() + Layer.invalidate_cache() + self.test_documents = [] if self.auto_create_document_type: @@ -45,6 +49,13 @@ class DocumentTestMixin(object): ) self.test_document_type = self.test_document_type + def _calculate_test_document_path(self): + if not self.test_document_path: + self.test_document_path = os.path.join( + settings.BASE_DIR, 'apps', 'documents', 'tests', 'contrib', + 'sample_documents', self.test_document_filename + ) + def upload_document(self, label=None): self._calculate_test_document_path() @@ -59,13 +70,6 @@ class DocumentTestMixin(object): self.test_document = document self.test_documents.append(document) - def _calculate_test_document_path(self): - if not self.test_document_path: - self.test_document_path = os.path.join( - settings.BASE_DIR, 'apps', 'documents', 'tests', 'contrib', - 'sample_documents', self.test_document_filename - ) - class DocumentTypeViewTestMixin(object): def _request_test_document_type_create_view(self): diff --git a/mayan/apps/documents/tests/test_api.py b/mayan/apps/documents/tests/test_api.py index a40165fac9..dea50b2618 100644 --- a/mayan/apps/documents/tests/test_api.py +++ b/mayan/apps/documents/tests/test_api.py @@ -7,7 +7,7 @@ from django.utils.encoding import force_text from django_downloadview import assert_download_response from rest_framework import status -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..models import Document, DocumentType from ..permissions import ( @@ -163,11 +163,11 @@ class DocumentAPIViewTestMixin(object): ) def _request_test_document_api_upload_view(self): - with open(TEST_DOCUMENT_PATH, mode='rb') as file_descriptor: + with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: return self.post( viewname='rest_api:document-list', data={ 'document_type': self.test_document_type.pk, - 'file': file_descriptor + 'file': file_object } ) diff --git a/mayan/apps/documents/tests/test_document_page_views.py b/mayan/apps/documents/tests/test_document_page_views.py index 57752396de..29ee28cec3 100644 --- a/mayan/apps/documents/tests/test_document_page_views.py +++ b/mayan/apps/documents/tests/test_document_page_views.py @@ -2,11 +2,152 @@ from __future__ import unicode_literals from django.utils.encoding import force_text -from ..permissions import permission_document_view +from ..permissions import ( + permission_document_edit, permission_document_view +) from .base import GenericDocumentViewTestCase +class DocumentPageDisableViewTestCase(GenericDocumentViewTestCase): + def setUp(self): + super(DocumentPageDisableViewTestCase, self).setUp() + self.test_document_page = self.test_document.pages_all.first() + + def _request_test_document_page_disable_view(self): + return self.post( + viewname='documents:document_page_disable', kwargs={ + 'pk': self.test_document_page.pk + } + ) + + def test_document_page_disable_view_no_permission(self): + test_document_page_count = self.test_document.pages.count() + + response = self._request_test_document_page_disable_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual( + test_document_page_count, self.test_document.pages.count() + ) + + def test_document_page_disable_view_with_access(self): + self.grant_access( + obj=self.test_document, permission=permission_document_edit + ) + + test_document_page_count = self.test_document.pages.count() + + response = self._request_test_document_page_disable_view() + self.assertEqual(response.status_code, 302) + + self.assertNotEqual( + test_document_page_count, self.test_document.pages.count() + ) + + def _request_test_document_page_multiple_disable_view(self): + return self.post( + viewname='documents:document_page_multiple_disable', data={ + 'id_list': self.test_document_page.pk + } + ) + + def test_document_page_multiple_disable_view_no_permission(self): + test_document_page_count = self.test_document.pages.count() + + response = self._request_test_document_page_multiple_disable_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual( + test_document_page_count, self.test_document.pages.count() + ) + + def test_document_page_multiple_disable_view_with_access(self): + self.grant_access( + obj=self.test_document, permission=permission_document_edit + ) + + test_document_page_count = self.test_document.pages.count() + + response = self._request_test_document_page_multiple_disable_view() + self.assertEqual(response.status_code, 302) + + self.assertNotEqual( + test_document_page_count, self.test_document.pages.count() + ) + + def _disable_test_document_page(self): + self.test_document_page.enabled = False + self.test_document_page.save() + + def _request_test_document_page_enable_view(self): + return self.post( + viewname='documents:document_page_enable', kwargs={ + 'pk': self.test_document_page.pk + } + ) + + def test_document_page_enable_view_no_permission(self): + self._disable_test_document_page() + + test_document_page_count = self.test_document.pages.count() + + response = self._request_test_document_page_enable_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual( + test_document_page_count, self.test_document.pages.count() + ) + + def test_document_page_enable_view_with_access(self): + self._disable_test_document_page() + self.grant_access( + obj=self.test_document, permission=permission_document_edit + ) + + test_document_page_count = self.test_document.pages.count() + + response = self._request_test_document_page_enable_view() + self.assertEqual(response.status_code, 302) + + self.assertNotEqual( + test_document_page_count, self.test_document.pages.count() + ) + + def _request_test_document_page_multiple_enable_view(self): + return self.post( + viewname='documents:document_page_multiple_enable', data={ + 'id_list': self.test_document_page.pk + } + ) + + def test_document_page_multiple_enable_view_no_permission(self): + self._disable_test_document_page() + test_document_page_count = self.test_document.pages.count() + + response = self._request_test_document_page_multiple_enable_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual( + test_document_page_count, self.test_document.pages.count() + ) + + def test_document_page_multiple_enable_view_with_access(self): + self._disable_test_document_page() + self.grant_access( + obj=self.test_document, permission=permission_document_edit + ) + + test_document_page_count = self.test_document.pages.count() + + response = self._request_test_document_page_multiple_enable_view() + self.assertEqual(response.status_code, 302) + + self.assertNotEqual( + test_document_page_count, self.test_document.pages.count() + ) + + class DocumentPageViewTestCase(GenericDocumentViewTestCase): def _request_test_document_page_list_view(self): return self.get( diff --git a/mayan/apps/documents/tests/test_document_views.py b/mayan/apps/documents/tests/test_document_views.py index bd57dc905e..381a71211d 100644 --- a/mayan/apps/documents/tests/test_document_views.py +++ b/mayan/apps/documents/tests/test_document_views.py @@ -1,10 +1,10 @@ from __future__ import unicode_literals -from django.contrib.contenttypes.models import ContentType from django.utils.encoding import force_text -from mayan.apps.converter.models import Transformation +from mayan.apps.converter.layers import layer_saved_transformations from mayan.apps.converter.permissions import permission_transformation_delete +from mayan.apps.converter.tests.mixins import LayerTestMixin from ..models import DeletedDocument, Document, DocumentType from ..permissions import ( @@ -17,12 +17,21 @@ from ..permissions import ( from .base import GenericDocumentViewTestCase from .literals import ( TEST_DOCUMENT_TYPE_2_LABEL, TEST_SMALL_DOCUMENT_FILENAME, - TEST_TRANSFORMATION_ARGUMENT, TEST_TRANSFORMATION_NAME, + TEST_TRANSFORMATION_ARGUMENT, TEST_TRANSFORMATION_CLASS ) from .mixins import DocumentViewTestMixin -class DocumentsViewsTestCase(DocumentViewTestMixin, GenericDocumentViewTestCase): +class DocumentsViewsTestCase( + LayerTestMixin, DocumentViewTestMixin, GenericDocumentViewTestCase +): + def _create_document_transformation(self): + layer_saved_transformations.add_transformation_to( + obj=self.test_document.pages.first(), + transformation_class=TEST_TRANSFORMATION_CLASS, + arguments=TEST_TRANSFORMATION_ARGUMENT + ) + def test_document_view_no_permissions(self): response = self._request_document_properties_view() self.assertEqual(response.status_code, 404) @@ -328,18 +337,11 @@ class DocumentsViewsTestCase(DocumentViewTestMixin, GenericDocumentViewTestCase) self.assertEqual(self.test_document.pages.count(), page_count) def test_document_clear_transformations_view_no_permission(self): - document_page = self.test_document.pages.first() - content_type = ContentType.objects.get_for_model(model=document_page) - transformation = Transformation.objects.create( - content_type=content_type, object_id=document_page.pk, - name=TEST_TRANSFORMATION_NAME, - arguments=TEST_TRANSFORMATION_ARGUMENT - ) + self._create_document_transformation() - self.assertQuerysetEqual( - Transformation.objects.get_for_object(obj=document_page), - (repr(transformation),) - ) + transformation_count = layer_saved_transformations.get_transformations_for( + obj=self.test_document.pages.first() + ).count() self.grant_access( obj=self.test_document, permission=permission_document_view @@ -348,26 +350,23 @@ class DocumentsViewsTestCase(DocumentViewTestMixin, GenericDocumentViewTestCase) response = self._request_document_clear_transformations_view() self.assertEqual(response.status_code, 404) - self.assertQuerysetEqual( - Transformation.objects.get_for_object(obj=document_page), - (repr(transformation),) + self.assertEqual( + transformation_count, + layer_saved_transformations.get_transformations_for( + obj=self.test_document.pages.first() + ).count() ) def test_document_clear_transformations_view_with_access(self): - document_page = self.test_document.pages.first() - content_type = ContentType.objects.get_for_model(model=document_page) - transformation = Transformation.objects.create( - content_type=content_type, object_id=document_page.pk, - name=TEST_TRANSFORMATION_NAME, - arguments=TEST_TRANSFORMATION_ARGUMENT - ) - self.assertQuerysetEqual( - Transformation.objects.get_for_object(obj=document_page), - (repr(transformation),) - ) + self._create_document_transformation() + + transformation_count = layer_saved_transformations.get_transformations_for( + obj=self.test_document.pages.first() + ).count() self.grant_access( - obj=self.test_document, permission=permission_transformation_delete + obj=self.test_document, + permission=permission_transformation_delete ) self.grant_access( obj=self.test_document, permission=permission_document_view @@ -377,45 +376,39 @@ class DocumentsViewsTestCase(DocumentViewTestMixin, GenericDocumentViewTestCase) self.assertEqual(response.status_code, 302) self.assertEqual( - Transformation.objects.get_for_object(obj=document_page).count(), 0 + transformation_count - 1, + layer_saved_transformations.get_transformations_for( + obj=self.test_document.pages.first() + ).count() ) def test_document_multiple_clear_transformations_view_no_permission(self): - document_page = self.test_document.pages.first() - content_type = ContentType.objects.get_for_model(model=document_page) - transformation = Transformation.objects.create( - content_type=content_type, object_id=document_page.pk, - name=TEST_TRANSFORMATION_NAME, - arguments=TEST_TRANSFORMATION_ARGUMENT - ) + self._create_document_transformation() - self.assertQuerysetEqual( - Transformation.objects.get_for_object(obj=document_page), - (repr(transformation),) - ) + transformation_count = layer_saved_transformations.get_transformations_for( + obj=self.test_document.pages.first() + ).count() - self.grant_permission(permission=permission_document_view) + self.grant_access( + obj=self.test_document, permission=permission_document_view + ) response = self._request_document_multiple_clear_transformations() self.assertEqual(response.status_code, 404) - self.assertQuerysetEqual( - Transformation.objects.get_for_object(obj=document_page), - (repr(transformation),) + + self.assertEqual( + transformation_count, + layer_saved_transformations.get_transformations_for( + obj=self.test_document.pages.first() + ).count() ) def test_document_multiple_clear_transformations_view_with_access(self): - document_page = self.test_document.pages.first() - content_type = ContentType.objects.get_for_model(model=document_page) - transformation = Transformation.objects.create( - content_type=content_type, object_id=document_page.pk, - name=TEST_TRANSFORMATION_NAME, - arguments=TEST_TRANSFORMATION_ARGUMENT - ) + self._create_document_transformation() - self.assertQuerysetEqual( - Transformation.objects.get_for_object(obj=document_page), - (repr(transformation),) - ) + transformation_count = layer_saved_transformations.get_transformations_for( + obj=self.test_document.pages.first() + ).count() self.grant_access( obj=self.test_document, permission=permission_document_view @@ -428,7 +421,10 @@ class DocumentsViewsTestCase(DocumentViewTestMixin, GenericDocumentViewTestCase) self.assertEqual(response.status_code, 302) self.assertEqual( - Transformation.objects.get_for_object(obj=document_page).count(), 0 + transformation_count - 1, + layer_saved_transformations.get_transformations_for( + obj=self.test_document.pages.first() + ).count() ) def test_trash_can_empty_view_no_permission(self): diff --git a/mayan/apps/documents/tests/test_models.py b/mayan/apps/documents/tests/test_models.py index fda9936e8c..4a441d43ff 100644 --- a/mayan/apps/documents/tests/test_models.py +++ b/mayan/apps/documents/tests/test_models.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from datetime import timedelta import time -from mayan.apps.common.tests import BaseTestCase +from mayan.apps.common.tests.base import BaseTestCase from ..literals import STUB_EXPIRATION_INTERVAL from ..models import ( diff --git a/mayan/apps/documents/tests/test_search.py b/mayan/apps/documents/tests/test_search.py index 76cc6b56be..531f6e8d2f 100644 --- a/mayan/apps/documents/tests/test_search.py +++ b/mayan/apps/documents/tests/test_search.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import BaseTestCase +from mayan.apps.common.tests.base import BaseTestCase from mayan.apps.documents.permissions import permission_document_view from mayan.apps.documents.search import document_search, document_page_search -from mayan.apps.documents.tests import DocumentTestMixin +from mayan.apps.documents.tests.mixins import DocumentTestMixin class DocumentSearchTestCase(DocumentTestMixin, BaseTestCase): diff --git a/mayan/apps/documents/tests/test_statistics.py b/mayan/apps/documents/tests/test_statistics.py index 5f342b8b42..61c7463f96 100644 --- a/mayan/apps/documents/tests/test_statistics.py +++ b/mayan/apps/documents/tests/test_statistics.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import BaseTestCase +from mayan.apps.common.tests.base import BaseTestCase from ..statistics import namespace diff --git a/mayan/apps/documents/tests/test_utils.py b/mayan/apps/documents/tests/test_utils.py index 1484282931..b89d6819d0 100644 --- a/mayan/apps/documents/tests/test_utils.py +++ b/mayan/apps/documents/tests/test_utils.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import BaseTestCase +from mayan.apps.common.tests.base import BaseTestCase from ..utils import parse_range diff --git a/mayan/apps/documents/urls.py b/mayan/apps/documents/urls.py index 6d954eebb0..84aec25094 100644 --- a/mayan/apps/documents/urls.py +++ b/mayan/apps/documents/urls.py @@ -12,30 +12,37 @@ from .api_views import ( APIDocumentVersionPageListView, APIDocumentVersionView, APIRecentDocumentListView ) -from .views import ( - ClearImageCacheView, DocumentDocumentTypeEditView, DocumentDownloadFormView, +from .views.document_views import ( + DocumentDocumentTypeEditView, DocumentDownloadFormView, DocumentDownloadView, DocumentDuplicatesListView, DocumentEditView, - DocumentListView, DocumentPageListView, DocumentPageNavigationFirst, - DocumentPageNavigationLast, DocumentPageNavigationNext, - DocumentPageNavigationPrevious, DocumentPageRotateLeftView, - DocumentPageRotateRightView, DocumentPageView, DocumentPageViewResetView, - DocumentPageZoomInView, DocumentPageZoomOutView, DocumentPreviewView, - DocumentPrint, DocumentTransformationsClearView, - DocumentTransformationsCloneView, DocumentTypeCreateView, - DocumentTypeDeleteView, DocumentTypeDocumentListView, - DocumentTypeFilenameCreateView, DocumentTypeFilenameDeleteView, - DocumentTypeFilenameEditView, DocumentTypeFilenameListView, - DocumentTypeListView, DocumentTypeEditView, DocumentUpdatePageCountView, + DocumentListView, DocumentPreviewView, DocumentPrint, + DocumentTransformationsClearView, DocumentTransformationsCloneView, + DocumentUpdatePageCountView, DocumentView, DuplicatedDocumentListView, + RecentAccessDocumentListView, RecentAddedDocumentListView +) +from .views.document_page_views import ( + DocumentPageDisable, DocumentPageEnable, DocumentPageListView, + DocumentPageNavigationFirst, DocumentPageNavigationLast, + DocumentPageNavigationNext, DocumentPageNavigationPrevious, + DocumentPageRotateLeftView, DocumentPageRotateRightView, + DocumentPageView, DocumentPageViewResetView, DocumentPageZoomInView, + DocumentPageZoomOutView +) +from .views.document_version_views import ( DocumentVersionDownloadFormView, DocumentVersionDownloadView, DocumentVersionListView, DocumentVersionRevertView, DocumentVersionView, - DocumentView, DuplicatedDocumentListView, - RecentAccessDocumentListView, RecentAddedDocumentListView, - ScanDuplicatedDocuments ) -from .views.document_type_views import DocumentTypeDeletionPoliciesEditView +from .views.document_type_views import ( + DocumentTypeCreateView, DocumentTypeDeleteView, + DocumentTypeDeletionPoliciesEditView, DocumentTypeDocumentListView, + DocumentTypeEditView, DocumentTypeFilenameCreateView, + DocumentTypeFilenameDeleteView, DocumentTypeFilenameEditView, + DocumentTypeFilenameListView, DocumentTypeListView +) from .views.favorite_document_views import ( FavoriteAddView, FavoriteDocumentListView, FavoriteRemoveView ) +from .views.misc_views import ScanDuplicatedDocuments from .views.trashed_document_views import ( DocumentTrashView, EmptyTrashCanView, TrashedDocumentDeleteView, TrashedDocumentListView, TrashedDocumentRestoreView @@ -43,48 +50,48 @@ from .views.trashed_document_views import ( urlpatterns_document_types = [ url( - regex=r'^type/list/$', view=DocumentTypeListView.as_view(), + regex=r'^document_types/$', view=DocumentTypeListView.as_view(), name='document_type_list' ), url( - regex=r'^type/create/$', view=DocumentTypeCreateView.as_view(), + regex=r'^document_types/create/$', view=DocumentTypeCreateView.as_view(), name='document_type_create' ), url( - regex=r'^type/(?P\d+)/edit/$', view=DocumentTypeEditView.as_view(), - name='document_type_edit' - ), - url( - regex=r'^type/(?P\d+)/delete/$', + regex=r'^document_types/(?P\d+)/delete/$', view=DocumentTypeDeleteView.as_view(), name='document_type_delete' ), url( - regex=r'^type/(?P\d+)/documents/$', + regex=r'^document_types/(?P\d+)/edit/$', view=DocumentTypeEditView.as_view(), + name='document_type_edit' + ), + url( + regex=r'^document_types/(?P\d+)/documents/$', view=DocumentTypeDocumentListView.as_view(), name='document_type_document_list' ), url( - regex=r'^type/(?P\d+)/filename/list/$', + regex=r'^document_types/(?P\d+)/filenames/$', view=DocumentTypeFilenameListView.as_view(), name='document_type_filename_list' ), url( - regex=r'^type/filename/(?P\d+)/edit/$', - view=DocumentTypeFilenameEditView.as_view(), - name='document_type_filename_edit' - ), - url( - regex=r'^type/filename/(?P\d+)/delete/$', + regex=r'^document_types/filenames/(?P\d+)/delete/$', view=DocumentTypeFilenameDeleteView.as_view(), name='document_type_filename_delete' ), url( - regex=r'^type/(?P\d+)/filename/create/$', + regex=r'^document_types/filenames/(?P\d+)/edit/$', + view=DocumentTypeFilenameEditView.as_view(), + name='document_type_filename_edit' + ), + url( + regex=r'^document_types/(?P\d+)/filenames/create/$', view=DocumentTypeFilenameCreateView.as_view(), name='document_type_filename_create' ), url( - regex=r'^type/(?P\d+)/deletion_policies/$', + regex=r'^document_types/(?P\d+)/deletion_policies/$', view=DocumentTypeDeletionPoliciesEditView.as_view(), name='document_type_policies' ), @@ -92,248 +99,270 @@ urlpatterns_document_types = [ urlpatterns_favorite_documents = [ url( - regex=r'^list/favorites/$', view=FavoriteDocumentListView.as_view(), + regex=r'^documents/favorites/$', view=FavoriteDocumentListView.as_view(), name='document_list_favorites' ), url( - regex=r'^(?P\d+)/add_to_favorites/$', + regex=r'^documents/(?P\d+)/add_to_favorites/$', view=FavoriteAddView.as_view(), name='document_add_to_favorites' ), url( - regex=r'^multiple/add_to_favorites/$', view=FavoriteAddView.as_view(), + regex=r'^documents/multiple/add_to_favorites/$', view=FavoriteAddView.as_view(), name='document_multiple_add_to_favorites' ), url( - regex=r'^(?P\d+)/remove_from_favorites/$', + regex=r'^documents/(?P\d+)/remove_from_favorites/$', view=FavoriteRemoveView.as_view(), name='document_remove_from_favorites' ), url( - regex=r'^multiple/remove_from_favorites/$', + regex=r'^documents/multiple/remove_from_favorites/$', view=FavoriteRemoveView.as_view(), name='document_multiple_remove_from_favorites' ), +] + +urlpatterns_documents = [ + url( + regex=r'^documents/$', view=DocumentListView.as_view(), name='document_list' + ), + url( + regex=r'^documents/recent_access/$', + view=RecentAccessDocumentListView.as_view(), + name='document_list_recent_access' + ), + url( + regex=r'^documents/recent_added/$', + view=RecentAddedDocumentListView.as_view(), + name='document_list_recent_added' + ), + url( + regex=r'^documents/duplicated/$', + view=DuplicatedDocumentListView.as_view(), + name='duplicated_document_list' + ), + url( + regex=r'^documents/(?P\d+)/preview/$', view=DocumentPreviewView.as_view(), + name='document_preview' + ), + url( + regex=r'^documents/(?P\d+)/properties/$', view=DocumentView.as_view(), + name='document_properties' + ), + url( + regex=r'^documents/(?P\d+)/duplicates/$', + view=DocumentDuplicatesListView.as_view(), + name='document_duplicates_list' + ), + url( + regex=r'^documents/(?P\d+)/type/$', + view=DocumentDocumentTypeEditView.as_view(), + name='document_document_type_edit' + ), + url( + regex=r'^documents/multiple/type/$', view=DocumentDocumentTypeEditView.as_view(), + name='document_multiple_document_type_edit' + ), + url( + regex=r'^documents/(?P\d+)/edit/$', view=DocumentEditView.as_view(), + name='document_edit' + ), + url( + regex=r'^documents/(?P\d+)/print/$', view=DocumentPrint.as_view(), + name='document_print' + ), + url( + regex=r'^documents/(?P\d+)/reset_page_count/$', + view=DocumentUpdatePageCountView.as_view(), + name='document_update_page_count' + ), + url( + regex=r'^documents/multiple/reset_page_count/$', + view=DocumentUpdatePageCountView.as_view(), + name='document_multiple_update_page_count' + ), + url( + regex=r'^documents/(?P\d+)/download/form/$', + view=DocumentDownloadFormView.as_view(), name='document_download_form' + ), + url( + regex=r'^documents/(?P\d+)/download/$', view=DocumentDownloadView.as_view(), + name='document_download' + ), + url( + regex=r'^documents/multiple/download/form/$', + view=DocumentDownloadFormView.as_view(), + name='document_multiple_download_form' + ), + url( + regex=r'^documents/multiple/download/$', view=DocumentDownloadView.as_view(), + name='document_multiple_download' + ), + url( + regex=r'^documents/(?P\d+)/transformations/clear/$', + view=DocumentTransformationsClearView.as_view(), + name='document_clear_transformations' + ), + url( + regex=r'^documents/multiple/clear_transformations/$', + view=DocumentTransformationsClearView.as_view(), + name='document_multiple_clear_transformations' + ), + url( + regex=r'^documents/(?P\d+)/transformations/clone/$', + view=DocumentTransformationsCloneView.as_view(), + name='document_clone_transformations' + ), +] + +urlpatterns_document_pages = [ + url( + regex=r'^documents/(?P\d+)/pages/$', view=DocumentPageListView.as_view(), + name='document_pages' + ), + url( + regex=r'^documents/pages/(?P\d+)/$', view=DocumentPageView.as_view(), + name='document_page_view' + ), + url( + regex=r'^documents/pages/(?P\d+)/disable/$', + name='document_page_disable', view=DocumentPageDisable.as_view() + ), + url( + regex=r'^documents/pages/multiple/disable/$', name='document_page_multiple_disable', + view=DocumentPageDisable.as_view() + ), + url( + regex=r'^documents/pages/(?P\d+)/enable/$', + name='document_page_enable', view=DocumentPageEnable.as_view() + ), + url( + regex=r'^documents/pages/multiple/enable/$', name='document_page_multiple_enable', + view=DocumentPageEnable.as_view() + ), + url( + regex=r'^documents/pages/(?P\d+)/navigation/next/$', + view=DocumentPageNavigationNext.as_view(), + name='document_page_navigation_next' + ), + url( + regex=r'^documents/pages/(?P\d+)/navigation/previous/$', + view=DocumentPageNavigationPrevious.as_view(), + name='document_page_navigation_previous' + ), + url( + regex=r'^documents/pages/(?P\d+)/navigation/first/$', + view=DocumentPageNavigationFirst.as_view(), + name='document_page_navigation_first' + ), + url( + regex=r'^documents/pages/(?P\d+)/navigation/last/$', + view=DocumentPageNavigationLast.as_view(), + name='document_page_navigation_last' + ), + url( + regex=r'^documents/pages/(?P\d+)/zoom/in/$', + view=DocumentPageZoomInView.as_view(), name='document_page_zoom_in' + ), + url( + regex=r'^documents/pages/(?P\d+)/zoom/out/$', + view=DocumentPageZoomOutView.as_view(), name='document_page_zoom_out' + ), + url( + regex=r'^documents/pages/(?P\d+)/rotate/left/$', + view=DocumentPageRotateLeftView.as_view(), + name='document_page_rotate_left' + ), + url( + regex=r'^documents/pages/(?P\d+)/rotate/right/$', + view=DocumentPageRotateRightView.as_view(), + name='document_page_rotate_right' + ), + url( + regex=r'^documents/pages/(?P\d+)/reset/$', + view=DocumentPageViewResetView.as_view(), + name='document_page_view_reset' + ), +] + +urlpatterns_document_versions = [ + url( + regex=r'^documents/(?P\d+)/versions/$', + view=DocumentVersionListView.as_view(), + name='document_version_list' + ), + url( + regex=r'^documents/versions/(?P\d+)/download/form/$', + view=DocumentVersionDownloadFormView.as_view(), + name='document_version_download_form' + ), + url( + regex=r'^documents/versions/(?P\d+)/$', + view=DocumentVersionView.as_view(), name='document_version_view' + ), + url( + regex=r'^documents/versions/(?P\d+)/download/$', + view=DocumentVersionDownloadView.as_view(), + name='document_version_download' + ), + url( + regex=r'^documents/versions/(?P\d+)/revert/$', + view=DocumentVersionRevertView.as_view(), + name='document_version_revert' + ), +] + +urlpatterns_tools = [ + url( + regex=r'^tools/documents/duplicated/scan/$', + view=ScanDuplicatedDocuments.as_view(), + name='duplicated_document_scan' + ), +] + +urlpatterns_trashed_documents = [ + url( + regex=r'^documents/(?P\d+)/trash/$', view=DocumentTrashView.as_view(), + name='document_trash' + ), + url( + regex=r'^documents/multiple/trash/$', view=DocumentTrashView.as_view(), + name='document_multiple_trash' + ), + url( + regex=r'^trashed_documents/$', view=TrashedDocumentListView.as_view(), + name='document_list_deleted' + ), + url( + regex=r'^trashed_documents/(?P\d+)/restore/$', + view=TrashedDocumentRestoreView.as_view(), name='document_restore' + ), + url( + regex=r'^trashed_documents/multiple/restore/$', view=TrashedDocumentRestoreView.as_view(), + name='document_multiple_restore' + ), + url( + regex=r'^trashed_documents/(?P\d+)/delete/$', + view=TrashedDocumentDeleteView.as_view(), name='document_delete' + ), + url( + regex=r'^trashed_documents/multiple/delete/$', + view=TrashedDocumentDeleteView.as_view(), + name='document_multiple_delete' + ), url( regex=r'^trash_can/empty/$', view=EmptyTrashCanView.as_view(), name='trash_can_empty' ), ] -urlpatterns_trashed_documents = [ - url( - regex=r'^(?P\d+)/trash/$', view=DocumentTrashView.as_view(), - name='document_trash' - ), - url( - regex=r'^multiple/trash/$', view=DocumentTrashView.as_view(), - name='document_multiple_trash' - ), - url( - regex=r'^list/deleted/$', view=TrashedDocumentListView.as_view(), - name='document_list_deleted' - ), - url( - regex=r'^(?P\d+)/restore/$', - view=TrashedDocumentRestoreView.as_view(), name='document_restore' - ), - url( - regex=r'^multiple/restore/$', view=TrashedDocumentRestoreView.as_view(), - name='document_multiple_restore' - ), - url( - regex=r'^(?P\d+)/delete/$', - view=TrashedDocumentDeleteView.as_view(), name='document_delete' - ), - url( - regex=r'^multiple/delete/$', - view=TrashedDocumentDeleteView.as_view(), - name='document_multiple_delete' - ), -] - -urlpatterns = [ - url( - regex=r'^list/$', view=DocumentListView.as_view(), name='document_list' - ), - url( - regex=r'^list/recent_access/$', - view=RecentAccessDocumentListView.as_view(), - name='document_list_recent_access' - ), - url( - regex=r'^list/recent_added/$', - view=RecentAddedDocumentListView.as_view(), - name='document_list_recent_added' - ), - url( - regex=r'^list/duplicated/$', - view=DuplicatedDocumentListView.as_view(), - name='duplicated_document_list' - ), - url( - regex=r'^(?P\d+)/preview/$', view=DocumentPreviewView.as_view(), - name='document_preview' - ), - url( - regex=r'^(?P\d+)/properties/$', view=DocumentView.as_view(), - name='document_properties' - ), - url( - regex=r'^(?P\d+)/duplicates/$', - view=DocumentDuplicatesListView.as_view(), - name='document_duplicates_list' - ), - url( - regex=r'^(?P\d+)/type/$', - view=DocumentDocumentTypeEditView.as_view(), - name='document_document_type_edit' - ), - url( - regex=r'^multiple/type/$', view=DocumentDocumentTypeEditView.as_view(), - name='document_multiple_document_type_edit' - ), - url( - regex=r'^(?P\d+)/edit/$', view=DocumentEditView.as_view(), - name='document_edit' - ), - url( - regex=r'^(?P\d+)/print/$', view=DocumentPrint.as_view(), - name='document_print' - ), - url( - regex=r'^(?P\d+)/reset_page_count/$', - view=DocumentUpdatePageCountView.as_view(), - name='document_update_page_count' - ), - url( - regex=r'^multiple/reset_page_count/$', - view=DocumentUpdatePageCountView.as_view(), - name='document_multiple_update_page_count' - ), - url( - regex=r'^(?P\d+)/download/form/$', - view=DocumentDownloadFormView.as_view(), name='document_download_form' - ), - url( - regex=r'^(?P\d+)/download/$', view=DocumentDownloadView.as_view(), - name='document_download' - ), - url( - regex=r'^multiple/download/form/$', - view=DocumentDownloadFormView.as_view(), - name='document_multiple_download_form' - ), - url( - regex=r'^multiple/download/$', view=DocumentDownloadView.as_view(), - name='document_multiple_download' - ), - url( - regex=r'^(?P\d+)/clear_transformations/$', - view=DocumentTransformationsClearView.as_view(), - name='document_clear_transformations' - ), - url( - regex=r'^(?P\d+)/clone_transformations/$', - view=DocumentTransformationsCloneView.as_view(), - name='document_clone_transformations' - ), - url( - regex=r'^(?P\d+)/version/all/$', - view=DocumentVersionListView.as_view(), - name='document_version_list' - ), - url( - regex=r'^document/version/(?P\d+)/download/form/$', - view=DocumentVersionDownloadFormView.as_view(), - name='document_version_download_form' - ), - url( - regex=r'^document/version/(?P\d+)/$', - view=DocumentVersionView.as_view(), name='document_version_view' - ), - url( - regex=r'^document/version/(?P\d+)/download/$', - view=DocumentVersionDownloadView.as_view(), - name='document_version_download' - ), - url( - regex=r'^document/version/(?P\d+)/revert/$', - view=DocumentVersionRevertView.as_view(), - name='document_version_revert' - ), - - url( - regex=r'^(?P\d+)/pages/all/$', view=DocumentPageListView.as_view(), - name='document_pages' - ), - - url( - regex=r'^multiple/clear_transformations/$', - view=DocumentTransformationsClearView.as_view(), - name='document_multiple_clear_transformations' - ), - url( - regex=r'^cache/clear/$', view=ClearImageCacheView.as_view(), - name='document_clear_image_cache' - ), - url( - regex=r'^page/(?P\d+)/$', view=DocumentPageView.as_view(), - name='document_page_view' - ), - url( - regex=r'^page/(?P\d+)/navigation/next/$', - view=DocumentPageNavigationNext.as_view(), - name='document_page_navigation_next' - ), - url( - regex=r'^page/(?P\d+)/navigation/previous/$', - view=DocumentPageNavigationPrevious.as_view(), - name='document_page_navigation_previous' - ), - url( - regex=r'^page/(?P\d+)/navigation/first/$', - view=DocumentPageNavigationFirst.as_view(), - name='document_page_navigation_first' - ), - url( - regex=r'^page/(?P\d+)/navigation/last/$', - view=DocumentPageNavigationLast.as_view(), - name='document_page_navigation_last' - ), - url( - regex=r'^page/(?P\d+)/zoom/in/$', - view=DocumentPageZoomInView.as_view(), name='document_page_zoom_in' - ), - url( - regex=r'^page/(?P\d+)/zoom/out/$', - view=DocumentPageZoomOutView.as_view(), name='document_page_zoom_out' - ), - url( - regex=r'^page/(?P\d+)/rotate/left/$', - view=DocumentPageRotateLeftView.as_view(), - name='document_page_rotate_left' - ), - url( - regex=r'^page/(?P\d+)/rotate/right/$', - view=DocumentPageRotateRightView.as_view(), - name='document_page_rotate_right' - ), - url( - regex=r'^page/(?P\d+)/reset/$', - view=DocumentPageViewResetView.as_view(), - name='document_page_view_reset' - ), - - # Tools - - url( - regex=r'^tools/documents/duplicated/scan/$', - view=ScanDuplicatedDocuments.as_view(), - name='duplicated_document_scan' - ), -] +urlpatterns = [] +urlpatterns.extend(urlpatterns_documents) +urlpatterns.extend(urlpatterns_document_pages) urlpatterns.extend(urlpatterns_document_types) +urlpatterns.extend(urlpatterns_document_versions) urlpatterns.extend(urlpatterns_favorite_documents) +urlpatterns.extend(urlpatterns_tools) urlpatterns.extend(urlpatterns_trashed_documents) api_urls = [ diff --git a/mayan/apps/documents/utils.py b/mayan/apps/documents/utils.py index 21b2df2107..e9ae97a24b 100644 --- a/mayan/apps/documents/utils.py +++ b/mayan/apps/documents/utils.py @@ -2,9 +2,17 @@ from __future__ import unicode_literals import pycountry +from django.apps import apps from django.utils.translation import ugettext_lazy as _ -from .settings import setting_language_codes +from .literals import DOCUMENT_IMAGES_CACHE_NAME + + +def callback_update_cache_size(setting): + Cache = apps.get_model(app_label='file_caching', model_name='Cache') + cache = Cache.objects.get(name=DOCUMENT_IMAGES_CACHE_NAME) + cache.maximum_size = setting.value + cache.save() def get_language(language_code): @@ -19,6 +27,8 @@ def get_language(language_code): def get_language_choices(): + from .settings import setting_language_codes + return sorted( [ ( diff --git a/mayan/apps/documents/views/document_page_views.py b/mayan/apps/documents/views/document_page_views.py index e3dac8c3b8..f05767d627 100644 --- a/mayan/apps/documents/views/document_page_views.py +++ b/mayan/apps/documents/views/document_page_views.py @@ -7,10 +7,12 @@ from furl import furl from django.contrib import messages from django.urls import reverse from django.utils.encoding import force_text -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _, ungettext from django.views.generic import RedirectView -from mayan.apps.common.generics import SimpleView, SingleObjectListView +from mayan.apps.common.generics import ( + MultipleObjectConfirmActionView, SimpleView, SingleObjectListView +) from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.common.settings import setting_home_view from mayan.apps.common.utils import resolve @@ -20,19 +22,20 @@ from ..forms import DocumentPageForm from ..icons import icon_document_pages from ..links import link_document_update_page_count from ..models import Document, DocumentPage -from ..permissions import permission_document_view +from ..permissions import permission_document_edit, permission_document_view from ..settings import ( setting_rotation_step, setting_zoom_percent_step, setting_zoom_max_level, setting_zoom_min_level ) __all__ = ( - 'DocumentPageListView', 'DocumentPageNavigationFirst', - 'DocumentPageNavigationLast', 'DocumentPageNavigationNext', - 'DocumentPageNavigationPrevious', 'DocumentPageView', - 'DocumentPageViewResetView', 'DocumentPageInteractiveTransformation', - 'DocumentPageZoomInView', 'DocumentPageZoomOutView', - 'DocumentPageRotateLeftView', 'DocumentPageRotateRightView' + 'DocumentPageDisable', 'DocumentPageEnable', 'DocumentPageListView', + 'DocumentPageNavigationFirst', 'DocumentPageNavigationLast', + 'DocumentPageNavigationNext', 'DocumentPageNavigationPrevious', + 'DocumentPageView', 'DocumentPageViewResetView', + 'DocumentPageInteractiveTransformation', 'DocumentPageZoomInView', + 'DocumentPageZoomOutView', 'DocumentPageRotateLeftView', + 'DocumentPageRotateRightView' ) logger = logging.getLogger(__name__) @@ -62,7 +65,7 @@ class DocumentPageListView(ExternalObjectMixin, SingleObjectListView): } def get_source_queryset(self): - return self.external_object.pages.all() + return self.external_object.pages_all class DocumentPageNavigationBase(ExternalObjectMixin, RedirectView): @@ -128,17 +131,17 @@ class DocumentPageNavigationNext(DocumentPageNavigationBase): def get_new_kwargs(self): document_page = self.get_object() - try: - document_page = document_page.siblings.get( - page_number=document_page.page_number + 1 - ) - except DocumentPage.DoesNotExist: + new_document_page = document_page.siblings.filter( + page_number__gt=document_page.page_number + ).first() + if new_document_page: + return {'pk': new_document_page.pk} + else: messages.warning( message=_( 'There are no more pages in this document' ), request=self.request ) - finally: return {'pk': document_page.pk} @@ -146,17 +149,17 @@ class DocumentPageNavigationPrevious(DocumentPageNavigationBase): def get_new_kwargs(self): document_page = self.get_object() - try: - document_page = document_page.siblings.get( - page_number=document_page.page_number - 1 - ) - except DocumentPage.DoesNotExist: + new_document_page = document_page.siblings.filter( + page_number__lt=document_page.page_number + ).last() + if new_document_page: + return {'pk': new_document_page.pk} + else: messages.warning( message=_( 'You are already at the first page of this document' ), request=self.request ) - finally: return {'pk': document_page.pk} @@ -261,3 +264,63 @@ class DocumentPageRotateRightView(DocumentPageInteractiveTransformation): query_dict['rotation'] = ( int(query_dict['rotation']) + setting_rotation_step.value ) % 360 + + +class DocumentPageDisable(MultipleObjectConfirmActionView): + object_permission = permission_document_edit + pk_url_kwarg = 'pk' + success_message_singular = '%(count)d document page disabled.' + success_message_plural = '%(count)d document pages disabled.' + + def get_extra_context(self): + queryset = self.object_list + + result = { + 'title': ungettext( + singular='Disable the selected document page?', + plural='Disable the selected document pages?', + number=queryset.count() + ) + } + + if queryset.count() == 1: + result['object'] = queryset.first() + + return result + + def get_source_queryset(self): + return DocumentPage.passthrough.all() + + def object_action(self, form, instance): + instance.enabled = False + instance.save() + + +class DocumentPageEnable(MultipleObjectConfirmActionView): + object_permission = permission_document_edit + pk_url_kwarg = 'pk' + success_message_singular = '%(count)d document page enabled.' + success_message_plural = '%(count)d document pages enabled.' + + def get_extra_context(self): + queryset = self.object_list + + result = { + 'title': ungettext( + singular='Enable the selected document page?', + plural='Enable the selected document pages?', + number=queryset.count() + ) + } + + if queryset.count() == 1: + result['object'] = queryset.first() + + return result + + def get_source_queryset(self): + return DocumentPage.passthrough.all() + + def object_action(self, form, instance): + instance.enabled = True + instance.save() diff --git a/mayan/apps/documents/views/document_views.py b/mayan/apps/documents/views/document_views.py index 4d2bf53ea6..566077c99c 100644 --- a/mayan/apps/documents/views/document_views.py +++ b/mayan/apps/documents/views/document_views.py @@ -2,8 +2,10 @@ from __future__ import absolute_import, unicode_literals import logging +from django.conf import settings from django.contrib import messages from django.core.exceptions import PermissionDenied +from django.db import transaction from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.urls import reverse @@ -17,7 +19,7 @@ from mayan.apps.common.generics import ( SingleObjectDetailView, SingleObjectDownloadView, SingleObjectEditView, SingleObjectListView ) -from mayan.apps.converter.models import Transformation +from mayan.apps.converter.layers import layer_saved_transformations from mayan.apps.converter.permissions import ( permission_transformation_delete, permission_transformation_edit ) @@ -522,7 +524,7 @@ class DocumentTransformationsClearView(MultipleObjectConfirmActionView): def object_action(self, form, instance): try: for page in instance.pages.all(): - Transformation.objects.get_for_object(obj=page).delete() + layer_saved_transformations.get_transformations_for(obj=page).delete() except Exception as exception: messages.error( self.request, _( @@ -545,24 +547,29 @@ class DocumentTransformationsCloneView(FormView): pk=form.cleaned_data['page'].pk ) - for page in target_pages: - Transformation.objects.get_for_object(obj=page).delete() + with transaction.atomic(): + for page in target_pages: + layer_saved_transformations.get_transformations_for(obj=page).delete() - Transformation.objects.copy( - source=form.cleaned_data['page'], targets=target_pages - ) + layer_saved_transformations.copy_transformations( + source=form.cleaned_data['page'], targets=target_pages + ) except Exception as exception: - messages.error( - self.request, _( - 'Error deleting the page transformations for ' - 'document: %(document)s; %(error)s.' - ) % { - 'document': instance, 'error': exception - } - ) + if settings.DEBUG: + raise + else: + messages.error( + message=_( + 'Error cloning the page transformations for ' + 'document: %(document)s; %(error)s.' + ) % { + 'document': instance, 'error': exception + }, request=self.request + ) else: messages.success( - self.request, _('Transformations cloned successfully.') + message=_('Transformations cloned successfully.'), + request=self.request ) return super(DocumentTransformationsCloneView, self).form_valid(form=form) diff --git a/mayan/apps/documents/views/misc_views.py b/mayan/apps/documents/views/misc_views.py index 0f2bf4fab8..b1f1712187 100644 --- a/mayan/apps/documents/views/misc_views.py +++ b/mayan/apps/documents/views/misc_views.py @@ -8,25 +8,12 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.common.generics import ConfirmView from ..permissions import permission_document_tools -from ..tasks import task_clear_image_cache, task_scan_duplicates_all +from ..tasks import task_scan_duplicates_all -__all__ = ('ClearImageCacheView', 'ScanDuplicatedDocuments') +__all__ = ('ScanDuplicatedDocuments',) logger = logging.getLogger(__name__) -class ClearImageCacheView(ConfirmView): - extra_context = { - 'title': _('Clear the document image cache?') - } - view_permission = permission_document_tools - - def view_action(self): - task_clear_image_cache.apply_async() - messages.success( - self.request, _('Document cache clearing queued successfully.') - ) - - class ScanDuplicatedDocuments(ConfirmView): extra_context = { 'title': _('Scan for duplicated documents?') @@ -36,5 +23,6 @@ class ScanDuplicatedDocuments(ConfirmView): def view_action(self): task_scan_duplicates_all.apply_async() messages.success( - self.request, _('Duplicated document scan queued successfully.') + message=_('Duplicated document scan queued successfully.'), + request=self.request ) diff --git a/mayan/apps/dynamic_search/tests/test_api.py b/mayan/apps/dynamic_search/tests/test_api.py index 7d1ef9290c..811065c39a 100644 --- a/mayan/apps/dynamic_search/tests/test_api.py +++ b/mayan/apps/dynamic_search/tests/test_api.py @@ -4,8 +4,8 @@ from rest_framework import status from mayan.apps.documents.search import document_search from mayan.apps.documents.permissions import permission_document_view -from mayan.apps.documents.tests import DocumentTestMixin -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.documents.tests.base import DocumentTestMixin +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..classes import SearchModel diff --git a/mayan/apps/dynamic_search/tests/test_models.py b/mayan/apps/dynamic_search/tests/test_models.py index c322d7d0fb..679311c669 100644 --- a/mayan/apps/dynamic_search/tests/test_models.py +++ b/mayan/apps/dynamic_search/tests/test_models.py @@ -2,10 +2,10 @@ from __future__ import unicode_literals from django.utils.encoding import force_text -from mayan.apps.common.tests import BaseTestCase +from mayan.apps.common.tests.base import BaseTestCase from mayan.apps.documents.permissions import permission_document_view from mayan.apps.documents.search import document_search -from mayan.apps.documents.tests import DocumentTestMixin +from mayan.apps.documents.tests.mixins import DocumentTestMixin class DocumentSearchTestCase(DocumentTestMixin, BaseTestCase): diff --git a/mayan/apps/dynamic_search/tests/test_views.py b/mayan/apps/dynamic_search/tests/test_views.py index 310f27481b..885c9ada42 100644 --- a/mayan/apps/dynamic_search/tests/test_views.py +++ b/mayan/apps/dynamic_search/tests/test_views.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from mayan.apps.documents.permissions import permission_document_view from mayan.apps.documents.search import document_search -from mayan.apps.documents.tests import DocumentTestMixin +from mayan.apps.documents.tests.mixins import DocumentTestMixin class Issue46TestCase(DocumentTestMixin, GenericViewTestCase): diff --git a/mayan/apps/events/apps.py b/mayan/apps/events/apps.py index 85dc255894..ae9069c7f1 100644 --- a/mayan/apps/events/apps.py +++ b/mayan/apps/events/apps.py @@ -7,7 +7,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_main, menu_object, menu_secondary, menu_tools, menu_user + menu_object, menu_secondary, menu_tools, menu_topbar, menu_user ) from mayan.apps.navigation.classes import SourceColumn @@ -87,7 +87,7 @@ class EventsApp(MayanAppConfig): source=Notification, widget=TwoStateWidget ) - menu_main.bind_links( + menu_topbar.bind_links( links=(link_user_notifications_list,), position=99 ) menu_object.bind_links( diff --git a/mayan/apps/events/tests/test_api.py b/mayan/apps/events/tests/test_api.py index 1312f8e741..185b46c9e4 100644 --- a/mayan/apps/events/tests/test_api.py +++ b/mayan/apps/events/tests/test_api.py @@ -4,8 +4,8 @@ from django.contrib.contenttypes.models import ContentType from rest_framework import status -from mayan.apps.documents.tests import DocumentTestMixin -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.documents.tests.mixins import DocumentTestMixin +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..permissions import permission_events_view diff --git a/mayan/apps/events/tests/test_views.py b/mayan/apps/events/tests/test_views.py index c600524f6f..f49cb8fbfe 100644 --- a/mayan/apps/events/tests/test_views.py +++ b/mayan/apps/events/tests/test_views.py @@ -2,8 +2,8 @@ from __future__ import absolute_import, unicode_literals from django.contrib.contenttypes.models import ContentType -from mayan.apps.common.tests import GenericViewTestCase -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from ..permissions import permission_events_view diff --git a/mayan/apps/events/urls.py b/mayan/apps/events/urls.py index f46d2b7ad2..457345c4a4 100644 --- a/mayan/apps/events/urls.py +++ b/mayan/apps/events/urls.py @@ -14,44 +14,55 @@ from .views import ( VerbEventListView ) -urlpatterns = [ - url(regex=r'^all/$', view=EventListView.as_view(), name='events_list'), +urlpatterns_events = [ + url(regex=r'^events/$', view=EventListView.as_view(), name='events_list'), url( - regex=r'^for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', + regex=r'^object/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/events/$', view=ObjectEventListView.as_view(), name='events_for_object' ), url( - regex=r'^by_verb/(?P[\w\-\.]+)/$', + regex=r'^verbs/(?P[\w\-\.]+)/$', view=VerbEventListView.as_view(), name='events_by_verb' ), - url( - regex=r'^notifications/(?P\d+)/mark_read/$', - view=NotificationMarkRead.as_view(), name='notification_mark_read' - ), - url( - regex=r'^notifications/all/mark_read/$', - view=NotificationMarkReadAll.as_view(), name='notification_mark_read_all' - ), url( regex=r'^user/events/$', name='current_user_events', view=CurrentUserEventListView.as_view() ), +] + +urlpatterns_notification = [ url( - regex=r'^user/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/subscriptions/$', - view=ObjectEventTypeSubscriptionListView.as_view(), - name='object_event_types_user_subcriptions_list' + regex=r'^user/notifications/$', view=NotificationListView.as_view(), + name='user_notifications_list' ), + url( + regex=r'^user/notifications/(?P\d+)/mark_read/$', + view=NotificationMarkRead.as_view(), name='notification_mark_read' + ), + url( + regex=r'^user/notifications/all/mark_read/$', + view=NotificationMarkReadAll.as_view(), name='notification_mark_read_all' + ), +] + +urlpatterns_subscriptions = [ url( regex=r'^user/event_types/subscriptions/$', view=EventTypeSubscriptionListView.as_view(), name='event_types_user_subcriptions_list' ), url( - regex=r'^user/notifications/$', view=NotificationListView.as_view(), - name='user_notifications_list' + regex=r'^user/object/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/subscriptions/$', + view=ObjectEventTypeSubscriptionListView.as_view(), + name='object_event_types_user_subcriptions_list' ), ] +urlpatterns = [] +urlpatterns.extend(urlpatterns_events) +urlpatterns.extend(urlpatterns_notification) +urlpatterns.extend(urlpatterns_subscriptions) + api_urls = [ url( regex=r'^event_type_namespaces/(?P[-\w]+)/$', diff --git a/mayan/apps/file_caching/__init__.py b/mayan/apps/file_caching/__init__.py new file mode 100644 index 0000000000..606c594dcf --- /dev/null +++ b/mayan/apps/file_caching/__init__.py @@ -0,0 +1,3 @@ +from __future__ import unicode_literals + +default_app_config = 'mayan.apps.file_caching.apps.FileCachingConfig' diff --git a/mayan/apps/file_caching/admin.py b/mayan/apps/file_caching/admin.py new file mode 100644 index 0000000000..a807f197c9 --- /dev/null +++ b/mayan/apps/file_caching/admin.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals + +from django.contrib import admin + +from .models import Cache + + +@admin.register(Cache) +class CacheAdmin(admin.ModelAdmin): + list_display = ('name', 'label', 'storage_instance_path', 'maximum_size') diff --git a/mayan/apps/file_caching/apps.py b/mayan/apps/file_caching/apps.py new file mode 100644 index 0000000000..2e87197712 --- /dev/null +++ b/mayan/apps/file_caching/apps.py @@ -0,0 +1,84 @@ +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.acls.classes import ModelPermission +from mayan.apps.acls.links import link_acl_list +from mayan.apps.acls.permissions import permission_acl_edit, permission_acl_view +from mayan.apps.common.apps import MayanAppConfig +from mayan.apps.common.menus import ( + menu_list_facet, menu_multi_item, menu_object, menu_secondary, menu_tools +) +from mayan.apps.events.classes import ModelEventType +from mayan.apps.events.links import ( + link_events_for_object, link_object_event_types_user_subcriptions_list +) +from mayan.apps.navigation.classes import SourceColumn + +from .events import event_cache_edited, event_cache_purged +from .links import ( + link_caches_list, link_cache_multiple_purge, link_cache_purge +) +from .permissions import permission_cache_purge, permission_cache_view + + +class FileCachingConfig(MayanAppConfig): + app_namespace = 'file_caching' + app_url = 'file_caching' + has_tests = True + name = 'mayan.apps.file_caching' + verbose_name = _('File caching') + + def ready(self): + super(FileCachingConfig, self).ready() + from actstream import registry + + Cache = self.get_model(model_name='Cache') + + ModelEventType.register( + event_types=(event_cache_edited, event_cache_purged,), + model=Cache + ) + + ModelPermission.register( + model=Cache, permissions=( + permission_acl_edit, permission_acl_view, + permission_cache_purge, permission_cache_view + ) + ) + + SourceColumn(attribute='label', is_sortable=True, source=Cache) + SourceColumn(attribute='name', is_sortable=True, source=Cache) + SourceColumn( + attribute='storage_instance_path', is_sortable=True, source=Cache + ) + SourceColumn( + attribute='get_maximum_size_display', is_sortable=True, + sort_field='maximum_size', source=Cache + ) + SourceColumn(attribute='get_total_size_display', source=Cache) + + menu_list_facet.bind_links( + links=( + link_acl_list, link_events_for_object, + link_object_event_types_user_subcriptions_list, + ), sources=(Cache,) + ) + + menu_object.bind_links( + links=(link_cache_purge,), + sources=(Cache,) + ) + menu_multi_item.bind_links( + links=(link_cache_multiple_purge,), + sources=(Cache,) + ) + menu_secondary.bind_links( + links=(link_caches_list,), sources=( + Cache, + ) + ) + + menu_tools.bind_links(links=(link_caches_list,)) + + registry.register(Cache) diff --git a/mayan/apps/file_caching/events.py b/mayan/apps/file_caching/events.py new file mode 100644 index 0000000000..529710d64b --- /dev/null +++ b/mayan/apps/file_caching/events.py @@ -0,0 +1,19 @@ +from __future__ import absolute_import, unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.events.classes import EventTypeNamespace + +namespace = EventTypeNamespace( + label=_('File caching'), name='file_caching' +) + +event_cache_created = namespace.add_event_type( + label=_('Cache created'), name='cache_created' +) +event_cache_edited = namespace.add_event_type( + label=_('Cache edited'), name='cache_edited' +) +event_cache_purged = namespace.add_event_type( + label=_('Cache purge'), name='cache_purged' +) diff --git a/mayan/apps/file_caching/icons.py b/mayan/apps/file_caching/icons.py new file mode 100644 index 0000000000..94b4287456 --- /dev/null +++ b/mayan/apps/file_caching/icons.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import, unicode_literals + +from mayan.apps.appearance.classes import Icon + +icon_file_caching = Icon( + driver_name='fontawesome', symbol='warehouse' +) +icon_cache_purge = Icon( + driver_name='fontawesome-dual', primary_symbol='warehouse', + secondary_symbol='check' +) diff --git a/mayan/apps/file_caching/links.py b/mayan/apps/file_caching/links.py new file mode 100644 index 0000000000..b0bc57cae4 --- /dev/null +++ b/mayan/apps/file_caching/links.py @@ -0,0 +1,22 @@ +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.navigation.classes import Link + +from .icons import icon_cache_purge, icon_file_caching +from .permissions import permission_cache_purge, permission_cache_view + +link_caches_list = Link( + icon_class=icon_file_caching, permissions=(permission_cache_view,), + text=_('File caches'), view='file_caching:cache_list' +) +link_cache_purge = Link( + icon_class=icon_cache_purge, kwargs={'cache_id': 'resolved_object.id'}, + permissions=(permission_cache_purge,), text=_('Purge cache'), + view='file_caching:cache_purge' +) +link_cache_multiple_purge = Link( + icon_class=icon_cache_purge, text=_('Purge cache'), + view='file_caching:cache_multiple_purge' +) diff --git a/mayan/apps/file_caching/locale/ar/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/ar/LC_MESSAGES/django.po new file mode 100644 index 0000000000..173a9d5ee2 --- /dev/null +++ b/mayan/apps/file_caching/locale/ar/LC_MESSAGES/django.po @@ -0,0 +1,72 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/bg/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/bg/LC_MESSAGES/django.po new file mode 100644 index 0000000000..4812952c38 --- /dev/null +++ b/mayan/apps/file_caching/locale/bg/LC_MESSAGES/django.po @@ -0,0 +1,71 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/bs_BA/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/bs_BA/LC_MESSAGES/django.po new file mode 100644 index 0000000000..b3bd609bb3 --- /dev/null +++ b/mayan/apps/file_caching/locale/bs_BA/LC_MESSAGES/django.po @@ -0,0 +1,70 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/da/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/da/LC_MESSAGES/django.po new file mode 100644 index 0000000000..4812952c38 --- /dev/null +++ b/mayan/apps/file_caching/locale/da/LC_MESSAGES/django.po @@ -0,0 +1,71 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/de_DE/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/de_DE/LC_MESSAGES/django.po new file mode 100644 index 0000000000..b3bd609bb3 --- /dev/null +++ b/mayan/apps/file_caching/locale/de_DE/LC_MESSAGES/django.po @@ -0,0 +1,70 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/en/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000000..b3bd609bb3 --- /dev/null +++ b/mayan/apps/file_caching/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,70 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/es/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/es/LC_MESSAGES/django.po new file mode 100644 index 0000000000..4812952c38 --- /dev/null +++ b/mayan/apps/file_caching/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,71 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/fa/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/fa/LC_MESSAGES/django.po new file mode 100644 index 0000000000..09b38b103d --- /dev/null +++ b/mayan/apps/file_caching/locale/fa/LC_MESSAGES/django.po @@ -0,0 +1,71 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/fr/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000000..c65665a79d --- /dev/null +++ b/mayan/apps/file_caching/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,71 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/hu/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/hu/LC_MESSAGES/django.po new file mode 100644 index 0000000000..4812952c38 --- /dev/null +++ b/mayan/apps/file_caching/locale/hu/LC_MESSAGES/django.po @@ -0,0 +1,71 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/id/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/id/LC_MESSAGES/django.po new file mode 100644 index 0000000000..09b38b103d --- /dev/null +++ b/mayan/apps/file_caching/locale/id/LC_MESSAGES/django.po @@ -0,0 +1,71 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/it/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/it/LC_MESSAGES/django.po new file mode 100644 index 0000000000..4812952c38 --- /dev/null +++ b/mayan/apps/file_caching/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,71 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/nl_NL/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/nl_NL/LC_MESSAGES/django.po new file mode 100644 index 0000000000..b3bd609bb3 --- /dev/null +++ b/mayan/apps/file_caching/locale/nl_NL/LC_MESSAGES/django.po @@ -0,0 +1,70 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/pl/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/pl/LC_MESSAGES/django.po new file mode 100644 index 0000000000..2f8fc8f37b --- /dev/null +++ b/mayan/apps/file_caching/locale/pl/LC_MESSAGES/django.po @@ -0,0 +1,73 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n" +"%100<12 || n%100>=14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n" +"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/pt/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/pt/LC_MESSAGES/django.po new file mode 100644 index 0000000000..4812952c38 --- /dev/null +++ b/mayan/apps/file_caching/locale/pt/LC_MESSAGES/django.po @@ -0,0 +1,71 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/pt_BR/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 0000000000..c65665a79d --- /dev/null +++ b/mayan/apps/file_caching/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,71 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/ro_RO/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/ro_RO/LC_MESSAGES/django.po new file mode 100644 index 0000000000..b3bd609bb3 --- /dev/null +++ b/mayan/apps/file_caching/locale/ro_RO/LC_MESSAGES/django.po @@ -0,0 +1,70 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/ru/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 0000000000..b73a01bbe4 --- /dev/null +++ b/mayan/apps/file_caching/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,73 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" +"%100>=11 && n%100<=14)? 2 : 3);\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/sl_SI/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/sl_SI/LC_MESSAGES/django.po new file mode 100644 index 0000000000..b3bd609bb3 --- /dev/null +++ b/mayan/apps/file_caching/locale/sl_SI/LC_MESSAGES/django.po @@ -0,0 +1,70 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/tr_TR/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/tr_TR/LC_MESSAGES/django.po new file mode 100644 index 0000000000..b3bd609bb3 --- /dev/null +++ b/mayan/apps/file_caching/locale/tr_TR/LC_MESSAGES/django.po @@ -0,0 +1,70 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/vi_VN/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/vi_VN/LC_MESSAGES/django.po new file mode 100644 index 0000000000..b3bd609bb3 --- /dev/null +++ b/mayan/apps/file_caching/locale/vi_VN/LC_MESSAGES/django.po @@ -0,0 +1,70 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/locale/zh_CN/LC_MESSAGES/django.po b/mayan/apps/file_caching/locale/zh_CN/LC_MESSAGES/django.po new file mode 100644 index 0000000000..b3bd609bb3 --- /dev/null +++ b/mayan/apps/file_caching/locale/zh_CN/LC_MESSAGES/django.po @@ -0,0 +1,70 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-07 21:32-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: models.py:23 models.py:70 +msgid "Name" +msgstr "" + +#: models.py:25 +msgid "Label" +msgstr "" + +#: models.py:26 +msgid "Maximum size" +msgstr "" + +#: models.py:28 +msgid "Storage instance path" +msgstr "" + +#: models.py:32 models.py:67 +msgid "Cache" +msgstr "" + +#: models.py:33 +msgid "Caches" +msgstr "" + +#: models.py:75 models.py:141 +msgid "Cache partition" +msgstr "" + +#: models.py:76 +msgid "Cache partitions" +msgstr "" + +#: models.py:144 +msgid "Date time" +msgstr "" + +#: models.py:146 +msgid "Filename" +msgstr "" + +#: models.py:148 +msgid "File size" +msgstr "" + +#: models.py:154 +msgid "Cache partition file" +msgstr "" + +#: models.py:155 +msgid "Cache partition files" +msgstr "" diff --git a/mayan/apps/file_caching/migrations/0001_initial.py b/mayan/apps/file_caching/migrations/0001_initial.py new file mode 100644 index 0000000000..46c20637ab --- /dev/null +++ b/mayan/apps/file_caching/migrations/0001_initial.py @@ -0,0 +1,64 @@ +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Cache', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128, unique=True, verbose_name='Name')), + ('label', models.CharField(max_length=128, verbose_name='Label')), + ('maximum_size', models.PositiveIntegerField(verbose_name='Maximum size')), + ('storage_instance_path', models.CharField(max_length=255, unique=True, verbose_name='Storage instance path')), + ], + options={ + 'verbose_name': 'Cache', + 'verbose_name_plural': 'Caches', + }, + ), + migrations.CreateModel( + name='CachePartition', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('cache', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='partitions', to='file_caching.Cache', verbose_name='Cache')), + ], + options={ + 'verbose_name': 'Cache partition', + 'verbose_name_plural': 'Cache partitions', + }, + ), + migrations.CreateModel( + name='CachePartitionFile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('datetime', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Date time')), + ('filename', models.CharField(max_length=255, verbose_name='Filename')), + ('file_size', models.PositiveIntegerField(default=0, verbose_name='File size')), + ('partition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='file_caching.CachePartition', verbose_name='Cache partition')), + ], + options={ + 'get_latest_by': 'datetime', + 'verbose_name': 'Cache partition file', + 'verbose_name_plural': 'Cache partition files', + }, + ), + migrations.AlterUniqueTogether( + name='cachepartitionfile', + unique_together=set([('partition', 'filename')]), + ), + migrations.AlterUniqueTogether( + name='cachepartition', + unique_together=set([('cache', 'name')]), + ), + ] diff --git a/mayan/apps/file_caching/migrations/0002_auto_20190729_0236.py b/mayan/apps/file_caching/migrations/0002_auto_20190729_0236.py new file mode 100644 index 0000000000..86643859fb --- /dev/null +++ b/mayan/apps/file_caching/migrations/0002_auto_20190729_0236.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-07-29 02:36 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('file_caching', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='cache', + name='label', + field=models.CharField(help_text='A short text describing the cache.', max_length=128, verbose_name='Label'), + ), + migrations.AlterField( + model_name='cache', + name='maximum_size', + field=models.PositiveIntegerField(help_text='Maximum size of the cache in bytes.', verbose_name='Maximum size'), + ), + migrations.AlterField( + model_name='cache', + name='name', + field=models.CharField(help_text='Internal name of the cache.', max_length=128, unique=True, verbose_name='Name'), + ), + migrations.AlterField( + model_name='cache', + name='storage_instance_path', + field=models.CharField(help_text='Dotted path to the actual storage class used for the cache.', max_length=255, unique=True, verbose_name='Storage instance path'), + ), + ] diff --git a/mayan/apps/file_caching/migrations/__init__.py b/mayan/apps/file_caching/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mayan/apps/file_caching/models.py b/mayan/apps/file_caching/models.py new file mode 100644 index 0000000000..2aaf942a32 --- /dev/null +++ b/mayan/apps/file_caching/models.py @@ -0,0 +1,243 @@ +from __future__ import unicode_literals + +from contextlib import contextmanager +import logging + +from django.core.files.base import ContentFile +from django.db import models, transaction +from django.db.models import Sum +from django.template.defaultfilters import filesizeformat +from django.utils.encoding import python_2_unicode_compatible +from django.utils.functional import cached_property +from django.utils.module_loading import import_string +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.lock_manager.exceptions import LockError +from mayan.apps.lock_manager.runtime import locking_backend + +from .events import ( + event_cache_created, event_cache_edited, event_cache_purged +) + +logger = logging.getLogger(__name__) + + +@python_2_unicode_compatible +class Cache(models.Model): + name = models.CharField( + db_index=True, help_text=_('Internal name of the cache.'), + max_length=128, unique=True, verbose_name=_('Name') + ) + label = models.CharField( + help_text=_('A short text describing the cache.'), max_length=128, + verbose_name=_('Label') + ) + maximum_size = models.PositiveIntegerField( + help_text=_('Maximum size of the cache in bytes.'), + verbose_name=_('Maximum size') + ) + storage_instance_path = models.CharField( + help_text=_( + 'Dotted path to the actual storage class used for the cache.' + ), max_length=255, unique=True, verbose_name=_('Storage instance path') + ) + + class Meta: + verbose_name = _('Cache') + verbose_name_plural = _('Caches') + + def __str__(self): + return self.label + + def get_files(self): + return CachePartitionFile.objects.filter(partition__cache__id=self.pk) + + def get_maximum_size_display(self): + return filesizeformat(bytes_=self.maximum_size) + + get_maximum_size_display.help_text = _( + 'Size at which the cache will start deleting old entries.' + ) + get_maximum_size_display.short_description = _('Maximum size') + + def get_total_size(self): + """ + Return the actual usage of the cache. + """ + return self.get_files().aggregate( + file_size__sum=Sum('file_size') + )['file_size__sum'] or 0 + + def get_total_size_display(self): + return filesizeformat(bytes_=self.get_total_size()) + + get_total_size_display.short_description = _('Total size') + get_total_size_display.help_text = _('Current size of the cache.') + + def prune(self): + """ + Deletes files until the total size of the cache is below the allowed + maximum size of the cache. + """ + while self.get_total_size() > self.maximum_size: + self.get_files().earliest().delete() + + def purge(self, _user=None): + """ + Deletes the entire cache. + """ + for partition in self.partitions.all(): + partition.purge() + + event_cache_purged.commit(actor=_user, target=self) + + def save(self, *args, **kwargs): + _user = kwargs.pop('_user', None) + with transaction.atomic(): + is_new = not self.pk + result = super(Cache, self).save(*args, **kwargs) + if is_new: + event_cache_created.commit( + actor=_user, target=self + ) + else: + event_cache_edited.commit( + actor=_user, target=self + ) + + self.prune() + return result + + @cached_property + def storage(self): + return import_string(self.storage_instance_path) + + +class CachePartition(models.Model): + cache = models.ForeignKey( + on_delete=models.CASCADE, related_name='partitions', + to=Cache, verbose_name=_('Cache') + ) + name = models.CharField( + max_length=128, verbose_name=_('Name') + ) + + class Meta: + unique_together = ('cache', 'name') + verbose_name = _('Cache partition') + verbose_name_plural = _('Cache partitions') + + @staticmethod + def get_combined_filename(parent, filename): + return '{}-{}'.format(parent, filename) + + @contextmanager + def create_file(self, filename): + lock_id = 'cache_partition-create_file-{}-{}'.format(self.pk, filename) + try: + logger.debug('trying to acquire lock: %s', lock_id) + lock = locking_backend.acquire_lock(lock_id) + logger.debug('acquired lock: %s', lock_id) + try: + self.cache.prune() + + # Since open "wb+" doesn't create files force the creation of an + # empty file. + self.cache.storage.delete( + name=self.get_full_filename(filename=filename) + ) + self.cache.storage.save( + name=self.get_full_filename(filename=filename), + content=ContentFile(content='') + ) + + try: + with transaction.atomic(): + partition_file = self.files.create(filename=filename) + yield partition_file.open(mode='wb') + partition_file.update_size() + except Exception as exception: + logger.error( + 'Unexpected exception while trying to save new ' + 'cache file; %s', exception + ) + self.cache.storage.delete( + name=self.get_full_filename(filename=filename) + ) + raise + finally: + lock.release() + except LockError: + logger.debug('unable to obtain lock: %s' % lock_id) + raise + + def delete(self, *args, **kwargs): + self.purge() + return super(CachePartition, self).delete(*args, **kwargs) + + def get_file(self, filename): + try: + return self.files.get(filename=filename) + except self.files.model.DoesNotExist: + return None + + def get_full_filename(self, filename): + return CachePartition.get_combined_filename( + parent=self.name, filename=filename + ) + + def purge(self): + for parition_file in self.files.all(): + parition_file.delete() + + +class CachePartitionFile(models.Model): + partition = models.ForeignKey( + on_delete=models.CASCADE, related_name='files', + to=CachePartition, verbose_name=_('Cache partition') + ) + datetime = models.DateTimeField( + auto_now_add=True, db_index=True, verbose_name=_('Date time') + ) + filename = models.CharField(max_length=255, verbose_name=_('Filename')) + file_size = models.PositiveIntegerField( + default=0, verbose_name=_('File size') + ) + + class Meta: + get_latest_by = 'datetime' + unique_together = ('partition', 'filename') + verbose_name = _('Cache partition file') + verbose_name_plural = _('Cache partition files') + + def delete(self, *args, **kwargs): + self.partition.cache.storage.delete(name=self.full_filename) + return super(CachePartitionFile, self).delete(*args, **kwargs) + + def exists(self): + return self.partition.cache.storage.exists(name=self.full_filename) + + @cached_property + def full_filename(self): + return CachePartition.get_combined_filename( + parent=self.partition.name, filename=self.filename + ) + + def open(self, mode='rb'): + # Open the file for reading. If the file is written to, the + # .update_size() must be called. + try: + return self.partition.cache.storage.open( + name=self.full_filename, mode=mode + ) + except Exception as exception: + logger.error( + 'Unexpected exception opening the cache file; %s', exception + ) + raise + + def update_size(self): + self.file_size = self.partition.cache.storage.size( + name=self.full_filename + ) + self.save() diff --git a/mayan/apps/file_caching/permissions.py b/mayan/apps/file_caching/permissions.py new file mode 100644 index 0000000000..4c2609ee10 --- /dev/null +++ b/mayan/apps/file_caching/permissions.py @@ -0,0 +1,14 @@ +from __future__ import absolute_import, unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.permissions import PermissionNamespace + +namespace = PermissionNamespace(label=_('File caching'), name='file_caching') + +permission_cache_purge = namespace.add_permission( + label=_('Purge a file cache'), name='file_caching_cache_purge' +) +permission_cache_view = namespace.add_permission( + label=_('View a file cache'), name='file_caching_cache_view' +) diff --git a/mayan/apps/file_caching/queues.py b/mayan/apps/file_caching/queues.py new file mode 100644 index 0000000000..77d8af40cb --- /dev/null +++ b/mayan/apps/file_caching/queues.py @@ -0,0 +1,10 @@ +from __future__ import absolute_import, unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.common.queues import queue_tools + +queue_tools.add_task_type( + dotted_path='mayan.apps.file_caching.tasks.task_cache_purge', + label=_('Purge a file cache') +) diff --git a/mayan/apps/file_caching/tasks.py b/mayan/apps/file_caching/tasks.py new file mode 100644 index 0000000000..06696a1c9d --- /dev/null +++ b/mayan/apps/file_caching/tasks.py @@ -0,0 +1,25 @@ +from __future__ import unicode_literals + +import logging + +from django.apps import apps +from django.contrib.auth import get_user_model + +from mayan.celery import app + +logger = logging.getLogger(__name__) + + +@app.task(ignore_result=True) +def task_cache_purge(cache_id, user_id=None): + Cache = apps.get_model( + app_label='file_caching', model_name='Cache' + ) + User = get_user_model() + + cache = Cache.objects.get(pk=cache_id) + user = User.objects.get(pk=user_id) + + logger.info('Starting cache id %s purge', cache) + cache.purge(_user=user) + logger.info('Finished cache id %s purge', cache) diff --git a/mayan/apps/file_caching/tests/__init__.py b/mayan/apps/file_caching/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mayan/apps/file_caching/tests/literals.py b/mayan/apps/file_caching/tests/literals.py new file mode 100644 index 0000000000..5d9d8ca140 --- /dev/null +++ b/mayan/apps/file_caching/tests/literals.py @@ -0,0 +1,9 @@ +from __future__ import unicode_literals + +TEST_CACHE_LABEL = 'test cache label' +TEST_CACHE_MAXIMUM_SIZE = 2 * 2 ** 20 # 2 Megabyte +TEST_CACHE_NAME = 'test_cache_name' +TEST_CACHE_PARTITION_FILE_FILENAME = 'test_cache_partition_file_filename' +TEST_CACHE_PARTITION_FILE_SIZE = 1 * 2 ** 20 # 1 Megabyte +TEST_CACHE_PARTITION_NAME = 'test_cache_partition_name' +TEST_CACHE_STORAGE_INSTANCE_PATH = 'mayan.apps.file_caching.tests.mixins.test_storage' diff --git a/mayan/apps/file_caching/tests/mixins.py b/mayan/apps/file_caching/tests/mixins.py new file mode 100644 index 0000000000..8afe491dba --- /dev/null +++ b/mayan/apps/file_caching/tests/mixins.py @@ -0,0 +1,70 @@ +from __future__ import unicode_literals + +from django.core.files.storage import FileSystemStorage +from django.utils.encoding import force_bytes + +from mayan.apps.storage.utils import fs_cleanup, mkdtemp + +from ..models import Cache + +from .literals import ( + TEST_CACHE_LABEL, TEST_CACHE_MAXIMUM_SIZE, TEST_CACHE_NAME, + TEST_CACHE_PARTITION_FILE_FILENAME, TEST_CACHE_PARTITION_FILE_SIZE, + TEST_CACHE_PARTITION_NAME, TEST_CACHE_STORAGE_INSTANCE_PATH +) + +test_storage = None + + +class CacheTestMixin(object): + def setUp(self): + super(CacheTestMixin, self).setUp() + global test_storage + self.temporary_directory = mkdtemp() + test_storage = FileSystemStorage(location=self.temporary_directory) + + def tearDown(self): + fs_cleanup(filename=self.temporary_directory) + super(CacheTestMixin, self).tearDown() + + def _create_test_cache(self): + self.test_cache = Cache.objects.create( + label=TEST_CACHE_LABEL, + storage_instance_path=TEST_CACHE_STORAGE_INSTANCE_PATH, + maximum_size=TEST_CACHE_MAXIMUM_SIZE, + name=TEST_CACHE_NAME, + ) + + def _create_test_cache_partition(self): + self.test_cache_partition = self.test_cache.partitions.create( + name=TEST_CACHE_PARTITION_NAME + ) + + def _create_test_cache_partition_file(self): + with self.test_cache_partition.create_file(filename=TEST_CACHE_PARTITION_FILE_FILENAME) as file_object: + file_object.write( + force_bytes(' ' * TEST_CACHE_PARTITION_FILE_SIZE) + ) + + self.test_cache_partition_file = self.test_cache_partition.files.get( + filename=TEST_CACHE_PARTITION_FILE_FILENAME + ) + + +class CacheViewTestMixin(object): + def _request_test_cache_list_view(self): + return self.get(viewname='file_caching:cache_list') + + def _request_test_cache_purge_view(self): + return self.post( + viewname='file_caching:cache_purge', kwargs={ + 'cache_id': self.test_cache.pk + } + ) + + def _request_test_cache_multiple_purge_view(self): + return self.post( + viewname='file_caching:cache_multiple_purge', data={ + 'id_list': self.test_cache.pk + } + ) diff --git a/mayan/apps/file_caching/tests/test_events.py b/mayan/apps/file_caching/tests/test_events.py new file mode 100644 index 0000000000..f64952560d --- /dev/null +++ b/mayan/apps/file_caching/tests/test_events.py @@ -0,0 +1,42 @@ +from __future__ import unicode_literals + +from actstream.models import Action + +from mayan.apps.common.tests.base import BaseTestCase + +from ..events import event_cache_created, event_cache_purged +from ..models import Cache + +from .mixins import CacheTestMixin + + +class CacheEventsTestCase(CacheTestMixin, BaseTestCase): + def test_cache_create_event(self): + action_count = Action.objects.count() + + self._create_test_cache() + + self.assertEqual(Action.objects.count(), action_count + 1) + + event = Action.objects.first() + + cache = Cache.objects.last() + + self.assertEqual(event.verb, event_cache_created.id) + self.assertEqual(event.target, cache) + + def test_cache_purge_event(self): + self._create_test_cache() + + action_count = Action.objects.count() + + self.test_cache.purge() + + self.assertEqual(Action.objects.count(), action_count + 1) + + event = Action.objects.first() + + cache = Cache.objects.last() + + self.assertEqual(event.verb, event_cache_purged.id) + self.assertEqual(event.target, cache) diff --git a/mayan/apps/file_caching/tests/test_models.py b/mayan/apps/file_caching/tests/test_models.py new file mode 100644 index 0000000000..62ccab2189 --- /dev/null +++ b/mayan/apps/file_caching/tests/test_models.py @@ -0,0 +1,18 @@ +from __future__ import unicode_literals + +from mayan.apps.common.tests.base import BaseTestCase + +from .mixins import CacheTestMixin + + +class CacheModelTestCase(CacheTestMixin, BaseTestCase): + def test_cache_purge(self): + self._create_test_cache() + self._create_test_cache_partition() + self._create_test_cache_partition_file() + + cache_total_size = self.test_cache.get_total_size() + + self.test_cache.purge() + + self.assertNotEqual(cache_total_size, self.test_cache.get_total_size()) diff --git a/mayan/apps/file_caching/tests/test_views.py b/mayan/apps/file_caching/tests/test_views.py new file mode 100644 index 0000000000..e50249ede7 --- /dev/null +++ b/mayan/apps/file_caching/tests/test_views.py @@ -0,0 +1,89 @@ +from __future__ import unicode_literals + +from mayan.apps.common.tests.base import GenericViewTestCase + +from ..permissions import ( + permission_cache_purge, permission_cache_view +) + +from .mixins import CacheTestMixin, CacheViewTestMixin + + +class CacheViewTestCase( + CacheTestMixin, CacheViewTestMixin, GenericViewTestCase +): + def test_cache_list_view_with_no_permission(self): + self._create_test_cache() + + response = self._request_test_cache_list_view() + self.assertNotContains( + response=response, text=self.test_cache.label, status_code=200 + ) + + def test_cache_list_view_with_access(self): + self._create_test_cache() + + self.grant_access( + obj=self.test_cache, permission=permission_cache_view + ) + + response = self._request_test_cache_list_view() + self.assertContains( + response=response, text=self.test_cache.label, status_code=200 + ) + + def test_cache_purge_view_no_permissions(self): + self._create_test_cache() + self._create_test_cache_partition() + self._create_test_cache_partition_file() + + cache_total_size = self.test_cache.get_total_size() + + response = self._request_test_cache_purge_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual(cache_total_size, self.test_cache.get_total_size()) + + def test_cache_purge_view_with_access(self): + self._create_test_cache() + self._create_test_cache_partition() + self._create_test_cache_partition_file() + + self.grant_access( + obj=self.test_cache, permission=permission_cache_purge + ) + + cache_total_size = self.test_cache.get_total_size() + + response = self._request_test_cache_purge_view() + self.assertEqual(response.status_code, 302) + + self.assertNotEqual(cache_total_size, self.test_cache.get_total_size()) + + def test_cache_multiple_purge_view_no_permissions(self): + self._create_test_cache() + self._create_test_cache_partition() + self._create_test_cache_partition_file() + + cache_total_size = self.test_cache.get_total_size() + + response = self._request_test_cache_multiple_purge_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual(cache_total_size, self.test_cache.get_total_size()) + + def test_cache_multiple_purge_view_with_access(self): + self._create_test_cache() + self._create_test_cache_partition() + self._create_test_cache_partition_file() + + self.grant_access( + obj=self.test_cache, permission=permission_cache_purge + ) + + cache_total_size = self.test_cache.get_total_size() + + response = self._request_test_cache_multiple_purge_view() + self.assertEqual(response.status_code, 302) + + self.assertNotEqual(cache_total_size, self.test_cache.get_total_size()) diff --git a/mayan/apps/file_caching/urls.py b/mayan/apps/file_caching/urls.py new file mode 100644 index 0000000000..4ce37409f6 --- /dev/null +++ b/mayan/apps/file_caching/urls.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +from django.conf.urls import url + +from .views import CacheListView, CachePurgeView + +urlpatterns = [ + url( + regex=r'^caches/$', + name='cache_list', view=CacheListView.as_view() + ), + url( + regex=r'^caches/(?P\d+)/purge/$', + name='cache_purge', view=CachePurgeView.as_view() + ), + url( + regex=r'^caches/multiple/purge/$', name='cache_multiple_purge', + view=CachePurgeView.as_view() + ), +] diff --git a/mayan/apps/file_caching/views.py b/mayan/apps/file_caching/views.py new file mode 100644 index 0000000000..c747b14b78 --- /dev/null +++ b/mayan/apps/file_caching/views.py @@ -0,0 +1,53 @@ +from __future__ import absolute_import, unicode_literals + +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext + +from mayan.apps.common.generics import ( + MultipleObjectConfirmActionView, SingleObjectListView +) + +from .models import Cache +from .permissions import permission_cache_purge, permission_cache_view + +from .tasks import task_cache_purge + + +class CacheListView(SingleObjectListView): + model = Cache + object_permission = permission_cache_view + + def get_extra_context(self): + return { + 'hide_object': True, + 'title': _('File caches list') + } + + +class CachePurgeView(MultipleObjectConfirmActionView): + model = Cache + object_permission = permission_cache_purge + pk_url_kwarg = 'cache_id' + success_message_singular = '%(count)d cache submitted for purging.' + success_message_plural = '%(count)d caches submitted for purging.' + + def get_extra_context(self): + queryset = self.object_list + + result = { + 'title': ungettext( + singular='Submit the selected cache for purging?', + plural='Submit the selected caches for purging?', + number=queryset.count() + ) + } + + if queryset.count() == 1: + result['object'] = queryset.first() + + return result + + def object_action(self, form, instance): + task_cache_purge.apply_async( + kwargs={'cache_id': instance.pk, 'user_id': self.request.user.pk} + ) diff --git a/mayan/apps/file_metadata/classes.py b/mayan/apps/file_metadata/classes.py index 6af9b7a79d..60e9510b32 100644 --- a/mayan/apps/file_metadata/classes.py +++ b/mayan/apps/file_metadata/classes.py @@ -31,7 +31,12 @@ class FileMetadataDriver(object): @classmethod def process_document_version(cls, document_version): - for driver_class in cls._registry.get(document_version.mimetype, ()): + # Get list of drivers for the document's MIME type + driver_classes = cls._registry.get(document_version.mimetype, ()) + # Add wilcard drivers, drivers meant to be executed for all MIME types. + driver_classes = driver_classes + tuple(cls._registry.get('*', ())) + + for driver_class in driver_classes: try: driver = driver_class() diff --git a/mayan/apps/file_metadata/drivers/exiftool.py b/mayan/apps/file_metadata/drivers/exiftool.py index 79a0993916..28eb3f065c 100644 --- a/mayan/apps/file_metadata/drivers/exiftool.py +++ b/mayan/apps/file_metadata/drivers/exiftool.py @@ -4,12 +4,6 @@ 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 _ @@ -46,8 +40,15 @@ class EXIFToolDriver(FileMetadataDriver): try: document_version.save_to_file(file_object=temporary_fileobject) temporary_fileobject.seek(0) - result = self.command_exiftool(temporary_fileobject.name) - return json.loads(s=result.stdout)[0] + try: + result = self.command_exiftool(temporary_fileobject.name) + except sh.ErrorReturnCode_1 as exception: + result = json.loads(s=exception.stdout)[0] + if result.get('Error', '') == 'Unknown file type': + # Not a fatal error + return result + else: + return json.loads(s=result.stdout)[0] finally: temporary_fileobject.close() else: @@ -57,40 +58,9 @@ class EXIFToolDriver(FileMetadataDriver): ) def read_settings(self): - driver_arguments = yaml.load( - stream=setting_drivers_arguments.value, Loader=SafeLoader - ) - - self.exiftool_path = driver_arguments.get( + self.exiftool_path = setting_drivers_arguments.value.get( 'exif_driver', {} ).get('exiftool_path', DEFAULT_EXIF_PATH) -EXIFToolDriver.register( - mimetypes=( - 'application/msword', - 'application/pdf', - 'application/vnd.oasis.opendocument.text', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/x-bittorrent', - 'application/x-gzip', - 'application/x-rar-compressed', - 'application/x-shockwave-flash', - 'application/zip', - 'application/zip', - 'audio/x-pn-realaudio-plugin', - 'audio/x-wav', - 'image/jpeg', - 'image/png', - 'image/svg+xml', - 'image/tiff', - 'image/x-portable-pixmap', - 'text/html', - 'text/rtf', - 'text/x-sh', - 'video/mp4', - 'video/webm', - 'video/x-flv', - 'video/x-matroska' - ) -) +EXIFToolDriver.register(mimetypes=('*',)) diff --git a/mayan/apps/file_metadata/settings.py b/mayan/apps/file_metadata/settings.py index 4fa78da06f..b0f8c19064 100644 --- a/mayan/apps/file_metadata/settings.py +++ b/mayan/apps/file_metadata/settings.py @@ -16,12 +16,7 @@ setting_auto_process = namespace.add_setting( ) ) setting_drivers_arguments = namespace.add_setting( - default=''' - {{ - exif_driver: {{exiftool_path: {}}}, - - }} - '''.replace('\n', '').format(DEFAULT_EXIF_PATH), help_text=_( + default={'exif_driver': {'exiftool_path': DEFAULT_EXIF_PATH}}, help_text=_( 'Arguments to pass to the drivers.' - ), global_name='FILE_METADATA_DRIVERS_ARGUMENTS', quoted=True + ), global_name='FILE_METADATA_DRIVERS_ARGUMENTS' ) diff --git a/mayan/apps/file_metadata/tests/test_classes.py b/mayan/apps/file_metadata/tests/test_classes.py index 42c8c5a74f..7076b5ebea 100644 --- a/mayan/apps/file_metadata/tests/test_classes.py +++ b/mayan/apps/file_metadata/tests/test_classes.py @@ -1,9 +1,8 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import BaseTestCase -from mayan.apps.documents.tests import ( - TEST_PDF_DOCUMENT_FILENAME, DocumentTestMixin -) +from mayan.apps.common.tests.base import BaseTestCase +from mayan.apps.documents.tests.literals import TEST_PDF_DOCUMENT_FILENAME +from mayan.apps.documents.tests.mixins import DocumentTestMixin from .literals import ( TEST_PDF_FILE_METADATA_DOTTED_NAME, TEST_PDF_FILE_METADATA_VALUE diff --git a/mayan/apps/file_metadata/tests/test_indexing.py b/mayan/apps/file_metadata/tests/test_indexing.py index 441a4e39ca..f1aa644b5e 100644 --- a/mayan/apps/file_metadata/tests/test_indexing.py +++ b/mayan/apps/file_metadata/tests/test_indexing.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import BaseTestCase +from mayan.apps.common.tests.base import BaseTestCase from mayan.apps.document_indexing.models import Index, IndexInstanceNode from mayan.apps.document_indexing.tests.literals import TEST_INDEX_LABEL -from mayan.apps.documents.tests import DocumentTestMixin +from mayan.apps.documents.tests.base import DocumentTestMixin from .literals import ( TEST_FILE_METADATA_INDEX_NODE_TEMPLATE, TEST_FILE_METADATA_VALUE diff --git a/mayan/apps/file_metadata/tests/test_views.py b/mayan/apps/file_metadata/tests/test_views.py index 7ddfbbf8f0..16fa68be38 100644 --- a/mayan/apps/file_metadata/tests/test_views.py +++ b/mayan/apps/file_metadata/tests/test_views.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.test import override_settings -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from ..permissions import ( permission_document_type_file_metadata_setup, diff --git a/mayan/apps/linking/tests/test_api.py b/mayan/apps/linking/tests/test_api.py index b7c0139d0b..dccd40e84e 100644 --- a/mayan/apps/linking/tests/test_api.py +++ b/mayan/apps/linking/tests/test_api.py @@ -3,8 +3,8 @@ from __future__ import unicode_literals from rest_framework import status from mayan.apps.documents.permissions import permission_document_view -from mayan.apps.documents.tests import DocumentTestMixin -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.documents.tests.base import DocumentTestMixin +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..models import SmartLink, SmartLinkCondition from ..permissions import ( diff --git a/mayan/apps/linking/tests/test_events.py b/mayan/apps/linking/tests/test_events.py index c798109e50..a7745df769 100644 --- a/mayan/apps/linking/tests/test_events.py +++ b/mayan/apps/linking/tests/test_events.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals from actstream.models import Action -from mayan.apps.common.tests import GenericViewTestCase -from mayan.apps.documents.tests import DocumentTestMixin +from mayan.apps.common.tests.base import GenericViewTestCase +from mayan.apps.documents.tests.base import DocumentTestMixin from ..permissions import ( permission_smart_link_create, permission_smart_link_edit, diff --git a/mayan/apps/linking/tests/test_models.py b/mayan/apps/linking/tests/test_models.py index 3c726323b6..2dffc41a6d 100644 --- a/mayan/apps/linking/tests/test_models.py +++ b/mayan/apps/linking/tests/test_models.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.documents.tests import GenericDocumentTestCase +from mayan.apps.documents.tests.base import GenericDocumentTestCase from .mixins import SmartLinkTestMixin diff --git a/mayan/apps/linking/tests/test_views.py b/mayan/apps/linking/tests/test_views.py index 3b446b4ef4..dfa530a409 100644 --- a/mayan/apps/linking/tests/test_views.py +++ b/mayan/apps/linking/tests/test_views.py @@ -1,8 +1,8 @@ from __future__ import absolute_import, unicode_literals -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from mayan.apps.documents.permissions import permission_document_view -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from ..models import SmartLink from ..permissions import ( diff --git a/mayan/apps/linking/urls.py b/mayan/apps/linking/urls.py index 6f4a75ffb5..f8a271b8fb 100644 --- a/mayan/apps/linking/urls.py +++ b/mayan/apps/linking/urls.py @@ -18,12 +18,12 @@ from .views import ( urlpatterns = [ url( - regex=r'^document/(?P\d+)/list/$', + regex=r'^documents/(?P\d+)/smart_links/$', view=DocumentSmartLinkListView.as_view(), name='smart_link_instances_for_document' ), url( - regex=r'^document/(?P\d+)/(?P\d+)/$', + regex=r'^documents/(?P\d+)/smart_links/(?P\d+)/$', view=ResolvedSmartLinkView.as_view(), name='smart_link_instance_view' ), url( @@ -32,46 +32,46 @@ urlpatterns = [ name='document_type_smart_links' ), url( - regex=r'^setup/list/$', view=SmartLinkListView.as_view(), + regex=r'^smart_links/$', view=SmartLinkListView.as_view(), name='smart_link_list' ), url( - regex=r'^setup/create/$', view=SmartLinkCreateView.as_view(), + regex=r'^smart_links/create/$', view=SmartLinkCreateView.as_view(), name='smart_link_create' ), url( - regex=r'^setup/(?P\d+)/delete/$', + regex=r'^smart_links/(?P\d+)/delete/$', view=SmartLinkDeleteView.as_view(), name='smart_link_delete' ), url( - regex=r'^setup/(?P\d+)/edit/$', view=SmartLinkEditView.as_view(), + regex=r'^smart_links/(?P\d+)/edit/$', view=SmartLinkEditView.as_view(), name='smart_link_edit' ), url( - regex=r'^setup/(?P\d+)/document_types/$', + regex=r'^smart_links/(?P\d+)/document_types/$', view=SetupSmartLinkDocumentTypesView.as_view(), name='smart_link_document_types' ), url( - regex=r'^setup/(?P\d+)/condition/list/$', + regex=r'^smart_links/(?P\d+)/conditions/$', view=SmartLinkConditionListView.as_view(), name='smart_link_condition_list' ), url( - regex=r'^setup/(?P\d+)/condition/create/$', + regex=r'^smart_links/(?P\d+)/conditions/create/$', view=SmartLinkConditionCreateView.as_view(), name='smart_link_condition_create' ), url( - regex=r'^setup/smart_link/condition/(?P\d+)/edit/$', - view=SmartLinkConditionEditView.as_view(), - name='smart_link_condition_edit' - ), - url( - regex=r'^setup/smart_link/condition/(?P\d+)/delete/$', + regex=r'^smart_links/conditions/(?P\d+)/delete/$', view=SmartLinkConditionDeleteView.as_view(), name='smart_link_condition_delete' ), + url( + regex=r'^smart_links/conditions/(?P\d+)/edit/$', + view=SmartLinkConditionEditView.as_view(), + name='smart_link_condition_edit' + ), ] api_urls = [ diff --git a/mayan/apps/mailer/literals.py b/mayan/apps/mailer/literals.py index b22405cee3..c76b51f7d3 100644 --- a/mayan/apps/mailer/literals.py +++ b/mayan/apps/mailer/literals.py @@ -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 = (',', ';') diff --git a/mayan/apps/mailer/settings.py b/mayan/apps/mailer/settings.py index 6ad1772a95..7fbf7f6b8b 100644 --- a/mayan/apps/mailer/settings.py +++ b/mayan/apps/mailer/settings.py @@ -5,28 +5,29 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.smart_settings.classes import Namespace from .literals import ( - DEFAULT_DOCUMENT_BODY_TEMPLATE, DEFAULT_LINK_BODY_TEMPLATE + DEFAULT_DOCUMENT_BODY_TEMPLATE, DEFAULT_DOCUMENT_SUBJECT_TEMPLATE, + DEFAULT_LINK_BODY_TEMPLATE, DEFAULT_LINK_SUBJECT_TEMPLATE ) namespace = Namespace(label=_('Mailing'), name='mailer') -setting_link_subject_template = namespace.add_setting( - default=_('Link for document: {{ document }}'), - help_text=_('Template for the document link email form subject line.'), - 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', quoted=True -) setting_document_subject_template = namespace.add_setting( - default=_('Document: {{ document }}'), + default=DEFAULT_DOCUMENT_SUBJECT_TEMPLATE, help_text=_('Template for the document email form subject line.'), - global_name='MAILER_DOCUMENT_SUBJECT_TEMPLATE', quoted=True + 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', quoted=True + global_name='MAILER_DOCUMENT_BODY_TEMPLATE' +) +setting_link_subject_template = namespace.add_setting( + default=DEFAULT_LINK_SUBJECT_TEMPLATE, + help_text=_('Template for the document link email form subject line.'), + global_name='MAILER_LINK_SUBJECT_TEMPLATE' +) +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' ) diff --git a/mayan/apps/mailer/tests/test_events.py b/mayan/apps/mailer/tests/test_events.py index 95ebe32b57..124ff8e97f 100644 --- a/mayan/apps/mailer/tests/test_events.py +++ b/mayan/apps/mailer/tests/test_events.py @@ -4,8 +4,8 @@ from django.core import mail from actstream.models import Action -from mayan.apps.common.tests import GenericViewTestCase -from mayan.apps.documents.tests import DocumentTestMixin +from mayan.apps.common.tests.base import GenericViewTestCase +from mayan.apps.documents.tests.base import DocumentTestMixin from ..permissions import ( permission_mailing_send_document, permission_user_mailer_use diff --git a/mayan/apps/mailer/tests/test_models.py b/mayan/apps/mailer/tests/test_models.py index 02f3cab3b1..a161889e31 100644 --- a/mayan/apps/mailer/tests/test_models.py +++ b/mayan/apps/mailer/tests/test_models.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals from django.core import mail -from mayan.apps.documents.tests.test_models import GenericDocumentTestCase +from mayan.apps.documents.tests.base import GenericDocumentTestCase from .literals import ( TEST_EMAIL_BODY_HTML, TEST_EMAIL_ADDRESS, TEST_EMAIL_FROM_ADDRESS, diff --git a/mayan/apps/mailer/tests/test_views.py b/mayan/apps/mailer/tests/test_views.py index 5b86cb6888..4cc58f4deb 100644 --- a/mayan/apps/mailer/tests/test_views.py +++ b/mayan/apps/mailer/tests/test_views.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals from django.core import mail -from mayan.apps.common.tests import GenericViewTestCase -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from ..models import UserMailer from ..permissions import ( diff --git a/mayan/apps/mailer/tests/test_workflow_actions.py b/mayan/apps/mailer/tests/test_workflow_actions.py new file mode 100644 index 0000000000..ce5e4408f2 --- /dev/null +++ b/mayan/apps/mailer/tests/test_workflow_actions.py @@ -0,0 +1,180 @@ +from __future__ import unicode_literals + +import json + +from django.core import mail + +from mayan.apps.common.tests.base import GenericViewTestCase +from mayan.apps.documents.tests.mixins import DocumentTestMixin +from mayan.apps.document_states.literals import WORKFLOW_ACTION_ON_ENTRY +from mayan.apps.document_states.tests.mixins import WorkflowTestMixin +from mayan.apps.document_states.tests.test_workflow_actions import ActionTestCase +from mayan.apps.metadata.tests.mixins import MetadataTypeTestMixin + +from ..permissions import permission_user_mailer_use +from ..workflow_actions import EmailAction + +from .literals import ( + TEST_EMAIL_ADDRESS, TEST_EMAIL_BODY, TEST_EMAIL_FROM_ADDRESS, + TEST_EMAIL_SUBJECT +) +from .mixins import MailerTestMixin + + +class EmailActionTestCase(MailerTestMixin, WorkflowTestMixin, ActionTestCase): + def test_email_action_literal_text(self): + self._create_test_user_mailer() + + action = EmailAction( + form_data={ + 'mailing_profile': self.test_user_mailer.pk, + 'recipient': TEST_EMAIL_ADDRESS, + 'subject': TEST_EMAIL_SUBJECT, + 'body': TEST_EMAIL_BODY, + } + ) + action.execute(context={'document': self.test_document}) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) + self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS]) + + def test_email_action_workflow_execute(self): + self._create_test_workflow() + self._create_test_workflow_state() + self._create_test_user_mailer() + + self.test_workflow_state.actions.create( + action_data=json.dumps( + { + 'mailing_profile': self.test_user_mailer.pk, + 'recipient': TEST_EMAIL_ADDRESS, + 'subject': TEST_EMAIL_SUBJECT, + 'body': TEST_EMAIL_BODY, + } + ), + action_path='mayan.apps.mailer.workflow_actions.EmailAction', + label='test email action', when=WORKFLOW_ACTION_ON_ENTRY, + ) + + self.test_workflow_state.initial = True + self.test_workflow_state.save() + self.test_workflow.document_types.add(self.test_document_type) + + self.upload_document() + + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) + self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS]) + + +class EmailActionTemplateTestCase(MetadataTypeTestMixin, MailerTestMixin, WorkflowTestMixin, ActionTestCase): + def test_email_action_recipient_template(self): + self._create_test_metadata_type() + self.test_document_type.metadata.create(metadata_type=self.test_metadata_type) + self.test_document.metadata.create(metadata_type=self.test_metadata_type, value=TEST_EMAIL_ADDRESS) + + self._create_test_user_mailer() + + action = EmailAction( + form_data={ + 'mailing_profile': self.test_user_mailer.pk, + 'recipient': '{{{{ document.metadata_value_of.{} }}}}'.format(self.test_metadata_type.name), + 'subject': TEST_EMAIL_SUBJECT, + 'body': '', + } + ) + action.execute(context={'document': self.test_document}) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) + self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS]) + + def test_email_action_subject_template(self): + self._create_test_metadata_type() + self.test_document_type.metadata.create(metadata_type=self.test_metadata_type) + self.test_document.metadata.create(metadata_type=self.test_metadata_type, value=TEST_EMAIL_SUBJECT) + + self._create_test_user_mailer() + + action = EmailAction( + form_data={ + 'mailing_profile': self.test_user_mailer.pk, + 'recipient': TEST_EMAIL_ADDRESS, + 'subject': '{{{{ document.metadata_value_of.{} }}}}'.format(self.test_metadata_type.name), + 'body': '', + } + ) + action.execute(context={'document': self.test_document}) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) + self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS]) + + def test_email_action_body_template(self): + self._create_test_metadata_type() + self.test_document_type.metadata.create(metadata_type=self.test_metadata_type) + self.test_document.metadata.create(metadata_type=self.test_metadata_type, value=TEST_EMAIL_BODY) + + self._create_test_user_mailer() + + action = EmailAction( + form_data={ + 'mailing_profile': self.test_user_mailer.pk, + 'recipient': TEST_EMAIL_ADDRESS, + 'subject': TEST_EMAIL_SUBJECT, + 'body': '{{{{ document.metadata_value_of.{} }}}}'.format(self.test_metadata_type.name), + } + ) + action.execute(context={'document': self.test_document}) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) + self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS]) + self.assertEqual(mail.outbox[0].body, TEST_EMAIL_BODY) + + +class EmailActionViewTestCase(DocumentTestMixin, MailerTestMixin, WorkflowTestMixin, GenericViewTestCase): + auto_upload_document = False + + def test_email_action_create_get_view(self): + self._create_test_workflow() + self._create_test_workflow_state() + self._create_test_user_mailer() + + response = self.get( + viewname='document_states:workflow_template_state_action_create', + kwargs={ + 'pk': self.test_workflow_state.pk, + 'class_path': 'mayan.apps.mailer.workflow_actions.EmailAction', + } + ) + self.assertEqual(response.status_code, 200) + + self.assertEqual(self.test_workflow_state.actions.count(), 0) + + def _request_email_action_create_post_view(self): + return self.post( + viewname='document_states:workflow_template_state_action_create', + kwargs={ + 'pk': self.test_workflow_state.pk, + 'class_path': 'mayan.apps.mailer.workflow_actions.EmailAction', + }, data={ + 'when': WORKFLOW_ACTION_ON_ENTRY, + 'label': 'test email action', + 'mailing_profile': self.test_user_mailer.pk, + 'recipient': TEST_EMAIL_ADDRESS, + 'subject': TEST_EMAIL_SUBJECT, + 'body': TEST_EMAIL_BODY, + } + ) + + def test_email_action_create_post_view(self): + self._create_test_workflow() + self._create_test_workflow_state() + self._create_test_user_mailer() + + self.grant_access( + obj=self.test_user_mailer, permission=permission_user_mailer_use + ) + + response = self._request_email_action_create_post_view() + self.assertEqual(response.status_code, 302) + + self.assertEqual(self.test_workflow_state.actions.count(), 1) diff --git a/mayan/apps/mailer/urls.py b/mayan/apps/mailer/urls.py index 5f6ab328e5..08b9effb02 100644 --- a/mayan/apps/mailer/urls.py +++ b/mayan/apps/mailer/urls.py @@ -11,19 +11,19 @@ from .views import ( urlpatterns = [ url( - regex=r'^(?P\d+)/send/link/$', view=MailDocumentLinkView.as_view(), + regex=r'^documents/(?P\d+)/send/link/$', view=MailDocumentLinkView.as_view(), name='send_document_link' ), url( - regex=r'^multiple/send/link/$', view=MailDocumentLinkView.as_view(), + regex=r'^documents/multiple/send/link/$', view=MailDocumentLinkView.as_view(), name='send_multiple_document_link' ), url( - regex=r'^(?P\d+)/send/document/$', view=MailDocumentView.as_view(), + regex=r'^documents/(?P\d+)/send/document/$', view=MailDocumentView.as_view(), name='send_document' ), url( - regex=r'^multiple/send/document/$', view=MailDocumentView.as_view(), + regex=r'^documents/multiple/send/document/$', view=MailDocumentView.as_view(), name='send_multiple_document' ), url( diff --git a/mayan/apps/mayan_statistics/classes.py b/mayan/apps/mayan_statistics/classes.py index e4296d095f..671dab799e 100644 --- a/mayan/apps/mayan_statistics/classes.py +++ b/mayan/apps/mayan_statistics/classes.py @@ -61,7 +61,7 @@ class Statistic(object): @staticmethod def purge_schedules(): PeriodicTask = apps.get_model( - app_label='djcelery', model_name='PeriodicTask' + app_label='django_celery_beat', model_name='PeriodicTask' ) StatisticResult = apps.get_model( app_label='mayan_statistics', model_name='StatisticResult' @@ -107,7 +107,7 @@ class Statistic(object): day_of_month=day_of_month, month_of_year=month_of_year, ) - app.conf.CELERYBEAT_SCHEDULE.update( + app.conf.beat_schedule.update( { self.get_task_name(): { 'task': task_execute_statistic.dotted_path, @@ -117,7 +117,7 @@ class Statistic(object): } ) - app.conf.CELERY_ROUTES.update( + app.conf.task_routes.update( { self.get_task_name(): { 'queue': queue_statistics.name diff --git a/mayan/apps/mayan_statistics/tests/test_views.py b/mayan/apps/mayan_statistics/tests/test_views.py index 4eeab8a6fb..6508abbe33 100644 --- a/mayan/apps/mayan_statistics/tests/test_views.py +++ b/mayan/apps/mayan_statistics/tests/test_views.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from ..classes import Statistic from ..permissions import permission_statistics_view diff --git a/mayan/apps/mayan_statistics/urls.py b/mayan/apps/mayan_statistics/urls.py index 424991df20..369077df81 100644 --- a/mayan/apps/mayan_statistics/urls.py +++ b/mayan/apps/mayan_statistics/urls.py @@ -8,17 +8,17 @@ from .views import ( ) urlpatterns = [ - url(regex=r'^$', view=NamespaceListView.as_view(), name='namespace_list'), + url(regex=r'^namespace/$', view=NamespaceListView.as_view(), name='namespace_list'), url( - regex=r'^namespace/(?P[\w-]+)/details/$', + regex=r'^namespaces/(?P[\w-]+)/$', view=NamespaceDetailView.as_view(), name='namespace_details' ), url( - regex=r'^(?P[\w-]+)/view/$', view=StatisticDetailView.as_view(), + regex=r'^statistics/(?P[\w-]+)/view/$', view=StatisticDetailView.as_view(), name='statistic_detail' ), url( - regex=r'^(?P[\w-]+)/queue/$', view=StatisticQueueView.as_view(), + regex=r'^statistics/(?P[\w-]+)/queue/$', view=StatisticQueueView.as_view(), name='statistic_queue' ), ] diff --git a/mayan/apps/metadata/migrations/0013_auto_20191005_0646.py b/mayan/apps/metadata/migrations/0013_auto_20191005_0646.py new file mode 100644 index 0000000000..ac98df1b3c --- /dev/null +++ b/mayan/apps/metadata/migrations/0013_auto_20191005_0646.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-10-05 06:46 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('metadata', '0012_auto_20190612_0526'), + ] + + operations = [ + migrations.AlterField( + model_name='metadatatype', + name='label', + field=models.CharField(help_text='Short description of this metadata type.', max_length=48, verbose_name='Label'), + ), + ] diff --git a/mayan/apps/metadata/models.py b/mayan/apps/metadata/models.py index c160a98e4f..e9b36af2b7 100644 --- a/mayan/apps/metadata/models.py +++ b/mayan/apps/metadata/models.py @@ -52,7 +52,10 @@ class MetadataType(models.Model): ), unique=True, verbose_name=_('Name') ) - label = models.CharField(max_length=48, verbose_name=_('Label')) + label = models.CharField( + help_text=_('Short description of this metadata type.'), + max_length=48, verbose_name=_('Label') + ) default = models.CharField( blank=True, max_length=128, null=True, help_text=_( diff --git a/mayan/apps/metadata/tests/test_api.py b/mayan/apps/metadata/tests/test_api.py index ab06e11725..f74f7cdfce 100644 --- a/mayan/apps/metadata/tests/test_api.py +++ b/mayan/apps/metadata/tests/test_api.py @@ -5,8 +5,8 @@ from rest_framework import status from mayan.apps.documents.permissions import ( permission_document_type_edit, permission_document_type_view ) -from mayan.apps.documents.tests import DocumentTestMixin -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.documents.tests.mixins import DocumentTestMixin +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..models import DocumentTypeMetadataType, MetadataType from ..permissions import ( diff --git a/mayan/apps/metadata/tests/test_events.py b/mayan/apps/metadata/tests/test_events.py index cdd8e1b45c..60c06e76a3 100644 --- a/mayan/apps/metadata/tests/test_events.py +++ b/mayan/apps/metadata/tests/test_events.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from actstream.models import Action -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from ..events import ( event_metadata_type_created, event_metadata_type_edited diff --git a/mayan/apps/metadata/tests/test_models.py b/mayan/apps/metadata/tests/test_models.py index 8cc9bbb08d..46f134c5bb 100644 --- a/mayan/apps/metadata/tests/test_models.py +++ b/mayan/apps/metadata/tests/test_models.py @@ -3,11 +3,10 @@ from __future__ import unicode_literals from django.core.exceptions import ValidationError -from mayan.apps.common.tests import BaseTestCase +from mayan.apps.common.tests.base import BaseTestCase from mayan.apps.documents.models import DocumentType -from mayan.apps.documents.tests import ( - DocumentTestMixin, TEST_DOCUMENT_TYPE_2_LABEL -) +from mayan.apps.documents.tests.literals import TEST_DOCUMENT_TYPE_2_LABEL +from mayan.apps.documents.tests.mixins import DocumentTestMixin from ..models import DocumentMetadata diff --git a/mayan/apps/metadata/tests/test_views.py b/mayan/apps/metadata/tests/test_views.py index 124b7cd34d..071a955ec5 100644 --- a/mayan/apps/metadata/tests/test_views.py +++ b/mayan/apps/metadata/tests/test_views.py @@ -1,14 +1,13 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from mayan.apps.documents.models import DocumentType from mayan.apps.documents.permissions import ( permission_document_properties_edit, permission_document_type_edit, permission_document_view ) -from mayan.apps.documents.tests import ( - GenericDocumentViewTestCase, TEST_DOCUMENT_TYPE_2_LABEL -) +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase +from mayan.apps.documents.tests.literals import TEST_DOCUMENT_TYPE_2_LABEL from ..models import MetadataType from ..permissions import ( diff --git a/mayan/apps/metadata/tests/test_wizard_steps.py b/mayan/apps/metadata/tests/test_wizard_steps.py index e6b7644c3e..a2384fdd83 100644 --- a/mayan/apps/metadata/tests/test_wizard_steps.py +++ b/mayan/apps/metadata/tests/test_wizard_steps.py @@ -5,9 +5,8 @@ from django.urls import reverse from mayan.apps.common.http import URL from mayan.apps.documents.models import Document from mayan.apps.documents.permissions import permission_document_create -from mayan.apps.documents.tests import ( - GenericDocumentViewTestCase, TEST_SMALL_DOCUMENT_PATH, -) +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase +from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH from mayan.apps.sources.models import WebFormSource from mayan.apps.sources.tests.literals import ( TEST_SOURCE_LABEL, TEST_SOURCE_UNCOMPRESS_N, @@ -45,10 +44,10 @@ class DocumentUploadMetadataTestCase(MetadataTypeTestMixin, GenericDocumentViewT ) # Upload the test document - with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_descriptor: + with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: response = self.post( path=url.to_string(), data={ - 'document-language': 'eng', 'source-file': file_descriptor, + 'document-language': 'eng', 'source-file': file_object, 'document_type_id': self.test_document_type.pk, } ) @@ -70,14 +69,16 @@ class DocumentUploadMetadataTestCase(MetadataTypeTestMixin, GenericDocumentViewT self.grant_access( permission=permission_document_create, obj=self.test_document_type ) + # Upload the test document - with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_descriptor: + with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: response = self.post( path=url.to_string(), data={ - 'document-language': 'eng', 'source-file': file_descriptor, + 'document-language': 'eng', 'source-file': file_object, 'document_type_id': self.test_document_type.pk, } ) + self.assertEqual(response.status_code, 302) self.assertEqual(Document.objects.count(), 1) diff --git a/mayan/apps/metadata/urls.py b/mayan/apps/metadata/urls.py index 05bcbfd638..5b7879c0ad 100644 --- a/mayan/apps/metadata/urls.py +++ b/mayan/apps/metadata/urls.py @@ -17,56 +17,56 @@ from .views import ( urlpatterns = [ url( - regex=r'^(?P\d+)/edit/$', view=DocumentMetadataEditView.as_view(), + regex=r'^documents/metadata/(?P\d+)/edit/$', view=DocumentMetadataEditView.as_view(), name='metadata_edit' ), url( - regex=r'^multiple/edit/$', view=DocumentMetadataEditView.as_view(), + regex=r'^documents/multiple/metadata/edit/$', view=DocumentMetadataEditView.as_view(), name='metadata_multiple_edit' ), url( - regex=r'^(?P\d+)/view/$', view=DocumentMetadataListView.as_view(), + regex=r'^documents/(?P\d+)/view/$', view=DocumentMetadataListView.as_view(), name='metadata_view' ), url( - regex=r'^(?P\d+)/add/$', view=DocumentMetadataAddView.as_view(), + regex=r'^documents/(?P\d+)/add/$', view=DocumentMetadataAddView.as_view(), name='metadata_add' ), url( - regex=r'^multiple/add/$', view=DocumentMetadataAddView.as_view(), + regex=r'^documents/multiple/add/$', view=DocumentMetadataAddView.as_view(), name='metadata_multiple_add' ), url( - regex=r'^(?P\d+)/remove/$', + regex=r'^documents/(?P\d+)/remove/$', view=DocumentMetadataRemoveView.as_view(), name='metadata_remove' ), url( - regex=r'^multiple/remove/$', view=DocumentMetadataRemoveView.as_view(), + regex=r'^documents/multiple/remove/$', view=DocumentMetadataRemoveView.as_view(), name='metadata_multiple_remove' ), url( - regex=r'^setup/type/list/$', view=MetadataTypeListView.as_view(), + regex=r'^metadata_types/$', view=MetadataTypeListView.as_view(), name='setup_metadata_type_list' ), url( - regex=r'^setup/type/create/$', view=MetadataTypeCreateView.as_view(), + regex=r'^metadata_types/create/$', view=MetadataTypeCreateView.as_view(), name='setup_metadata_type_create' ), url( - regex=r'^setup/type/(?P\d+)/edit/$', + regex=r'^metadata_types/(?P\d+)/edit/$', view=MetadataTypeEditView.as_view(), name='setup_metadata_type_edit' ), url( - regex=r'^setup/type/(?P\d+)/delete/$', + regex=r'^metadata_types/(?P\d+)/delete/$', view=MetadataTypeDeleteView.as_view(), name='setup_metadata_type_delete' ), url( - regex=r'^setup/document_types/(?P\d+)/metadata_types/$', + regex=r'^document_types/(?P\d+)/metadata_types/$', view=SetupDocumentTypeMetadataTypes.as_view(), name='setup_document_type_metadata_types' ), url( - regex=r'^setup/metadata_types/(?P\d+)/document_types/$', + regex=r'^metadata_types/(?P\d+)/document_types/$', view=SetupMetadataTypesDocumentTypes.as_view(), name='setup_metadata_type_document_types' ), diff --git a/mayan/apps/metadata/widgets.py b/mayan/apps/metadata/widgets.py index 8e4a428069..166a2ec413 100644 --- a/mayan/apps/metadata/widgets.py +++ b/mayan/apps/metadata/widgets.py @@ -8,7 +8,7 @@ def get_metadata_string(document): Return a formated representation of a document's metadata values """ return format_html_join( - '\n', '', + '\n', '', ( ( document_metadata.metadata_type, document_metadata.metadata_type_id, document_metadata.id, document_metadata.value diff --git a/mayan/apps/mimetype/tests/test_functions.py b/mayan/apps/mimetype/tests/test_functions.py index 68dca2ade4..2123d3e771 100644 --- a/mayan/apps/mimetype/tests/test_functions.py +++ b/mayan/apps/mimetype/tests/test_functions.py @@ -4,10 +4,11 @@ import resource from django.test import override_settings, tag -from mayan.apps.common.tests import BaseTestCase +from mayan.apps.common.tests.base import BaseTestCase from mayan.apps.common.tests.literals import EXCLUDE_TEST_TAG from mayan.apps.documents.models import Document -from mayan.apps.documents.tests import DocumentTestMixin, TEST_PDF_DOCUMENT_FILENAME +from mayan.apps.documents.tests.base import DocumentTestMixin +from mayan.apps.documents.tests.literals import TEST_PDF_DOCUMENT_FILENAME # This constant may need tweaking as document upload code path changes. # The value is targeted at making the document upload process fail exactly diff --git a/mayan/apps/mirroring/tests/test_caches.py b/mayan/apps/mirroring/tests/test_caches.py index 42249ce375..91bb7d8a98 100644 --- a/mayan/apps/mirroring/tests/test_caches.py +++ b/mayan/apps/mirroring/tests/test_caches.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals import warnings -from mayan.apps.common.tests import BaseTestCase +from mayan.apps.common.tests.base import BaseTestCase from ..caches import IndexFilesystemCache diff --git a/mayan/apps/mirroring/tests/test_filesystems.py b/mayan/apps/mirroring/tests/test_filesystems.py index 3036f929f6..7d41900383 100644 --- a/mayan/apps/mirroring/tests/test_filesystems.py +++ b/mayan/apps/mirroring/tests/test_filesystems.py @@ -4,10 +4,10 @@ import hashlib from fuse import FuseOSError -from mayan.apps.common.tests import BaseTestCase +from mayan.apps.common.tests.base import BaseTestCase from mayan.apps.documents.models import Document -from mayan.apps.documents.tests import DocumentTestMixin -from mayan.apps.document_indexing.tests import IndexTestMixin +from mayan.apps.documents.tests.mixins import DocumentTestMixin +from mayan.apps.document_indexing.tests.mixins import IndexTestMixin from ..filesystems import IndexFilesystem diff --git a/mayan/apps/motd/tests/test_api.py b/mayan/apps/motd/tests/test_api.py index d9619ae416..9e83ff4c6c 100644 --- a/mayan/apps/motd/tests/test_api.py +++ b/mayan/apps/motd/tests/test_api.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from rest_framework import status -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..models import Message from ..permissions import ( diff --git a/mayan/apps/motd/tests/test_models.py b/mayan/apps/motd/tests/test_models.py index 574e1e7a08..0fe60f2bd6 100644 --- a/mayan/apps/motd/tests/test_models.py +++ b/mayan/apps/motd/tests/test_models.py @@ -2,15 +2,16 @@ from __future__ import unicode_literals from datetime import timedelta -from django.test import TestCase from django.utils import timezone +from mayan.apps.common.tests.base import BaseTestCase + from ..models import Message from .mixins import MOTDTestMixin -class MOTDTestCase(MOTDTestMixin, TestCase): +class MOTDTestCase(MOTDTestMixin, BaseTestCase): def setUp(self): super(MOTDTestCase, self).setUp() self._create_test_message() diff --git a/mayan/apps/motd/urls.py b/mayan/apps/motd/urls.py index f1d4dae73e..06e463cb63 100644 --- a/mayan/apps/motd/urls.py +++ b/mayan/apps/motd/urls.py @@ -8,18 +8,18 @@ from .views import ( ) urlpatterns = [ - url(regex=r'^list/$', view=MessageListView.as_view(), name='message_list'), + url(regex=r'^messages/$', view=MessageListView.as_view(), name='message_list'), url( - regex=r'^create/$', view=MessageCreateView.as_view(), + regex=r'^messages/create/$', view=MessageCreateView.as_view(), name='message_create' ), url( - regex=r'^(?P\d+)/edit/$', view=MessageEditView.as_view(), - name='message_edit' + regex=r'^messages/(?P\d+)/delete/$', view=MessageDeleteView.as_view(), + name='message_delete' ), url( - regex=r'^(?P\d+)/delete/$', view=MessageDeleteView.as_view(), - name='message_delete' + regex=r'^messages/(?P\d+)/edit/$', view=MessageEditView.as_view(), + name='message_edit' ), ] diff --git a/mayan/apps/navigation/classes.py b/mayan/apps/navigation/classes.py index f7dd20d5ca..14923557ba 100644 --- a/mayan/apps/navigation/classes.py +++ b/mayan/apps/navigation/classes.py @@ -6,7 +6,9 @@ import logging from furl import furl from django.apps import apps -from django.contrib.admin.utils import label_for_field +from django.contrib.admin.utils import ( + help_text_for_field, label_for_field +) from django.core.exceptions import ( FieldDoesNotExist, ImproperlyConfigured, PermissionDenied ) @@ -27,6 +29,7 @@ from mayan.apps.common.settings import setting_home_view from mayan.apps.common.utils import resolve_attribute from mayan.apps.permissions import Permission +from .html_widgets import SourceColumnLinkWidget from .utils import get_current_view_name logger = logging.getLogger(__name__) @@ -45,19 +48,21 @@ class Link(object): def __init__( self, text=None, view=None, args=None, badge_text=None, condition=None, - conditional_disable=None, description=None, html_data=None, - html_extra_classes=None, icon_class=None, icon_class_path=None, - keep_query=False, kwargs=None, name=None, permissions=None, - remove_from_query=None, tags=None, url=None + conditional_active=None, conditional_disable=None, description=None, + html_data=None, html_extra_classes=None, icon_class=None, + icon_class_path=None, keep_query=False, kwargs=None, name=None, + permissions=None, remove_from_query=None, tags=None, url=None ): self.args = args or [] self.badge_text = badge_text self.condition = condition + self.conditional_active = conditional_active self.conditional_disable = conditional_disable self.description = description self.html_data = html_data self.html_extra_classes = html_extra_classes self.icon_class = icon_class + self.icon_class_path = icon_class_path self.keep_query = keep_query self.kwargs = kwargs or {} self.name = name @@ -68,7 +73,13 @@ class Link(object): self.view = view self.url = url - if icon_class_path: + self.process_icon() + + if name: + self.__class__._registry[name] = self + + def process_icon(self): + if self.icon_class_path: if self.icon_class: raise ImproperlyConfigured( 'Specify the icon_class or the icon_class_path but not ' @@ -76,17 +87,14 @@ class Link(object): ) else: try: - self.icon_class = import_string(dotted_path=icon_class_path) + self.icon_class = import_string(dotted_path=self.icon_class_path) except ImportError as exception: logger.error( - 'Exception importing icon: %s; %s', icon_class_path, + 'Exception importing icon: %s; %s', self.icon_class_path, exception ) raise - if name: - self.__class__._registry[name] = self - def resolve(self, context=None, request=None, resolved_object=None): if not context and not request: raise ImproperlyConfigured( @@ -373,6 +381,18 @@ class Menu(object): try: if inspect.isclass(bound_source): if type(resolved_navigation_object) == bound_source: + # Check to see if object is a proxy model. If it is, add its parent model + # menu links too. + if hasattr(resolved_navigation_object, '_meta'): + parent_model = resolved_navigation_object._meta.proxy_for_model + if parent_model: + parent_instance = parent_model.objects.filter(pk=resolved_navigation_object.pk) + if parent_instance: + for link_set in self.resolve(context=context, source=parent_instance.first()): + for link in link_set['links']: + if link.link not in self.unbound_links.get(bound_source, ()): + resolved_links.append(link) + for link in links: resolved_link = link.resolve( context=context, @@ -395,10 +415,22 @@ class Menu(object): resolved_links.append(resolved_link) # No need for further content object match testing break + except TypeError: # When source is a dictionary pass + # Remove duplicated resolved link by using their source link + # instance as reference. The actual resolved link can't be used + # since a single source link can produce multiple resolved links. + # Since dictionaries keys can't have duplicates, we use that as a + # native deduplicator. + resolved_links_dict = {} + for resolved_link in resolved_links: + resolved_links_dict[resolved_link.link] = resolved_link + + resolved_links = resolved_links_dict.values() + if resolved_links: result.append( { @@ -498,7 +530,13 @@ class ResolvedLink(object): @property def active(self): - return self.link.view == self.current_view_name + conditional_active = self.link.conditional_active + if conditional_active: + return conditional_active( + context=self.context, resolved_link=self + ) + else: + return self.link.view == self.current_view_name @property def badge_text(self): @@ -611,8 +649,6 @@ class SourceColumn(object): # Try it as a queryset columns.extend(cls._registry[source.model]) except AttributeError: - pass - try: # Special case for queryset items produced from # .defer() or .only() optimizations @@ -666,17 +702,18 @@ class SourceColumn(object): return final_result def __init__( - self, source, attribute=None, empty_value=None, exclude=None, func=None, - include_label=False, is_attribute_absolute_url=False, + self, source, attribute=None, empty_value=None, func=None, + help_text=None, include_label=False, is_attribute_absolute_url=False, is_object_absolute_url=False, is_identifier=False, is_sortable=False, kwargs=None, label=None, order=None, sort_field=None, views=None, - widget=None + widget=None, widget_condition=None ): - self.source = source self._label = label + self._help_text = help_text + self.source = source self.attribute = attribute self.empty_value = empty_value - self.exclude = exclude or () + self.exclude = () self.func = func self.is_attribute_absolute_url = is_attribute_absolute_url self.is_object_absolute_url = is_object_absolute_url @@ -688,11 +725,38 @@ class SourceColumn(object): self.sort_field = sort_field self.views = views or [] self.widget = widget + self.widget_condition = widget_condition + + if self.is_attribute_absolute_url or self.is_object_absolute_url: + if not self.widget: + self.widget = SourceColumnLinkWidget self.__class__._registry.setdefault(source, []) self.__class__._registry[source].append(self) self._calculate_label() + self._calculate_help_text() + + def _calculate_help_text(self): + if not self._help_text: + if self.attribute: + try: + attribute = resolve_attribute( + obj=self.source, attribute=self.attribute + ) + self._help_text = getattr(attribute, 'help_text') + except AttributeError: + try: + name, model = SourceColumn.get_attribute_recursive( + attribute=self.attribute, model=self.source._meta.model + ) + self._help_text = help_text_for_field( + name=name, model=model + ) + except AttributeError: + self._help_text = self.attribute + + self.help_text = self._help_text def _calculate_label(self): if not self._label: @@ -719,6 +783,15 @@ class SourceColumn(object): self.label = self._label + def add_exclude(self, source): + self.exclude = self.exclude + (source,) + + def check_widget_condition(self, context): + if self.widget_condition: + return self.widget_condition(context=context) + else: + return True + def get_absolute_url(self, obj): if self.is_object_absolute_url: return obj.get_absolute_url() @@ -772,9 +845,12 @@ class SourceColumn(object): else: result = context['object'] + self.absolute_url = self.get_absolute_url(obj=context['object']) if self.widget: - widget_instance = self.widget() - return widget_instance.render(name=self.attribute, value=result) + if self.check_widget_condition(context=context): + widget_instance = self.widget() + widget_instance.column = self + return widget_instance.render(name=self.attribute, value=result) if not result: if self.empty_value: diff --git a/mayan/apps/navigation/html_widgets.py b/mayan/apps/navigation/html_widgets.py new file mode 100644 index 0000000000..2f37294140 --- /dev/null +++ b/mayan/apps/navigation/html_widgets.py @@ -0,0 +1,14 @@ +from __future__ import unicode_literals + +from django.template.loader import render_to_string + + +class SourceColumnLinkWidget(object): + template_name = 'navigation/source_column_link_widget.html' + + def render(self, name=None, value=None): + return render_to_string( + template_name=self.template_name, context={ + 'column': self.column, 'value': value + } + ) diff --git a/mayan/apps/navigation/icons.py b/mayan/apps/navigation/icons.py new file mode 100644 index 0000000000..d5d2bcdd7a --- /dev/null +++ b/mayan/apps/navigation/icons.py @@ -0,0 +1,10 @@ +from __future__ import absolute_import, unicode_literals + +from mayan.apps.appearance.classes import Icon + +icon_source_column_help_text = Icon( + driver_name='fontawesome', symbol='question' +) +icon_source_column_help_text = Icon( + driver_name='fontawesomecss', css_classes='far fa-question-circle' +) diff --git a/mayan/apps/navigation/templates/navigation/large_button_link.html b/mayan/apps/navigation/templates/navigation/large_button_link.html index dacbe785d9..e9c2d84f97 100644 --- a/mayan/apps/navigation/templates/navigation/large_button_link.html +++ b/mayan/apps/navigation/templates/navigation/large_button_link.html @@ -1,6 +1,8 @@ +{% load appearance_tags %} +
    - - {% if link.icon_class %}{{ link.icon_class.render }}{% endif %} + + {% if link.icon_class %}{% appearance_icon_render link.icon_class enable_shadow=True %}{% endif %}
    {{ link.text }}
    diff --git a/mayan/apps/navigation/templates/navigation/source_column_link_widget.html b/mayan/apps/navigation/templates/navigation/source_column_link_widget.html new file mode 100644 index 0000000000..6b1950a996 --- /dev/null +++ b/mayan/apps/navigation/templates/navigation/source_column_link_widget.html @@ -0,0 +1 @@ +{{ value }} diff --git a/mayan/apps/navigation/templatetags/navigation_tags.py b/mayan/apps/navigation/templatetags/navigation_tags.py index 2786acc6dd..107c241791 100644 --- a/mayan/apps/navigation/templatetags/navigation_tags.py +++ b/mayan/apps/navigation/templatetags/navigation_tags.py @@ -108,11 +108,6 @@ def navigation_resolve_menus(context, names, source=None, sort_results=None): return result -@register.simple_tag() -def navigation_source_column_get_absolute_url(source_column, obj): - return source_column.get_absolute_url(obj=obj) - - @register.simple_tag(takes_context=True) def navigation_source_column_resolve(context, column): if column: diff --git a/mayan/apps/navigation/tests/test_classes.py b/mayan/apps/navigation/tests/test_classes.py index 6a75f57740..8bca9d6af3 100644 --- a/mayan/apps/navigation/tests/test_classes.py +++ b/mayan/apps/navigation/tests/test_classes.py @@ -6,7 +6,7 @@ from django.urls import reverse from furl import furl from mayan.apps.acls.classes import ModelPermission -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from mayan.apps.common.tests.literals import TEST_VIEW_NAME from mayan.apps.permissions import Permission, PermissionNamespace diff --git a/mayan/apps/ocr/apps.py b/mayan/apps/ocr/apps.py index 1636761ca8..33b4f1893b 100644 --- a/mayan/apps/ocr/apps.py +++ b/mayan/apps/ocr/apps.py @@ -97,7 +97,7 @@ class OCRApp(MayanAppConfig): ) ModelField( - model=Document, name='versions__pages__ocr_content__content' + model=Document, name='versions__version_pages__ocr_content__content' ) ModelPermission.register( @@ -128,7 +128,7 @@ class OCRApp(MayanAppConfig): ) document_search.add_model_field( - field='versions__pages__ocr_content__content', label=_('OCR') + field='versions__version_pages__ocr_content__content', label=_('OCR') ) document_page_search.add_model_field( diff --git a/mayan/apps/ocr/backends/tesseract.py b/mayan/apps/ocr/backends/tesseract.py index 3444198e4d..0a8f87cf2e 100644 --- a/mayan/apps/ocr/backends/tesseract.py +++ b/mayan/apps/ocr/backends/tesseract.py @@ -4,11 +4,6 @@ import logging import shutil 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 _ @@ -115,15 +110,10 @@ class Tesseract(OCRBackendBase): logger.debug('Available languages: %s', ', '.join(self.languages)) def read_settings(self): - backend_arguments = yaml.load( - Loader=SafeLoader, - stream=setting_ocr_backend_arguments.value or '{}', - ) - - self.tesseract_binary_path = backend_arguments.get( + self.tesseract_binary_path = setting_ocr_backend_arguments.value.get( 'tesseract_path', DEFAULT_TESSERACT_BINARY_PATH ) - self.command_timeout = backend_arguments.get( + self.command_timeout = setting_ocr_backend_arguments.value.get( 'timeout', DEFAULT_TESSERACT_TIMEOUT ) diff --git a/mayan/apps/ocr/links.py b/mayan/apps/ocr/links.py index 5e580013c1..343bf26d18 100644 --- a/mayan/apps/ocr/links.py +++ b/mayan/apps/ocr/links.py @@ -9,8 +9,13 @@ from .permissions import ( permission_document_type_ocr_setup ) + +def is_document_page_disabled(context): + return not context['resolved_object'].enabled + + link_document_page_ocr_content = Link( - args='resolved_object.id', + args='resolved_object.id', conditional_disable=is_document_page_disabled, icon_class_path='mayan.apps.ocr.icons.icon_document_page_ocr_content', permissions=(permission_ocr_content_view,), text=_('OCR'), view='ocr:document_page_ocr_content', diff --git a/mayan/apps/ocr/managers.py b/mayan/apps/ocr/managers.py index b456c7bd25..ee8e1bd95a 100644 --- a/mayan/apps/ocr/managers.py +++ b/mayan/apps/ocr/managers.py @@ -8,7 +8,6 @@ from django.apps import apps from django.conf import settings from django.db import models, transaction -from mayan.apps.documents.storages import storage_documentimagecache from mayan.apps.documents.literals import DOCUMENT_IMAGE_TASK_TIMEOUT from mayan.apps.documents.tasks import task_generate_document_page_image @@ -47,9 +46,9 @@ class DocumentPageOCRContentManager(models.Manager): ) ) - cache_filename = task.get(timeout=DOCUMENT_IMAGE_TASK_TIMEOUT) + cache_filename = task.get(timeout=DOCUMENT_IMAGE_TASK_TIMEOUT, disable_sync_subtasks=False) - with storage_documentimagecache.open(cache_filename) as file_object: + with document_page.cache_partition.get_file(filename=cache_filename).open() as file_object: document_page_content, created = DocumentPageOCRContent.objects.get_or_create( document_page=document_page ) diff --git a/mayan/apps/ocr/runtime.py b/mayan/apps/ocr/runtime.py index 1d8643819b..55e6a36a35 100644 --- a/mayan/apps/ocr/runtime.py +++ b/mayan/apps/ocr/runtime.py @@ -1,20 +1,9 @@ from __future__ import unicode_literals -import yaml - -try: - from yaml import CSafeLoader as SafeLoader -except ImportError: - from yaml import SafeLoader - from django.utils.module_loading import import_string from .settings import setting_ocr_backend, setting_ocr_backend_arguments ocr_backend = import_string( dotted_path=setting_ocr_backend.value -)( - **yaml.load( - stream=setting_ocr_backend_arguments.value or '{}', Loader=SafeLoader - ) -) +)(**setting_ocr_backend_arguments.value) diff --git a/mayan/apps/ocr/settings.py b/mayan/apps/ocr/settings.py index 4293c3ac24..51b92149aa 100644 --- a/mayan/apps/ocr/settings.py +++ b/mayan/apps/ocr/settings.py @@ -13,7 +13,7 @@ setting_ocr_backend = namespace.add_setting( ) setting_ocr_backend_arguments = namespace.add_setting( global_name='OCR_BACKEND_ARGUMENTS', - default='' + default={} ) setting_auto_ocr = namespace.add_setting( global_name='OCR_AUTO_OCR', default=True, diff --git a/mayan/apps/ocr/tasks.py b/mayan/apps/ocr/tasks.py index 1ef2ffedf1..a6d9c82efb 100644 --- a/mayan/apps/ocr/tasks.py +++ b/mayan/apps/ocr/tasks.py @@ -28,7 +28,7 @@ def task_do_ocr(self, document_version_pk): logger.debug('trying to acquire lock: %s', lock_id) # Acquire lock to avoid doing OCR on the same document version more # than once concurrently - lock = locking_backend.acquire_lock(lock_id, LOCK_EXPIRE) + lock = locking_backend.acquire_lock(name=lock_id, timeout=LOCK_EXPIRE) logger.debug('acquired lock: %s', lock_id) document_version = None try: diff --git a/mayan/apps/ocr/tests/test_api.py b/mayan/apps/ocr/tests/test_api.py index b34c8a73f1..e847550141 100644 --- a/mayan/apps/ocr/tests/test_api.py +++ b/mayan/apps/ocr/tests/test_api.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals from rest_framework import status -from mayan.apps.documents.tests import DocumentTestMixin -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.documents.tests.mixins import DocumentTestMixin +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..permissions import ( permission_ocr_document, permission_ocr_content_view, diff --git a/mayan/apps/ocr/tests/test_indexing.py b/mayan/apps/ocr/tests/test_indexing.py index 2e785f391f..41c641774f 100644 --- a/mayan/apps/ocr/tests/test_indexing.py +++ b/mayan/apps/ocr/tests/test_indexing.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import BaseTestCase -from mayan.apps.documents.tests import DocumentTestMixin +from mayan.apps.common.tests.base import BaseTestCase +from mayan.apps.documents.tests.mixins import DocumentTestMixin from mayan.apps.document_indexing.models import Index, IndexInstanceNode from mayan.apps.document_indexing.tests.literals import TEST_INDEX_LABEL diff --git a/mayan/apps/ocr/tests/test_models.py b/mayan/apps/ocr/tests/test_models.py index d2a842054a..cd4992d0d4 100644 --- a/mayan/apps/ocr/tests/test_models.py +++ b/mayan/apps/ocr/tests/test_models.py @@ -2,10 +2,9 @@ from __future__ import unicode_literals from django.test import override_settings -from mayan.apps.common.tests import BaseTestCase -from mayan.apps.documents.tests import ( - DocumentTestMixin, TEST_DEU_DOCUMENT_PATH -) +from mayan.apps.common.tests.base import BaseTestCase +from mayan.apps.documents.tests.mixins import DocumentTestMixin +from mayan.apps.documents.tests.literals import TEST_DEU_DOCUMENT_PATH from .literals import ( TEST_DOCUMENT_CONTENT, TEST_DOCUMENT_CONTENT_DEU_1, diff --git a/mayan/apps/ocr/tests/test_views.py b/mayan/apps/ocr/tests/test_views.py index 49b865189d..6ed278c54a 100644 --- a/mayan/apps/ocr/tests/test_views.py +++ b/mayan/apps/ocr/tests/test_views.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from ..models import DocumentPageOCRContent from ..permissions import ( diff --git a/mayan/apps/ocr/urls.py b/mayan/apps/ocr/urls.py index 5ef6a45244..3d2d9b0b25 100644 --- a/mayan/apps/ocr/urls.py +++ b/mayan/apps/ocr/urls.py @@ -14,11 +14,6 @@ from .views import ( ) urlpatterns = [ - url( - regex=r'^documents/pages/(?P\d+)/content/$', - view=DocumentPageOCRContentView.as_view(), - name='document_page_ocr_content' - ), url( regex=r'^documents/(?P\d+)/content/$', view=DocumentOCRContentView.as_view(), name='document_ocr_content' @@ -34,21 +29,8 @@ urlpatterns = [ name='document_ocr_content_delete_multiple' ), url( - regex=r'^documents/(?P\d+)/submit/$', - view=DocumentSubmitView.as_view(), name='document_submit' - ), - url( - regex=r'^document_types/submit/$', - view=DocumentTypeSubmitView.as_view(), name='document_type_submit' - ), - url( - regex=r'^documents/multiple/submit/$', - view=DocumentSubmitView.as_view(), name='document_submit_multiple' - ), - url( - regex=r'^document_types/(?P\d+)/ocr/settings/$', - view=DocumentTypeSettingsEditView.as_view(), - name='document_type_ocr_settings' + regex=r'^documents/(?P\d+)/ocr/download/$', + view=DocumentOCRDownloadView.as_view(), name='document_ocr_download' ), url( regex=r'^documents/(?P\d+)/ocr/errors/$', @@ -56,10 +38,28 @@ urlpatterns = [ name='document_ocr_error_list' ), url( - regex=r'^documents/(?P\d+)/ocr/download/$', - view=DocumentOCRDownloadView.as_view(), name='document_ocr_download' + regex=r'^documents/(?P\d+)/submit/$', + view=DocumentSubmitView.as_view(), name='document_submit' ), - url(regex=r'^all/$', view=EntryListView.as_view(), name='entry_list'), + url( + regex=r'^documents/multiple/submit/$', + view=DocumentSubmitView.as_view(), name='document_submit_multiple' + ), + url( + regex=r'^documents/pages/(?P\d+)/content/$', + view=DocumentPageOCRContentView.as_view(), + name='document_page_ocr_content' + ), + url( + regex=r'^document_types/submit/$', + view=DocumentTypeSubmitView.as_view(), name='document_type_submit' + ), + url( + regex=r'^document_types/(?P\d+)/ocr/settings/$', + view=DocumentTypeSettingsEditView.as_view(), + name='document_type_ocr_settings' + ), + url(regex=r'^logs/$', view=EntryListView.as_view(), name='entry_list'), ] api_urls = [ diff --git a/mayan/apps/permissions/apps.py b/mayan/apps/permissions/apps.py index c1c7392966..16c7de3f1c 100644 --- a/mayan/apps/permissions/apps.py +++ b/mayan/apps/permissions/apps.py @@ -31,7 +31,7 @@ from .methods import method_group_roles_add, method_group_roles_remove from .permissions import ( permission_role_delete, permission_role_edit, permission_role_view ) -from .search import * # NOQA +from .search import role_search # NOQA class PermissionsApp(MayanAppConfig): diff --git a/mayan/apps/permissions/tests/test_api.py b/mayan/apps/permissions/tests/test_api.py index 816eb49b48..19e318d510 100644 --- a/mayan/apps/permissions/tests/test_api.py +++ b/mayan/apps/permissions/tests/test_api.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from rest_framework import status -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.rest_api.tests.base import BaseAPITestCase from mayan.apps.user_management.tests.mixins import GroupTestMixin from ..classes import Permission diff --git a/mayan/apps/permissions/tests/test_events.py b/mayan/apps/permissions/tests/test_events.py index 9fa5057bc6..6a207ac9d8 100644 --- a/mayan/apps/permissions/tests/test_events.py +++ b/mayan/apps/permissions/tests/test_events.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from actstream.models import Action -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from ..events import event_role_created, event_role_edited from ..permissions import permission_role_create, permission_role_edit diff --git a/mayan/apps/permissions/tests/test_models.py b/mayan/apps/permissions/tests/test_models.py index 3448cdbb15..c10dde1d05 100644 --- a/mayan/apps/permissions/tests/test_models.py +++ b/mayan/apps/permissions/tests/test_models.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.core.exceptions import PermissionDenied -from mayan.apps.common.tests import BaseTestCase +from mayan.apps.common.tests.base import BaseTestCase from mayan.apps.user_management.tests.mixins import GroupTestMixin from ..classes import Permission, PermissionNamespace diff --git a/mayan/apps/permissions/tests/test_views.py b/mayan/apps/permissions/tests/test_views.py index bdc7399d17..6276b178cb 100644 --- a/mayan/apps/permissions/tests/test_views.py +++ b/mayan/apps/permissions/tests/test_views.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from mayan.apps.user_management.permissions import permission_group_edit from mayan.apps.user_management.tests.mixins import GroupTestMixin diff --git a/mayan/apps/permissions/urls.py b/mayan/apps/permissions/urls.py index 3cf84ecfcd..5ab95f277a 100644 --- a/mayan/apps/permissions/urls.py +++ b/mayan/apps/permissions/urls.py @@ -10,30 +10,30 @@ from .views import ( urlpatterns = [ url( - regex=r'^group/(?P\d+)/roles/$', + regex=r'^groups/(?P\d+)/roles/$', view=GroupRolesView.as_view(), name='group_roles' ), - url(regex=r'^role/list/$', view=RoleListView.as_view(), name='role_list'), + url(regex=r'^roles/$', view=RoleListView.as_view(), name='role_list'), url( - regex=r'^role/create/$', view=RoleCreateView.as_view(), + regex=r'^roles/create/$', view=RoleCreateView.as_view(), name='role_create' ), url( - regex=r'^role/(?P\d+)/permissions/$', - view=SetupRolePermissionsView.as_view(), name='role_permissions' - ), - url( - regex=r'^role/(?P\d+)/edit/$', view=RoleEditView.as_view(), - name='role_edit' - ), - url( - regex=r'^role/(?P\d+)/delete/$', view=RoleDeleteView.as_view(), + regex=r'^roles/(?P\d+)/delete/$', view=RoleDeleteView.as_view(), name='role_delete' ), url( - regex=r'^role/(?P\d+)/groups/$', + regex=r'^roles/(?P\d+)/edit/$', view=RoleEditView.as_view(), + name='role_edit' + ), + url( + regex=r'^roles/(?P\d+)/groups/$', view=SetupRoleMembersView.as_view(), name='role_groups' ), + url( + regex=r'^roles/(?P\d+)/permissions/$', + view=SetupRolePermissionsView.as_view(), name='role_permissions' + ), ] api_urls = [ diff --git a/mayan/apps/platform/classes.py b/mayan/apps/platform/classes.py index 1d0152442f..8fc3a92cb6 100644 --- a/mayan/apps/platform/classes.py +++ b/mayan/apps/platform/classes.py @@ -2,20 +2,16 @@ from __future__ import absolute_import, unicode_literals import os -import yaml -try: - from yaml import CSafeLoader as SafeLoader -except ImportError: - from yaml import SafeLoader - from django.template import loader +from django.utils.html import mark_safe from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ -from mayan.apps.common.settings import ( +from mayan.apps.common.serialization import yaml_dump, yaml_load +from mayan.apps.task_manager.classes import Worker +from mayan.apps.task_manager.settings import ( setting_celery_broker_url, setting_celery_result_backend ) -from mayan.apps.task_manager.classes import Worker class Variable(object): @@ -24,9 +20,25 @@ class Variable(object): self.default = default self.environment_name = environment_name - def get_value(self): + def _get_value(self): return os.environ.get(self.environment_name, self.default) + def get_value(self): + return mark_safe(self._get_value()) + + +class YAMLVariable(Variable): + def _get_value(self): + value = os.environ.get(self.environment_name) + if value: + value = yaml_load(stream=value) + else: + value = self.default + + return yaml_dump( + data=value, allow_unicode=True, default_flow_style=True, width=999 + ).replace('...\n', '').replace('\n', '') + @python_2_unicode_compatible class PlatformTemplate(object): @@ -95,9 +107,7 @@ class PlatformTemplate(object): if context_string: context.update( - yaml.load( - stream=context_string, Loader=SafeLoader - ) + yaml_load(stream=context_string) ) return loader.render_to_string( template_name=self.get_template_name(), @@ -106,10 +116,6 @@ class PlatformTemplate(object): class PlatformTemplateSupervisord(PlatformTemplate): - context_defaults = { - 'BROKER_URL': 'redis://127.0.0.1:6379/0', - 'CELERY_RESULT_BACKEND': 'redis://127.0.0.1:6379/0', - } label = _('Template for Supervisord.') name = 'supervisord' settings = ( @@ -124,35 +130,38 @@ class PlatformTemplateSupervisord(PlatformTemplate): name='GUNICORN_TIMEOUT', default=120, environment_name='MAYAN_GUNICORN_TIMEOUT' ), - Variable( - name='DATABASE_CONN_MAX_AGE', default=0, - environment_name='MAYAN_DATABASE_CONN_MAX_AGE' - ), - Variable( - name='DATABASE_ENGINE', default='django.db.backends.postgresql', - environment_name='MAYAN_DATABASE_ENGINE' - ), - Variable( - name='DATABASE_HOST', default='127.0.0.1', - environment_name='MAYAN_DATABASE_HOST' - ), - Variable( - name='DATABASE_NAME', default='mayan', - environment_name='MAYAN_DATABASE_NAME' - ), - Variable( - name='DATABASE_PASSWORD', default='mayanuserpass', - environment_name='MAYAN_DATABASE_PASSWORD' - ), - Variable( - name='DATABASE_USER', default='mayan', - environment_name='MAYAN_DATABASE_USER' - ), Variable( name='INSTALLATION_PATH', default='/opt/mayan-edms', environment_name='MAYAN_INSTALLATION_PATH' ), - Variable( + YAMLVariable( + name='ALLOWED_HOSTS', + default=['*'], + environment_name='MAYAN_ALLOWED_HOSTS' + ), + YAMLVariable( + name='CELERY_BROKER_URL', + default='redis://127.0.0.1:6379/0', + environment_name='MAYAN_CELERY_BROKER_URL' + ), + YAMLVariable( + name='CELERY_RESULT_BACKEND', + default='redis://127.0.0.1:6379/0', + environment_name='MAYAN_CELERY_RESULT_BACKEND' + ), + YAMLVariable( + name='DATABASES', + default={ + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'mayan', 'PASSWORD': 'mayanuserpass', + 'USER': 'mayan', 'HOST': '127.0.0.1' + } + }, + environment_name='MAYAN_DATABASES' + ), + YAMLVariable + ( name='MEDIA_ROOT', default='/opt/mayan-edms/media', environment_name='MAYAN_MEDIA_ROOT' ), @@ -172,5 +181,28 @@ class PlatformTemplateSupervisordDocker(PlatformTemplate): return {'workers': Worker.all()} +class PlatformTemplateWorkerQueues(PlatformTemplate): + label = _('Template showing the queues of a worker.') + name = 'worker_queues' + + variables = ( + Variable( + name='WORKER_NAME', default=None, + environment_name='MAYAN_WORKER_NAME' + ), + ) + + def get_context(self): + worker_name = self.get_variables_context().get('WORKER_NAME') + queues = Worker.get(name=worker_name).queues + + return { + 'queues': queues, 'queue_names': sorted( + map(lambda x: x.name, queues) + ) + } + + PlatformTemplate.register(klass=PlatformTemplateSupervisord) PlatformTemplate.register(klass=PlatformTemplateSupervisordDocker) +PlatformTemplate.register(klass=PlatformTemplateWorkerQueues) diff --git a/mayan/apps/platform/templates/platform/supervisord.tmpl b/mayan/apps/platform/templates/platform/supervisord.tmpl index b87686c827..8f34d4406e 100644 --- a/mayan/apps/platform/templates/platform/supervisord.tmpl +++ b/mayan/apps/platform/templates/platform/supervisord.tmpl @@ -1,17 +1,12 @@ [supervisord] environment= - MAYAN_ALLOWED_HOSTS='["*"]', # Allow access to other network hosts other than localhost + PYTHONPATH="%(ENV_PYTHONPATH)s:{{ MEDIA_ROOT }}/mayan_settings", + DJANGO_SETTINGS_MODULE=mayan.settings.production, + MAYAN_MEDIA_ROOT="{{ MEDIA_ROOT }}", + MAYAN_ALLOWED_HOSTS="{{ ALLOWED_HOSTS }}", MAYAN_CELERY_RESULT_BACKEND="{{ CELERY_RESULT_BACKEND }}", - MAYAN_BROKER_URL="{{ BROKER_URL }}", - PYTHONPATH={{ INSTALLATION_PATH }}/lib/python2.7/site-packages:{{ MEDIA_ROOT }}/mayan_settings, - MAYAN_MEDIA_ROOT={{ MEDIA_ROOT }}, - MAYAN_DATABASE_ENGINE={{ DATABASE_ENGINE }}, - MAYAN_DATABASE_HOST={{ DATABASE_HOST }}, - MAYAN_DATABASE_NAME={{ DATABASE_NAME }}, - MAYAN_DATABASE_PASSWORD={{ DATABASE_PASSWORD }}, - MAYAN_DATABASE_USER={{ DATABASE_USER }}, - MAYAN_DATABASE_CONN_MAX_AGE={{ DATABASE_CONN_MAX_AGE }}, - DJANGO_SETTINGS_MODULE=mayan.settings.production + MAYAN_CELERY_BROKER_URL="{{ CELERY_BROKER_URL }}", + MAYAN_DATABASES="{{ DATABASES }}" [program:mayan-gunicorn] autorestart = true @@ -23,7 +18,7 @@ user = mayan [program:mayan-worker-{{ worker.name }}] autorestart = true autostart = true -command = nice -n {{ worker.nice_level }} {{ INSTALLATION_PATH }}/bin/mayan-edms.py celery worker -Ofair -l ERROR -Q {% for queue in worker.queues %}{{ queue.name }}{% if not forloop.last %},{% endif %}{% endfor %} -n mayan-worker-{{ worker.name }}.%%h --concurrency=1 +command = nice -n {{ worker.nice_level }} {{ INSTALLATION_PATH }}/bin/celery worker -A mayan -Ofair -l ERROR -Q {% for queue in worker.queues %}{{ queue.name }}{% if not forloop.last %},{% endif %}{% endfor %} -n mayan-worker-{{ worker.name }}.%%h --concurrency=1 killasgroup = true numprocs = 1 priority = 998 @@ -35,7 +30,7 @@ user = mayan [program:mayan-celery-beat] autorestart = true autostart = true -command = nice -n 1 {{ INSTALLATION_PATH }}/bin/mayan-edms.py celery beat --pidfile= -l ERROR +command = nice -n 1 {{ INSTALLATION_PATH }}/bin/celery beat -A mayan --pidfile= -l ERROR killasgroup = true numprocs = 1 priority = 998 diff --git a/mayan/apps/platform/templates/platform/supervisord_docker.tmpl b/mayan/apps/platform/templates/platform/supervisord_docker.tmpl index 7cad0e0c3b..7ca6278191 100644 --- a/mayan/apps/platform/templates/platform/supervisord_docker.tmpl +++ b/mayan/apps/platform/templates/platform/supervisord_docker.tmpl @@ -1,7 +1,7 @@ [program:mayan-gunicorn] autorestart = false autostart = true -command = /bin/bash -c "${MAYAN_GUNICORN_BIN} -w ${MAYAN_GUNICORN_WORKERS} mayan.wsgi --max-requests 500 --max-requests-jitter 50 --worker-class gevent --bind 0.0.0.0:8000 --env DJANGO_SETTINGS_MODULE=${MAYAN_SETTINGS_MODULE}" --timeout ${MAYAN_GUNICORN_TIMEOUT} +command = /bin/bash -c "${MAYAN_GUNICORN_BIN} -w ${MAYAN_GUNICORN_WORKERS} mayan.wsgi --max-requests 500 --max-requests-jitter 50 --worker-class sync --bind 0.0.0.0:8000 --env DJANGO_SETTINGS_MODULE=${MAYAN_SETTINGS_MODULE}" --timeout ${MAYAN_GUNICORN_TIMEOUT} redirect_stderr = true stderr_logfile = /dev/fd/2 stderr_logfile_maxbytes = 0 @@ -9,21 +9,11 @@ stdout_logfile = /dev/fd/1 stdout_logfile_maxbytes = 0 user = mayan -[program:redis] -autorestart = false -autostart = true -command = /bin/bash -c "if [ ${MAYAN_BROKER_URL} == ${MAYAN_DEFAULT_BROKER_URL} ] && [ ${MAYAN_CELERY_RESULT_BACKEND} == ${MAYAN_DEFAULT_CELERY_RESULT_BACKEND} ];then /usr/bin/redis-server /etc/redis/;fi" -stderr_logfile = /dev/fd/2 -stderr_logfile_maxbytes = 0 -stdout_logfile = /dev/fd/1 -stdout_logfile_maxbytes = 0 -user = root - {% for worker in workers %} [program:mayan-worker-{{ worker.name }}] autorestart = false autostart = true -command = nice -n {{ worker.nice_level }} /bin/bash -c "${MAYAN_BIN} celery --settings=${MAYAN_SETTINGS_MODULE} worker -Ofair -l ERROR -Q {% for queue in worker.queues %}{{ queue.name }}{% if not forloop.last %},{% endif %}{% endfor %} -n mayan-worker-{{ worker.name }}.%%h ${MAYAN_WORKER_{{ worker.name|upper }}_CONCURRENCY}" +command = nice -n {{ worker.nice_level }} /bin/bash -c "${MAYAN_PYTHON_BIN_DIR}celery worker -A mayan -Ofair -l ERROR -Q {% for queue in worker.queues %}{{ queue.name }}{% if not forloop.last %},{% endif %}{% endfor %} -n mayan-worker-{{ worker.name }}.%%h ${MAYAN_WORKER_{{ worker.name|upper }}_CONCURRENCY}" killasgroup = true numprocs = 1 priority = 998 @@ -39,7 +29,7 @@ user = mayan [program:mayan-celery-beat] autorestart = false autostart = true -command = nice -n 1 /bin/bash -c "${MAYAN_BIN} celery --settings=${MAYAN_SETTINGS_MODULE} beat --pidfile= -l ERROR" +command = nice -n 1 /bin/bash -c "${MAYAN_PYTHON_BIN_DIR}celery -A mayan beat --pidfile= -l ERROR" killasgroup = true numprocs = 1 priority = 998 diff --git a/mayan/apps/platform/templates/platform/worker_queues.tmpl b/mayan/apps/platform/templates/platform/worker_queues.tmpl new file mode 100644 index 0000000000..871cafccb8 --- /dev/null +++ b/mayan/apps/platform/templates/platform/worker_queues.tmpl @@ -0,0 +1 @@ +{{ queue_names|join:"," }} diff --git a/mayan/apps/redactions/apps.py b/mayan/apps/redactions/apps.py index 4c729c2684..a7460b817d 100644 --- a/mayan/apps/redactions/apps.py +++ b/mayan/apps/redactions/apps.py @@ -6,15 +6,12 @@ from django.apps import apps from django.utils.translation import ugettext_lazy as _ from mayan.apps.common.apps import MayanAppConfig -from mayan.apps.common.menus import ( - menu_list_facet, menu_object, menu_secondary, -) +from mayan.apps.converter.links import link_transformation_list +from mayan.apps.common.menus import menu_list_facet from .dependencies import * # NOQA -from .links import ( - link_redaction_create, link_redaction_delete, link_redaction_edit, - link_redaction_list -) +from .layers import layer_redactions +from .transformations import * # NOQA logger = logging.getLogger(__name__) @@ -33,24 +30,12 @@ class RedactionsApp(MayanAppConfig): DocumentPage = apps.get_model( app_label='documents', model_name='DocumentPage' ) - Redaction = self.get_model(model_name='Redaction') + + link_redaction_list = link_transformation_list.copy( + layer=layer_redactions + ) + link_redaction_list.text = _('Redactions') menu_list_facet.bind_links( - links=( - link_redaction_list, - ), sources=(DocumentPage,) - ) - menu_object.bind_links( - links=(link_redaction_delete, link_redaction_edit,), - sources=(Redaction,) - ) - menu_secondary.bind_links( - links=(link_redaction_create,), sources=(Redaction,) - ) - menu_secondary.bind_links( - links=(link_redaction_create,), - sources=( - 'redactions:redaction_create', - 'redactions:redaction_list' - ) + links=(link_redaction_list,), sources=(DocumentPage,) ) diff --git a/mayan/apps/redactions/dependencies.py b/mayan/apps/redactions/dependencies.py index 698a1a09f4..77c4dc7879 100644 --- a/mayan/apps/redactions/dependencies.py +++ b/mayan/apps/redactions/dependencies.py @@ -2,9 +2,7 @@ from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ -from mayan.apps.dependencies.classes import ( - GoogleFontDependency, JavaScriptDependency -) +from mayan.apps.dependencies.classes import JavaScriptDependency JavaScriptDependency( label=_('JavaScript image cropper'), module=__name__, name='cropperjs', diff --git a/mayan/apps/redactions/layers.py b/mayan/apps/redactions/layers.py new file mode 100644 index 0000000000..e44fa77856 --- /dev/null +++ b/mayan/apps/redactions/layers.py @@ -0,0 +1,27 @@ +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.converter.classes import Layer +from mayan.apps.converter.layers import layer_saved_transformations + +from .permissions import ( + permission_redaction_create, permission_redaction_delete, + permission_redaction_edit, permission_redaction_exclude, + permission_redaction_view +) + +layer_redactions = Layer( + empty_results_text=_( + 'Redactions allow removing access to confidential and ' + 'sensitive information without having to modify the document.' + ), label=_('Redactions'), name='redactions', + order=layer_saved_transformations.order - 1, permissions={ + 'create': permission_redaction_create, + 'delete': permission_redaction_delete, + 'exclude': permission_redaction_exclude, + 'edit': permission_redaction_edit, + 'select': permission_redaction_create, + 'view': permission_redaction_view, + }, symbol='highlighter' +) diff --git a/mayan/apps/redactions/models.py b/mayan/apps/redactions/models.py deleted file mode 100644 index 1ae9d7c00b..0000000000 --- a/mayan/apps/redactions/models.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import unicode_literals - -from django.utils.translation import ugettext_lazy as _ - -from mayan.apps.converter.models import Transformation - - -class Redaction(Transformation): - class Meta: - proxy = True - verbose_name = _('Redaction') - verbose_name_plural = _('Redactions') diff --git a/mayan/apps/redactions/permissions.py b/mayan/apps/redactions/permissions.py index e3ad39609e..1ee2d31799 100644 --- a/mayan/apps/redactions/permissions.py +++ b/mayan/apps/redactions/permissions.py @@ -15,6 +15,9 @@ permission_redaction_delete = namespace.add_permission( permission_redaction_edit = namespace.add_permission( label=_('Edit redactions'), name='redaction_edit' ) +permission_redaction_exclude = namespace.add_permission( + label=_('Exclude redactions'), name='redaction_exclude' +) permission_redaction_view = namespace.add_permission( label=_('View existing redactions'), name='redaction_view' ) diff --git a/mayan/apps/redactions/templates/redactions/cropper.html b/mayan/apps/redactions/templates/redactions/cropper.html index a042ea840b..6dcbb8bea5 100644 --- a/mayan/apps/redactions/templates/redactions/cropper.html +++ b/mayan/apps/redactions/templates/redactions/cropper.html @@ -4,13 +4,19 @@ {% load static %} {% load common_tags %} +{% load documents_tags %} -{% block title %}{% blocktrans with object as object %}Redaction coordinates for: {{ object }}{% endblocktrans %}{% endblock title %} +{% block title %}{{ title }}{% endblock title %} {% block stylesheets %} {% endblock %} {% block content %}
    - +

    @@ -38,29 +48,33 @@ var pic_real_width, pic_real_height; var canvasData; var containerData; - var $image = $('.cropper-main img'); - var cropperInstance; + var $image = $('#cropper-img'); + var image = document.getElementById('cropper-img'); var defaultArguments = { left: 10, top: 10, right: 10, bottom: 10, - fillcolor: '#000000', } - var initialArguments = JSON.parse($('#id_arguments').text() || JSON.stringify(defaultArguments)); + var initialArguments = JSON.parse( + $('#id_arguments').text() || JSON.stringify(defaultArguments) + ); var callbackCrop = function (data) { var crop_left = (data.detail.x / pic_real_width * 100).toFixed(2); var crop_top = (data.detail.y / pic_real_height * 100).toFixed(2); - var crop_right = (100.001 - (data.detail.x + data.detail.width) / pic_real_width * 100).toFixed(2); - var crop_bottom = (100.001 - (data.detail.y + data.detail.height) / pic_real_height * 100).toFixed(2); + var crop_right = ( + 100.001 - (data.detail.x + data.detail.width) / pic_real_width * 100 + ).toFixed(2); + var crop_bottom = ( + 100.001 - (data.detail.y + data.detail.height) / pic_real_height * 100 + ).toFixed(2); var arguments = { 'left': parseFloat(crop_left), 'top': parseFloat(crop_top), 'right': parseFloat(crop_right), 'bottom': parseFloat(crop_bottom), - 'fillcolor': '#000000', } $('#id_arguments').text(JSON.stringify(arguments)); @@ -85,16 +99,16 @@ pic_real_height = this.height; }); - cropperInstance = $image.cropper({ + new Cropper( + image, { crop: callbackCrop, + highlight: false, mouseWheelZoom: false, movable: false, - //preview: '.cropper-preview', ready: function () { - canvasData = $image.cropper('getCanvasData'); - containerData = $image.cropper('getContainerData'); - - $image.cropper('setCropBoxData', { + canvasData = this.cropper.getCanvasData(); + containerData = this.cropper.getContainerData(); + this.cropper.setCropBoxData({ left: initialArguments.left / 100.0 * canvasData.width + canvasData.left, top: initialArguments.top / 100.0 * canvasData.height + canvasData.top, width: (100.0 - initialArguments.right - initialArguments.left) / 100.0 * canvasData.width, diff --git a/mayan/apps/redactions/transformations.py b/mayan/apps/redactions/transformations.py new file mode 100644 index 0000000000..19e594e5c9 --- /dev/null +++ b/mayan/apps/redactions/transformations.py @@ -0,0 +1,21 @@ +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.converter.transformations import ( + BaseTransformation, TransformationDrawRectanglePercent +) + +from .layers import layer_redactions + + +class TransformationRedactionPercent(TransformationDrawRectanglePercent): + arguments = ('left', 'top', 'right', 'bottom') + label = _('Redaction') + name = 'redaction_percent' + template_name = 'redactions/cropper.html' + + +BaseTransformation.register( + layer=layer_redactions, transformation=TransformationRedactionPercent +) diff --git a/mayan/apps/rest_api/tests/__init__.py b/mayan/apps/rest_api/tests/__init__.py index 0b7c12d02a..e69de29bb2 100644 --- a/mayan/apps/rest_api/tests/__init__.py +++ b/mayan/apps/rest_api/tests/__init__.py @@ -1 +0,0 @@ -from .base import BaseAPITestCase # NOQA diff --git a/mayan/apps/rest_api/tests/base.py b/mayan/apps/rest_api/tests/base.py index 114549d89e..7419688585 100644 --- a/mayan/apps/rest_api/tests/base.py +++ b/mayan/apps/rest_api/tests/base.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals from rest_framework.test import APITestCase -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from mayan.apps.permissions.classes import Permission from mayan.apps.smart_settings.classes import Namespace diff --git a/mayan/apps/smart_settings/classes.py b/mayan/apps/smart_settings/classes.py index 9f35323608..4ea8ae11dd 100644 --- a/mayan/apps/smart_settings/classes.py +++ b/mayan/apps/smart_settings/classes.py @@ -9,11 +9,6 @@ import sys import yaml -try: - from yaml import CSafeLoader as SafeLoader, CSafeDumper as SafeDumper -except ImportError: - from yaml import SafeLoader, SafeDumper - from django.apps import apps from django.conf import settings from django.utils.functional import Promise @@ -21,6 +16,10 @@ from django.utils.encoding import ( force_bytes, force_text, python_2_unicode_compatible ) +from mayan.apps.common.serialization import yaml_dump, yaml_load + +from .utils import read_configuration_file + logger = logging.getLogger(__name__) @@ -82,10 +81,11 @@ class Namespace(object): class Setting(object): _registry = {} _cache_hash = None + _config_file_cache = None @staticmethod def deserialize_value(value): - return yaml.load(stream=value, Loader=SafeLoader) + return yaml_load(stream=value) @staticmethod def express_promises(value): @@ -101,9 +101,9 @@ class Setting(object): @staticmethod def serialize_value(value): - result = yaml.dump( + result = yaml_dump( data=Setting.express_promises(value), allow_unicode=True, - Dumper=SafeDumper + default_flow_style=False, ) # safe_dump returns bytestrings # Disregard the last 3 dots that mark the end of the YAML document @@ -128,8 +128,8 @@ class Setting(object): if (filter_term and filter_term.lower() in setting.global_name.lower()) or not filter_term: dictionary[setting.global_name] = Setting.express_promises(setting.value) - return yaml.dump( - data=dictionary, default_flow_style=False, Dumper=SafeDumper + return yaml_dump( + data=dictionary, default_flow_style=False ) @classmethod @@ -140,6 +140,16 @@ class Setting(object): def get_all(cls): return sorted(cls._registry.values(), key=lambda x: x.global_name) + @classmethod + def get_config_file_content(cls): + # Cache content of config file to speed up initial boot up + if not cls._config_file_cache: + cls._config_file_cache = read_configuration_file( + path=settings.CONFIGURATION_FILEPATH + ) + + return cls._config_file_cache + @classmethod def get_hash(cls): return force_text( @@ -167,14 +177,14 @@ class Setting(object): path=settings.CONFIGURATION_LAST_GOOD_FILEPATH ) - def __init__(self, namespace, global_name, default, help_text=None, is_path=False, quoted=False): + def __init__(self, namespace, global_name, default, help_text=None, is_path=False, post_edit_function=None): self.global_name = global_name self.default = default self.help_text = help_text self.loaded = False self.namespace = namespace - self.quoted = quoted self.environment_variable = False + self.post_edit_function = post_edit_function namespace._settings.append(self) self.__class__._registry[global_name] = self @@ -186,7 +196,7 @@ class Setting(object): if environment_value: self.environment_variable = True try: - self.raw_value = environment_value + self.raw_value = yaml_load(stream=environment_value) except yaml.YAMLError as exception: raise type(exception)( 'Error interpreting environment variable: {} with ' @@ -195,7 +205,12 @@ class Setting(object): ) ) else: - self.raw_value = getattr(settings, self.global_name, self.default) + self.raw_value = self.get_config_file_content().get( + self.global_name, getattr( + settings, self.global_name, self.default + ) + ) + self.yaml = Setting.serialize_value(self.raw_value) self.loaded = True @@ -225,3 +240,5 @@ class Setting(object): # value is in YAML format self.yaml = value self.raw_value = Setting.deserialize_value(value) + if self.post_edit_function: + self.post_edit_function(setting=self) diff --git a/mayan/apps/smart_settings/forms.py b/mayan/apps/smart_settings/forms.py index d72548316d..ecc7067ffd 100644 --- a/mayan/apps/smart_settings/forms.py +++ b/mayan/apps/smart_settings/forms.py @@ -2,15 +2,12 @@ from __future__ import unicode_literals import yaml -try: - from yaml import CSafeLoader as SafeLoader -except ImportError: - from yaml import SafeLoader - from django import forms from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ +from mayan.apps.common.serialization import yaml_load + class SettingForm(forms.Form): value = forms.CharField( @@ -25,20 +22,8 @@ class SettingForm(forms.Form): self.fields['value'].initial = self.setting.serialized_value def clean(self): - quotes = ['"', "'"] - - if self.setting.quoted: - stripped = self.cleaned_data['value'].strip() - - if stripped[0] not in quotes or stripped[-1] not in quotes: - raise ValidationError( - _( - 'Value must be properly quoted.' - ) - ) - try: - yaml.load(stream=self.cleaned_data['value'], Loader=SafeLoader) + yaml_load(stream=self.cleaned_data['value']) except yaml.YAMLError: raise ValidationError( _( diff --git a/mayan/apps/smart_settings/literals.py b/mayan/apps/smart_settings/literals.py new file mode 100644 index 0000000000..6db3f2ee9c --- /dev/null +++ b/mayan/apps/smart_settings/literals.py @@ -0,0 +1,36 @@ +from __future__ import unicode_literals + +# Default in YAML format +BOOTSTRAP_SETTING_LIST = ( + {'name': 'ALLOWED_HOSTS', 'default': "['127.0.0.1', 'localhost', '[::1]']"}, + {'name': 'APPEND_SLASH'}, + {'name': 'AUTH_PASSWORD_VALIDATORS'}, + {'name': 'COMMON_DISABLED_APPS'}, + {'name': 'COMMON_EXTRA_APPS'}, + {'name': 'DATA_UPLOAD_MAX_MEMORY_SIZE'}, + {'name': 'DATABASES'}, + {'name': 'DEBUG', 'default': 'false'}, + {'name': 'DEFAULT_FROM_EMAIL'}, + {'name': 'DISALLOWED_USER_AGENTS'}, + {'name': 'EMAIL_BACKEND'}, + {'name': 'EMAIL_HOST'}, + {'name': 'EMAIL_HOST_PASSWORD'}, + {'name': 'EMAIL_HOST_USER'}, + {'name': 'EMAIL_PORT'}, + {'name': 'EMAIL_TIMEOUT'}, + {'name': 'EMAIL_USE_SSL'}, + {'name': 'EMAIL_USE_TLS'}, + {'name': 'FILE_UPLOAD_MAX_MEMORY_SIZE'}, + {'name': 'HOME_VIEW'}, + {'name': 'INSTALLED_APPS'}, + {'name': 'INTERNAL_IPS', 'default': "['127.0.0.1']"}, + {'name': 'LANGUAGES'}, + {'name': 'LANGUAGE_CODE'}, + {'name': 'LOGIN_REDIRECT_URL', 'default': 'common:home'}, + {'name': 'LOGIN_URL', 'default': 'authentication:login_view'}, + {'name': 'LOGOUT_REDIRECT_URL', 'default': 'authentication:login_view'}, + {'name': 'STATIC_URL'}, + {'name': 'STATICFILES_STORAGE'}, + {'name': 'TIME_ZONE'}, + {'name': 'WSGI_APPLICATION'} +) diff --git a/mayan/apps/smart_settings/settings.py b/mayan/apps/smart_settings/settings.py new file mode 100644 index 0000000000..8ee2f84879 --- /dev/null +++ b/mayan/apps/smart_settings/settings.py @@ -0,0 +1,302 @@ +from __future__ import unicode_literals + +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.smart_settings.classes import Namespace + +# Don't import anything on start import, we just want to make it easy +# for apps.py to activate the settings in this module. +__all__ = () +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.' + ), +) diff --git a/mayan/apps/smart_settings/tests/test_classes.py b/mayan/apps/smart_settings/tests/test_classes.py index 4eb6839ea9..266e1e75f4 100644 --- a/mayan/apps/smart_settings/tests/test_classes.py +++ b/mayan/apps/smart_settings/tests/test_classes.py @@ -8,7 +8,7 @@ from django.conf import settings from django.utils.encoding import force_text from mayan.apps.common.settings import setting_paginate_by -from mayan.apps.common.tests import BaseTestCase +from mayan.apps.common.tests.base import BaseTestCase from mayan.apps.storage.utils import fs_cleanup from ..classes import Setting diff --git a/mayan/apps/smart_settings/tests/test_views.py b/mayan/apps/smart_settings/tests/test_views.py index 50748d5f6d..78e972e069 100644 --- a/mayan/apps/smart_settings/tests/test_views.py +++ b/mayan/apps/smart_settings/tests/test_views.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, unicode_literals -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from ..permissions import permission_settings_view diff --git a/mayan/apps/smart_settings/urls.py b/mayan/apps/smart_settings/urls.py index 5640901c1a..56d05c5e5a 100644 --- a/mayan/apps/smart_settings/urls.py +++ b/mayan/apps/smart_settings/urls.py @@ -6,15 +6,15 @@ from .views import NamespaceDetailView, NamespaceListView, SettingEditView urlpatterns = [ url( - regex=r'^namespace/all/$', view=NamespaceListView.as_view(), + regex=r'^namespaces/$', view=NamespaceListView.as_view(), name='namespace_list' ), url( - regex=r'^namespace/(?P\w+)/$', + regex=r'^namespaces/(?P\w+)/$', view=NamespaceDetailView.as_view(), name='namespace_detail' ), url( - regex=r'^edit/(?P\w+)/$', + regex=r'^namespaces/settings/(?P\w+)/edit/$', view=SettingEditView.as_view(), name='setting_edit_view' ), ] diff --git a/mayan/apps/smart_settings/utils.py b/mayan/apps/smart_settings/utils.py new file mode 100644 index 0000000000..9d9d6dbc91 --- /dev/null +++ b/mayan/apps/smart_settings/utils.py @@ -0,0 +1,77 @@ +from __future__ import unicode_literals + +import errno +import os + +import yaml + +try: + from yaml import CSafeLoader as SafeLoader +except ImportError: + from yaml import SafeLoader + +from .literals import BOOTSTRAP_SETTING_LIST + + +def get_default(name, fallback_default=None): + for item in BOOTSTRAP_SETTING_LIST: + if item['name'] == name: + return item.get('default', fallback_default) + + return fallback_default + + +def get_environment_variables(): + result = {} + + for setting in BOOTSTRAP_SETTING_LIST: + environment_value = os.environ.get('MAYAN_{}'.format(setting['name'])) + if environment_value: + environment_value = yaml.load(stream=environment_value, Loader=SafeLoader) + result[setting['name']] = environment_value + + return result + + +def get_environment_setting(name, fallback_default=None): + value = os.environ.get( + 'MAYAN_{}'.format(name), get_default( + name=name, fallback_default=fallback_default + ) + ) + + if value: + return yaml.load(stream=value, Loader=SafeLoader) + + +def read_configuration_file(path): + try: + with open(path) as file_object: + file_object.seek(0, os.SEEK_END) + if file_object.tell(): + file_object.seek(0) + try: + return yaml.load(stream=file_object, Loader=SafeLoader) + except yaml.YAMLError as exception: + exit( + 'Error loading configuration file: {}; {}'.format( + path, exception + ) + ) + except IOError as exception: + if exception.errno == errno.ENOENT: + return {} # No config file, return empty dictionary + else: + raise + + +def yaml_loads(data, error_message=None): + if not error_message: + error_message = 'Error loading: {}; {}' + + try: + return yaml.load(stream=data, Loader=SafeLoader) + except yaml.YAMLError as exception: + exit( + error_message.format(data, exception) + ) diff --git a/mayan/apps/sources/apps.py b/mayan/apps/sources/apps.py index f74954b481..7139e5a846 100644 --- a/mayan/apps/sources/apps.py +++ b/mayan/apps/sources/apps.py @@ -17,8 +17,8 @@ from mayan.apps.navigation.classes import SourceColumn from .classes import StagingFile from .dependencies import * # NOQA from .handlers import ( - handler_copy_transformations_to_version, handler_create_default_document_source, - handler_initialize_periodic_tasks + handler_copy_transformations_to_version, + handler_create_default_document_source, handler_initialize_periodic_tasks ) from .links import ( link_document_create_multiple, link_setup_sources, diff --git a/mayan/apps/sources/handlers.py b/mayan/apps/sources/handlers.py index 522031e405..a83d841c13 100644 --- a/mayan/apps/sources/handlers.py +++ b/mayan/apps/sources/handlers.py @@ -3,19 +3,16 @@ from __future__ import unicode_literals from django.apps import apps from django.utils.translation import ugettext_lazy as _ +from mayan.apps.converter.layers import layer_saved_transformations + from .literals import SOURCE_UNCOMPRESS_CHOICE_ASK -def handler_copy_transformations_to_version(sender, **kwargs): - Transformation = apps.get_model( - app_label='converter', model_name='Transformation' - ) - - instance = kwargs['instance'] - +def handler_copy_transformations_to_version(sender, instance, **kwargs): # TODO: Fix this, source should be previous version # TODO: Fix this, shouldn't this be at the documents app - Transformation.objects.copy( + + layer_saved_transformations.copy_transformations( source=instance.document, targets=instance.pages.all() ) diff --git a/mayan/apps/sources/models/base.py b/mayan/apps/sources/models/base.py index bcca608256..d7cf22489c 100644 --- a/mayan/apps/sources/models/base.py +++ b/mayan/apps/sources/models/base.py @@ -7,12 +7,12 @@ from django.db import models, transaction from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ -from djcelery.models import PeriodicTask, IntervalSchedule +from django_celery_beat.models import PeriodicTask, IntervalSchedule from model_utils.managers import InheritanceManager from mayan.apps.common.compressed_files import Archive from mayan.apps.common.exceptions import NoMIMETypeMatch -from mayan.apps.converter.models import Transformation +from mayan.apps.converter.layers import layer_saved_transformations from mayan.apps.documents.models import Document, DocumentType from mayan.apps.documents.settings import setting_language @@ -131,7 +131,7 @@ class Source(models.Model): if user: document.add_as_recent_document_for_user(user=user) - Transformation.objects.copy( + layer_saved_transformations.copy_transformations( source=self, targets=document_version.pages.all() ) diff --git a/mayan/apps/sources/models/email_sources.py b/mayan/apps/sources/models/email_sources.py index 4c07219088..fa8ba913f8 100644 --- a/mayan/apps/sources/models/email_sources.py +++ b/mayan/apps/sources/models/email_sources.py @@ -4,18 +4,13 @@ import imaplib import logging import poplib -import yaml -try: - from yaml import CSafeLoader as SafeLoader -except ImportError: - from yaml import SafeLoader - from django.core.exceptions import ValidationError from django.core.files.base import ContentFile from django.db import models from django.utils.encoding import force_bytes from django.utils.translation import ugettext_lazy as _ +from mayan.apps.common.serialization import yaml_load from mayan.apps.documents.models import Document from mayan.apps.metadata.api import set_bulk_metadata from mayan.apps.metadata.models import MetadataType @@ -142,8 +137,8 @@ class EmailBaseModel(IntervalBaseModel): with ContentFile(content=message.body, name=label) as file_object: if label == source.metadata_attachment_name: - metadata_dictionary = yaml.load( - stream=file_object.read(), Loader=SafeLoader + metadata_dictionary = yaml_load( + stream=file_object.read() ) logger.debug( 'Got metadata dictionary: %s', diff --git a/mayan/apps/sources/settings.py b/mayan/apps/sources/settings.py index 8f09ae99b6..aef20ad07d 100644 --- a/mayan/apps/sources/settings.py +++ b/mayan/apps/sources/settings.py @@ -21,13 +21,13 @@ setting_staging_file_image_cache_storage = namespace.add_setting( default='django.core.files.storage.FileSystemStorage', help_text=_( 'Path to the Storage subclass to use when storing the cached ' 'staging_file image files.' - ), quoted=True + ) ) setting_staging_file_image_cache_storage_arguments = namespace.add_setting( global_name='SOURCES_STAGING_FILE_CACHE_STORAGE_BACKEND_ARGUMENTS', - default='{{location: {}}}'.format( - os.path.join(settings.MEDIA_ROOT, 'staging_file_cache') - ), help_text=_( + default={ + 'location': os.path.join(settings.MEDIA_ROOT, 'staging_file_cache') + }, help_text=_( 'Arguments to pass to the SOURCES_STAGING_FILE_CACHE_STORAGE_BACKEND.' - ), quoted=True, + ) ) diff --git a/mayan/apps/sources/storages.py b/mayan/apps/sources/storages.py index 30e23ab7c5..faa863ae21 100644 --- a/mayan/apps/sources/storages.py +++ b/mayan/apps/sources/storages.py @@ -1,23 +1,12 @@ from __future__ import unicode_literals -import yaml -try: - from yaml import CSafeLoader as SafeLoader -except ImportError: - from yaml import SafeLoader - -from django.utils.module_loading import import_string +from mayan.apps.storage.utils import get_storage_subclass from .settings import ( setting_staging_file_image_cache_storage, setting_staging_file_image_cache_storage_arguments, ) -storage_staging_file_image_cache = import_string( +storage_staging_file_image_cache = get_storage_subclass( dotted_path=setting_staging_file_image_cache_storage.value -)( - **yaml.load( - stream=setting_staging_file_image_cache_storage_arguments.value or '{}', - Loader=SafeLoader - ) -) +)(**setting_staging_file_image_cache_storage_arguments.value) diff --git a/mayan/apps/sources/tests/test_classes.py b/mayan/apps/sources/tests/test_classes.py index 319c3eeb37..76f70e1935 100644 --- a/mayan/apps/sources/tests/test_classes.py +++ b/mayan/apps/sources/tests/test_classes.py @@ -3,8 +3,8 @@ from __future__ import unicode_literals import os import shutil -from mayan.apps.common.tests import BaseTestCase -from mayan.apps.documents.tests import TEST_NON_ASCII_DOCUMENT_PATH +from mayan.apps.common.tests.base import BaseTestCase +from mayan.apps.documents.tests.literals import TEST_NON_ASCII_DOCUMENT_PATH from mayan.apps.storage.utils import mkdtemp from ..classes import StagingFile diff --git a/mayan/apps/sources/tests/test_links.py b/mayan/apps/sources/tests/test_links.py index 2466ab7897..7be34c1157 100644 --- a/mayan/apps/sources/tests/test_links.py +++ b/mayan/apps/sources/tests/test_links.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals -from mayan.apps.documents.tests import GenericDocumentViewTestCase from mayan.apps.documents.permissions import permission_document_create +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from ..links import link_document_create_multiple diff --git a/mayan/apps/sources/tests/test_models.py b/mayan/apps/sources/tests/test_models.py index 54ccda5fc2..8d142f1e9b 100644 --- a/mayan/apps/sources/tests/test_models.py +++ b/mayan/apps/sources/tests/test_models.py @@ -6,21 +6,17 @@ import shutil import mock from pathlib2 import Path -import yaml -try: - from yaml import CSafeDumper as SafeDumper -except ImportError: - from yaml import SafeDumper from django.core import mail from django.utils.encoding import force_text +from mayan.apps.common.serialization import yaml_dump from mayan.apps.documents.models import Document -from mayan.apps.documents.tests import ( - GenericDocumentTestCase, TEST_COMPRESSED_DOCUMENT_PATH, - TEST_NON_ASCII_DOCUMENT_FILENAME, TEST_NON_ASCII_DOCUMENT_PATH, - TEST_NON_ASCII_COMPRESSED_DOCUMENT_PATH, TEST_SMALL_DOCUMENT_FILENAME, - TEST_SMALL_DOCUMENT_PATH +from mayan.apps.documents.tests.base import GenericDocumentTestCase +from mayan.apps.documents.tests.literals import ( + TEST_COMPRESSED_DOCUMENT_PATH, TEST_NON_ASCII_DOCUMENT_FILENAME, + TEST_NON_ASCII_DOCUMENT_PATH, TEST_NON_ASCII_COMPRESSED_DOCUMENT_PATH, + TEST_SMALL_DOCUMENT_FILENAME, TEST_SMALL_DOCUMENT_PATH ) from mayan.apps.metadata.models import MetadataType from mayan.apps.storage.utils import mkdtemp @@ -215,8 +211,8 @@ class EmailBaseTestCase(GenericDocumentTestCase): metadata_type=test_metadata_type_2 ) - test_metadata_yaml = yaml.dump( - Dumper=SafeDumper, data={ + test_metadata_yaml = yaml_dump( + data={ test_metadata_type_1.name: TEST_METADATA_VALUE_1, test_metadata_type_2.name: TEST_METADATA_VALUE_2, } diff --git a/mayan/apps/sources/tests/test_views.py b/mayan/apps/sources/tests/test_views.py index 2fd4172a8d..ee64dfdffd 100644 --- a/mayan/apps/sources/tests/test_views.py +++ b/mayan/apps/sources/tests/test_views.py @@ -4,13 +4,13 @@ import os import shutil from mayan.apps.checkouts.models import NewVersionBlock -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from mayan.apps.documents.models import Document from mayan.apps.documents.permissions import permission_document_create -from mayan.apps.documents.tests import ( - GenericDocumentViewTestCase, TEST_COMPRESSED_DOCUMENT_PATH, - TEST_DOCUMENT_DESCRIPTION, TEST_SMALL_DOCUMENT_CHECKSUM, - TEST_SMALL_DOCUMENT_PATH, +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase +from mayan.apps.documents.tests.literals import ( + TEST_COMPRESSED_DOCUMENT_PATH, TEST_DOCUMENT_DESCRIPTION, + TEST_SMALL_DOCUMENT_CHECKSUM, TEST_SMALL_DOCUMENT_PATH ) from mayan.apps.storage.utils import fs_cleanup, mkdtemp @@ -155,11 +155,11 @@ class DocumentUploadIssueTestCase(GenericDocumentViewTestCase): self.assertEqual(WebFormSource.objects.count(), 1) # Upload the test document - with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_descriptor: + with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: self.post( viewname='sources:upload_interactive', data={ 'document-language': 'eng', - 'source-file': file_descriptor, + 'source-file': file_object, 'document_type_id': self.test_document_type.pk } ) @@ -253,7 +253,7 @@ class StagingFolderViewTestCase( self.filename = os.path.basename(TEST_SMALL_DOCUMENT_PATH) def tearDown(self): - fs_cleanup(self.temporary_directory) + fs_cleanup(filename=self.temporary_directory) super(StagingFolderViewTestCase, self).tearDown() def test_staging_file_delete_no_permission(self): diff --git a/mayan/apps/sources/urls.py b/mayan/apps/sources/urls.py index 8796f632df..fdc7adc330 100644 --- a/mayan/apps/sources/urls.py +++ b/mayan/apps/sources/urls.py @@ -15,59 +15,59 @@ from .wizards import DocumentCreateWizard urlpatterns = [ url( - regex=r'^staging_file/(?P\d+)/(?P.+)/delete/$', + regex=r'^staging_files/(?P\d+)/(?P.+)/delete/$', view=StagingFileDeleteView.as_view(), name='staging_file_delete' ), + + # Document create views + url( - regex=r'^upload/document/new/interactive/(?P\d+)/$', + regex=r'^documents/create/from/local/multiple/$', + view=DocumentCreateWizard.as_view(), name='document_create_multiple' + ), + url( + regex=r'^documents/upload/new/interactive/(?P\d+)/$', view=UploadInteractiveView.as_view(), name='upload_interactive' ), url( - regex=r'^upload/document/new/interactive/$', + regex=r'^documents/upload/new/interactive/$', view=UploadInteractiveView.as_view(), name='upload_interactive' ), url( - regex=r'^upload/document/(?P\d+)/version/interactive/(?P\d+)/$', + regex=r'^documents/(?P\d+)/versions/upload/interactive/(?P\d+)/$', view=UploadInteractiveVersionView.as_view(), name='upload_version' ), url( - regex=r'^upload/document/(?P\d+)/version/interactive/$', + regex=r'^documents/(?P\d+)/versions/upload/interactive/$', view=UploadInteractiveVersionView.as_view(), name='upload_version' ), # Setup views url( - regex=r'^setup/list/$', view=SetupSourceListView.as_view(), + regex=r'^sources/$', view=SetupSourceListView.as_view(), name='setup_source_list' ), url( - regex=r'^setup/(?P\d+)/edit/$', view=SetupSourceEditView.as_view(), + regex=r'^sources/(?P\d+)/edit/$', view=SetupSourceEditView.as_view(), name='setup_source_edit' ), url( - regex=r'^setup/(?P\d+)/logs/$', view=SourceLogListView.as_view(), + regex=r'^sources/(?P\d+)/logs/$', view=SourceLogListView.as_view(), name='setup_source_logs' ), url( - regex=r'^setup/(?P\d+)/delete/$', + regex=r'^sources/(?P\d+)/delete/$', view=SetupSourceDeleteView.as_view(), name='setup_source_delete' ), url( - regex=r'^setup/(?P\w+)/create/$', + regex=r'^sources/(?P\w+)/create/$', view=SetupSourceCreateView.as_view(), name='setup_source_create' ), url( - regex=r'^setup/(?P\d+)/check/$', + regex=r'^sources/(?P\d+)/check/$', view=SetupSourceCheckView.as_view(), name='setup_source_check' ), - - # Document create views - - url( - regex=r'^create/from/local/multiple/$', - view=DocumentCreateWizard.as_view(), name='document_create_multiple' - ), ] api_urls = [ diff --git a/mayan/apps/storage/classes.py b/mayan/apps/storage/classes.py new file mode 100644 index 0000000000..0c4bdd2cd5 --- /dev/null +++ b/mayan/apps/storage/classes.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals + + +class FakeStorageSubclass(object): + """ + Placeholder class to allow serializing the real storage subclass to + support migrations. + """ + def __eq__(self, other): + return True diff --git a/mayan/apps/storage/utils.py b/mayan/apps/storage/utils.py index 90eab5bae5..1475727a6e 100644 --- a/mayan/apps/storage/utils.py +++ b/mayan/apps/storage/utils.py @@ -5,6 +5,8 @@ import os import shutil import tempfile +from django.utils.module_loading import import_string + from .settings import setting_temporary_directory logger = logging.getLogger(__name__) @@ -20,13 +22,10 @@ def NamedTemporaryFile(*args, **kwargs): return tempfile.NamedTemporaryFile(*args, **kwargs) -def fs_cleanup(filename, file_descriptor=None, suppress_exceptions=True): +def fs_cleanup(filename, suppress_exceptions=True): """ - Tries to remove the given filename. Ignores non-existent files + Tries to remove the given filename. Ignores non-existent files. """ - if file_descriptor: - os.close(file_descriptor) - try: os.remove(filename) except OSError: @@ -39,7 +38,34 @@ def fs_cleanup(filename, file_descriptor=None, suppress_exceptions=True): raise +def get_storage_subclass(dotted_path): + """ + Import a storage class and return a subclass that will always return eq + True to avoid creating a new migration when for runtime storage class + changes. + """ + imported_storage_class = import_string(dotted_path=dotted_path) + + class StorageSubclass(imported_storage_class): + def __init__(self, *args, **kwargs): + return super(StorageSubclass, self).__init__(*args, **kwargs) + + def __eq__(self, other): + return True + + def deconstruct(self): + return ('mayan.apps.storage.classes.FakeStorageSubclass', (), {}) + + return StorageSubclass + + def mkdtemp(*args, **kwargs): + """ + Creates a temporary directory in the most secure manner possible. + There are no race conditions in the directory's creation. + The directory is readable, writable, and searchable only by the creating + user ID. + """ kwargs.update({'dir': setting_temporary_directory.value}) return tempfile.mkdtemp(*args, **kwargs) diff --git a/mayan/apps/tags/tests/test_api.py b/mayan/apps/tags/tests/test_api.py index cb7a1ca4dd..efc133f4e8 100644 --- a/mayan/apps/tags/tests/test_api.py +++ b/mayan/apps/tags/tests/test_api.py @@ -5,8 +5,8 @@ from django.utils.encoding import force_text from rest_framework import status from mayan.apps.documents.permissions import permission_document_view -from mayan.apps.documents.tests import DocumentTestMixin -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.documents.tests.mixins import DocumentTestMixin +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..models import Tag from ..permissions import ( diff --git a/mayan/apps/tags/tests/test_events.py b/mayan/apps/tags/tests/test_events.py index 82e8dc1327..ecd26c7bf9 100644 --- a/mayan/apps/tags/tests/test_events.py +++ b/mayan/apps/tags/tests/test_events.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from actstream.models import Action -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from ..events import event_tag_created, event_tag_edited from ..models import Tag diff --git a/mayan/apps/tags/tests/test_indexing.py b/mayan/apps/tags/tests/test_indexing.py index 897c1ffff0..9bb205e059 100644 --- a/mayan/apps/tags/tests/test_indexing.py +++ b/mayan/apps/tags/tests/test_indexing.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import BaseTestCase -from mayan.apps.documents.tests import DocumentTestMixin +from mayan.apps.common.tests.base import BaseTestCase +from mayan.apps.documents.tests.mixins import DocumentTestMixin from mayan.apps.document_indexing.models import Index, IndexInstanceNode from mayan.apps.document_indexing.tests.literals import TEST_INDEX_LABEL diff --git a/mayan/apps/tags/tests/test_models.py b/mayan/apps/tags/tests/test_models.py index c0df95db9c..3e3a7f13c5 100644 --- a/mayan/apps/tags/tests/test_models.py +++ b/mayan/apps/tags/tests/test_models.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import BaseTestCase -from mayan.apps.documents.tests import DocumentTestMixin +from mayan.apps.common.tests.base import BaseTestCase +from mayan.apps.documents.tests.mixins import DocumentTestMixin from .mixins import TagTestMixin diff --git a/mayan/apps/tags/tests/test_views.py b/mayan/apps/tags/tests/test_views.py index 23189d7635..3ef2bf3a22 100644 --- a/mayan/apps/tags/tests/test_views.py +++ b/mayan/apps/tags/tests/test_views.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals from django.utils.encoding import force_text -from mayan.apps.common.tests import GenericViewTestCase -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from ..models import Tag from ..permissions import ( diff --git a/mayan/apps/tags/tests/test_widgets.py b/mayan/apps/tags/tests/test_widgets.py index 8f6fcc18c3..659785e452 100644 --- a/mayan/apps/tags/tests/test_widgets.py +++ b/mayan/apps/tags/tests/test_widgets.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.utils.encoding import force_text -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from mayan.apps.documents.tests.mixins import DocumentViewTestMixin from mayan.apps.documents.permissions import permission_document_view diff --git a/mayan/apps/tags/tests/test_wizard_steps.py b/mayan/apps/tags/tests/test_wizard_steps.py index a241b9725d..46824a83b1 100644 --- a/mayan/apps/tags/tests/test_wizard_steps.py +++ b/mayan/apps/tags/tests/test_wizard_steps.py @@ -2,9 +2,8 @@ from __future__ import unicode_literals from mayan.apps.documents.models import Document from mayan.apps.documents.permissions import permission_document_create -from mayan.apps.documents.tests import ( - GenericDocumentViewTestCase, TEST_SMALL_DOCUMENT_PATH, -) +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase +from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH from mayan.apps.sources.models import WebFormSource from mayan.apps.sources.tests.literals import ( TEST_SOURCE_LABEL, TEST_SOURCE_UNCOMPRESS_N diff --git a/mayan/apps/tags/tests/test_workflow_actions.py b/mayan/apps/tags/tests/test_workflow_actions.py index bceebdfb58..bb52f2eb62 100644 --- a/mayan/apps/tags/tests/test_workflow_actions.py +++ b/mayan/apps/tags/tests/test_workflow_actions.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from mayan.apps.document_states.tests.mixins import WorkflowTestMixin from mayan.apps.document_states.tests.test_workflow_actions import ActionTestCase @@ -37,7 +37,7 @@ class TagActionViewTestCase(WorkflowTestMixin, GenericViewTestCase): self._create_test_workflow_state() response = self.get( - viewname='document_states:setup_workflow_state_action_create', + viewname='document_states:workflow_template_state_action_create', kwargs={ 'pk': self.test_workflow_state.pk, 'class_path': 'mayan.apps.tags.workflow_actions.AttachTagAction' @@ -51,7 +51,7 @@ class TagActionViewTestCase(WorkflowTestMixin, GenericViewTestCase): self._create_test_workflow_state() response = self.get( - viewname='document_states:setup_workflow_state_action_create', + viewname='document_states:workflow_template_state_action_create', kwargs={ 'pk': self.test_workflow_state.pk, 'class_path': 'mayan.apps.tags.workflow_actions.RemoveTagAction' diff --git a/mayan/apps/tags/urls.py b/mayan/apps/tags/urls.py index 61fb5d1e3c..51ef7c340c 100644 --- a/mayan/apps/tags/urls.py +++ b/mayan/apps/tags/urls.py @@ -13,44 +13,44 @@ from .views import ( ) urlpatterns = [ - url(regex=r'^list/$', view=TagListView.as_view(), name='tag_list'), - url(regex=r'^create/$', view=TagCreateView.as_view(), name='tag_create'), + url(regex=r'^tags/$', view=TagListView.as_view(), name='tag_list'), + url(regex=r'^tags/create/$', view=TagCreateView.as_view(), name='tag_create'), url( - regex=r'^(?P\d+)/delete/$', view=TagDeleteActionView.as_view(), + regex=r'^tags/(?P\d+)/delete/$', view=TagDeleteActionView.as_view(), name='tag_delete' ), url( - regex=r'^(?P\d+)/edit/$', view=TagEditView.as_view(), + regex=r'^tags/(?P\d+)/edit/$', view=TagEditView.as_view(), name='tag_edit' ), url( - regex=r'^(?P\d+)/documents/$', view=TagDocumentListView.as_view(), + regex=r'^tags/(?P\d+)/documents/$', view=TagDocumentListView.as_view(), name='tag_document_list' ), url( - regex=r'^multiple/delete/$', view=TagDeleteActionView.as_view(), + regex=r'^tags/multiple/delete/$', view=TagDeleteActionView.as_view(), name='tag_multiple_delete' ), url( - regex=r'^multiple/remove/document/(?P\d+)/$', + regex=r'^documents/(?P\d+)/tags/remove/$', view=TagRemoveActionView.as_view(), name='single_document_multiple_tag_remove' ), url( - regex=r'^multiple/remove/document/multiple/$', + regex=r'^documents/multiple/tags/remove/$', view=TagRemoveActionView.as_view(), name='multiple_documents_selection_tag_remove' ), url( - regex=r'^selection/attach/document/(?P\d+)/$', + regex=r'^documents/(?P\d+)/tags/attach/$', view=TagAttachActionView.as_view(), name='tag_attach' ), url( - regex=r'^selection/attach/document/multiple/$', + regex=r'^documents/multiple/tags/attach/$', view=TagAttachActionView.as_view(), name='multiple_documents_tag_attach' ), url( - regex=r'^document/(?P\d+)/tags/$', + regex=r'^documents/(?P\d+)/tags/$', view=DocumentTagListView.as_view(), name='document_tag_list' ), ] diff --git a/mayan/apps/task_manager/apps.py b/mayan/apps/task_manager/apps.py index 00996c6528..51044d3c52 100644 --- a/mayan/apps/task_manager/apps.py +++ b/mayan/apps/task_manager/apps.py @@ -4,21 +4,17 @@ 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 +from mayan.apps.common.menus import menu_tools from mayan.apps.navigation.classes import SourceColumn from .classes import CeleryQueue, Task -from .links import ( - link_queue_list, link_queue_active_task_list, - link_queue_scheduled_task_list, link_queue_reserved_task_list, - link_task_manager -) +from .links import link_task_manager +from .settings import * # NOQA class TaskManagerApp(MayanAppConfig): app_namespace = 'task_manager' app_url = 'task_manager' - has_tests = True name = 'mayan.apps.task_manager' verbose_name = _('Task manager') @@ -64,16 +60,4 @@ class TaskManagerApp(MayanAppConfig): func=lambda context: context['object'].kwargs['worker_pid'] ) - menu_object.bind_links( - links=( - link_queue_active_task_list, link_queue_scheduled_task_list, - link_queue_reserved_task_list, - ), sources=(CeleryQueue,) - ) - - menu_secondary.bind_links( - links=(link_queue_list,), - sources=(CeleryQueue, Task, 'task_manager:queue_list') - ) - menu_tools.bind_links(links=(link_task_manager,)) diff --git a/mayan/apps/task_manager/classes.py b/mayan/apps/task_manager/classes.py index 90770f9152..73c9d4632e 100644 --- a/mayan/apps/task_manager/classes.py +++ b/mayan/apps/task_manager/classes.py @@ -1,18 +1,13 @@ from __future__ import absolute_import, unicode_literals -from datetime import timedelta from importlib import import_module import logging from kombu import Exchange, Queue -from celery.five import monotonic -from celery.task.control import inspect - from django.apps import apps from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.module_loading import import_string -from django.utils.timezone import now from mayan.celery import app as celery_app @@ -62,18 +57,10 @@ class Task(object): def __str__(self): return force_text(self.task_type) - def get_time_started(self): - time_start = self.kwargs.get('time_start') - if time_start: - return now() - timedelta(seconds=monotonic() - self.kwargs['time_start']) - else: - return None - @python_2_unicode_compatible class CeleryQueue(object): _registry = {} - _inspect_instance = inspect() @staticmethod def initialize(): @@ -132,21 +119,6 @@ class CeleryQueue(object): self.task_types.append(task_type) return task_type - def get_active_tasks(self): - return self._process_task_dictionary( - task_dictionary=self.__class__._inspect_instance.active() - ) - - def get_reserved_tasks(self): - return self._process_task_dictionary( - task_dictionary=self.__class__._inspect_instance.reserved() - ) - - def get_scheduled_tasks(self): - return self._process_task_dictionary( - task_dictionary=self.__class__._inspect_instance.scheduled() - ) - def _update_celery(self): kwargs = { 'name': self.name, 'exchange': Exchange(self.name), @@ -156,13 +128,13 @@ class CeleryQueue(object): if self.transient: kwargs['delivery_mode'] = 1 - celery_app.conf.CELERY_QUEUES.append(Queue(**kwargs)) + celery_app.conf.task_queues.append(Queue(**kwargs)) if self.default_queue: - celery_app.conf.CELERY_DEFAULT_QUEUE = self.name + celery_app.conf.task_default_queue = self.name for task_type in self.task_types: - celery_app.conf.CELERY_ROUTES.update( + celery_app.conf.task_routes.update( { task_type.dotted_path: { 'queue': self.name @@ -171,7 +143,7 @@ class CeleryQueue(object): ) if task_type.schedule: - celery_app.conf.CELERYBEAT_SCHEDULE.update( + celery_app.conf.beat_schedule.update( { task_type.name: { 'task': task_type.dotted_path, @@ -188,6 +160,10 @@ class Worker(object): def all(cls): return cls._registry.values() + @classmethod + def get(cls, name): + return cls._registry[name] + def __init__(self, name, label=None, nice_level=0): self.name = name self.label = label diff --git a/mayan/apps/task_manager/dependencies.py b/mayan/apps/task_manager/dependencies.py new file mode 100644 index 0000000000..ea0921f3fa --- /dev/null +++ b/mayan/apps/task_manager/dependencies.py @@ -0,0 +1,120 @@ +from __future__ import absolute_import, unicode_literals + +from mayan.apps.dependencies.classes import PythonDependency + +PythonDependency( + copyright_text=''' + Copyright (c) 2015 Ask Solem & contributors. All rights reserved. + Copyright (c) 2012-2014 GoPivotal, Inc. All rights reserved. + Copyright (c) 2009, 2010, 2011, 2012 Ask Solem, and individual contributors. All rights reserved. + + Celery is licensed under The BSD License (3 Clause, also known as + the new BSD license). The license is an OSI approved Open Source + license and is GPL-compatible(1). + + The license text can also be found here: + http://www.opensource.org/licenses/BSD-3-Clause + + License + ======= + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Ask Solem, nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Ask Solem OR CONTRIBUTORS + BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + Documentation License + ===================== + + The documentation portion of Celery (the rendered contents of the + "docs" directory of a software distribution or checkout) is supplied + under the Creative Commons Attribution-Noncommercial-Share Alike 3.0 + United States License as described by + http://creativecommons.org/licenses/by-nc-sa/3.0/us/ + + Footnotes + ========= + (1) A GPL-compatible license makes it possible to + combine Celery with other software that is released + under the GPL, it does not mean that we're distributing + Celery under the GPL license. The BSD license, unlike the GPL, + let you distribute a modified version without making your + changes open source. + ''', module=__name__, name='celery', version_string='==4.3.0' +) +PythonDependency( + copyright_text=''' + Copyright (c) 2015-2016 Ask Solem. All Rights Reserved. + Copyright (c) 2012-2014 GoPivotal, Inc. All Rights Reserved. + Copyright (c) 2009-2012 Ask Solem. All Rights Reserved. + + django-celery-beat is licensed under The BSD License (3 Clause, also known as + the new BSD license). The license is an OSI approved Open Source + license and is GPL-compatible(1). + + The license text can also be found here: + http://www.opensource.org/licenses/BSD-3-Clause + + License + ======= + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Ask Solem nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Ask Solem OR CONTRIBUTORS + BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + Documentation License + ===================== + + The documentation portion of django-celery-beat (the rendered contents of the + "docs" directory of a software distribution or checkout) is supplied + under the "Creative Commons Attribution-ShareAlike 4.0 + International" (CC BY-SA 4.0) License as described by + http://creativecommons.org/licenses/by-sa/4.0/ + + Footnotes + ========= + (1) A GPL-compatible license makes it possible to + combine django-celery-beat with other software that is released + under the GPL, it does not mean that we're distributing + django-celery-beat under the GPL license. The BSD license, unlike the GPL, + let you distribute a modified version without making your + changes open source. + ''', module=__name__, name='django-celery-beat', version_string='==1.5.0' +) diff --git a/mayan/apps/task_manager/icons.py b/mayan/apps/task_manager/icons.py index 39f1a46bb6..336feb7f67 100644 --- a/mayan/apps/task_manager/icons.py +++ b/mayan/apps/task_manager/icons.py @@ -3,4 +3,3 @@ from __future__ import absolute_import, unicode_literals from mayan.apps.appearance.classes import Icon icon_task_manager = Icon(driver_name='fontawesome', symbol='braille') -icon_queue_list = Icon(driver_name='fontawesome', symbol='braille') diff --git a/mayan/apps/task_manager/links.py b/mayan/apps/task_manager/links.py index 767008ab59..131a0bf9a1 100644 --- a/mayan/apps/task_manager/links.py +++ b/mayan/apps/task_manager/links.py @@ -4,26 +4,10 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.navigation.classes import Link -from .icons import icon_task_manager, icon_queue_list +from .icons import icon_task_manager from .permissions import permission_task_view link_task_manager = Link( icon_class=icon_task_manager, permissions=(permission_task_view,), text=_('Task manager'), view='task_manager:queue_list' ) -link_queue_list = Link( - icon_class=icon_queue_list, permissions=(permission_task_view,), - text=_('Background task queues'), view='task_manager:queue_list' -) -link_queue_active_task_list = Link( - args='resolved_object.name', permissions=(permission_task_view,), - text=_('Active tasks'), view='task_manager:queue_active_task_list' -) -link_queue_reserved_task_list = Link( - args='resolved_object.name', permissions=(permission_task_view,), - text=_('Reserved tasks'), view='task_manager:queue_reserved_task_list' -) -link_queue_scheduled_task_list = Link( - args='resolved_object.name', permissions=(permission_task_view,), - text=_('Scheduled tasks'), view='task_manager:queue_scheduled_task_list' -) diff --git a/mayan/apps/task_manager/settings.py b/mayan/apps/task_manager/settings.py new file mode 100644 index 0000000000..bcf273262f --- /dev/null +++ b/mayan/apps/task_manager/settings.py @@ -0,0 +1,29 @@ +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.smart_settings.classes import Namespace + +# Don't import anything on start import, we just want to make it easy +# for apps.py to activate the settings in this module. +__all__ = () +namespace = Namespace(label=_('Celery'), name='celery') + +setting_celery_broker_url = namespace.add_setting( + global_name='CELERY_BROKER_URL', default=None, + 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=None, + 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' + ) +) diff --git a/mayan/apps/task_manager/tests/test_views.py b/mayan/apps/task_manager/tests/test_views.py index c672c48dd6..a9459da180 100644 --- a/mayan/apps/task_manager/tests/test_views.py +++ b/mayan/apps/task_manager/tests/test_views.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase from ..classes import Worker, CeleryQueue from ..permissions import permission_task_view @@ -22,29 +22,11 @@ class TaskManagerViewTestCase(TaskManagerTestMixin, GenericViewTestCase): super(TaskManagerViewTestCase, self).setUp() self._create_test_queue() - def _request_active_task_list(self): - return self.get( - viewname='task_manager:queue_active_task_list', - kwargs={'queue_name': self.test_queue.name}, follow=True - ) - def _request_queue_list(self): return self.get( viewname='task_manager:queue_list', follow=True ) - def _request_reserved_task_list(self): - return self.get( - viewname='task_manager:queue_reserved_task_list', - kwargs={'queue_name': self.test_queue.name}, follow=True - ) - - def _request_scheduled_task_list(self): - return self.get( - viewname='task_manager:queue_scheduled_task_list', - kwargs={'queue_name': self.test_queue.name}, follow=True - ) - def test_queue_list_view_no_permissions(self): response = self._request_queue_list() @@ -57,33 +39,3 @@ class TaskManagerViewTestCase(TaskManagerTestMixin, GenericViewTestCase): self.assertContains( response, text=self.test_queue.name, status_code=200 ) - - def test_active_task_list_view_no_permissions(self): - response = self._request_active_task_list() - self.assertEqual(response.status_code, 403) - - def test_active_task_list_view_with_permissions(self): - self.grant_permission(permission=permission_task_view) - - response = self._request_active_task_list() - self.assertEqual(response.status_code, 200) - - def test_reserved_task_list_view_no_permissions(self): - response = self._request_reserved_task_list() - self.assertEqual(response.status_code, 403) - - def test_reserved_task_list_view_with_permissions(self): - self.grant_permission(permission=permission_task_view) - - response = self._request_reserved_task_list() - self.assertEqual(response.status_code, 200) - - def test_scheduled_task_list_view_no_permissions(self): - response = self._request_scheduled_task_list() - self.assertEqual(response.status_code, 403) - - def test_scheduled_task_list_view_with_permissions(self): - self.grant_permission(permission=permission_task_view) - - response = self._request_scheduled_task_list() - self.assertEqual(response.status_code, 200) diff --git a/mayan/apps/task_manager/urls.py b/mayan/apps/task_manager/urls.py index de48bf12ff..622a46fea4 100644 --- a/mayan/apps/task_manager/urls.py +++ b/mayan/apps/task_manager/urls.py @@ -2,29 +2,11 @@ from __future__ import unicode_literals from django.conf.urls import url -from .views import ( - QueueListView, QueueActiveTaskListView, QueueScheduledTaskListView, - QueueReservedTaskListView -) - +from .views import QueueListView urlpatterns = [ url( regex=r'^queues/$', view=QueueListView.as_view(), name='queue_list' ), - url( - regex=r'^queues/(?P[-\w]+)/tasks/active/$', - view=QueueActiveTaskListView.as_view(), name='queue_active_task_list' - ), - url( - regex=r'^queues/(?P[-\w]+)/tasks/scheduled/$', - view=QueueScheduledTaskListView.as_view(), - name='queue_scheduled_task_list' - ), - url( - regex=r'^queues/(?P[-\w]+)/tasks/reserved/$', - view=QueueReservedTaskListView.as_view(), - name='queue_reserved_task_list' - ), ] diff --git a/mayan/apps/task_manager/views.py b/mayan/apps/task_manager/views.py index ab229a0c87..fff85054d5 100644 --- a/mayan/apps/task_manager/views.py +++ b/mayan/apps/task_manager/views.py @@ -1,6 +1,5 @@ from __future__ import absolute_import, unicode_literals -from django.contrib import messages from django.utils.translation import ugettext_lazy as _ from mayan.apps.common.generics import SingleObjectListView @@ -18,55 +17,3 @@ class QueueListView(SingleObjectListView): def get_source_queryset(self): return CeleryQueue.all() - - -class QueueActiveTaskListView(SingleObjectListView): - view_permission = permission_task_view - - def get_extra_context(self): - return { - 'hide_object': True, - 'object': self.get_object(), - 'title': _('Active tasks in queue: %s') % self.get_object() - } - - def get_object(self): - return CeleryQueue.get(queue_name=self.kwargs['queue_name']) - - def get_source_queryset(self): - try: - return self.get_task_list() - except Exception as exception: - messages.error( - message=_( - 'Unable to retrieve task list; %s' - ) % exception, request=self.request - ) - return () - - def get_task_list(self): - return self.get_object().get_active_tasks() - - -class QueueScheduledTaskListView(QueueActiveTaskListView): - def get_extra_context(self): - return { - 'hide_object': True, - 'object': self.get_object(), - 'title': _('Scheduled tasks in queue: %s') % self.get_object() - } - - def get_task_list(self): - return self.get_object().get_scheduled_tasks() - - -class QueueReservedTaskListView(QueueActiveTaskListView): - def get_extra_context(self): - return { - 'hide_object': True, - 'object': self.get_object(), - 'title': _('Reserved tasks in queue: %s') % self.get_object() - } - - def get_task_list(self): - return self.get_object().get_reserved_tasks() diff --git a/mayan/apps/user_management/apps.py b/mayan/apps/user_management/apps.py index 22ac437887..28ee9e1615 100644 --- a/mayan/apps/user_management/apps.py +++ b/mayan/apps/user_management/apps.py @@ -56,7 +56,7 @@ from .permissions import ( permission_group_view, permission_user_delete, permission_user_edit, permission_user_view ) -from .search import * # NOQA +from .search import group_search, user_search # NOQA def get_groups(): diff --git a/mayan/apps/user_management/search.py b/mayan/apps/user_management/search.py index 03e5494aec..90591d7824 100644 --- a/mayan/apps/user_management/search.py +++ b/mayan/apps/user_management/search.py @@ -8,6 +8,16 @@ from mayan.apps.dynamic_search.classes import SearchModel from .permissions import permission_group_view, permission_user_view from .querysets import get_user_queryset +group_search = SearchModel( + app_label='auth', label=_('Group'), model_name='Group', + permission=permission_group_view, + serializer_path='mayan.apps.user_management.serializers.GroupSerializer' +) + +group_search.add_model_field( + field='name', label=_('Name') +) + user_app, user_model = settings.AUTH_USER_MODEL.split('.') user_search = SearchModel( @@ -31,13 +41,3 @@ user_search.add_model_field( user_search.add_model_field( field='username', label=_('username') ) - -group_search = SearchModel( - app_label='auth', label=_('Group'), model_name='Group', - permission=permission_group_view, - serializer_path='mayan.apps.user_management.serializers.GroupSerializer' -) - -group_search.add_model_field( - field='name', label=_('Name') -) diff --git a/mayan/apps/user_management/tests/mixins.py b/mayan/apps/user_management/tests/mixins.py index 548df197e8..a37792fd55 100644 --- a/mayan/apps/user_management/tests/mixins.py +++ b/mayan/apps/user_management/tests/mixins.py @@ -249,11 +249,19 @@ class UserTestMixin(object): self.test_superuser.cleartext_password = TEST_USER_PASSWORD def _create_test_user(self): + total_test_users = len(self.test_users) + username = '{}_{}'.format(TEST_USER_USERNAME, total_test_users) + self.test_user = get_user_model().objects.create_user( - username=TEST_USER_USERNAME, email=TEST_USER_EMAIL, + username=username, email=TEST_USER_EMAIL, password=TEST_USER_PASSWORD ) self.test_user.cleartext_password = TEST_USER_PASSWORD + self.test_users.append(self.test_user) + + def setUp(self): + super(UserTestMixin, self).setUp() + self.test_users = [] class UserViewTestMixin(object): diff --git a/mayan/apps/user_management/tests/test_api.py b/mayan/apps/user_management/tests/test_api.py index 7ae2d4a947..33e24cc0f0 100644 --- a/mayan/apps/user_management/tests/test_api.py +++ b/mayan/apps/user_management/tests/test_api.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import Group from rest_framework import status -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..permissions import ( permission_group_create, permission_group_delete, diff --git a/mayan/apps/user_management/tests/test_events.py b/mayan/apps/user_management/tests/test_events.py index 245c407eea..cef117c1f1 100644 --- a/mayan/apps/user_management/tests/test_events.py +++ b/mayan/apps/user_management/tests/test_events.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals from actstream.models import Action -from mayan.apps.common.tests import GenericViewTestCase -from mayan.apps.rest_api.tests import BaseAPITestCase +from mayan.apps.common.tests.base import GenericViewTestCase +from mayan.apps.rest_api.tests.base import BaseAPITestCase from ..permissions import ( permission_group_create, permission_group_edit, permission_user_create, diff --git a/mayan/apps/user_management/tests/test_views.py b/mayan/apps/user_management/tests/test_views.py index 15a4a12c12..6d989393c3 100644 --- a/mayan/apps/user_management/tests/test_views.py +++ b/mayan/apps/user_management/tests/test_views.py @@ -3,8 +3,8 @@ from __future__ import unicode_literals from django.contrib.auth import get_user_model from django.contrib.auth.models import Group -from mayan.apps.common.tests import GenericViewTestCase -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from mayan.apps.common.tests.base import GenericViewTestCase +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from mayan.apps.metadata.permissions import permission_document_metadata_edit from mayan.apps.metadata.tests.mixins import MetadataTypeTestMixin diff --git a/mayan/apps/user_management/urls.py b/mayan/apps/user_management/urls.py index aa22e65206..9f8a36047b 100644 --- a/mayan/apps/user_management/urls.py +++ b/mayan/apps/user_management/urls.py @@ -26,22 +26,22 @@ urlpatterns_current_user = [ urlpatterns_groups = [ url( - regex=r'^group/list/$', view=GroupListView.as_view(), name='group_list' + regex=r'^groups/$', view=GroupListView.as_view(), name='group_list' ), url( - regex=r'^group/create/$', view=GroupCreateView.as_view(), + regex=r'^groups/create/$', view=GroupCreateView.as_view(), name='group_create' ), url( - regex=r'^group/(?P\d+)/edit/$', view=GroupEditView.as_view(), - name='group_edit' - ), - url( - regex=r'^group/(?P\d+)/delete/$', view=GroupDeleteView.as_view(), + regex=r'^groups/(?P\d+)/delete/$', view=GroupDeleteView.as_view(), name='group_delete' ), url( - regex=r'^group/(?P\d+)/users/$', view=GroupUsersView.as_view(), + regex=r'^groups/(?P\d+)/edit/$', view=GroupEditView.as_view(), + name='group_edit' + ), + url( + regex=r'^groups/(?P\d+)/users/$', view=GroupUsersView.as_view(), name='group_members' ) ] diff --git a/mayan/apps/web_links/__init__.py b/mayan/apps/web_links/__init__.py new file mode 100644 index 0000000000..df30eba251 --- /dev/null +++ b/mayan/apps/web_links/__init__.py @@ -0,0 +1,3 @@ +from __future__ import unicode_literals + +default_app_config = 'mayan.apps.web_links.apps.WebLinksApp' diff --git a/mayan/apps/web_links/admin.py b/mayan/apps/web_links/admin.py new file mode 100644 index 0000000000..c29ac45e52 --- /dev/null +++ b/mayan/apps/web_links/admin.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals + +from django.contrib import admin +from .models import WebLink + + +@admin.register(WebLink) +class WebLinkAdmin(admin.ModelAdmin): + def document_type_list(self, instance): + return ','.join( + instance.document_types.values_list('label', flat=True) + ) + + filter_horizontal = ('document_types',) + list_display = ('label', 'template', 'enabled', 'document_type_list') diff --git a/mayan/apps/web_links/apps.py b/mayan/apps/web_links/apps.py new file mode 100644 index 0000000000..b8413a26a2 --- /dev/null +++ b/mayan/apps/web_links/apps.py @@ -0,0 +1,125 @@ +from __future__ import unicode_literals + +from django.apps import apps +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.acls.classes import ModelPermission +from mayan.apps.acls.links import link_acl_list +from mayan.apps.acls.permissions import permission_acl_edit, permission_acl_view +from mayan.apps.common.apps import MayanAppConfig +from mayan.apps.common.html_widgets import TwoStateWidget +from mayan.apps.common.menus import ( + menu_facet, menu_list_facet, menu_object, menu_secondary, menu_setup +) +from mayan.apps.events.classes import ModelEventType +from mayan.apps.events.links import ( + link_events_for_object, link_object_event_types_user_subcriptions_list +) +from mayan.apps.navigation.classes import SourceColumn + +from .events import event_web_link_edited +from .links import ( + link_document_type_web_links, link_document_web_link_list, + link_web_link_create, link_web_link_delete, link_web_link_document_types, + link_web_link_edit, link_web_link_instance_view, + link_web_link_list, link_web_link_setup +) +from .permissions import ( + permission_web_link_delete, permission_web_link_edit, + permission_web_link_instance_view, permission_web_link_view +) + + +class WebLinksApp(MayanAppConfig): + app_namespace = 'web_links' + app_url = 'web_links' + has_rest_api = False + has_tests = True + name = 'mayan.apps.web_links' + verbose_name = _('Links') + + def ready(self): + super(WebLinksApp, self).ready() + from actstream import registry + + Document = apps.get_model( + app_label='documents', model_name='Document' + ) + DocumentType = apps.get_model( + app_label='documents', model_name='DocumentType' + ) + + ResolvedWebLink = self.get_model(model_name='ResolvedWebLink') + WebLink = self.get_model(model_name='WebLink') + + ModelEventType.register( + event_types=( + event_web_link_edited, + ), model=WebLink + ) + + ModelPermission.register( + model=Document, permissions=( + permission_web_link_instance_view, + ) + ) + ModelPermission.register( + model=DocumentType, permissions=( + permission_web_link_instance_view, + ) + ) + ModelPermission.register( + model=WebLink, permissions=( + permission_acl_edit, permission_acl_view, + permission_web_link_delete, permission_web_link_edit, + permission_web_link_instance_view, permission_web_link_view + ) + ) + + SourceColumn( + attribute='label', is_identifier=True, is_sortable=True, + source=ResolvedWebLink + ) + SourceColumn( + attribute='label', is_identifier=True, is_sortable=True, + source=WebLink + ) + SourceColumn( + attribute='enabled', is_sortable=True, source=WebLink, + widget=TwoStateWidget + ) + + menu_facet.bind_links( + links=(link_document_web_link_list,), + sources=(Document,) + ) + menu_list_facet.bind_links( + links=( + link_acl_list, link_events_for_object, + link_web_link_document_types, + link_object_event_types_user_subcriptions_list, + ), sources=(WebLink,) + ) + menu_list_facet.bind_links( + links=(link_document_type_web_links,), sources=(DocumentType,) + ) + menu_object.bind_links( + links=( + link_web_link_delete, link_web_link_edit + ), sources=(WebLink,) + ) + menu_object.bind_links( + links=(link_web_link_instance_view,), + sources=(ResolvedWebLink,) + ) + menu_secondary.bind_links( + links=(link_web_link_list, link_web_link_create), + sources=( + WebLink, 'web_links:web_link_list', + 'web_links:web_link_create' + ) + ) + menu_setup.bind_links(links=(link_web_link_setup,)) + + registry.register(ResolvedWebLink) + registry.register(WebLink) diff --git a/mayan/apps/web_links/events.py b/mayan/apps/web_links/events.py new file mode 100644 index 0000000000..ab03e62e31 --- /dev/null +++ b/mayan/apps/web_links/events.py @@ -0,0 +1,19 @@ +from __future__ import absolute_import, unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.events.classes import EventTypeNamespace + +namespace = EventTypeNamespace( + label=_('Web links'), name='linking' +) + +event_web_link_created = namespace.add_event_type( + label=_('Web link created'), name='web_link_created' +) +event_web_link_edited = namespace.add_event_type( + label=_('Web link edited'), name='web_link_edited' +) +event_web_link_navigated = namespace.add_event_type( + label=_('Web link navigated'), name='web_link_navigated' +) diff --git a/mayan/apps/web_links/forms.py b/mayan/apps/web_links/forms.py new file mode 100644 index 0000000000..e5e6beae69 --- /dev/null +++ b/mayan/apps/web_links/forms.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals + +from django import forms + +from .models import WebLink + + +class WebLinkForm(forms.ModelForm): + class Meta: + fields = ('label', 'template', 'enabled') + model = WebLink diff --git a/mayan/apps/web_links/icons.py b/mayan/apps/web_links/icons.py new file mode 100644 index 0000000000..9de99ddc33 --- /dev/null +++ b/mayan/apps/web_links/icons.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import, unicode_literals + +from mayan.apps.appearance.classes import Icon +from mayan.apps.documents.icons import icon_document_type + +icon_web_link = Icon(driver_name='fontawesome', symbol='external-link-alt') +icon_document_type_web_links = icon_web_link +icon_document_web_link_list = Icon( + driver_name='fontawesome', symbol='external-link-alt' +) +icon_web_link_create = Icon( + driver_name='fontawesome-dual', primary_symbol='external-link-alt', + secondary_symbol='plus' +) +icon_web_link_delete = Icon(driver_name='fontawesome', symbol='times') +icon_web_link_document_types = icon_document_type +icon_web_link_edit = Icon(driver_name='fontawesome', symbol='pencil-alt') +icon_web_link_instance_view = Icon( + driver_name='fontawesome', symbol='external-link-alt' +) +icon_web_link_setup = icon_web_link +icon_web_link_list = icon_web_link diff --git a/mayan/apps/web_links/links.py b/mayan/apps/web_links/links.py new file mode 100644 index 0000000000..9a914d4064 --- /dev/null +++ b/mayan/apps/web_links/links.py @@ -0,0 +1,62 @@ +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.documents.permissions import permission_document_type_edit +from mayan.apps.navigation.classes import Link + +from .permissions import ( + permission_web_link_create, permission_web_link_delete, + permission_web_link_edit, permission_web_link_instance_view, +) + +link_document_type_web_links = Link( + args='resolved_object.pk', + icon_class_path='mayan.apps.web_links.icons.icon_document_type_web_links', + permissions=(permission_document_type_edit,), text=_('Web links'), + view='web_links:document_type_web_links', +) +link_web_link_create = Link( + icon_class_path='mayan.apps.web_links.icons.icon_web_link_create', + permissions=(permission_web_link_create,), + text=_('Create new web link'), view='web_links:web_link_create' +) +link_web_link_delete = Link( + args='object.pk', + icon_class_path='mayan.apps.web_links.icons.icon_web_link_delete', + permissions=(permission_web_link_delete,), + tags='dangerous', text=_('Delete'), view='web_links:web_link_delete', +) +link_web_link_document_types = Link( + args='object.pk', + icon_class_path='mayan.apps.web_links.icons.icon_web_link_document_types', + permissions=(permission_web_link_edit,), + text=_('Document types'), view='web_links:web_link_document_types', +) +link_web_link_edit = Link( + args='object.pk', + icon_class_path='mayan.apps.web_links.icons.icon_web_link_edit', + permissions=(permission_web_link_edit,), + text=_('Edit'), view='web_links:web_link_edit', +) +link_web_link_instance_view = Link( + icon_class_path='mayan.apps.web_links.icons.icon_web_link_instance_view', + args=('document.pk', 'object.pk',), + permissions=(permission_web_link_instance_view,), tags='new_window', + text=_('Navigate'), view='web_links:web_link_instance_view', +) +link_document_web_link_list = Link( + args='resolved_object.pk', + icon_class_path='mayan.apps.web_links.icons.icon_document_web_link_list', + permissions=(permission_web_link_instance_view,), text=_('Web links'), + view='web_links:document_web_link_list', +) +link_web_link_list = Link( + icon_class_path='mayan.apps.web_links.icons.icon_web_link_list', + text=_('Web links'), view='web_links:web_link_list' +) +link_web_link_setup = Link( + icon_class_path='mayan.apps.web_links.icons.icon_web_link_setup', + permissions=(permission_web_link_create,), text=_('Web links'), + view='web_links:web_link_list' +) diff --git a/mayan/apps/web_links/locale/ar/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/ar/LC_MESSAGES/django.po new file mode 100644 index 0000000000..8a9db3d88a --- /dev/null +++ b/mayan/apps/web_links/locale/ar/LC_MESSAGES/django.po @@ -0,0 +1,146 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:34-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/bg/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/bg/LC_MESSAGES/django.po new file mode 100644 index 0000000000..5d48aa7c76 --- /dev/null +++ b/mayan/apps/web_links/locale/bg/LC_MESSAGES/django.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:34-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/bs_BA/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/bs_BA/LC_MESSAGES/django.po new file mode 100644 index 0000000000..e46fa79997 --- /dev/null +++ b/mayan/apps/web_links/locale/bs_BA/LC_MESSAGES/django.po @@ -0,0 +1,144 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:34-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/cs/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/cs/LC_MESSAGES/django.po new file mode 100644 index 0000000000..2f29ef339d --- /dev/null +++ b/mayan/apps/web_links/locale/cs/LC_MESSAGES/django.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/da_DK/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/da_DK/LC_MESSAGES/django.po new file mode 100644 index 0000000000..39549df32e --- /dev/null +++ b/mayan/apps/web_links/locale/da_DK/LC_MESSAGES/django.po @@ -0,0 +1,144 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/de_DE/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/de_DE/LC_MESSAGES/django.po new file mode 100644 index 0000000000..39549df32e --- /dev/null +++ b/mayan/apps/web_links/locale/de_DE/LC_MESSAGES/django.po @@ -0,0 +1,144 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/el/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/el/LC_MESSAGES/django.po new file mode 100644 index 0000000000..75dc9f44df --- /dev/null +++ b/mayan/apps/web_links/locale/el/LC_MESSAGES/django.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/en/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000000..39549df32e --- /dev/null +++ b/mayan/apps/web_links/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,144 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/es/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/es/LC_MESSAGES/django.po new file mode 100644 index 0000000000..75dc9f44df --- /dev/null +++ b/mayan/apps/web_links/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/fa/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/fa/LC_MESSAGES/django.po new file mode 100644 index 0000000000..748fd75f9b --- /dev/null +++ b/mayan/apps/web_links/locale/fa/LC_MESSAGES/django.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/fr/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000000..3ef34c6d97 --- /dev/null +++ b/mayan/apps/web_links/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/hu/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/hu/LC_MESSAGES/django.po new file mode 100644 index 0000000000..75dc9f44df --- /dev/null +++ b/mayan/apps/web_links/locale/hu/LC_MESSAGES/django.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/id/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/id/LC_MESSAGES/django.po new file mode 100644 index 0000000000..748fd75f9b --- /dev/null +++ b/mayan/apps/web_links/locale/id/LC_MESSAGES/django.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/it/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/it/LC_MESSAGES/django.po new file mode 100644 index 0000000000..75dc9f44df --- /dev/null +++ b/mayan/apps/web_links/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/lv/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/lv/LC_MESSAGES/django.po new file mode 100644 index 0000000000..4b1aa7d2fc --- /dev/null +++ b/mayan/apps/web_links/locale/lv/LC_MESSAGES/django.po @@ -0,0 +1,146 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : " +"2);\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/nl_NL/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/nl_NL/LC_MESSAGES/django.po new file mode 100644 index 0000000000..39549df32e --- /dev/null +++ b/mayan/apps/web_links/locale/nl_NL/LC_MESSAGES/django.po @@ -0,0 +1,144 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/pl/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/pl/LC_MESSAGES/django.po new file mode 100644 index 0000000000..68abd74869 --- /dev/null +++ b/mayan/apps/web_links/locale/pl/LC_MESSAGES/django.po @@ -0,0 +1,147 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n" +"%100<12 || n%100>=14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n" +"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/pt/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/pt/LC_MESSAGES/django.po new file mode 100644 index 0000000000..75dc9f44df --- /dev/null +++ b/mayan/apps/web_links/locale/pt/LC_MESSAGES/django.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/pt_BR/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 0000000000..3ef34c6d97 --- /dev/null +++ b/mayan/apps/web_links/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/ro_RO/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/ro_RO/LC_MESSAGES/django.po new file mode 100644 index 0000000000..39549df32e --- /dev/null +++ b/mayan/apps/web_links/locale/ro_RO/LC_MESSAGES/django.po @@ -0,0 +1,144 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/ru/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 0000000000..cc164a8ee6 --- /dev/null +++ b/mayan/apps/web_links/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,147 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" +"%100>=11 && n%100<=14)? 2 : 3);\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/sl_SI/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/sl_SI/LC_MESSAGES/django.po new file mode 100644 index 0000000000..39549df32e --- /dev/null +++ b/mayan/apps/web_links/locale/sl_SI/LC_MESSAGES/django.po @@ -0,0 +1,144 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/tr_TR/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/tr_TR/LC_MESSAGES/django.po new file mode 100644 index 0000000000..39549df32e --- /dev/null +++ b/mayan/apps/web_links/locale/tr_TR/LC_MESSAGES/django.po @@ -0,0 +1,144 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/vi_VN/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/vi_VN/LC_MESSAGES/django.po new file mode 100644 index 0000000000..39549df32e --- /dev/null +++ b/mayan/apps/web_links/locale/vi_VN/LC_MESSAGES/django.po @@ -0,0 +1,144 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/locale/zh/LC_MESSAGES/django.po b/mayan/apps/web_links/locale/zh/LC_MESSAGES/django.po new file mode 100644 index 0000000000..39549df32e --- /dev/null +++ b/mayan/apps/web_links/locale/zh/LC_MESSAGES/django.po @@ -0,0 +1,144 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-26 23:35-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: apps.py:38 +msgid "Links" +msgstr "" + +#: events.py:8 links.py:17 links.py:52 links.py:57 links.py:61 models.py:36 +#: permissions.py:7 views.py:145 +msgid "Web links" +msgstr "" + +#: events.py:12 +msgid "Web link created" +msgstr "" + +#: events.py:15 +msgid "Web link edited" +msgstr "" + +#: links.py:23 views.py:193 +msgid "Create new web link" +msgstr "" + +#: links.py:29 +msgid "Delete" +msgstr "" + +#: links.py:35 models.py:28 +msgid "Document types" +msgstr "" + +#: links.py:41 +msgid "Edit" +msgstr "" + +#: links.py:47 +msgid "Navigate" +msgstr "" + +#: models.py:22 +msgid "Label" +msgstr "" + +#: models.py:24 +msgid "Template" +msgstr "" + +#: models.py:25 +msgid "Enabled" +msgstr "" + +#: models.py:35 +msgid "Web link" +msgstr "" + +#: permissions.py:10 +msgid "Create new web links" +msgstr "" + +#: permissions.py:13 +msgid "Delete web links" +msgstr "" + +#: permissions.py:16 +msgid "Edit web links" +msgstr "" + +#: permissions.py:19 +msgid "View existing web links" +msgstr "" + +#: permissions.py:22 +msgid "View web link instances" +msgstr "" + +#: views.py:44 +msgid "Available web links" +msgstr "" + +#: views.py:45 +msgid "Web links enabled" +msgstr "" + +#: views.py:77 +#, python-format +msgid "Web links to enable for document type: %s" +msgstr "" + +#: views.py:111 +msgid "Available document types" +msgstr "" + +#: views.py:112 +msgid "Document types enabled" +msgstr "" + +#: views.py:122 +#, python-format +msgid "Document type for which to enable web link: %s" +msgstr "" + +#: views.py:139 +msgid "Web links allow generating links from documents to external resources." +msgstr "" + +#: views.py:143 +msgid "There are no web links" +msgstr "" + +#: views.py:180 +msgid "There are no web links for this document" +msgstr "" + +#: views.py:183 +#, python-format +msgid "Web links for document: %s" +msgstr "" + +#: views.py:214 +#, python-format +msgid "Delete web link: %s" +msgstr "" + +#: views.py:229 +#, python-format +msgid "Edit web link: %s" +msgstr "" diff --git a/mayan/apps/web_links/managers.py b/mayan/apps/web_links/managers.py new file mode 100644 index 0000000000..4e56f22a43 --- /dev/null +++ b/mayan/apps/web_links/managers.py @@ -0,0 +1,16 @@ +from django.db import models + +from mayan.apps.acls.models import AccessControlList + +from .permissions import permission_web_link_instance_view + + +class WebLinkManager(models.Manager): + def get_for(self, document, user): + queryset = self.filter( + document_types=document.document_type, enabled=True + ) + return AccessControlList.objects.restrict_queryset( + permission=permission_web_link_instance_view, + queryset=queryset, user=user + ) diff --git a/mayan/apps/web_links/migrations/0001_initial.py b/mayan/apps/web_links/migrations/0001_initial.py new file mode 100644 index 0000000000..4a844060c4 --- /dev/null +++ b/mayan/apps/web_links/migrations/0001_initial.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-07-27 04:29 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('documents', '0050_auto_20190725_0451'), + ] + + operations = [ + migrations.CreateModel( + name='WebLink', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.CharField(db_index=True, help_text='A short text describing the weblink.', max_length=96, verbose_name='Label')), + ('template', models.TextField(help_text='Template that will be used to craft the final URL of the weblink. The {{ document }} variable is available to the template.', verbose_name='Template')), + ('enabled', models.BooleanField(default=True, verbose_name='Enabled')), + ('document_types', models.ManyToManyField(related_name='web_links', to='documents.DocumentType', verbose_name='Document types')), + ], + options={ + 'ordering': ('label',), + 'verbose_name': 'Web link', + 'verbose_name_plural': 'Web links', + }, + ), + migrations.CreateModel( + name='ResolvedWebLink', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + }, + bases=('web_links.weblink',), + ), + ] diff --git a/mayan/apps/web_links/migrations/__init__.py b/mayan/apps/web_links/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mayan/apps/web_links/models.py b/mayan/apps/web_links/models.py new file mode 100644 index 0000000000..cccb845a5c --- /dev/null +++ b/mayan/apps/web_links/models.py @@ -0,0 +1,104 @@ +from __future__ import unicode_literals + +from django.db import models, transaction +from django.template import Context, Template +from django.urls import reverse +from django.utils.encoding import python_2_unicode_compatible +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.documents.events import event_document_type_edited +from mayan.apps.documents.models import DocumentType + +from .events import event_web_link_created, event_web_link_edited +from .managers import WebLinkManager + + +@python_2_unicode_compatible +class WebLink(models.Model): + """ + This model stores the basic fields for a web link. Web links allow + generating links from documents to external resources. + """ + label = models.CharField( + db_index=True, help_text=_('A short text describing the weblink.'), + max_length=96, verbose_name=_('Label') + ) + template = models.TextField( + help_text=_( + 'Template that will be used to craft the final URL of the ' + 'weblink. The {{ document }} variable is available to the template.' + ), verbose_name=_('Template') + ) + enabled = models.BooleanField(default=True, verbose_name=_('Enabled')) + document_types = models.ManyToManyField( + related_name='web_links', to=DocumentType, + verbose_name=_('Document types') + ) + + objects = WebLinkManager() + + class Meta: + ordering = ('label',) + verbose_name = _('Web link') + verbose_name_plural = _('Web links') + + def __str__(self): + return self.label + + def document_types_add(self, queryset, _user=None): + with transaction.atomic(): + event_web_link_edited.commit( + actor=_user, target=self + ) + for obj in queryset: + self.document_types.add(obj) + event_document_type_edited.commit( + actor=_user, action_object=self, target=obj + ) + + def document_types_remove(self, queryset, _user=None): + with transaction.atomic(): + event_web_link_edited.commit( + actor=_user, target=self + ) + for obj in queryset: + self.document_types.remove(obj) + event_document_type_edited.commit( + actor=_user, action_object=self, target=obj + ) + + def get_absolute_url(self): + return reverse( + viewname='weblinks:web_link_edit', kwargs={ + 'pk': self.pk + } + ) + + def save(self, *args, **kwargs): + _user = kwargs.pop('_user', None) + + with transaction.atomic(): + is_new = not self.pk + super(WebLink, self).save(*args, **kwargs) + if is_new: + event_web_link_created.commit( + actor=_user, target=self + ) + else: + event_web_link_edited.commit( + actor=_user, target=self + ) + + +class ResolvedWebLink(WebLink): + """ + Proxy model to represent an already resolved web link. Used for easier + colums registration. + """ + class Meta: + proxy = True + + def get_url_for(self, document): + context = Context({'document': document}) + + return Template(self.template).render(context=context) diff --git a/mayan/apps/web_links/permissions.py b/mayan/apps/web_links/permissions.py new file mode 100644 index 0000000000..2c377d960e --- /dev/null +++ b/mayan/apps/web_links/permissions.py @@ -0,0 +1,23 @@ +from __future__ import absolute_import, unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.permissions import PermissionNamespace + +namespace = PermissionNamespace(label=_('Web links'), name='web_links') + +permission_web_link_create = namespace.add_permission( + label=_('Create new web links'), name='web_link_create' +) +permission_web_link_delete = namespace.add_permission( + label=_('Delete web links'), name='web_link_delete' +) +permission_web_link_edit = namespace.add_permission( + label=_('Edit web links'), name='web_link_edit' +) +permission_web_link_view = namespace.add_permission( + label=_('View existing web links'), name='web_link_view' +) +permission_web_link_instance_view = namespace.add_permission( + label=_('View web link instances'), name='web_link_instance_view' +) diff --git a/mayan/apps/web_links/tests/__init__.py b/mayan/apps/web_links/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mayan/apps/web_links/tests/literals.py b/mayan/apps/web_links/tests/literals.py new file mode 100644 index 0000000000..b5aeddc599 --- /dev/null +++ b/mayan/apps/web_links/tests/literals.py @@ -0,0 +1,5 @@ +from __future__ import unicode_literals + +TEST_WEB_LINK_LABEL = 'test web link label' +TEST_WEB_LINK_LABEL_EDITED = 'test web link label edited' +TEST_WEB_LINK_TEMPLATE = 'http://www.example.com/document-{{ document.uuid }}' diff --git a/mayan/apps/web_links/tests/mixins.py b/mayan/apps/web_links/tests/mixins.py new file mode 100644 index 0000000000..59539f7de5 --- /dev/null +++ b/mayan/apps/web_links/tests/mixins.py @@ -0,0 +1,59 @@ +from __future__ import unicode_literals + +from ..models import WebLink + +from .literals import ( + TEST_WEB_LINK_LABEL, TEST_WEB_LINK_LABEL_EDITED, TEST_WEB_LINK_TEMPLATE +) + + +class WebLinkTestMixin(object): + def _create_test_web_link(self): + self.test_web_link = WebLink.objects.create( + label=TEST_WEB_LINK_LABEL, template=TEST_WEB_LINK_TEMPLATE, + ) + + +class WebLinkViewTestMixin(object): + def _request_test_document_web_link_instance_view(self): + return self.post( + viewname='web_links:web_link_instance_view', kwargs={ + 'document_pk': self.test_document.pk, + 'web_link_pk': self.test_web_link.pk + } + ) + + def _request_test_document_web_link_list_view(self): + return self.get( + viewname='web_links:document_web_link_list', kwargs={ + 'pk': self.test_document.pk + } + ) + + def _request_test_web_link_create_view(self): + return self.post( + viewname='web_links:web_link_create', data={ + 'label': TEST_WEB_LINK_LABEL, + 'template': TEST_WEB_LINK_TEMPLATE + } + ) + + def _request_test_web_link_delete_view(self): + return self.post( + viewname='web_links:web_link_delete', kwargs={ + 'pk': self.test_web_link.pk + } + ) + + def _request_test_web_link_edit_view(self): + return self.post( + viewname='web_links:web_link_edit', kwargs={ + 'pk': self.test_web_link.pk + }, data={ + 'label': TEST_WEB_LINK_LABEL_EDITED, + 'template': TEST_WEB_LINK_TEMPLATE + } + ) + + def _request_test_web_link_list_view(self): + return self.get(viewname='web_links:web_link_list') diff --git a/mayan/apps/web_links/tests/test_views.py b/mayan/apps/web_links/tests/test_views.py new file mode 100644 index 0000000000..8d2b03f664 --- /dev/null +++ b/mayan/apps/web_links/tests/test_views.py @@ -0,0 +1,218 @@ +from __future__ import unicode_literals + +from django.utils.encoding import force_text + +from mayan.apps.common.tests.base import GenericViewTestCase +from mayan.apps.documents.tests.base import GenericDocumentViewTestCase + +from ..models import WebLink +from ..permissions import ( + permission_web_link_create, permission_web_link_delete, + permission_web_link_edit, permission_web_link_view, + permission_web_link_instance_view +) + +from .literals import TEST_WEB_LINK_TEMPLATE +from .mixins import WebLinkTestMixin, WebLinkViewTestMixin + + +class WebLinkViewTestCase( + WebLinkTestMixin, WebLinkViewTestMixin, GenericViewTestCase +): + def test_web_link_create_view_no_permissions(self): + web_link_count = WebLink.objects.count() + + response = self._request_test_web_link_create_view() + self.assertEqual(response.status_code, 403) + + self.assertEqual(WebLink.objects.count(), web_link_count) + + def test_web_link_create_view_with_permissions(self): + self.grant_permission(permission=permission_web_link_create) + + web_link_count = WebLink.objects.count() + + response = self._request_test_web_link_create_view() + self.assertEqual(response.status_code, 302) + + self.assertEqual(WebLink.objects.count(), web_link_count + 1) + + def test_web_link_delete_view_no_permissions(self): + self._create_test_web_link() + + web_link_count = WebLink.objects.count() + + response = self._request_test_web_link_delete_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual(WebLink.objects.count(), web_link_count) + + def test_web_link_delete_view_with_access(self): + self._create_test_web_link() + + self.grant_access( + obj=self.test_web_link, permission=permission_web_link_delete + ) + + web_link_count = WebLink.objects.count() + + response = self._request_test_web_link_delete_view() + self.assertEqual(response.status_code, 302) + + self.assertEqual(WebLink.objects.count(), web_link_count - 1) + + def test_web_link_edit_view_no_permissions(self): + self._create_test_web_link() + + web_link_label = self.test_web_link.label + + response = self._request_test_web_link_edit_view() + self.assertEqual(response.status_code, 404) + + self.test_web_link.refresh_from_db() + self.assertEqual(self.test_web_link.label, web_link_label) + + def test_web_link_edit_view_with_access(self): + self._create_test_web_link() + + self.grant_access( + obj=self.test_web_link, permission=permission_web_link_edit + ) + + web_link_label = self.test_web_link.label + + response = self._request_test_web_link_edit_view() + self.assertEqual(response.status_code, 302) + + self.test_web_link.refresh_from_db() + self.assertNotEqual(self.test_web_link.label, web_link_label) + + def test_web_link_list_view_with_no_permission(self): + self._create_test_web_link() + + response = self._request_test_web_link_list_view() + self.assertNotContains( + response=response, text=self.test_web_link.label, status_code=200 + ) + + def test_web_link_list_view_with_access(self): + self._create_test_web_link() + + self.grant_access(obj=self.test_web_link, permission=permission_web_link_view) + + response = self._request_test_web_link_list_view() + self.assertContains( + response=response, text=self.test_web_link.label, status_code=200 + ) + + +class DocumentWebLinkViewTestCase( + WebLinkTestMixin, WebLinkViewTestMixin, GenericDocumentViewTestCase +): + def setUp(self): + super(DocumentWebLinkViewTestCase, self).setUp() + self._create_test_web_link() + self.test_web_link.document_types.add(self.test_document_type) + + def test_document_web_links_list_view_no_permissions(self): + response = self._request_test_document_web_link_list_view() + self.assertNotContains( + response=response, text=force_text(self.test_document), + status_code=404 + ) + self.assertNotContains( + response=response, text=force_text(self.test_web_link), + status_code=404 + ) + + def test_document_web_links_list_view_with_document_access(self): + self.grant_access( + obj=self.test_document, + permission=permission_web_link_instance_view + ) + + response = self._request_test_document_web_link_list_view() + self.assertContains( + response=response, text=force_text(self.test_document), + status_code=200 + ) + self.assertNotContains( + response=response, text=force_text(self.test_web_link), + status_code=200 + ) + + def test_document_web_links_list_view_with_web_link_access(self): + self.grant_access( + obj=self.test_web_link, + permission=permission_web_link_instance_view + ) + + response = self._request_test_document_web_link_list_view() + self.assertNotContains( + response=response, text=force_text(self.test_document), + status_code=404 + ) + self.assertNotContains( + response=response, text=force_text(self.test_web_link), + status_code=404 + ) + + def test_document_web_links_list_view_with_full_access(self): + self.grant_access( + obj=self.test_document, + permission=permission_web_link_instance_view + ) + self.grant_access( + obj=self.test_web_link, + permission=permission_web_link_instance_view + ) + + response = self._request_test_document_web_link_list_view() + self.assertContains( + response=response, text=force_text(self.test_document), + status_code=200 + ) + self.assertContains( + response=response, text=force_text(self.test_web_link), + status_code=200 + ) + + def test_document_resolved_web_link_view_no_permissions(self): + response = self._request_test_document_web_link_instance_view() + self.assertEqual(response.status_code, 404) + + def test_document_resolved_web_link_view_with_document_access(self): + self.grant_access( + obj=self.test_document, + permission=permission_web_link_instance_view + ) + + response = self._request_test_document_web_link_instance_view() + self.assertEqual(response.status_code, 404) + + def test_document_resolved_web_link_view_with_web_link_access(self): + self.grant_access( + obj=self.test_web_link, + permission=permission_web_link_instance_view + ) + + response = self._request_test_document_web_link_instance_view() + self.assertEqual(response.status_code, 404) + + def test_document_resolved_web_link_view_with_full_access(self): + self.grant_access( + obj=self.test_document, + permission=permission_web_link_instance_view + ) + self.grant_access( + obj=self.test_web_link, + permission=permission_web_link_instance_view + ) + + response = self._request_test_document_web_link_instance_view() + self.assertEqual(response.status_code, 302) + self.assertEqual( + response.url, TEST_WEB_LINK_TEMPLATE.replace( + '{{ document.uuid }}', force_text(self.test_document.uuid) + ) + ) diff --git a/mayan/apps/web_links/urls.py b/mayan/apps/web_links/urls.py new file mode 100644 index 0000000000..340f337f9b --- /dev/null +++ b/mayan/apps/web_links/urls.py @@ -0,0 +1,47 @@ +from __future__ import unicode_literals + +from django.conf.urls import url + +from .views import ( + DocumentWebLinkListView, DocumentTypeWebLinksView, ResolvedWebLinkView, + WebLinkCreateView, WebLinkDeleteView, WebLinkDocumentTypesView, + WebLinkEditView, WebLinkListView +) + +urlpatterns = [ + url( + regex=r'^document_types/(?P\d+)/web_links/$', + view=DocumentTypeWebLinksView.as_view(), + name='document_type_web_links' + ), + url( + regex=r'^documents/(?P\d+)/web_links/$', + view=DocumentWebLinkListView.as_view(), + name='document_web_link_list' + ), + url( + regex=r'^documents/(?P\d+)/(?P\d+)/$', + view=ResolvedWebLinkView.as_view(), name='web_link_instance_view' + ), + url( + regex=r'^weblinks/$', view=WebLinkListView.as_view(), + name='web_link_list' + ), + url( + regex=r'^weblinks/create/$', view=WebLinkCreateView.as_view(), + name='web_link_create' + ), + url( + regex=r'^weblinks/(?P\d+)/delete/$', + view=WebLinkDeleteView.as_view(), name='web_link_delete' + ), + url( + regex=r'^weblinks/(?P\d+)/document_types/$', + view=WebLinkDocumentTypesView.as_view(), + name='web_link_document_types' + ), + url( + regex=r'^weblinks/(?P\d+)/edit/$', view=WebLinkEditView.as_view(), + name='web_link_edit' + ), +] diff --git a/mayan/apps/web_links/views.py b/mayan/apps/web_links/views.py new file mode 100644 index 0000000000..33fb370a10 --- /dev/null +++ b/mayan/apps/web_links/views.py @@ -0,0 +1,234 @@ +from __future__ import absolute_import, unicode_literals + +import logging + +from django.db import transaction +from django.shortcuts import get_object_or_404 +from django.template import RequestContext +from django.urls import reverse_lazy +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import RedirectView + +from mayan.apps.acls.models import AccessControlList +from mayan.apps.common.generics import ( + AddRemoveView, SingleObjectCreateView, SingleObjectDeleteView, + 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 Document, DocumentType +from mayan.apps.documents.permissions import permission_document_type_edit + +from .events import event_web_link_edited, event_web_link_navigated +from .forms import WebLinkForm +from .icons import icon_web_link_setup +from .links import link_web_link_create +from .models import ResolvedWebLink, WebLink +from .permissions import ( + permission_web_link_create, permission_web_link_delete, + permission_web_link_edit, permission_web_link_instance_view, + permission_web_link_view +) + +logger = logging.getLogger(__name__) + + +class DocumentTypeWebLinksView(AddRemoveView): + main_object_method_add = 'web_link_add' + main_object_method_remove = 'web_link_remove' + main_object_permission = permission_document_type_edit + main_object_model = DocumentType + main_object_pk_url_kwarg = 'pk' + secondary_object_model = WebLink + secondary_object_permission = permission_web_link_edit + list_available_title = _('Available web links') + list_added_title = _('Web links enabled') + related_field = 'web_links' + + 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.web_links.add(obj) + event_web_link_edited.commit( + actor=_user, action_object=self.main_object, 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.web_links.remove(obj) + event_web_link_edited.commit( + actor=_user, action_object=self.main_object, target=obj + ) + + def get_actions_extra_kwargs(self): + return {'_user': self.request.user} + + def get_extra_context(self): + return { + 'object': self.main_object, + 'title': _( + 'Web links to enable for document type: %s' + ) % self.main_object, + } + + +class ResolvedWebLinkView(ExternalObjectMixin, RedirectView): + external_object_class = Document + external_object_pk_url_kwarg = 'document_pk' + external_object_permission = permission_web_link_instance_view + + def get_redirect_url(self, *args, **kwargs): + event_web_link_navigated.commit( + actor=self.request.user, action_object=self.external_object, + target=self.get_web_link() + ) + return self.get_web_link().get_url_for( + document=self.external_object + ) + + def get_web_link(self): + return get_object_or_404( + klass=self.get_web_link_queryset(), pk=self.kwargs['web_link_pk'] + ) + + def get_web_link_queryset(self): + queryset = ResolvedWebLink.objects.get_for( + document=self.external_object, user=self.request.user + ) + return AccessControlList.objects.restrict_queryset( + permission=permission_web_link_instance_view, queryset=queryset, + user=self.request.user + ) + + +class WebLinkDocumentTypesView(AddRemoveView): + main_object_method_add = 'document_types_add' + main_object_method_remove = 'document_types_remove' + main_object_permission = permission_web_link_edit + main_object_model = WebLink + 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 enabled') + 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, + 'title': _( + 'Document type for which to enable web link: %s' + ) % self.main_object, + } + + +class WebLinkListView(SingleObjectListView): + object_permission = permission_web_link_view + + def get_extra_context(self): + return { + 'hide_link': True, + 'hide_object': True, + 'no_results_icon': icon_web_link_setup, + 'no_results_main_link': link_web_link_create.resolve( + context=RequestContext(request=self.request) + ), + 'no_results_text': _( + 'Web links allow generating HTTP links from documents to ' + 'external resources. The link URL\'s can contain document ' + 'properties values.' + ), + 'no_results_title': _( + 'There are no web links' + ), + 'title': _('Web links'), + } + + def get_source_queryset(self): + return self.get_web_link_queryset() + + def get_web_link_queryset(self): + return WebLink.objects.all() + + +class DocumentWebLinkListView(ExternalObjectMixin, WebLinkListView): + external_object_class = Document + external_object_permission = permission_web_link_instance_view + object_permission = permission_web_link_instance_view + + def get_extra_context(self): + return { + 'document': self.external_object, + 'hide_link': True, + 'hide_object': True, + 'no_results_icon': icon_web_link_setup, + 'no_results_text': _( + 'Web links allow generating HTTP links from documents to ' + 'external resources. The link URL\'s can contain document ' + 'properties values.' + ), + 'no_results_title': _( + 'There are no web links for this document' + ), + 'object': self.external_object, + 'title': _('Web links for document: %s') % self.external_object, + } + + def get_web_link_queryset(self): + return ResolvedWebLink.objects.get_for( + document=self.external_object, user=self.request.user + ) + + +class WebLinkCreateView(SingleObjectCreateView): + extra_context = {'title': _('Create new web link')} + form_class = WebLinkForm + post_action_redirect = reverse_lazy( + viewname='web_links:web_link_list' + ) + view_permission = permission_web_link_create + + def get_save_extra_data(self): + return {'_user': self.request.user} + + +class WebLinkDeleteView(SingleObjectDeleteView): + model = WebLink + object_permission = permission_web_link_delete + post_action_redirect = reverse_lazy( + viewname='web_links:web_link_list' + ) + + def get_extra_context(self): + return { + 'object': self.get_object(), + 'title': _('Delete web link: %s') % self.get_object() + } + + +class WebLinkEditView(SingleObjectEditView): + form_class = WebLinkForm + model = WebLink + object_permission = permission_web_link_edit + post_action_redirect = reverse_lazy( + viewname='web_links:web_link_list' + ) + + def get_extra_context(self): + return { + 'object': self.get_object(), + 'title': _('Edit web link: %s') % self.get_object() + } + + def get_save_extra_data(self): + return {'_user': self.request.user} diff --git a/mayan/celery.py b/mayan/celery.py index ca8fc9cdad..a9265d0568 100644 --- a/mayan/celery.py +++ b/mayan/celery.py @@ -9,6 +9,5 @@ from .runtime import celery_class os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mayan.settings.production') app = celery_class('mayan') - -app.config_from_object('django.conf:settings') +app.config_from_object('django.conf:settings', namespace='CELERY') app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) diff --git a/mayan/settings/base.py b/mayan/settings/base.py index 53264739b0..5072fb150a 100644 --- a/mayan/settings/base.py +++ b/mayan/settings/base.py @@ -17,15 +17,15 @@ import sys from django.core.exceptions import ImproperlyConfigured from django.utils.translation import ugettext_lazy as _ -import environ +from mayan.apps.smart_settings.literals import BOOTSTRAP_SETTING_LIST +from mayan.apps.smart_settings.utils import ( + get_environment_setting, get_environment_variables, read_configuration_file +) from .literals import ( CONFIGURATION_FILENAME, CONFIGURATION_LAST_GOOD_FILENAME, DEFAULT_SECRET_KEY, SECRET_KEY_FILENAME, SYSTEM_DIR ) -from .utils import yaml_loads, read_configuration_file - -env = environ.Env() # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -34,8 +34,8 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ -MEDIA_ROOT = os.environ.get( - 'MAYAN_MEDIA_ROOT', os.path.join(BASE_DIR, 'media') +MEDIA_ROOT = get_environment_setting( + name='MEDIA_ROOT', fallback_default=os.path.join(BASE_DIR, 'media') ) # SECURITY WARNING: keep the secret key used in production secret! @@ -50,13 +50,9 @@ else: SECRET_KEY = DEFAULT_SECRET_KEY # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = env.bool('MAYAN_DEBUG', default=False) +DEBUG = get_environment_setting(name='DEBUG') -ALLOWED_HOSTS = yaml_loads( - env( - 'MAYAN_ALLOWED_HOSTS', default="['127.0.0.1', 'localhost', '[::1]']" - ) -) +ALLOWED_HOSTS = get_environment_setting(name='ALLOWED_HOSTS') # Application definition @@ -78,7 +74,7 @@ INSTALLED_APPS = ( 'actstream', 'colorful', 'corsheaders', - 'djcelery', + 'django_celery_beat', 'formtools', 'mathfilters', 'mptt', @@ -99,6 +95,7 @@ INSTALLED_APPS = ( 'mayan.apps.django_gpg', 'mayan.apps.dynamic_search', 'mayan.apps.events', + 'mayan.apps.file_caching', 'mayan.apps.lock_manager', 'mayan.apps.mimetype', 'mayan.apps.navigation', @@ -128,10 +125,11 @@ INSTALLED_APPS = ( 'mayan.apps.mirroring', 'mayan.apps.redactions', 'mayan.apps.ocr', + 'mayan.apps.redactions', 'mayan.apps.sources', 'mayan.apps.storage', 'mayan.apps.tags', - 'mayan.apps.weblinks', + 'mayan.apps.web_links', # Placed after rest_api to allow template overriding 'drf_yasg', ) @@ -260,12 +258,10 @@ TEST_RUNNER = 'mayan.apps.common.tests.runner.MayanTestRunner' # --------- Django ------------------- -LOGIN_URL = env('MAYAN_LOGIN_URL', default='authentication:login_view') -LOGIN_REDIRECT_URL = env('MAYAN_LOGIN_REDIRECT_URL', default='common:root') -LOGOUT_REDIRECT_URL = env( - 'MAYAN_LOGOUT_REDIRECT_URL', default='authentication:login_view' -) -INTERNAL_IPS = ('127.0.0.1',) +LOGIN_URL = get_environment_setting(name='LOGIN_URL') +LOGIN_REDIRECT_URL = get_environment_setting(name='LOGIN_REDIRECT_URL') +LOGOUT_REDIRECT_URL = get_environment_setting(name='LOGOUT_REDIRECT_URL') +INTERNAL_IPS = get_environment_setting(name='INTERNAL_IPS') # ---------- Django REST framework ----------- @@ -282,24 +278,31 @@ REST_FRAMEWORK = { # --------- Pagination -------- PAGINATION_SETTINGS = { - 'PAGE_RANGE_DISPLAYED': 8, + 'PAGE_RANGE_DISPLAYED': 5, 'MARGIN_PAGES_DISPLAYED': 2, } # ----------- Celery ---------- +CELERY_BROKER_URL = get_environment_setting(name='CELERY_BROKER_URL') +CELERY_RESULT_BACKEND = get_environment_setting(name='CELERY_RESULT_BACKEND') +CELERY_TASK_ALWAYS_EAGER = get_environment_setting( + name='CELERY_TASK_ALWAYS_EAGER' +) + CELERY_ACCEPT_CONTENT = ('json',) -CELERY_ALWAYS_EAGER = False -CELERY_CREATE_MISSING_QUEUES = False +CELERY_BEAT_SCHEDULE = {} +CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers.DatabaseScheduler' CELERY_DISABLE_RATE_LIMITS = True -CELERY_EAGER_PROPAGATES_EXCEPTIONS = True CELERY_ENABLE_UTC = True -CELERY_QUEUES = [] CELERY_RESULT_SERIALIZER = 'json' -CELERY_ROUTES = {} +CELERY_TASK_ALWAYS_EAGER = False +CELERY_TASK_CREATE_MISSING_QUEUES = False +CELERY_TASK_EAGER_PROPAGATES = True +CELERY_TASK_QUEUES = [] +CELERY_TASK_ROUTES = {} CELERY_TASK_SERIALIZER = 'json' CELERY_TIMEZONE = 'UTC' -CELERYBEAT_SCHEDULER = 'djcelery.schedulers.DatabaseScheduler' # ------------ CORS ------------ @@ -326,53 +329,39 @@ SWAGGER_SETTINGS = { AJAX_REDIRECT_CODE = 278 -# ----- Celery ----- - -BROKER_URL = os.environ.get('MAYAN_BROKER_URL') -CELERY_ALWAYS_EAGER = env.bool('MAYAN_CELERY_ALWAYS_EAGER', default=True) -CELERY_RESULT_BACKEND = os.environ.get('MAYAN_CELERY_RESULT_BACKEND') - # ----- Database ----- -environment_database_engine = os.environ.get('MAYAN_DATABASE_ENGINE') - -if environment_database_engine: - environment_database_conn_max_age = os.environ.get('MAYAN_DATABASE_CONN_MAX_AGE', 0) - if environment_database_conn_max_age: - environment_database_conn_max_age = int(environment_database_conn_max_age) - - DATABASES = { - 'default': { - 'ENGINE': environment_database_engine, - 'NAME': os.environ['MAYAN_DATABASE_NAME'], - 'USER': os.environ['MAYAN_DATABASE_USER'], - 'PASSWORD': os.environ['MAYAN_DATABASE_PASSWORD'], - 'HOST': os.environ.get('MAYAN_DATABASE_HOST', None), - 'PORT': os.environ.get('MAYAN_DATABASE_PORT', None), - 'CONN_MAX_AGE': environment_database_conn_max_age, - } +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(MEDIA_ROOT, 'db.sqlite3'), } -else: - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(MEDIA_ROOT, 'db.sqlite3'), - } - } - +} BASE_INSTALLED_APPS = INSTALLED_APPS COMMON_EXTRA_APPS = () COMMON_DISABLED_APPS = () -CONFIGURATION_FILEPATH = os.path.join(MEDIA_ROOT, CONFIGURATION_FILENAME) -CONFIGURATION_LAST_GOOD_FILEPATH = os.path.join( - MEDIA_ROOT, CONFIGURATION_LAST_GOOD_FILENAME +CONFIGURATION_FILEPATH = get_environment_setting( + name='CONFIGURATION_FILEPATH', fallback_default=os.path.join( + MEDIA_ROOT, CONFIGURATION_FILENAME + ) +) + +CONFIGURATION_LAST_GOOD_FILEPATH = get_environment_setting( + name='CONFIGURATION_LAST_GOOD_FILEPATH', fallback_default=os.path.join( + MEDIA_ROOT, CONFIGURATION_LAST_GOOD_FILENAME + ) ) if 'revertsettings' not in sys.argv: - result = read_configuration_file(CONFIGURATION_FILEPATH) - if result: - globals().update(result) + configuration_result = read_configuration_file(CONFIGURATION_FILEPATH) + environment_result = get_environment_variables() + + for setting in BOOTSTRAP_SETTING_LIST: + if setting['name'] in configuration_result: + globals().update({setting['name']: configuration_result[setting['name']]}) + elif setting['name'] in environment_result: + globals().update({setting['name']: environment_result[setting['name']]}) for app in INSTALLED_APPS: diff --git a/mayan/settings/development.py b/mayan/settings/development.py index 77961253a9..8668627db3 100644 --- a/mayan/settings/development.py +++ b/mayan/settings/development.py @@ -1,17 +1,20 @@ from __future__ import absolute_import, unicode_literals +from mayan.apps.smart_settings.utils import get_environment_setting + from . import * # NOQA ALLOWED_HOSTS = ['*'] DEBUG = True -CELERY_ALWAYS_EAGER = True -CELERY_EAGER_PROPAGATES_EXCEPTIONS = CELERY_ALWAYS_EAGER +CELERY_TASK_ALWAYS_EAGER = get_environment_setting( + name='CELERY_TASK_ALWAYS_EAGER', fallback_default='true' +) +CELERY_TASK_EAGER_PROPAGATES = CELERY_TASK_ALWAYS_EAGER EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' - if 'rosetta' not in INSTALLED_APPS: try: import rosetta diff --git a/mayan/settings/production.py b/mayan/settings/production.py index f5cc3f6af5..fe7ba1a037 100644 --- a/mayan/settings/production.py +++ b/mayan/settings/production.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals from . import * # NOQA -CELERY_ALWAYS_EAGER = False +CELERY_TASK_ALWAYS_EAGER = False TEMPLATES[0]['OPTIONS']['loaders'] = ( ( diff --git a/mayan/settings/staging/docker.py b/mayan/settings/staging/docker.py index 464daabcf2..640a1b2a96 100644 --- a/mayan/settings/staging/docker.py +++ b/mayan/settings/staging/docker.py @@ -12,6 +12,6 @@ DATABASES = { } } -BROKER_URL = 'redis://127.0.0.1:6379/0' +CELERY_BROKER_URL = 'redis://127.0.0.1:6379/0' CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0' DEBUG = True diff --git a/mayan/settings/testing/base.py b/mayan/settings/testing/base.py index d576514d98..4eb37026c1 100644 --- a/mayan/settings/testing/base.py +++ b/mayan/settings/testing/base.py @@ -2,8 +2,8 @@ from __future__ import absolute_import, unicode_literals from .. import * # NOQA -CELERY_ALWAYS_EAGER = True -CELERY_EAGER_PROPAGATES_EXCEPTIONS = True +CELERY_TASK_ALWAYS_EAGER = True +CELERY_TASK_EAGER_PROPAGATES = True BROKER_BACKEND = 'memory' COMMON_PRODUCTION_ERROR_LOG_PATH = '/tmp/mayan-errors.log' diff --git a/mayan/settings/utils.py b/mayan/settings/utils.py deleted file mode 100644 index bb2a8da489..0000000000 --- a/mayan/settings/utils.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import unicode_literals - -import errno -import os - -import yaml - - -def read_configuration_file(path): - try: - with open(path) as file_object: - file_object.seek(0, os.SEEK_END) - if file_object.tell(): - file_object.seek(0) - try: - return yaml.safe_load(file_object) - except yaml.YAMLError as exception: - exit( - 'Error loading configuration file: {}; {}'.format( - path, exception - ) - ) - except IOError as exception: - if exception.errno == errno.ENOENT: - pass - else: - raise - - -def yaml_loads(data, error_message=None): - if not error_message: - error_message = 'Error loading: {}; {}' - - try: - return yaml.safe_load(data) - except yaml.YAMLError as exception: - exit( - error_message.format(data, exception) - ) diff --git a/removals.txt b/removals.txt index 6a40aee535..5d3132d41e 100644 --- a/removals.txt +++ b/removals.txt @@ -1,6 +1,8 @@ # Packages to be remove during upgrades cssmin django-autoadmin +django-celery +django-environ django-suit django-compressor django-filetransfers diff --git a/requirements/base.txt b/requirements/base.txt index 7919f51384..e74cc93d45 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,9 +1,9 @@ Pillow==6.0.0 PyPDF2==1.26.0 PyYAML==5.1.1 -celery==3.1.24 +celery==4.3.0 django-activity-stream==0.7.0 -django-celery==3.2.1 +django-celery-beat==1.5.0 django-colorful==1.3 django-cors-headers==2.5.2 django-downloadview==1.9 diff --git a/requirements/development.txt b/requirements/development.txt index 8689376c36..36332a99b3 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -1,4 +1,5 @@ Werkzeug==0.15.4 +devpi-server==5.0.0 django-debug-toolbar==1.11 django-extensions==2.1.9 django-rosetta==0.9.3 diff --git a/setup.py b/setup.py index 8b0516670d..67898584f0 100644 --- a/setup.py +++ b/setup.py @@ -60,9 +60,9 @@ django==1.11.24 Pillow==6.0.0 PyPDF2==1.26.0 PyYAML==5.1.1 -celery==3.1.24 +celery==4.3.0 django-activity-stream==0.7.0 -django-celery==3.2.1 +django-celery-beat==1.5.0 django-colorful==1.3 django-cors-headers==2.5.2 django-downloadview==1.9