├── .editorconfig ├── .flake8 ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── AUTHORS ├── LICENSE ├── README.md ├── django_tools ├── __init__.py ├── admin_tools │ ├── __init__.py │ └── test_generator.py ├── apps.py ├── auto_update_cache │ ├── README.creole │ ├── __init__.py │ └── filebased.py ├── cache │ ├── README.creole │ ├── __init__.py │ ├── site_cache_middleware.py │ └── smooth_cache_backends.py ├── checks.py ├── constants.py ├── context_managers.py ├── context_processors.py ├── debug │ ├── README.creole │ ├── __init__.py │ ├── delay.py │ └── middlewares.py ├── decorators.py ├── exception_plus.py ├── exceptions.py ├── fields │ ├── __init__.py │ ├── directory.py │ ├── language_code.py │ ├── media_path.py │ ├── sign_separated.py │ ├── static_path.py │ └── url.py ├── file_storage │ ├── __init__.py │ └── file_system_storage.py ├── filemanager │ ├── README.creole │ ├── __init__.py │ ├── exceptions.py │ ├── filemanager.py │ ├── filesystem_browser.py │ ├── tests.py │ └── utils.py ├── fixture_tools │ ├── __init__.py │ └── languages.py ├── forms_utils.py ├── limit_to_usergroups.py ├── local_sync_cache │ ├── LocalSyncCacheMiddleware.py │ ├── __init__.py │ └── local_sync_cache.py ├── log_utils │ ├── __init__.py │ ├── syslog_handler.py │ └── throttle_admin_email_handler.py ├── mail │ ├── __init__.py │ └── send_mail.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── clear_cache.py │ │ ├── database_info.py │ │ ├── generate_model_test_code.py │ │ ├── list_models.py │ │ ├── logging_info.py │ │ ├── nice_diffsettings.py │ │ ├── permission_info.py │ │ ├── run_testserver.py │ │ └── update_permissions.py ├── middlewares │ ├── LogHeaders.py │ ├── QueryLogMiddleware.py │ ├── SlowerDevServer.py │ ├── ThreadLocal.py │ ├── TracebackLogMiddleware.py │ ├── __init__.py │ └── local_auto_login.py ├── model_utils.py ├── model_version_protect │ ├── README.md │ ├── __init__.py │ ├── apps.py │ ├── models.py │ ├── templates │ │ └── model_version_protect │ │ │ └── version_widget.html │ └── tests │ │ ├── __init__.py │ │ ├── test_admin.py │ │ ├── test_admin_basic_django41_1.snapshot.html │ │ ├── test_admin_basic_django41_2.snapshot.html │ │ ├── test_admin_basic_django42_1.snapshot.html │ │ ├── test_admin_basic_django42_2.snapshot.html │ │ ├── test_admin_basic_django51_1.snapshot.html │ │ ├── test_admin_basic_django51_2.snapshot.html │ │ ├── test_models.py │ │ └── utils.py ├── models.py ├── parler_utils │ ├── __init__.py │ └── parler_fixtures.py ├── permissions.py ├── serve_media_app │ ├── README.md │ ├── __init__.py │ ├── apps.py │ ├── constants.py │ ├── exceptions.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_signals.py │ │ └── test_views.py │ ├── urls.py │ ├── utils.py │ └── views │ │ ├── __init__.py │ │ └── serve_user_files.py ├── settings_utils.py ├── template │ ├── __init__.py │ ├── filters.py │ ├── loader.py │ ├── render.py │ └── warn_invalid_template_vars.py ├── templates │ └── tagging_addon │ │ └── jQueryTagField.html ├── unittest_utils │ ├── BrowserDebug.py │ ├── __init__.py │ ├── assertments.py │ ├── call_management_commands.py │ ├── disable_migrations.py │ ├── django_command.py │ ├── email.py │ ├── isolated_filesystem.py │ ├── logging_utils.py │ ├── mockup.py │ ├── model_test_code_generator.py │ ├── print_sql.py │ ├── project_setup.py │ ├── signals.py │ ├── stdout_redirect.py │ ├── temp_media_root.py │ ├── tempdir.py │ ├── template.py │ ├── unittest_base.py │ └── user.py ├── utils │ ├── __init__.py │ ├── client_storage.py │ ├── html_utils.py │ ├── importlib.py │ ├── info_print.py │ ├── installed_apps_utils.py │ ├── messages.py │ ├── request.py │ ├── stack_info.py │ ├── time_utils.py │ └── url.py ├── validators.py └── views │ ├── __init__.py │ └── csrf.py ├── django_tools_project ├── __init__.py ├── __main__.py ├── constants.py ├── django_tools_test_app │ ├── __init__.py │ ├── admin.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ ├── mail_test.html │ │ ├── mail_test.txt │ │ └── test_template.html │ └── views.py ├── middlewares.py ├── settings │ ├── __init__.py │ ├── local.py │ ├── prod.py │ └── tests.py ├── templates │ └── .gitkeep ├── tests │ ├── __init__.py │ ├── test_ThreadLocal_middleware.py │ ├── test_TracebackLogMiddleware.py │ ├── test_admin.py │ ├── test_admin_staff_admin_index_django41_1.snapshot.html │ ├── test_admin_staff_admin_index_django42_1.snapshot.html │ ├── test_admin_staff_admin_index_django51_1.snapshot.html │ ├── test_admin_superuser_admin_index_django41_1.snapshot.html │ ├── test_admin_superuser_admin_index_django42_1.snapshot.html │ ├── test_admin_superuser_admin_index_django51_1.snapshot.html │ ├── test_assertments.py │ ├── test_basics.py │ ├── test_database_info.py │ ├── test_debug_delay.py │ ├── test_doctest.py │ ├── test_email.py │ ├── test_file_system_storage.py │ ├── test_filesystem_browser.py │ ├── test_html_utils.py │ ├── test_installed_apps_utils.py │ ├── test_isolated_filesystem.py │ ├── test_limit_to_usergroups.py │ ├── test_local_sync_cache.py │ ├── test_log_utils.py │ ├── test_log_utils_syslog_handler.py │ ├── test_logging_utils.py │ ├── test_mockup.py │ ├── test_model_test_generator.py │ ├── test_model_utils.py │ ├── test_parler_fixtures.py │ ├── test_permissions.py │ ├── test_permissions_add_app_permissions_1.snapshot.json │ ├── test_permissions_get_admin_permissions_1.snapshot.json │ ├── test_permissions_get_filtered_permissions_1.snapshot.json │ ├── test_permissions_pprint_filtered_permissions_1.snapshot.txt │ ├── test_project_setup.py │ ├── test_render.py │ ├── test_request_utils.py │ ├── test_settings_utils.py │ ├── test_signed_cookie.py │ ├── test_template_loader.py │ ├── test_unittest_django_command.py │ ├── test_unittest_django_command_clear_cache_1.snapshot.txt │ ├── test_unittest_django_command_help_django41_1.snapshot.txt │ ├── test_unittest_django_command_help_django42_1.snapshot.txt │ ├── test_unittest_django_command_help_django51_1.snapshot.txt │ ├── test_unittest_django_command_list_models_1.snapshot.txt │ ├── test_unittest_django_command_nice_diffsettings_django41_1.snapshot.txt │ ├── test_unittest_django_command_nice_diffsettings_django42_1.snapshot.txt │ ├── test_unittest_django_command_nice_diffsettings_django51_1.snapshot.txt │ ├── test_unittest_django_command_no_username_given_1.snapshot.txt │ ├── test_unittest_django_command_normal_test_user_1.snapshot.txt │ ├── test_unittest_django_command_run_testserver_1.snapshot.txt │ ├── test_unittest_django_command_update_permissions_1.snapshot.txt │ ├── test_unittest_django_command_wrong_username_given_1.snapshot.txt │ ├── test_unittest_utils.py │ ├── test_unittest_utils_stdout_redirect.py │ ├── test_unittest_utils_temp_media_root.py │ ├── test_unittest_utils_user.py │ ├── test_unittest_utils_user_get_or_create_user_and_group_1.snapshot.txt │ ├── test_unittest_utils_user_remove_obsolete_permissions_django41_1.snapshot.txt │ ├── test_unittest_utils_user_remove_obsolete_permissions_django42_1.snapshot.txt │ ├── test_unittest_utils_user_remove_obsolete_permissions_django51_1.snapshot.txt │ ├── test_url.py │ ├── test_utils_importlib.py │ ├── test_utils_stack_info.py │ ├── test_utils_url.py │ ├── test_warn_decorators.py │ └── utils.py ├── urls.py └── wsgi.py ├── logo ├── logo.svg ├── logo_black.svg └── logo_white.svg ├── manage.py ├── pyproject.toml ├── requirements.dev.txt ├── requirements.django41.txt ├── requirements.django42.txt ├── requirements.django51.txt └── requirements.txt /.editorconfig: -------------------------------------------------------------------------------- 1 | # see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{html,css,js}] 13 | insert_final_newline = false 14 | 15 | [*.py] 16 | max_line_length = 119 17 | 18 | [{Makefile,**.mk}] 19 | indent_style = tab 20 | insert_final_newline = false 21 | 22 | [{*.yaml,*.yml}] 23 | indent_size = 2 24 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | # 2 | # Move to pyproject.toml after: https://github.com/PyCQA/flake8/issues/234 3 | # 4 | [flake8] 5 | exclude = .*, dist, htmlcov, */migrations/* 6 | #ignore = E402 7 | max-line-length = 119 8 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | 2 | 3 | name: tests 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | schedule: 11 | - cron: '0 8 * * *' 12 | 13 | jobs: 14 | test: 15 | name: 'Python ${{ matrix.python-version }} Django ${{ matrix.django-version }}' 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | python-version: ['3.12', '3.11'] 21 | django-version: ['5.1', '4.2', '4.1'] 22 | steps: 23 | - name: Checkout 24 | run: | 25 | echo $GITHUB_REF $GITHUB_SHA 26 | git clone https://github.com/$GITHUB_REPOSITORY.git . 27 | git fetch origin $GITHUB_SHA:temporary-ci-branch 28 | git checkout $GITHUB_SHA || (git fetch && git checkout $GITHUB_SHA) 29 | 30 | - name: 'Set up Python ${{ matrix.python-version }}' 31 | uses: actions/setup-python@v5 32 | # https://github.com/marketplace/actions/setup-python 33 | with: 34 | python-version: '${{ matrix.python-version }}' 35 | cache: 'pip' # caching pip dependencies 36 | cache-dependency-path: '**/requirements.*.txt' 37 | 38 | - name: 'Bootstrap' 39 | # The first manage.py call will create the .venv 40 | run: | 41 | ./manage.py version 42 | 43 | - name: 'Display all Django commands' 44 | run: | 45 | ./manage.py --help 46 | 47 | - name: 'pip-audit' 48 | run: | 49 | ./manage.py pip_audit 50 | 51 | - name: 'Python ${{ matrix.python-version }} Django ${{ matrix.django-version }}' 52 | env: 53 | PYTHONUNBUFFERED: 1 54 | PYTHONWARNINGS: always 55 | run: | 56 | ./manage.py tox -e $(echo py${{ matrix.python-version }}-django${{ matrix.django-version }} | tr -d .) 57 | 58 | - name: 'Upload coverage report' 59 | uses: codecov/codecov-action@v4 60 | # https://github.com/marketplace/actions/codecov 61 | with: 62 | fail_ci_if_error: false 63 | verbose: true 64 | 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.egg-info 3 | __pycache__ 4 | /dist/ 5 | /build/ 6 | /coverage.* 7 | *.orig 8 | 9 | !.github 10 | !.editorconfig 11 | !.flake8 12 | !.gitignore 13 | !.pre-commit-config.yaml 14 | !.pre-commit-hooks.yaml 15 | !.gitkeep 16 | 17 | # from test projects: 18 | /static/ 19 | /media/ 20 | *.sqlite3 21 | *.json 22 | 23 | # Include "ignored" *.json: 24 | !**/fixtures/*.json 25 | 26 | # Include all test snapshot files: 27 | !**/*.snapshot.* 28 | 29 | # Migration of test app are always generated on-the-fly, so ignore them: 30 | django_tools_project/django_tools_test_app/migrations/* 31 | !django_tools_project/django_tools_test_app/migrations/__init__.py 32 | 33 | # Test project 34 | secret.txt 35 | 36 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # pre-commit plugin configuration 2 | # See https://pre-commit.com for more information 3 | default_install_hook_types: 4 | - pre-commit 5 | - post-rewrite 6 | - pre-push 7 | 8 | repos: 9 | - repo: https://github.com/jedie/cli-base-utilities 10 | rev: v0.10.3 11 | hooks: 12 | - id: update-readme-history 13 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | # https://pre-commit.com/#creating-new-hooks 2 | - id: update-readme-history 3 | name: cli-base-utilities 4 | description: >- 5 | Update history in README.md from git log. 6 | entry: "python -m cli_base update-readme-history -v" 7 | language: python 8 | language_version: python3 9 | require_serial: true 10 | pass_filenames: false 11 | always_run: true 12 | verbose: true 13 | stages: [pre-commit, post-rewrite, pre-push] 14 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | 2 | PRIMARY AUTHORS are and/or have been (alphabetic order): 3 | 4 | * Diemer, Jens 5 | Main Developer since the first code line. 6 | ohloh.net profile: 7 | Homepage: 8 | 9 | 10 | CONTRIBUTORS are and/or have been (alphabetic order): 11 | - Ben Konrath 12 | - Don Naegely 13 | - Lucas Wiman 14 | - Samuel Spencer 15 | - Yuekui 16 | 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | All rights reserved. 2 | 3 | 4 | django-tools is free software; you can redistribute it and/or modify it 5 | under the terms of the GNU General Public License version 3 or later as published 6 | by the Free Software Foundation. 7 | 8 | complete GNU General Public License version 3: 9 | http://www.gnu.org/licenses/gpl-3.0.txt 10 | 11 | German translation: 12 | http://www.gnu.de/documents/gpl.de.html 13 | 14 | 15 | copyleft 2009 by the django-tools team, see AUTHORS for more details. 16 | 17 | SVN info: 18 | $LastChangedDate: $ 19 | $Rev: $ 20 | -------------------------------------------------------------------------------- /django_tools/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | "django-tools 3 | miscellaneous tools for Django based projects 4 | """ 5 | 6 | __version__ = '0.56.2' 7 | __author__ = 'Jens Diemer ' 8 | -------------------------------------------------------------------------------- /django_tools/admin_tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/admin_tools/__init__.py -------------------------------------------------------------------------------- /django_tools/admin_tools/test_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | :created: 24.04.2018 by Jens Diemer 3 | :copyleft: 2018 by the django-tools team, see AUTHORS for more details. 4 | :license: GNU GPL v3 or above, see LICENSE for more details. 5 | """ 6 | 7 | from django.contrib import messages 8 | from django.http import HttpResponse 9 | from django.utils.translation import gettext_lazy as _ 10 | 11 | # https://github.com/jedie/django-tools 12 | from django_tools.unittest_utils.model_test_code_generator import ModelTestGenerator 13 | 14 | 15 | def generate_test_code(modeladmin, request, queryset): 16 | """ 17 | Django admin action to generate unittest code from model instances. 18 | 19 | To activate globally for all models, put this into one of your admin.py, e.g.: 20 | admin.site.add_action(generate_test_code) 21 | 22 | Select in admin some model entries and call the action from drop-down menu. 23 | """ 24 | user = request.user 25 | if not user.is_superuser: 26 | messages.error(request, "Only allowed for superusers!") 27 | return 28 | 29 | model_test_generator = ModelTestGenerator() 30 | content = model_test_generator.from_queryset(queryset) 31 | # print(content) 32 | 33 | response = HttpResponse(content, content_type="text/python") 34 | 35 | model_label = modeladmin.model._meta.label 36 | response['Content-Disposition'] = f'attachment; filename={model_label}.py' 37 | 38 | return response 39 | 40 | 41 | generate_test_code.short_description = _("Generate unittest code") 42 | -------------------------------------------------------------------------------- /django_tools/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig as BaseAppConfig 2 | 3 | 4 | class AppConfig(BaseAppConfig): 5 | name = 'django_tools' 6 | verbose_name = 'django-tools' 7 | 8 | def ready(self): 9 | import django_tools.checks # noqa 10 | -------------------------------------------------------------------------------- /django_tools/auto_update_cache/README.creole: -------------------------------------------------------------------------------- 1 | 2 | === Auto update cache 3 | 4 | **Experimental** "Auto update" cache. 5 | 6 | Using the django cache framework can save processing-overhead. 7 | The problem is Validation/Invalidation of the cache. 8 | 9 | ==== The problem 10 | 11 | One simple solution: Do 'cache.clear()' every time the data changed. 12 | This ensures that everything is always up to date. 13 | But an empty cache causes very high load when many requests are at the same time. 14 | 15 | ==== The solution 16 | 17 | "Auto update cache" doesn't clear the cache at once. It renew out dated entries 18 | variable based on the system load. 19 | The cache would be updated more quickly if system load is low than under heavy load. 20 | 21 | === usage 22 | 23 | In your settings use: 24 | {{{ 25 | 'django_tools.auto_update_cache.filebased.AutoUpdateFileBasedCache' 26 | }}} 27 | for {{{CACHES[foobar]['BACKEND']}}} 28 | 29 | The django app must call {{{cache.save_change_time()}}} if the database change 30 | and all old cache items are potentially out of date. -------------------------------------------------------------------------------- /django_tools/auto_update_cache/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/auto_update_cache/__init__.py -------------------------------------------------------------------------------- /django_tools/cache/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/cache/__init__.py -------------------------------------------------------------------------------- /django_tools/checks.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/checks.py -------------------------------------------------------------------------------- /django_tools/constants.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/constants.py -------------------------------------------------------------------------------- /django_tools/context_managers.py: -------------------------------------------------------------------------------- 1 | class MassContextManagerBase: 2 | """ 3 | ContextManager enter and exit more managers ;) 4 | """ 5 | 6 | context_managers = None # Should be set by child class 7 | 8 | def __enter__(self): 9 | assert self.context_managers 10 | for cm in self.context_managers: 11 | cm.__enter__() 12 | return self 13 | 14 | def __exit__(self, exc_type, exc_val, exc_tb): 15 | for cm in self.context_managers: 16 | cm.__exit__(exc_type, exc_val, exc_tb) 17 | -------------------------------------------------------------------------------- /django_tools/context_processors.py: -------------------------------------------------------------------------------- 1 | from django_tools import __version__ 2 | 3 | 4 | def django_tools_version_string(request): 5 | return {"version_string": f"v{__version__}"} 6 | -------------------------------------------------------------------------------- /django_tools/debug/README.creole: -------------------------------------------------------------------------------- 1 | == debug tools 2 | 3 | === Delay tools 4 | 5 | Sometimes you want to simulate when processing takes a little longer. 6 | There exists {{{django_tools.debug.delay.SessionDelay}}} and {{{django_tools.debug.delay.CacheDelay}}} for this. 7 | The usage will create logging entries and user messages, if user is authenticated. 8 | 9 | {{{SessionDelay}}} stores the sleep seconds into {{{request.session}}} and {{{CacheDelay}}} used the django cache backend. 10 | 11 | {{{SessionDelay}}} can be used if {{{request}}} object is available while the delay is to be executed. e.g.: usage in views. 12 | {{{CacheDelay}}} can be used to set the delay value in a view but the delay should be executed where no {{{request}}} is available, e.g.: in models or tasks. 13 | 14 | 15 | {{{SessionDelay}}} usage e.g.: 16 | 17 | {{{ 18 | from django_tools.debug.delay import SessionDelay 19 | 20 | def your_view(request): 21 | # Save delay value of "?delay" if it appears in the URL to request.session: 22 | SessionDelay( 23 | request, 24 | key="slow_foo", # Used as session key 25 | only_debug=True # Delay only if settings.DEBUG == True 26 | ).load( 27 | request, 28 | query_string="delay", # The GET parameter name 29 | default=5 # fallback value if GET parameter contains no value, e.g.: "?delay" 30 | ) 31 | 32 | #...do something... 33 | 34 | # get "?delay=XX" from session and delay with time.sleep() if exists: 35 | SessionDelay( 36 | request, 37 | key="slow_foo" 38 | ).sleep() 39 | 40 | return your_response 41 | }}} 42 | 43 | 44 | {{{CacheDelay}}} usage e.g.: 45 | 46 | {{{ 47 | from django_tools.debug.delay import CacheDelay 48 | 49 | class FooBarModel(models.Model): 50 | # ... 51 | def save(self, **kwargs): 52 | # Get the "?delay=XX" from cache and delay with time.sleep() if exists: 53 | CacheDelay(key="slow_save_%s" % instance.pk).sleep() 54 | 55 | super().save(**kwargs) 56 | 57 | 58 | def your_view(request): 59 | 60 | instance = get_foo_bar_model_instance(request) 61 | 62 | # Save delay value of "?delay" if it appears in the URL to cache: 63 | CacheDelay( 64 | key="slow_save_%s" % instance.pk, # Used as cache key 65 | only_debug=True # Delay only if settings.DEBUG == True 66 | ).load( 67 | self.request, 68 | query_string="delay", # The GET parameter name 69 | default=5 # fallback value if GET parameter contains no value, e.g.: "?delay" 70 | ) 71 | 72 | # ... 73 | 74 | instance.save() 75 | 76 | return your_response 77 | }}} 78 | 79 | 80 | === middlewares 81 | 82 | ==== SetRequestDebugMiddleware 83 | 84 | Add 'debug' bool attribute to request object. 85 | 86 | request.debug is True if: 87 | settings.DEBUG == True 88 | *OR* 89 | remote IP is in settings.INTERNAL_IPS 90 | 91 | ===== usage 92 | 93 | Add this to your settings: 94 | 95 | {{{ 96 | MIDDLEWARE_CLASSES = ( 97 | ... 98 | # Set request.debug bool value: 99 | 'django_tools.debug.middlewares.SetRequestDebugMiddleware', 100 | ... 101 | ) 102 | }}} 103 | -------------------------------------------------------------------------------- /django_tools/debug/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/debug/__init__.py -------------------------------------------------------------------------------- /django_tools/debug/middlewares.py: -------------------------------------------------------------------------------- 1 | """ 2 | debug middlewares 3 | ~~~~~~~~~~~~~~~~~ 4 | 5 | more information in the README. 6 | 7 | :copyleft: 2012 by the django-tools team, see AUTHORS for more details. 8 | :license: GNU GPL v3 or above, see LICENSE for more details. 9 | """ 10 | 11 | 12 | from django.conf import settings 13 | 14 | 15 | class SetRequestDebugMiddleware: 16 | """ 17 | add 'debug' bool attribute to request object 18 | 19 | debug is on if: 20 | settings.DEBUG == True 21 | *OR* 22 | remote IP is in settings.INTERNAL_IPS 23 | """ 24 | 25 | def process_request(self, request): 26 | request.debug = settings.DEBUG or request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS 27 | -------------------------------------------------------------------------------- /django_tools/exception_plus.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from bx_py_utils.error_handling import print_exc_plus as print_exc_plus2 4 | 5 | 6 | def print_exc_plus(exc=None, stop_on_file_path=None, max_chars=None): 7 | """ 8 | Print the usual traceback information, followed by a listing of all the 9 | local variables in each frame. 10 | """ 11 | warnings.warn("Use bx_py_utils.error_handling.print_exc_plus!", DeprecationWarning, stacklevel=2) 12 | print_exc_plus2(exc=exc, stop_on_file_path=stop_on_file_path, max_chars=max_chars) 13 | -------------------------------------------------------------------------------- /django_tools/exceptions.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/exceptions.py -------------------------------------------------------------------------------- /django_tools/fields/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/fields/__init__.py -------------------------------------------------------------------------------- /django_tools/fields/directory.py: -------------------------------------------------------------------------------- 1 | """ 2 | directory selection 3 | ~~~~~~~~~~~~~~~~~~~~ 4 | 5 | :copyleft: 2011-2016 by the django-tools team, see AUTHORS for more details. 6 | :license: GNU GPL v3 or above, see LICENSE for more details. 7 | """ 8 | 9 | 10 | import os 11 | 12 | from django import forms 13 | from django.conf import settings 14 | from django.db import models 15 | from django.utils.translation import gettext_lazy as _ 16 | 17 | from django_tools import validators 18 | 19 | 20 | class DirectoryWidget(forms.TextInput): 21 | """ 22 | TODO: Add AJAX Stuff for easy select a existing directory path. 23 | """ 24 | pass 25 | 26 | 27 | class DirectoryFormField(forms.CharField): 28 | def __init__(self, base_path=settings.MEDIA_ROOT, *args, **kwargs): 29 | super().__init__(*args, **kwargs) 30 | self.validators.append( 31 | validators.ExistingDirValidator(base_path=base_path) 32 | ) 33 | 34 | def clean(self, value): 35 | value = super().clean(value) 36 | value = os.path.normpath(value) 37 | return value 38 | 39 | 40 | class DirectoryModelField(models.CharField): # , with_metaclass(models.SubfieldBase)): 41 | """ 42 | >>> settings.DEBUG=False # Don't add path to error messages 43 | >>> dir = DirectoryModelField() 44 | >>> dir.run_validators("does/not/exist") 45 | Traceback (most recent call last): 46 | ... 47 | django.core.exceptions.ValidationError: ["Directory doesn't exist!"] 48 | 49 | >>> dir.run_validators("../") 50 | Traceback (most recent call last): 51 | ... 52 | django.core.exceptions.ValidationError: ['Directory is not in base path!'] 53 | 54 | >>> dir = DirectoryModelField(base_path="/") 55 | >>> dir.run_validators("/etc/default/") 56 | >>> dir.run_validators("var/log") 57 | 58 | >>> dir.run_validators("../bullshit") 59 | Traceback (most recent call last): 60 | ... 61 | django.core.exceptions.ValidationError: ["Directory doesn't exist!"] 62 | """ 63 | default_validators = [] 64 | description = _("A existing/accessible directory") 65 | 66 | def __init__(self, max_length=256, base_path=settings.MEDIA_ROOT, *args, **kwargs): 67 | super().__init__(*args, max_length=max_length, **kwargs) 68 | self.validators.append( 69 | validators.ExistingDirValidator(base_path=base_path) 70 | ) 71 | 72 | def formfield(self, **kwargs): 73 | """ Use always own widget and form field. """ 74 | kwargs["widget"] = DirectoryWidget 75 | kwargs['form_class'] = DirectoryFormField 76 | return super().formfield(**kwargs) 77 | -------------------------------------------------------------------------------- /django_tools/fields/language_code.py: -------------------------------------------------------------------------------- 1 | """ 2 | need full model and form fields 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | :copyleft: 2010-2016 by the django-tools team, see AUTHORS for more details. 6 | :license: GNU GPL v3 or above, see LICENSE for more details. 7 | """ 8 | 9 | 10 | from django import forms 11 | from django.db import models 12 | from django.utils.translation import gettext_lazy as _ 13 | 14 | from django_tools import validators 15 | 16 | 17 | class LanguageCodeFormField(forms.CharField): 18 | """ 19 | Language Code form field in Accept-Language header format (RFC 2616) 20 | 21 | >>> LanguageCodeFormField().clean('en') 22 | 'en' 23 | 24 | >>> LanguageCodeFormField().clean('en-GB') 25 | 'en-GB' 26 | 27 | >>> try: 28 | ... LanguageCodeFormField().clean("this is wrong") 29 | ... except Exception as err: 30 | ... print(err.__class__.__name__, err) 31 | ValidationError ['Enter a valid language code (Accept-Language header format, see RFC2616)'] 32 | 33 | >>> try: 34 | ... LanguageCodeFormField().clean(None) 35 | ... except Exception as err: 36 | ... print(err.__class__.__name__, err) 37 | ValidationError ['This field is required.'] 38 | 39 | >>> LanguageCodeFormField(required=False).clean(None) 40 | '' 41 | """ 42 | 43 | def __init__(self, *args, **kwargs): 44 | super().__init__(*args, **kwargs) 45 | self.validators.append(validators.validate_language_code) 46 | 47 | 48 | class LanguageCodeModelField(models.CharField): 49 | """ 50 | >>> LanguageCodeModelField(max_length=20).run_validators('en-GB') 51 | 52 | >>> try: 53 | ... LanguageCodeModelField(max_length=20).run_validators("this is wrong") 54 | ... except Exception as err: 55 | ... print(err.__class__.__name__, err) 56 | ValidationError ['Enter a valid language code (Accept-Language header format, see RFC2616)'] 57 | """ 58 | default_validators = [validators.validate_language_code] 59 | description = _("Language Code in Accept-Language header format defined in RFC 2616") 60 | -------------------------------------------------------------------------------- /django_tools/fields/media_path.py: -------------------------------------------------------------------------------- 1 | """ 2 | media path selection 3 | ~~~~~~~~~~~~~~~~~~~~ 4 | 5 | TODO: Made this generic and don't use settings.MEDIA_ROOT direct, let it 6 | be set as a argument to widget/fields etc. 7 | 8 | * model field 9 | * form field 10 | * widget 11 | 12 | INFO: This exist only for backward-compatibility and will be removed 13 | in the future. Please use static_path! 14 | 15 | :copyleft: 2010-2020 by the django-tools team, see AUTHORS for more details. 16 | :license: GNU GPL v3 or above, see LICENSE for more details. 17 | """ 18 | 19 | 20 | import os 21 | import warnings 22 | 23 | from django import forms 24 | from django.conf import settings 25 | from django.db import models 26 | 27 | from django_tools.utils.messages import failsafe_message 28 | 29 | 30 | def directory_walk(path): 31 | """ 32 | Directory tree generator 33 | similar to os.walk, except: 34 | - yield only directories 35 | - sorted list case-insensitive 36 | - follow links (os.walk can do this since python 2.6) 37 | """ 38 | dirs = [] 39 | for name in os.listdir(path): 40 | if os.path.isdir(os.path.join(path, name)): 41 | dirs.append(name) 42 | 43 | yield path 44 | 45 | # Sort case-insensitive 46 | dirs.sort(key=str.lower) 47 | 48 | for dir in dirs: 49 | sub_path = os.path.join(path, dir) 50 | yield from directory_walk(sub_path) 51 | 52 | 53 | class MediaPathWidget(forms.Select): 54 | """ 55 | Select a sub directory in settings.MEDIA_ROOT 56 | """ 57 | 58 | def __init__(self, attrs=None): 59 | super().__init__(attrs) 60 | 61 | self._base_path = os.path.abspath(os.path.normpath(settings.MEDIA_ROOT)) 62 | 63 | try: 64 | self.choices = self._get_path_choices() 65 | except OSError as err: 66 | self.choices = [] 67 | if settings.DEBUG: 68 | failsafe_message(f"Can't read MEDIA_ROOT: {err}") 69 | 70 | warnings.warn( 71 | "MediaPathWidget is deprecated and will removed in the future!" " Please use StaticPathWidget.", 72 | PendingDeprecationWarning, 73 | stacklevel=2, 74 | ) 75 | 76 | def _get_path_choices(self): 77 | media_dirs_choices = [] 78 | cut_pos = len(self._base_path) 79 | for root in directory_walk(self._base_path): 80 | rel_dir = root[cut_pos:].strip(os.sep) 81 | if rel_dir: 82 | media_dirs_choices.append((rel_dir, rel_dir)) 83 | return media_dirs_choices 84 | 85 | 86 | class MediaPathModelField(models.TextField): 87 | 88 | def formfield(self, **kwargs): 89 | """ Use always own widget and form field. """ 90 | 91 | warnings.warn( 92 | "MediaPathModelField is deprecated and will removed in the future!" " Please use StaticPathModelField.", 93 | PendingDeprecationWarning, 94 | stacklevel=2, 95 | ) 96 | 97 | kwargs["widget"] = MediaPathWidget 98 | # kwargs["form_class"] = SignSeparatedFormField 99 | return super().formfield(**kwargs) 100 | -------------------------------------------------------------------------------- /django_tools/fields/static_path.py: -------------------------------------------------------------------------------- 1 | """ 2 | static path selection 3 | ~~~~~~~~~~~~~~~~~~~~ 4 | 5 | TODO: Made this generic and don't use settings.STATIC_ROOT direct, let it 6 | be set as a argument to widget/fields etc. 7 | 8 | * model field 9 | * form field 10 | * widget 11 | 12 | :copyleft: 2012-2015 by the django-tools team, see AUTHORS for more details. 13 | :license: GNU GPL v3 or above, see LICENSE for more details. 14 | """ 15 | 16 | 17 | import os 18 | 19 | from django import forms 20 | from django.conf import settings 21 | from django.db import models 22 | 23 | from django_tools.utils.messages import failsafe_message 24 | 25 | 26 | def directory_walk(path): 27 | """ 28 | Directory tree generator 29 | similar to os.walk, except: 30 | - yield only directories 31 | - sorted list case-insensitive 32 | - follow links (os.walk can do this since python 2.6) 33 | """ 34 | dirs = [] 35 | for name in os.listdir(path): 36 | if os.path.isdir(os.path.join(path, name)): 37 | dirs.append(name) 38 | 39 | yield path 40 | 41 | # Sort case-insensitive 42 | dirs.sort(key=str.lower) 43 | 44 | for dir in dirs: 45 | sub_path = os.path.join(path, dir) 46 | yield from directory_walk(sub_path) 47 | 48 | 49 | class StaticPathWidget(forms.Select): 50 | """ 51 | Select a sub directory in settings.STATIC_ROOT 52 | 53 | >>> import django_tools 54 | >>> from pathlib import Path 55 | >>> settings.STATIC_ROOT = Path(django_tools.__file__).parent 56 | >>> StaticPathWidget().choices[:2] 57 | [('__pycache__', '__pycache__'), ('admin_tools', 'admin_tools')] 58 | """ 59 | 60 | def __init__(self, attrs=None): 61 | super().__init__(attrs) 62 | 63 | self._base_path = os.path.abspath(os.path.normpath(settings.STATIC_ROOT)) 64 | 65 | try: 66 | self.choices = self._get_path_choices() 67 | except OSError as err: 68 | self.choices = [] 69 | if settings.DEBUG: 70 | failsafe_message(f"Can't read STATIC_ROOT: {err}") 71 | 72 | def _get_path_choices(self): 73 | Static_dirs_choices = [] 74 | cut_pos = len(self._base_path) 75 | for root in directory_walk(self._base_path): 76 | rel_dir = root[cut_pos:].strip(os.sep) 77 | if rel_dir: 78 | Static_dirs_choices.append((rel_dir, rel_dir)) 79 | return Static_dirs_choices 80 | 81 | 82 | # @six.add_metaclass(models.SubfieldBase) 83 | class StaticPathModelField(models.TextField): 84 | """ 85 | Model field for select a sub directory in settings.STATIC_ROOT 86 | """ 87 | 88 | # def __init__(self, separator=",", strip_items=True, skip_empty=True, *args, **kwargs): 89 | # self.separator = separator 90 | # self.strip_items = strip_items 91 | # self.skip_empty = skip_empty 92 | # super(StaticPathModelField, self).__init__(*args, **kwargs) 93 | 94 | def formfield(self, **kwargs): 95 | """ Use always own widget and form field. """ 96 | kwargs["widget"] = StaticPathWidget 97 | # kwargs["form_class"] = SignSeparatedFormField 98 | return super().formfield(**kwargs) 99 | -------------------------------------------------------------------------------- /django_tools/file_storage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/file_storage/__init__.py -------------------------------------------------------------------------------- /django_tools/filemanager/README.creole: -------------------------------------------------------------------------------- 1 | == Filemanager library 2 | 3 | Library for building django application like filemanager, gallery etc. 4 | Provides some base classes to build your own views. 5 | 6 | There exists two base classes: 7 | 8 | **[[https://github.com/jedie/django-tools/blob/master/django_tools/filemanager/filesystem_browser.py|filesystem_browser.BaseFilesystemBrowser]]** 9 | * Base class for browse a specified sub directory 10 | * with a protection against [[https://en.wikipedia.org/wiki/Directory_traversal_attack|directory traversal attacks]]. 11 | 12 | **[[https://github.com/jedie/django-tools/blob/master/django_tools/filemanager/filemanager.py|filemanager.BaseFilemanager]]** 13 | * Base class for a app like a file manager 14 | * provides directory/file objects with many informations (e.g. to display in a django template): 15 | ** file path (absolute & relative) 16 | ** size 17 | ** file’s permission bits in different formats: 18 | *** native stat.ST_MODE 19 | *** octal notation 20 | *** symbol notation as string, e.g.: {{{u'rw-rw-r--'}}} 21 | ** modify datetime 22 | ** name & id of user and user group 23 | * base function to handle file uploads 24 | 25 | == examples 26 | 27 | * [[https://github.com/jedie/PyLucid/blob/master/pylucid_project/pylucid_plugins/filemanager/admin_views.py|static file manager in PyLucid]] 28 | * [[https://github.com/jedie/PyLucid/blob/master/pylucid_project/pylucid_plugins/gallery/views.py|picture gallery in PyLucid]] -------------------------------------------------------------------------------- /django_tools/filemanager/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/filemanager/__init__.py -------------------------------------------------------------------------------- /django_tools/filemanager/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | exceptions for filemanager 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | :copyleft: 2012 by the django-tools team, see AUTHORS for more details. 6 | :license: GNU GPL v3 or above, see LICENSE for more details. 7 | """ 8 | 9 | 10 | class FilemanagerError(Exception): 11 | """ 12 | for errors with a message to staff/admin users. 13 | e.g.: Gallery filesystem path doesn't exist anymore. 14 | """ 15 | pass 16 | 17 | 18 | class DirectoryTraversalAttack(FilemanagerError): 19 | """ 20 | Some unauthorized signs are found or the path is out of the base path. 21 | """ 22 | pass 23 | -------------------------------------------------------------------------------- /django_tools/fixture_tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/fixture_tools/__init__.py -------------------------------------------------------------------------------- /django_tools/fixture_tools/languages.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | from django.utils import translation 5 | 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | 10 | def iter_languages(languages=None): 11 | """ 12 | Iterate over all existing languages with activated translations. 13 | """ 14 | if languages is None: 15 | languages = settings.LANGUAGES 16 | 17 | for language_code, lang_name in languages: 18 | with translation.override(language_code): 19 | yield language_code, lang_name 20 | -------------------------------------------------------------------------------- /django_tools/forms_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | forms utils 3 | ~~~~~~~~~~~ 4 | 5 | Last commit info: 6 | ~~~~~~~~~~~~~~~~~ 7 | $LastChangedDate$ 8 | $Rev$ 9 | $Author:$ 10 | 11 | :copyleft: 2009 by the django-tools team, see AUTHORS for more details. 12 | :license: GNU GPL v3 or above, see LICENSE for more details. 13 | """ 14 | 15 | 16 | from django import forms 17 | 18 | 19 | class LimitManyToManyFields: 20 | """ 21 | Limit ManyToMany fields in forms. Hide the field, if only one item can be selected. 22 | 23 | e.g. limit sites choices only to accessible sites: 24 | -------------------------------------------------------------------------- 25 | class MyModel(models.Model): 26 | sites = models.ManyToManyField(Site) 27 | ... 28 | 29 | class UserProfile(models.Model): 30 | sites = models.ManyToManyField(Site) 31 | ... 32 | 33 | class MyForm(LimitManyToManyFields, forms.ModelForm): # <- Order is important ! 34 | class Meta: 35 | model = MyModel 36 | 37 | def my_view(request): 38 | user_profile = request.user.get_profile() 39 | 40 | m2m_limit = {"sites": user_profile.sites.values_list("id", "name")} 41 | 42 | if request.method == "POST": 43 | form = MyForm(m2m_limit, request.POST) 44 | if form.is_valid(): 45 | ... 46 | else: 47 | # Preselect all existing sites 48 | form = MyForm(m2m_limit) 49 | ... 50 | -------------------------------------------------------------------------- 51 | 52 | crosspost: http://www.djangosnippets.org/snippets/1692/ 53 | """ 54 | 55 | def __init__(self, m2m_limit, *args, **kwargs): 56 | """ 57 | preselect site select options. If user can only access one site or there exist only one site 58 | we remove the site field and insert the site info in save() method. 59 | """ 60 | assert isinstance(m2m_limit, dict), \ 61 | f"{self.__class__.__name__} error: first argument must be the m2m limit dict!" 62 | 63 | super().__init__(*args, **kwargs) 64 | 65 | for field_name, limits in m2m_limit.items(): 66 | if len(limits) == 1: 67 | value = int(limits[0][0]) 68 | # Only one item can be selected. Hide the ManyToMany field. To hide the field and 69 | # for validation, we changed the MultipleChoiceField to a IntegerField. 70 | self.fields[field_name] = forms.IntegerField( 71 | max_value=value, min_value=value, initial=value 72 | ) 73 | # self.fields[field_name].widget.input_type = 'hidden' 74 | # self.fields[field_name].widget.is_hidden = True 75 | else: 76 | # Limit the ManyToMany field choices 77 | self.fields[field_name].choices = limits 78 | -------------------------------------------------------------------------------- /django_tools/local_sync_cache/LocalSyncCacheMiddleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | Local sync cache Middleware 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | Calls check_state() in every existing LocalSyncCache instance. 6 | 7 | For more information look into DocString in local_sync_cache.py ! 8 | 9 | :copyleft: 2011 by the django-tools team, see AUTHORS for more details. 10 | :license: GNU GPL v3 or above, see LICENSE for more details. 11 | """ 12 | 13 | 14 | from django_tools.local_sync_cache.local_sync_cache import LocalSyncCache 15 | 16 | 17 | class LocalSyncCacheMiddleware: 18 | def process_request(self, request): 19 | for cache in LocalSyncCache.CACHES: 20 | cache.check_state() 21 | -------------------------------------------------------------------------------- /django_tools/local_sync_cache/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/local_sync_cache/__init__.py -------------------------------------------------------------------------------- /django_tools/log_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/log_utils/__init__.py -------------------------------------------------------------------------------- /django_tools/log_utils/syslog_handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import syslog 3 | 4 | 5 | class SyslogHandler(logging.Handler): 6 | """ 7 | Replacement for logging.handlers.SysLogHandler() 8 | Just use syslog.syslog() to emit a log message. 9 | """ 10 | 11 | LEVEL_MAP = { 12 | logging.CRITICAL: syslog.LOG_EMERG, 13 | logging.ERROR: syslog.LOG_ERR, 14 | logging.WARNING: syslog.LOG_WARNING, 15 | logging.INFO: syslog.LOG_INFO, 16 | logging.DEBUG: syslog.LOG_DEBUG, 17 | logging.NOTSET: 'NOTSET', 18 | } 19 | 20 | def emit(self, record: logging.LogRecord): 21 | if record.levelno == logging.NOTSET: 22 | return 23 | 24 | try: 25 | syslog_priority = self.LEVEL_MAP[record.levelno] 26 | except KeyError: 27 | syslog.syslog( 28 | syslog.LOG_ERR, 29 | f'Log level {record.levelno!r} ({record.levelname}) is unknown, fallback to WARNING.', 30 | ) 31 | syslog_priority = syslog.LOG_WARNING 32 | 33 | message = record.getMessage() 34 | syslog.syslog(syslog_priority, message) 35 | -------------------------------------------------------------------------------- /django_tools/log_utils/throttle_admin_email_handler.py: -------------------------------------------------------------------------------- 1 | """ 2 | ThrottledAdminEmailHandler 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | created 25.09.2019 by Jens Diemer 6 | :copyleft: 2019 by the django-tools team, see AUTHORS for more details. 7 | :license: GNU GPL v3 or above, see LICENSE for more details. 8 | """ 9 | import logging 10 | import time 11 | 12 | from django.core import mail 13 | from django.utils.log import AdminEmailHandler 14 | 15 | 16 | log = logging.getLogger(__name__) 17 | 18 | 19 | class ThrottledAdminEmailHandler(AdminEmailHandler): 20 | """ 21 | Throttled email handler. 22 | 23 | Works similar as the origin django.utils.log.AdminEmailHandler 24 | But mails send in >min_delay_sec< time range will be skiped. 25 | The mail subject of skipped mails will be added to the next error mails. 26 | 27 | Note: Currently "include_html" is not supported and will be deactivated! 28 | 29 | usage, e.g.: 30 | 31 | LOGGING = { 32 | # ... 33 | "handlers": { 34 | "mail_admins": { 35 | "level": "ERROR", 36 | "class": "django_tools.log_utils.throttle_admin_email_handler.ThrottledAdminEmailHandler", 37 | "formatter": "email", 38 | "min_delay_sec": 20, # << -- skip mails in this time range 39 | }, 40 | # ... 41 | }, 42 | # ... 43 | } 44 | """ 45 | 46 | def __init__(self, *args, min_delay_sec=30, **kwargs): 47 | super().__init__(*args, **kwargs) 48 | 49 | self.include_html = False # TODO: support html mails, too 50 | 51 | self.min_delay_sec = min_delay_sec 52 | self.next_mail = None 53 | self.skipped_subjects = [] 54 | 55 | def send_mail(self, subject, message, *args, **kwargs): 56 | if self.next_mail: 57 | if self.next_mail > time.time(): 58 | log.debug("Throttle error mail: %r", subject) 59 | self.skipped_subjects.append(subject) 60 | return 61 | 62 | if self.skipped_subjects: 63 | prefix = f"\nNote: there are {len(self.skipped_subjects)} skipped mails:\n" 64 | prefix += "\n\t* " + "\n\t* ".join(self.skipped_subjects) 65 | prefix += "\n\n" 66 | message = prefix + message 67 | self.skipped_subjects.clear() 68 | 69 | log.debug("Send mail: %r", subject) 70 | 71 | mail.mail_admins(subject, message, *args, connection=self.connection(), **kwargs) 72 | 73 | self.next_mail = time.time() + self.min_delay_sec 74 | -------------------------------------------------------------------------------- /django_tools/mail/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/mail/__init__.py -------------------------------------------------------------------------------- /django_tools/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/management/__init__.py -------------------------------------------------------------------------------- /django_tools/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/management/commands/__init__.py -------------------------------------------------------------------------------- /django_tools/management/commands/clear_cache.py: -------------------------------------------------------------------------------- 1 | """ 2 | 'clear django cache' manage command 3 | 4 | 5 | setup: 6 | 7 | INSTALLED_APPS = [ 8 | ... 9 | 'django_tools', 10 | ... 11 | ] 12 | 13 | 14 | usage: 15 | 16 | $ ./manage.py clear_cache 17 | 18 | 19 | :copyleft: 2017 by the django-tools team, see AUTHORS for more details. 20 | :created: 2017 by Jens Diemer 21 | :license: GNU GPL v3 or above, see LICENSE for more details. 22 | """ 23 | 24 | from django.core.cache import caches 25 | from django.core.management.base import BaseCommand 26 | 27 | 28 | class Command(BaseCommand): 29 | help = "Clears the complete Django cache." 30 | 31 | def handle(self, *args, **options): 32 | self.stdout.write("\nClear caches:\n") 33 | for cache in caches.all(): 34 | self.stdout.write(f"\tClear '{cache.__class__.__name__}'\n") 35 | cache.clear() 36 | 37 | self.stdout.write("\ndone.\n") 38 | -------------------------------------------------------------------------------- /django_tools/management/commands/database_info.py: -------------------------------------------------------------------------------- 1 | """ 2 | database_info manage command 3 | 4 | 5 | setup: 6 | 7 | INSTALLED_APPS = [ 8 | ... 9 | 'django_tools', 10 | ... 11 | ] 12 | 13 | 14 | usage: 15 | 16 | $ ./manage.py database_info 17 | 18 | 19 | :copyleft: 2017 by the django-tools team, see AUTHORS for more details. 20 | :created: 2017 by Jens Diemer 21 | :license: GNU GPL v3 or above, see LICENSE for more details. 22 | """ 23 | 24 | from pprint import pformat 25 | 26 | from django.conf import settings 27 | from django.core.management.base import BaseCommand 28 | from django.db import connections 29 | 30 | 31 | class Command(BaseCommand): 32 | help = "Information about the used database and connections" 33 | 34 | def handle(self, *args, **options): 35 | self.stdout.write("") 36 | self.stdout.write("_" * 79) 37 | self.stdout.write(self.help) 38 | self.stdout.write("") 39 | 40 | # self.stdout.write("settings.DATABASES = ", ending="") 41 | # pprint(settings.DATABASES) 42 | # self.stdout.write("-"*79) 43 | 44 | for alias, settings_dict in settings.DATABASES.items(): 45 | self.stdout.write(f"Database alias {alias!r}:") 46 | self.stdout.write("") 47 | engine = settings_dict["ENGINE"] 48 | engine = engine.rsplit(".", 1)[-1] 49 | self.stdout.write(f"engine...............: {engine!r}") 50 | if engine == "sqlite3": 51 | # https://docs.python.org/3/library/sqlite3.html#module-functions-and-constants 52 | import sqlite3 53 | 54 | self.stdout.write(f"sqlite version.......: {sqlite3.sqlite_version!r}") 55 | 56 | self.stdout.write(f"name.................: {settings_dict['NAME']!r}") 57 | self.stdout.write(f"user.................: {settings_dict['USER']!r}") 58 | self.stdout.write(f"host.................: {settings_dict['HOST']!r}") 59 | self.stdout.write(f"port.................: {settings_dict['PORT']!r}") 60 | 61 | connection_list = connections.all() 62 | 63 | self.stdout.write("") 64 | self.stdout.write(f"There are {len(connection_list)} connections.") 65 | 66 | for no, conn in enumerate(connection_list, 1): 67 | self.stdout.write("") 68 | self.stdout.write(f"connection {no:d} alias: {conn.alias!r} settings_dict:") 69 | self.stdout.write(pformat(conn.settings_dict)) 70 | 71 | self.stdout.write("") 72 | self.stdout.write("-" * 79) 73 | -------------------------------------------------------------------------------- /django_tools/management/commands/generate_model_test_code.py: -------------------------------------------------------------------------------- 1 | """ 2 | :created: 24.04.2018 by Jens Diemer 3 | :copyleft: 2018 by the django-tools team, see AUTHORS for more details. 4 | :license: GNU GPL v3 or above, see LICENSE for more details. 5 | """ 6 | 7 | from django.core.management import BaseCommand 8 | from django.utils import translation 9 | 10 | # https://github.com/jedie/django-tools 11 | from django_tools.unittest_utils.model_test_code_generator import ModelTestGenerator 12 | 13 | 14 | class Command(BaseCommand): 15 | """ 16 | List all models, e.g.: 17 | $ ./manage.py generate_model_test_code 18 | 19 | Generate test code, e.g.: 20 | $ ./manage.py generate_model_test_code auth 21 | $ ./manage.py generate_model_test_code auth.User 22 | $ ./manage.py generate_model_test_code cms. 23 | 24 | """ 25 | help = "Generate unittest code for a model" 26 | 27 | def add_arguments(self, parser): 28 | parser.add_argument('model_label', nargs="?", 29 | help='Name of the Django model (can only be the first characters! We use "startwith"' 30 | ) 31 | parser.add_argument('--translation', help='Language code that will be activated', default="en") 32 | parser.add_argument('--count', help='Number of data records to be generated.', type=int, default=2) 33 | 34 | def handle(self, *args, **options): 35 | model_label = options['model_label'] 36 | language_code = options["translation"] 37 | count = int(options["count"]) 38 | 39 | translation.activate(language_code) 40 | 41 | model_test_generator = ModelTestGenerator() 42 | 43 | if model_label is None: 44 | print("\nNo model_label given.\n") 45 | model_test_generator.print_all_plugins() 46 | return 47 | 48 | models = model_test_generator.get_models_startwith_label(model_label) 49 | if not models: 50 | print(f"\nERROR: No models starts with given label {model_label!r}\n") 51 | model_test_generator.print_all_plugins() 52 | return 53 | 54 | for model in models: 55 | queryset = model.objects.all() 56 | 57 | existing_count = queryset.count() 58 | if existing_count == 0: 59 | print("#") 60 | print(f"# ERROR: {model._meta.label!r} is empty!") 61 | print("#") 62 | continue 63 | 64 | if existing_count < count: 65 | print("#") 66 | print(f"# Warning: They exists only {existing_count:d} items in {model._meta.label!r}!") 67 | print("#") 68 | 69 | queryset = queryset[:count] 70 | 71 | content = model_test_generator.from_queryset(queryset) 72 | print(content) 73 | -------------------------------------------------------------------------------- /django_tools/management/commands/list_models.py: -------------------------------------------------------------------------------- 1 | """ 2 | $ ./manage.py list_models 3 | 4 | """ 5 | 6 | 7 | from django.apps import apps 8 | from django.conf import settings 9 | from django.core.management.base import BaseCommand 10 | 11 | 12 | class Command(BaseCommand): 13 | help = 'Just list all existing models in app_label.ModelName format.' 14 | 15 | def handle(self, *args, **options): 16 | self.stdout.write("\nexisting models in app_label.ModelName format:\n\n") 17 | 18 | dotnames = [] 19 | app_configs = apps.get_app_configs() 20 | for app_config in app_configs: 21 | app_label = app_config.label 22 | models = app_config.get_models() 23 | for model in models: 24 | dotnames.append( 25 | f"{app_label}.{model._meta.object_name}" 26 | ) 27 | 28 | for no, dotname in enumerate(sorted(dotnames), 1): 29 | self.stdout.write(f"{no:02d} - {dotname}\n") 30 | 31 | self.stdout.write(f"\nINSTALLED_APPS....: {len(settings.INSTALLED_APPS)}\n") 32 | self.stdout.write(f"Apps with models..: {len(app_configs)}\n\n") 33 | -------------------------------------------------------------------------------- /django_tools/management/commands/logging_info.py: -------------------------------------------------------------------------------- 1 | """ 2 | Display information about the logging setup 3 | 4 | created 06.09.2018 by Jens Diemer 5 | :copyleft: 2018 by the django-tools team, see AUTHORS for more details. 6 | :license: GNU GPL v3 or above, see LICENSE for more details. 7 | """ 8 | 9 | import logging 10 | from pprint import pprint 11 | 12 | from django.conf import settings 13 | from django.core.management.base import BaseCommand 14 | 15 | 16 | log = logging.getLogger(__name__) 17 | 18 | 19 | class Command(BaseCommand): 20 | """ 21 | call via: 22 | $ ./manage.py logging_info 23 | """ 24 | help = "Shows a list of all loggers and marks which ones are configured in settings.LOGGING" 25 | 26 | def handle(self, *args, **options): 27 | self.stdout.write("") 28 | self.stdout.write("_" * 79) 29 | self.stdout.write(self.help) 30 | self.stdout.write("") 31 | 32 | self.stdout.write("-" * 100) 33 | pprint(settings.LOGGING) 34 | self.stdout.write("-" * 100) 35 | 36 | for log_name in sorted(logging.Logger.manager.loggerDict.keys()): 37 | 38 | if log_name in settings.LOGGING["loggers"]: 39 | prefix = "\t[*]" 40 | else: 41 | prefix = "\t[ ]" 42 | 43 | self.stdout.write(f"{prefix}{log_name!r}") 44 | 45 | self.stdout.write('[ ] -> not configured in settings.LOGGING["loggers"]') 46 | -------------------------------------------------------------------------------- /django_tools/management/commands/nice_diffsettings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pretty version of Django's "diffsettings" using rich 3 | 4 | $ ./manage.py nice_diffsettings 5 | 6 | The code based on: 7 | django.core.management.commands.diffsettings 8 | """ 9 | 10 | 11 | from django_rich.management import RichCommand 12 | from rich.highlighter import ReprHighlighter 13 | from rich.pretty import pretty_repr 14 | 15 | 16 | def module_to_dict(module, omittable=lambda k: k.startswith("_")): 17 | """ 18 | Same as django.core.management.commands.diffsettings.module_to_dict 19 | but didn't use repr() ;) 20 | """ 21 | return {k: v for k, v in module.__dict__.items() if not omittable(k)} 22 | 23 | 24 | class Command(RichCommand): 25 | help = """Displays differences between the current settings.py and Django's 26 | default settings in a pretty-printed representation.""" 27 | 28 | requires_system_checks = [] 29 | 30 | def add_arguments(self, parser): 31 | parser.add_argument( 32 | "--all", 33 | action="store_true", 34 | dest="all", 35 | default=False, 36 | help="Display all settings, regardless of their value.", 37 | ) 38 | 39 | def handle(self, **options): 40 | from django.conf import global_settings, settings 41 | 42 | # Because settings are imported lazily, we need to explicitly load them. 43 | if not settings.configured: 44 | settings._setup() 45 | 46 | user_settings = module_to_dict(settings._wrapped) 47 | default_settings = module_to_dict(global_settings) 48 | 49 | highlighter = ReprHighlighter() 50 | 51 | self.console.print("-" * 79) 52 | 53 | for key in sorted(user_settings): 54 | display = False 55 | if key not in default_settings: 56 | display = True 57 | elif user_settings[key] != default_settings[key]: 58 | display = True 59 | elif options["all"]: 60 | display = True 61 | 62 | if display: 63 | 64 | value = user_settings[key] 65 | try: 66 | pformated = pretty_repr(value, expand_all=True) 67 | pformated = highlighter(pformated) 68 | except Exception as err: 69 | # e.g.: https://github.com/andymccurdy/redis-py/issues/995 70 | pformated = f"" 71 | 72 | self.console.print(f"{key} = {pformated}\n\n") 73 | 74 | self.console.print("-" * 79) 75 | -------------------------------------------------------------------------------- /django_tools/management/commands/update_permissions.py: -------------------------------------------------------------------------------- 1 | """ 2 | update_permissions manage command 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | 6 | setup: 7 | 8 | INSTALLED_APPS = [ 9 | ... 10 | 'django_tools', 11 | ... 12 | ] 13 | 14 | 15 | usage: 16 | 17 | $ ./manage.py update_permissions 18 | 19 | 20 | :copyleft: 2017 by the django-tools team, see AUTHORS for more details. 21 | :created: 2017 by Jens Diemer 22 | :license: GNU GPL v3 or above, see LICENSE for more details. 23 | """ 24 | 25 | 26 | from django.apps import apps 27 | from django.contrib.auth.management import create_permissions 28 | from django.core.management.base import BaseCommand 29 | 30 | 31 | class Command(BaseCommand): 32 | help = "Create missing permissions for all app models." 33 | 34 | def handle(self, *args, **options): 35 | self.stdout.write("Create permissions for:") 36 | 37 | verbosity = int(options.get('verbosity', 0)) 38 | app_configs = apps.get_app_configs() 39 | for app_config in app_configs: 40 | app_label = app_config.label 41 | self.stdout.write(f" * {app_label}") 42 | create_permissions(app_config, verbosity=verbosity) 43 | -------------------------------------------------------------------------------- /django_tools/middlewares/LogHeaders.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | log = logging.getLogger(__name__) 5 | 6 | 7 | class LogRequestHeadersMiddleware: 8 | def __init__(self, get_response): 9 | self.get_response = get_response 10 | 11 | def __call__(self, request): 12 | for key, value in request.environ.items(): 13 | if key.startswith('HTTP_'): 14 | log.info(f'{key[5:].lower():<25}: {value!r}') 15 | 16 | return self.get_response(request) 17 | -------------------------------------------------------------------------------- /django_tools/middlewares/QueryLogMiddleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | Print the query log to standard out. 3 | 4 | Useful for optimizing database calls. 5 | 6 | Inspired by the method at: 7 | """ 8 | try: 9 | from django.utils.deprecation import MiddlewareMixin 10 | except ImportError: 11 | MiddlewareMixin = object # fallback for Django < 1.10 12 | 13 | 14 | class QueryLogMiddleware(MiddlewareMixin): 15 | 16 | def process_response(self, request, response): 17 | from django.conf import settings 18 | from django.db import connection 19 | 20 | if settings.DEBUG: 21 | queries = {} 22 | for query in connection.queries: 23 | sql = query["sql"] 24 | queries.setdefault(sql, 0) 25 | queries[sql] += 1 26 | duplicates = sum(count - 1 for count in list(queries.values())) 27 | print("------------------------------------------------------") 28 | print(f"Total Queries: {len(queries)}") 29 | print(f"Duplicate Queries: {duplicates}") 30 | print() 31 | for query, count in list(queries.items()): 32 | print(f"{count} x {query}") 33 | print("------------------------------------------------------") 34 | return response 35 | -------------------------------------------------------------------------------- /django_tools/middlewares/SlowerDevServer.py: -------------------------------------------------------------------------------- 1 | """ 2 | SlowerDevServer 3 | ~~~~~~~~~~~~~~~ 4 | 5 | Simple slow down the django developer server. 6 | The middleware insert in every 200 response a time.sleep 7 | 8 | Put this into your settings: 9 | -------------------------------------------------------------------------- 10 | MIDDLEWARE_CLASSES = ( 11 | ... 12 | 'django_tools.middlewares.SlowerDevServer.SlowerDevServerMiddleware', 13 | ... 14 | ) 15 | SLOWER_DEV_SERVER_SLEEP = 0.3 # time.sleep() value (in sec.) 16 | -------------------------------------------------------------------------- 17 | 18 | Last commit info: 19 | ~~~~~~~~~~~~~~~~~ 20 | $LastChangedDate$ 21 | $Rev$ 22 | $Author:$ 23 | 24 | :copyleft: 2009 by the django-tools team, see AUTHORS for more details. 25 | :license: GNU GPL v3 or above, see LICENSE for more details. 26 | """ 27 | 28 | 29 | import time 30 | import warnings 31 | 32 | from django.conf import settings 33 | 34 | 35 | try: 36 | from django.utils.deprecation import MiddlewareMixin 37 | except ImportError: 38 | MiddlewareMixin = object # fallback for Django < 1.10 39 | 40 | 41 | class SlowerDevServerMiddleware(MiddlewareMixin): 42 | def __init__(self): 43 | warnings.warn("Slower developer server used!", stacklevel=2) 44 | 45 | def process_response(self, request, response): 46 | if response.status_code == 200: 47 | print(f"SlowerDevServerMiddleware: Wait for {settings.SLOWER_DEV_SERVER_SLEEP}Sec...") 48 | time.sleep(settings.SLOWER_DEV_SERVER_SLEEP) 49 | return response 50 | -------------------------------------------------------------------------------- /django_tools/middlewares/ThreadLocal.py: -------------------------------------------------------------------------------- 1 | """ 2 | threadlocals middleware 3 | ~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | make the request object everywhere available (e.g. in model instance). 6 | 7 | based on: http://code.djangoproject.com/wiki/CookBookThreadlocalsAndUser 8 | 9 | Put this into your settings: 10 | -------------------------------------------------------------------------- 11 | MIDDLEWARE_CLASSES = ( 12 | ... 13 | 'django_tools.middlewares.ThreadLocal.ThreadLocalMiddleware', 14 | ... 15 | ) 16 | -------------------------------------------------------------------------- 17 | 18 | 19 | Usage: 20 | -------------------------------------------------------------------------- 21 | from django_tools.middlewares import ThreadLocal 22 | 23 | # Get the current request object: 24 | request = ThreadLocal.get_current_request() 25 | 26 | # You can get the current user directly with: 27 | user = ThreadLocal.get_current_user() 28 | -------------------------------------------------------------------------- 29 | 30 | :copyleft: 2009-2017 by the django-tools team, see AUTHORS for more details. 31 | :license: GNU GPL v3 or above, see LICENSE for more details. 32 | """ 33 | 34 | 35 | try: 36 | from threading import local 37 | except ImportError: 38 | from django.utils._threading_local import local 39 | 40 | try: 41 | from django.utils.deprecation import MiddlewareMixin 42 | except ImportError: 43 | MiddlewareMixin = object # fallback for Django < 1.10 44 | 45 | 46 | _thread_locals = local() 47 | 48 | 49 | def get_current_request(): 50 | """ returns the request object for this thread """ 51 | return getattr(_thread_locals, "request", None) 52 | 53 | 54 | def get_current_user(): 55 | """ returns the current user, if exist, otherwise returns None """ 56 | request = get_current_request() 57 | if request: 58 | return getattr(request, "user", None) 59 | 60 | 61 | class ThreadLocalMiddleware(MiddlewareMixin): 62 | """ Simple middleware that adds the request object in thread local storage.""" 63 | 64 | def process_request(self, request): 65 | _thread_locals.request = request 66 | 67 | def process_response(self, request, response): 68 | if hasattr(_thread_locals, 'request'): 69 | del _thread_locals.request 70 | return response 71 | 72 | def process_exception(self, request, exception): 73 | if hasattr(_thread_locals, 'request'): 74 | del _thread_locals.request 75 | -------------------------------------------------------------------------------- /django_tools/middlewares/TracebackLogMiddleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | Put this into your settings: 3 | -------------------------------------------------------------------------- 4 | MIDDLEWARE_CLASSES = ( 5 | ... 6 | 'django_tools.middlewares.TracebackLogMiddleware.TracebackLogMiddleware', 7 | ... 8 | ) 9 | -------------------------------------------------------------------------- 10 | 11 | :copyleft: 2016 by the django-tools team, see AUTHORS for more details. 12 | :license: GNU GPL v3 or above, see LICENSE for more details. 13 | """ 14 | 15 | 16 | import logging 17 | 18 | 19 | try: 20 | from django.utils.deprecation import MiddlewareMixin 21 | except ImportError: 22 | MiddlewareMixin = object # fallback for Django < 1.10 23 | 24 | 25 | class TracebackLogMiddleware(MiddlewareMixin): 26 | 27 | def process_exception(self, request, exception): 28 | logging.exception(f'Exception on url: {request.path}') 29 | -------------------------------------------------------------------------------- /django_tools/middlewares/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/middlewares/__init__.py -------------------------------------------------------------------------------- /django_tools/middlewares/local_auto_login.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from django.conf import settings 5 | from django.contrib import messages 6 | from django.contrib.auth import authenticate, get_user_model, login 7 | 8 | 9 | User = get_user_model() 10 | 11 | 12 | # Used values if not defined in settings: 13 | DEFAULT_USERNAME = 'local-test-superuser' 14 | DEFAULT_USERPASS = 'This is no secret!' 15 | DEFAULT_USEREMAIL = 'nobody@local.intranet' 16 | ADMIN_URL_PREFIX = '/admin/' 17 | 18 | 19 | class AlwaysLoggedInAsSuperUserMiddleware: 20 | """ 21 | Auto login all users as default superuser. ***** Never use this in production !!! ***** 22 | Default user will be created, if not exist. 23 | 24 | Can be disabled by deactivate the default user. 25 | 26 | WARNING: Include this middleware only in your "local" settings! 27 | """ 28 | 29 | def __init__(self, get_response): 30 | assert 'RUN_MAIN' in os.environ, 'Only allowed running by Django dev. server !' 31 | self.get_response = get_response 32 | 33 | def __call__(self, request): 34 | self._auto_login(request) 35 | response = self.get_response(request) 36 | 37 | return response 38 | 39 | def _print_and_message(self, request, msg, level=messages.WARNING): 40 | print(f' *** {msg} ***', file=sys.stderr) 41 | messages.add_message(request, level, msg) 42 | 43 | def _auto_login(self, request): 44 | if request.user.is_authenticated: 45 | return 46 | 47 | # Restrict auto login for the admin: 48 | admin_url_prefix = getattr(settings, 'ADMIN_URL_PREFIX', ADMIN_URL_PREFIX) 49 | assert admin_url_prefix.startswith('/') 50 | if not request.path.startswith(admin_url_prefix): 51 | return 52 | 53 | username = getattr(settings, 'DEFAULT_USERNAME', DEFAULT_USERNAME) 54 | email = getattr(settings, 'DEFAULT_USEREMAIL', DEFAULT_USEREMAIL) 55 | password = getattr(settings, 'DEFAULT_USERPASS', DEFAULT_USERPASS) 56 | 57 | user = self._get_or_create_user(request, username, email, password) 58 | if user: 59 | self._print_and_message(request, f'Autologin applied. Your logged in as {username!r}') 60 | user = authenticate(request=request, username=username, password=password) 61 | login(request, user) 62 | 63 | def _get_or_create_user(self, request, username, email, password): 64 | try: 65 | user = User.objects.get(username=username) 66 | except User.DoesNotExist: 67 | self._print_and_message(request, f'Create test django user: {username!r}') 68 | user = User.objects.create_superuser( 69 | username=username, 70 | email=email, 71 | password=password, 72 | ) 73 | else: 74 | if not user.is_active: 75 | self._print_and_message( 76 | request, 'Default User was deactivated!', level=messages.ERROR 77 | ) 78 | return None 79 | 80 | user.set_password(password) 81 | user.save() 82 | return user 83 | -------------------------------------------------------------------------------- /django_tools/model_version_protect/README.md: -------------------------------------------------------------------------------- 1 | # Django-Tools - Mode Version Protect 2 | 3 | Protect a model against overwriting a newer entry with an older one, by adding a auto increment version number. 4 | 5 | ## Problem 6 | 7 | If you work in Django Admin with more than one Browser Tabs, 8 | then it can happen that the user loses the overview and has the same object open in two tabs. 9 | Maybe one tab contains a older version than the current version in database. 10 | 11 | Normally the user is not protect to overwrite a newer version with the old one, because 12 | he an Django can't see the state of the model. 13 | 14 | ## Solution 15 | 16 | `VersionProtectBaseModel` model will add a auto increment `version` number to every instance 17 | and raise `ValidationError` in `full_clean()` if a older version should be saved. 18 | 19 | Note: `VersionProtectBaseModel` will call `full_clean()` in `save()` if not done before! 20 | 21 | ## Usage 22 | 23 | change settings: 24 | 25 | ``` 26 | INSTALLED_APPS = ( 27 | ... 28 | 'django_tools.model_version_protect.apps.ModelVersionProtectConfig', 29 | ... 30 | ) 31 | ``` 32 | 33 | Use it as model mixin, e.g.: 34 | ``` 35 | class FooBarModel(VersionProtectBaseModel): 36 | name = models.CharField(max_length=100) 37 | ``` 38 | 39 | ## Examples 40 | 41 | Test project model `VersioningTestModel` is here: django_tools_project/django_tools_test_app/models.py 42 | Tests are here: django_tools/model_version_protect/tests/*.py 43 | 44 | How a admin overwrite error looks, can you see here: django_tools/model_version_protect/tests/test_admin_basic_2.snapshot.html 45 | -------------------------------------------------------------------------------- /django_tools/model_version_protect/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/model_version_protect/__init__.py -------------------------------------------------------------------------------- /django_tools/model_version_protect/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ModelVersionProtectConfig(AppConfig): 5 | name = 'django_tools.model_version_protect' 6 | verbose_name = ( 7 | 'Protect overwriting model instance with a older entry' 8 | ' by auto increment a version number' 9 | ) 10 | -------------------------------------------------------------------------------- /django_tools/model_version_protect/templates/model_version_protect/version_widget.html: -------------------------------------------------------------------------------- 1 | 2 | {{ widget.value|default_if_none:'(new-version)' }} -------------------------------------------------------------------------------- /django_tools/model_version_protect/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/model_version_protect/tests/__init__.py -------------------------------------------------------------------------------- /django_tools/model_version_protect/tests/utils.py: -------------------------------------------------------------------------------- 1 | def shorten_logs(logs): 2 | return [ 3 | log.replace(':django_tools.model_version_protect.models:', ': ') 4 | for log in logs 5 | ] 6 | -------------------------------------------------------------------------------- /django_tools/parler_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/parler_utils/__init__.py -------------------------------------------------------------------------------- /django_tools/serve_media_app/README.md: -------------------------------------------------------------------------------- 1 | # Django-Tools - Serve User Media File 2 | 3 | The `django_tools.serve_media_app` django app serves `settings.MEDIA_ROOT` file requests only for allowed users. So you can limit the access to these files. 4 | 5 | The idea is, that `settings.STATIC_ROOT` is for public files (css, js etc) and should be served by the web server. 6 | `settings.MEDIA_ROOT` is not public and served by the django application to restrict the access to active user session. 7 | 8 | Files are stored in these file path: 9 | 10 | ``` 11 | /{settings.MEDIA_ROOT}/{random-user-token}/{random-file-token}/ 12 | ``` 13 | 14 | Example: 15 | 16 | ``` 17 | /media/w7tytv3lyupc/fm4kf9hp0c_vfx3onskrffw1/filename.ext 18 | ``` 19 | 20 | The `{random-user-token}` will be created automatically via signals and stored into `django_tools.serve_media_app.models.UserMediaTokenModel` to identify the owner of the requested file. 21 | 22 | 23 | Limitations: 24 | 25 | * Works currently only for `FileSystemStorage` 26 | * Dosen't support faster `sendfile` solution 27 | 28 | The files served by streaming HTTP response. But serve files from the application is slow. 29 | It's faster to use `sendfile` from the web server. 30 | Maybe use https://pypi.org/project/django-sendfile2/ 31 | 32 | 33 | ## usage 34 | 35 | 36 | settings: 37 | ``` 38 | INSTALLED_APPS = ( 39 | # ... 40 | 'django_tools.serve_media_app.apps.UserMediaFilesConfig', 41 | # ... 42 | ) 43 | ``` 44 | 45 | 46 | urls: 47 | ``` 48 | urlpatterns = [ 49 | # ... 50 | path(settings.MEDIA_URL.lstrip('/'), include('django_tools.serve_media_app.urls')), 51 | # ... 52 | ] 53 | ``` 54 | (See: `django_tools_project/urls.py`) 55 | 56 | 57 | You can use it in models like this (optional): 58 | ``` 59 | from django_tools.serve_media_app.models import user_directory_path 60 | 61 | class ExampleModel(models.Model): 62 | user = models.ForeignKey( # "Owner" of this entry, field name must be "user" ! 63 | settings.AUTH_USER_MODEL, 64 | related_name='+', 65 | on_delete=models.CASCADE, 66 | ) 67 | # e.g.: 68 | foo = models.FileField(upload_to=user_directory_path) 69 | bar = models.ImageField(upload_to=user_directory_path) 70 | ``` 71 | (see: ```django_tools_project.django_tools_test_app.models.UserMediaFiles```) 72 | 73 | Note: The model will not checked in file request! 74 | You can add own access checks by signals, e.g.: 75 | ``` 76 | from django_tools.serve_media_app.views.serve_user_files import serve_file_request 77 | 78 | def access_callback(user, path, media_path, **kwargs): 79 | # ... check the access ... 80 | if not access: 81 | raise PermissionDenied 82 | 83 | serve_file_request.connect(access_callback) 84 | ``` 85 | 86 | 87 | If you manually save files for users, do this: 88 | ``` 89 | from django_tools.serve_media_app.models import generate_media_path 90 | 91 | file_path = generate_media_path(user, filename='foobar.txt') 92 | with open(file_path, 'wb') as f: 93 | f.write(...something...) 94 | ``` 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /django_tools/serve_media_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/serve_media_app/__init__.py -------------------------------------------------------------------------------- /django_tools/serve_media_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UserMediaFilesConfig(AppConfig): 5 | name = 'django_tools.serve_media_app' 6 | verbose_name = "Serve Media Files" 7 | -------------------------------------------------------------------------------- /django_tools/serve_media_app/constants.py: -------------------------------------------------------------------------------- 1 | USER_TOKEN_LENGTH = 12 2 | PATH_TOKEN_LENGTH = 24 3 | -------------------------------------------------------------------------------- /django_tools/serve_media_app/exceptions.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.core.exceptions import SuspiciousOperation 4 | 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class NoUserToken(SuspiciousOperation): 10 | def __init__(self, *, user): 11 | logger.error('Current user "%s" has no token!', user) 12 | -------------------------------------------------------------------------------- /django_tools/serve_media_app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.4 on 2020-12-06 08:23 2 | import django.db.models.deletion 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | 6 | import django_tools.serve_media_app.models 7 | 8 | 9 | def forward_code(apps, schema_editor): 10 | app_name, model_name = settings.AUTH_USER_MODEL.split('.') 11 | User = apps.get_model(app_name, model_name) 12 | UserMediaTokenModel = apps.get_model('serve_media_app', 'UserMediaTokenModel') 13 | for user in User.objects.all(): 14 | UserMediaTokenModel.objects.get_or_create(user_id=user.pk) 15 | 16 | 17 | class Migration(migrations.Migration): 18 | 19 | initial = True 20 | 21 | dependencies = [ 22 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 23 | ] 24 | 25 | operations = [ 26 | migrations.CreateModel( 27 | name='UserMediaTokenModel', 28 | fields=[ 29 | ('id', 30 | models.AutoField( 31 | auto_created=True, 32 | primary_key=True, 33 | serialize=False, 34 | verbose_name='ID')), 35 | ('token', 36 | models.CharField( 37 | default=django_tools.serve_media_app.models.generate_token, 38 | max_length=12, 39 | unique=True)), 40 | ('user', 41 | models.ForeignKey( 42 | editable=False, 43 | on_delete=django.db.models.deletion.CASCADE, 44 | related_name='token', 45 | to=settings.AUTH_USER_MODEL)), 46 | ], 47 | ), 48 | migrations.RunPython( 49 | forward_code, 50 | reverse_code=migrations.RunPython.noop), 51 | ] 52 | -------------------------------------------------------------------------------- /django_tools/serve_media_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/serve_media_app/migrations/__init__.py -------------------------------------------------------------------------------- /django_tools/serve_media_app/models.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | from django.contrib.auth import get_user_model 5 | from django.db import models 6 | from django.db.models.signals import post_save 7 | from django.dispatch import receiver 8 | 9 | from django_tools.serve_media_app.constants import PATH_TOKEN_LENGTH, USER_TOKEN_LENGTH 10 | from django_tools.serve_media_app.exceptions import NoUserToken 11 | from django_tools.serve_media_app.utils import clean_filename, get_random_string 12 | 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | def generate_token(): 18 | return get_random_string(USER_TOKEN_LENGTH) 19 | 20 | 21 | def generate_media_path(user, filename): 22 | """ 23 | Generate random MEDIA path for the given user and filename 24 | """ 25 | user_token = UserMediaTokenModel.objects.get_user_token(user) 26 | 27 | random_string = get_random_string(PATH_TOKEN_LENGTH) 28 | filename = clean_filename(filename) 29 | filename = f'{user_token}/{random_string}/{filename}' 30 | 31 | log.info('Upload file path: %r', filename) 32 | return filename 33 | 34 | 35 | def user_directory_path(instance, filename): 36 | """ 37 | FileField/ImageField 'upload_to' handler 38 | Function for e.g.: models.ImageField(upload_to=user_directory_path, 39 | 40 | Upload to /MEDIA_ROOT/... 41 | """ 42 | return generate_media_path(user=instance.user, filename=filename) 43 | 44 | 45 | class UserMediaTokenQuerySet(models.QuerySet): 46 | def get_from_user(self, user): 47 | instance = self.filter(user_id=user.pk).first() 48 | if not instance: 49 | # Should be created via migrations/signals for all users 50 | raise NoUserToken(user=user) # -> SuspiciousOperation -> HttpResponseBadRequest 51 | return instance 52 | 53 | def get_user_token(self, user): 54 | instance = self.get_from_user(user=user) 55 | if instance is not None: 56 | return instance.token 57 | 58 | 59 | class UserMediaTokenModel(models.Model): 60 | """ 61 | Store for every User a random token. 62 | This token will be added to uploaded MEDIA files for 63 | identify the user. 64 | Auto created via post_save signal, see below. 65 | """ 66 | objects = UserMediaTokenQuerySet.as_manager() 67 | 68 | user = models.ForeignKey( # "Owner" of this entry 69 | settings.AUTH_USER_MODEL, 70 | related_name='token', 71 | on_delete=models.CASCADE, 72 | editable=False, 73 | ) 74 | token = models.CharField( 75 | max_length=USER_TOKEN_LENGTH, 76 | default=generate_token, 77 | unique=True, 78 | ) 79 | 80 | def __repr__(self): 81 | return f'' 82 | 83 | 84 | @receiver(post_save, sender=get_user_model()) 85 | def create_token(sender, created=None, instance=None, **kwargs): 86 | assert instance is not None 87 | token_instance, created = UserMediaTokenModel.objects.get_or_create(user_id=instance.pk) 88 | if created: 89 | log.info('User media token created for: %s', instance) 90 | else: 91 | log.debug('User media token exists for: %s', instance) 92 | -------------------------------------------------------------------------------- /django_tools/serve_media_app/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/serve_media_app/tests/__init__.py -------------------------------------------------------------------------------- /django_tools/serve_media_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from django_tools.serve_media_app.views.serve_user_files import UserMediaView 4 | 5 | 6 | app_name = 'serve_media_app' 7 | urlpatterns = [ 8 | path('/', UserMediaView.as_view(), name='serve-media'), 9 | ] 10 | -------------------------------------------------------------------------------- /django_tools/serve_media_app/utils.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | from pathlib import Path 3 | 4 | from django.utils.text import slugify 5 | 6 | 7 | def get_random_string(length, only_lower=True): 8 | """ 9 | >>> x = get_random_string(length=10) 10 | >>> len(x) 11 | 10 12 | """ 13 | # Note: nbytes is not the length of the final String! 14 | random_string = secrets.token_urlsafe(nbytes=length + 10) 15 | random_string = random_string[:length] 16 | if only_lower: 17 | random_string = random_string.lower() 18 | return random_string 19 | 20 | 21 | def clean_filename(filename): 22 | """ 23 | Convert filename to ASCII only via slugify, e.g.: 24 | 25 | >>> clean_filename('bar.py') 26 | 'bar.py' 27 | >>> clean_filename('No-Extension!') 28 | 'no_extension' 29 | >>> clean_filename('testäöüß!.exe') 30 | 'testaou.exe' 31 | >>> clean_filename('nameäöü.extäöü') 32 | 'nameaou.extaou' 33 | """ 34 | def convert(txt): 35 | txt = slugify(txt, allow_unicode=False) 36 | return txt.replace('-', '_') 37 | 38 | suffix = Path(filename).suffix 39 | if suffix: 40 | filename = filename[:-len(suffix)] 41 | suffix = f'.{convert(suffix)}' 42 | filename = convert(filename) 43 | return f'{filename}{suffix}' 44 | -------------------------------------------------------------------------------- /django_tools/serve_media_app/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/serve_media_app/views/__init__.py -------------------------------------------------------------------------------- /django_tools/serve_media_app/views/serve_user_files.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import django.dispatch 4 | from django.conf import settings 5 | from django.core.exceptions import PermissionDenied 6 | from django.views.generic.base import View 7 | from django.views.static import serve 8 | 9 | from django_tools.serve_media_app.models import UserMediaTokenModel 10 | 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | serve_file_request = django.dispatch.Signal() 15 | 16 | 17 | class UserMediaView(View): 18 | """ 19 | Serve MEDIA_URL files, but check the current user: 20 | """ 21 | 22 | def check_access_permission(self, request, user_token): 23 | if request.user.id is None: 24 | logger.error('Anonymous try to access user media files') 25 | raise PermissionDenied 26 | 27 | user = request.user 28 | 29 | token = UserMediaTokenModel.objects.get_user_token(user=user) 30 | if token != user_token: 31 | # A user tries to access a file from a other user? 32 | logger.error('Wrong user (%s) token: %r is not %r', user, token, user_token) 33 | raise PermissionDenied 34 | 35 | def get(self, request, user_token, path): 36 | media_path = f'{user_token}/{path}' 37 | logger.debug('Serve: %r', media_path) 38 | 39 | if not request.user.is_superuser: 40 | self.check_access_permission(request, user_token) 41 | 42 | serve_file_request.send( 43 | sender=self.__class__, 44 | user=request.user, 45 | path=path, 46 | media_path=media_path 47 | ) 48 | 49 | # Send the file to the user: 50 | return serve( 51 | request, 52 | path=media_path, 53 | document_root=settings.MEDIA_ROOT, 54 | show_indexes=False 55 | ) 56 | -------------------------------------------------------------------------------- /django_tools/settings_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | utilities for settings.py 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | :copyleft: 2015-2017 by the django-tools team, see AUTHORS for more details. 6 | :license: GNU GPL v3 or above, see LICENSE for more details. 7 | """ 8 | 9 | 10 | from fnmatch import fnmatch 11 | 12 | 13 | class IpPattern(str): 14 | def __init__(self, pattern): 15 | self.pattern = pattern 16 | 17 | def __eq__(self, pat): 18 | """ 19 | ALLOWED_HOSTS compares via " == ALLOWED_HOSTS" 20 | see: django.http.request.validate_host 21 | """ 22 | return fnmatch(pat, self.pattern) 23 | 24 | def lower(self): 25 | return IpPattern(self.pattern.lower()) 26 | 27 | def startswith(self, *args): 28 | return False 29 | 30 | def __str__(self): 31 | return self.pattern 32 | 33 | 34 | class FnMatchIps(list): 35 | """ 36 | Allows you to use Unix shell-style wildcards of IP addresses 37 | for settings.INTERNAL_IPS and settings.ALLOWED_HOSTS 38 | used the fnmatch module. 39 | 40 | settings.py e.g.: 41 | -------------------------------------------------------------------------- 42 | from django_tools.settings_utils import FnMatchIps 43 | 44 | INTERNAL_IPS = FnMatchIps(["127.0.0.1", "::1", "192.168.*.*", "10.0.*.*"]) 45 | ALLOWED_HOSTS = FnMatchIps(["127.0.0.1", "::1", "192.168.*.*", "10.0.*.*"]) 46 | -------------------------------------------------------------------------- 47 | 48 | borrowed from https://djangosnippets.org/snippets/1380/ 49 | """ 50 | 51 | def __init__(self, pattern_list): 52 | super().__init__([IpPattern(pat) for pat in pattern_list]) 53 | 54 | def __contains__(self, pat): 55 | # INTERNAL_IPS checks via " in INTERNAL_IPS" 56 | for ip in self: 57 | if pat == ip: 58 | return True 59 | return False 60 | 61 | 62 | InternalIps = FnMatchIps # for compatibility 63 | -------------------------------------------------------------------------------- /django_tools/template/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/template/__init__.py -------------------------------------------------------------------------------- /django_tools/template/filters.py: -------------------------------------------------------------------------------- 1 | """ 2 | some additional template filters 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | :copyleft: 2007-2016 by the django-tools team, see AUTHORS for more details. 6 | :license: GNU GPL v3 or above, see LICENSE for more details. 7 | """ 8 | 9 | 10 | import warnings 11 | 12 | from bx_django_utils.humanize.time import human_timedelta 13 | from django.template.defaultfilters import stringfilter 14 | 15 | 16 | CHMOD_TRANS_DATA = ( 17 | "---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx" 18 | ) 19 | 20 | 21 | def chmod_symbol(octal_value): 22 | """ 23 | Transform a os.stat().st_octal_value octal value to a symbolic string. 24 | ignores meta information like SUID, SGID or the Sticky-Bit. 25 | e.g. 40755 -> rwxr-xr-x 26 | >>> chmod_symbol(644) 27 | 'rw-r--r--' 28 | >>> chmod_symbol(40755) 29 | 'rwxr-xr-x' 30 | >>> chmod_symbol("777") 31 | 'rwxrwxrwx' 32 | """ 33 | octal_value_string = str(octal_value)[-3:] # strip "meta info" 34 | return ''.join(CHMOD_TRANS_DATA[int(num)] for num in octal_value_string) 35 | 36 | 37 | chmod_symbol.is_safe = True 38 | chmod_symbol = stringfilter(chmod_symbol) 39 | 40 | 41 | def get_oct(value): 42 | """ 43 | Convert an integer number to an octal string. 44 | 45 | >>> get_oct(123) 46 | '0o173' 47 | >>> get_oct('abc') 48 | 'abc' 49 | """ 50 | try: 51 | return oct(value) 52 | except TypeError: 53 | return value 54 | 55 | 56 | get_oct.is_safe = False 57 | 58 | 59 | def human_duration(t): 60 | """ 61 | Converts a time duration into a friendly text representation. 62 | 63 | >>> human_duration(0.01) 64 | '10.0\xa0ms' 65 | >>> human_timedelta(59 * 60) 66 | '59.0\xa0minutes' 67 | >>> human_duration(1.05*60*60) 68 | '1.1\xa0hours' 69 | >>> human_duration(2.54 * 60 * 60 * 24 * 365) 70 | '2.5\xa0years' 71 | """ 72 | warnings.warn("Use bx_django_utils.humanize.time.human_timedelta!", DeprecationWarning, stacklevel=2) 73 | return human_timedelta(t) 74 | 75 | 76 | human_duration.is_safe = True 77 | -------------------------------------------------------------------------------- /django_tools/template/loader.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add HTML comments with the template name/path 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | The rendered templates will have inserted this: 6 | 7 | ... 8 | 9 | 10 | Usage, e.g.: 11 | 12 | if DEBUG: 13 | TEMPLATES[0]["OPTIONS"]["loaders"] = [ 14 | ( 15 | "django_tools.template.loader.DebugCacheLoader", ( 16 | 'django.template.loaders.filesystem.Loader', 17 | 'django.template.loaders.app_directories.Loader', 18 | ) 19 | ) 20 | ] 21 | """ 22 | 23 | 24 | from django.template.base import TextNode 25 | from django.template.loaders.cached import Loader as CachedLoader 26 | 27 | 28 | class BaseDebugTemplateCache(dict): 29 | def add_tags(self, template): 30 | """ 31 | Add START, END comments TextNodes in template nodelist. 32 | 33 | :param template: a django.template.base.Template instance 34 | """ 35 | template.nodelist.insert(0, TextNode(f"\n")) 36 | template.nodelist.append(TextNode(f"\n")) 37 | 38 | 39 | class DebugTemplateCache(BaseDebugTemplateCache): 40 | def __setitem__(self, key, value): 41 | try: 42 | template, origin = value 43 | except TypeError: 44 | # e.g.: value is TemplateDoesNotExist class instance ;) 45 | pass 46 | else: 47 | self.add_tags(template) 48 | dict.__setitem__(self, key, value) 49 | 50 | 51 | class GetDebugTemplateCache(BaseDebugTemplateCache): 52 | def __setitem__(self, key, template): 53 | if hasattr(template, "nodelist"): 54 | # e.g.: TemplateDoesNotExist class instance didn't has nodelist ;) 55 | self.add_tags(template) 56 | dict.__setitem__(self, key, template) 57 | 58 | 59 | class DebugCacheLoader(CachedLoader): 60 | """ 61 | Works like the normal cache loader: 'django.template.loaders.cached.Loader' 62 | but we add the START/END comments to every cached template. 63 | """ 64 | 65 | def __init__(self, *args, **kwargs): 66 | super().__init__(*args, **kwargs) 67 | 68 | # Change the cache to our own dict classes 69 | self.get_template_cache = GetDebugTemplateCache() 70 | self.template_cache = DebugTemplateCache() 71 | -------------------------------------------------------------------------------- /django_tools/template/render.py: -------------------------------------------------------------------------------- 1 | """ 2 | template render 3 | ~~~~~~~~~~~~~~ 4 | 5 | Some tools around template rendering 6 | 7 | :copyleft: 2009-2017 by the django-tools team, see AUTHORS for more details. 8 | :license: GNU GPL v3 or above, see LICENSE for more details. 9 | """ 10 | 11 | 12 | from django.template import Context, Template 13 | from django.template.loader import get_template 14 | 15 | 16 | def render_template_file(path, context): 17 | """ 18 | Render given template file and return the rendered string 19 | """ 20 | t = get_template(path) 21 | return t.render(context) 22 | 23 | 24 | def render_string_template(template, context): 25 | """ 26 | render the given template string with the context and return it as a string 27 | """ 28 | t = Template(template) 29 | c = Context(context) 30 | return t.render(c) 31 | -------------------------------------------------------------------------------- /django_tools/template/warn_invalid_template_vars.py: -------------------------------------------------------------------------------- 1 | """ 2 | Send warnings if a template variable does not exist in the content. 3 | 4 | experimental. 5 | 6 | Put this into your settings.py: 7 | -------------------------------------------------------------------------- 8 | if DEBUG: 9 | from django_tools.template import warn_invalid_template_vars 10 | warn_invalid_template_vars.add_warning() 11 | -------------------------------------------------------------------------- 12 | """ 13 | 14 | import warnings 15 | 16 | from django import template 17 | 18 | 19 | # from django.utils.encoding import smart_unicode, force_unicode, smart_str 20 | 21 | _WARN_ADDED = False 22 | 23 | MAX_LEN = 79 24 | 25 | 26 | class InvalidTemplateKey(Warning): 27 | pass 28 | 29 | 30 | def add_warning(): 31 | global _WARN_ADDED 32 | if _WARN_ADDED: 33 | return 34 | 35 | class WarnVariableDoesNotExist(template.VariableDoesNotExist): 36 | def __init__(self, *args, **kwargs): 37 | super().__init__(*args, **kwargs) 38 | 39 | warn_msg = str(self) # get the complete message encoded in UTF-8 40 | if len(warn_msg) > MAX_LEN: 41 | warn_msg = warn_msg[:MAX_LEN] + "..." 42 | warnings.warn(warn_msg, category=InvalidTemplateKey, stacklevel=2) 43 | 44 | template.VariableDoesNotExist = WarnVariableDoesNotExist 45 | 46 | _WARN_ADDED = True 47 | -------------------------------------------------------------------------------- /django_tools/templates/tagging_addon/jQueryTagField.html: -------------------------------------------------------------------------------- 1 | {{ html }} 2 | 3 |
4 | {% for tag in tag_cloud %} 5 | {% if not forloop.first %}|{% endif %} 6 | {{ tag.name }} 7 | {% endfor %} 8 |
9 | 10 | 19 | 20 | -------------------------------------------------------------------------------- /django_tools/unittest_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/unittest_utils/__init__.py -------------------------------------------------------------------------------- /django_tools/unittest_utils/call_management_commands.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import io 3 | 4 | from cli_base.cli_tools.test_utils.rich_test_utils import NoColorEnvRichClick 5 | from click._compat import strip_ansi as strip_ansi_codes 6 | from django.core.management import BaseCommand, call_command 7 | 8 | from django_tools.unittest_utils.stdout_redirect import DenyStdWrite 9 | 10 | 11 | class Buffer(io.StringIO): 12 | def __repr__(self): 13 | return '' 14 | 15 | 16 | def captured_call_command(command, **kwargs) -> tuple[str, str]: 17 | """ 18 | Call django manage command and return stdout + stderr 19 | """ 20 | with NoColorEnvRichClick(): 21 | try: 22 | assert inspect.ismodule(command) 23 | CommandClass = command.Command 24 | assert issubclass(CommandClass, BaseCommand) 25 | except Exception as err: 26 | raise AssertionError(f'{command!r} is no Django Management command: {err}') 27 | 28 | command_name = command.__name__ 29 | command_name = command_name.rsplit('.', 1)[-1] 30 | 31 | command_instance = CommandClass() 32 | 33 | capture_stdout = Buffer() 34 | capture_stderr = Buffer() 35 | kwargs.update( 36 | dict( 37 | stdout=capture_stdout, 38 | stderr=capture_stderr, 39 | ) 40 | ) 41 | with DenyStdWrite(name=command_name): 42 | call_command(command_instance, **kwargs) 43 | 44 | stdout_output = strip_ansi_codes(capture_stdout.getvalue()) 45 | stderr_output = strip_ansi_codes(capture_stderr.getvalue()) 46 | return stdout_output, stderr_output 47 | -------------------------------------------------------------------------------- /django_tools/unittest_utils/disable_migrations.py: -------------------------------------------------------------------------------- 1 | class DisableMigrations: 2 | """ 3 | Speedup test run start by disable migrations, just create the database tables ;) 4 | 5 | Usage in test settings.py: 6 | 7 | from django_tools.unittest_utils.disable_migrations import DisableMigrations 8 | MIGRATION_MODULES = DisableMigrations() 9 | """ 10 | 11 | def __contains__(self, item): 12 | return True 13 | 14 | def __getitem__(self, item): 15 | return "notmigrations" 16 | -------------------------------------------------------------------------------- /django_tools/unittest_utils/email.py: -------------------------------------------------------------------------------- 1 | """ 2 | :created: 2018 by Jens Diemer 3 | :copyleft: 2018-2019 by the django-tools team, see AUTHORS for more details. 4 | :license: GNU GPL v3 or above, see LICENSE for more details. 5 | """ 6 | from email.mime.image import MIMEImage 7 | 8 | from django.utils.text import Truncator 9 | 10 | 11 | def print_mailbox(outbox, max_length=120): 12 | """ 13 | Usefull for debugging tests, e.g.: 14 | 15 | # ... do something that send mails 16 | print_mailbox(mail.outbox) 17 | self.assertEqual(len(mail.outbox), 2) 18 | """ 19 | 20 | def cutted_bytes(data, num=80): 21 | return Truncator(repr(data)).chars(num=num) 22 | 23 | print(f"There are {len(outbox)} mails") 24 | for no, mail_instance in enumerate(outbox, start=1): 25 | print("_" * 79) 26 | print(f" *** Mail No. {no:d}: ***") 27 | 28 | attr_names = [ 29 | attr_name for attr_name in dir(mail_instance) if not attr_name.startswith("_") and attr_name != "body" 30 | ] 31 | attr_names.sort() 32 | attr_names.append("body") 33 | 34 | for attr_name in attr_names: 35 | attr = getattr(mail_instance, attr_name, "--") 36 | 37 | if callable(attr) or not isinstance(attr, (str, list, tuple, bool)): 38 | continue 39 | 40 | print("%20s:" % attr_name, end=" ", flush=True) 41 | 42 | if attr_name == "alternatives": 43 | for data in attr: 44 | print(cutted_bytes(data, num=max_length)) 45 | 46 | elif attr_name == "attachments": 47 | print("\n") 48 | for attachment in attr: 49 | if isinstance(attachment, MIMEImage): 50 | line = f" * {attachment!r}" 51 | data = attachment.get_payload(decode=True) 52 | else: 53 | filename, data, mine_type = attachment 54 | line = f" * {filename} ({mine_type})" 55 | 56 | print(line, end=" ") 57 | print(cutted_bytes(data, num=(max_length - len(line)))) 58 | print() 59 | 60 | elif attr_name == "body": 61 | print(attr) 62 | print("-" * max_length) 63 | 64 | else: 65 | if len(attr) > max_length: 66 | attr = cutted_bytes(data, num=max_length) 67 | print(attr) 68 | 69 | print("=" * 120) 70 | -------------------------------------------------------------------------------- /django_tools/unittest_utils/isolated_filesystem.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import shutil 4 | import tempfile 5 | from pathlib import Path 6 | 7 | from django.test.utils import TestContextDecorator 8 | 9 | from django_tools.unittest_utils.assertments import assert_is_dir 10 | 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class isolated_filesystem(TestContextDecorator): 16 | """ 17 | Acts as either a decorator or a context manager. 18 | 19 | with isolated_filesystem(prefix="temp_dir_prefix"): 20 | print("I'm in the temp path here: %s" % Path().cwd()) 21 | 22 | or: 23 | 24 | class FooBarTestCase(TestCase): 25 | @isolated_filesystem() 26 | def test_foo(self): 27 | print("I'm in the temp path here: %s" % Path().cwd()) 28 | """ 29 | 30 | def __init__(self, prefix=None): 31 | super().__init__() 32 | 33 | self.prefix = prefix 34 | self.prefix_candidate = None 35 | self.cwd = None 36 | self.temp_path = None 37 | 38 | def enable(self): 39 | if self.prefix is None: 40 | prefix = self.prefix_candidate 41 | else: 42 | prefix = self.prefix 43 | 44 | print(f"Use prefix: {prefix!r}") 45 | 46 | self.cwd = Path().cwd() 47 | self.temp_path = tempfile.mkdtemp(prefix=prefix) 48 | os.chdir(self.temp_path) 49 | 50 | def disable(self): 51 | print(f'Delete: {self.temp_path}') 52 | os.chdir(str(self.cwd)) # str() needed for older python <=3.5 53 | try: 54 | shutil.rmtree(self.temp_path) 55 | except OSError as err: 56 | logger.exception('Cleanup error: %s', err) 57 | 58 | def __exit__(self, exc_type, exc_value, traceback): 59 | temp_path = Path(self.temp_path) 60 | assert_is_dir(temp_path) 61 | 62 | super().__exit__(exc_type, exc_value, traceback) 63 | 64 | assert not temp_path.is_dir(), f'ERROR: {temp_path!r} still exists?!?' 65 | 66 | def decorate_class(self, cls): 67 | raise NotImplementedError('Decorating a class is not supported!') 68 | 69 | def decorate_callable(self, func): 70 | self.prefix_candidate = f'{func.__name__}_' 71 | print(f"prefix from func {func!r}: {self.prefix_candidate!r}") 72 | return super().decorate_callable(func) 73 | -------------------------------------------------------------------------------- /django_tools/unittest_utils/print_sql.py: -------------------------------------------------------------------------------- 1 | """ 2 | print SQL 3 | ~~~~~~~~~ 4 | 5 | :copyleft: 2012-2015 by the django-tools team, see AUTHORS for more details. 6 | :license: GNU GPL v3 or above, see LICENSE for more details. 7 | """ 8 | 9 | from django.db import DEFAULT_DB_ALIAS, connections 10 | from django.test.utils import CaptureQueriesContext 11 | from django.utils.encoding import smart_str 12 | 13 | 14 | PFORMAT_SQL_KEYWORDS = ("FROM", "WHERE", "ORDER BY", "VALUES") 15 | 16 | 17 | def pformat_sql(sql): 18 | # remove unicode u'' 19 | sql = sql.replace("u'", "'").replace('u"', '"') 20 | 21 | sql = sql.replace('`', '') 22 | for keyword in PFORMAT_SQL_KEYWORDS: 23 | sql = sql.replace(f' {keyword} ', f'\n\t{keyword} ') 24 | 25 | return smart_str(sql) 26 | 27 | 28 | class PrintQueries(CaptureQueriesContext): 29 | """ 30 | with context manager to print the used SQL Queries. 31 | usage e.g.: 32 | --------------------------------------------------------------------------- 33 | 34 | from django_tools.unittest_utils.print_sql import PrintQueries 35 | 36 | # e.g. use in unittests: 37 | class MyTests(TestCase): 38 | def test_foobar(self): 39 | with PrintQueries("Create object"): 40 | FooBar.objects.create("name"=foo) 41 | 42 | # e.g. use in views: 43 | def my_view(request): 44 | with PrintQueries("Create object"): 45 | FooBar.objects.create("name"=foo) 46 | 47 | the output is like: 48 | ___________________________________________________________________________ 49 | *** Create object *** 50 | 1 - INSERT INTO "foobar" ("name") 51 | VALUES (foo) 52 | --------------------------------------------------------------------------- 53 | """ 54 | 55 | def __init__(self, headline, **kwargs): 56 | self.headline = headline 57 | 58 | if "connection" not in kwargs: 59 | using = kwargs.pop("using", DEFAULT_DB_ALIAS) 60 | kwargs["connection"] = connections[using] 61 | 62 | super().__init__(**kwargs) 63 | 64 | def __exit__(self, exc_type, exc_value, traceback): 65 | super().__exit__(exc_type, exc_value, traceback) 66 | if exc_type is not None: 67 | return 68 | 69 | print() 70 | print("_" * 79) 71 | if self.headline: 72 | print(f" *** {self.headline} ***") 73 | for no, q in enumerate(self.captured_queries, 1): 74 | sql = pformat_sql(q["sql"]) 75 | msg = smart_str(f"{no:d} - {sql}\n") 76 | print(msg) 77 | print("-" * 79) 78 | -------------------------------------------------------------------------------- /django_tools/unittest_utils/project_setup.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from pathlib import Path 3 | 4 | 5 | def check_editor_config(package_root: Path, config_defaults=None) -> None: 6 | warnings.warn( 7 | "Use manageprojects.test_utils.project_setup.check_editor_config!", 8 | DeprecationWarning, 9 | stacklevel=2, 10 | ) 11 | 12 | from manageprojects.test_utils.project_setup import check_editor_config 13 | 14 | check_editor_config(package_root=package_root, config_defaults=config_defaults) 15 | -------------------------------------------------------------------------------- /django_tools/unittest_utils/signals.py: -------------------------------------------------------------------------------- 1 | class SignalsContextManager: 2 | def __init__(self, signal, callback): 3 | self.signal = signal 4 | self.callback = callback 5 | 6 | def __enter__(self): 7 | self.signal.connect(self.callback) 8 | 9 | def __exit__(self, exc_type, exc_val, exc_tb): 10 | self.signal.disconnect(self.callback) 11 | -------------------------------------------------------------------------------- /django_tools/unittest_utils/stdout_redirect.py: -------------------------------------------------------------------------------- 1 | import io 2 | from contextlib import redirect_stderr, redirect_stdout 3 | 4 | from django_tools.context_managers import MassContextManagerBase 5 | 6 | 7 | class StdoutStderrBuffer(MassContextManagerBase): 8 | """ 9 | redirect stderr and stdout 10 | 11 | e.g: 12 | 13 | with StdoutStderrBuffer() as buffer: 14 | print("foo") 15 | 16 | output = buffer.get_output() 17 | """ 18 | 19 | def __init__(self): 20 | self.buffer = io.StringIO() 21 | self.context_managers = [ 22 | redirect_stdout(self.buffer), 23 | redirect_stderr(self.buffer), 24 | ] 25 | 26 | def get_output(self): 27 | output = self.buffer.getvalue() 28 | return output 29 | 30 | 31 | class DenyStdWriteHandler: 32 | def __init__(self, name, std_type): 33 | self.name = name 34 | self.std_type = std_type 35 | 36 | def write(self, out, *args, **kwargs): 37 | raise AssertionError(f'{self.name} writes to std{self.std_type}:\n{out}') 38 | 39 | 40 | class DenyStdWrite(MassContextManagerBase): 41 | """ 42 | ContextManager that raise an AssertionError on std(out|err).write calls. 43 | """ 44 | 45 | def __init__(self, name): 46 | self.context_managers = [ 47 | redirect_stdout(DenyStdWriteHandler(name=name, std_type='out')), 48 | redirect_stderr(DenyStdWriteHandler(name=name, std_type='err')), 49 | ] 50 | -------------------------------------------------------------------------------- /django_tools/unittest_utils/temp_media_root.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import tempfile 3 | from pathlib import Path 4 | 5 | from django.test import override_settings 6 | 7 | 8 | class TempMediaRoot: 9 | """ 10 | Context Manager / Decorator to set a temporary MEDIA_ROOT 11 | """ 12 | 13 | def __enter__(self) -> Path: 14 | self.temp_dir_cm = tempfile.TemporaryDirectory() 15 | self.temp_dir = self.temp_dir_cm.__enter__() 16 | self.override_cm = override_settings(MEDIA_ROOT=self.temp_dir) 17 | self.override_cm.__enter__() 18 | return Path(self.temp_dir) 19 | 20 | def __exit__(self, exc_type, exc_value, traceback): 21 | self.temp_dir_cm.__exit__(exc_type, exc_value, traceback) 22 | self.override_cm.__exit__(exc_type, exc_value, traceback) 23 | if exc_type: 24 | return False 25 | 26 | def __call__(self, func): 27 | """ 28 | Use this Context Manager a Decorator, too 29 | """ 30 | 31 | @functools.wraps(func) 32 | def wrapped_func(*args, **kwargs): 33 | with self: 34 | return func(*args, **kwargs) 35 | 36 | return wrapped_func 37 | -------------------------------------------------------------------------------- /django_tools/unittest_utils/tempdir.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import tempfile 3 | 4 | 5 | class TempDir(): 6 | def __init__(self, prefix=""): 7 | self.tempfolder = tempfile.mkdtemp(prefix=prefix) 8 | 9 | def __enter__(self): 10 | return self.tempfolder 11 | 12 | def __exit__(self, exc_type, exc_value, traceback): 13 | shutil.rmtree(self.tempfolder) 14 | -------------------------------------------------------------------------------- /django_tools/unittest_utils/template.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from django.conf import settings 4 | from django.test import override_settings 5 | 6 | 7 | TEMPLATE_INVALID_PREFIX = '***invalid:' 8 | 9 | 10 | class set_string_if_invalid(override_settings): 11 | """ 12 | Decorator that set 'string_if_invalid' in settings.TEMPLATES 13 | more info in README 14 | 15 | see also: 16 | https://docs.djangoproject.com/en/1.8/ref/templates/api/#invalid-template-variables 17 | """ 18 | 19 | def __init__(self): 20 | string_if_invalid = '%s%%s***' % TEMPLATE_INVALID_PREFIX 21 | 22 | TEMPLATES = deepcopy(settings.TEMPLATES) 23 | for template_settings in TEMPLATES: 24 | template_settings['OPTIONS']['string_if_invalid'] = string_if_invalid 25 | 26 | super().__init__(TEMPLATES=TEMPLATES) 27 | -------------------------------------------------------------------------------- /django_tools/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/utils/__init__.py -------------------------------------------------------------------------------- /django_tools/utils/client_storage.py: -------------------------------------------------------------------------------- 1 | """ 2 | Client storage 3 | ~~~~~~~~~~~~~~ 4 | 5 | Use dumps() and loads() from django.core.signing to store data into a Cookie. 6 | 7 | See: 8 | https://docs.djangoproject.com/en/1.4/topics/signing/#verifying-timestamped-values 9 | 10 | Usage e.g.: 11 | -------------------------------------------------------------------------- 12 | from django_tools.utils.client_storage import SignedCookieStorageError, SignedCookieStorage 13 | 14 | def view1(request): 15 | response = HttpResponse("Hello World!") 16 | c = SignedCookieStorage(cookie_key="my_key", max_age=60) 17 | response = c.save_data(my_data, response) 18 | return response 19 | 20 | def view2(request): 21 | c = SignedCookieStorage(cookie_key="my_key", max_age=60) 22 | try: 23 | data = c.get_data(request) 24 | except SignedCookieStorageError, err: 25 | ...cookie missing or outdated or wrong data... 26 | else: 27 | ...do something with the data... 28 | -------------------------------------------------------------------------- 29 | 30 | :copyleft: 2010-2015 by the django-tools team, see AUTHORS for more details. 31 | :license: GNU GPL v3 or above, see LICENSE for more details. 32 | """ 33 | 34 | 35 | import warnings 36 | 37 | from django.core import signing 38 | 39 | 40 | class SignedCookieStorageError(signing.BadSignature): 41 | pass 42 | 43 | 44 | class SignedCookieStorage: 45 | """ 46 | see: 47 | django_tools_tests.test_signed_cookie.TestSignedCookieStorage 48 | """ 49 | 50 | def __init__(self, cookie_key, max_age=60 * 60 * 24 * 7 * 52, compress=False): 51 | self.cookie_key = cookie_key 52 | self.max_age = max_age 53 | self.compress = compress 54 | 55 | def save_data(self, data, response, **kwargs): 56 | """ Add a cookie to response object, with data and security hash. """ 57 | signed_data = signing.dumps(data, compress=self.compress, **kwargs) 58 | response.set_cookie(key=self.cookie_key, value=signed_data, max_age=self.max_age) 59 | return response 60 | 61 | def get_data(self, request): 62 | """ Return the stored data from cookie, if security hash is valid. """ 63 | try: 64 | raw_data = request.COOKIES[self.cookie_key] 65 | except KeyError: 66 | raise SignedCookieStorageError(f"Cookie {self.cookie_key!r} doesn't exists") 67 | 68 | try: 69 | data = signing.loads(raw_data, max_age=self.max_age) 70 | except Exception as err: 71 | raise SignedCookieStorageError(f"Can't load data: {err}") 72 | 73 | return data 74 | 75 | 76 | class ClientCookieStorage: 77 | """ 78 | Support the old API 79 | 80 | TODO: remove in future 81 | """ 82 | 83 | def __new__(self, *args, **kwargs): 84 | warnings.warn( 85 | ( 86 | "ClientCookieStorage is old API!" 87 | " Please change to SignedCookieStorage!" 88 | " This will be removed in the future!" 89 | ), 90 | FutureWarning, 91 | stacklevel=2) 92 | return SignedCookieStorage(*args, **kwargs) 93 | -------------------------------------------------------------------------------- /django_tools/utils/html_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | :copyleft: 2017-2020 by the django-tools team, see AUTHORS for more details. 3 | :license: GNU GPL v3 or above, see LICENSE for more details. 4 | """ 5 | 6 | 7 | import bleach 8 | from django.utils.html import strip_tags 9 | 10 | 11 | def html2text(value): 12 | """ 13 | Returns the text from HTML by strip all HTML tags. 14 | Used first bleach.clean() and then django 'strip_tags' 15 | """ 16 | if value: 17 | value = value.strip() 18 | 19 | if value: 20 | # use Django 21 | value = strip_tags(value) 22 | 23 | value = bleach.clean(value) 24 | 25 | return value 26 | -------------------------------------------------------------------------------- /django_tools/utils/importlib.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-tools import helper 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | additional helper to the existing django.utils.importlib 6 | 7 | :copyleft: 2012-2016 by the django-tools team, see AUTHORS for more details. 8 | :license: GNU GPL v3 or above, see LICENSE for more details. 9 | """ 10 | 11 | 12 | import logging 13 | from importlib import import_module 14 | 15 | from django.conf import settings 16 | from django.core.exceptions import ImproperlyConfigured 17 | 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | def get_attr_from_string(path, obj_name=""): 23 | """ 24 | Return a attribute from a module by the given path string. 25 | 26 | >>> get_attr_from_string("django_tools.cache.site_cache_middleware.UpdateCacheMiddleware") 27 | 28 | """ 29 | try: 30 | module_name, class_name = path.rsplit('.', 1) 31 | except ValueError: 32 | raise ImproperlyConfigured(f"{path} isn't a {obj_name} module") 33 | 34 | try: 35 | mod = import_module(module_name) 36 | except ImportError as e: 37 | raise ImproperlyConfigured(f'Error importing {obj_name} module {module_name}: "{e}"') 38 | 39 | try: 40 | attr = getattr(mod, class_name) 41 | except AttributeError: 42 | raise ImproperlyConfigured(f'{obj_name} module "{module_name}" does not define a "{class_name}" class') 43 | 44 | return attr 45 | 46 | 47 | def get_class_instance(path, obj_name=""): 48 | """ 49 | Returns a class instance from a module by the given path string. 50 | 51 | >>> get_class_instance("django_tools.cache.site_cache_middleware.UpdateCacheMiddleware") # doctest: +ELLIPSIS 52 | 53 | """ 54 | class_obj = get_attr_from_string(path, obj_name) 55 | class_instance = class_obj() 56 | return class_instance 57 | 58 | 59 | def get_setting(setting_name): 60 | """ 61 | return the settings value, create debug log if not set/empty. 62 | 63 | >>> get_setting("EMAIL_BACKEND") 64 | 'django.core.mail.backends.locmem.EmailBackend' 65 | """ 66 | path = getattr(settings, setting_name, None) 67 | if path: 68 | return path 69 | else: 70 | if not hasattr(settings, setting_name): 71 | logger.debug(f"{setting_name!r} not in settings defined") 72 | else: 73 | logger.debug(f"settings.{setting_name} is None or empty") 74 | 75 | 76 | def get_attr_from_settings(setting_name, obj_name=""): 77 | """ 78 | returns a attribute from the given settings path string. 79 | 80 | >>> get_attr_from_settings("EMAIL_BACKEND", "email backend") 81 | 82 | """ 83 | path = get_setting(setting_name) 84 | if path: 85 | return get_attr_from_string(path, obj_name) 86 | 87 | 88 | def get_class_instance_from_settings(setting_name, obj_name=""): 89 | """ 90 | returns a class instance from the given settings path string. 91 | 92 | >>> get_class_instance_from_settings("EMAIL_BACKEND", "email backend") # doctest: +ELLIPSIS 93 | 94 | """ 95 | path = get_setting(setting_name) 96 | if path: 97 | return get_class_instance(path, obj_name) 98 | -------------------------------------------------------------------------------- /django_tools/utils/info_print.py: -------------------------------------------------------------------------------- 1 | """ 2 | Info print 3 | ~~~~~~~~~~ 4 | 5 | Insert in every stdout.write() a info line from witch code line this print comes. 6 | Usefull to find debug print statements ;) 7 | 8 | WARNING: This is very slow and should only be used with the developer server ;) 9 | 10 | Simply put this two lines in your settings: 11 | ---------------------------------------------------------------------- 12 | from django_tools.utils import info_print 13 | info_print.redirect_stdout() 14 | ---------------------------------------------------------------------- 15 | 16 | Last commit info: 17 | ~~~~~~~~~~~~~~~~~ 18 | $LastChangedDate$ 19 | $Rev$ 20 | $Author:$ 21 | 22 | :copyleft: 2009-2010 by the django-tools team, see AUTHORS for more details. 23 | :license: GNU GPL v3 or above, see LICENSE for more details. 24 | """ 25 | 26 | 27 | import inspect 28 | import os 29 | import sys 30 | import warnings 31 | 32 | 33 | MAX_FILEPATH_LEN = 66 34 | 35 | 36 | class InfoStdout: 37 | """ Insert in every stdout.write() a info line from witch code line this print comes.""" 38 | 39 | def __init__(self, orig_stdout): 40 | self.orig_stdout = orig_stdout 41 | self.old_fileinfo = None 42 | 43 | def write(self, txt): 44 | fileinfo = self._get_fileinfo() 45 | if fileinfo != self.old_fileinfo: 46 | self.orig_stdout.write(f"\n{fileinfo}:\n{txt}") 47 | self.old_fileinfo = fileinfo 48 | else: 49 | self.orig_stdout.write(txt) 50 | 51 | def flush(self): 52 | self.orig_stdout.flush() 53 | 54 | def _get_fileinfo(self): 55 | """ return fileinfo: Where from the announcement comes? """ 56 | try: 57 | self_basename = os.path.basename(__file__) 58 | if self_basename.endswith(".pyc"): 59 | # cut: ".pyc" -> ".py" 60 | self_basename = self_basename[:-1] 61 | 62 | for stack_frame in inspect.stack(): 63 | # go forward in the stack, to outside of this file. 64 | filename = stack_frame[1] 65 | lineno = stack_frame[2] 66 | if os.path.basename(filename) != self_basename: 67 | break 68 | 69 | if len(filename) >= MAX_FILEPATH_LEN: 70 | filename = f"...{filename[-MAX_FILEPATH_LEN:]}" 71 | fileinfo = f"{filename} line {lineno}" 72 | except Exception as e: 73 | fileinfo = f"(inspect Error: {e})" 74 | 75 | return fileinfo 76 | 77 | 78 | __redirected = False 79 | 80 | 81 | def redirect_stdout(): 82 | global __redirected 83 | 84 | if not __redirected: 85 | __redirected = True 86 | try: 87 | warnings.warn("Redirect stdout/stderr for info print!", stacklevel=2) 88 | orig_stdout = sys.stdout 89 | sys.stdout = InfoStdout(orig_stdout) 90 | orig_stderr = sys.stderr 91 | sys.stderr = InfoStdout(orig_stderr) 92 | except Exception as err: 93 | print("Error:", err) 94 | -------------------------------------------------------------------------------- /django_tools/utils/installed_apps_utils.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | 3 | from django.apps import apps 4 | from django.urls import Resolver404 5 | from django.urls.resolvers import RegexPattern, URLResolver 6 | 7 | 8 | def get_filtered_apps(resolve_url="/", no_args=True, debug=False, skip_fail=False): 9 | """ 10 | Create a list of all Apps witch can resolve the given url >resolve_url< 11 | 12 | @param resolve_url: url used for RegexURLResolver 13 | @param no_args: Only views without args/kwargs ? 14 | @parm skip_fail: If True: raise exception if app is not importable 15 | 16 | Please look at: 17 | 18 | django_tools.django_tools_tests.test_installed_apps_utils 19 | 20 | with debug, some print messages would be created: 21 | 22 | e.g.: get_filtered_apps(debug=True) 23 | found 'django.contrib.admindocs' with urls.py 24 | found 'django.contrib.auth' with urls.py 25 | Skip 'django.contrib.auth': Can't handle root url. 26 | found 'django.contrib.flatpages' with urls.py 27 | ['django.contrib.admindocs'] 28 | """ 29 | root_apps = [] 30 | app_configs = apps.get_app_configs() 31 | for app_config in app_configs: 32 | app_pkg = app_config.name 33 | urls_pkg = f"{app_pkg}.urls" 34 | try: 35 | url_mod = import_module(urls_pkg) 36 | except ImportError as err: 37 | if debug: 38 | print(f"Skip {app_pkg!r}: has no urls.py") 39 | if str(err) == f"No module named '{urls_pkg}'": 40 | continue 41 | if not skip_fail: 42 | raise 43 | except Exception as err: 44 | if debug: 45 | print(f"Error importing {app_pkg!r}: {err}") 46 | if not skip_fail: 47 | raise 48 | else: 49 | continue 50 | 51 | if debug: 52 | print(f"found {app_pkg!r} with urls.py") 53 | 54 | try: 55 | urlpatterns = url_mod.urlpatterns 56 | except AttributeError: 57 | if debug: 58 | print(f"Skip {app_pkg!r}: urls.py has no 'urlpatterns'") 59 | continue 60 | 61 | resolver = URLResolver(RegexPattern(r'^'), urlpatterns) 62 | try: 63 | func, func_args, func_kwargs = resolver.resolve(resolve_url) 64 | except Resolver404 as err: 65 | if debug: 66 | print(f"Skip {app_pkg!r}: Can't handle root url. ({err})") 67 | continue 68 | if not no_args or func_args == () and func_kwargs == {}: 69 | root_apps.append(app_pkg) 70 | return root_apps 71 | -------------------------------------------------------------------------------- /django_tools/utils/request.py: -------------------------------------------------------------------------------- 1 | """ 2 | :copyleft: 2017 by the django-tools team, see AUTHORS for more details. 3 | :license: GNU GPL v3 or above, see LICENSE for more details. 4 | """ 5 | 6 | 7 | from django.conf import settings 8 | from django.contrib.auth.models import AnonymousUser 9 | from django.test import RequestFactory 10 | 11 | 12 | def create_fake_request(url="/", session=None, language_code=None, user=None, **extra): 13 | if session is None: 14 | session = {} 15 | 16 | if language_code is None: 17 | language_code = settings.LANGUAGE_CODE 18 | 19 | if user is None: 20 | user = AnonymousUser() 21 | 22 | fake_request = RequestFactory().get(url) 23 | 24 | fake_request.session = session 25 | fake_request.LANGUAGE_CODE = language_code 26 | fake_request.user = user 27 | 28 | for key, value in extra.items(): 29 | setattr(fake_request, key, value) 30 | 31 | return fake_request 32 | -------------------------------------------------------------------------------- /django_tools/utils/stack_info.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from pathlib import Path 3 | 4 | 5 | STACK_LIMIT = 6 # Display only the last X stack lines 6 | 7 | 8 | def format_list(extracted_list): 9 | """ 10 | Format a list of traceback entry tuples. 11 | """ 12 | list = [] 13 | cwd = Path.cwd() 14 | for _, filename, lineno, func_name, line, _ in extracted_list: 15 | filename = Path(filename) 16 | try: 17 | filename = filename.relative_to(cwd) 18 | except ValueError: 19 | pass 20 | code = "".join(line).strip() 21 | item = ('File "%s", line %d, in %s\n' ' %s') % (filename, lineno, func_name, code) 22 | list.append(item) 23 | return list 24 | 25 | 26 | def get_stack_info(filepath_filter): 27 | """ 28 | return stack_info: Where from the announcement comes? 29 | """ 30 | before = True # before we visit filepath_filter 31 | after = False # after we left filepath_filter 32 | 33 | stack_list = [] 34 | for frame in reversed(inspect.stack()): 35 | filepath = frame[1] 36 | 37 | if before and filepath_filter not in filepath: 38 | before = False 39 | continue 40 | 41 | if not after and filepath_filter in filepath: 42 | after = True 43 | 44 | if after: 45 | stack_list.append(frame) 46 | 47 | stack_info = format_list(stack_list) 48 | return stack_info 49 | -------------------------------------------------------------------------------- /django_tools/utils/time_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | utils around time/date/datetime 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | :copyleft: 2011 by the django-tools team, see AUTHORS for more details. 6 | :license: GNU GPL v3 or above, see LICENSE for more details. 7 | """ 8 | 9 | 10 | import datetime 11 | 12 | 13 | def datetime2float(t): 14 | """ 15 | >>> datetime2float(datetime.timedelta(seconds=1)) 16 | 1.0 17 | 18 | >>> f = datetime2float(datetime.timedelta(weeks=2, seconds=20.1, microseconds=100)) 19 | >>> f 20 | 1209620.1001 21 | >>> f == 20.1 + 0.0001 + 2 * 7 * 24 * 60 * 60 22 | True 23 | 24 | >>> datetime2float("type error") 25 | Traceback (most recent call last): 26 | ... 27 | TypeError: datetime2float() argument must be a timedelta instance. 28 | """ 29 | if not isinstance(t, datetime.timedelta): 30 | raise TypeError("datetime2float() argument must be a timedelta instance.") 31 | 32 | # timedelta.total_seconds() is new in Python 2.7 33 | return (float(t.microseconds) + (t.seconds + t.days * 24 * 3600) * 1E6) / 1E6 34 | -------------------------------------------------------------------------------- /django_tools/utils/url.py: -------------------------------------------------------------------------------- 1 | """ 2 | some utils around url 3 | ~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | :created: by Jens Diemer 6 | :copyleft: 2017 by the django-tools team, see AUTHORS for more details. 7 | :license: GNU GPL v3 or above, see LICENSE for more details. 8 | """ 9 | from urllib.parse import quote 10 | 11 | from django.http import QueryDict 12 | from django.utils.encoding import force_bytes 13 | 14 | 15 | class GetDict(QueryDict): 16 | """ 17 | Similar to origin django.http.QueryDict but: 18 | - always mutable 19 | - urlencode() output changed (see DocString there) 20 | """ 21 | 22 | def __init__(self, *args, **kwargs): 23 | kwargs["mutable"] = True 24 | super().__init__(*args, **kwargs) 25 | 26 | def _encode(self, k, v, safe=None): 27 | if safe is None: 28 | safe = "" 29 | 30 | if v is None: 31 | return quote(k, safe) 32 | 33 | v = force_bytes(v, self.encoding) 34 | return f'{quote(k, safe)}={quote(v, safe)}' 35 | 36 | def urlencode(self, safe=None): 37 | """ 38 | Same as django.http.QueryDict.urlencode() but: 39 | - None values will be a empty GET parameter "?empty" (Django: "?empty=") 40 | - output will be sorted (easier for tests ;) 41 | """ 42 | output = [] 43 | for k, list_ in sorted(self.lists()): 44 | k = force_bytes(k, self.encoding) 45 | output.extend( 46 | self._encode(k, v, safe) 47 | for v in sorted(list_) 48 | ) 49 | return '&'.join(output) 50 | -------------------------------------------------------------------------------- /django_tools/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools/views/__init__.py -------------------------------------------------------------------------------- /django_tools/views/csrf.py: -------------------------------------------------------------------------------- 1 | """ 2 | csrf related views 3 | ~~~~~~~~~~~~~~~~~~ 4 | 5 | 6 | debug_csrf_failure() 7 | ~~~~~~~~~~~~~~~~~~~~ 8 | Display the normal debug page and not the minimal csrf debug page. 9 | 10 | usage: Add this to your settings: 11 | ----------------------------------------------------------------------- 12 | CSRF_FAILURE_VIEW='django_tools.views.csrf.debug_csrf_failure' 13 | ----------------------------------------------------------------------- 14 | 15 | 16 | :copyleft: 2012 by the django-tools team, see AUTHORS for more details. 17 | :license: GNU GPL v3 or above, see LICENSE for more details. 18 | """ 19 | 20 | 21 | from django.conf import settings 22 | from django.views.csrf import csrf_failure 23 | 24 | 25 | class CsrfFailure(Exception): 26 | pass 27 | 28 | 29 | def debug_csrf_failure(request, reason=""): 30 | """ 31 | raised own CsrfFailure() exception to get the normal debug page on 32 | Csrf failures. 33 | See also: 34 | https://docs.djangoproject.com/en/1.3/ref/contrib/csrf/#rejected-requests 35 | 36 | More Info: See DocString above. 37 | """ 38 | if not settings.DEBUG: 39 | # Use original HttpResponseForbidden: 40 | return csrf_failure(request, reason) 41 | 42 | raise CsrfFailure(f"csrf failure debug: {reason!r}") 43 | -------------------------------------------------------------------------------- /django_tools_project/__init__.py: -------------------------------------------------------------------------------- 1 | import django_tools 2 | 3 | 4 | __version__ = django_tools.__version__ 5 | -------------------------------------------------------------------------------- /django_tools_project/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Allow django-tools to be executable 3 | through `python -m django_tools`. 4 | """ 5 | 6 | from manage_django_project.manage import execute_django_from_command_line 7 | 8 | 9 | def main(): 10 | """ 11 | entrypoint installed via pyproject.toml and [project.scripts] section. 12 | Must be set in ./manage.py and PROJECT_SHELL_SCRIPT 13 | """ 14 | execute_django_from_command_line() 15 | 16 | 17 | if __name__ == '__main__': 18 | main() 19 | -------------------------------------------------------------------------------- /django_tools_project/constants.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from bx_py_utils.path import assert_is_file 4 | 5 | import django_tools 6 | 7 | 8 | PACKAGE_ROOT = Path(django_tools.__file__).parent.parent 9 | assert_is_file(PACKAGE_ROOT / 'pyproject.toml') 10 | -------------------------------------------------------------------------------- /django_tools_project/django_tools_test_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools_project/django_tools_test_app/__init__.py -------------------------------------------------------------------------------- /django_tools_project/django_tools_test_app/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # https://github.com/jedie/django-tools 4 | from django_tools.admin_tools.test_generator import generate_test_code 5 | from django_tools_project.django_tools_test_app.models import ( 6 | OverwriteFileSystemStorageModel, 7 | PermissionTestModel, 8 | VersioningTestModel, 9 | ) 10 | 11 | 12 | @admin.register(PermissionTestModel) 13 | class PermissionTestModelAdmin(admin.ModelAdmin): 14 | pass 15 | 16 | 17 | # Activate globally for all models: 18 | admin.site.add_action(generate_test_code) 19 | 20 | 21 | @admin.register(OverwriteFileSystemStorageModel) 22 | class OverwriteFileSystemStorageModelAdmin(admin.ModelAdmin): 23 | pass 24 | 25 | 26 | @admin.register(VersioningTestModel) 27 | class VersioningTestModelModelAdmin(admin.ModelAdmin): 28 | fields = (('id', 'version'), 'user', 'name') 29 | readonly_fields = ('id',) 30 | -------------------------------------------------------------------------------- /django_tools_project/django_tools_test_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools_project/django_tools_test_app/migrations/__init__.py -------------------------------------------------------------------------------- /django_tools_project/django_tools_test_app/templates/mail_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

This is is a test mail.
6 | It used the django template: {{ foo }}, {{ bar }}

7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /django_tools_project/django_tools_test_app/templates/mail_test.txt: -------------------------------------------------------------------------------- 1 | This is is a test mail. 2 | It used the django template: {{ foo }}, {{ bar }} 3 | -------------------------------------------------------------------------------- /django_tools_project/django_tools_test_app/templates/test_template.html: -------------------------------------------------------------------------------- 1 | Hello {{ foo }} ! 2 | -------------------------------------------------------------------------------- /django_tools_project/django_tools_test_app/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | Dynamic SITE ID unittests 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | :copyleft: 2012-2018 by the django-tools team, see AUTHORS for more details. 6 | :license: GNU GPL v3 or above, see LICENSE for more details. 7 | """ 8 | 9 | import json 10 | import logging 11 | 12 | from django.conf import settings 13 | from django.contrib import messages 14 | from django.contrib.sites.models import Site 15 | from django.http import HttpResponse, HttpResponseRedirect 16 | from django.views.decorators.cache import never_cache 17 | from django.views.generic import TemplateView 18 | 19 | # https://github.com/jedie/django-tools 20 | from django_tools.debug.delay import SessionDelay 21 | from django_tools.middlewares.ThreadLocal import get_current_request 22 | 23 | 24 | log = logging.getLogger(__name__) 25 | 26 | 27 | @never_cache 28 | def display_site(request): 29 | settings_id = settings.SITE_ID 30 | current_site = Site.objects.get_current() 31 | current_id = current_site.id 32 | 33 | txt = f"ID from settings: {settings_id!r} - id from get_current(): {current_id!r}" 34 | log.debug("display_site(): %s", txt) 35 | return HttpResponse(txt) 36 | 37 | 38 | class ExampleException(Exception): 39 | pass 40 | 41 | 42 | def raise_exception(request, msg=""): 43 | """ 44 | This view just raises an exception as a way to test middleware exception 45 | handling. 46 | """ 47 | raise ExampleException(msg) 48 | 49 | 50 | def get_current_get_parameters(request): 51 | """ 52 | Returns a JSON version of request.GET from the current request 53 | """ 54 | return HttpResponse(json.dumps(get_current_request().GET)) 55 | 56 | 57 | class TemplateDoesNotExists(TemplateView): 58 | template_name = "/template/does/not/exists.html" 59 | 60 | 61 | def create_message_normal_response(request, msg): 62 | messages.info(request, msg) 63 | return HttpResponse("django_tools_project.django_tools_test_app.views.create_message_normal_response") 64 | 65 | 66 | def create_message_redirect_response(request, msg): 67 | messages.info(request, msg) 68 | return HttpResponseRedirect("/create_message_redirect_response/") 69 | 70 | 71 | def delay_view(request): 72 | """ 73 | Used in django_tools_tests.test_debug_delay.SessionDelayTests 74 | """ 75 | SessionDelay(request, key="delay_view", only_debug=False).load(request, query_string="sec", default=5) 76 | 77 | SessionDelay(request, key="delay_view").sleep() 78 | 79 | return HttpResponse("django_tools_project.django_tools_test_app.views.delay_view") 80 | -------------------------------------------------------------------------------- /django_tools_project/middlewares.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | from debug_toolbar.middleware import show_toolbar 5 | from django.conf import settings 6 | 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def djdt_show(request): 12 | """ 13 | Determining whether the Django Debug Toolbar should show or not. 14 | """ 15 | if not settings.DEBUG: 16 | return False 17 | 18 | if Path('/.dockerenv').exists(): 19 | # We run in a docker container 20 | # skip the `request.META['REMOTE_ADDR'] in settings.INTERNAL_IPS` check. 21 | return True 22 | 23 | return show_toolbar(request) 24 | -------------------------------------------------------------------------------- /django_tools_project/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools_project/settings/__init__.py -------------------------------------------------------------------------------- /django_tools_project/settings/local.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: E405 2 | 3 | """ 4 | Django settings for local development 5 | """ 6 | 7 | import os as __os 8 | import sys as __sys 9 | 10 | from django_tools_project.settings.prod import * # noqa 11 | 12 | 13 | # SECURITY WARNING: don't run with debug turned on in production! 14 | DEBUG = True 15 | 16 | 17 | # Serve static/media files for local development: 18 | SERVE_FILES = True 19 | 20 | 21 | # Disable caches: 22 | CACHES = {'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}} 23 | 24 | # Required for the debug toolbar to be displayed: 25 | INTERNAL_IPS = ('127.0.0.1', '0.0.0.0', 'localhost') 26 | 27 | ALLOWED_HOSTS = INTERNAL_IPS 28 | 29 | DATABASES = { 30 | 'default': { 31 | 'ENGINE': 'django.db.backends.sqlite3', 32 | 'NAME': str(BASE_PATH / 'django_tools-database.sqlite3'), 33 | # https://docs.djangoproject.com/en/dev/ref/databases/#database-is-locked-errors 34 | 'timeout': 30, 35 | } 36 | } 37 | print(f'Use Database: {DATABASES["default"]["NAME"]!r}', file=__sys.stderr) 38 | 39 | # _____________________________________________________________________________ 40 | 41 | if __os.environ.get('AUTOLOGIN') != '0': 42 | # Auto login for dev. server: 43 | MIDDLEWARE = MIDDLEWARE.copy() 44 | MIDDLEWARE += ['django_tools.middlewares.local_auto_login.AlwaysLoggedInAsSuperUserMiddleware'] 45 | 46 | # _____________________________________________________________________________ 47 | # Manage Django Project 48 | 49 | INSTALLED_APPS.append('manage_django_project') 50 | 51 | # _____________________________________________________________________________ 52 | # Django-Debug-Toolbar 53 | 54 | 55 | INSTALLED_APPS.append('debug_toolbar') 56 | MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware') 57 | 58 | DEBUG_TOOLBAR_PATCH_SETTINGS = True 59 | from debug_toolbar.settings import CONFIG_DEFAULTS as DEBUG_TOOLBAR_CONFIG # noqa 60 | 61 | 62 | # Disable some more panels that will slow down the page: 63 | DEBUG_TOOLBAR_CONFIG['DISABLE_PANELS'].add('debug_toolbar.panels.sql.SQLPanel') 64 | DEBUG_TOOLBAR_CONFIG['DISABLE_PANELS'].add('debug_toolbar.panels.cache.CachePanel') 65 | 66 | # don't load jquery from ajax.googleapis.com, just use django's version: 67 | DEBUG_TOOLBAR_CONFIG['JQUERY_URL'] = STATIC_URL + 'admin/js/vendor/jquery/jquery.min.js' 68 | 69 | DEBUG_TOOLBAR_CONFIG['SHOW_TEMPLATE_CONTEXT'] = True 70 | DEBUG_TOOLBAR_CONFIG['SHOW_COLLAPSED'] = True # Show toolbar collapsed by default. 71 | DEBUG_TOOLBAR_CONFIG['SHOW_TOOLBAR_CALLBACK'] = 'django_tools_project.middlewares.djdt_show' 72 | -------------------------------------------------------------------------------- /django_tools_project/settings/tests.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: E405 2 | """ 3 | Settings used to run tests 4 | """ 5 | from django_tools_project.settings.prod import * # noqa 6 | 7 | 8 | ALLOWED_HOSTS = ['testserver'] 9 | 10 | 11 | # _____________________________________________________________________________ 12 | # Manage Django Project 13 | 14 | INSTALLED_APPS.append('manage_django_project') 15 | 16 | # _____________________________________________________________________________ 17 | 18 | 19 | DATABASES = { 20 | 'default': { 21 | 'ENGINE': 'django.db.backends.sqlite3', 22 | 'NAME': ':memory:', 23 | } 24 | } 25 | 26 | SECRET_KEY = 'No individual secret for tests ;)' 27 | 28 | DEBUG = True 29 | 30 | # Speedup tests by change the Password hasher: 31 | PASSWORD_HASHERS = ('django.contrib.auth.hashers.MD5PasswordHasher',) 32 | 33 | # _____________________________________________________________________________ 34 | 35 | # All tests should use django-override-storage! 36 | # Set root to not existing path, so that wrong tests will fail: 37 | STATIC_ROOT = '/not/exists/static/' 38 | MEDIA_ROOT = '/not/exists/media/' 39 | -------------------------------------------------------------------------------- /django_tools_project/templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedie/django-tools/62cac0dd5c0baf7728e091bd8ce17f04ed82cecd/django_tools_project/templates/.gitkeep -------------------------------------------------------------------------------- /django_tools_project/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest.util 3 | from pathlib import Path 4 | 5 | from bx_py_utils.test_utils.deny_requests import deny_any_real_request 6 | from typeguard import install_import_hook 7 | 8 | 9 | # Check type annotations via typeguard in all tests: 10 | install_import_hook(packages=('django_tools', 'django_tools_project')) 11 | 12 | 13 | def pre_configure_tests() -> None: 14 | print(f'Configure unittests via "load_tests Protocol" from {Path(__file__).relative_to(Path.cwd())}') 15 | 16 | # Hacky way to display more "assert"-Context in failing tests: 17 | _MIN_MAX_DIFF = unittest.util._MAX_LENGTH - unittest.util._MIN_DIFF_LEN 18 | unittest.util._MAX_LENGTH = int(os.environ.get('UNITTEST_MAX_LENGTH', 600)) 19 | unittest.util._MIN_DIFF_LEN = unittest.util._MAX_LENGTH - _MIN_MAX_DIFF 20 | 21 | # Deny any request via docket/urllib3 because tests they should mock all requests: 22 | deny_any_real_request() 23 | 24 | 25 | def load_tests(loader, tests, pattern): 26 | """ 27 | Use unittest "load_tests Protocol" as a hook to setup test environment before running tests. 28 | https://docs.python.org/3/library/unittest.html#load-tests-protocol 29 | """ 30 | pre_configure_tests() 31 | return loader.discover(start_dir=Path(__file__).parent, pattern=pattern) 32 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_ThreadLocal_middleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | :copyleft: 2017-2019 by the django-tools team, see AUTHORS for more details. 3 | :license: GNU GPL v3 or above, see LICENSE for more details. 4 | """ 5 | 6 | import json 7 | 8 | from django.test import TestCase 9 | 10 | # https://github.com/jedie/django-tools 11 | from django_tools.middlewares.ThreadLocal import get_current_request 12 | from django_tools.unittest_utils.assertments import assert_pformat_equal 13 | from django_tools_project.django_tools_test_app.views import ExampleException 14 | 15 | 16 | class TestGetCurrentRequest(TestCase): 17 | def test_current_request_receives_current_request(self): 18 | response = self.client.get("/get_current_get_parameters/?") 19 | current_get_parameters = json.loads(response.content.decode("utf-8")) 20 | assert_pformat_equal(current_get_parameters, {}) 21 | response = self.client.get("/get_current_get_parameters/?foo=bar") 22 | current_get_parameters = json.loads(response.content.decode("utf-8")) 23 | assert_pformat_equal(current_get_parameters, {"foo": "bar"}) 24 | 25 | def test_current_request_is_cleared_after_request_is_finished(self): 26 | self.client.get("/get_current_get_parameters/") 27 | assert_pformat_equal(get_current_request(), None) 28 | 29 | def test_current_request_is_cleared_when_exception_is_raised(self): 30 | with self.assertRaises(ExampleException): 31 | self.client.get("/raise_exception/TestGetCurrentRequest/") 32 | assert_pformat_equal(get_current_request(), None) 33 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_TracebackLogMiddleware.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from django_tools_project.django_tools_test_app.views import ExampleException 4 | 5 | 6 | class TestTracebackLogMiddleware(TestCase): 7 | def test_exception_logging(self): 8 | with self.assertLogs(logger=None, level=None) as logs, self.assertRaises(ExampleException): 9 | self.client.get('/raise_exception/TestTracebackLogMiddleware/') 10 | 11 | output = '\n'.join([str(entry) for entry in logs.output]) 12 | 13 | assert 'Exception on url: /raise_exception/TestTracebackLogMiddleware/' in output 14 | assert 'Traceback (most recent call last):' in output 15 | assert 'django_tools_test_app/views.py' in output 16 | assert 'Exception: TestTracebackLogMiddleware' in output 17 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_admin.py: -------------------------------------------------------------------------------- 1 | from bx_django_utils.test_utils.html_assertion import ( 2 | HtmlAssertionMixin, 3 | assert_html_response_snapshot, 4 | get_django_name_suffix, 5 | ) 6 | from django.contrib.auth.models import User 7 | from django.test import TestCase 8 | from model_bakery import baker 9 | 10 | 11 | class AdminAnonymousTests(HtmlAssertionMixin, TestCase): 12 | """ 13 | Anonymous will be redirected to the login page. 14 | """ 15 | 16 | def test_login(self): 17 | response = self.client.get('/admin/') 18 | self.assertRedirects(response, expected_url='/admin/login/?next=/admin/') 19 | 20 | 21 | class AdminLoggedinTests(HtmlAssertionMixin, TestCase): 22 | """ 23 | Some basics test with the django admin 24 | """ 25 | 26 | @classmethod 27 | def setUpTestData(cls): 28 | cls.superuser = baker.make(User, username='superuser', is_staff=True, is_active=True, is_superuser=True) 29 | cls.staffuser = baker.make(User, username='staff_test_user', is_staff=True, is_active=True, is_superuser=False) 30 | 31 | def test_staff_admin_index(self): 32 | self.client.force_login(self.staffuser) 33 | 34 | response = self.client.get("/admin/") 35 | self.assert_html_parts( 36 | response, 37 | parts=( 38 | "

Site administration

", 39 | "staff_test_user", 40 | "

You don’t have permission to view or edit anything.

", 41 | ), 42 | ) 43 | self.assertTemplateUsed(response, template_name="admin/index.html") 44 | assert_html_response_snapshot(response, validate=False, name_suffix=get_django_name_suffix()) 45 | 46 | def test_superuser_admin_index(self): 47 | self.client.force_login(self.superuser) 48 | response = self.client.get("/admin/") 49 | self.assert_html_parts( 50 | response, 51 | parts=( 52 | "

Site administration

", 53 | "django_tools", 54 | "superuser", 55 | "/admin/auth/group/add/", 56 | "/admin/auth/user/add/", 57 | ), 58 | ) 59 | self.assertTemplateUsed(response, template_name="admin/index.html") 60 | assert_html_response_snapshot(response, validate=False, name_suffix=get_django_name_suffix()) 61 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_admin_staff_admin_index_django41_1.snapshot.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Site administration 4 |

5 |
6 | 7 |

8 | You don’t have permission to view or edit anything. 9 |

10 | 11 |
12 | 25 |
26 |
-------------------------------------------------------------------------------- /django_tools_project/tests/test_admin_staff_admin_index_django42_1.snapshot.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Site administration 4 |

5 |
6 | 7 |

8 | You don’t have permission to view or edit anything. 9 |

10 | 11 |
12 | 25 |
26 |
-------------------------------------------------------------------------------- /django_tools_project/tests/test_admin_staff_admin_index_django51_1.snapshot.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Site administration 4 |

5 |
6 | 7 |

8 | You don’t have permission to view or edit anything. 9 |

10 | 11 |
12 | 25 |
26 |
-------------------------------------------------------------------------------- /django_tools_project/tests/test_basics.py: -------------------------------------------------------------------------------- 1 | """ 2 | :copyleft: 2017-2020 by the django-tools team, see AUTHORS for more details. 3 | :created: 2017 by Jens Diemer 4 | :license: GNU GPL v3 or above, see LICENSE for more details. 5 | """ 6 | 7 | from django.conf import settings 8 | from django.core.management import call_command 9 | from django.test import SimpleTestCase 10 | 11 | # https://github.com/jedie/django-tools 12 | from django_tools.unittest_utils.stdout_redirect import StdoutStderrBuffer 13 | 14 | 15 | class SettingsTests(SimpleTestCase): 16 | def test_settings_module(self): 17 | self.assertIn('django_tools_project.settings.test', settings.SETTINGS_MODULE) 18 | 19 | def test_diffsettings(self): 20 | """ 21 | Check some settings 22 | """ 23 | with StdoutStderrBuffer() as buff: 24 | call_command('diffsettings') 25 | output = buff.get_output() 26 | print(output) 27 | self.assertIn('django_tools_project.settings.test', output) # SETTINGS_MODULE 28 | 29 | 30 | class ManageCheckTests(SimpleTestCase): 31 | def test_django_check(self): 32 | """ 33 | call './manage.py check' directly via 'call_command' 34 | """ 35 | with StdoutStderrBuffer() as buff: 36 | call_command('check') 37 | output = buff.get_output() 38 | self.assertIn('System check identified no issues (0 silenced).', output) 39 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_database_info.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test /django_tools/management/commands/database_info.py 3 | 4 | :copyleft: 2017-2022 by the django-tools team, see AUTHORS for more details. 5 | :created: 2017 by Jens Diemer 6 | :license: GNU GPL v3 or above, see LICENSE for more details. 7 | """ 8 | 9 | from unittest import TestCase 10 | 11 | from cli_base.cli_tools.test_utils.assertion import assert_in 12 | from django.test import SimpleTestCase 13 | 14 | from django_tools.management.commands import database_info 15 | from django_tools.unittest_utils.call_management_commands import captured_call_command 16 | from django_tools.unittest_utils.django_command import DjangoCommandMixin 17 | from django_tools_project.constants import PACKAGE_ROOT 18 | 19 | 20 | class DatabaseInfoCallCommandTests(SimpleTestCase): 21 | """ 22 | Test directly via django.core.management.call_command() 23 | Note: 24 | So the test database settings is active. 25 | """ 26 | 27 | def test_database_info(self): 28 | output, stderr = captured_call_command(command=database_info) 29 | assert stderr == '' 30 | assert_in( 31 | content=output, 32 | parts=( 33 | "engine...............: 'sqlite3'", 34 | "There are 1 connections.", 35 | "'CONN_MAX_AGE': 0,", 36 | ), 37 | ) 38 | self.assertTrue( 39 | "name.................: ':memory:'" in output 40 | or "name.................: 'file:memorydb_default?mode=memory&cache=shared'" in output 41 | ) 42 | 43 | 44 | class DatabaseInfoSubprocessTests(DjangoCommandMixin, TestCase): 45 | """ 46 | Test command via "manage.py" subprocess 47 | Note: 48 | So the real database settings is active and not the test settings! 49 | """ 50 | 51 | def test_database_info(self): 52 | output = self.call_manage_py(["database_info"], manage_dir=PACKAGE_ROOT) 53 | assert_in( 54 | content=output, 55 | parts=( 56 | "engine...............: 'sqlite3'", 57 | "name.................: ':memory:'", 58 | "There are 1 connections.", 59 | "'CONN_MAX_AGE': 0,", 60 | ), 61 | ) 62 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_doctest.py: -------------------------------------------------------------------------------- 1 | from bx_py_utils.test_utils.unittest_utils import BaseDocTests 2 | 3 | import django_tools 4 | import django_tools_project 5 | 6 | 7 | class DocTests(BaseDocTests): 8 | def test_doctests(self): 9 | self.run_doctests( 10 | modules=(django_tools, django_tools_project), 11 | ) 12 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_email.py: -------------------------------------------------------------------------------- 1 | """ 2 | :copyleft: 2017-2019 by the django-tools team, see AUTHORS for more details. 3 | :license: GNU GPL v3 or above, see LICENSE for more details. 4 | """ 5 | 6 | from django.core import mail 7 | from django.core.mail import EmailMultiAlternatives 8 | from django.test import SimpleTestCase 9 | 10 | # https://github.com/jedie/django-tools 11 | from django_tools.mail.send_mail import SendMail 12 | from django_tools.unittest_utils.assertments import assert_equal_dedent, assert_pformat_equal 13 | from django_tools.unittest_utils.email import print_mailbox 14 | from django_tools.unittest_utils.unittest_base import BaseUnittestCase 15 | 16 | 17 | class TestEMail(BaseUnittestCase, SimpleTestCase): 18 | def test_no_recipient(self): 19 | with self.assertRaises(AssertionError): 20 | SendMail( 21 | template_base="mail_test.{ext}", 22 | mail_context={"foo": "first", "bar": "second"}, 23 | subject="Only a test", 24 | recipient_list=None, 25 | ) 26 | with self.assertRaises(AssertionError): 27 | SendMail( 28 | template_base="mail_test.{ext}", 29 | mail_context={"foo": "first", "bar": "second"}, 30 | subject="Only a test", 31 | recipient_list=[], 32 | ) 33 | 34 | def test_SendMail(self): 35 | assert_pformat_equal(len(mail.outbox), 0) 36 | 37 | ok = SendMail( 38 | template_base="mail_test.{ext}", 39 | mail_context={"foo": "first", "bar": "second"}, 40 | subject="Only a test", 41 | recipient_list="foo@bar.tld", 42 | ).send() 43 | 44 | assert_pformat_equal(ok, True) 45 | 46 | print_mailbox(mail.outbox) 47 | 48 | assert_pformat_equal(len(mail.outbox), 1) 49 | email = mail.outbox[0] 50 | assert_pformat_equal(email.subject, "Only a test") 51 | 52 | assert_equal_dedent( 53 | email.body, 54 | """ 55 | 56 | This is is a test mail. 57 | It used the django template: first, second 58 | 59 | 60 | """, 61 | ) 62 | assert_pformat_equal(email.from_email, "webmaster@localhost") 63 | assert_pformat_equal(email.to, ["foo@bar.tld"]) 64 | 65 | self.assertIsInstance(email, EmailMultiAlternatives) 66 | 67 | html_email = email.alternatives[0] 68 | 69 | assert_equal_dedent( 70 | html_email[0].strip(), 71 | """ 72 | 73 | 74 | 75 | 76 | 77 |

This is is a test mail.
78 | It used the django template: first, second

79 | 80 | 81 | 82 | 83 | 84 | """, 85 | ) 86 | assert_pformat_equal(html_email[1], "text/html") 87 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_filesystem_browser.py: -------------------------------------------------------------------------------- 1 | """ 2 | :copyleft: 2017-2019 by the django-tools team, see AUTHORS for more details. 3 | :license: GNU GPL v3 or above, see LICENSE for more details. 4 | """ 5 | 6 | import os 7 | 8 | from django.http import Http404 9 | from django.test import SimpleTestCase 10 | from django.test.utils import override_settings 11 | 12 | # https://github.com/jedie/django-tools 13 | import django_tools 14 | from django_tools.filemanager.filesystem_browser import BaseFilesystemBrowser 15 | from django_tools.unittest_utils.assertments import assert_pformat_equal 16 | 17 | 18 | @override_settings(DEBUG=False) 19 | class TestFilesystemBrowser(SimpleTestCase): 20 | """ 21 | e.g.: 22 | https://en.wikipedia.org/wiki/Directory_traversal_attack 23 | """ 24 | 25 | BASE_PATH = os.path.abspath(os.path.dirname(django_tools.__file__)) 26 | 27 | def test_directory_traversal_attack1(self): 28 | try: 29 | BaseFilesystemBrowser(request=None, absolute_path=self.BASE_PATH, base_url="bar", rest_url="../") 30 | except Http404 as err: 31 | assert_pformat_equal(str(err), "Directory doesn't exist!") 32 | 33 | @override_settings(DEBUG=True) 34 | def test_debug_message(self): 35 | sub_path = os.path.normpath(os.path.join(self.BASE_PATH, "..")) 36 | try: 37 | BaseFilesystemBrowser(request=None, absolute_path=self.BASE_PATH, base_url="bar", rest_url="../") 38 | except Http404 as err: 39 | assert_pformat_equal( 40 | err.args[0].message, f"Directory '{sub_path}' is not in base path ('{self.BASE_PATH}')" 41 | ) 42 | 43 | def test_directory_traversal_attack_encodings(self): 44 | rest_urls = ( 45 | "/etc/passwd", 46 | "..", 47 | "../", 48 | "\\\\", 49 | # URI encoded directory traversal: 50 | "%2e%2e%2f", # ../ 51 | "%2e%2e/", # ../ 52 | "..%2f", # ../ 53 | "%2e%2e%5c", # ..\ 54 | # Unicode / UTF-8 encoded directory traversal: 55 | "..%c1%1c", # ../ 56 | "..%c0%af", # ..\ 57 | "%c0%ae%c0%ae%c1%1c", # %c0%ae -> . -> ../ 58 | "%c0%ae%c0%ae%c0%af", # %c0%ae -> . -> ..\ 59 | ) 60 | for rest_url in rest_urls: 61 | # print(rest_url) 62 | try: 63 | BaseFilesystemBrowser(request=None, absolute_path=self.BASE_PATH, base_url="bar", rest_url=rest_url) 64 | except Http404 as err: 65 | assert_pformat_equal(str(err), "Directory doesn't exist!") 66 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_html_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | :copyleft: 2017-2019 by the django-tools team, see AUTHORS for more details. 3 | :license: GNU GPL v3 or above, see LICENSE for more details. 4 | """ 5 | 6 | # https://github.com/jedie/django-tools 7 | from django_tools.unittest_utils.assertments import assert_pformat_equal 8 | from django_tools.unittest_utils.unittest_base import BaseUnittestCase 9 | from django_tools.utils.html_utils import html2text 10 | 11 | 12 | class TestHtmlUtils(BaseUnittestCase): 13 | def test_none(self): 14 | assert_pformat_equal(html2text(None), None) 15 | 16 | def test_empty(self): 17 | assert_pformat_equal(html2text(""), "") 18 | 19 | def test_text_only(self): 20 | assert_pformat_equal(html2text("foo"), "foo") 21 | 22 | def test_invalid_tags(self): 23 | assert_pformat_equal(html2text("the text"), "the text") 24 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_installed_apps_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | :copyleft: 2017-2019 by the django-tools team, see AUTHORS for more details. 3 | :license: GNU GPL v3 or above, see LICENSE for more details. 4 | """ 5 | 6 | from django.test import SimpleTestCase 7 | 8 | # https://github.com/jedie/django-tools 9 | from django_tools.unittest_utils.assertments import assert_pformat_equal 10 | from django_tools.utils.installed_apps_utils import get_filtered_apps 11 | 12 | 13 | class TestGetFilteredApps(SimpleTestCase): 14 | def test_get_filtered_apps1(self): 15 | apps = get_filtered_apps() 16 | assert_pformat_equal(apps, []) 17 | 18 | def test_get_filtered_apps2(self): 19 | apps = get_filtered_apps(no_args=False) 20 | assert_pformat_equal(apps, ["django.contrib.flatpages"]) 21 | 22 | def test_get_filtered_apps3(self): 23 | apps = get_filtered_apps( 24 | resolve_url="login/", 25 | # debug=True 26 | ) 27 | assert_pformat_equal(apps, ["django.contrib.auth"]) 28 | 29 | def test_get_filtered_apps4(self): 30 | apps = get_filtered_apps(resolve_url="login/", no_args=False) 31 | assert_pformat_equal(apps, ["django.contrib.auth", "django.contrib.flatpages"]) 32 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_isolated_filesystem.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from pathlib import Path 3 | from unittest import TestCase 4 | 5 | from django_tools.unittest_utils import assertments 6 | from django_tools.unittest_utils.isolated_filesystem import isolated_filesystem 7 | 8 | 9 | CWD = Path().cwd() 10 | 11 | 12 | class TestIsolatedFilesystem(TestCase): 13 | def setUp(self): 14 | super().setUp() 15 | 16 | # set in self.assert_in_isolated_filesystem() 17 | self.temp_cwd = None 18 | self.temp_file = None 19 | 20 | def assert_in_isolated_filesystem(self, prefix=None): 21 | self.temp_cwd = Path().cwd() 22 | self.assertNotEqual(self.temp_cwd, CWD) 23 | 24 | temp_dir = tempfile.gettempdir() 25 | assertments.assert_startswith(str(self.temp_cwd), temp_dir) 26 | 27 | if prefix is not None: 28 | reference = Path(temp_dir, prefix) 29 | assertments.assert_startswith(str(self.temp_cwd), str(reference)) 30 | 31 | self.temp_file = Path(self.temp_cwd, "test_file.txt") 32 | 33 | with self.temp_file.open("w") as f: 34 | f.write(prefix or "no prefix") 35 | 36 | self.assertTrue(self.temp_file.is_file()) 37 | 38 | def assert_after(self): 39 | self.assertTrue(self.temp_cwd is not None, "ERROR: 'self.assert_in_isolated_filesystem()' not called?!?") 40 | self.assertFalse(self.temp_cwd.is_dir(), f"ERROR: {self.temp_cwd!r} still exists?!?") 41 | self.assertFalse(self.temp_file.is_file(), f"ERROR: {self.temp_file!r} still exists!") 42 | 43 | def test_as_context_manager(self): 44 | prefix = "test_as_context_manager" 45 | with isolated_filesystem(prefix=prefix): 46 | self.assert_in_isolated_filesystem(prefix) 47 | 48 | self.assert_after() 49 | 50 | def test_as_func_decotator(self): 51 | @isolated_filesystem() 52 | def in_isolated_filesystem(): 53 | self.assert_in_isolated_filesystem(prefix="in_isolated_filesystem") 54 | 55 | in_isolated_filesystem() 56 | self.assert_after() 57 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_log_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | created 25.09.2019 by Jens Diemer 3 | :copyleft: 2019 by the django-tools team, see AUTHORS for more details. 4 | :license: GNU GPL v3 or above, see LICENSE for more details. 5 | """ 6 | 7 | import time 8 | 9 | from django.core import mail 10 | from django.test import SimpleTestCase, override_settings 11 | 12 | # https://github.com/jedie/django-tools 13 | from django_tools.log_utils.throttle_admin_email_handler import ThrottledAdminEmailHandler 14 | from django_tools.unittest_utils.email import print_mailbox 15 | 16 | 17 | class ThrottledAdminEmailHandlerTestCase(SimpleTestCase): 18 | 19 | @override_settings(ADMINS=(("Mr. Test", "foo.bar@example.tld"),)) 20 | def test_throttle_admin_email_handler(self): 21 | handler = ThrottledAdminEmailHandler(min_delay_sec=0.1) 22 | 23 | for char in ("A", "B"): 24 | for no in range(3): 25 | handler.send_mail(subject=f"subject {char} {no:d}", message=f"message {char} {no:d}") 26 | 27 | time.sleep(0.11) 28 | handler.send_mail(subject=f"subject {char} last", message=f"message {char} last") 29 | time.sleep(0.11) 30 | 31 | print_mailbox(mail.outbox) 32 | assert len(mail.outbox) == 4 33 | 34 | ########################################################################## 35 | # 1. "normal" mail: 36 | 37 | email = mail.outbox[0] 38 | assert "subject A 0" in email.subject 39 | assert "message A 0" in email.body 40 | assert "skipped mails" not in email.body 41 | 42 | ########################################################################## 43 | # 2. mail with skipped mails: 44 | 45 | email = mail.outbox[1] 46 | assert "subject A last" in email.subject 47 | 48 | assert "2 skipped mails" in email.body 49 | assert "* subject A 1" in email.body 50 | assert "* subject A 2" in email.body 51 | 52 | assert "message A last" in email.body 53 | 54 | ########################################################################## 55 | # 3. "normal" mail: 56 | 57 | email = mail.outbox[2] 58 | assert "subject B 0" in email.subject 59 | assert "message B 0" in email.body 60 | assert "skipped mails" not in email.body 61 | 62 | ########################################################################## 63 | # 4. mail with skipped mails: 64 | 65 | email = mail.outbox[3] 66 | assert "subject B last" in email.subject 67 | 68 | assert "2 skipped mails" in email.body 69 | assert "* subject B 1" in email.body 70 | assert "* subject B 2" in email.body 71 | 72 | assert "message B last" in email.body 73 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_log_utils_syslog_handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import syslog 3 | from unittest import TestCase, mock 4 | 5 | from django_tools.context_managers import MassContextManagerBase 6 | from django_tools.log_utils import syslog_handler 7 | from django_tools.log_utils.syslog_handler import SyslogHandler 8 | 9 | 10 | class SyslogMock: 11 | def __init__(self): 12 | self._messages = [] 13 | self._level_map = { 14 | syslog.LOG_EMERG: 'LOG_EMERG', 15 | syslog.LOG_ERR: 'LOG_ERR', 16 | syslog.LOG_WARNING: 'LOG_WARNING', 17 | syslog.LOG_INFO: 'LOG_INFO', 18 | syslog.LOG_DEBUG: 'LOG_DEBUG', 19 | } 20 | 21 | def __getattribute__(self, item): 22 | try: 23 | return super().__getattribute__(item) 24 | except AttributeError: 25 | return getattr(syslog, item) 26 | 27 | def syslog(self, priority, message): 28 | priority_name = self._level_map[priority] 29 | self._messages.append((priority_name, message)) 30 | 31 | 32 | class SyslogHandlerMock(MassContextManagerBase): 33 | def __init__(self): 34 | self.syslog_mock = SyslogMock() 35 | 36 | def __enter__(self): 37 | self.context_managers = [mock.patch.object(syslog_handler, 'syslog', self.syslog_mock)] 38 | return super().__enter__() 39 | 40 | def get_messages(self): 41 | return self.syslog_mock._messages 42 | 43 | 44 | class SyslogHandlerTestCase(TestCase): 45 | def setUp(self) -> None: 46 | logger = logging.getLogger(__file__) 47 | handler = SyslogHandler() 48 | handler.setLevel(1) 49 | logger.addHandler(handler) 50 | logger.setLevel(1) 51 | self.logger = logger 52 | 53 | def tearDown(self) -> None: 54 | self.logger.handlers.clear() 55 | 56 | def test_syslog_handler(self): 57 | with SyslogHandlerMock() as m: 58 | self.logger.debug('Test DEBUG message') 59 | self.logger.info('Test INFO message') 60 | self.logger.warning('Test WARNING message') 61 | self.logger.error('Test ERROR message') 62 | self.logger.critical('Test CRITICAL message') 63 | self.logger.exception('Test a exception log message') 64 | self.logger.log(level=1, msg='Test level=1 log message') 65 | self.logger.log(level=99, msg='Test level=99 log message') 66 | self.logger.log(level=logging.NOTSET, msg='Test NOTSET message') 67 | 68 | messages = m.get_messages() 69 | self.assertEqual( 70 | messages, 71 | [ 72 | ('LOG_DEBUG', 'Test DEBUG message'), 73 | ('LOG_INFO', 'Test INFO message'), 74 | ('LOG_WARNING', 'Test WARNING message'), 75 | ('LOG_ERR', 'Test ERROR message'), 76 | ('LOG_EMERG', 'Test CRITICAL message'), 77 | ('LOG_ERR', 'Test a exception log message'), 78 | ('LOG_ERR', 'Log level 1 (Level 1) is unknown, fallback to WARNING.'), 79 | ('LOG_WARNING', 'Test level=1 log message'), 80 | ('LOG_ERR', 'Log level 99 (Level 99) is unknown, fallback to WARNING.'), 81 | ('LOG_WARNING', 'Test level=99 log message'), 82 | ], 83 | ) 84 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_logging_utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django_tools.unittest_utils.logging_utils import FilterAndLogWarnings 4 | from django_tools.unittest_utils.unittest_base import BaseUnittestCase 5 | 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | 10 | class TestLoggingUtilsTestCase(BaseUnittestCase): 11 | def test_filter_and_log_warnings_direct_call(self): 12 | instance = FilterAndLogWarnings() 13 | with self.assertLogs(logger="django_tools.unittest_utils.logging_utils", level=logging.DEBUG) as logs: 14 | instance( 15 | message="test_filter_and_log_warnings_direct_call", 16 | category=UserWarning, 17 | filename="/foo/bar.py", 18 | lineno=123, 19 | ) 20 | 21 | assert logs.output == [ 22 | "WARNING:django_tools.unittest_utils.logging_utils:UserWarning:test_filter_and_log_warnings_direct_call", 23 | ] 24 | 25 | def test_filter_and_log_skip_external_package(self): 26 | instance = FilterAndLogWarnings() 27 | with self.assertLogs(logger="django_tools.unittest_utils.logging_utils", level=logging.DEBUG) as logs: 28 | for i in range(3): 29 | instance( 30 | message=f"test_filter_and_log_skip_external_package dist-packages {i:d}", 31 | category=UserWarning, 32 | filename="/foo/dist-packages/bar.py", 33 | lineno=456, 34 | ) 35 | instance( 36 | message=f"test_filter_and_log_skip_external_package site-packages {i:d}", 37 | category=UserWarning, 38 | filename="/foo/site-packages/bar.py", 39 | lineno=789, 40 | ) 41 | 42 | assert logs.output == [ 43 | "WARNING:django_tools.unittest_utils.logging_utils:There are warnings in: /foo/dist-packages/bar.py", 44 | "WARNING:django_tools.unittest_utils.logging_utils:There are warnings in: /foo/site-packages/bar.py", 45 | ] 46 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_mockup.py: -------------------------------------------------------------------------------- 1 | """ 2 | test Mockups 3 | ~~~~~~~~~~~~ 4 | 5 | :copyleft: 2018-2019 by the django-tools team, see AUTHORS for more details. 6 | :license: GNU GPL v3 or above, see LICENSE for more details. 7 | """ 8 | 9 | import io 10 | 11 | from bx_django_utils.test_utils.html_assertion import HtmlAssertionMixin 12 | from cli_base.cli_tools.test_utils.assertion import assert_startswith 13 | from django.conf import settings 14 | from django.core.files import File as DjangoFile 15 | from django.test import TestCase 16 | 17 | from django_tools.unittest_utils.assertments import assert_endswith, assert_pformat_equal 18 | from django_tools.unittest_utils.mockup import ImageDummy 19 | from django_tools.unittest_utils.temp_media_root import TempMediaRoot 20 | from django_tools.unittest_utils.user import TestUserMixin 21 | 22 | 23 | @TempMediaRoot() # Move settings.MEDIA_ROOT into /tmp/ 24 | class TestMockupImage(TestUserMixin, HtmlAssertionMixin, TestCase): 25 | def test_create_pil_image(self): 26 | pil_image = ImageDummy(width=300, height=150).create_pil_image() 27 | 28 | assert_pformat_equal(pil_image.width, 300) 29 | assert_pformat_equal(pil_image.height, 150) 30 | 31 | assert_pformat_equal(pil_image.verify(), None) 32 | 33 | def test_create_info_image(self): 34 | image_dummy = ImageDummy(width=400, height=200) 35 | assert_pformat_equal(image_dummy.text_color, "#ffffff") 36 | assert_pformat_equal(image_dummy.text_align, "center") 37 | pil_image = image_dummy.create_info_image(text="foo") 38 | 39 | assert_pformat_equal(pil_image.width, 400) 40 | assert_pformat_equal(pil_image.height, 200) 41 | 42 | assert_pformat_equal(pil_image.verify(), None) 43 | 44 | def test_create_temp_filer_info_image(self): 45 | assert_endswith(settings.MEDIA_ROOT, '/django-tools/media') 46 | user = self.login(usertype="normal") 47 | 48 | filer_info_image = ImageDummy(width=500, height=300, format="png").create_temp_filer_info_image( 49 | text="A test image", user=user 50 | ) 51 | assert_pformat_equal(filer_info_image.width, 500) 52 | assert_pformat_equal(filer_info_image.height, 300) 53 | 54 | assert_pformat_equal(filer_info_image.size, 1791) 55 | self.assertIn(".png", filer_info_image.path) 56 | 57 | assert_startswith(filer_info_image.path, prefix=settings.MEDIA_ROOT) 58 | 59 | def test_create_django_file_info_image(self): 60 | django_file = ImageDummy(width=100, height=50).create_django_file_info_image(text="foo bar") 61 | 62 | self.assertIsInstance(django_file, DjangoFile) 63 | 64 | def test_in_memory_image_file(self): 65 | img = ImageDummy(width=1, height=1, format='png').in_memory_image_file(filename='test.png') 66 | assert isinstance(img, io.BytesIO) 67 | assert img.name == 'test.png' 68 | assert img.tell() == 0 69 | assert img.read().startswith(b'\x89PNG\r\n') 70 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_model_test_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | :created: 24.04.2018 by Jens Diemer 3 | :copyleft: 2018-2019 by the django-tools team, see AUTHORS for more details. 4 | :license: GNU GPL v3 or above, see LICENSE for more details. 5 | """ 6 | 7 | from bx_django_utils.test_utils.html_assertion import HtmlAssertionMixin 8 | from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME 9 | from django.contrib.auth.models import User 10 | from django.test import TestCase 11 | 12 | from django_tools.unittest_utils.assertments import assert_pformat_equal 13 | from django_tools.unittest_utils.user import TestUserMixin 14 | 15 | 16 | class ModelTestGeneratorAdminTestCase(TestUserMixin, HtmlAssertionMixin, TestCase): 17 | 18 | def test_generate_code_action_exists(self): 19 | self.login(usertype="superuser") 20 | 21 | response = self.client.get("/admin/auth/user/") 22 | self.assert_html_parts( 23 | response, 24 | parts=( 25 | 'superuser', 26 | '', 27 | ), 28 | ) 29 | self.assertTemplateUsed(response, "admin/change_list.html") 30 | 31 | def test_generate_code(self): 32 | self.login(usertype="superuser") 33 | 34 | user_pks = User.objects.all().values_list("pk", flat=True) 35 | 36 | response = self.client.post( 37 | path="/admin/auth/user/", 38 | data={"action": "generate_test_code", ACTION_CHECKBOX_NAME: user_pks, "index": 0}, 39 | HTTP_ACCEPT_LANGUAGE="en", 40 | ) 41 | # TODO: check content-type and filename! 42 | 43 | content = response.content.decode() 44 | must_contain = ( 45 | 'from auth.User', 46 | 'django.contrib.auth.models.User', 47 | "user = User.objects.create(", 48 | "username='normal_test_user', # CharField, String (up to 150)", 49 | "username='staff_test_user', # CharField, String (up to 150)", 50 | "username='superuser', # CharField, String (up to 150)", 51 | ) 52 | for part in must_contain: 53 | self.assertIn(part, content) 54 | 55 | headers = response.headers 56 | assert_pformat_equal(headers['content-disposition'], 'attachment; filename=auth.User.py') 57 | assert_pformat_equal(headers['content-type'], 'text/python') 58 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_model_utils.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | 3 | from django.test import TestCase 4 | 5 | from django_tools.model_utils import compare_model_instance, serialize_instance 6 | from django_tools.unittest_utils.assertments import assert_pformat_equal 7 | from django_tools_project.django_tools_test_app.models import VersioningTestModel 8 | 9 | 10 | class ModelUtilsTestCase(TestCase): 11 | def test_serialize_instance(self): 12 | instance = VersioningTestModel(pk=UUID('00000000-0000-0000-1111-000000000001'), name='Foo Bar') 13 | result = serialize_instance(instance) 14 | assert_pformat_equal(result, {'name': 'Foo Bar', 'user': None, 'version': 0}) 15 | 16 | def test_compare_model_instance(self): 17 | instance1 = VersioningTestModel(pk=UUID('00000000-0000-0000-1111-000000000001'), name='Instance One Name') 18 | instance2 = VersioningTestModel(pk=UUID('00000000-0000-0000-1111-000000000001'), name='Instance Two Name') 19 | result = list(compare_model_instance(instance1, instance2)) 20 | assert_pformat_equal(result, [('name', 'Instance One Name', 'Instance Two Name')]) 21 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_parler_fixtures.py: -------------------------------------------------------------------------------- 1 | """ 2 | :copyleft: 2017-2019 by the django-tools team, see AUTHORS for more details. 3 | :license: GNU GPL v3 or above, see LICENSE for more details. 4 | """ 5 | 6 | from django.conf import settings 7 | from django.test import TestCase 8 | from django.utils import translation 9 | from pprintpp import pprint 10 | 11 | # https://github.com/jedie/django-tools 12 | from django_tools.unittest_utils.assertments import assert_pformat_equal 13 | from django_tools_project.django_tools_test_app.models import SimpleParlerModel, generate_simple_parler_dummies 14 | 15 | 16 | class ParlerFixturesTestCase(TestCase): 17 | def set_setup(self): 18 | assert_pformat_equal(settings.LANGUAGES, (("de", "German"), ("en", "English"))) 19 | 20 | def test_generate_simple_parler_dummies(self): 21 | generate_simple_parler_dummies() 22 | 23 | assert_pformat_equal(SimpleParlerModel.objects.all().count(), 5) 24 | 25 | with translation.override("en"): 26 | qs = SimpleParlerModel.objects.language(language_code="en").all() 27 | 28 | info = [] 29 | for instance in qs: 30 | info.append((instance.slug, list(instance.get_available_languages()))) 31 | 32 | pprint(info) 33 | 34 | # Every dummy entry should be translated in de and en: 35 | assert_pformat_equal( 36 | info, 37 | [ 38 | ("simpleparlermodel-en-1", ["de", "en"]), 39 | ("simpleparlermodel-en-2", ["de", "en"]), 40 | ("simpleparlermodel-en-3", ["de", "en"]), 41 | ("simpleparlermodel-en-4", ["de", "en"]), 42 | ("simpleparlermodel-en-5", ["de", "en"]), 43 | ], 44 | ) 45 | 46 | # TODO: 47 | # def test_generate_parler_publisher_dummies(self): 48 | # generate_parler_publisher_dummies() 49 | # 50 | # assert_pformat_equal(ParlerPublisherModel.objects.all().count(), 5) 51 | # 52 | # with translation.override("en"): 53 | # qs = ParlerPublisherModel.objects.language(language_code="en").all().published() 54 | # 55 | # info = [] 56 | # for instance in qs: 57 | # info.append( 58 | # (instance.slug, list(instance.get_available_languages())) 59 | # ) 60 | # 61 | # pprint(info) 62 | # 63 | # # Every dummy entry should be translated in de and en: 64 | # assert_pformat_equal(info, [ 65 | # ('simpleparlermodel-en-1', ['de', 'en']), 66 | # ('simpleparlermodel-en-2', ['de', 'en']), 67 | # ('simpleparlermodel-en-3', ['de', 'en']), 68 | # ('simpleparlermodel-en-4', ['de', 'en']), 69 | # ('simpleparlermodel-en-5', ['de', 'en']) 70 | # ]) 71 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_permissions_add_app_permissions_1.snapshot.json: -------------------------------------------------------------------------------- 1 | [ 2 | "django_tools_test_app.add_limittousergroupstestmodel", 3 | "django_tools_test_app.change_limittousergroupstestmodel", 4 | "django_tools_test_app.delete_limittousergroupstestmodel", 5 | "django_tools_test_app.view_limittousergroupstestmodel", 6 | "django_tools_test_app.add_overwritefilesystemstoragemodel", 7 | "django_tools_test_app.change_overwritefilesystemstoragemodel", 8 | "django_tools_test_app.delete_overwritefilesystemstoragemodel", 9 | "django_tools_test_app.view_overwritefilesystemstoragemodel", 10 | "django_tools_test_app.add_permissiontestmodel", 11 | "django_tools_test_app.change_permissiontestmodel", 12 | "django_tools_test_app.delete_permissiontestmodel", 13 | "django_tools_test_app.extra_permission", 14 | "django_tools_test_app.view_permissiontestmodel", 15 | "django_tools_test_app.add_simpleparlermodel", 16 | "django_tools_test_app.change_simpleparlermodel", 17 | "django_tools_test_app.delete_simpleparlermodel", 18 | "django_tools_test_app.view_simpleparlermodel", 19 | "django_tools_test_app.add_usermediafiles", 20 | "django_tools_test_app.change_usermediafiles", 21 | "django_tools_test_app.delete_usermediafiles", 22 | "django_tools_test_app.view_usermediafiles", 23 | "django_tools_test_app.add_versioningtestmodel", 24 | "django_tools_test_app.change_versioningtestmodel", 25 | "django_tools_test_app.delete_versioningtestmodel", 26 | "django_tools_test_app.view_versioningtestmodel" 27 | ] -------------------------------------------------------------------------------- /django_tools_project/tests/test_permissions_get_admin_permissions_1.snapshot.json: -------------------------------------------------------------------------------- 1 | [ 2 | "auth.add_group", 3 | "auth.change_group", 4 | "auth.delete_group", 5 | "auth.view_group", 6 | "auth.add_user", 7 | "auth.change_user", 8 | "auth.delete_user", 9 | "auth.view_user", 10 | "django_tools_test_app.add_overwritefilesystemstoragemodel", 11 | "django_tools_test_app.change_overwritefilesystemstoragemodel", 12 | "django_tools_test_app.delete_overwritefilesystemstoragemodel", 13 | "django_tools_test_app.view_overwritefilesystemstoragemodel", 14 | "django_tools_test_app.add_permissiontestmodel", 15 | "django_tools_test_app.change_permissiontestmodel", 16 | "django_tools_test_app.delete_permissiontestmodel", 17 | "django_tools_test_app.extra_permission", 18 | "django_tools_test_app.view_permissiontestmodel", 19 | "django_tools_test_app.add_versioningtestmodel", 20 | "django_tools_test_app.change_versioningtestmodel", 21 | "django_tools_test_app.delete_versioningtestmodel", 22 | "django_tools_test_app.view_versioningtestmodel", 23 | "flatpages.add_flatpage", 24 | "flatpages.change_flatpage", 25 | "flatpages.delete_flatpage", 26 | "flatpages.view_flatpage", 27 | "sites.add_site", 28 | "sites.change_site", 29 | "sites.delete_site", 30 | "sites.view_site" 31 | ] -------------------------------------------------------------------------------- /django_tools_project/tests/test_permissions_get_filtered_permissions_1.snapshot.json: -------------------------------------------------------------------------------- 1 | [ 2 | "auth.add_group", 3 | "auth.change_group", 4 | "auth.view_group", 5 | "auth.add_permission", 6 | "auth.change_permission", 7 | "auth.delete_permission", 8 | "auth.view_permission", 9 | "auth.add_user", 10 | "auth.change_user", 11 | "auth.view_user", 12 | "contenttypes.change_contenttype", 13 | "contenttypes.view_contenttype", 14 | "django_tools_test_app.add_overwritefilesystemstoragemodel", 15 | "django_tools_test_app.change_overwritefilesystemstoragemodel", 16 | "django_tools_test_app.delete_overwritefilesystemstoragemodel", 17 | "django_tools_test_app.view_overwritefilesystemstoragemodel", 18 | "django_tools_test_app.add_simpleparlermodel", 19 | "django_tools_test_app.change_simpleparlermodel", 20 | "django_tools_test_app.delete_simpleparlermodel", 21 | "django_tools_test_app.view_simpleparlermodel", 22 | "django_tools_test_app.add_usermediafiles", 23 | "django_tools_test_app.change_usermediafiles", 24 | "django_tools_test_app.delete_usermediafiles", 25 | "django_tools_test_app.view_usermediafiles", 26 | "django_tools_test_app.add_versioningtestmodel", 27 | "django_tools_test_app.change_versioningtestmodel", 28 | "django_tools_test_app.delete_versioningtestmodel", 29 | "django_tools_test_app.view_versioningtestmodel", 30 | "flatpages.add_flatpage", 31 | "flatpages.change_flatpage", 32 | "flatpages.delete_flatpage", 33 | "flatpages.view_flatpage", 34 | "serve_media_app.add_usermediatokenmodel", 35 | "serve_media_app.change_usermediatokenmodel", 36 | "serve_media_app.delete_usermediatokenmodel", 37 | "serve_media_app.view_usermediatokenmodel", 38 | "sessions.add_session", 39 | "sessions.change_session", 40 | "sessions.delete_session", 41 | "sessions.view_session", 42 | "sites.add_site", 43 | "sites.change_site", 44 | "sites.delete_site", 45 | "sites.view_site" 46 | ] -------------------------------------------------------------------------------- /django_tools_project/tests/test_permissions_pprint_filtered_permissions_1.snapshot.txt: -------------------------------------------------------------------------------- 1 | [ ] admin.add_logentry 2 | [ ] admin.change_logentry 3 | [ ] admin.delete_logentry 4 | [ ] admin.view_logentry 5 | [*] auth.add_group 6 | [ ] auth.change_group 7 | [ ] auth.delete_group 8 | [*] auth.view_group 9 | [*] auth.add_permission 10 | [*] auth.change_permission 11 | [ ] auth.delete_permission 12 | [*] auth.view_permission 13 | [*] auth.add_user 14 | [ ] auth.change_user 15 | [ ] auth.delete_user 16 | [*] auth.view_user 17 | [ ] contenttypes.add_contenttype 18 | [*] contenttypes.change_contenttype 19 | [ ] contenttypes.delete_contenttype 20 | [*] contenttypes.view_contenttype 21 | [ ] django_tools_test_app.add_limittousergroupstestmodel 22 | [ ] django_tools_test_app.change_limittousergroupstestmodel 23 | [ ] django_tools_test_app.delete_limittousergroupstestmodel 24 | [ ] django_tools_test_app.view_limittousergroupstestmodel 25 | [*] django_tools_test_app.add_overwritefilesystemstoragemodel 26 | [*] django_tools_test_app.change_overwritefilesystemstoragemodel 27 | [ ] django_tools_test_app.delete_overwritefilesystemstoragemodel 28 | [*] django_tools_test_app.view_overwritefilesystemstoragemodel 29 | [ ] django_tools_test_app.add_permissiontestmodel 30 | [ ] django_tools_test_app.change_permissiontestmodel 31 | [ ] django_tools_test_app.delete_permissiontestmodel 32 | [ ] django_tools_test_app.extra_permission 33 | [ ] django_tools_test_app.view_permissiontestmodel 34 | [*] django_tools_test_app.add_simpleparlermodel 35 | [*] django_tools_test_app.change_simpleparlermodel 36 | [ ] django_tools_test_app.delete_simpleparlermodel 37 | [*] django_tools_test_app.view_simpleparlermodel 38 | [*] django_tools_test_app.add_usermediafiles 39 | [*] django_tools_test_app.change_usermediafiles 40 | [ ] django_tools_test_app.delete_usermediafiles 41 | [*] django_tools_test_app.view_usermediafiles 42 | [*] django_tools_test_app.add_versioningtestmodel 43 | [*] django_tools_test_app.change_versioningtestmodel 44 | [ ] django_tools_test_app.delete_versioningtestmodel 45 | [*] django_tools_test_app.view_versioningtestmodel 46 | [*] flatpages.add_flatpage 47 | [*] flatpages.change_flatpage 48 | [ ] flatpages.delete_flatpage 49 | [*] flatpages.view_flatpage 50 | [*] serve_media_app.add_usermediatokenmodel 51 | [*] serve_media_app.change_usermediatokenmodel 52 | [ ] serve_media_app.delete_usermediatokenmodel 53 | [*] serve_media_app.view_usermediatokenmodel 54 | [*] sessions.add_session 55 | [*] sessions.change_session 56 | [ ] sessions.delete_session 57 | [*] sessions.view_session 58 | [*] sites.add_site 59 | [*] sites.change_site 60 | [ ] sites.delete_site 61 | [*] sites.view_site 62 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_render.py: -------------------------------------------------------------------------------- 1 | """ 2 | :copyleft: 2016-2019 by the django-tools team, see AUTHORS for more details. 3 | :license: GNU GPL v3 or above, see LICENSE for more details. 4 | """ 5 | 6 | from django.test import SimpleTestCase 7 | 8 | # https://github.com/jedie/django-tools 9 | from django_tools.template.render import render_string_template, render_template_file 10 | from django_tools.unittest_utils.assertments import assert_pformat_equal 11 | 12 | 13 | class TestRender(SimpleTestCase): 14 | def test_render_template_file(self): 15 | context = {"foo": "bar"} 16 | path = "test_template.html" 17 | x = render_template_file(path, context) 18 | print(x) 19 | 20 | # Note: START/END comments added by: django_tools.template.loader.DebugCacheLoader 21 | 22 | assert_pformat_equal( 23 | x, ("\n" "Hello bar !\n\n" "") 24 | ) 25 | 26 | def test_render_string_template(self): 27 | x = render_string_template("Foo {{ bar }}!", {"bar": "BAR"}) 28 | assert_pformat_equal(x, "Foo BAR!") 29 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_request_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | :copyleft: 2017-2019 by the django-tools team, see AUTHORS for more details. 3 | :license: GNU GPL v3 or above, see LICENSE for more details. 4 | """ 5 | 6 | import unittest 7 | 8 | from django.conf import settings 9 | from django.contrib.auth.models import AnonymousUser 10 | from django.core.handlers.wsgi import WSGIRequest 11 | 12 | # https://github.com/jedie/django-tools 13 | from django_tools.unittest_utils.assertments import assert_pformat_equal 14 | from django_tools.utils.request import create_fake_request 15 | 16 | 17 | class TestRequestUtils(unittest.TestCase): 18 | def test_defaults(self): 19 | fake_request = create_fake_request() 20 | self.assertIsInstance(fake_request, WSGIRequest) 21 | 22 | assert_pformat_equal(fake_request.path, "/") 23 | assert_pformat_equal(fake_request.session, {}) 24 | assert_pformat_equal(fake_request.LANGUAGE_CODE, settings.LANGUAGE_CODE) 25 | self.assertIsInstance(fake_request.user, AnonymousUser) 26 | 27 | def test_change_defaults(self): 28 | class MockUser: 29 | pass 30 | 31 | mock_user = MockUser() 32 | 33 | fake_request = create_fake_request( 34 | url="/foo/bar", 35 | session={"foo": "bar"}, 36 | language_code="es", 37 | user=mock_user, 38 | extra_attr1=True, 39 | extra_attr2=1234, 40 | ) 41 | self.assertIsInstance(fake_request, WSGIRequest) 42 | 43 | assert_pformat_equal(fake_request.path, "/foo/bar") 44 | assert_pformat_equal(fake_request.session, {"foo": "bar"}) 45 | assert_pformat_equal(fake_request.LANGUAGE_CODE, "es") 46 | assert_pformat_equal(fake_request.user, mock_user) 47 | assert_pformat_equal(fake_request.extra_attr1, True) 48 | assert_pformat_equal(fake_request.extra_attr2, 1234) 49 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_settings_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | :created: 2017 by Jens Diemer 3 | :copyleft: 2017-2019 by the django-tools team, see AUTHORS for more details. 4 | :license: GNU GPL v3 or above, see LICENSE for more details. 5 | """ 6 | 7 | import unittest 8 | 9 | from django.http.request import host_validation_re, split_domain_port, validate_host 10 | 11 | # https://github.com/jedie/django-tools 12 | from django_tools.settings_utils import FnMatchIps 13 | from django_tools.unittest_utils.assertments import assert_pformat_equal 14 | 15 | 16 | class TestSettingsUtils(unittest.TestCase): 17 | @classmethod 18 | def setUpClass(cls): 19 | cls.fnmatch_ips = FnMatchIps(["127.0.0.1", "::1", "192.168.*.*"]) 20 | 21 | def test_contains(self): 22 | # INTERNAL_IPS used this 23 | self.assertTrue("192.168.1.2" in self.fnmatch_ips) 24 | self.assertFalse("10.0.1.2" in self.fnmatch_ips) 25 | 26 | def test_compare(self): 27 | # ALLOWED_HOSTS with django.http.request.validate_host 28 | 29 | self.assertTrue(validate_host("192.168.1.2", self.fnmatch_ips)) 30 | 31 | self.assertFalse(validate_host("10.0.1.2", self.fnmatch_ips)) 32 | 33 | def test_str(self): 34 | assert_pformat_equal("127.0.0.1", str(self.fnmatch_ips[0])) 35 | 36 | def test_re_usage(self): 37 | """ 38 | e.g.: django.http.request.split_domain_port used RE in test environment 39 | :return: 40 | """ 41 | host = self.fnmatch_ips[0] 42 | 43 | self.assertTrue(host_validation_re.match(host)) 44 | 45 | domain, port = split_domain_port(host) 46 | assert_pformat_equal(domain, "127.0.0.1") 47 | assert_pformat_equal(port, "") 48 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_template_loader.py: -------------------------------------------------------------------------------- 1 | """ 2 | test django_tools.template.loader 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | :copyleft: 2017-2019 by the django-tools team, see AUTHORS for more details. 6 | :license: GNU GPL v3 or above, see LICENSE for more details. 7 | """ 8 | 9 | from bx_django_utils.test_utils.html_assertion import HtmlAssertionMixin 10 | from django.template import TemplateDoesNotExist 11 | from django.test import TestCase 12 | 13 | # https://github.com/jedie/django-tools 14 | from django_tools.unittest_utils.assertments import assert_pformat_equal 15 | 16 | 17 | class DebugCacheLoaderTest(HtmlAssertionMixin, TestCase): 18 | def test(self): 19 | response = self.client.get("/admin/login/") 20 | self.assert_html_parts( 21 | response, 22 | parts=( 23 | "", 24 | "", 25 | "", 26 | "", 27 | "", 28 | "", 29 | ), 30 | ) 31 | self.assertTemplateUsed(response, "admin/login.html") 32 | 33 | def test_template_does_not_exists(self): 34 | with self.assertRaises(TemplateDoesNotExist) as cm: 35 | self.client.get("/raise_template_not_exists/") 36 | 37 | output = "\n".join(cm.exception.args) 38 | assert_pformat_equal("/template/does/not/exists.html", output) 39 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_unittest_django_command_clear_cache_1.snapshot.txt: -------------------------------------------------------------------------------- 1 | 2 | Clear caches: 3 | Clear 'LocMemCache' 4 | 5 | done. 6 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_unittest_django_command_help_django41_1.snapshot.txt: -------------------------------------------------------------------------------- 1 | + .../django-tools/.venv/bin/django_tools_project --help 2 | 3 | [replaced django_tools.__version__] 4 | 5 | DJANGO_SETTINGS_MODULE=django_tools_project.settings.tests 6 | 7 | BASE_PATH:.../django-tools 8 | 9 | Type 'django_tools_project help ' for help on a specific subcommand. 10 | 11 | Available subcommands: 12 | 13 | [auth] 14 | changepassword 15 | createsuperuser 16 | 17 | [contenttypes] 18 | remove_stale_contenttypes 19 | 20 | [django] 21 | check 22 | compilemessages 23 | createcachetable 24 | dbshell 25 | diffsettings 26 | dumpdata 27 | flush 28 | inspectdb 29 | loaddata 30 | makemessages 31 | makemigrations 32 | migrate 33 | optimizemigration 34 | sendtestemail 35 | showmigrations 36 | sqlflush 37 | sqlmigrate 38 | sqlsequencereset 39 | squashmigrations 40 | startapp 41 | startproject 42 | test 43 | testserver 44 | 45 | [django_tools] 46 | clear_cache 47 | database_info 48 | generate_model_test_code 49 | list_models 50 | logging_info 51 | nice_diffsettings 52 | permission_info 53 | run_testserver 54 | update_permissions 55 | 56 | [manage_django_project] 57 | code_style 58 | coverage 59 | install 60 | pip_audit 61 | playwright 62 | project_info 63 | publish 64 | run_dev_server 65 | shell 66 | tox 67 | update_req 68 | update_test_snapshot_files 69 | 70 | [sessions] 71 | clearsessions 72 | 73 | [staticfiles] 74 | collectstatic 75 | findstatic 76 | runserver -------------------------------------------------------------------------------- /django_tools_project/tests/test_unittest_django_command_help_django42_1.snapshot.txt: -------------------------------------------------------------------------------- 1 | + .../django-tools/.venv/bin/django_tools_project --help 2 | 3 | [replaced django_tools.__version__] 4 | 5 | DJANGO_SETTINGS_MODULE=django_tools_project.settings.tests 6 | 7 | BASE_PATH:.../django-tools 8 | 9 | Type 'django_tools_project help ' for help on a specific subcommand. 10 | 11 | Available subcommands: 12 | 13 | [auth] 14 | changepassword 15 | createsuperuser 16 | 17 | [contenttypes] 18 | remove_stale_contenttypes 19 | 20 | [django] 21 | check 22 | compilemessages 23 | createcachetable 24 | dbshell 25 | diffsettings 26 | dumpdata 27 | flush 28 | inspectdb 29 | loaddata 30 | makemessages 31 | makemigrations 32 | migrate 33 | optimizemigration 34 | sendtestemail 35 | showmigrations 36 | sqlflush 37 | sqlmigrate 38 | sqlsequencereset 39 | squashmigrations 40 | startapp 41 | startproject 42 | test 43 | testserver 44 | 45 | [django_tools] 46 | clear_cache 47 | database_info 48 | generate_model_test_code 49 | list_models 50 | logging_info 51 | nice_diffsettings 52 | permission_info 53 | run_testserver 54 | update_permissions 55 | 56 | [manage_django_project] 57 | code_style 58 | coverage 59 | install 60 | pip_audit 61 | playwright 62 | project_info 63 | publish 64 | run_dev_server 65 | shell 66 | tox 67 | update_req 68 | update_test_snapshot_files 69 | 70 | [sessions] 71 | clearsessions 72 | 73 | [staticfiles] 74 | collectstatic 75 | findstatic 76 | runserver -------------------------------------------------------------------------------- /django_tools_project/tests/test_unittest_django_command_help_django51_1.snapshot.txt: -------------------------------------------------------------------------------- 1 | + .../django-tools/.venv/bin/django_tools_project --help 2 | 3 | [replaced django_tools.__version__] 4 | 5 | DJANGO_SETTINGS_MODULE=django_tools_project.settings.tests 6 | 7 | BASE_PATH:.../django-tools 8 | 9 | Type 'django_tools_project help ' for help on a specific subcommand. 10 | 11 | Available subcommands: 12 | 13 | [auth] 14 | changepassword 15 | createsuperuser 16 | 17 | [contenttypes] 18 | remove_stale_contenttypes 19 | 20 | [django] 21 | check 22 | compilemessages 23 | createcachetable 24 | dbshell 25 | diffsettings 26 | dumpdata 27 | flush 28 | inspectdb 29 | loaddata 30 | makemessages 31 | makemigrations 32 | migrate 33 | optimizemigration 34 | sendtestemail 35 | showmigrations 36 | sqlflush 37 | sqlmigrate 38 | sqlsequencereset 39 | squashmigrations 40 | startapp 41 | startproject 42 | test 43 | testserver 44 | 45 | [django_tools] 46 | clear_cache 47 | database_info 48 | generate_model_test_code 49 | list_models 50 | logging_info 51 | nice_diffsettings 52 | permission_info 53 | run_testserver 54 | update_permissions 55 | 56 | [manage_django_project] 57 | code_style 58 | coverage 59 | install 60 | pip_audit 61 | playwright 62 | project_info 63 | publish 64 | run_dev_server 65 | shell 66 | tox 67 | update_req 68 | update_test_snapshot_files 69 | 70 | [sessions] 71 | clearsessions 72 | 73 | [staticfiles] 74 | collectstatic 75 | findstatic 76 | runserver -------------------------------------------------------------------------------- /django_tools_project/tests/test_unittest_django_command_list_models_1.snapshot.txt: -------------------------------------------------------------------------------- 1 | 2 | existing models in app_label.ModelName format: 3 | 4 | 01 - admin.LogEntry 5 | 02 - auth.Group 6 | 03 - auth.Permission 7 | 04 - auth.User 8 | 05 - contenttypes.ContentType 9 | 06 - django_tools_test_app.LimitToUsergroupsTestModel 10 | 07 - django_tools_test_app.OverwriteFileSystemStorageModel 11 | 08 - django_tools_test_app.PermissionTestModel 12 | 09 - django_tools_test_app.SimpleParlerModel 13 | 10 - django_tools_test_app.SimpleParlerModelTranslation 14 | 11 - django_tools_test_app.UserMediaFiles 15 | 12 - django_tools_test_app.VersioningTestModel 16 | 13 - flatpages.FlatPage 17 | 14 - serve_media_app.UserMediaTokenModel 18 | 15 - sessions.Session 19 | 16 - sites.Site 20 | 21 | INSTALLED_APPS....: 15 22 | Apps with models..: 15 23 | 24 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_unittest_django_command_no_username_given_1.snapshot.txt: -------------------------------------------------------------------------------- 1 | 2 | _______________________________________________________________________________ 3 | Display effective user permissions in the same format as user.has_perm() argument: . 4 | 5 | No username given! 6 | 7 | All existing users are: 8 | normal_test_user, staff_test_user, superuser 9 | (3 users) 10 | 11 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_unittest_django_command_normal_test_user_1.snapshot.txt: -------------------------------------------------------------------------------- 1 | 2 | _______________________________________________________________________________ 3 | Display effective user permissions in the same format as user.has_perm() argument: . 4 | 5 | All permissions for user 'normal_test_user': 6 | is_active : yes 7 | is_staff : no 8 | is_superuser : no 9 | [ ] admin.add_logentry 10 | [ ] admin.change_logentry 11 | [ ] admin.delete_logentry 12 | [ ] admin.view_logentry 13 | [ ] auth.add_group 14 | [ ] auth.change_group 15 | [ ] auth.delete_group 16 | [ ] auth.view_group 17 | [ ] auth.add_permission 18 | [ ] auth.change_permission 19 | [ ] auth.delete_permission 20 | [ ] auth.view_permission 21 | [ ] auth.add_user 22 | [ ] auth.change_user 23 | [ ] auth.delete_user 24 | [ ] auth.view_user 25 | [ ] contenttypes.add_contenttype 26 | [ ] contenttypes.change_contenttype 27 | [ ] contenttypes.delete_contenttype 28 | [ ] contenttypes.view_contenttype 29 | [ ] django_tools_test_app.add_limittousergroupstestmodel 30 | [ ] django_tools_test_app.change_limittousergroupstestmodel 31 | [ ] django_tools_test_app.delete_limittousergroupstestmodel 32 | [ ] django_tools_test_app.view_limittousergroupstestmodel 33 | [ ] django_tools_test_app.add_overwritefilesystemstoragemodel 34 | [ ] django_tools_test_app.change_overwritefilesystemstoragemodel 35 | [ ] django_tools_test_app.delete_overwritefilesystemstoragemodel 36 | [ ] django_tools_test_app.view_overwritefilesystemstoragemodel 37 | [ ] django_tools_test_app.add_permissiontestmodel 38 | [ ] django_tools_test_app.change_permissiontestmodel 39 | [ ] django_tools_test_app.delete_permissiontestmodel 40 | [ ] django_tools_test_app.extra_permission 41 | [ ] django_tools_test_app.view_permissiontestmodel 42 | [ ] django_tools_test_app.add_simpleparlermodel 43 | [ ] django_tools_test_app.change_simpleparlermodel 44 | [ ] django_tools_test_app.delete_simpleparlermodel 45 | [ ] django_tools_test_app.view_simpleparlermodel 46 | [ ] django_tools_test_app.add_usermediafiles 47 | [ ] django_tools_test_app.change_usermediafiles 48 | [ ] django_tools_test_app.delete_usermediafiles 49 | [ ] django_tools_test_app.view_usermediafiles 50 | [ ] django_tools_test_app.add_versioningtestmodel 51 | [ ] django_tools_test_app.change_versioningtestmodel 52 | [ ] django_tools_test_app.delete_versioningtestmodel 53 | [ ] django_tools_test_app.view_versioningtestmodel 54 | [ ] flatpages.add_flatpage 55 | [ ] flatpages.change_flatpage 56 | [ ] flatpages.delete_flatpage 57 | [ ] flatpages.view_flatpage 58 | [ ] serve_media_app.add_usermediatokenmodel 59 | [ ] serve_media_app.change_usermediatokenmodel 60 | [ ] serve_media_app.delete_usermediatokenmodel 61 | [ ] serve_media_app.view_usermediatokenmodel 62 | [ ] sessions.add_session 63 | [ ] sessions.change_session 64 | [ ] sessions.delete_session 65 | [ ] sessions.view_session 66 | [ ] sites.add_site 67 | [ ] sites.change_site 68 | [ ] sites.delete_site 69 | [ ] sites.view_site 70 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_unittest_django_command_run_testserver_1.snapshot.txt: -------------------------------------------------------------------------------- 1 | _______________________________________________________________________________ 2 | Call "makemigrations" 3 | 4 | 5 | _______________________________________________________________________________ 6 | Call "migrate" 7 | 8 | 9 | _______________________________________________________________________________ 10 | Call "runserver" force_color:False no_color:False skip_checks:True stderr: stdout: traceback:False use_ipv6:False use_reloader:True use_threading:True verbosity:1 -------------------------------------------------------------------------------- /django_tools_project/tests/test_unittest_django_command_update_permissions_1.snapshot.txt: -------------------------------------------------------------------------------- 1 | Create permissions for: 2 | * auth 3 | * contenttypes 4 | * sessions 5 | * sites 6 | * admin 7 | * staticfiles 8 | * admindocs 9 | * flatpages 10 | * messages 11 | * local_sync_cache 12 | * django_tools_test_app 13 | * serve_media_app 14 | * model_version_protect 15 | * django_tools 16 | * manage_django_project 17 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_unittest_django_command_wrong_username_given_1.snapshot.txt: -------------------------------------------------------------------------------- 1 | 2 | _______________________________________________________________________________ 3 | Display effective user permissions in the same format as user.has_perm() argument: . 4 | 5 | All permissions for user 'not existing username': 6 | Username 'not existing username' doesn't exists: User matching query does not exist. 7 | 8 | All existing users are: 9 | normal_test_user, staff_test_user, superuser 10 | (3 users) 11 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_unittest_utils_stdout_redirect.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.test import SimpleTestCase 4 | 5 | from django_tools.unittest_utils.stdout_redirect import DenyStdWrite, StdoutStderrBuffer 6 | 7 | 8 | class StdoutRedirectTestCase(SimpleTestCase): 9 | def test_DenyStdWrite(self): 10 | with DenyStdWrite(name='foo'): 11 | pass 12 | 13 | with self.assertRaisesMessage(AssertionError, 'foo writes to stdout:\nbar'): 14 | with DenyStdWrite(name='foo'): 15 | print('bar') 16 | 17 | with self.assertRaisesMessage(AssertionError, 'bar writes to stderr:\nfoo'): 18 | with DenyStdWrite(name='bar'): 19 | print('foo', file=sys.stderr) 20 | 21 | # DenyStdWrite cleanup ok? 22 | 23 | with StdoutStderrBuffer() as buffer: 24 | sys.stdout.write('One') 25 | sys.stderr.write('Two') 26 | assert buffer.get_output() == 'OneTwo' 27 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_unittest_utils_temp_media_root.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from pathlib import Path 3 | 4 | from bx_py_utils.path import assert_is_dir 5 | from cli_base.cli_tools.test_utils.assertion import assert_startswith 6 | from django.conf import settings 7 | from django.test import SimpleTestCase 8 | 9 | from django_tools.unittest_utils.temp_media_root import TempMediaRoot 10 | 11 | 12 | class TempMediaRootTestCase(SimpleTestCase): 13 | def test_happy_path(self): 14 | tempdir = tempfile.gettempdir() 15 | 16 | with TempMediaRoot() as media_root_path: 17 | self.assertIsInstance(media_root_path, Path) 18 | self.assertTrue(media_root_path.is_absolute()) 19 | assert_is_dir(media_root_path) 20 | 21 | self.assertEqual(settings.MEDIA_ROOT, str(media_root_path)) 22 | 23 | # check that's in the temp dir: 24 | assert_startswith(str(media_root_path), prefix=tempdir) 25 | 26 | Path(media_root_path / 'test.txt').write_text('test') 27 | 28 | self.assertFalse(media_root_path.exists()) 29 | 30 | # Works also as decorator: 31 | @TempMediaRoot() 32 | def example(): 33 | assert_startswith(settings.MEDIA_ROOT, prefix=tempdir) 34 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_unittest_utils_user_get_or_create_user_and_group_1.snapshot.txt: -------------------------------------------------------------------------------- 1 | 2 | _______________________________________________________________________________ 3 | Display effective user permissions in the same format as user.has_perm() argument: . 4 | 5 | All permissions for user 'testuser': 6 | is_active : yes 7 | is_staff : yes 8 | is_superuser : no 9 | [ ] admin.add_logentry 10 | [ ] admin.change_logentry 11 | [ ] admin.delete_logentry 12 | [ ] admin.view_logentry 13 | [*] auth.add_group 14 | [*] auth.change_group 15 | [ ] auth.delete_group 16 | [*] auth.view_group 17 | [*] auth.add_permission 18 | [*] auth.change_permission 19 | [*] auth.delete_permission 20 | [*] auth.view_permission 21 | [*] auth.add_user 22 | [*] auth.change_user 23 | [ ] auth.delete_user 24 | [*] auth.view_user 25 | [ ] contenttypes.add_contenttype 26 | [*] contenttypes.change_contenttype 27 | [ ] contenttypes.delete_contenttype 28 | [*] contenttypes.view_contenttype 29 | [*] django_tools_test_app.add_limittousergroupstestmodel 30 | [*] django_tools_test_app.change_limittousergroupstestmodel 31 | [*] django_tools_test_app.delete_limittousergroupstestmodel 32 | [*] django_tools_test_app.view_limittousergroupstestmodel 33 | [*] django_tools_test_app.add_overwritefilesystemstoragemodel 34 | [*] django_tools_test_app.change_overwritefilesystemstoragemodel 35 | [*] django_tools_test_app.delete_overwritefilesystemstoragemodel 36 | [*] django_tools_test_app.view_overwritefilesystemstoragemodel 37 | [*] django_tools_test_app.add_permissiontestmodel 38 | [*] django_tools_test_app.change_permissiontestmodel 39 | [*] django_tools_test_app.delete_permissiontestmodel 40 | [*] django_tools_test_app.extra_permission 41 | [*] django_tools_test_app.view_permissiontestmodel 42 | [*] django_tools_test_app.add_simpleparlermodel 43 | [*] django_tools_test_app.change_simpleparlermodel 44 | [*] django_tools_test_app.delete_simpleparlermodel 45 | [*] django_tools_test_app.view_simpleparlermodel 46 | [*] django_tools_test_app.add_usermediafiles 47 | [*] django_tools_test_app.change_usermediafiles 48 | [*] django_tools_test_app.delete_usermediafiles 49 | [*] django_tools_test_app.view_usermediafiles 50 | [*] django_tools_test_app.add_versioningtestmodel 51 | [*] django_tools_test_app.change_versioningtestmodel 52 | [*] django_tools_test_app.delete_versioningtestmodel 53 | [*] django_tools_test_app.view_versioningtestmodel 54 | [*] flatpages.add_flatpage 55 | [*] flatpages.change_flatpage 56 | [*] flatpages.delete_flatpage 57 | [*] flatpages.view_flatpage 58 | [*] serve_media_app.add_usermediatokenmodel 59 | [*] serve_media_app.change_usermediatokenmodel 60 | [*] serve_media_app.delete_usermediatokenmodel 61 | [*] serve_media_app.view_usermediatokenmodel 62 | [ ] sessions.add_session 63 | [ ] sessions.change_session 64 | [ ] sessions.delete_session 65 | [ ] sessions.view_session 66 | [ ] sites.add_site 67 | [ ] sites.change_site 68 | [ ] sites.delete_site 69 | [ ] sites.view_site 70 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_unittest_utils_user_remove_obsolete_permissions_django41_1.snapshot.txt: -------------------------------------------------------------------------------- 1 | Check 'admin' 2 | Check 'sites' 3 | remove permission: auth | user | Can delete user 4 | remove permission: contenttypes | content type | Can add content type 5 | remove permission: sessions | session | Can add session 6 | remove permission: sessions | session | Can change session 7 | remove permission: sessions | session | Can delete session 8 | remove permission: sessions | session | Can view session 9 | remove permission: sites | site | Can add site 10 | remove permission: sites | site | Can change site 11 | remove permission: sites | site | Can delete site 12 | remove permission: sites | site | Can view site 13 | Add 45 permissions to 'testgroup' 14 | Group testgroup has 45 permissions 15 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_unittest_utils_user_remove_obsolete_permissions_django42_1.snapshot.txt: -------------------------------------------------------------------------------- 1 | Check 'admin' 2 | Check 'sites' 3 | remove permission: auth | user | Can delete user 4 | remove permission: contenttypes | content type | Can add content type 5 | remove permission: sessions | session | Can add session 6 | remove permission: sessions | session | Can change session 7 | remove permission: sessions | session | Can delete session 8 | remove permission: sessions | session | Can view session 9 | remove permission: sites | site | Can add site 10 | remove permission: sites | site | Can change site 11 | remove permission: sites | site | Can delete site 12 | remove permission: sites | site | Can view site 13 | Add 45 permissions to 'testgroup' 14 | Group testgroup has 45 permissions 15 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_unittest_utils_user_remove_obsolete_permissions_django51_1.snapshot.txt: -------------------------------------------------------------------------------- 1 | Check 'admin' 2 | Check 'sites' 3 | remove permission: Authentication and Authorization | user | Can delete user 4 | remove permission: Content Types | content type | Can add content type 5 | remove permission: Sessions | session | Can add session 6 | remove permission: Sessions | session | Can change session 7 | remove permission: Sessions | session | Can delete session 8 | remove permission: Sessions | session | Can view session 9 | remove permission: Sites | site | Can add site 10 | remove permission: Sites | site | Can change site 11 | remove permission: Sites | site | Can delete site 12 | remove permission: Sites | site | Can view site 13 | Add 45 permissions to 'testgroup' 14 | Group testgroup has 45 permissions 15 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_utils_stack_info.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from django_tools.utils.stack_info import get_stack_info 4 | 5 | 6 | class TestStackInfo(unittest.TestCase): 7 | def test_get_stack_info(self): 8 | stack_info = get_stack_info(filepath_filter='django_tools') 9 | 10 | text_block = '\n'.join(stack_info) 11 | 12 | self.assertIn('File "django_tools_project/tests/test_utils_stack_info.py", line ', text_block) 13 | self.assertIn('File "django_tools/utils/stack_info.py", line ', text_block) 14 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_utils_url.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test django_tools.utils.url 3 | 4 | :copyleft: 2017-2019 by the django-tools team, see AUTHORS for more details. 5 | :license: GNU GPL v3 or above, see LICENSE for more details. 6 | """ 7 | 8 | from django.test import SimpleTestCase 9 | 10 | # https://github.com/jedie/django-tools 11 | from django_tools.unittest_utils.assertments import assert_pformat_equal 12 | from django_tools.utils.url import GetDict 13 | 14 | 15 | class TestGetDict(SimpleTestCase): 16 | def test_django_example(self): 17 | q = GetDict() 18 | q["next"] = "/a&b/" 19 | assert_pformat_equal(q.urlencode(), "next=%2Fa%26b%2F") 20 | assert_pformat_equal(q.urlencode(safe="/"), "next=/a%26b/") 21 | 22 | def test_empty(self): 23 | q = GetDict() 24 | q["this_is_empty"] = None 25 | assert_pformat_equal(q.urlencode(), "this_is_empty") 26 | q["another_empty"] = None 27 | assert_pformat_equal(q.urlencode(), "another_empty&this_is_empty") 28 | 29 | def test_multi(self): 30 | q = GetDict("vote=yes&vote=no") 31 | assert_pformat_equal(q.urlencode(), "vote=no&vote=yes") 32 | q["empty"] = None 33 | assert_pformat_equal(q.urlencode(), "empty&vote=no&vote=yes") 34 | 35 | def test_enhanced_example(self): 36 | q = GetDict() 37 | q["next"] = "/a&b/" 38 | q["foo"] = "bar" 39 | q["number"] = 123 40 | q["empty"] = None 41 | assert_pformat_equal(q.urlencode(), "empty&foo=bar&next=%2Fa%26b%2F&number=123") 42 | assert_pformat_equal(q.urlencode(safe="/"), "empty&foo=bar&next=/a%26b/&number=123") 43 | -------------------------------------------------------------------------------- /django_tools_project/tests/test_warn_decorators.py: -------------------------------------------------------------------------------- 1 | """ 2 | :created: 11.12.2018 by Jens Diemer 3 | :copyleft: 2018-2019 by the django-tools team, see AUTHORS for more details. 4 | :license: GNU GPL v3 or above, see LICENSE for more details. 5 | """ 6 | 7 | import unittest 8 | 9 | # https://github.com/jedie/django-tools 10 | from django_tools.decorators import warn_class_usage, warn_function_usage 11 | from django_tools.unittest_utils.assertments import assert_pformat_equal 12 | 13 | 14 | class TestWarnDecorators(unittest.TestCase): 15 | def test_warn_class_usage(self): 16 | @warn_class_usage(message="Test warn class usage !") 17 | class FooBar: 18 | pass 19 | 20 | with self.assertWarns(DeprecationWarning) as cm: 21 | FooBar() 22 | 23 | assert_pformat_equal(str(cm.warning), "Test warn class usage !") 24 | 25 | def test_warn_function_usage(self): 26 | @warn_function_usage(message="Test warn function usage !") 27 | def foo_bar(): 28 | pass 29 | 30 | with self.assertWarns(DeprecationWarning) as cm: 31 | foo_bar() 32 | 33 | assert_pformat_equal(str(cm.warning), "Test warn function usage !") 34 | -------------------------------------------------------------------------------- /django_tools_project/tests/utils.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from pathlib import Path 3 | from unittest import TestCase 4 | 5 | # https://github.com/jedie/django-tools 6 | from django_tools.unittest_utils.django_command import DjangoCommandMixin 7 | 8 | 9 | class ForRunnersCommandTestCase(DjangoCommandMixin, TestCase): 10 | @classmethod 11 | def setUpClass(cls): 12 | # installed via setup.py entry points ! 13 | cls.django_tools_bin = shutil.which("django_tools") 14 | cls.manage_bin = shutil.which("manage") 15 | 16 | def _call_django_tools(self, cmd, **kwargs): 17 | django_tools_path = Path(self.django_tools_bin) 18 | return self.call_manage_py( 19 | cmd=cmd, 20 | manage_dir=str(django_tools_path.parent), 21 | manage_py=django_tools_path.name, # Python 3.5 needs str() 22 | **kwargs, 23 | ) 24 | 25 | def _call_manage(self, cmd, **kwargs): 26 | manage_path = Path(self.manage_bin) 27 | return self.call_manage_py( 28 | cmd=cmd, manage_dir=str(manage_path.parent), manage_py=manage_path.name, **kwargs # Python 3.5 needs str() 29 | ) 30 | -------------------------------------------------------------------------------- /django_tools_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls import static 3 | from django.contrib import admin 4 | from django.urls import include, path 5 | 6 | from django_tools_project.django_tools_test_app.views import ( 7 | TemplateDoesNotExists, 8 | create_message_normal_response, 9 | create_message_redirect_response, 10 | delay_view, 11 | display_site, 12 | get_current_get_parameters, 13 | raise_exception, 14 | ) 15 | 16 | 17 | admin.autodiscover() 18 | 19 | 20 | urlpatterns = [ 21 | path('display_site/', display_site), 22 | path('get_current_get_parameters/', get_current_get_parameters), 23 | path('raise_exception//', raise_exception), 24 | path('raise_template_not_exists/', TemplateDoesNotExists.as_view()), 25 | path('create_message_normal_response//', create_message_normal_response), 26 | path('create_message_redirect_response//', create_message_redirect_response), 27 | path('delay/', delay_view), 28 | # Serve user files only for authorized users: 29 | path(settings.MEDIA_URL.lstrip('/'), include('django_tools.serve_media_app.urls')), 30 | # 31 | # Django Admin: 32 | path('admin/', admin.site.urls), 33 | ] 34 | 35 | 36 | if settings.SERVE_FILES: 37 | urlpatterns += static.static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 38 | 39 | 40 | if settings.DEBUG: 41 | import debug_toolbar 42 | 43 | urlpatterns = [ 44 | path('__debug__/', include(debug_toolbar.urls)), 45 | ] + urlpatterns 46 | -------------------------------------------------------------------------------- /django_tools_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config 3 | """ 4 | 5 | from django.core.wsgi import get_wsgi_application 6 | 7 | 8 | application = get_wsgi_application() 9 | --------------------------------------------------------------------------------