diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 772bd315d3..eb06baf4dc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -173,7 +173,8 @@ test-mysql: script: - apt-get install -qq libmysqlclient-dev mysql-client - . venv/bin/activate - - pip install mysqlclient + - set -a && . ./config.env && set +a + - pip install mysqlclient==$PYTHON_MYSQL_VERSION - mysql -h"$MYSQL_PORT_3306_TCP_ADDR" -P"$MYSQL_PORT_3306_TCP_PORT" -uroot -p"$MYSQL_ENV_MYSQL_ROOT_PASSWORD" -e "set global character_set_server=utf8mb4;" - python manage.py test --mayan-apps --settings=mayan.settings.testing.gitlab-ci.db_mysql --nomigrations tags: @@ -189,7 +190,8 @@ test-postgres: script: - apt-get install -qq libpq-dev - . venv/bin/activate - - pip install psycopg2 + - set -a && . ./config.env && set +a + - pip install psycopg2==$PYTHON_PSYCOPG2_VERSION - python manage.py test --mayan-apps --settings=mayan.settings.testing.gitlab-ci.db_postgres --nomigrations tags: - postgres diff --git a/HISTORY.rst b/HISTORY.rst index 282cf91bcb..7b494a2aa9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -137,6 +137,19 @@ - Add makefile target to launch a production staging Docker image. - Improve duplicated document list view logic to not show documents with trashed duplicates. +- Backport Docker composer makefile targets. +- Add PermissionTestCaseMixin and SmartSettingTestCaseMixin to better + organize cache invalidation of both apps for tests. +- Add a version attribute to setting namespace. These are dumped + as SMART_SETTINGS_NAMESPACES. +- Add savesettings command. +- Add extra logging to the IMAP email source. GitLab issue #682. + Thanks to Patrick Hütter (@PatrickHuetter) for the report. +- Rename all instances of the IMAP server from mailbox to + server for clarity. +- Add book link in the about menu. +- Add unknown exception handling when checking for the latest + version. 3.2.8 (2019-10-01) ================== diff --git a/Makefile b/Makefile index ad4c64febd..567a7d73f1 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +#!make +include config.env .PHONY: clean-pyc clean-build DOCKER_MYSQL_IMAGE = mysql:8.0 @@ -5,11 +7,6 @@ DOCKER_ORACLE_IMAGE = wnameless/oracle-xe-11g DOCKER_POSTGRES_IMAGE = postgres:9.6-alpine DOCKER_REDIS_IMAGE = redis:5.0-alpine -PYTHON_MYSQL_VERSION = 1.4.4 -PYTHON_PSYCOPG2_VERSION = 2.8.3 -PYTHON_RABBITMQ_VERSION = 2.0.0 -PYTHON_REDIS_VERSION = 3.2.1 - help: @echo "Usage: make \n" @awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z_-]+:.*?## / { printf " * %-40s -%s\n", $$1, $$2 }' $(MAKEFILE_LIST)|sort @@ -43,7 +40,7 @@ test-all: clean-pyc test-launch-postgres: @docker rm -f test-postgres || true @docker volume rm test-postgres || true - docker run -d --name test-postgres -p 5432:5432 -v test-postgres:/var/lib/postgresql/data $(DOCKER_POSTGRES_IMAGE) + docker run -d --name test-postgres -p 5432:5432 -v test-postgres:/var/lib/postgresql/data $(DOCKER_POSTGRES_IMAGE_VERSION) sudo apt-get install -q libpq-dev pip install psycopg2==$(PYTHON_PSYCOPG2_VERSION) while ! nc -z 127.0.0.1 5432; do sleep 1; done @@ -63,7 +60,7 @@ test-with-postgres-all: test-launch-postgres test-launch-mysql: @docker rm -f test-mysql || true @docker volume rm test-mysql || true - docker run -d --name test-mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=True -e MYSQL_DATABASE=mayan -v test-mysql:/var/lib/mysql $(DOCKER_MYSQL_IMAGE) + docker run -d --name test-mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=True -e MYSQL_DATABASE=mayan -v test-mysql:/var/lib/mysql $(DOCKER_MYSQL_IMAGE_VERSION) sudo apt-get install -q libmysqlclient-dev mysql-client pip install mysqlclient==$(PYTHON_MYSQL_VERSION) while ! nc -z 127.0.0.1 3306; do sleep 1; done @@ -85,9 +82,9 @@ test-with-mysql-all: test-launch-mysql test-launch-oracle: @docker rm -f test-oracle || true @docker volume rm test-oracle || true - docker run -d --name test-oracle -p 49160:22 -p 49161:1521 -e ORACLE_ALLOW_REMOTE=true -v test-oracle:/u01/app/oracle $(DOCKER_ORACLE_IMAGE) + docker run -d --name test-oracle -p 49160:22 -p 49161:1521 -e ORACLE_ALLOW_REMOTE=true -v test-oracle:/u01/app/oracle $(DOCKER_ORACLE_IMAGE_VERSION) # https://gist.github.com/kimus/10012910 - pip install cx_Oracle + pip install cx_Oracle==$(PYTHON_ORACLE_VERSION) while ! nc -z 127.0.0.1 49161; do sleep 1; done sleep 10 @@ -256,8 +253,8 @@ shell_plus: ## Run the shell_plus command. ./manage.py shell_plus --settings=mayan.settings.development test-with-docker-services-on: ## Launch and initialize production-like services using Docker (Postgres and Redis). - docker run -d --name redis -p 6379:6379 $(DOCKER_REDIS_IMAGE) - docker run -d --name postgres -p 5432:5432 $(DOCKER_POSTGRES_IMAGE) + docker run -d --name redis -p 6379:6379 $(DOCKER_REDIS_IMAGE_VERSION) + docker run -d --name postgres -p 5432:5432 $(DOCKER_POSTGRES_IMAGE_VERSION) while ! nc -z 127.0.0.1 6379; do sleep 1; done while ! nc -z 127.0.0.1 5432; do sleep 1; done sleep 4 @@ -275,7 +272,7 @@ test-with-docker-worker: ## Launch a worker instance that uses the production-li 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 $(DOCKER_MYSQL_IMAGE) + docker run -d --name mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=True -e MYSQL_DATABASE=mayan_edms $(DOCKER_MYSQL_IMAGE_VERSION) while ! nc -z 127.0.0.1 3306; do sleep 1; done docker-mysql-off: ## Stop and delete the MySQL Docker container. @@ -283,7 +280,7 @@ docker-mysql-off: ## Stop and delete the MySQL Docker container. docker rm mysql docker-postgres-on: ## Launch and initialize a PostgreSQL Docker container. - docker run -d --name postgres -p 5432:5432 $(DOCKER_POSTGRES_IMAGE) + docker run -d --name postgres -p 5432:5432 $(DOCKER_POSTGRES_IMAGE_VERSION) while ! nc -z 127.0.0.1 5432; do sleep 1; done docker-postgres-off: ## Stop and delete the PostgreSQL Docker container. diff --git a/config.env b/config.env new file mode 100644 index 0000000000..a2e3f42415 --- /dev/null +++ b/config.env @@ -0,0 +1,12 @@ +DOCKER_MYSQL_IMAGE_VERSION=mysql:8.0 +DOCKER_ORACLE_IMAGE=wnameless/oracle-xe-11g +DOCKER_POSTGRES_IMAGE_VERSION=postgres:9.6-alpine +DOCKER_RABBITMQ_IMAGE_VERSION=rabbitmq:3 +DOCKER_REDIS_IMAGE_VERSION=redis:5.0-alpine +PYTHON_FLOWER_VERSION=0.9.3 +PYTHON_LIBRABBITMQ_VERSION=2.0.0 +PYTHON_MYSQL_VERSION=1.4.4 +PYTHON_ORACLE_VERSION=7.2.3 +PYTHON_PSYCOPG2_VERSION=2.8.3 +PYTHON_PSUTIL_VERSION=5.6.2 +PYTHON_REDIS_VERSION=3.2.1 diff --git a/docker/Dockerfile b/docker/Dockerfile index cb193e6ad2..c23a45dc91 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -8,6 +8,8 @@ FROM debian:10.0-slim as BASE_IMAGE LABEL maintainer="Roberto Rosario roberto.rosario@mayan-edms.com" +COPY config.env /config.env + ENV PYTHONUNBUFFERED=1 \ LC_ALL=C.UTF-8 \ PROJECT_INSTALL_DIR=/opt/mayan-edms @@ -104,18 +106,22 @@ apt-get install -y --no-install-recommends \ && chown -R mayan:mayan /src USER mayan -RUN python3 -m venv "${PROJECT_INSTALL_DIR}" \ + +RUN set -a \ +&& . /config.env \ +&& set +a \ +&& python3 -m venv "${PROJECT_INSTALL_DIR}" \ && . "${PROJECT_INSTALL_DIR}/bin/activate" \ && pip install --no-cache-dir \ - librabbitmq==2.0.0 \ - mysqlclient==1.4.2.post1 \ - psycopg2==2.8.3 \ - redis==3.2.1 \ - flower==0.9.3 \ + librabbitmq==$PYTHON_LIBRABBITMQ_VERSION \ + mysql-python==$PYTHON_MYSQL_VERSION \ + psycopg2==$PYTHON_PSYCOPG2_VERSION \ + redis==$PYTHON_REDIS_VERSION \ + flower==$PYTHON_FLOWER_VERSION \ # 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 \ - psutil==5.6.2 \ + psutil==$PYTHON_PSUTIL_VERSION \ ; fi \ # Install the Python packages needed to build Mayan EDMS && pip install --no-cache-dir -r /src/requirements/build.txt \ diff --git a/docker/Makefile b/docker/Makefile index 26bea14737..443777d69c 100755 --- a/docker/Makefile +++ b/docker/Makefile @@ -1,3 +1,6 @@ +#!make +include config.env + HOST_IP = `/sbin/ip route|awk '/docker0/ { print $$9 }'` APT_PROXY ?= $(HOST_IP):3142 CONSOLE_COLUMNS ?= `echo $$(tput cols)` @@ -51,13 +54,13 @@ docker-staging-container-postgresql-start: -e POSTGRES_DB=mayan \ -e POSTGRES_PASSWORD=mayanuserpass \ -v mayan-staging-postgres:/var/lib/postgresql/data \ - postgres:9.6-alpine + $(DOCKER_POSTGRES_IMAGE_VERSION) docker-staging-container-redis-start: docker run -d \ --name mayan-staging-redis \ --network=mayan-staging \ - redis:5.0-alpine \ + $(DOCKER_REDIS_IMAGE_VERSION) \ redis-server \ --databases \ "2" \ diff --git a/docs/chapters/backups.rst b/docs/chapters/backups.rst index 5d65284308..496ce9c766 100644 --- a/docs/chapters/backups.rst +++ b/docs/chapters/backups.rst @@ -59,7 +59,7 @@ Example:: -e POSTGRES_DB=mayan \ -e POSTGRES_PASSWORD=mayanuserpass \ -v /docker-volumes/mayan-edms/postgres-new:/var/lib/postgresql/data \ - -d postgres:9.6 + |DOCKER_POSTGRES_IMAGE_VERSION| docker exec -i mayan-edms-pg-new pg_restore -U mayan -d mayan < 2018-06-07_17-09-34.dump diff --git a/docs/chapters/deploying.rst b/docs/chapters/deploying.rst index 7bbc889a17..8b574ec2cb 100644 --- a/docs/chapters/deploying.rst +++ b/docs/chapters/deploying.rst @@ -84,14 +84,14 @@ For another setup that offers more performance and scalability refer to the ------------------------------------------------------ :: - sudo -u mayan /opt/mayan-edms/bin/pip install --no-cache-dir --no-use-pep517 psycopg2==2.7.3.2 redis==2.10.6 + sudo -u mayan /opt/mayan-edms/bin/pip install --no-cache-dir --no-use-pep517 psycopg2==|PYTHON_PSYCOPG2_VERSION| redis==|PYTHON_REDIS_VERSION| .. note:: Platforms with the ARM CPU might also need additional requirements. :: - sudo -u mayan /opt/mayan-edms/bin/pip install --no-cache-dir --no-use-pep517 psutil==5.6.2 + sudo -u mayan /opt/mayan-edms/bin/pip install --no-cache-dir --no-use-pep517 psutil==|PYTHON_PSUTIL_VERSION| 8. Create the database for the installation: @@ -204,7 +204,7 @@ of a restart or power failure. The Gunicorn workers are increased to 3. ------------------------------------------ :: - sudo -u mayan /opt/mayan-edms/bin/pip install --no-cache-dir --no-use-pep517 librabbitmq==2.0.0 + sudo -u mayan /opt/mayan-edms/bin/pip install --no-cache-dir --no-use-pep517 librabbitmq==|PYTHON_LIBRABBITMQ_VERSION| 3. Create the RabbitMQ user and vhost: diff --git a/docs/chapters/development.rst b/docs/chapters/development.rst index 90e20552c9..ddca8b1b10 100644 --- a/docs/chapters/development.rst +++ b/docs/chapters/development.rst @@ -247,7 +247,7 @@ Mayan EDMS follows a simplified model layout based on Vincent Driessen's Working branches for unfinished and unmerged feature. Likely unstable, don't use in production. Once the feature is complete, it is merged into one of the versions branches and deleted. - + Special branches: ``releases/all`` @@ -317,7 +317,7 @@ Steps to deploy a development version $ git clone https://gitlab.com/mayan-edms/mayan-edms.git $ cd mayan-edms - $ git checkout development + $ git checkout $ virtualenv venv $ source venv/bin/activate $ pip install -r requirements.txt @@ -558,3 +558,10 @@ Manual release :: make release-via-docker-ubuntu + +Other steps +----------- + +#. Update the contrib/scripts/install/docker.sh values + +#. Upload contrib/scripts/install/docker.sh to https://get.mayan-edms.com diff --git a/docs/chapters/docker.rst b/docs/chapters/docker.rst index 401ea472d1..8ae848e815 100644 --- a/docs/chapters/docker.rst +++ b/docs/chapters/docker.rst @@ -23,7 +23,7 @@ tag here, remember to do so in the next steps also.:: Then download version 9.6 of the Docker PostgreSQL image:: - docker pull postgres:9.6-alpine + docker pull |DOCKER_POSTGRES_IMAGE_VERSION| Create and run a PostgreSQL container:: @@ -35,7 +35,7 @@ Create and run a PostgreSQL container:: -e POSTGRES_DB=mayan \ -e POSTGRES_PASSWORD=mayanuserpass \ -v /docker-volumes/mayan-edms/postgres:/var/lib/postgresql/data \ - -d postgres:9.6-alpine + |DOCKER_POSTGRES_IMAGE_VERSION| The PostgreSQL container will have one database named ``mayan``, with an user named ``mayan`` too, with a password of ``mayanuserpass``. The container will @@ -92,7 +92,7 @@ binding (``-p 5432:5432``):: -e POSTGRES_DB=mayan \ -e POSTGRES_PASSWORD=mayanuserpass \ -v /docker-volumes/mayan-edms/postgres:/var/lib/postgresql/data \ - -d postgres:9.6-alpine + |DOCKER_POSTGRES_IMAGE_VERSION| Launch the Mayan EDMS container with the network option and change the database hostname to the PostgreSQL container name (``mayan-edms-postgres``) diff --git a/docs/chapters/scaling_up.rst b/docs/chapters/scaling_up.rst index de03468b83..84ef71a6cb 100644 --- a/docs/chapters/scaling_up.rst +++ b/docs/chapters/scaling_up.rst @@ -92,7 +92,7 @@ section for the required changes. For the Docker image, launch a separate RabbitMQ container (https://hub.docker.com/_/rabbitmq/):: - docker run -d --name mayan-edms-rabbitmq -e RABBITMQ_DEFAULT_USER=mayan -e RABBITMQ_DEFAULT_PASS=mayanrabbitmqpassword -e RABBITMQ_DEFAULT_VHOST=mayan rabbitmq:3 + docker run -d --name mayan-edms-rabbitmq -e RABBITMQ_DEFAULT_USER=mayan -e RABBITMQ_DEFAULT_PASS=mayanrabbitmqpassword -e RABBITMQ_DEFAULT_VHOST=mayan |DOCKER_RABBITMQ_IMAGE_VERSION| 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 diff --git a/docs/conf.py b/docs/conf.py index 520ff4791e..5605895f04 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,12 +29,12 @@ sys.path.append( # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -#extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] -#extensions = ["djangodocs", "sphinx.ext.intersphinx"] +# extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] +# extensions = ["djangodocs", "sphinx.ext.intersphinx"] extensions = [ 'sphinx.ext.extlinks', 'sphinxcontrib.blockdiag', 'sphinxcontrib.spelling' @@ -72,20 +72,20 @@ release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True @@ -102,7 +102,7 @@ show_authors = False pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- @@ -119,23 +119,23 @@ html_theme_options = { } # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -148,40 +148,40 @@ html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'MayanEDMSdoc' @@ -191,40 +191,42 @@ html_show_sphinx = False # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'MayanEDMS.tex', 'Mayan EDMS Documentation', - mayan.__author__, 'manual'), + ( + 'index', 'MayanEDMS.tex', 'Mayan EDMS Documentation', + mayan.__author__, 'manual' + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- @@ -255,5 +257,25 @@ extlinks = { } +def _load_env_file(filename='../config.env'): + result = [] + with open(filename) as file_object: + for line in file_object: + if not line.startswith('#'): + key, value = line.strip().split('=') + result.append(('|{}|'.format(key), value)) + + return result + + +def GlobalSubstitution(app, docname, source): + for old, new in global_subtitutions: + source[0] = source[0].replace(old, new) + + def setup(app): app.add_stylesheet('css/custom.css') + app.connect('source-read', GlobalSubstitution) + + +global_subtitutions = _load_env_file() diff --git a/mayan/apps/common/apps.py b/mayan/apps/common/apps.py index 56ddfd89e4..d8031940fd 100644 --- a/mayan/apps/common/apps.py +++ b/mayan/apps/common/apps.py @@ -22,7 +22,7 @@ from .handlers import ( handler_user_locale_profile_session_config, handler_user_locale_profile_create ) from .links import ( - link_about, link_current_user_locale_profile_edit, link_license, + link_about, link_book, link_current_user_locale_profile_edit, link_license, link_object_error_list_clear, link_setup, link_tools ) @@ -109,7 +109,7 @@ class CommonApp(MayanAppConfig): menu_about.bind_links( links=( - link_tools, link_setup, link_about, link_license, + link_tools, link_setup, link_about, link_book, link_license, ) ) diff --git a/mayan/apps/common/icons.py b/mayan/apps/common/icons.py index 0c8919ec0c..65df6c45f6 100644 --- a/mayan/apps/common/icons.py +++ b/mayan/apps/common/icons.py @@ -11,6 +11,7 @@ icon_add_all = Icon( ) icon_assign_remove_add = Icon(driver_name='fontawesome', symbol='plus') icon_assign_remove_remove = Icon(driver_name='fontawesome', symbol='minus') +icon_book = Icon(driver_name='fontawesome', symbol='book') icon_confirm_form_submit = Icon(driver_name='fontawesome', symbol='check') icon_confirm_form_cancel = Icon(driver_name='fontawesome', symbol='times') icon_current_user_locale_profile_details = Icon( diff --git a/mayan/apps/common/links.py b/mayan/apps/common/links.py index 5fbea67244..46b4328400 100644 --- a/mayan/apps/common/links.py +++ b/mayan/apps/common/links.py @@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.navigation.classes import Link from .icons import ( - icon_about, icon_current_user_locale_profile_details, + icon_about, icon_book, icon_current_user_locale_profile_details, icon_current_user_locale_profile_edit, icon_documentation, icon_forum, icon_license, icon_setup, icon_source_code, icon_support, icon_tools @@ -35,6 +35,10 @@ def get_kwargs_factory(variable_name): link_about = Link( icon_class=icon_about, text=_('About this'), view='common:about_view' ) +link_book = Link( + icon_class=icon_book, tags='new_window', text=_('Get the book'), + url='https://mayan-edms.com/book/' +) link_current_user_locale_profile_details = Link( icon_class=icon_current_user_locale_profile_details, text=_('Locale profile'), diff --git a/mayan/apps/common/tests/base.py b/mayan/apps/common/tests/base.py index 81802e63e7..d92d2168ec 100644 --- a/mayan/apps/common/tests/base.py +++ b/mayan/apps/common/tests/base.py @@ -6,8 +6,10 @@ from django_downloadview import assert_download_response from mayan.apps.acls.tests.mixins import ACLTestCaseMixin from mayan.apps.converter.tests.mixins import LayerTestCaseMixin -from mayan.apps.permissions.classes import Permission -from mayan.apps.smart_settings.classes import Namespace +from mayan.apps.permissions.tests.mixins import PermissionTestCaseMixin +from mayan.apps.smart_settings.tests.mixins import SmartSettingsTestCaseMixin + + from mayan.apps.user_management.tests.mixins import UserTestMixin from .mixins import ( @@ -22,19 +24,15 @@ from .mixins import ( class BaseTestCase( LayerTestCaseMixin, SilenceLoggerTestCaseMixin, ConnectionsCheckTestCaseMixin, RandomPrimaryKeyModelMonkeyPatchMixin, ACLTestCaseMixin, - ModelTestCaseMixin, OpenFileCheckTestCaseMixin, - TempfileCheckTestCasekMixin, UserTestMixin, TestCase + ModelTestCaseMixin, OpenFileCheckTestCaseMixin, PermissionTestCaseMixin, + SmartSettingsTestCaseMixin, TempfileCheckTestCasekMixin, UserTestMixin, + TestCase ): """ This is the most basic test case class any test in the project should use. """ assert_download_response = assert_download_response - def setUp(self): - super(BaseTestCase, self).setUp() - Namespace.invalidate_cache_all() - Permission.invalidate_cache() - class GenericViewTestCase( ClientMethodsTestCaseMixin, ContentTypeCheckTestCaseMixin, diff --git a/mayan/apps/dependencies/views.py b/mayan/apps/dependencies/views.py index e5d9951e7e..7e970be5be 100644 --- a/mayan/apps/dependencies/views.py +++ b/mayan/apps/dependencies/views.py @@ -28,6 +28,12 @@ class CheckVersionView(SimpleView): 'It is not possible to determine the latest version ' 'available.' ) + except Exception as exception: + message = _( + 'Unexpected error trying to determine the latest version ' + 'available. Make sure your installation has a connection to ' + 'the internet; %s' + ) % exception else: message = _('Your version is up-to-date.') diff --git a/mayan/apps/permissions/tests/mixins.py b/mayan/apps/permissions/tests/mixins.py index 146746e44a..4270626c0c 100644 --- a/mayan/apps/permissions/tests/mixins.py +++ b/mayan/apps/permissions/tests/mixins.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from ..classes import PermissionNamespace +from ..classes import Permission, PermissionNamespace from ..models import Role from .literals import ( @@ -45,6 +45,12 @@ class PermissionTestMixin(object): ) +class PermissionTestCaseMixin(object): + def setUp(self): + super(PermissionTestCaseMixin, self).setUp() + Permission.invalidate_cache() + + class RoleAPIViewTestMixin(object): def _request_test_role_create_api_view(self, extra_data=None): data = { diff --git a/mayan/apps/smart_settings/classes.py b/mayan/apps/smart_settings/classes.py index 4ea8ae11dd..c63dd7763f 100644 --- a/mayan/apps/smart_settings/classes.py +++ b/mayan/apps/smart_settings/classes.py @@ -21,6 +21,7 @@ from mayan.apps.common.serialization import yaml_dump, yaml_load from .utils import read_configuration_file logger = logging.getLogger(__name__) +SMART_SETTINGS_NAMESPACES_NAME = 'SMART_SETTINGS_NAMESPACES' @python_2_unicode_compatible @@ -39,26 +40,35 @@ class Namespace(object): exception ) + @classmethod + def get(cls, name): + return cls._registry[name] + @classmethod def get_all(cls): return sorted(cls._registry.values(), key=lambda x: x.label) @classmethod - def get(cls, name): - return cls._registry[name] + def get_namespace_config(cls, name): + return cls.get_namespaces_config().get(name, {}) + + @classmethod + def get_namespaces_config(cls): + return getattr(settings, SMART_SETTINGS_NAMESPACES_NAME, {}) @classmethod def invalidate_cache_all(cls): for namespace in cls.get_all(): namespace.invalidate_cache() - def __init__(self, name, label): + def __init__(self, name, label, version='0001'): if name in self.__class__._registry: raise Exception( 'Namespace names must be unique; "%s" already exists.' % name ) self.name = name self.label = label + self.version = version self.__class__._registry[name] = self self._settings = [] @@ -68,6 +78,9 @@ class Namespace(object): def add_setting(self, **kwargs): return Setting(namespace=self, **kwargs) + def get_config_version(self): + return Namespace.get_namespace_config(name=self.name).get('version', None) + def invalidate_cache(self): for setting in self._settings: setting.invalidate_cache() @@ -123,7 +136,18 @@ class Setting(object): def dump_data(cls, filter_term=None, namespace=None): dictionary = {} + if not namespace: + namespace_dictionary = {} + for _namespace in Namespace.get_all(): + namespace_dictionary[_namespace.name] = { + 'version': _namespace.version + } + + dictionary[SMART_SETTINGS_NAMESPACES_NAME] = namespace_dictionary + for setting in cls.get_all(): + # If a namespace is specified, filter the list by that namespace + # otherwise return always True to include all (or not None == True) if (namespace and setting.namespace.name == namespace) or not namespace: 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) @@ -157,9 +181,12 @@ class Setting(object): ) @classmethod - def save_configuration(cls, path=settings.CONFIGURATION_FILEPATH): + def save_configuration(cls, path=None): + if not path: + path = settings.CONFIGURATION_FILEPATH + try: - with open(path, 'w') as file_object: + with open(path, mode='w') as file_object: file_object.write(cls.dump_data()) except IOError as exception: if exception.errno == errno.ENOENT: diff --git a/mayan/apps/smart_settings/management/commands/savesettings.py b/mayan/apps/smart_settings/management/commands/savesettings.py new file mode 100644 index 0000000000..4d6e17fd19 --- /dev/null +++ b/mayan/apps/smart_settings/management/commands/savesettings.py @@ -0,0 +1,18 @@ +from __future__ import unicode_literals + +from django.core import management + +from ...classes import Setting + + +class Command(management.BaseCommand): + help = 'Save the current settings into the configuration file.' + + def add_arguments(self, parser): + parser.add_argument( + '--filepath', action='store', dest='filepath', + help='Filename and path where to save the configuration file.' + ) + + def handle(self, *args, **options): + Setting.save_configuration(path=options.get('filepath')) diff --git a/mayan/apps/smart_settings/tests/mixins.py b/mayan/apps/smart_settings/tests/mixins.py index 83cadcb4c3..2991121903 100644 --- a/mayan/apps/smart_settings/tests/mixins.py +++ b/mayan/apps/smart_settings/tests/mixins.py @@ -5,6 +5,12 @@ from ..classes import Namespace from .literals import TEST_NAMESPACE_LABEL, TEST_NAMESPACE_NAME +class SmartSettingsTestCaseMixin(object): + def setUp(self): + super(SmartSettingsTestCaseMixin, self).setUp() + Namespace.invalidate_cache_all() + + class SmartSettingTestMixin(object): def _create_test_settings_namespace(self): try: diff --git a/mayan/apps/sources/models/email_sources.py b/mayan/apps/sources/models/email_sources.py index 4f4bd5245b..429da5a042 100644 --- a/mayan/apps/sources/models/email_sources.py +++ b/mayan/apps/sources/models/email_sources.py @@ -261,7 +261,14 @@ class IMAPEmail(EmailBaseModel): server = imaplib.IMAP4(host=self.host, port=self.port) server.login(user=self.username, password=self.password) - server.select(mailbox=self.mailbox) + try: + server.select(mailbox=self.mailbox) + except Exception as exception: + raise SourceException( + 'Error selecting mailbox: {}; {}'.format( + self.mailbox, exception + ) + ) try: status, data = server.uid( @@ -280,10 +287,26 @@ class IMAPEmail(EmailBaseModel): for uid in uids: logger.debug('message uid: %s', uid) - status, data = server.uid('FETCH', uid, '(RFC822)') - EmailBaseModel.process_message( - source=self, message_text=data[0][1] - ) + + try: + status, data = server.uid('FETCH', uid, '(RFC822)') + except Exception as exception: + raise SourceException( + 'Error fetching message uid: {}; {}'.format( + uid, exception + ) + ) + + try: + EmailBaseModel.process_message( + source=self, message_text=data[0][1] + ) + except Exception as exception: + raise SourceException( + 'Error processing message uid: {}; {}'.format( + uid, exception + ) + ) if not test: if self.store_commands: