├── changes └── .directory ├── tests ├── __init__.py ├── test_utils │ ├── __init__.py │ ├── example2 │ │ ├── __init__.py │ │ └── models.py │ ├── custom_user │ │ ├── __init__.py │ │ └── models.py │ ├── example1 │ │ ├── templates │ │ │ ├── cms │ │ │ │ └── plugins │ │ │ │ │ └── text.html │ │ │ └── fake_plugin.html │ │ ├── __init__.py │ │ ├── cms_app.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── test_pytest.py │ │ │ └── test_fake.py │ │ ├── views.py │ │ ├── urls.py │ │ ├── models.py │ │ ├── cms_plugins.py │ │ ├── cms_apps.py │ │ ├── data │ │ │ ├── django_0001_partial.py │ │ │ ├── django_0001_initial.py │ │ │ └── django.po │ │ └── locale │ │ │ └── en │ │ │ └── LC_MESSAGES │ │ │ └── placeholder │ ├── AUTHORS │ ├── pytest.ini │ ├── cms_helper_extra_runner.py │ ├── conftest.py │ ├── cms_helper_extra.py │ ├── runners.py │ ├── helper_no_cms.py │ ├── helper.py │ ├── cms_helper.py │ └── cms_helper_custom.py ├── test_others.py └── test_base_classes.py ├── requirements.txt ├── app_helper ├── test_data │ ├── __init__.py │ ├── models.py │ └── templates │ │ ├── page.html │ │ ├── fullwidth.html │ │ └── base.html ├── __init__.py ├── asgi.py ├── locale │ └── en │ │ └── LC_MESSAGES │ │ └── django.po ├── urls.py ├── pytest_runner.py ├── default_settings.py ├── server.py ├── runner.py ├── main.py └── utils.py ├── docs ├── history.rst ├── contributing.rst ├── index.rst ├── migrating.rst ├── development.rst ├── asgi.rst ├── basetest.rst ├── runner.rst ├── pytest.rst ├── introduction.rst ├── settings.rst ├── reference.rst ├── Makefile ├── make.bat └── conf.py ├── setup.py ├── .pyup.yml ├── requirements-test.txt ├── .codeclimate.yml ├── .github ├── dependabot.yml ├── workflows │ ├── logger.yml │ ├── publish.yml │ ├── lint.yml │ ├── test.yml │ └── codeql.yml ├── pull_request_template.md └── ISSUE_TEMPLATE │ ├── ---feature-request.md │ └── ---bug-report.md ├── djangocms_helper └── __init__.py ├── MANIFEST.in ├── CONTRIBUTING.rst ├── AUTHORS.rst ├── .readthedocs.yml ├── .editorconfig ├── .coveragerc ├── helper.py ├── .pre-commit-config.yaml ├── pyproject.toml ├── setup.cfg ├── tox.ini ├── tasks.py ├── README.rst ├── HISTORY.rst ├── .gitignore └── LICENSE /changes/.directory: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | -------------------------------------------------------------------------------- /tests/test_utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app_helper/test_data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app_helper/test_data/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_utils/example2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_utils/custom_user/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../HISTORY.rst 2 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup() 4 | -------------------------------------------------------------------------------- /tests/test_utils/example1/templates/cms/plugins/text.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_utils/example1/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /tests/test_utils/AUTHORS: -------------------------------------------------------------------------------- 1 | 2 | * Iacopo Spalletti 3 | -------------------------------------------------------------------------------- /tests/test_utils/example1/cms_app.py: -------------------------------------------------------------------------------- 1 | from .cms_apps import * # NOQA # nopyflakes 2 | -------------------------------------------------------------------------------- /tests/test_utils/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = tests.py test_*.py *_tests.py 3 | -------------------------------------------------------------------------------- /tests/test_utils/example1/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .test_fake import FakeTests # NOQA # nopyflakes 2 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | update: all 2 | pin: False 3 | branch: 4 | schedule: "every week" 5 | search: True 6 | branch_prefix: pyup/ 7 | close_prs: True 8 | -------------------------------------------------------------------------------- /tests/test_utils/example1/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | 4 | def hello(request): 5 | return HttpResponse("Hello") 6 | -------------------------------------------------------------------------------- /tests/test_utils/example1/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import hello 4 | 5 | urlpatterns = [path("/hello", hello, name="hello")] # NOQA 6 | -------------------------------------------------------------------------------- /app_helper/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "3.3.6.dev1" 2 | __author__ = "Iacopo Spalletti " 3 | __all__ = ["runner"] 4 | 5 | HELPER_FILE = "helper.py" 6 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | coverage 3 | coveralls>=2.0 4 | coverage-lcov 5 | wheel 6 | pep517 7 | invoke 8 | tox>=1.8 9 | pytest<7.1 10 | pytest-django 11 | django-filer 12 | -------------------------------------------------------------------------------- /tests/test_utils/custom_user/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | 3 | 4 | class CustomUser(AbstractUser): 5 | @property 6 | def email(self): 7 | return "some@example.com" 8 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | Ruby: false 3 | JavaScript: false 4 | PHP: false 5 | Python: true 6 | exclude_paths: 7 | - 'app_helper/test_data/*' 8 | - 'app_helper/test_utils/*' 9 | - 'app_helper/tests/*' 10 | -------------------------------------------------------------------------------- /tests/test_utils/example1/templates/fake_plugin.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags cms_tags %} 2 | fake text 3 | {{ request.LANGUAGE_CODE }} 4 | {% page_attribute "title" %} 5 | {% addtoblock "css" %}{% endaddtoblock %} 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | reviewers: 9 | - yakky 10 | versioning-strategy: lockfile-only 11 | -------------------------------------------------------------------------------- /app_helper/test_data/templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load cms_tags %} 3 | 4 | {% block title %}{% page_attribute 'title' %}{% endblock title %} 5 | 6 | {% block content %} 7 | {% placeholder "content" %} 8 | {% endblock content %} 9 | -------------------------------------------------------------------------------- /tests/test_utils/example2/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class ExampleModel2(models.Model): 6 | test_field = models.CharField(max_length=20, verbose_name=_("Test field")) 7 | -------------------------------------------------------------------------------- /app_helper/asgi.py: -------------------------------------------------------------------------------- 1 | import os # pragma: no cover 2 | 3 | from django.core.asgi import get_asgi_application # pragma: no cover 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "helper") # pragma: no cover 6 | 7 | application = get_asgi_application() 8 | -------------------------------------------------------------------------------- /app_helper/test_data/templates/fullwidth.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load cms_tags %} 3 | 4 | {% block title %}{% page_attribute 'title' %}{% endblock title %} 5 | 6 | {% block content %} 7 | {% placeholder "content" %} 8 | {% endblock content %} 9 | -------------------------------------------------------------------------------- /tests/test_utils/example1/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class ExampleModel1(models.Model): 6 | test_field = models.CharField(max_length=20, default="", verbose_name=_("Test field")) 7 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | .. toctree:: 4 | :maxdepth: 2 5 | 6 | introduction 7 | reference 8 | settings 9 | runner 10 | migrating 11 | basetest 12 | asgi 13 | pytest 14 | development 15 | contributing 16 | history 17 | -------------------------------------------------------------------------------- /tests/test_utils/example1/tests/test_pytest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.contrib.auth import get_user_model 3 | 4 | User = get_user_model() 5 | 6 | 7 | @pytest.mark.django_db 8 | def test_user(): 9 | user = User.objects.create(username="foo") 10 | assert user.pk 11 | -------------------------------------------------------------------------------- /.github/workflows/logger.yml: -------------------------------------------------------------------------------- 1 | name: Event Logger 2 | on: push 3 | 4 | jobs: 5 | log-github-event-goodies: 6 | name: "LOG Everything on GitHub Event" 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Logging 10 | run: | 11 | echo '${{toJSON(github.event)}}' 12 | -------------------------------------------------------------------------------- /djangocms_helper/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a compatibility package to allow legacy import to work without any changes 3 | """ 4 | 5 | import sys 6 | 7 | # clone app_helper in djangocms_helper module 8 | import app_helper # NOQA 9 | 10 | sys.modules[__name__] = sys.modules["app_helper"] 11 | -------------------------------------------------------------------------------- /tests/test_utils/example1/cms_plugins.py: -------------------------------------------------------------------------------- 1 | from cms.plugin_base import CMSPluginBase 2 | from cms.plugin_pool import plugin_pool 3 | 4 | 5 | class FakePlugin(CMSPluginBase): 6 | name = "FakePlugin" 7 | render_template = "fake_plugin.html" 8 | 9 | 10 | plugin_pool.register_plugin(FakePlugin) 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include LICENSE 3 | include README.rst 4 | include CONTRIBUTING.rst 5 | include HISTORY.rst 6 | include requirements.txt 7 | include requirements-docs.txt 8 | include requirements-tests.txt 9 | recursive-include app_helper *.html *.png *.gif *js *jpg *jpeg *svg *py *mo *po 10 | -------------------------------------------------------------------------------- /tests/test_utils/cms_helper_extra_runner.py: -------------------------------------------------------------------------------- 1 | from helper import HELPER_SETTINGS 2 | 3 | HELPER_SETTINGS["TEMPLATE_CONTEXT_PROCESSORS"] = ["django.core.context_processors.debug"] 4 | HELPER_SETTINGS["TEST_RUNNER"] = "runners.CapturedOutputRunner" 5 | HELPER_SETTINGS["INSTALLED_APPS"].remove("djangocms_text_ckeditor") 6 | -------------------------------------------------------------------------------- /tests/test_utils/example1/cms_apps.py: -------------------------------------------------------------------------------- 1 | from cms.app_base import CMSApp 2 | from cms.apphook_pool import apphook_pool 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | 6 | class Example(CMSApp): 7 | name = _("Example") 8 | urls = ["app_helper.urls"] 9 | 10 | 11 | apphook_pool.register(Example) 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ############ 2 | Contributing 3 | ############ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | Please read the instructions `here `_ to start contributing to `django-app-helper`. 9 | -------------------------------------------------------------------------------- /tests/test_utils/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope="session") 5 | def django_db_setup( 6 | request, 7 | django_test_environment, 8 | django_db_blocker, 9 | django_db_use_migrations, 10 | django_db_keepdb, 11 | django_db_modify_db_settings, 12 | ): 13 | django_db_blocker.unblock() 14 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Iacopo Spalletti 9 | 10 | Contributors 11 | ------------ 12 | 13 | * Adam Johnson 14 | * Andrea Stagi 15 | * Daniele Procida 16 | * Eraldo Energy 17 | * Fabian Braun 18 | * Felipe Prenholato 19 | * Jacob Rief 20 | * Kirill Kniazev 21 | * Leonardo Cavallucci 22 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.10" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | fail_on_warning: false 11 | 12 | formats: 13 | - epub 14 | - pdf 15 | 16 | python: 17 | install: 18 | - requirements: requirements-test.txt 19 | - method: pip 20 | path: . 21 | extra_requirements: 22 | - docs 23 | -------------------------------------------------------------------------------- /tests/test_utils/example1/data/django_0001_partial.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | initial = True 6 | 7 | dependencies = [] 8 | 9 | operations = [ 10 | migrations.CreateModel( 11 | name="ExampleModel1", 12 | fields=[ 13 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 14 | ], 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /tests/test_utils/example1/data/django_0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | dependencies = [] 6 | 7 | operations = [ 8 | migrations.CreateModel( 9 | name="ExampleModel1", 10 | fields=[ 11 | ("id", models.AutoField(primary_key=True, serialize=False, verbose_name="ID", auto_created=True)), 12 | ], 13 | options={}, 14 | bases=(models.Model,), 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /tests/test_others.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from tempfile import gettempdir 3 | 4 | from app_helper.utils import make_temp_dir 5 | 6 | 7 | class TestUtils(unittest.TestCase): 8 | def test_make_temp_dir(self): 9 | temp1 = make_temp_dir("suff") 10 | self.assertTrue(temp1.startswith("/dev/shm")) 11 | self.assertTrue(temp1.endswith("suff")) 12 | 13 | def test_make_temp_dir_not_shm(self): 14 | temp1 = make_temp_dir("suff", container="/some/random/path") 15 | self.assertTrue(temp1.startswith(gettempdir())) 16 | self.assertTrue(temp1.endswith("suff")) 17 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Describe: 4 | 5 | * Content of the pull request 6 | * Feature added / Problem fixed 7 | 8 | ## References 9 | 10 | Provide any github issue fixed (as in ``Fix #XYZ``) 11 | 12 | # Checklist 13 | 14 | * [ ] I have read the [contribution guide](https://django-app-helper.readthedocs.io/en/latest/contributing.html) 15 | * [ ] Code lint checked via `inv lint` 16 | * [ ] ``changes`` file included (see [docs](https://django-app-helper.readthedocs.io/en/latest/contributing.html#pull-request-guidelines)) 17 | * [ ] Usage documentation added in case of new features 18 | * [ ] Tests added 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://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 | max_line_length = 120 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [*.rst] 17 | max_line_length = 120 18 | 19 | [*.py] 20 | max_line_length = 120 21 | 22 | [*.{scss,html}] 23 | indent_size = 2 24 | indent_style = space 25 | max_line_length = 120 26 | 27 | [*.{js,vue,json}] 28 | indent_size = 2 29 | max_line_length = 120 30 | 31 | [*.{yml,yaml}] 32 | indent_size = 2 33 | 34 | [Makefile] 35 | indent_style = tab 36 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = app_helper 4 | 5 | [report] 6 | omit = *migrations*,*tests*,*test_utils* 7 | # Regexes for lines to exclude from consideration 8 | exclude_lines = 9 | # Have to re-enable the standard pragma 10 | pragma: no cover 11 | 12 | # Don't complain about missing debug-only code: 13 | def __repr__ 14 | if self\.debug 15 | 16 | # Don't complain if tests don't hit defensive assertion code: 17 | raise AssertionError 18 | raise NotImplementedError 19 | 20 | # Don't complain if non-runnable code isn't run: 21 | if 0: 22 | if __name__ == .__main__.: 23 | 24 | ignore_errors = True 25 | 26 | [html] 27 | directory = coverage_html 28 | -------------------------------------------------------------------------------- /tests/test_utils/cms_helper_extra.py: -------------------------------------------------------------------------------- 1 | try: 2 | import djangocms_text_ckeditor # NOQA 3 | 4 | text_plugin = ["djangocms_text_ckeditor"] 5 | except ImportError: 6 | text_plugin = [] 7 | 8 | HELPER_SETTINGS = { 9 | "TIME_ZONE": "Europe/Paris", 10 | "INSTALLED_APPS": ["some_app"] + text_plugin, 11 | "TEMPLATE_CONTEXT_PROCESSORS": ["django.core.context_processors.debug"], 12 | "TEMPLATE_LOADERS": ["admin_tools.template_loaders.Loader"], 13 | "TOP_MIDDLEWARE_CLASSES": ["top_middleware"], 14 | "MIDDLEWARE_CLASSES": ["some_middleware"], 15 | "TOP_INSTALLED_APPS": ["djangocms_admin_style"], 16 | "ALDRYN_BOILERPLATE_NAME": "legacy", 17 | "TEMPLATE_DIRS": ["some/dir"], 18 | } 19 | -------------------------------------------------------------------------------- /tests/test_utils/example1/data/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2014-08-09 18:00+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: models.py:8 21 | msgid "Test fie" 22 | msgstr "" 23 | -------------------------------------------------------------------------------- /tests/test_utils/example1/locale/en/LC_MESSAGES/placeholder: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2014-08-09 18:00+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: models.py:8 21 | msgid "Test fie" 22 | msgstr "" 23 | -------------------------------------------------------------------------------- /app_helper/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2014-08-14 08:25+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: test_utils/example/models.py:8 21 | msgid "Test field" 22 | msgstr "" 23 | -------------------------------------------------------------------------------- /tests/test_utils/runners.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import unittest 4 | 5 | from django.test.runner import DiscoverRunner 6 | 7 | from app_helper.utils import work_in 8 | 9 | 10 | class CapturedOutputRunner(DiscoverRunner): 11 | """Custom runner to discover tests only in sample applications.""" 12 | 13 | def build_suite(self, test_labels=None, extra_tests=None, **kwargs): 14 | self.top_level = os.path.dirname(__file__) 15 | with work_in(os.path.dirname(__file__)): 16 | return super().build_suite(test_labels=test_labels, extra_tests=extra_tests, **kwargs) 17 | 18 | def run_suite(self, suite, **kwargs): 19 | return unittest.TextTestRunner(verbosity=self.verbosity, failfast=self.failfast, stream=sys.stderr).run(suite) 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F389 Feature request" 3 | about: Share your idea, let's discuss it! 4 | title: '' 5 | labels: 'type: feature' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | ## Description 15 | 16 | 19 | 20 | ## Use cases 21 | 22 | 25 | 26 | ## Proposed solution 27 | 28 | 31 | 32 | ## Alternatives 33 | 34 | 37 | 38 | ## Additional information 39 | 40 | 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'type: bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | ## Description 15 | 16 | 19 | 20 | ## Steps to reproduce 21 | 22 | 25 | 26 | ## Versions 27 | 28 | 31 | 32 | ## Expected behaviour 33 | 34 | 37 | 38 | ## Actual behaviour 39 | 40 | 43 | 44 | ## Additional information 45 | 46 | 49 | -------------------------------------------------------------------------------- /tests/test_utils/helper_no_cms.py: -------------------------------------------------------------------------------- 1 | from tempfile import mkdtemp 2 | 3 | HELPER_SETTINGS = { 4 | "TIME_ZONE": "Europe/Rome", 5 | "INSTALLED_APPS": ["example2", "filer"], 6 | "CMS_LANGUAGES": { 7 | 1: [ 8 | {"code": "en", "name": "English", "public": True}, 9 | {"code": "it", "name": "Italiano", "public": True}, 10 | {"code": "fr", "name": "French", "public": True}, 11 | ], 12 | "default": {"hide_untranslated": False}, 13 | }, 14 | "FILE_UPLOAD_TEMP_DIR": mkdtemp(), 15 | "TEST_RUNNER": "app_helper.pytest_runner.PytestTestRunner", 16 | "ALLOWED_HOSTS": ["testserver"], 17 | } 18 | 19 | 20 | def run(): 21 | from app_helper import runner 22 | 23 | runner.run("example1") 24 | 25 | 26 | def setup(): 27 | import sys 28 | 29 | from app_helper import runner 30 | 31 | runner.setup("example1", sys.modules[__name__], use_cms=False) 32 | 33 | 34 | if __name__ == "__main__": 35 | run() 36 | -------------------------------------------------------------------------------- /app_helper/test_data/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load cms_tags static menu_tags sekizai_tags %} 2 | 3 | 4 | 5 | {% block title %}This is my new project home page{% endblock title %} 6 | {% render_block "css" %} 7 | 17 | 18 | 19 | {% cms_toolbar %} 20 |
21 | 24 | {% block content %} 25 | {% endblock content %} 26 |
27 | {% render_block "js" %} 28 | {% with_data "js-script" as jsset %} 29 | {% for js in jsset %}{% endfor %} 30 | {% end_with_data %} 31 | {% render_block "js_end" %} 32 | 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published,prereleased] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: '3.x' 16 | - name: Cache pip 17 | uses: actions/cache@v3 18 | with: 19 | path: ~/.cache/pip 20 | key: ${{ runner.os }}-pip-${{ matrix.toxenv }} 21 | restore-keys: | 22 | ${{ runner.os }}-pip-${{ matrix.toxenv }} 23 | - name: Cache tox 24 | uses: actions/cache@v3 25 | with: 26 | path: .tox 27 | key: ${{ runner.os }}-tox-release-${{ hashFiles('setup.cfg') }} 28 | restore-keys: | 29 | ${{ runner.os }}-tox-release- 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip setuptools tox>4 33 | - name: Build and publish 34 | env: 35 | TWINE_USERNAME: __token__ 36 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 37 | run: | 38 | tox -erelease 39 | -------------------------------------------------------------------------------- /app_helper/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.i18n import i18n_patterns 3 | from django.contrib import admin 4 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 5 | from django.urls import include, path, re_path 6 | from django.views.i18n import JavaScriptCatalog 7 | from django.views.static import serve 8 | 9 | from .utils import load_from_file 10 | 11 | admin.autodiscover() 12 | 13 | urlpatterns = [ 14 | re_path(r"^media/(?P.*)$", serve, {"document_root": settings.MEDIA_ROOT, "show_indexes": True}), # NOQA 15 | re_path(r"^jsi18n/(?P\S+?)/$", JavaScriptCatalog.as_view()), # NOQA 16 | ] 17 | i18n_urls = [ 18 | re_path(r"^admin/", admin.site.urls), 19 | ] 20 | 21 | try: 22 | load_from_file("%s.urls" % settings.BASE_APPLICATION) 23 | i18n_urls.append( 24 | re_path(r"^%s/" % settings.BASE_APPLICATION, include("%s.urls" % settings.BASE_APPLICATION)) 25 | ) # NOQA 26 | except OSError: # pragma: no cover 27 | pass 28 | 29 | if settings.USE_CMS: 30 | i18n_urls.append(path("", include("cms.urls"))) # NOQA 31 | 32 | urlpatterns += i18n_patterns(*i18n_urls) 33 | urlpatterns += staticfiles_urlpatterns() 34 | -------------------------------------------------------------------------------- /tests/test_utils/helper.py: -------------------------------------------------------------------------------- 1 | from tempfile import mkdtemp 2 | 3 | try: 4 | import djangocms_text_ckeditor # NOQA 5 | 6 | text_plugin = ["djangocms_text_ckeditor"] 7 | except ImportError: 8 | text_plugin = [] 9 | 10 | HELPER_SETTINGS = { 11 | "TIME_ZONE": "Europe/Rome", 12 | "INSTALLED_APPS": ["example2", "filer"] + text_plugin, 13 | "CMS_LANGUAGES": { 14 | 1: [ 15 | {"code": "en", "name": "English", "public": True}, 16 | {"code": "it", "name": "Italiano", "public": True}, 17 | {"code": "fr", "name": "French", "public": True}, 18 | ], 19 | "default": {"hide_untranslated": False}, 20 | }, 21 | "FILE_UPLOAD_TEMP_DIR": mkdtemp(), 22 | } 23 | 24 | 25 | def run(): 26 | from app_helper import runner 27 | 28 | runner.cms("example1") 29 | 30 | 31 | def setup(): 32 | import sys 33 | 34 | from app_helper import runner 35 | 36 | runner.setup("example1", sys.modules[__name__], use_cms=True) 37 | 38 | 39 | def setup_nocms(): 40 | import sys 41 | 42 | from app_helper import runner 43 | 44 | runner.setup("example1", sys.modules[__name__], use_cms=False) 45 | 46 | 47 | if __name__ == "__main__": 48 | run() 49 | -------------------------------------------------------------------------------- /tests/test_utils/cms_helper.py: -------------------------------------------------------------------------------- 1 | from tempfile import mkdtemp 2 | 3 | try: 4 | import djangocms_text_ckeditor # NOQA 5 | 6 | text_plugin = ["djangocms_text_ckeditor"] 7 | except ImportError: 8 | text_plugin = [] 9 | 10 | HELPER_SETTINGS = { 11 | "TIME_ZONE": "Europe/Rome", 12 | "INSTALLED_APPS": ["example2", "filer"] + text_plugin, 13 | "CMS_LANGUAGES": { 14 | 1: [ 15 | {"code": "en", "name": "English", "public": True}, 16 | {"code": "it", "name": "Italiano", "public": True}, 17 | {"code": "fr", "name": "French", "public": True}, 18 | ], 19 | "default": {"hide_untranslated": False}, 20 | }, 21 | "FILE_UPLOAD_TEMP_DIR": mkdtemp(), 22 | } 23 | 24 | 25 | def run(): 26 | from app_helper import runner 27 | 28 | runner.cms("example1") 29 | 30 | 31 | def setup(): 32 | import sys 33 | 34 | from app_helper import runner 35 | 36 | runner.setup("example1", sys.modules[__name__], use_cms=True) 37 | 38 | 39 | def setup_nocms(): 40 | import sys 41 | 42 | from app_helper import runner 43 | 44 | runner.setup("example1", sys.modules[__name__], use_cms=False) 45 | 46 | 47 | if __name__ == "__main__": 48 | run() 49 | -------------------------------------------------------------------------------- /tests/test_utils/cms_helper_custom.py: -------------------------------------------------------------------------------- 1 | from tempfile import mkdtemp 2 | 3 | try: 4 | import djangocms_text_ckeditor # NOQA 5 | 6 | text_plugin = ["djangocms_text_ckeditor"] 7 | except ImportError: 8 | text_plugin = [] 9 | 10 | HELPER_SETTINGS = { 11 | "TIME_ZONE": "Europe/Rome", 12 | "INSTALLED_APPS": ["example2", "filer"] + text_plugin, 13 | "CMS_LANGUAGES": { 14 | 1: [ 15 | {"code": "en", "name": "English", "public": True}, 16 | {"code": "it", "name": "Italiano", "public": True}, 17 | {"code": "fr", "name": "French", "public": True}, 18 | ], 19 | "default": {"hide_untranslated": False}, 20 | }, 21 | "FILE_UPLOAD_TEMP_DIR": mkdtemp(), 22 | } 23 | 24 | 25 | def run(): 26 | from app_helper import runner 27 | 28 | runner.cms("example1") 29 | 30 | 31 | def setup(): 32 | import sys 33 | 34 | from app_helper import runner 35 | 36 | runner.setup("example1", sys.modules[__name__], use_cms=True) 37 | 38 | 39 | def setup_nocms(): 40 | import sys 41 | 42 | from app_helper import runner 43 | 44 | runner.setup("example1", sys.modules[__name__], use_cms=False) 45 | 46 | 47 | if __name__ == "__main__": 48 | run() 49 | -------------------------------------------------------------------------------- /docs/migrating.rst: -------------------------------------------------------------------------------- 1 | _migrating: 2 | 3 | #################################################### 4 | Migrating from djangocms-helper to django-app-helper 5 | #################################################### 6 | 7 | This project used to be called djangocms-helper. 8 | It's been renamed in version 2.0 to clarify that it's not limited to django CMS apps. 9 | 10 | Migration is straightforward as it does not require any change to the codebase: 11 | 12 | * all imports from ``djangocms_helper`` namespace are still valid and they won't be deprecated soon 13 | * runner filername ``cms_helper.py`` is still valid and it won't be deprecated soon 14 | 15 | ********************************* 16 | Migration path 17 | ********************************* 18 | 19 | * Replace ``djangocms-helper`` package name from any dependendency declaration 20 | (``setup.py``, ``tox.ini``, ``requirements.txt`` ...) 21 | 22 | That's it! 23 | 24 | ********************************* 25 | Bugfixes and further development 26 | ********************************* 27 | 28 | Bugfixes to djangocms-helper 1.2.x will be released until reasonable under the 29 | old package name, while new features (including new Django / django CMS 30 | versions support will only be available in the django-app-helper package). 31 | -------------------------------------------------------------------------------- /helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | 6 | def gettext(s): 7 | return s 8 | 9 | 10 | HELPER_SETTINGS = dict( 11 | INSTALLED_APPS=[ 12 | "tests.test_utils", 13 | "tests.test_utils.example1", 14 | "tests.test_utils.example2", 15 | "filer", 16 | "easy_thumbnails", 17 | ], 18 | LANGUAGE_CODE="en", 19 | ) 20 | try: 21 | import cms # noqa: F401 22 | 23 | HELPER_SETTINGS["INSTALLED_APPS"].append("djangocms_text_ckeditor") 24 | except (ImportError, ModuleNotFoundError): 25 | pass 26 | 27 | 28 | def run(): 29 | from app_helper import runner 30 | 31 | try: 32 | import cms # noqa: F401 F811 33 | 34 | runner.cms("app_helper") 35 | except (ImportError, ModuleNotFoundError): 36 | runner.run("app_helper") 37 | 38 | 39 | def setup(): 40 | from app_helper import runner 41 | 42 | try: 43 | import cms # noqa: F401 F811 44 | 45 | use_cms = True 46 | except (ImportError, ModuleNotFoundError): 47 | use_cms = False 48 | runner.setup("app_helper", sys.modules[__name__], use_cms=use_cms) 49 | 50 | 51 | if __name__ == "__main__": 52 | run() 53 | 54 | if __name__ == "helper": 55 | # this is needed to run cms_helper in pycharm 56 | setup() 57 | -------------------------------------------------------------------------------- /app_helper/pytest_runner.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shlex 3 | 4 | 5 | class PytestTestRunner: 6 | """Runs pytest to discover and run tests.""" 7 | 8 | def __init__(self, verbosity=1, failfast=False, keepdb=False, **kwargs): 9 | self.verbosity = verbosity 10 | self.failfast = failfast 11 | self.keepdb = keepdb 12 | self.extra_args = kwargs.pop("pytest_args", "") 13 | 14 | def run_tests(self, test_labels, *args, **kwargs): 15 | """Run pytest and return the exitcode. 16 | 17 | It translates some of Django's test command option to pytest's. 18 | """ 19 | import pytest 20 | 21 | self.verbosity = kwargs.get("verbosity", self.verbosity) 22 | self.failfast = kwargs.get("failfast", self.failfast) 23 | self.keepdb = kwargs.get("keepdb", self.keepdb) 24 | argv = shlex.split(os.environ.get("PYTEST_ARGS", "")) 25 | if self.extra_args: 26 | argv.extend(shlex.split(self.extra_args)) 27 | if self.verbosity == 0: # pragma: no cover 28 | argv.append("--quiet") 29 | if self.verbosity == 2: # pragma: no cover 30 | argv.append("--verbose") 31 | if self.verbosity == 3: # pragma: no cover 32 | argv.append("-vv") 33 | if self.failfast: # pragma: no cover 34 | argv.append("--exitfirst") 35 | if self.keepdb: # pragma: no cover 36 | argv.append("--reuse-db") 37 | 38 | argv.extend(test_labels) 39 | return pytest.main(argv) 40 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | exclude: "(.idea|node_modules|.tox)" 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v6.0.0 7 | hooks: 8 | - id: trailing-whitespace 9 | exclude: "setup.cfg" 10 | - id: end-of-file-fixer 11 | - id: check-yaml 12 | - id: check-added-large-files 13 | - id: check-builtin-literals 14 | - id: check-executables-have-shebangs 15 | - id: check-merge-conflict 16 | - id: check-toml 17 | - repo: https://github.com/PyCQA/isort 18 | rev: "7.0.0" 19 | hooks: 20 | - id: isort 21 | - repo: https://github.com/psf/black-pre-commit-mirror 22 | rev: 25.11.0 23 | hooks: 24 | - id: black 25 | - repo: https://github.com/astral-sh/ruff-pre-commit 26 | rev: 'v0.14.7' 27 | hooks: 28 | - id: ruff 29 | - repo: https://github.com/asottile/pyupgrade 30 | rev: v3.21.2 31 | hooks: 32 | - id: pyupgrade 33 | args: 34 | - --py3-plus 35 | - repo: https://github.com/adamchainz/django-upgrade 36 | rev: "1.29.1" 37 | hooks: 38 | - id: django-upgrade 39 | args: [--target-version, "3.2"] 40 | - repo: local 41 | hooks: 42 | - id: towncrier 43 | name: towncrier 44 | entry: inv towncrier-check 45 | language: system 46 | pass_filenames: false 47 | always_run: true 48 | ci: 49 | skip: 50 | - towncrier 51 | -------------------------------------------------------------------------------- /docs/development.rst: -------------------------------------------------------------------------------- 1 | ####################### 2 | Development & community 3 | ####################### 4 | 5 | Django App Helper is an open-source project. 6 | 7 | You don't need to be an expert developer to make a valuable contribution - all you need is a little 8 | knowledge, and a willingness to follow the contribution guidelines. 9 | 10 | ******* 11 | Nephila 12 | ******* 13 | 14 | Django App Helper was created by Iacopo Spalletti at `Nephila `_ 15 | and is released under a GNU GENERAL PUBLIC LICENSE. 16 | 17 | Nephila is an active supporter of django CMS and its community. Django App Helper is intended to 18 | help make it easier for developers in the django CMS ecosystem to work effectively and create 19 | high quality applications. 20 | 21 | Nephila maintains overall control of the `Django App Helper repository 22 | `_. 23 | 24 | ******************** 25 | Standards & policies 26 | ******************** 27 | 28 | Django App Helper is a django CMS application, and shares much of django CMS's standards and 29 | policies (when relevant). 30 | 31 | These include: 32 | 33 | * `guidelines and policies 34 | `_ for contributing 35 | to the project 36 | * a `code of conduct 37 | `_ for community 38 | activity 39 | 40 | Please familiarise yourself with this documentation if you'd like to contribute to 41 | Django App Helper. 42 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Code quality 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 9 | strategy: 10 | matrix: 11 | python-version: ["3.11.x"] 12 | toxenv: [ruff, isort, black, pypi-description, docs, towncrier] 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | repository: ${{ github.event.pull_request.head.repo.full_name }} 17 | ref: ${{ github.event.pull_request.head.ref }} 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Cache pip 23 | uses: actions/cache@v3 24 | with: 25 | path: ~/.cache/pip 26 | key: ${{ runner.os }}-pip-${{ matrix.toxenv }} 27 | restore-keys: | 28 | ${{ runner.os }}-pip-${{ matrix.toxenv }} 29 | - name: Cache tox 30 | uses: actions/cache@v3 31 | with: 32 | path: .tox 33 | key: ${{ runner.os }}-lint-${{ matrix.toxenv }}-${{ hashFiles('setup.cfg') }} 34 | restore-keys: | 35 | ${{ runner.os }}-lint-${{ matrix.toxenv }}- 36 | - name: Install dependencies 37 | run: | 38 | python -m pip install --upgrade pip setuptools tox>4 39 | - name: Test with tox 40 | if: ${{ matrix.toxenv != 'towncrier' || (!contains(github.event.head_commit.message, '[pre-commit.ci]') && !contains(github.event.pull_request.body, 'pre-commit.ci start')) }} 41 | run: | 42 | tox -e${{ matrix.toxenv }} 43 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=40.6.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | line-length = 119 7 | target-version = ["py36"] 8 | include = 'app_helper/*py' 9 | 10 | [tool.towncrier] 11 | package = "app_helper" 12 | directory = "changes" 13 | filename = "HISTORY.rst" 14 | title_format = "{version} ({project_date})" 15 | 16 | [tool.interrogate] 17 | ignore-init-method = true 18 | ignore-init-module = true 19 | ignore-magic = false 20 | ignore-semiprivate = false 21 | ignore-private = false 22 | ignore-module = true 23 | ignore-nested-functions = true 24 | fail-under = 0 25 | exclude = ["docs", ".tox"] 26 | ignore-regex = ["^get$", "^mock_.*", ".*BaseClass.*"] 27 | verbose = 0 28 | quiet = false 29 | whitelist-regex = [] 30 | color = true 31 | 32 | [tool.isort] 33 | profile = "black" 34 | combine_as_imports = true 35 | default_section = "THIRDPARTY" 36 | force_grid_wrap = 0 37 | include_trailing_comma = true 38 | known_first_party = "knocker" 39 | line_length = 119 40 | multi_line_output = 3 41 | use_parentheses = true 42 | 43 | [tool.ruff] 44 | ignore = [] 45 | line-length = 119 46 | target-version = "py310" 47 | 48 | [tool.ruff.mccabe] 49 | max-complexity = 10 50 | 51 | [tool.bumpversion] 52 | allow_dirty = false 53 | commit = true 54 | message = "Release {new_version}" 55 | commit_args = "--no-verify" 56 | tag = false 57 | current_version = "3.3.6.dev1" 58 | parse = """(?x) 59 | (?P[0-9]+) 60 | \\.(?P[0-9]+) 61 | \\.(?P[0-9]+) 62 | (?: 63 | .(?Pdev) 64 | (?:(?P[0-9]+))? 65 | )? 66 | """ 67 | serialize = [ 68 | "{major}.{minor}.{patch}.{release}{relver}", 69 | "{major}.{minor}.{patch}" 70 | ] 71 | 72 | [tool.bumpversion.parts.release] 73 | values = [ 74 | "dev", 75 | "" 76 | ] 77 | optional_value = "dev" 78 | 79 | [[tool.bumpversion.files]] 80 | filename = "app_helper/__init__.py" 81 | search = "{current_version}" 82 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = django-app-helper 3 | version = attr: app_helper.__version__ 4 | url = https://github.com/nephila/django-app-helper 5 | project_urls = 6 | Documentation = https://django-app-helper.readthedocs.io/ 7 | author = Iacopo Spalletti 8 | author_email = i.spalletti@nephila.digital 9 | description = Helper for django applications development 10 | long_description = file: README.rst, HISTORY.rst 11 | long_description_content_type = text/x-rst 12 | license = GPLv2+ 13 | license_files = LICENSE 14 | classifiers = 15 | License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+) 16 | Development Status :: 5 - Production/Stable 17 | Framework :: Django 18 | Framework :: Django :: 3.2 19 | Framework :: Django :: 4.1 20 | Framework :: Django :: 4.2 21 | Framework :: Django :: 5.0 22 | Programming Language :: Python 23 | Programming Language :: Python :: 3 24 | Programming Language :: Python :: 3.8 25 | Programming Language :: Python :: 3.9 26 | Programming Language :: Python :: 3.10 27 | Programming Language :: Python :: 3.11 28 | Programming Language :: Python :: 3.12 29 | 30 | [options] 31 | include_package_data = True 32 | install_requires = 33 | dj-database-url 34 | docopt 35 | six 36 | setuptools; python_version>="3.12" 37 | setup_requires = 38 | setuptools 39 | packages = find: 40 | python_requires = >=3.7 41 | test_suite = app_helper.tests 42 | zip_safe = False 43 | keywords = 44 | django 45 | tests 46 | development 47 | pytest 48 | django-cms 49 | 50 | [options.package_data] 51 | * = *.txt, *.rst 52 | app_helper = *.html *.png *.gif *js *jpg *jpeg *svg *py *mo *po 53 | 54 | [options.entry_points] 55 | console_scripts = 56 | django-app-helper = app_helper.main:main 57 | 58 | [options.extras_require] 59 | cms = django-cms>=3.7,<3.12 60 | async = 61 | channels 62 | daphne 63 | docs = 64 | sphinx-rtd-theme 65 | 66 | [upload] 67 | repository = https://upload.pypi.org/legacy/ 68 | 69 | [sdist] 70 | formats = zip 71 | 72 | [bdist_wheel] 73 | universal = 1 74 | -------------------------------------------------------------------------------- /docs/asgi.rst: -------------------------------------------------------------------------------- 1 | ####################### 2 | ASGI / Channels support 3 | ####################### 4 | 5 | django-app-helper comes with minimal channels / ASGI support. 6 | 7 | Default configuration provides a sample ``asgi`` application already enabled in ``ASGI_APPLICATION`` setting. 8 | 9 | This means that if you install ``channels`` or ``daphne`` in your rest environment ``./helper.py server`` can run a channels / ASGI enabled instance. 10 | 11 | .. note:: Pure ASGI support is available only for Django 3.0+. 12 | 13 | ************************ 14 | Run with channels 15 | ************************ 16 | 17 | To run with channels you must provide an ``ASGI_APPLICATION`` in the project ``helper.py`` pointing to your base channels application. 18 | 19 | Optionally you can set ``CHANNEL_LAYERS``. 20 | 21 | Example: 22 | 23 | .. code-block:: python 24 | 25 | HELPER_SETTINGS = dict( 26 | ... 27 | # required 28 | ASGI_APPLICATION='tests.example_app.routing.application', 29 | # Optional 30 | CHANNEL_LAYERS={ 31 | 'default': { 32 | 'BACKEND': 'channels_redis.core.RedisChannelLayer', 33 | 'CONFIG': { 34 | 'hosts': [('localhost', 6379)], 35 | }, 36 | }, 37 | }, 38 | ... 39 | ) 40 | 41 | 42 | The run the ``server`` command with the ``--use-channels`` option set:: 43 | 44 | $ python helper.py server --use-channels 45 | 46 | ************************ 47 | Run with daphne 48 | ************************ 49 | 50 | To run with daphne you can provide a custom ``ASGI_APPLICATION`` in the project ``helper.py`` if you actually have one or more ASGI application configure beyond django. The default ``ASGI_APPLICATION`` will run the django runserver command. 51 | 52 | Example: 53 | 54 | .. code-block:: python 55 | 56 | 57 | HELPER_SETTINGS = dict( 58 | ... 59 | ASGI_APPLICATION='my_project.asgi:application', 60 | ... 61 | ) 62 | 63 | 64 | The run the ``server`` command with the ``--use-daphne`` option set:: 65 | 66 | $ python helper.py server --use-daphne 67 | -------------------------------------------------------------------------------- /docs/basetest.rst: -------------------------------------------------------------------------------- 1 | ################ 2 | Base test mixins 3 | ################ 4 | 5 | The following mixins are available to provide helpers and methods that implements helpers and functions commonly 6 | used in tests. 7 | :py:class:`BaseTestCase`, :py:class:`BaseTransactionTestCase` are concrete classes implementing all the mixins and 8 | extending respectively :py:class:`django.tests.TestCase` and :py:class:`django.tests.TransactionTestCase` 9 | 10 | 11 | .. autoclass:: app_helper.base_test.RequestTestCaseMixin 12 | :members: 13 | :private-members: 14 | 15 | 16 | .. autoclass:: app_helper.base_test.CreateTestDataMixin 17 | :members: 18 | 19 | .. automethod:: app_helper.base_test.CreateTestDataMixin._setup_users 20 | .. automethod:: app_helper.base_test.CreateTestDataMixin._teardown_users 21 | 22 | .. autoattribute:: app_helper.base_test.CreateTestDataMixin._admin_user_username 23 | .. autoattribute:: app_helper.base_test.CreateTestDataMixin._admin_user_password 24 | .. autoattribute:: app_helper.base_test.CreateTestDataMixin._admin_user_email 25 | .. autoattribute:: app_helper.base_test.CreateTestDataMixin._staff_user_username 26 | .. autoattribute:: app_helper.base_test.CreateTestDataMixin._staff_user_password 27 | .. autoattribute:: app_helper.base_test.CreateTestDataMixin._staff_user_email 28 | .. autoattribute:: app_helper.base_test.CreateTestDataMixin._user_user_username 29 | .. autoattribute:: app_helper.base_test.CreateTestDataMixin._user_user_password 30 | .. autoattribute:: app_helper.base_test.CreateTestDataMixin._user_user_email 31 | 32 | 33 | .. autoclass:: app_helper.base_test.CMSPageRenderingMixin 34 | :members: 35 | 36 | .. automethod:: app_helper.base_test.CMSPageRenderingMixin._setup_cms 37 | .. autoattribute:: app_helper.base_test.CMSPageRenderingMixin._pages_data 38 | 39 | 40 | .. autoclass:: app_helper.base_test.GenericHelpersMixin 41 | :members: 42 | :private-members: 43 | 44 | .. autoclass:: app_helper.base_test.BaseNoDataTestCaseMixin 45 | :members: 46 | :private-members: 47 | 48 | 49 | .. autoclass:: app_helper.base_test.BaseTestCaseMixin 50 | :members: 51 | :private-members: 52 | 53 | 54 | .. autoclass:: app_helper.base_test.BaseTestCase 55 | 56 | .. autoclass:: app_helper.base_test.BaseTransactionTestCase 57 | -------------------------------------------------------------------------------- /docs/runner.rst: -------------------------------------------------------------------------------- 1 | .. _runner: 2 | 3 | ################# 4 | Integrated runner 5 | ################# 6 | 7 | Django App Helper provide a runner to invoke the commands without requiring the 8 | ``django-app-helper`` file; this can be useful to invoke tests with coverage or to 9 | have a simpler syntax to remember. 10 | 11 | Typically you'd setup the runner function in the :ref:`extra settings file `:: 12 | 13 | 14 | HELPER_SETTINGS={ 15 | 'INSTALLED_APPS': [ 16 | 'any_django_app', 17 | ], 18 | 'ANY_SETTING: False, 19 | ... 20 | } 21 | 22 | 23 | def run(): 24 | from app_helper import runner 25 | runner.cms('my_app') 26 | 27 | if __name__ == "__main__": 28 | run() 29 | 30 | 31 | with the above code in place you can run any Django App Helper command as:: 32 | 33 | python helper.py 34 | 35 | and adding the ``test_suite`` argument to ``setup.py``:: 36 | 37 | setup( 38 | ... 39 | test_suite='app_helper.run', 40 | ... 41 | ) 42 | 43 | you can invoke the tests with:: 44 | 45 | python setup.py test 46 | 47 | 48 | ****************** 49 | Django environment 50 | ****************** 51 | 52 | If you don't need django CMS, you can use a runner function with no CMS attached:: 53 | 54 | 55 | def run(): 56 | from app_helper import runner 57 | runner.run('my_app') 58 | 59 | if __name__ == "__main__": 60 | run() 61 | 62 | 63 | .. warning:: The runner **must** be invoked from the **settings** file. 64 | The runner takes care of setting up the file in which is 65 | invoked as the ``extra_settings`` file. 66 | 67 | .. _naked_runner: 68 | 69 | ****************** 70 | Naked setup 71 | ****************** 72 | 73 | Sometimes you just want to properly setup a Django environment without running any commands 74 | (e.g: when building Sphinx docs using autodoc). Naked setup allows to do so:: 75 | 76 | 77 | def setup(): 78 | import sys 79 | from app_helper import runner 80 | runner.setup('my_app', sys.modules[__name__], use_cms=True) 81 | 82 | if __name__ == "cms_helper": 83 | setup() 84 | 85 | the last lines allows to auto-load naked setup when runner file is imported. This is 86 | useful when running tests in a PyCharm environment. 87 | In case you customized the runner filename, replace ``"cms_helper"`` with the 88 | custom name. 89 | 90 | .. warning:: The runner **must** be invoked from the **settings** file. 91 | The runner takes care of setting up the file in which is 92 | invoked as the ``extra_settings`` file. 93 | -------------------------------------------------------------------------------- /app_helper/default_settings.py: -------------------------------------------------------------------------------- 1 | def get_default_settings(CMS_APP, CMS_PROCESSORS, CMS_MIDDLEWARE, CMS_APP_STYLE, URLCONF, application): # NOQA 2 | return dict( # NOQA 3 | INSTALLED_APPS=[ 4 | "django.contrib.contenttypes", 5 | "django.contrib.auth", 6 | "django.contrib.sessions", 7 | "django.contrib.sites", 8 | "django.contrib.staticfiles", 9 | ] 10 | + CMS_APP_STYLE 11 | + ["django.contrib.admin", "app_helper.test_data", "django.contrib.messages"] 12 | + CMS_APP, 13 | DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}}, 14 | TEMPLATE_LOADERS=[ 15 | "django.template.loaders.filesystem.Loader", 16 | "django.template.loaders.app_directories.Loader", 17 | ], 18 | STATICFILES_FINDERS=[ 19 | "django.contrib.staticfiles.finders.FileSystemFinder", 20 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 21 | ], 22 | TEMPLATE_CONTEXT_PROCESSORS=[ 23 | "django.contrib.auth.context_processors.auth", 24 | "django.contrib.messages.context_processors.messages", 25 | "django.core.context_processors.i18n", 26 | "django.core.context_processors.csrf", 27 | "django.core.context_processors.debug", 28 | "django.core.context_processors.tz", 29 | "django.core.context_processors.request", 30 | "django.core.context_processors.media", 31 | "django.core.context_processors.static", 32 | ] 33 | + CMS_PROCESSORS, 34 | MIDDLEWARE_CLASSES=[ 35 | "django.middleware.http.ConditionalGetMiddleware", 36 | "django.contrib.sessions.middleware.SessionMiddleware", 37 | "django.contrib.auth.middleware.AuthenticationMiddleware", 38 | "django.contrib.messages.middleware.MessageMiddleware", 39 | "django.middleware.csrf.CsrfViewMiddleware", 40 | "django.middleware.locale.LocaleMiddleware", 41 | "django.middleware.common.CommonMiddleware", 42 | ] 43 | + CMS_MIDDLEWARE, 44 | ROOT_URLCONF=URLCONF, 45 | SITE_ID=1, 46 | LANGUAGE_CODE="en", 47 | LANGUAGES=(("en", "English"),), 48 | STATIC_URL="/static/", 49 | MEDIA_URL="/media/", 50 | DEBUG=True, 51 | CMS_TEMPLATES=(("fullwidth.html", "Fullwidth"), ("page.html", "Normal page")), 52 | PASSWORD_HASHERS=("django.contrib.auth.hashers.MD5PasswordHasher",), 53 | MIGRATION_MODULES={}, 54 | EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", 55 | ASGI_APPLICATION="app_helper.asgi:application", 56 | ) 57 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tox tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 8 | runs-on: ubuntu-latest 9 | continue-on-error: ${{ matrix.continue-on-error }} 10 | strategy: 11 | matrix: 12 | python-version: ["3.12", "3.11", "3.10", "3.9", "3.8"] 13 | django: [50, 42, 32] 14 | cms: [nocms, cms311, async] 15 | continue-on-error: [false] 16 | exclude: 17 | - django: 50 18 | python-version: 3.9 19 | - django: 50 20 | python-version: 3.8 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Cache pip 28 | uses: actions/cache@v3 29 | with: 30 | path: ~/.cache/pip 31 | key: ${{ runner.os }}-pip-${{ matrix.toxenv }} 32 | restore-keys: | 33 | ${{ runner.os }}-pip-${{ matrix.toxenv }} 34 | - name: Cache tox 35 | uses: actions/cache@v3 36 | with: 37 | path: .tox 38 | key: ${{ runner.os }}-tox-${{ format('{{py{0}-django{1}-{2}}}', matrix.python-version, matrix.django, matrix.cms) }}-${{ hashFiles('setup.cfg') }} 39 | restore-keys: | 40 | ${{ runner.os }}-tox-${{ format('{{py{0}-django{1}-{2}}}', matrix.python-version, matrix.django, matrix.cms) }}- 41 | - name: Install dependencies 42 | run: | 43 | sudo apt-get install gettext 44 | python -m pip install --upgrade pip setuptools tox>4 45 | - name: Test with tox 46 | env: 47 | TOX_ENV: ${{ format('py{0}-django{1}-{2}', matrix.python-version, matrix.django, matrix.cms) }} 48 | COMMAND: coverage run 49 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | COVERALLS_SERVICE_NAME: github 51 | run: | 52 | tox -e$TOX_ENV 53 | .tox/$TOX_ENV/bin/coverage xml 54 | - name: Coveralls Parallel 55 | uses: coverallsapp/github-action@v2 56 | with: 57 | github-token: ${{ secrets.GITHUB_TOKEN }} 58 | parallel: true 59 | debug: true 60 | files: ./coverage.xml 61 | flag-name: run-${{ format('py{0}-django{1}-{2}', matrix.python-version, matrix.django, matrix.cms) }} 62 | - uses: codecov/codecov-action@v3 63 | with: 64 | token: ${{ secrets.CODECOV_TOKEN }} 65 | flags: unittests 66 | files: ./coverage.xml 67 | fail_ci_if_error: false 68 | finish: 69 | needs: test 70 | if: ${{ always() }} 71 | runs-on: ubuntu-latest 72 | steps: 73 | - name: Coveralls Finished 74 | uses: coverallsapp/github-action@v2 75 | with: 76 | parallel-finished: true 77 | -------------------------------------------------------------------------------- /docs/pytest.rst: -------------------------------------------------------------------------------- 1 | ############## 2 | pytest support 3 | ############## 4 | 5 | While django-app-helper was born with Django ``TestCase`` in mind, it can be used with ``pytest`` with some configuration 6 | together with ``pytest-django``. 7 | 8 | ************************ 9 | django-app-helper runner 10 | ************************ 11 | 12 | You can run pytest tests by using a custom runner (based on `pytest-django`_ documentation); to enable it, 13 | add the following to project ``helper.py`` file: 14 | 15 | .. code-block:: python 16 | 17 | HELPER_SETTINGS = { 18 | ... 19 | "TEST_RUNNER": "app_helper.pytest_runner.PytestTestRunner", 20 | ... 21 | } 22 | 23 | Using this approach you can mix pytest tests and Django ``TestCase`` ones, the runner will take care 24 | of discovering and running both. 25 | 26 | Running tests 27 | ============== 28 | 29 | Invoke ``app_helper`` as usual:: 30 | 31 | $ python helper.py test 32 | 33 | pytest options 34 | ============== 35 | 36 | The runner support translates the following Django test runner options to pytest ones: 37 | 38 | * ``verbosity == 0``: ``--quiet`` 39 | * ``verbosity == 2``: ``--verbose`` 40 | * ``verbosity == 3``: ``-vv`` 41 | * ``failfast``: ``--exitfirst`` 42 | * ``keepdb``: ``--reuse-db`` 43 | 44 | All the other pytest and pytest plugins are supported either via ``PYTEST_ARGS`` enviroment variable or 45 | ``--runner-options`` cmdline argument. 46 | 47 | Environment variable example:: 48 | 49 | PYTEST_ARGS='-s -k my_test' python helper.py test 50 | 51 | argument variable example:: 52 | 53 | python helper.py test --runner-options="-k my_test" 54 | 55 | In case arguments are passed via both channels they are merged together, with runner-options arguments having priority 56 | over environment variables in case of overlapping options. 57 | 58 | .. _pytest-django: https://pytest-django.readthedocs.io/en/latest/faq.html#how-can-i-use-manage-py-test-with-pytest-django 59 | 60 | *************** 61 | standard pytest 62 | *************** 63 | 64 | Running tests 65 | ============== 66 | 67 | Invoke ``pytest`` as usual:: 68 | 69 | $ python -mpytest 70 | 71 | or:: 72 | 73 | $ pytest 74 | 75 | In this case you don't need any special syntax to pass commands as the 76 | django-app-helper pytest runner is not executed and pytest is full in control. 77 | 78 | .. warning: the ``pytest`` invocation will only works if you add the current directory in the ``PYTHONPATH``, thus the 79 | ``python -mpytest`` version is preferred. 80 | 81 | Using BaseTestCaseMixin 82 | ======================= 83 | 84 | While its :py:class:`~app_helper.base_test.BaseTestCaseMixin` is built on Django ``TestCase``, it can be used in pytest classes: 85 | 86 | Fixtures, markers and decorators can be used as usual on test methods as in classic pytest classes. 87 | 88 | .. code-block:: python 89 | 90 | class TestTags(BaseTestCaseMixin): 91 | ... 92 | def test_foo(self): 93 | ... 94 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '37 3 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: 'ubuntu-latest' 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Use only 'java' to analyze code written in Java, Kotlin or both 38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v3 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v2 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | # queries: security-extended,security-and-quality 56 | 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@v2 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 65 | 66 | # If the Autobuild fails above, remove it and uncomment the following three lines. 67 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 68 | 69 | # - run: | 70 | # echo "Run, Build Application using script" 71 | # ./location_of_script_within_repo/buildscript.sh 72 | 73 | - name: Perform CodeQL Analysis 74 | uses: github/codeql-action/analyze@v2 75 | with: 76 | category: "/language:${{matrix.language}}" 77 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | black 4 | blacken 5 | docs 6 | isort 7 | isort_format 8 | ruff 9 | pypi-description 10 | towncrier 11 | py{3.12}-django{50,42}-{cms311,nocms,async} 12 | py{3.11}-django{50,42,41,40,32}-{cms311,nocms,async} 13 | py{3.10}-django{50,42,41,40,32}-{cms311,nocms,async} 14 | py{3.9}-django{32}-{cms311,nocms,async} 15 | py{3.8}-django{32}-{cms311,nocms,async} 16 | minversion = 3.23 17 | 18 | [testenv] 19 | commands = {env:COMMAND:python} helper.py {posargs} 20 | deps= 21 | django32: django~=3.2.0 22 | django40: django~=4.0.0 23 | django41: django~=4.1.0 24 | django42: django~=4.2.0 25 | django50: django~=5.0.0 26 | cms311: https://github.com/django-cms/django-cms/archive/develop.zip 27 | cms311: djangocms-text-ckeditor>=5.0,<6.0 28 | py3.8: dj-database-url<2.3.0 29 | -r{toxinidir}/requirements-test.txt 30 | async: channels[daphne]~=4.0.0 31 | passenv = 32 | COMMAND 33 | PYTEST_* 34 | 35 | [testenv:nocms] 36 | alwayscopy = True 37 | 38 | [testenv:ruff] 39 | commands = 40 | {envpython} -m ruff check app_helper tests {posargs} 41 | {envpython} -minterrogate -c pyproject.toml app_helper tests 42 | deps = 43 | interrogate 44 | ruff 45 | skip_install = true 46 | 47 | [testenv:isort] 48 | commands = 49 | {envpython} -m isort -c --df app_helper tests 50 | deps = isort~=5.12.0 51 | skip_install = true 52 | 53 | [testenv:isort_format] 54 | commands = 55 | {envpython} -m isort app_helper tests 56 | deps = {[testenv:isort]deps} 57 | skip_install = true 58 | 59 | [testenv:black] 60 | commands = 61 | {envpython} -m black --check --diff . 62 | deps = black 63 | skip_install = true 64 | 65 | [testenv:blacken] 66 | commands = 67 | {envpython} -m black . 68 | deps = {[testenv:black]deps} 69 | skip_install = true 70 | 71 | [testenv:docs] 72 | commands = 73 | {envpython} -m invoke docbuild 74 | deps = 75 | invoke 76 | sphinx 77 | sphinx-rtd-theme 78 | sphinx-autobuild 79 | livereload~=2.6 80 | django<4.0 81 | -rrequirements-test.txt 82 | skip_install = true 83 | 84 | [testenv:towncrier] 85 | commands = 86 | {envpython} -m invoke towncrier-check 87 | deps = 88 | invoke 89 | skip_install = true 90 | 91 | [testenv:pypi-description] 92 | commands = 93 | {envpython} -m invoke clean 94 | {envpython} -m check_manifest 95 | {envpython} -m build . 96 | {envpython} -m twine check dist/* 97 | deps = 98 | invoke 99 | check-manifest 100 | build 101 | twine 102 | skip_install = true 103 | 104 | [testenv:release] 105 | commands = 106 | {envpython} -m invoke clean 107 | {envpython} -m check_manifest 108 | {envpython} -m build . 109 | {envpython} -m twine upload {posargs} dist/* 110 | deps = {[testenv:pypi-description]deps} 111 | passenv = 112 | TWINE_* 113 | skip_install = true 114 | 115 | [check-manifest] 116 | ignore = 117 | .* 118 | *.ini 119 | *.toml 120 | *.json 121 | *.txt 122 | *.yml 123 | *.yaml 124 | .tx/** 125 | changes/** 126 | docs/** 127 | helper.py 128 | tasks.py 129 | tests/** 130 | *.mo 131 | ignore-bad-ideas = 132 | *.mo 133 | 134 | [pytest] 135 | DJANGO_SETTINGS_MODULE = helper 136 | python_files = test_*.py 137 | traceback = short 138 | addopts = --reuse-db -p no:warnings 139 | -------------------------------------------------------------------------------- /app_helper/server.py: -------------------------------------------------------------------------------- 1 | from django.utils import autoreload 2 | 3 | from .utils import _create_db, create_user, get_user_model 4 | 5 | 6 | def _run_django(settings, bind, port, migrate_cmd, verbose): 7 | """Run channels runserver.""" 8 | from django.core.management.commands import runserver 9 | 10 | _setup_db(migrate_cmd) 11 | _init_runserver(runserver, bind, port, verbose) 12 | 13 | 14 | def _run_daphne(settings, bind, port, migrate_cmd, verbose): 15 | """Run daphne runserver.""" 16 | from daphne.cli import CommandLineInterface 17 | 18 | _setup_db(migrate_cmd) 19 | daphne_args = ["-b", bind, "-p", port, "-v", verbose or "1", settings.ASGI_APPLICATION] 20 | autoreload.run_with_reloader(CommandLineInterface().run, daphne_args) 21 | 22 | 23 | def _run_channels(settings, bind, port, migrate_cmd, verbose): 24 | """Run channels runserver.""" 25 | from channels.management.commands import runserver 26 | 27 | _setup_db(migrate_cmd) 28 | _init_runserver(runserver, bind, port, verbose, channels=True) 29 | 30 | 31 | def _init_runserver(runserver_module, bind, port, verbose, logger=None, channels=False): 32 | """Run base django / channels runserver with autoreloader.""" 33 | rs = runserver_module.Command() 34 | rs.use_ipv6 = False 35 | rs._raw_ipv6 = False 36 | rs.addr = bind 37 | rs.port = port 38 | if logger: # pragma: no cover 39 | rs.logger = logger 40 | if channels: 41 | rs.http_timeout = 60 42 | rs.websocket_handshake_timeout = 5 43 | autoreload.run_with_reloader( 44 | rs.inner_run, 45 | **{ 46 | "addrport": "{}:{}".format(bind, port), 47 | "insecure_serving": True, 48 | "use_static_handler": True, 49 | "use_threading": True, 50 | "verbosity": verbose, 51 | "skip_checks": True, 52 | "use_reloader": True, 53 | }, 54 | ) 55 | 56 | 57 | def _setup_db(migrate_cmd): 58 | """Initialize the database running migrations and creating the default user.""" 59 | _create_db(migrate_cmd) 60 | User = get_user_model() # NOQA 61 | if not User.objects.filter(is_superuser=True).exists(): 62 | usr = create_user("admin", "admin@admin.com", "admin", is_staff=True, is_superuser=True) 63 | print("") 64 | print("A admin user (username: %s, password: admin) " "has been created." % usr.get_username()) 65 | print("") 66 | 67 | 68 | def run(settings, bind, port, migrate_cmd, verbose, use_channels, use_daphne): 69 | """ 70 | Run runserver command with reloader enabled. 71 | 72 | Currently support channels, daphne and plain django. 73 | 74 | :param settings: Django settings module 75 | :type settings: Settings 76 | :param bind: host to bind on 77 | :type bind: str 78 | :param port: port to bind on 79 | :type port: int 80 | :param migrate_cmd: run migrations when creating the database 81 | :type migrate_cmd: bool 82 | :param verbose: verbosity level 83 | :type verbose: int 84 | :param use_channels: run channels server 85 | :type use_channels: bool 86 | :param use_daphne: run daphne server 87 | :type use_daphne: bool 88 | """ 89 | try: 90 | from channels.management.commands import runserver # noqa: F401 91 | 92 | channels_enabled = True 93 | except ImportError: 94 | channels_enabled = False 95 | try: 96 | import daphne # noqa: F401 97 | 98 | daphne_enabled = True 99 | except ImportError: 100 | daphne_enabled = False 101 | if use_channels and channels_enabled: 102 | _run_channels(settings, bind, port, migrate_cmd, verbose) 103 | elif use_daphne and daphne_enabled: 104 | _run_daphne(settings, bind, port, migrate_cmd, verbose) 105 | else: 106 | _run_django(settings, bind, port, migrate_cmd, verbose) 107 | -------------------------------------------------------------------------------- /docs/introduction.rst: -------------------------------------------------------------------------------- 1 | ############################ 2 | How to use Django App Helper 3 | ############################ 4 | 5 | We'll assume that you have an application for django CMS that you're working on. 6 | 7 | Once you have django CMS installed, it'll be available using ``django-app-helper`` command. 8 | 9 | ``cd`` into the root directory of your application (that is, the outer directory containing its 10 | ``setup.py``). You need to be here to run the ``django-app-helper`` command. 11 | 12 | 13 | ********************************* 14 | Running Django App Helper command 15 | ********************************* 16 | 17 | Try it:: 18 | 19 | django-app-helper test --cms # change to your application's actual name 20 | 21 | It'll spawn its virtual project and run your tests in it. You should see some output along these 22 | lines (there may well be some other output before it gets to this stage):: 23 | 24 | Creating test database for alias 'default'... 25 | F 26 | ====================================================================== 27 | FAIL: test_bad_maths (djangocms_maths.tests.SmokeTest) 28 | ---------------------------------------------------------------------- 29 | Traceback (most recent call last): 30 | File "./djangocms_maths/tests.py", line 6, in test_bad_maths 31 | self.assertEqual(1 + 1, 3) 32 | AssertionError: 2 != 3 33 | 34 | ---------------------------------------------------------------------- 35 | Ran 1 test in 0.000s 36 | 37 | FAILED (failures=1) 38 | 39 | All commands take a form similar to the one you've just run, sharing the basic command structure:: 40 | 41 | django-app-helper [options ...] 42 | 43 | where **** is the Django application name and **** is one 44 | of the available commands. Options vary for each command. 45 | 46 | ************************************ 47 | But I haven't written any tests yet! 48 | ************************************ 49 | 50 | It helps if you actually have some tests of course - if you don't, simply create a ``tests.py`` 51 | file in your application (not in this directory, but in the package directory, alongside its 52 | models and views and so on):: 53 | 54 | from django.test import TestCase 55 | 56 | class SmokeTest(TestCase): 57 | 58 | # a deliberately-failing test 59 | def test_bad_maths(self): 60 | self.assertEqual(1 + 1, 3) 61 | 62 | ********************************* 63 | The --cms option 64 | ********************************* 65 | 66 | You'll need the ``--cms`` option most of the time. It sets up the virtual project appropriately 67 | for django CMS, providing the required configuration (see :ref:`cms-option` for details). 68 | 69 | ********************************* 70 | Other commands 71 | ********************************* 72 | 73 | Try a couple of the other commands; they're mostly self-explanatory:: 74 | 75 | django-app-helper shell --cms # start a Django shell for the virtual project 76 | 77 | django-app-helper check --cms # runs the Django check command 78 | 79 | django-app-helper cms_check # runs the django CMS check command 80 | 81 | Note that the last of these doesn't require the ``--cms`` option, because of course that is implied 82 | anyway by ``cms_check``. 83 | 84 | 85 | 86 | ********************************* 87 | Integrated runner 88 | ********************************* 89 | 90 | In some contexts running commands by using the complete syntax can be clunky or unfeasible. 91 | 92 | Django App Helper contains function that allows to run the commands with a much shorter syntax:: 93 | 94 | python helper.py 95 | 96 | to run tests 97 | 98 | Or:: 99 | 100 | python helper.py server 101 | 102 | to invoke a server. 103 | 104 | See :doc:`runner` for details. 105 | 106 | 107 | ********************************* 108 | Sphinx integration 109 | ********************************* 110 | 111 | When documenting a project using Sphinx autodoc, you mostly need a proper project setup, because 112 | the imports in your application's modules will trigger Django setup code anyway. 113 | 114 | Using the :ref:`naked_runner` it's easy to let helper setup an environment for you: 115 | 116 | * setup the :ref:`naked_runner` 117 | * add the following code to sphinx ``conf.py``:: 118 | 119 | sys.path.insert(0, os.path.abspath('..')) 120 | import app_helper 121 | app_helper.setup() 122 | -------------------------------------------------------------------------------- /app_helper/runner.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import os.path 3 | import sys 4 | 5 | 6 | def run(app, argv=sys.argv, extra_args=None): 7 | """ 8 | Run commands in a plain django environment 9 | 10 | :param app: application 11 | :param argv: arguments (default to sys.argv) 12 | :param extra_args: list of extra arguments 13 | """ 14 | if app not in argv[:2]: 15 | # app is automatically added if not present 16 | argv.insert(1, app) 17 | if len(argv) < 3 and "test" not in argv[:2]: 18 | # test argument is given if not argument is passed 19 | argv.insert(2, "test") 20 | if extra_args: 21 | argv.extend(extra_args) 22 | return runner(argv) 23 | 24 | 25 | def cms(app, argv=sys.argv, extra_args=None): 26 | """ 27 | Run commands in a django cMS environment 28 | 29 | :param app: application 30 | :param argv: arguments (default to sys.argv) 31 | :param extra_args: list of extra arguments 32 | """ 33 | try: 34 | import cms # NOQA # nopyflakes 35 | except ImportError: 36 | print("runner.cms is available only if django CMS is installed") 37 | raise 38 | if app not in argv[:2]: 39 | # app is automatically added if not present 40 | argv.insert(1, app) 41 | if len(argv) < 3 and "test" not in argv[:2]: 42 | # test argument is given if not argument is passed 43 | argv.insert(2, "test") 44 | if "--cms" not in argv: 45 | # this is the cms runner, just add the cms argument 46 | argv.append("--cms") 47 | if extra_args: 48 | argv.extend(extra_args) 49 | return runner(argv) 50 | 51 | 52 | def setup(app, helper_module, extra_args=None, use_cms=False): 53 | """ 54 | Setup the Django / django CMS environment and return the environment settings. 55 | 56 | :param app: application 57 | :param helper_module: helper module 58 | :param extra_args: list of extra arguments 59 | :param use_cms: setup a django CMS environemtn 60 | :return: Django settings module 61 | """ 62 | 63 | def _pytest_setup(settings, module): 64 | excluded_settings = { 65 | "PASSWORD_RESET_TIMEOUT": "PASSWORD_RESET_TIMEOUT_DAYS", 66 | "DEFAULT_FILE_STORAGE": "STORAGES", 67 | "STATICFILES_STORAGE": "STORAGES", 68 | } 69 | default_settings = {"SECRET_KEY": "secret"} 70 | for setting in dir(settings): 71 | if setting.isupper(): 72 | setting_value = getattr(settings, setting) 73 | 74 | # Empty settings value in the original value which must have a default are checked on 75 | # default_settings dictionary and default is set, if available 76 | default_value = default_settings.get(setting, None) 77 | if default_value and not setting_value: 78 | setting_value = default_value 79 | 80 | # If two settings exclude each other, we check if the alternate setting is already defined 81 | # in the settings module and if not, we set the current setting 82 | alternate_setting = excluded_settings.get(setting, None) 83 | alternate_setting_value = None 84 | if alternate_setting: 85 | alternate_setting_value = getattr(settings, alternate_setting, None) 86 | if not alternate_setting_value: 87 | setattr(module, setting, setting_value) 88 | 89 | helper = helper_module.__file__ 90 | argv = [os.path.basename(helper), app, "setup", "--extra-settings={}".format(helper)] 91 | if use_cms: 92 | argv.append("--cms") 93 | if extra_args: 94 | argv.extend(extra_args) 95 | settings = runner(argv) 96 | if "pytest_django" in sys.modules: 97 | _pytest_setup(settings, helper_module) 98 | return settings 99 | 100 | 101 | def runner(argv): 102 | from . import HELPER_FILE 103 | from .main import main 104 | 105 | # This is a hackish way to get the caller file which is the file 106 | # which contains the HELPER_SETTINGS 107 | helper = os.path.abspath(inspect.getframeinfo(inspect.stack()[2][0]).filename) 108 | # check if extra settings has been passed 109 | # if not, user the helper file 110 | extra_settings = any(x.startswith("--extra-settings=") for x in argv) 111 | if os.path.basename(helper) not in (HELPER_FILE,) and not extra_settings: 112 | argv.append("--extra-settings=%s" % helper) 113 | return main(argv) 114 | -------------------------------------------------------------------------------- /docs/settings.rst: -------------------------------------------------------------------------------- 1 | ####################################### 2 | Project settings with Django App Helper 3 | ####################################### 4 | 5 | .. _extra-settings: 6 | 7 | ****************** 8 | Extra settings 9 | ****************** 10 | 11 | Django App Helper provide a basic set of settings, you'll probably need to provide your own. 12 | 13 | Extra settings can be provided by creating a ``helper.py`` file in the application root 14 | directory and providing the settings as a dictionary named ``HELPER_SETTINGS``:: 15 | 16 | HELPER_SETTINGS={ 17 | 'INSTALLED_APPS': [ 18 | 'any_django_app', 19 | ], 20 | 'ANY_SETTING: False, 21 | ... 22 | } 23 | 24 | An alternative, and possibly clearer form is:: 25 | 26 | HELPER_SETTINGS=dict( 27 | INSTALLED_APPS=[ 28 | 'any_django_app', 29 | ], 30 | ANY_SETTING=False, 31 | ... 32 | ) 33 | 34 | By default any setting option provided in ``helper.py`` will override the default ones. 35 | 36 | .. warning:: On Django 4.2 and up you **cannot** use ``DEFAULT_FILE_STORAGE`` and ``STATICFILES_STORAGE`` 37 | in ``HELPER_SETTINGS``: use new ``STORAGES`` setting instead. 38 | 39 | 40 | Special settings 41 | ================ 42 | 43 | The following settings will not override the defaults ones, but they are appended to the defaults 44 | to make easier to customise the configuration: 45 | 46 | * ``INSTALLED_APPS`` 47 | * ``TEMPLATE_CONTEXT_PROCESSORS`` 48 | * ``TEMPLATE_LOADERS`` 49 | * ``TEMPLATE_DIRS`` 50 | * ``MIDDLEWARE_CLASSES`` 51 | 52 | Other extra setting: 53 | 54 | * ``TOP_INSTALLED_APPS``: items in this setting will be inserted on top of ``INSTALLED_APPS`` 55 | (e.g.: to control the templates and static files override from standard applications 56 | configured by django-app-helper). 57 | 58 | * ``TOP_MIDDLEWARE_CLASSES``: items in this setting will be inserted on top of 59 | ``MIDDLEWARE_CLASSES``. 60 | 61 | Django 1.8 support 62 | ================== 63 | 64 | All ``TEMPLATES_`` settings from Django 1.6/1.7 are automatically translated to Django 1.8 65 | ``TEMPLATE`` setting. To support both, just use the **old** names, and ``django-app-helper`` 66 | will take care of converting. 67 | 68 | ================ 69 | default settings 70 | ================ 71 | 72 | These are the applications, context processors and middlewares loaded by default 73 | 74 | Applications:: 75 | 76 | 'django.contrib.contenttypes', 77 | 'django.contrib.auth', 78 | 'django.contrib.sessions', 79 | 'django.contrib.sites', 80 | 'django.contrib.staticfiles', 81 | 'django.contrib.admin', 82 | 'app_helper.test_data', # this provides basic templates and urlconf 83 | 'django.contrib.messages', 84 | 85 | Template context processors:: 86 | 87 | 'django.contrib.auth.context_processors.auth', 88 | 'django.contrib.messages.context_processors.messages', 89 | 'django.core.context_processors.i18n', 90 | 'django.core.context_processors.csrf', 91 | 'django.core.context_processors.debug', 92 | 'django.core.context_processors.tz', 93 | 'django.core.context_processors.request', 94 | 'django.core.context_processors.media', 95 | 'django.core.context_processors.static', 96 | 97 | 98 | .. note:: On Django 1.8 these are translated to the new path ``django.template.context_processors.*`` 99 | 100 | 101 | Middlewares:: 102 | 103 | 'django.middleware.http.ConditionalGetMiddleware', 104 | 'django.contrib.sessions.middleware.SessionMiddleware', 105 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 106 | 'django.contrib.messages.middleware.MessageMiddleware', 107 | 'django.middleware.csrf.CsrfViewMiddleware', 108 | 'django.middleware.locale.LocaleMiddleware', 109 | 'django.middleware.common.CommonMiddleware', 110 | 111 | 112 | .. _cms-option: 113 | 114 | ****************** 115 | --cms option 116 | ****************** 117 | 118 | When using ``--cms`` option, ``INSTALLED_APPS``, ``TEMPLATE_CONTEXT_PROCESSORS`` and 119 | ``MIDDLEWARE_CLASSES`` related to django CMS are added to the default settings so you 120 | won't need to provide it yourself. 121 | 122 | Applications:: 123 | 124 | 'djangocms_admin_style', 125 | 'mptt', 126 | 'cms', 127 | 'menus', 128 | 'sekizai', 129 | 130 | When django CMS 3.1+ is used, ``treebeard`` is configured instead of ``mptt``. 131 | 132 | Template context processors:: 133 | 134 | 'cms.context_processors.cms_settings', 135 | 'sekizai.context_processors.sekizai', 136 | 137 | 138 | Middlewares:: 139 | 140 | 'cms.middleware.language.LanguageCookieMiddleware', 141 | 'cms.middleware.user.CurrentUserMiddleware', 142 | 'cms.middleware.page.CurrentPageMiddleware', 143 | 'cms.middleware.toolbar.ToolbarMiddleware', 144 | 145 | ``django-app-helper`` discovers automtically the South / Django migrations layout and configure 146 | the settings accordingly. As of the current version ``filer``, ``djangocms_text_ckeditor``, 147 | ``cmplugin_filer`` are supported. 148 | 149 | 150 | .. _compatibility mixin: https://docs.djangoproject.com/en/1.10/topics/http/middleware/#upgrading-middleware 151 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import re 4 | import sys 5 | from glob import glob 6 | 7 | from invoke import task 8 | 9 | DOCS_PORT = os.environ.get("DOCS_PORT", 8000) 10 | #: branch prefixes for which some checks are skipped 11 | SPECIAL_BRANCHES = ("master", "develop", "release") 12 | 13 | 14 | @task 15 | def clean(c): 16 | """Remove artifacts and binary files.""" 17 | c.run("python setup.py clean --all") 18 | patterns = ["build", "dist"] 19 | patterns.extend(glob("*.egg*")) 20 | patterns.append("docs/_build") 21 | patterns.append("**/*.pyc") 22 | for pattern in patterns: 23 | c.run("rm -rf {}".format(pattern)) 24 | 25 | 26 | @task 27 | def lint(c): 28 | """Run linting tox environments.""" 29 | c.run("tox -eruff,isort,black,pypi-description") 30 | 31 | 32 | @task # NOQA 33 | def format(c): # NOQA 34 | """Run code formatting tasks.""" 35 | c.run("tox -eblacken,isort_format") 36 | 37 | 38 | @task 39 | def towncrier_check(c): # NOQA 40 | """Check towncrier files.""" 41 | output = io.StringIO() 42 | c.run("git branch -a --contains HEAD", out_stream=output) 43 | skipped_branch_prefix = ["pull/", "release/", "develop", "master", "HEAD"] 44 | # cleanup branch names by removing PR-only names in local, remote and disconnected branches to ensure the current 45 | # (i.e. user defined) branch name is used 46 | branches = list( 47 | filter( 48 | lambda x: x and all(not x.startswith(part) for part in skipped_branch_prefix), 49 | ( 50 | branch.replace("origin/", "").replace("remotes/", "").strip("* (") 51 | for branch in output.getvalue().split("\n") 52 | ), 53 | ) 54 | ) 55 | print("Candidate branches", ", ".join(output.getvalue().split("\n"))) 56 | if not branches: 57 | # if no branch name matches, we are in one of the excluded branches above, so we just exit 58 | print("Skip check, branch excluded by configuration") 59 | return 60 | branch = branches[0] 61 | towncrier_file = None 62 | for branch in branches: 63 | if any(branch.startswith(prefix) for prefix in SPECIAL_BRANCHES): 64 | sys.exit(0) 65 | try: 66 | parts = re.search(r"(?P\w+)/\D*(?P\d+)\D*", branch).groups() 67 | towncrier_file = os.path.join("changes", "{1}.{0}".format(*parts)) 68 | if not os.path.exists(towncrier_file) or os.path.getsize(towncrier_file) == 0: 69 | print( 70 | "=========================\n" 71 | "Current tree does not contain the towncrier file {} or file is empty\n" 72 | "please check CONTRIBUTING documentation.\n" 73 | "=========================" 74 | "".format(towncrier_file) 75 | ) 76 | sys.exit(2) 77 | else: 78 | break 79 | except AttributeError: 80 | pass 81 | if not towncrier_file: 82 | print( 83 | "=========================\n" 84 | "Branch {} does not respect the '/(-)-description' format\n" 85 | "=========================\n" 86 | "".format(branch) 87 | ) 88 | sys.exit(1) 89 | 90 | 91 | @task 92 | def test(c): 93 | """Run test in local environment.""" 94 | c.run("python setup.py test") 95 | 96 | 97 | @task 98 | def test_all(c): 99 | """Run all tox environments.""" 100 | c.run("tox") 101 | 102 | 103 | @task 104 | def coverage(c): 105 | """Run test with coverage in local environment.""" 106 | c.run("coverage erase") 107 | c.run("run setup.py test") 108 | c.run("report -m") 109 | 110 | 111 | @task 112 | def tag_release(c, level, new_version=""): 113 | """Tag release version.""" 114 | if new_version: 115 | new_version = f" --new-version {new_version}" 116 | c.run(f"bump-my-version bump {level}{new_version}") 117 | 118 | 119 | @task 120 | def tag_dev(c, level, new_version=""): 121 | """Tag development version.""" 122 | if new_version: 123 | new_version = f" --new-version {new_version}" 124 | elif level == "release": 125 | c.run("bump-my-version bump patch --no-commit") 126 | level = "relver" 127 | c.run(f"bump-my-version bump {level} --message='Bump develop version [ci skip]' {new_version} --allow-dirty") 128 | 129 | 130 | @task(pre=[clean]) 131 | def docbuild(c): 132 | """Build documentation.""" 133 | os.chdir("docs") 134 | build_dir = os.environ.get("BUILD_DIR", "_build/html") 135 | c.run("python -msphinx -W -b html -d _build/doctrees . %s" % build_dir) 136 | 137 | 138 | @task(docbuild) 139 | def docserve(c): 140 | """Serve docs at http://localhost:$DOCS_PORT/ (default port is 8000).""" 141 | from livereload import Server 142 | 143 | server = Server() 144 | server.watch("docs/conf.py", lambda: docbuild(c)) 145 | server.watch("CONTRIBUTING.rst", lambda: docbuild(c)) 146 | server.watch("docs/*.rst", lambda: docbuild(c)) 147 | server.serve(port=DOCS_PORT, root="_build/html") 148 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Django App helper 3 | ================= 4 | 5 | |Gitter| |PyPiVersion| |PyVersion| |GAStatus| |TestCoverage| |CodeClimate| |License| 6 | 7 | .. warning:: 8 | Starting from 3.3 django-app-helper only supports Django 3.2+ and django CMS 3.11. If you need support for older (unsupported) versions, use django-app-helper<3.3 9 | 10 | Starting from 3 django-app-helper only supports Django 2.2+ and django CMS 3.7+. If you need support for older (unsupported) versions, use django-app-helper 2. 11 | 12 | ****************************************** 13 | Helper for django applications development 14 | ****************************************** 15 | 16 | django-app-helper is a set of commands and helper methods 17 | to make developing and testing reusable Django applications easier. 18 | 19 | Being born in the django CMS ecosystem, it provides a lot of utility 20 | functions to develop, run and test django CMS applications. 21 | 22 | It's a modified version of django CMS's own ``develop.py`` script, modified 23 | to handle generic application development process. 24 | 25 | It supports both tests writted using Django ``TestCase`` and pytest ones 26 | (see `pytest support`_). 27 | 28 | Supported versions 29 | ================== 30 | 31 | Python: 3.8, 3.9, 3.10, 3.11, 3.12 32 | 33 | Django: 3.2, 4.0, 4.1, 4.2, 5.0 34 | 35 | django CMS: 3.11 36 | 37 | Newer versions might work but are not tested yet. 38 | 39 | Common options 40 | ============== 41 | 42 | * ``--cms``: Loads configuration to properly run a django CMS-based application; 43 | * ``--extra-settings``: Path to a helper file to set extra settings; see 44 | `Project settings with Django App Helper`_ for details; 45 | 46 | ***** 47 | Usage 48 | ***** 49 | 50 | The command **must** be executed in the main plugin directory (i.e. in the same 51 | directory as the ``setup.py`` file) as it needs to import files relative to the 52 | current directory. 53 | 54 | The basic command structure is:: 55 | 56 | django-app-helper [options ...] 57 | 58 | where **** is the django application name and **** is one 59 | of the available commands. Options vary for each command. 60 | 61 | Base test class 62 | =============== 63 | 64 | A base test class is available to provide helpers and methods that 65 | implements repetitive tasks during development or compatibility shims 66 | (especially for django CMS). 67 | 68 | ************* 69 | Bootstrapping 70 | ************* 71 | 72 | To bootstrap a project using ``django-app-helper`` you may want to have a look at `cookiecutter-djangopackage-helper`_, a `cookiecutter`_ template for ``django-app-helper``. 73 | 74 | To use it follows `usage`_ 75 | 76 | ****** 77 | Runner 78 | ****** 79 | 80 | By using the integrated runned in the settings file you'll be able to run 81 | the commands without invoking ``django-app-helper``: see `Integrated runner`_ 82 | for reference. 83 | 84 | *************** 85 | ASGI / Channels 86 | *************** 87 | 88 | ASGI / Channels are supported by installing the project with ``django-app-helper[async]``. 89 | 90 | With Daphne / Channels installed you can run ``django-app-helper server --use-daphne|--use-channels`` to run the 91 | project on ASGI. 92 | 93 | See `ASGI / Channels support`_ 94 | 95 | Pure ASGI support is available only for Django 3.0+. 96 | 97 | ************ 98 | Installation 99 | ************ 100 | 101 | Installing from pip:: 102 | 103 | pip install django-app-helper 104 | 105 | Installing from source:: 106 | 107 | pip install git+https://github.com/nephila/django-app-helper#egg=django-app-helper 108 | 109 | Requirements 110 | ============ 111 | 112 | * django CMS optional; required only to work with ``--cms`` option 113 | * docopt 114 | * tox 115 | * dj-database-url 116 | 117 | ************* 118 | Documentation 119 | ************* 120 | 121 | Documentation is available on `readthedocs`_. 122 | 123 | 124 | ******* 125 | Authors 126 | ******* 127 | 128 | ``django-app-helper`` was written by `Iacopo Spalletti `_ with help from 129 | other contributors. 130 | 131 | Thanks 132 | ====== 133 | 134 | The general logic and part of the code of the whole application is heavily taken from 135 | `django CMS's`_ own ``develop.py`` so all the contributors 136 | deserve a huge thanks for their work. 137 | 138 | 139 | 140 | .. |Gitter| image:: https://img.shields.io/badge/GITTER-join%20chat-brightgreen.svg?style=flat-square 141 | :target: https://gitter.im/nephila/applications 142 | :alt: Join the Gitter chat 143 | 144 | .. |PyPiVersion| image:: https://img.shields.io/pypi/v/django-app-helper.svg?style=flat-square 145 | :target: https://pypi.python.org/pypi/django-app-helper 146 | :alt: Latest PyPI version 147 | 148 | .. |PyVersion| image:: https://img.shields.io/pypi/pyversions/django-app-helper.svg?style=flat-square 149 | :target: https://pypi.python.org/pypi/django-app-helper 150 | :alt: Python versions 151 | 152 | .. |GAStatus| image:: https://github.com/nephila/django-app-helper/workflows/Tox%20tests/badge.svg 153 | :target: https://github.com/nephila/django-app-helper 154 | :alt: Latest CI build status 155 | 156 | .. |TestCoverage| image:: https://img.shields.io/coveralls/nephila/django-app-helper/master.svg?style=flat-square 157 | :target: https://coveralls.io/r/nephila/django-app-helper?branch=master 158 | :alt: Test coverage 159 | 160 | .. |License| image:: https://img.shields.io/github/license/nephila/django-app-helper.svg?style=flat-square 161 | :target: https://pypi.python.org/pypi/django-app-helper/ 162 | :alt: License 163 | 164 | .. |CodeClimate| image:: https://codeclimate.com/github/nephila/django-app-helper/badges/gpa.svg?style=flat-square 165 | :target: https://codeclimate.com/github/nephila/django-app-helper 166 | :alt: Code Climate 167 | 168 | .. _Migrating from djangocms-helper to django-app-helper: https://django-app-helper.readthedocs.io/en/latest/migrating.html 169 | .. _Project settings with Django App Helper: https://django-app-helper.readthedocs.io/en/latest/settings.html 170 | .. _Integrated runner: https://django-app-helper.readthedocs.io/en/latest/runner.html 171 | .. _cookiecutter: https://github.com/audreyr/cookiecutter 172 | .. _cookiecutter-djangopackage-helper: https://github.com/nephila/cookiecutter-djangopackage-helper 173 | .. _readthedocs: https://django-app-helper.readthedocs.io 174 | .. _django CMS's: https://github.com/divio/django-cms: 175 | .. _usage: https://github.com/nephila/cookiecutter-djangopackage-helper#usage 176 | .. _pytest support: https://django-app-helper.readthedocs.io/en/latest/pytest.html 177 | .. _ASGI / Channels support: https://django-app-helper.readthedocs.io/en/latest/asgi.html 178 | -------------------------------------------------------------------------------- /docs/reference.rst: -------------------------------------------------------------------------------- 1 | ########################### 2 | Django App Helper reference 3 | ########################### 4 | 5 | ********************************* 6 | Commands 7 | ********************************* 8 | 9 | Commands take the general form:: 10 | 11 | django-app-helper [options ...] 12 | 13 | where **** is the Django application name and **** is a Django supported 14 | command, *or* one of the django-app-helper commands detailed below. Options vary for each command. 15 | 16 | .. note:: while all examples here use the ``django-app-helper`` CLI, a more idiomatic way to run commands is by using 17 | :ref:`runner`. 18 | 19 | 20 | Common options 21 | ============== 22 | 23 | * ``--extra-settings=path``: loads the extra settings from the provided file instead of the 24 | default ``helper.py`` 25 | * ``cms``: loads django CMS specific options (see :ref:`cms-option` for details) 26 | 27 | 28 | Django commands 29 | =============== 30 | 31 | Django App Helper supports any Django command available according to the project setup; the 32 | general syntax is:: 33 | 34 | django-app-helper [options] [--extra-settings=] [--cms] 35 | 36 | Example: ``django-app-helper some_application shell --cms`` 37 | 38 | Arguments 39 | --------- 40 | 41 | * ```` is any available Django command 42 | * ``[options]`` is any option/argument accepted by the above command 43 | 44 | 45 | 46 | test 47 | ==== 48 | 49 | :: 50 | 51 | django-app-helper test [--failfast] [--migrate] [...] [--xvfb] [--runner=] [--extra-settings=] [--cms] [--simple-runner] [--runner-options=,] 52 | 53 | Example: ``django-app-helper some_application test --cms`` 54 | 55 | Runs the application's test suite in Django App Helper's virtual environment. 56 | 57 | Arguments 58 | --------- 59 | 60 | * ````: a space-separated list of tests to run; test labels depends on the runner 61 | test suite building protocol, please, refer to the runner documentation to know the 62 | test label format; 63 | 64 | Options 65 | ------- 66 | 67 | * ``--runner``: custom test runner to use in dotted path notation; 68 | * ``--runner-options=,``: comma separated list of command 69 | line options for the test runner: e.g. ``--runner-options="--with-coverage,--cover-package=my_package"`` 70 | * ``--failfast``: whether to stop at first test failure; 71 | * ``--migrate``: use migrations (default); 72 | * ``--persistent``: use persistent storage for media and static; by default storage is created 73 | in ``data`` directory in the root of the application; if a different 74 | directory is needed, use ``--persistent-path`` to provide the path; 75 | * ``--persistent-path``: persistent storage path, instead of ``data`` 76 | * ``--no-migrate``: skip migrations; 77 | * ``--xvfb``: whether to configure ``xvfb`` (for frontend tests); 78 | * ``--native`` use the native Django command: the use of this option is **incompatible** with 79 | the options above. 80 | 81 | Test structure 82 | -------------- 83 | 84 | Currently two different tests layouts are supported: 85 | 86 | * tests outside the application module:: 87 | 88 | setup.py 89 | tests 90 | __init__.py 91 | test_module1.py 92 | .... 93 | 94 | * tests inside the application:: 95 | 96 | setup.py 97 | application 98 | tests 99 | __init__.py 100 | test_module1.py 101 | ... 102 | 103 | Depending on the used test runner you may need to setup your tests accordingly. 104 | 105 | The default runner is the Django one, but it's possible to specify your own custom runner with the ``--runner`` option. 106 | 107 | 108 | cms_check 109 | ========= 110 | 111 | :: 112 | 113 | django-app-helper cms_check [--extra-settings=] [--migrate] 114 | 115 | Runs the django CMS ``cms check`` command. 116 | 117 | Example: ``django-app-helper some_application cms_check`` 118 | 119 | update and compile locales 120 | ========================== 121 | 122 | :: 123 | 124 | django-app-helper makemessages [--extra-settings=] [--cms] [--locale=locale] 125 | django-app-helper compilemessages [--extra-settings=] [--cms] 126 | 127 | Examples:: 128 | 129 | django-app-helper some_application makemessages --cms 130 | django-app-helper some_application compilemessages --cms 131 | 132 | These two commands compiles and update the locale messages. 133 | 134 | Options 135 | ------- 136 | 137 | * ``--locale=locale``: ``makemessages`` allows a single option to choose the locale to update. 138 | If not provided **en** is used. 139 | 140 | makemigrations 141 | ============== 142 | 143 | :: 144 | 145 | django-app-helper makemigrations [--extra-settings=] [--cms] [--merge] [--dry-run] [--empty] [...] 146 | 147 | Updates the application migrations (south migrations or Django migrations 148 | according to the current installed Django version). For South, it automatically 149 | handles ``initial`` and ``auto`` options. 150 | 151 | Options 152 | ------- 153 | 154 | * ``--merge``: Enable fixing of migration conflicts 155 | * ``--empty``: It generates an empty migration for customisations 156 | * ``--dry-run``: Does not create migrations file 157 | 158 | Arguments 159 | --------- 160 | 161 | * ````: Spaces separated list of applications to migrate 162 | 163 | squashmigrations 164 | ================ 165 | 166 | :: 167 | 168 | django-app-helper squashmigrations 169 | 170 | 171 | Runs the ``squashmigrations`` command. It operates on the current application. 172 | 173 | Arguments 174 | --------- 175 | 176 | * ````: Squash migrations until this migration 177 | 178 | authors 179 | ======= 180 | 181 | :: 182 | 183 | django-app-helper authors [--extra-settings=] [--cms] 184 | 185 | Generates an authors list from the git log, in a form suitable for the **AUTHORS** file. 186 | 187 | server 188 | ====== 189 | 190 | :: 191 | 192 | django-app-helper server [--port=] [--bind=] [--extra-settings=] [--cms] [--migrate] [--no-migrate] [--persistent | --persistent-path=] [--verbose=] [--use-daphne] [--use-channels] 193 | 194 | Starts a runserver instance. 195 | 196 | * ``--port=``: port to bind the server on; 197 | * ``--bind=``: address to bind the server on; 198 | * ``--extra-settings=``: path to extra settings file; 199 | * ``--cms``: enable django CMS settings; 200 | * ``--migrate``: run migrations on server start (default); 201 | * ``--no-migrate``: do not run migrations on server start; 202 | * ``--persistent | --persistent-path=``: persist generated media directory; optionally you can provide a fixed path; 203 | * ``--verbose=``: verbosity level; 204 | * ``--use-daphne``: use daphne server; 205 | * ``--use-channels]``: use channels server; 206 | -------------------------------------------------------------------------------- /tests/test_base_classes.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import unittest 4 | 5 | from django.conf import settings 6 | from django.contrib.auth import get_user_model 7 | from django.contrib.auth.models import AnonymousUser 8 | from django.http import SimpleCookie 9 | from django.test import Client, TestCase 10 | 11 | from app_helper.base_test import GenericHelpersMixin, RequestTestCaseMixin 12 | from app_helper.utils import create_user 13 | 14 | 15 | class TestRequestTestCaseMixin(TestCase): 16 | class RequestTestCase(RequestTestCaseMixin, TestCase): 17 | client = Client() 18 | 19 | def test_request(self): 20 | """Base request object is created with cookies, session and language.""" 21 | test_instance = self.RequestTestCase() 22 | 23 | request = test_instance.request("/") 24 | 25 | self.assertTrue(request.session) 26 | self.assertTrue(isinstance(request.user, AnonymousUser)) 27 | self.assertTrue(isinstance(request.cookies, SimpleCookie)) 28 | self.assertTrue(request.errors) 29 | self.assertEqual(request.LANGUAGE_CODE, "") 30 | with self.assertRaises(AttributeError): 31 | self.assertTrue(request.toolbar) 32 | 33 | def test_post_request(self): 34 | """POST request object is created with cookies, session and language, csrf checks disabled.""" 35 | test_instance = self.RequestTestCase() 36 | 37 | request = test_instance.request("/", method="post", data={"some": "data"}) 38 | 39 | self.assertEqual(request.session._session_key, "session_key") 40 | self.assertTrue(isinstance(request.user, AnonymousUser)) 41 | self.assertTrue(isinstance(request.cookies, SimpleCookie)) 42 | self.assertTrue(request.errors) 43 | self.assertTrue(request._dont_enforce_csrf_checks) 44 | self.assertEqual(request.LANGUAGE_CODE, "") 45 | with self.assertRaises(AttributeError): 46 | self.assertTrue(request.toolbar) 47 | 48 | def test_auth_request(self): 49 | """Request object with authenticated user, user is assigned to request.""" 50 | 51 | User = get_user_model() # noqa: N806 52 | user = User(username="some") 53 | test_instance = self.RequestTestCase() 54 | 55 | request = test_instance.request("/", method="post", data={"some": "data"}, user=user) 56 | self.assertTrue(request.session) 57 | self.assertEqual(request.user, user) 58 | self.assertTrue(isinstance(request.cookies, SimpleCookie)) 59 | self.assertTrue(request.errors) 60 | self.assertTrue(request._dont_enforce_csrf_checks) 61 | self.assertEqual(request.LANGUAGE_CODE, "") 62 | with self.assertRaises(AttributeError): 63 | self.assertTrue(request.toolbar) 64 | 65 | def test_lang_request(self): 66 | """Request with forced language, language is assigned to LANGUAGE_CODE.""" 67 | 68 | test_instance = self.RequestTestCase() 69 | 70 | request = test_instance.request("/", lang="en") 71 | self.assertEqual(request.LANGUAGE_CODE, "en") 72 | 73 | def test_login_user_context(self): 74 | """login_user_context force requests to be authenticated.""" 75 | user = create_user("some", "some@testcom", "some") 76 | test_instance = self.RequestTestCase() 77 | with self.assertRaises(AssertionError): 78 | with test_instance.login_user_context(user, password="wrong"): 79 | test_instance.request("/", lang="en") 80 | with test_instance.login_user_context(user): 81 | request = test_instance.request("/", lang="en") 82 | self.assertEqual(request.user, user) 83 | request = test_instance.request("/", lang="en") 84 | self.assertTrue(isinstance(request.user, AnonymousUser)) 85 | 86 | def test_apply_middleware(self): 87 | """Request with applied middlewares,""" 88 | test_instance = self.RequestTestCase() 89 | response = test_instance.request("/", lang="en", use_middlewares=True) 90 | 91 | self.assertFalse(response.current_page) 92 | # messages are not faked by the basic :py:attr:`request` call, we can use it to detect if middlewares are run 93 | self.assertIsNotNone(response._messages) 94 | try: 95 | import cms # noqa: F401 96 | 97 | # toolbar is also set if django CMS is installed 98 | self.assertTrue(response.toolbar) 99 | except ImportError: 100 | pass 101 | 102 | def test_use_toolbar(self): 103 | """Request with django CMS toolbar.""" 104 | try: 105 | import cms # noqa: F401 106 | except ImportError: 107 | raise unittest.SkipTest("django CMS not available, skipping test") 108 | test_instance = self.RequestTestCase() 109 | response = test_instance.request("/", lang="en", use_toolbar=True) 110 | 111 | self.assertFalse(response.current_page) 112 | # messages are not faked by the basic :py:attr:`request` call, we can use it to detect if middlewares are run 113 | with self.assertRaises(AttributeError): 114 | self.assertIsNone(response._messages) 115 | # toolbar is also set 116 | self.assertTrue(response.toolbar) 117 | 118 | def test_set_page(self): 119 | """Request with django CMS page.""" 120 | try: 121 | from cms.api import create_page 122 | except ImportError: 123 | raise unittest.SkipTest("django CMS not available, skipping test") 124 | test_instance = self.RequestTestCase() 125 | page = create_page(title="a page", language="en", template=settings.CMS_TEMPLATES[0][0]) 126 | response = test_instance.request("/", lang="en", page=page) 127 | 128 | self.assertEqual(response.current_page, page) 129 | 130 | 131 | class TestGenericHelpersMixin(TestCase): 132 | class GenericHelper(GenericHelpersMixin, TestCase): 133 | pass 134 | 135 | def test_reload_model(self): 136 | """:py:meth:`GenericHelper.reload_model` create a new object, do not patch the existing one.""" 137 | user = create_user("some", "some@testcom", "some") 138 | test_instance = self.GenericHelper() 139 | user.random_attr = "random" 140 | user_new = test_instance.reload_model(user) 141 | with self.assertRaises(AttributeError): 142 | self.assertIsNotNone(user_new.random_attr) 143 | self.assertNotEqual(id(user), id(user_new)) 144 | 145 | def test_temp_dir(self): 146 | """:py:meth:`GenericHelper.temp_dir` provide a temporary directory which is removed on context manager exit.""" 147 | test_instance = self.GenericHelper() 148 | with test_instance.temp_dir() as temp_path: 149 | test_file = os.path.join(temp_path, "afile") 150 | open(test_file, "w") 151 | self.assertTrue(os.path.exists(test_file)) 152 | self.assertFalse(os.path.exists(test_file)) 153 | 154 | def test_captured_output(self): 155 | """:py:meth:`GenericHelper.captured_output` context manager capture output.""" 156 | test_instance = self.GenericHelper() 157 | with test_instance.captured_output() as (out, err): 158 | print("out string") 159 | print("err string", file=sys.stderr) 160 | self.assertEqual(out.getvalue(), "out string\n") 161 | self.assertEqual(err.getvalue(), "err string\n") 162 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoAppHelper.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoAppHelper.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoAppHelper" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoAppHelper" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\DjangoAppHelper.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\DjangoAppHelper.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # Django App Helper documentation build configuration file, created by 3 | # sphinx-quickstart on Thu Dec 18 19:02:40 2014. 4 | # 5 | # This file is execfile()d with the current directory set to its 6 | # containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | # fmt: off 15 | import os 16 | import sys 17 | from distutils.version import LooseVersion 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | sys.path.insert(0, os.path.abspath("..")) 23 | import helper # isort:skip # noqa 24 | import app_helper # isort:skip # noqa 25 | # fmt: on 26 | 27 | helper.setup() 28 | 29 | # -- General configuration ------------------------------------------------ 30 | 31 | # If your documentation needs a minimal Sphinx version, state it here. 32 | # needs_sphinx = '1.0' 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be 35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 36 | # ones. 37 | extensions = [ 38 | "sphinx.ext.autodoc", 39 | "sphinx.ext.intersphinx", 40 | "sphinx.ext.todo", 41 | "sphinx.ext.coverage", 42 | "sphinx.ext.ifconfig", 43 | "sphinx.ext.viewcode", 44 | ] 45 | intersphinx_mapping = { 46 | "djangocms": ("https://django-cms.readthedocs.io/en/latest/", None), 47 | "django": ("https://django.readthedocs.io/en/latest/", None), 48 | } 49 | # Add any paths that contain templates here, relative to this directory. 50 | templates_path = ["_templates"] 51 | 52 | # The suffix of source filenames. 53 | source_suffix = ".rst" 54 | 55 | # The encoding of source files. 56 | # source_encoding = 'utf-8-sig' 57 | 58 | # The master toctree document. 59 | master_doc = "index" 60 | 61 | # General information about the project. 62 | project = "Django App Helper" 63 | copyright = "2014, Iacopo Spalletti" # noqa: A001 64 | 65 | # The version info for the project you're documenting, acts as replacement for 66 | # |version| and |release|, also used in various other places throughout the 67 | # built documents. 68 | # 69 | # The short X.Y version. 70 | version = LooseVersion(app_helper.__version__).vstring.rpartition(".")[0] 71 | # The full version, including alpha/beta/rc tags. 72 | release = app_helper.__version__ 73 | 74 | # The language for content autogenerated by Sphinx. Refer to documentation 75 | # for a list of supported languages. 76 | # language = None 77 | 78 | # There are two options for replacing |today|: either, you set today to some 79 | # non-false value, then it is used: 80 | # today = '' 81 | # Else, today_fmt is used as the format for a strftime call. 82 | # today_fmt = '%B %d, %Y' 83 | 84 | # List of patterns, relative to source directory, that match files and 85 | # directories to ignore when looking for source files. 86 | exclude_patterns = ["_build"] 87 | 88 | # The reST default role (used for this markup: `text`) to use for all 89 | # documents. 90 | # default_role = None 91 | 92 | # If true, '()' will be appended to :func: etc. cross-reference text. 93 | # add_function_parentheses = True 94 | 95 | # If true, the current module name will be prepended to all description 96 | # unit titles (such as .. function::). 97 | # add_module_names = True 98 | 99 | # If true, sectionauthor and moduleauthor directives will be shown in the 100 | # output. They are ignored by default. 101 | # show_authors = False 102 | 103 | # The name of the Pygments (syntax highlighting) style to use. 104 | pygments_style = "sphinx" 105 | 106 | # A list of ignored prefixes for module index sorting. 107 | # modindex_common_prefix = [] 108 | 109 | # If true, keep warnings as "system message" paragraphs in the built documents. 110 | # keep_warnings = False 111 | 112 | 113 | # -- Options for HTML output ---------------------------------------------- 114 | 115 | 116 | html_theme = "sphinx_rtd_theme" 117 | 118 | # Theme options are theme-specific and customize the look and feel of a theme 119 | # further. For a list of options available for each theme, see the 120 | # documentation. 121 | # html_theme_options = {} 122 | 123 | # Add any paths that contain custom themes here, relative to this directory. 124 | # html_theme_path = [] 125 | 126 | # The name for this set of Sphinx documents. If None, it defaults to 127 | # " v documentation". 128 | # html_title = None 129 | 130 | # A shorter title for the navigation bar. Default is the same as html_title. 131 | # html_short_title = None 132 | 133 | # The name of an image file (relative to this directory) to place at the top 134 | # of the sidebar. 135 | # html_logo = None 136 | 137 | # The name of an image file (within the static path) to use as favicon of the 138 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 139 | # pixels large. 140 | # html_favicon = None 141 | 142 | # Add any paths that contain custom static files (such as style sheets) here, 143 | # relative to this directory. They are copied after the builtin static files, 144 | # so a file named "default.css" will overwrite the builtin "default.css". 145 | # html_static_path = ['_static'] 146 | 147 | # Add any extra paths that contain custom files (such as robots.txt or 148 | # .htaccess) here, relative to this directory. These files are copied 149 | # directly to the root of the documentation. 150 | # html_extra_path = [] 151 | 152 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 153 | # using the given strftime format. 154 | # html_last_updated_fmt = '%b %d, %Y' 155 | 156 | # If true, SmartyPants will be used to convert quotes and dashes to 157 | # typographically correct entities. 158 | # html_use_smartypants = True 159 | 160 | # Custom sidebar templates, maps document names to template names. 161 | # html_sidebars = {} 162 | 163 | # Additional templates that should be rendered to pages, maps page names to 164 | # template names. 165 | # html_additional_pages = {} 166 | 167 | # If false, no module index is generated. 168 | # html_domain_indices = True 169 | 170 | # If false, no index is generated. 171 | # html_use_index = True 172 | 173 | # If true, the index is split into individual pages for each letter. 174 | # html_split_index = False 175 | 176 | # If true, links to the reST sources are added to the pages. 177 | # html_show_sourcelink = True 178 | 179 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 180 | # html_show_sphinx = True 181 | 182 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 183 | # html_show_copyright = True 184 | 185 | # If true, an OpenSearch description file will be output, and all pages will 186 | # contain a tag referring to it. The value of this option must be the 187 | # base URL from which the finished HTML is served. 188 | # html_use_opensearch = '' 189 | 190 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 191 | # html_file_suffix = None 192 | 193 | # Output file base name for HTML help builder. 194 | htmlhelp_basename = "DjangoAppHelperrdoc" 195 | 196 | 197 | # -- Options for LaTeX output --------------------------------------------- 198 | 199 | latex_elements = { 200 | # The paper size ('letterpaper' or 'a4paper'). 201 | # 'papersize': 'letterpaper', 202 | # The font size ('10pt', '11pt' or '12pt'). 203 | # 'pointsize': '10pt', 204 | # Additional stuff for the LaTeX preamble. 205 | # 'preamble': '', 206 | } 207 | 208 | # Grouping the document tree into LaTeX files. List of tuples 209 | # (source start file, target name, title, 210 | # author, documentclass [howto, manual, or own class]). 211 | latex_documents = [ 212 | ("index", "DjangoAppHelperr.tex", "Django App Helper Documentation", "Iacopo Spalletti", "manual"), 213 | ] 214 | 215 | # The name of an image file (relative to this directory) to place at the top of 216 | # the title page. 217 | # latex_logo = None 218 | 219 | # For "manual" documents, if this is true, then toplevel headings are parts, 220 | # not chapters. 221 | # latex_use_parts = False 222 | 223 | # If true, show page references after internal links. 224 | # latex_show_pagerefs = False 225 | 226 | # If true, show URL addresses after external links. 227 | # latex_show_urls = False 228 | 229 | # Documents to append as an appendix to all manuals. 230 | # latex_appendices = [] 231 | 232 | # If false, no module index is generated. 233 | # latex_domain_indices = True 234 | 235 | 236 | # -- Options for manual page output --------------------------------------- 237 | 238 | # One entry per manual page. List of tuples 239 | # (source start file, name, description, authors, manual section). 240 | man_pages = [("index", "DjangoAppHelperr", "Django App Helper Documentation", ["Iacopo Spalletti"], 1)] 241 | 242 | # If true, show URL addresses after external links. 243 | # man_show_urls = False 244 | 245 | 246 | # -- Options for Texinfo output ------------------------------------------- 247 | 248 | # Grouping the document tree into Texinfo files. List of tuples 249 | # (source start file, target name, title, author, 250 | # dir menu entry, description, category) 251 | texinfo_documents = [ 252 | ( 253 | "index", 254 | "DjangoAppHelperr", 255 | "Django App Helper Documentation", 256 | "Iacopo Spalletti", 257 | "DjangoAppHelperr", 258 | "One line description of project.", 259 | "Miscellaneous", 260 | ), 261 | ] 262 | 263 | # Documents to append as an appendix to all manuals. 264 | # texinfo_appendices = [] 265 | 266 | # If false, no module index is generated. 267 | # texinfo_domain_indices = True 268 | 269 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 270 | # texinfo_show_urls = 'footnote' 271 | 272 | # If true, do not generate a @detailmenu in the "Top" node's menu. 273 | # texinfo_no_detailmenu = False 274 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | ******* 4 | History 5 | ******* 6 | 7 | .. towncrier release notes start 8 | 9 | 3.3.5 (2025-06-05) 10 | ================== 11 | 12 | Bugfixes 13 | -------- 14 | 15 | - Fix CI build on python 3.8 (#288) 16 | 17 | 18 | 3.3.4 (2024-01-28) 19 | ================== 20 | 21 | Features 22 | -------- 23 | 24 | - Add DjangoCMS 3.11 to Django 5.0 test matrix (#252) 25 | 26 | 27 | Bugfixes 28 | -------- 29 | 30 | - Fix support for DEFAULT_FILE_STORAGE/STATICFILES_STORAGE in django 4.2 (#255) 31 | 32 | 33 | 3.3.3 (2023-11-28) 34 | ================== 35 | 36 | Features 37 | -------- 38 | 39 | - Prepare for Django 5.0 / Python 3.12 compatibility (#244) 40 | - Switch to Coveralls Github action (#248) 41 | 42 | 43 | 3.3.2 (2023-09-26) 44 | ================== 45 | 46 | Features 47 | -------- 48 | 49 | - Migrate to bump-my-version (#237) 50 | 51 | 52 | Bugfixes 53 | -------- 54 | 55 | - Fix ruff linting (#232) 56 | 57 | 58 | 3.3.1 (2023-07-09) 59 | ================== 60 | 61 | Bugfixes 62 | -------- 63 | 64 | - Fix runner-options argument on Django test runner (#220) 65 | - Do not add mptt with django-filer 3+ (#225) 66 | 67 | 68 | 3.3.0 (2023-05-07) 69 | ================== 70 | 71 | Features 72 | -------- 73 | 74 | - Add support for Django 4.x (#208) 75 | 76 | 77 | 3.2.0 (2023-01-19) 78 | ================== 79 | 80 | Features 81 | -------- 82 | 83 | - Add djangocms 3.11 to tox, fix tests accordingly (#311) 84 | 85 | 86 | 3.1.0 (2022-07-29) 87 | ================== 88 | 89 | Features 90 | -------- 91 | 92 | - Upgrade Python / Django versions (#204) 93 | - Add minimal django 4.0 support (#208) 94 | 95 | 96 | 3.0.1 (2020-12-09) 97 | ================== 98 | 99 | Bugfixes 100 | -------- 101 | 102 | - Fix loading setting with pytest-django and django 3.1 (#202) 103 | 104 | 105 | 3.0.0 (2020-11-14) 106 | ================== 107 | 108 | Features 109 | -------- 110 | 111 | - Add support for Django 3.1 / django CMS 3.8 (#196) 112 | - Add Django 3.0 / django CMS 3.7.2 support (#142) 113 | - Drop Python 2 / Django 1.11 (#148) 114 | - Add support for Daphne / channels runserver (#198) 115 | - Refactor BaseTestCaseMixin to more composable mixins (#107) 116 | - Replace makefile with invoke (#143) 117 | - Use pre-commit for code formatting (#149) 118 | - Allow to pass arguments to pytest via runner-options argument (#159) 119 | - Add support to pytest command (#167) 120 | - Update dotfiles to latest version (#189) 121 | - Reorganize tests outside main package (#191) 122 | - Remove support for aldryn-boilerplates (#199) 123 | 124 | 125 | Bugfixes 126 | -------- 127 | 128 | - Fix runner_options support (#92) 129 | - Improve GA - Update contribution guide (#161) 130 | - Allow extra arguments in PytestTestRunner.run_tests (#165) 131 | - Update isort and linting configuration (#188) 132 | 133 | 134 | Misc 135 | ---- 136 | 137 | - #152, #185 138 | 139 | 140 | 2.2.2 (2020-05-15) 141 | ======================= 142 | 143 | Bugfixes 144 | -------- 145 | 146 | - Fix pytest args splitting (#155) 147 | - Fix runserver autoreload with channels 2.4 (#157) 148 | 149 | 150 | 2.2.1 (2020-04-23) 151 | ================== 152 | 153 | - Fix packaging error 154 | 155 | 2.2.0 (2020-04-23) 156 | ================== 157 | 158 | Features 159 | -------- 160 | 161 | - Add Django 3.0 / django CMS 3.7.2 support (#142) 162 | - Replace makefile with invoke (#143) 163 | 164 | 165 | 2.1.1 (2020-02-04) 166 | ================== 167 | 168 | - Improved pytest compatibility 169 | 170 | 2.1.0 (2019-12-27) 171 | ================== 172 | 173 | - Reformat code with black and improve flake8 configuration 174 | - Add pytest-compatible runner 175 | 176 | 2.0.1 (2019-12-22) 177 | ================== 178 | 179 | - Add Django 3.0 preliminary support 180 | 181 | 2.0.0 (2019-10-13) 182 | ================== 183 | 184 | - Rename application to django-app-helper 185 | 186 | 1.2.5 (2019-08-16) 187 | ================== 188 | 189 | - Add django CMS 3.7 190 | - Add Django 2.2 191 | 192 | 1.2.4 (2019-08-08) 193 | ================== 194 | 195 | - Fix regression introduced by #116 196 | 197 | 1.2.3 (2019-08-05) 198 | ================== 199 | 200 | - Move pyflakes to extras_require 201 | - Fix error in get_request / post_request not preserving current_page 202 | 203 | 1.2.2 (2019-07-05) 204 | ================== 205 | 206 | - Improve request generation by adding a more generic request method 207 | 208 | 1.2.1 (2019-07-04) 209 | ================== 210 | 211 | - Fix error when creating users with non-writable email attribute 212 | 213 | 1.2.0 (2019-03-22) 214 | ================== 215 | 216 | - Drop compatiblity with Django <1.11, Python 3.4 217 | - Add django CMS 3.6 218 | - Add django 2.0, 2.1 219 | 220 | 1.1.1 (2019-07-03) 221 | ================== 222 | 223 | - Fix error when creating users with non-writable email attribute 224 | 225 | 1.1.0 (2018-02-20) 226 | ================== 227 | 228 | - Remove Django <1.8, Python 2.6, 3.3 from setup.py 229 | - Add Django 1.11, Python 3.6 230 | - Switch to new-style middlewares for Django 1.10+ 231 | - Create static methods to generate images 232 | - Fix persistent option behavior with arbitrary commands 233 | - Add minimal changes to allow third party application to run test on django 2.0 234 | - Fix options for channels runserver 235 | - Remove support for django-nose test runner 236 | 237 | 1.0.0 (2017-07-25) 238 | ================== 239 | 240 | - Add ApphookReloadMiddleware in server mode 241 | - Add a default for FILE_UPLOAD_TEMP_DIR 242 | - Add fix for django CMS 3.4.4 render_plugin 243 | 244 | 0.9.8 (2017-03-04) 245 | ================== 246 | 247 | - Fix compatibility with newer channels releases 248 | 249 | 0.9.7 (2016-12-03) 250 | ================== 251 | 252 | - Add support for django-sekizai 0.10 253 | - Fix mock dependency in setup.py 254 | - Fix issue with server command in Django 1.10 255 | - Fix issue with urls.py in Django 1.10 256 | - Fix issue in tests with django CMS 3.4 257 | 258 | 0.9.6 (2016-08-25) 259 | ================== 260 | 261 | - Add support for channels runserver. 262 | - Add verbosity level to server command. 263 | - Add support for Django 1.10. 264 | - Add support for django CMS 3.4. 265 | 266 | 0.9.5 (2016-06-06) 267 | ================== 268 | 269 | - Fix issue with mocked session storage 270 | - Add verbosity level to tests 271 | - Fix user creation 272 | - Add option to allow parametrizing auto-created user 273 | - Fix extra_applications 274 | 275 | 0.9.4 (2016-01-20) 276 | ================== 277 | 278 | - Add Naked setup mode 279 | - Add TEMPLATE_DIRS to special settings 280 | - Add TEMPLATE_LOADERS to special settings 281 | - Allow to specify a locale in makemessages 282 | 283 | 0.9.3 (2015-10-07) 284 | ================== 285 | 286 | - Add --no-migrate option to skip migrations 287 | - Add secure argument to generate HTTPS requests 288 | - Better request mocking 289 | - Fix test on django CMS 3.2 (develop) 290 | - Add support for Python 3.5 291 | - Add --persistent option for persistent storage 292 | 293 | 0.9.2 (2015-09-14) 294 | ================== 295 | 296 | - Add support for apphooks and parent pages in BaseTestCase.create_pages 297 | - If pages contains apphook, urlconf is reloaded automatically 298 | - Improve documentation 299 | - Add support for top-positioned MIDDLEWARE_CLASSES 300 | - Code cleanup 301 | 302 | 0.9.1 (2015-08-30) 303 | ================== 304 | 305 | - Better support for aldryn-boilerplates 306 | 307 | 0.9.0 (2015-08-20) 308 | ================== 309 | 310 | - Complete support for Django 1.8 / django CMS develop 311 | - Support for aldryn-boilerplates settings 312 | - Migrations are now enabled by default during tests 313 | - Minor BaseTestCase refactoring 314 | - Remove support for Django 1.5 315 | - Fix treebeard support 316 | - Minor fixes 317 | - Adds login_user_context method to BaseTestCase 318 | 319 | 0.8.1 (2015-05-31) 320 | ================== 321 | 322 | - Add basic support for Django 1.8 / django CMS develop 323 | - Code cleanups 324 | - Smarter migration layout detection 325 | 326 | 0.8.0 (2015-03-22) 327 | ================== 328 | 329 | - Add --native option to use native test command instead of django-app-helper one 330 | - Use django-discover-runner on Django 1.5 if present 331 | - Better handling of runner options 332 | - Add support for empty/dry-run arguments to makemigrations 333 | - Add USE_CMS flag to settings when using django CMS configuration 334 | 335 | 0.7.0 (2015-01-22) 336 | ================== 337 | 338 | - Fix an error which prevents the runner to discover the settings 339 | - django CMS is no more a dependency, install it manually to enable django CMS support 340 | 341 | 0.6.0 (2015-01-10) 342 | ================== 343 | 344 | - Add a runner to make cms_helper file itself a runner for django-app-helper 345 | - Fix issues with mptt / treebeard and Django 1.7 346 | - Fix some makemigrations / --migrate issues 347 | - Make django-app-helper less django CMS dependent 348 | 349 | 0.5.0 (2015-01-01) 350 | ================== 351 | 352 | - Fixing bugs when using extra settings 353 | - Add messages framework to default environment 354 | - Add CSRF middleware / context_processor to default settings 355 | - Add base helper class for test cases 356 | - Complete Django 1.7 support 357 | - Smarter detection of migration operations in Django 1.6- 358 | - Add option to create migrations for external applications 359 | 360 | 0.4.0 (2014-09-18) 361 | ================== 362 | 363 | - Add support for command line test runner options; 364 | - Add check command on Django 1.7+; 365 | - Add cms check command (which triggers cms inclusion); 366 | - Add squashmigration command Django 1.7+; 367 | - Add support for makemigrations merge on Django 1.7+; 368 | - Add helpers for custom user models; 369 | 370 | 0.3.1 (2014-08-25) 371 | ================== 372 | 373 | - Add staticfiles application; 374 | - Add djangocms_admin_style if cms is enabled; 375 | 376 | 0.3.0 (2014-08-14) 377 | ================== 378 | 379 | - Add support for django nose test runner; 380 | - Add default CMS template; 381 | 382 | 0.2.0 (2014-08-12) 383 | ================== 384 | 385 | - Add option to customize sample project settings; 386 | - Add option to exclude django CMS from test project configurations; 387 | - Add support for Django 1.7; 388 | 389 | 0.1.0 (2014-08-09) 390 | ================== 391 | 392 | - First public release. 393 | -------------------------------------------------------------------------------- /tests/test_utils/example1/tests/test_fake.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from django.contrib.auth.models import AnonymousUser 5 | from django.test import tag 6 | 7 | from app_helper.base_test import BaseTestCase 8 | from app_helper.utils import get_user_model_labels 9 | 10 | 11 | class FakeTests(BaseTestCase): 12 | _pages_data = ( 13 | { 14 | "en": {"title": "Page title", "template": "page.html", "publish": True}, 15 | "fr": {"title": "Titre", "publish": True}, 16 | "it": {"title": "Titolo pagina", "publish": False}, 17 | }, 18 | { 19 | "en": { 20 | "title": "Second page", 21 | "template": "page.html", 22 | "publish": True, 23 | "parent": "page-title", 24 | "apphook": "Example", 25 | }, 26 | "fr": {"title": "Deuxieme", "publish": True}, 27 | "it": {"title": "Seconda pagina", "publish": False}, 28 | }, 29 | ) 30 | 31 | def test_fake(self): 32 | self.assertTrue(True) 33 | 34 | def test_pages(self): 35 | from django.conf import settings 36 | 37 | data = self.get_pages_data() 38 | self.assertEqual(set(data[0].keys()), {"en", "fr", "it"}) 39 | 40 | if "cms" in settings.INSTALLED_APPS: 41 | pages = self.get_pages() 42 | self.assertEqual(len(pages), 2) 43 | 44 | def test_get(self): 45 | from django.conf import settings 46 | 47 | if "cms" not in settings.INSTALLED_APPS: 48 | raise unittest.SkipTest("django CMS not available, skipping test") 49 | from cms.api import add_plugin 50 | 51 | pages = self.get_pages() 52 | add_plugin(placeholder=pages[0].placeholders.get(slot="content"), plugin_type="FakePlugin", language="en") 53 | pages[0].publish("en") 54 | response = self.client.get("/en/") 55 | self.assertContains(response, "fake text") 56 | self.assertContains(response, "body{font-weight: bold;}") 57 | self.assertContains(response, "Page title") 58 | 59 | def test_create_user(self): 60 | from django.conf import settings 61 | 62 | if "cms" not in settings.INSTALLED_APPS: 63 | raise unittest.SkipTest("django CMS not available, skipping test") 64 | user = self.create_user( 65 | username="random", 66 | email="random@example.com", 67 | password="random", 68 | is_staff=True, 69 | base_cms_permissions=True, 70 | permissions=["add_placeholder"], 71 | ) 72 | self._login_context = self.login_user_context(user) 73 | self._login_context.user.has_perm("add_placeholdr") 74 | self._login_context.user.has_perm("add_text") 75 | 76 | @tag("a-tag") 77 | def test_login_context(self): 78 | request = self.get_request(None, "en", path="/en") 79 | self.assertTrue(request.user, AnonymousUser()) 80 | 81 | self._login_context = self.login_user_context(self.user) 82 | request = self.get_request(None, "en", path="/en") 83 | self.assertTrue(request.user, self.user) 84 | self._login_context.__exit__(None, None, None) 85 | 86 | request = self.get_request(None, "en", path="/en") 87 | self.assertTrue(request.user, AnonymousUser()) 88 | 89 | with self.login_user_context(self.user): 90 | request = self.get_request(None, "en", path="/en") 91 | self.assertTrue(request.user, self.user) 92 | 93 | request = self.get_request(None, "en", path="/en") 94 | self.assertTrue(request.user, AnonymousUser()) 95 | 96 | def test_requests(self): 97 | from django.conf import settings 98 | 99 | print("visible string") 100 | with self.captured_output() as (out, err): 101 | print("hidden string") 102 | if "cms" in settings.INSTALLED_APPS: 103 | pages = self.get_pages() 104 | 105 | request = self.get_request(pages[1], "en") 106 | self.assertEqual(request.path, "/en/second-page/") 107 | self.assertEqual(request.META["REQUEST_METHOD"], "GET") 108 | self.assertEqual(request.current_page, pages[1]) 109 | 110 | request = self.get_request(None, "en", path="/cumstom-path/") 111 | self.assertEqual(request.path, "/cumstom-path/") 112 | self.assertEqual(request.META["REQUEST_METHOD"], "GET") 113 | self.assertEqual(request.current_page, None) 114 | 115 | request = self.post_request(pages[1], "en", data={"payload": 1}) 116 | self.assertEqual(request.path, "/en/second-page/") 117 | self.assertEqual(request.META["REQUEST_METHOD"], "POST") 118 | self.assertEqual(request.POST.get("payload"), "1") 119 | 120 | request = self.get_page_request( 121 | pages[1], 122 | lang="en", 123 | user=self.user_staff, 124 | edit=True, 125 | ) 126 | self.assertEqual(request.path, "/en/second-page/") 127 | self.assertTrue(request.toolbar) 128 | self.assertEqual(request.META["REQUEST_METHOD"], "GET") 129 | 130 | def test_get_user_model(self): 131 | if os.environ.get("AUTH_USER_MODEL", "auth.User"): 132 | user_orm_label, user_model_label = get_user_model_labels() 133 | self.assertEqual(user_orm_label, "auth.User") 134 | self.assertEqual(user_model_label, "auth.user") 135 | 136 | def test_create_image(self): 137 | from filer.models import Image 138 | 139 | image = self.create_filer_image_object() 140 | self.assertEqual(image.original_filename, self.image_name) 141 | self.assertEqual(image.width, 800) 142 | self.assertEqual(image.height, 600) 143 | self.assertEqual(Image.objects.count(), 1) 144 | 145 | def test_create_filer_image(self): 146 | from filer.models import Image 147 | 148 | image = BaseTestCase.create_filer_image(self.user, "random.jpg") 149 | self.assertEqual(image.original_filename, "random.jpg") 150 | self.assertEqual(image.width, 800) 151 | self.assertEqual(image.height, 600) 152 | self.assertEqual(Image.objects.count(), 1) 153 | 154 | def test_create_django_image_object(self): 155 | image = self.create_django_image_object() 156 | self.assertEqual(image.name, self.image_name) 157 | 158 | def test_render_plugin(self): 159 | from django.conf import settings 160 | 161 | if "cms" not in settings.INSTALLED_APPS: 162 | raise unittest.SkipTest("django CMS not available, skipping test") 163 | 164 | from cms.api import add_plugin 165 | 166 | sample_text = "\nfake text\nen\nPage title\n\n" 167 | pages = self.get_pages() 168 | public = pages[0].get_public_object() 169 | placeholder = pages[0].placeholders.get(slot="content") 170 | plugin = add_plugin(placeholder=placeholder, plugin_type="FakePlugin", language="en") 171 | pages[0].publish("en") 172 | rendered_2 = self.render_plugin(public, "en", plugin) 173 | self.assertEqual(rendered_2, sample_text) 174 | 175 | def test_request(self): 176 | from django.conf import settings 177 | 178 | if "cms" not in settings.INSTALLED_APPS: 179 | raise unittest.SkipTest("django CMS not available, skipping test") 180 | 181 | pages = self.get_pages() 182 | 183 | request = self.get_request(pages[1], lang="en") 184 | self.assertIsNone(getattr(request, "toolbar", None)) 185 | 186 | request = self.get_page_request(pages[1], user=self.user) 187 | self.assertIsNotNone(getattr(request, "toolbar", None)) 188 | 189 | request = self.get_request(pages[1], "en", use_middlewares=True) 190 | self.assertIsNotNone(getattr(request, "toolbar", None)) 191 | self.assertIsNotNone(getattr(request, "_messages", None)) 192 | 193 | request = self.get_request(pages[1], "en", secure=True) 194 | self.assertTrue(request.is_secure()) 195 | 196 | def test_request_full_middlewares(self): 197 | # naked request 198 | request = self.get_request(page=None, lang="en", user=None, path="/en/second-page/", use_middlewares=True) 199 | self.assertEqual(request.user, AnonymousUser()) 200 | with self.assertRaises(AttributeError): 201 | self.assertTrue(request.current_page.pk) 202 | self.assertIsNotNone(getattr(request, "_messages", None)) 203 | 204 | # passing user 205 | request = self.get_request(page=None, lang="en", user=self.user, path="/en/second-page/", use_middlewares=True) 206 | self.assertEqual(request.user, self.user) 207 | with self.assertRaises(AttributeError): 208 | self.assertTrue(request.current_page.pk) 209 | self.assertIsNotNone(getattr(request, "_messages", None)) 210 | 211 | # logged in user 212 | with self.login_user_context(self.user): 213 | request = self.get_request(page=None, lang="en", user=None, path="/en/second-page/", use_middlewares=True) 214 | self.assertEqual(request.user, self.user) 215 | with self.assertRaises(AttributeError): 216 | self.assertTrue(request.current_page.pk) 217 | 218 | def test_request_full_middlewares_cms(self): 219 | from django.conf import settings 220 | 221 | if "cms" not in settings.INSTALLED_APPS: 222 | raise unittest.SkipTest("django CMS not available, skipping test") 223 | 224 | pages = self.get_pages() 225 | # naked request, page and language is derived from path 226 | request = self.get_request(page=None, lang=None, user=None, path="/en/second-page/", use_middlewares=True) 227 | self.assertEqual(request.user, AnonymousUser()) 228 | self.assertEqual(request.LANGUAGE_CODE, "en") 229 | self.assertEqual(request.current_page.get_absolute_url(), pages[1].get_absolute_url()) 230 | self.assertIsNotNone(getattr(request, "_messages", None)) 231 | 232 | # naked request, page and language is derived from path, user is logged in 233 | request = self.get_request(page=None, lang=None, user=self.user, path="/en/second-page/", use_middlewares=True) 234 | self.assertEqual(request.user, self.user) 235 | self.assertEqual(request.LANGUAGE_CODE, "en") 236 | self.assertEqual(request.current_page.get_absolute_url(), pages[1].get_absolute_url()) 237 | self.assertIsNotNone(getattr(request, "_messages", None)) 238 | 239 | # naked request, path is derived from path 240 | request = self.get_request(page=pages[1], lang="en", user=self.user, use_middlewares=True) 241 | self.assertEqual(request.user, self.user) 242 | self.assertEqual(request.path_info, "/en/second-page/") 243 | self.assertEqual(request.LANGUAGE_CODE, "en") 244 | self.assertEqual(request.current_page.get_absolute_url(), pages[1].get_absolute_url()) 245 | self.assertIsNotNone(getattr(request, "_messages", None)) 246 | 247 | # logged in user through context manager 248 | with self.login_user_context(self.user): 249 | request = self.get_request(page=None, lang=None, user=None, path="/en/second-page/", use_middlewares=True) 250 | self.assertEqual(request.user, self.user) 251 | self.assertEqual(request.current_page.get_absolute_url(), pages[1].get_absolute_url()) 252 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/vim,node,sass,vuejs,linux,macos,django,python,pycharm,windows,virtualenv,sublimetext,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=vim,node,sass,vuejs,linux,macos,django,python,pycharm,windows,virtualenv,sublimetext,visualstudiocode 4 | 5 | ### Django ### 6 | *.log 7 | *.pot 8 | *.pyc 9 | __pycache__/ 10 | local_settings.py 11 | db.sqlite3 12 | db.sqlite3-journal 13 | media 14 | 15 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 16 | # in your Git repository. Update and uncomment the following line accordingly. 17 | # /staticfiles/ 18 | 19 | ### Django.Python Stack ### 20 | # Byte-compiled / optimized / DLL files 21 | *.py[cod] 22 | *$py.class 23 | 24 | # C extensions 25 | *.so 26 | 27 | # Distribution / packaging 28 | .Python 29 | build/ 30 | develop-eggs/ 31 | dist/ 32 | downloads/ 33 | eggs/ 34 | .eggs/ 35 | lib/ 36 | lib64/ 37 | parts/ 38 | sdist/ 39 | var/ 40 | wheels/ 41 | pip-wheel-metadata/ 42 | share/python-wheels/ 43 | *.egg-info/ 44 | .installed.cfg 45 | *.egg 46 | MANIFEST 47 | 48 | # PyInstaller 49 | # Usually these files are written by a python script from a template 50 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 51 | *.manifest 52 | *.spec 53 | 54 | # Installer logs 55 | pip-log.txt 56 | pip-delete-this-directory.txt 57 | 58 | # Unit test / coverage reports 59 | htmlcov/ 60 | .tox/ 61 | .nox/ 62 | .coverage 63 | .coverage.* 64 | .cache 65 | nosetests.xml 66 | coverage.xml 67 | *.cover 68 | .hypothesis/ 69 | .pytest_cache/ 70 | 71 | # Translations 72 | *.mo 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # pipenv 87 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 88 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 89 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 90 | # install all needed dependencies. 91 | #Pipfile.lock 92 | 93 | # celery beat schedule file 94 | celerybeat-schedule 95 | 96 | # SageMath parsed files 97 | *.sage.py 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # Mr Developer 107 | .mr.developer.cfg 108 | .project 109 | .pydevproject 110 | 111 | # mkdocs documentation 112 | /site 113 | 114 | # mypy 115 | .mypy_cache/ 116 | .dmypy.json 117 | dmypy.json 118 | 119 | # Pyre type checker 120 | .pyre/ 121 | 122 | ### Linux ### 123 | *~ 124 | 125 | # temporary files which can be created if a process still has a handle open of a deleted file 126 | .fuse_hidden* 127 | 128 | # KDE directory preferences 129 | .directory 130 | 131 | # Linux trash folder which might appear on any partition or disk 132 | .Trash-* 133 | 134 | # .nfs files are created when an open file is removed but is still being accessed 135 | .nfs* 136 | 137 | ### macOS ### 138 | # General 139 | .DS_Store 140 | .AppleDouble 141 | .LSOverride 142 | 143 | # Icon must end with two \r 144 | Icon 145 | 146 | # Thumbnails 147 | ._* 148 | 149 | # Files that might appear in the root of a volume 150 | .DocumentRevisions-V100 151 | .fseventsd 152 | .Spotlight-V100 153 | .TemporaryItems 154 | .Trashes 155 | .VolumeIcon.icns 156 | .com.apple.timemachine.donotpresent 157 | 158 | # Directories potentially created on remote AFP share 159 | .AppleDB 160 | .AppleDesktop 161 | Network Trash Folder 162 | Temporary Items 163 | .apdisk 164 | 165 | ### Node ### 166 | # Logs 167 | logs 168 | npm-debug.log* 169 | yarn-debug.log* 170 | yarn-error.log* 171 | lerna-debug.log* 172 | 173 | # Diagnostic reports (https://nodejs.org/api/report.html) 174 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 175 | 176 | # Runtime data 177 | pids 178 | *.pid 179 | *.seed 180 | *.pid.lock 181 | 182 | # Directory for instrumented libs generated by jscoverage/JSCover 183 | lib-cov 184 | 185 | # Coverage directory used by tools like istanbul 186 | coverage 187 | *.lcov 188 | 189 | # nyc test coverage 190 | .nyc_output 191 | 192 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 193 | .grunt 194 | 195 | # Bower dependency directory (https://bower.io/) 196 | bower_components 197 | 198 | # node-waf configuration 199 | .lock-wscript 200 | 201 | # Compiled binary addons (https://nodejs.org/api/addons.html) 202 | build/Release 203 | 204 | # Dependency directories 205 | node_modules/ 206 | jspm_packages/ 207 | 208 | # TypeScript v1 declaration files 209 | typings/ 210 | 211 | # TypeScript cache 212 | *.tsbuildinfo 213 | 214 | # Optional npm cache directory 215 | .npm 216 | 217 | # Optional eslint cache 218 | .eslintcache 219 | 220 | # Optional REPL history 221 | .node_repl_history 222 | 223 | # Output of 'npm pack' 224 | *.tgz 225 | 226 | # Yarn Integrity file 227 | .yarn-integrity 228 | 229 | # dotenv environment variables file 230 | .env 231 | .env.test 232 | 233 | # parcel-bundler cache (https://parceljs.org/) 234 | 235 | # next.js build output 236 | .next 237 | 238 | # nuxt.js build output 239 | .nuxt 240 | 241 | # rollup.js default build output 242 | 243 | # Uncomment the public line if your project uses Gatsby 244 | # https://nextjs.org/blog/next-9-1#public-directory-support 245 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 246 | # public 247 | 248 | # Storybook build outputs 249 | .out 250 | .storybook-out 251 | 252 | # vuepress build output 253 | .vuepress/dist 254 | 255 | # Serverless directories 256 | .serverless/ 257 | 258 | # FuseBox cache 259 | .fusebox/ 260 | 261 | # DynamoDB Local files 262 | .dynamodb/ 263 | 264 | # Temporary folders 265 | tmp/ 266 | temp/ 267 | 268 | ### PyCharm ### 269 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 270 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 271 | 272 | # User-specific stuff 273 | .idea/**/workspace.xml 274 | .idea/**/tasks.xml 275 | .idea/**/usage.statistics.xml 276 | .idea/**/dictionaries 277 | .idea/**/shelf 278 | 279 | # Generated files 280 | .idea/**/contentModel.xml 281 | 282 | # Sensitive or high-churn files 283 | .idea/**/dataSources/ 284 | .idea/**/dataSources.ids 285 | .idea/**/dataSources.local.xml 286 | .idea/**/sqlDataSources.xml 287 | .idea/**/dynamic.xml 288 | .idea/**/uiDesigner.xml 289 | .idea/**/dbnavigator.xml 290 | 291 | # Gradle 292 | .idea/**/gradle.xml 293 | .idea/**/libraries 294 | 295 | # Gradle and Maven with auto-import 296 | # When using Gradle or Maven with auto-import, you should exclude module files, 297 | # since they will be recreated, and may cause churn. Uncomment if using 298 | # auto-import. 299 | # .idea/modules.xml 300 | # .idea/*.iml 301 | # .idea/modules 302 | # *.iml 303 | # *.ipr 304 | 305 | # CMake 306 | cmake-build-*/ 307 | 308 | # Mongo Explorer plugin 309 | .idea/**/mongoSettings.xml 310 | 311 | # File-based project format 312 | *.iws 313 | 314 | # IntelliJ 315 | out/ 316 | 317 | # mpeltonen/sbt-idea plugin 318 | .idea_modules/ 319 | 320 | # JIRA plugin 321 | atlassian-ide-plugin.xml 322 | 323 | # Cursive Clojure plugin 324 | .idea/replstate.xml 325 | 326 | # Crashlytics plugin (for Android Studio and IntelliJ) 327 | com_crashlytics_export_strings.xml 328 | crashlytics.properties 329 | crashlytics-build.properties 330 | fabric.properties 331 | 332 | # Editor-based Rest Client 333 | .idea/httpRequests 334 | 335 | # Android studio 3.1+ serialized cache file 336 | .idea/caches/build_file_checksums.ser 337 | 338 | ### PyCharm Patch ### 339 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 340 | 341 | # *.iml 342 | # modules.xml 343 | # .idea/misc.xml 344 | # *.ipr 345 | 346 | # Sonarlint plugin 347 | .idea/**/sonarlint/ 348 | 349 | # SonarQube Plugin 350 | .idea/**/sonarIssues.xml 351 | 352 | # Markdown Navigator plugin 353 | .idea/**/markdown-navigator.xml 354 | .idea/**/markdown-navigator/ 355 | 356 | ### Python ### 357 | # Byte-compiled / optimized / DLL files 358 | 359 | # C extensions 360 | 361 | # Distribution / packaging 362 | 363 | # PyInstaller 364 | # Usually these files are written by a python script from a template 365 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 366 | 367 | # Installer logs 368 | 369 | # Unit test / coverage reports 370 | 371 | # Translations 372 | 373 | # Scrapy stuff: 374 | 375 | # Sphinx documentation 376 | 377 | # PyBuilder 378 | 379 | # pyenv 380 | 381 | # pipenv 382 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 383 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 384 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 385 | # install all needed dependencies. 386 | 387 | # celery beat schedule file 388 | 389 | # SageMath parsed files 390 | 391 | # Spyder project settings 392 | 393 | # Rope project settings 394 | 395 | # Mr Developer 396 | 397 | # mkdocs documentation 398 | 399 | # mypy 400 | 401 | # Pyre type checker 402 | 403 | ### Sass ### 404 | .sass-cache/ 405 | *.css.map 406 | *.sass.map 407 | *.scss.map 408 | 409 | ### SublimeText ### 410 | # Cache files for Sublime Text 411 | *.tmlanguage.cache 412 | *.tmPreferences.cache 413 | *.stTheme.cache 414 | 415 | # Workspace files are user-specific 416 | *.sublime-workspace 417 | 418 | # Project files should be checked into the repository, unless a significant 419 | # proportion of contributors will probably not be using Sublime Text 420 | # *.sublime-project 421 | 422 | # SFTP configuration file 423 | sftp-config.json 424 | 425 | # Package control specific files 426 | Package Control.last-run 427 | Package Control.ca-list 428 | Package Control.ca-bundle 429 | Package Control.system-ca-bundle 430 | Package Control.cache/ 431 | Package Control.ca-certs/ 432 | Package Control.merged-ca-bundle 433 | Package Control.user-ca-bundle 434 | oscrypto-ca-bundle.crt 435 | bh_unicode_properties.cache 436 | 437 | # Sublime-github package stores a github token in this file 438 | # https://packagecontrol.io/packages/sublime-github 439 | GitHub.sublime-settings 440 | 441 | ### Vim ### 442 | # Swap 443 | [._]*.s[a-v][a-z] 444 | [._]*.sw[a-p] 445 | [._]s[a-rt-v][a-z] 446 | [._]ss[a-gi-z] 447 | [._]sw[a-p] 448 | 449 | # Session 450 | Session.vim 451 | Sessionx.vim 452 | 453 | # Temporary 454 | .netrwhist 455 | 456 | # Auto-generated tag files 457 | tags 458 | 459 | # Persistent undo 460 | [._]*.un~ 461 | 462 | # Coc configuration directory 463 | .vim 464 | 465 | ### VirtualEnv ### 466 | # Virtualenv 467 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 468 | pyvenv.cfg 469 | .venv 470 | env/ 471 | venv/ 472 | ENV/ 473 | env.bak/ 474 | venv.bak/ 475 | pip-selfcheck.json 476 | 477 | ### VisualStudioCode ### 478 | .vscode/* 479 | !.vscode/settings.json 480 | !.vscode/tasks.json 481 | !.vscode/launch.json 482 | !.vscode/extensions.json 483 | 484 | ### VisualStudioCode Patch ### 485 | # Ignore all local history of files 486 | .history 487 | 488 | ### Vuejs ### 489 | # Recommended template: Node.gitignore 490 | 491 | npm-debug.log 492 | yarn-error.log 493 | 494 | ### Windows ### 495 | # Windows thumbnail cache files 496 | Thumbs.db 497 | Thumbs.db:encryptable 498 | ehthumbs.db 499 | ehthumbs_vista.db 500 | 501 | # Dump file 502 | *.stackdump 503 | 504 | # Folder config file 505 | [Dd]esktop.ini 506 | 507 | # Recycle Bin used on file shares 508 | $RECYCLE.BIN/ 509 | 510 | # Windows Installer files 511 | *.cab 512 | *.msi 513 | *.msix 514 | *.msm 515 | *.msp 516 | 517 | # Windows shortcuts 518 | *.lnk 519 | 520 | # End of https://www.gitignore.io/api/vim,node,sass,vuejs,linux,macos,django,python,pycharm,windows,virtualenv,sublimetext,visualstudiocode 521 | local.sqlite 522 | -------------------------------------------------------------------------------- /app_helper/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import contextlib 4 | import os 5 | import subprocess 6 | import sys 7 | import warnings 8 | 9 | from django.utils.encoding import force_str 10 | from docopt import DocoptExit, docopt 11 | 12 | from . import __version__ 13 | from .utils import _create_db, _make_settings, ensure_unicoded_and_unique, persistent_dir, temp_dir, work_in 14 | 15 | __doc__ = """django CMS applications development helper script. 16 | 17 | To use a different database, set the DATABASE_URL environment variable to a 18 | dj-database-url compatible value. 19 | 20 | Usage: 21 | django-app-helper test [--failfast] [--migrate] [--no-migrate] [...] [--xvfb] [--runner=] [--extra-settings=] [--cms] [--runner-options=,] [--native] [--persistent] [--persistent-path=] [--verbose=] 22 | django-app-helper cms_check [--extra-settings=] [--cms] [--migrate] [--no-migrate] 23 | django-app-helper compilemessages [--extra-settings=] [--cms] 24 | django-app-helper makemessages [--extra-settings=] [--cms] [--locale=locale] 25 | django-app-helper makemigrations [--extra-settings=] [--cms] [--merge] [--empty] [--dry-run] [...] 26 | django-app-helper authors [--extra-settings=] [--cms] 27 | django-app-helper server [--port=] [--bind=] [--extra-settings=] [--cms] [--migrate] [--no-migrate] [--persistent | --persistent-path=] [--verbose=] [--use-daphne] [--use-channels] 28 | django-app-helper setup [--extra-settings=] [--cms] 29 | django-app-helper [options] [--extra-settings=] [--cms] [--persistent] [--persistent-path=] [--migrate] [--no-migrate] 30 | 31 | Options: 32 | -h --help Show this screen. 33 | --version Show version. 34 | --migrate Use migrations. 35 | --no-migrate Skip south migrations. 36 | --cms Add support for CMS in the project configuration. 37 | --merge Merge migrations 38 | --failfast Stop tests on first failure. 39 | --native Use the native test command, instead of the django-app-helper on 40 | --persistent Use persistent storage 41 | --persistent-path= Persistent storage path 42 | --locale=locale,-l=locale Update messgaes for given locale 43 | --xvfb Use a virtual X framebuffer for frontend testing, requires xvfbwrapper to be installed. 44 | --extra-settings= Filesystem path to a custom app_helper file which defines custom settings 45 | --runner= Dotted path to a custom test runner 46 | --runner-options=, Comma separated list of command line options for the test runner 47 | --port= Port to listen on [default: 8000]. 48 | --bind= Interface to bind to [default: 127.0.0.1]. 49 | --use-channels Run the channels runserver instead of the Django one 50 | --use-daphne Run the Daphne runserver instead of the Django one 51 | Comma separated list of applications to create migrations for 52 | """ # NOQA # nopyflakes 53 | 54 | 55 | def _parse_runner_options(test_runner_class, options): 56 | if hasattr(test_runner_class, "add_arguments"): 57 | parser = argparse.ArgumentParser() 58 | test_runner_class.add_arguments(parser) 59 | args = parser.parse_args(options) 60 | return vars(args) 61 | return {} # pragma: no cover 62 | 63 | 64 | def _test_run_worker(test_labels, test_runner, failfast=False, runner_options=None, verbose=1): 65 | warnings.filterwarnings( 66 | "error", 67 | r"DateTimeField received a naive datetime", 68 | RuntimeWarning, 69 | r"django\.db\.models\.fields", 70 | ) 71 | from django.conf import settings 72 | from django.test.utils import get_runner 73 | 74 | try: 75 | verbose = int(verbose) 76 | except (ValueError, TypeError): 77 | verbose = 1 78 | runner_options = runner_options or [] 79 | settings.TEST_RUNNER = test_runner 80 | TestRunner = get_runner(settings) # NOQA 81 | 82 | kwargs = {"verbosity": verbose, "interactive": False, "failfast": failfast} 83 | if runner_options: 84 | if "PytestTestRunner" in test_runner: 85 | kwargs["pytest_args"] = runner_options 86 | else: 87 | if not isinstance(runner_options, list): 88 | runner_options = runner_options.split(" ") 89 | extra = _parse_runner_options(TestRunner, runner_options) 90 | extra.update(kwargs) 91 | kwargs = extra 92 | test_runner = TestRunner(**kwargs) 93 | failures = test_runner.run_tests(test_labels) 94 | return failures 95 | 96 | 97 | def test(test_labels, application, failfast=False, test_runner=None, runner_options=None, verbose=1): 98 | """ 99 | Runs the test suite 100 | :param test_labels: space separated list of test labels 101 | :param failfast: option to stop the testsuite on the first error 102 | """ 103 | if not test_labels and "PytestTestRunner" not in test_runner: 104 | if os.path.exists("tests"): # pragma: no cover 105 | test_labels = ["tests"] 106 | elif os.path.exists(os.path.join(application, "tests")): 107 | test_labels = ["%s.tests" % application] 108 | elif isinstance(test_labels, str): # pragma: no cover 109 | test_labels = [test_labels] 110 | runner_options = runner_options or [] 111 | return _test_run_worker(test_labels, test_runner, failfast, runner_options, verbose) 112 | 113 | 114 | def compilemessages(application): 115 | """ 116 | Compiles locale messages 117 | """ 118 | from django.core.management import call_command 119 | 120 | with work_in(application): 121 | call_command("compilemessages") 122 | 123 | 124 | def makemessages(application, locale): 125 | """ 126 | Updates the locale message files 127 | """ 128 | from django.core.management import call_command 129 | 130 | if not locale: 131 | locale = "en" 132 | with work_in(application): 133 | call_command("makemessages", locale=(locale,)) 134 | 135 | 136 | def cms_check(migrate_cmd=False): 137 | """ 138 | Runs the django CMS ``cms check`` command 139 | """ 140 | from django.core.management import call_command 141 | 142 | try: 143 | import cms # NOQA # nopyflakes 144 | 145 | _create_db(migrate_cmd) 146 | call_command("cms", "check") 147 | except ImportError: # pragma: no cover 148 | print("cms_check available only if django CMS is installed") 149 | 150 | 151 | def makemigrations(application, merge=False, dry_run=False, empty=False, extra_applications=None): 152 | """ 153 | Generate migrations 154 | """ 155 | from django.core.management import call_command 156 | 157 | apps = [application] 158 | if extra_applications: 159 | if isinstance(extra_applications, str): # pragma: no cover 160 | apps += [extra_applications] 161 | elif isinstance(extra_applications, list): 162 | apps += extra_applications 163 | 164 | for app in apps: 165 | call_command("makemigrations", *(app,), merge=merge, dry_run=dry_run, empty=empty) 166 | 167 | 168 | def generate_authors(): 169 | """ 170 | Updates the authors list 171 | """ 172 | print("Generating AUTHORS") 173 | 174 | # Get our list of authors 175 | print("Collecting author names") 176 | r = subprocess.Popen(["git", "log", "--use-mailmap", "--format=%aN"], stdout=subprocess.PIPE) 177 | seen_authors = [] 178 | authors = [] 179 | for authfile in ("AUTHORS", "AUTHORS.rst"): 180 | if os.path.exists(authfile): 181 | break 182 | with open(authfile) as f: 183 | for line in f.readlines(): 184 | if line.startswith("*"): 185 | author = force_str(line).strip("* \n") 186 | if author.lower() not in seen_authors: 187 | seen_authors.append(author.lower()) 188 | authors.append(author) 189 | for author in r.stdout.readlines(): 190 | author = force_str(author).strip() 191 | if author.lower() not in seen_authors: 192 | seen_authors.append(author.lower()) 193 | authors.append(author) 194 | 195 | # Sort our list of Authors by their case insensitive name 196 | authors = sorted(authors, key=lambda x: x.lower()) 197 | 198 | # Write our authors to the AUTHORS file 199 | print("Authors ({}):\n\n\n* {}".format(len(authors), "\n* ".join(authors))) 200 | 201 | 202 | def server( 203 | settings, bind="127.0.0.1", port=8000, migrate_cmd=False, verbose=1, use_channels=False, use_daphne=False 204 | ): # pragma: no cover 205 | from .server import run 206 | 207 | run(settings, bind, port, migrate_cmd, verbose, use_channels, use_daphne) 208 | 209 | 210 | def setup_env(settings): 211 | return settings 212 | 213 | 214 | def _map_argv(argv, application_module): 215 | try: 216 | # by default docopt uses sys.argv[1:]; ensure correct args passed 217 | args = docopt(__doc__, argv=argv[1:], version=application_module.__version__) 218 | if argv[2] == "help": 219 | raise DocoptExit() 220 | except DocoptExit: 221 | if argv[2] == "help": 222 | raise 223 | args = docopt(__doc__, argv[1:3], version=application_module.__version__) 224 | args["--cms"] = "--cms" in argv 225 | args["--persistent"] = "--persistent" in argv 226 | for arg in argv: 227 | if arg.startswith("--extra-settings="): 228 | args["--extra-settings"] = arg.split("=")[1] 229 | if arg.startswith("--runner="): 230 | args["--runner"] = arg.split("=")[1] 231 | if arg.startswith("--persistent-path="): 232 | args["--persistent-path"] = arg.split("=")[1] 233 | args["--persistent"] = True 234 | args["options"] = [argv[0]] + argv[2:] 235 | if args["test"] and "--native" in args["options"]: 236 | args["test"] = False 237 | args[""] = "test" 238 | args["options"].remove("--native") 239 | return args 240 | 241 | 242 | def core(args, application): 243 | from django.conf import settings 244 | 245 | # configure django 246 | warnings.filterwarnings( 247 | "error", 248 | r"DateTimeField received a naive datetime", 249 | RuntimeWarning, 250 | r"django\.db\.models\.fields", 251 | ) 252 | if args["--persistent"]: 253 | create_dir = persistent_dir 254 | if args["--persistent-path"]: 255 | parent_path = args["--persistent-path"] 256 | else: 257 | parent_path = "data" 258 | else: 259 | create_dir = temp_dir 260 | parent_path = "/dev/shm" 261 | 262 | with create_dir("static", parent_path) as STATIC_ROOT: # NOQA 263 | with create_dir("media", parent_path) as MEDIA_ROOT: # NOQA 264 | args["MEDIA_ROOT"] = MEDIA_ROOT 265 | args["STATIC_ROOT"] = STATIC_ROOT 266 | if args["cms_check"]: 267 | args["--cms"] = True 268 | 269 | if args[""]: 270 | from django.core.management import execute_from_command_line 271 | 272 | options = [ 273 | option 274 | for option in args["options"] 275 | if ( 276 | option != "--cms" 277 | and "--extra-settings" not in option 278 | and not option.startswith("--persistent") 279 | ) 280 | ] 281 | _make_settings(args, application, settings, STATIC_ROOT, MEDIA_ROOT) 282 | execute_from_command_line(options) 283 | 284 | else: 285 | _make_settings(args, application, settings, STATIC_ROOT, MEDIA_ROOT) 286 | # run 287 | if args["test"]: 288 | if args["--runner"]: 289 | runner = args["--runner"] 290 | else: 291 | runner = settings.TEST_RUNNER 292 | 293 | # make "Address already in use" errors less likely, see Django 294 | # docs for more details on this env variable. 295 | os.environ.setdefault("DJANGO_LIVE_TEST_SERVER_ADDRESS", "localhost:8000-9000") 296 | if args["--xvfb"]: # pragma: no cover 297 | import xvfbwrapper 298 | 299 | context = xvfbwrapper.Xvfb(width=1280, height=720) 300 | else: 301 | 302 | @contextlib.contextmanager 303 | def null_context(): 304 | yield 305 | 306 | context = null_context() 307 | 308 | with context: 309 | num_failures = test( 310 | args[""], 311 | application, 312 | args["--failfast"], 313 | runner, 314 | args["--runner-options"], 315 | args.get("--verbose", 1), 316 | ) 317 | sys.exit(num_failures) 318 | elif args["server"]: 319 | server( 320 | settings, 321 | args["--bind"], 322 | args["--port"], 323 | args.get("--migrate", True), 324 | args.get("--verbose", 1), 325 | args.get("--use-channels", False), 326 | args.get("--use-daphne", False), 327 | ) 328 | elif args["cms_check"]: 329 | cms_check(args.get("--migrate", True)) 330 | elif args["compilemessages"]: 331 | compilemessages(application) 332 | elif args["makemessages"]: 333 | makemessages(application, locale=args["--locale"]) 334 | elif args["makemigrations"]: 335 | makemigrations( 336 | application, 337 | merge=args["--merge"], 338 | dry_run=args["--dry-run"], 339 | empty=args["--empty"], 340 | extra_applications=args[""], 341 | ) 342 | elif args["authors"]: 343 | return generate_authors() 344 | elif args["setup"]: 345 | return setup_env(settings) 346 | 347 | 348 | def main(argv=sys.argv): # pragma: no cover 349 | # Command is executed in the main directory of the plugin, and we must 350 | # include it in the current path for the imports to work 351 | sys.path.insert(0, ".") 352 | if len(argv) > 1: 353 | application = argv[1] 354 | # ensure that argv, are unique and the same type as doc string 355 | argv = ensure_unicoded_and_unique(argv, application) 356 | application_module = __import__(application) 357 | args = _map_argv(argv, application_module) 358 | return core(args=args, application=application) 359 | else: 360 | args = docopt(__doc__, version=__version__) 361 | -------------------------------------------------------------------------------- /app_helper/utils.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import os 3 | import random 4 | import shutil 5 | import string 6 | import sys 7 | from tempfile import mkdtemp 8 | from unittest.mock import patch 9 | 10 | import django 11 | import six 12 | from django.core.management import call_command 13 | from django.urls import clear_url_caches 14 | from django.utils.functional import empty 15 | from packaging import version 16 | 17 | from . import HELPER_FILE 18 | 19 | try: 20 | import cms # NOQA 21 | 22 | CMS = True 23 | CMS_42 = version.parse("4.2") <= version.parse(cms.__version__) < version.parse("4.3") 24 | CMS_41 = version.parse("4.1") <= version.parse(cms.__version__) < version.parse("4.2") 25 | CMS_40 = version.parse("4.0") <= version.parse(cms.__version__) < version.parse("4.1") 26 | CMS_311 = version.parse("3.11") <= version.parse(cms.__version__) < version.parse("4.0") 27 | CMS_310 = version.parse("3.10") <= version.parse(cms.__version__) < version.parse("3.11") 28 | CMS_39 = version.parse("3.9") <= version.parse(cms.__version__) < version.parse("3.10") 29 | CMS_38 = version.parse("3.8") <= version.parse(cms.__version__) < version.parse("3.9") 30 | CMS_37 = version.parse("3.7") <= version.parse(cms.__version__) < version.parse("3.8") 31 | CMS_36 = version.parse("3.6") <= version.parse(cms.__version__) < version.parse("3.7") 32 | CMS_35 = version.parse("3.5") <= version.parse(cms.__version__) < version.parse("3.6") 33 | CMS_34 = version.parse("3.4") <= version.parse(cms.__version__) < version.parse("3.5") 34 | CMS_33 = version.parse("3.3") <= version.parse(cms.__version__) < version.parse("3.4") 35 | CMS_32 = version.parse("3.2") <= version.parse(cms.__version__) < version.parse("3.3") 36 | CMS_31 = version.parse("3.1") <= version.parse(cms.__version__) < version.parse("3.2") 37 | CMS_30 = version.parse("3.0") <= version.parse(cms.__version__) < version.parse("3.1") 38 | except ImportError: # pragma: no cover 39 | CMS = False 40 | CMS_42 = False 41 | CMS_41 = False 42 | CMS_40 = False 43 | CMS_311 = False 44 | CMS_310 = False 45 | CMS_39 = False 46 | CMS_38 = False 47 | CMS_37 = False 48 | CMS_36 = False 49 | CMS_35 = False 50 | CMS_34 = False 51 | CMS_33 = False 52 | CMS_32 = False 53 | CMS_31 = False 54 | CMS_30 = False 55 | 56 | DJANGO_2_2 = version.parse("2.2") <= version.parse(django.get_version()) < version.parse("3.0") 57 | DJANGO_3_0 = version.parse("3.0") <= version.parse(django.get_version()) < version.parse("3.1") 58 | DJANGO_3_1 = version.parse("3.1") <= version.parse(django.get_version()) < version.parse("3.2") 59 | DJANGO_3_2 = version.parse("3.2") <= version.parse(django.get_version()) < version.parse("4.0") 60 | DJANGO_4_0 = version.parse("4.0") <= version.parse(django.get_version()) < version.parse("4.1") 61 | DJANGO_4_1 = version.parse("4.1") <= version.parse(django.get_version()) < version.parse("4.2") 62 | DJANGO_4_2 = version.parse("4.2") <= version.parse(django.get_version()) < version.parse("5.0") 63 | DJANGO_5_0 = version.parse("5.0") <= version.parse(django.get_version()) < version.parse("5.1") 64 | DJANGO_5_1 = version.parse("5.1") <= version.parse(django.get_version()) < version.parse("5.2") 65 | DJANGO_5_2 = version.parse("5.2") <= version.parse(django.get_version()) < version.parse("6.0") 66 | 67 | 68 | def load_from_file(module_path): 69 | """ 70 | Load a python module from its absolute filesystem path 71 | 72 | Borrowed from django-cms 73 | """ 74 | imported = None 75 | if module_path: 76 | try: 77 | from importlib.machinery import SourceFileLoader 78 | 79 | imported = SourceFileLoader("mod", module_path).load_module() 80 | except ImportError: # pragma: no cover 81 | from imp import PY_SOURCE, load_module 82 | 83 | with open(module_path) as openfile: 84 | imported = load_module("mod", openfile, module_path, ("imported", "r", PY_SOURCE)) 85 | return imported 86 | 87 | 88 | @contextlib.contextmanager 89 | def work_in(dirname=None): 90 | """ 91 | Context manager version of os.chdir. When exited, returns to the working 92 | directory prior to entering. 93 | 94 | Grabbed from cookiecutter, thanks audreyr! 95 | """ 96 | curdir = os.getcwd() 97 | try: 98 | if dirname is not None: 99 | if dirname not in sys.path: 100 | sys.path.insert(0, dirname) 101 | os.chdir(dirname) 102 | yield 103 | finally: 104 | os.chdir(curdir) 105 | 106 | 107 | @contextlib.contextmanager 108 | def captured_output(): 109 | with patch("sys.stdout", new_callable=six.StringIO) as out: 110 | with patch("sys.stderr", new_callable=six.StringIO) as err: 111 | yield out, err 112 | 113 | 114 | # Borrowed from django CMS codebase 115 | @contextlib.contextmanager 116 | def temp_dir(suffix="", container="/dev/shm/"): 117 | name = make_temp_dir(suffix, container) 118 | yield name 119 | shutil.rmtree(name) 120 | 121 | 122 | def make_temp_dir(suffix="", container="/dev/shm/"): 123 | if os.path.exists(container): 124 | return mkdtemp(suffix=suffix, dir=container) 125 | return mkdtemp(suffix=suffix) 126 | 127 | 128 | @contextlib.contextmanager 129 | def persistent_dir(suffix, container="data"): 130 | name = os.path.abspath(os.path.join(container, suffix)) 131 | if not os.path.exists(name): 132 | os.makedirs(name) 133 | yield name 134 | 135 | 136 | class DisableMigrations: 137 | def __contains__(self, item): # pragma: no cover 138 | return True 139 | 140 | def __getitem__(self, item): # pragma: no cover 141 | return None 142 | 143 | 144 | def _reset_django(settings): 145 | """ 146 | Hackish way to reset the django instance settings and AppConfig 147 | :param settings: django settings module 148 | """ 149 | if settings._wrapped != empty: 150 | clear_url_caches() 151 | from django.apps import apps 152 | 153 | apps.clear_cache() 154 | settings._wrapped = empty 155 | clear_url_caches() 156 | 157 | 158 | def _make_settings(args, application, settings, STATIC_ROOT, MEDIA_ROOT): # NOQA 159 | """ 160 | Setup the Django settings 161 | :param args: docopt arguments 162 | :param application: application module name 163 | :param settings: Django settings module 164 | :param STATIC_ROOT: static root directory 165 | :param MEDIA_ROOT: media root directory 166 | :return: 167 | """ 168 | import dj_database_url 169 | 170 | from .default_settings import get_default_settings 171 | 172 | try: 173 | extra_settings_file = args.get("--extra-settings") 174 | if not extra_settings_file: 175 | extra_settings_file = HELPER_FILE 176 | if extra_settings_file[-3:] != ".py": # pragma: no cover 177 | filename, __ = os.path.splitext(extra_settings_file) 178 | extra_settings_file = "{}.py".format(filename) 179 | extra_settings = load_from_file(extra_settings_file).HELPER_SETTINGS 180 | except (OSError, AttributeError): 181 | extra_settings = None 182 | default_name = ":memory:" if args["test"] else "local.sqlite" 183 | db_url = os.environ.get("DATABASE_URL", "sqlite://localhost/%s" % default_name) 184 | configs = { 185 | "DATABASES": {"default": dj_database_url.parse(db_url)}, 186 | "STATIC_ROOT": STATIC_ROOT, 187 | "MEDIA_ROOT": MEDIA_ROOT, 188 | "USE_TZ": True, 189 | "USE_CMS": args["--cms"], 190 | "BASE_APPLICATION": application, 191 | } 192 | 193 | if configs["USE_CMS"] or getattr(extra_settings, "USE_CMS", False): 194 | CMS_APPS = [ # NOQA 195 | "cms", 196 | "menus", 197 | "sekizai", 198 | ] 199 | CMS_APP_STYLE = ["djangocms_admin_style"] # NOQA 200 | CMS_PROCESSORS = [ # NOQA 201 | "cms.context_processors.cms_settings", 202 | "sekizai.context_processors.sekizai", 203 | ] 204 | CMS_MIDDLEWARE = [ # NOQA 205 | "cms.middleware.language.LanguageCookieMiddleware", 206 | "cms.middleware.user.CurrentUserMiddleware", 207 | "cms.middleware.page.CurrentPageMiddleware", 208 | "cms.middleware.toolbar.ToolbarMiddleware", 209 | ] 210 | if args["server"]: 211 | CMS_MIDDLEWARE.append("cms.middleware.utils.ApphookReloadMiddleware") 212 | URLCONF = "app_helper.urls" # NOQA 213 | else: 214 | CMS_APPS = [] # NOQA 215 | CMS_APP_STYLE = [] # NOQA 216 | CMS_MIDDLEWARE = [] # NOQA 217 | CMS_PROCESSORS = [] # NOQA 218 | URLCONF = "app_helper.urls" # NOQA 219 | 220 | default_settings = get_default_settings( 221 | CMS_APPS, 222 | CMS_PROCESSORS, 223 | CMS_MIDDLEWARE, 224 | CMS_APP_STYLE, 225 | URLCONF, 226 | application, 227 | ) 228 | migrate = args.get("--migrate") or not args.get("--no-migrate") 229 | default_settings.update(configs) 230 | 231 | if extra_settings: 232 | apps = extra_settings.pop("INSTALLED_APPS", []) 233 | apps_top = extra_settings.pop("TOP_INSTALLED_APPS", []) 234 | template_processors = extra_settings.pop("TEMPLATE_CONTEXT_PROCESSORS", []) 235 | template_loaders = extra_settings.pop("TEMPLATE_LOADERS", []) 236 | template_dirs = extra_settings.pop("TEMPLATE_DIRS", []) 237 | middleware = extra_settings.pop("MIDDLEWARE_CLASSES", []) 238 | middleware_top = extra_settings.pop("TOP_MIDDLEWARE_CLASSES", []) 239 | default_settings.update(extra_settings) 240 | for app in apps_top: 241 | default_settings["INSTALLED_APPS"].insert(0, app) 242 | default_settings["INSTALLED_APPS"].extend(apps) 243 | default_settings["TEMPLATE_CONTEXT_PROCESSORS"].extend(template_processors) 244 | default_settings["TEMPLATE_LOADERS"].extend(template_loaders) 245 | if "TEMPLATE_DIRS" not in default_settings: 246 | default_settings["TEMPLATE_DIRS"] = [] 247 | default_settings["TEMPLATE_DIRS"].extend(template_dirs) 248 | default_settings["MIDDLEWARE_CLASSES"].extend(middleware) 249 | for middleware in middleware_top: 250 | default_settings["MIDDLEWARE_CLASSES"].insert(0, middleware) 251 | 252 | if "cms" in default_settings["INSTALLED_APPS"]: 253 | if "treebeard" not in default_settings["INSTALLED_APPS"]: 254 | default_settings["INSTALLED_APPS"].append("treebeard") 255 | if "filer" in default_settings["INSTALLED_APPS"] and "mptt" not in default_settings["INSTALLED_APPS"]: 256 | from filer import __version__ as filer_version 257 | 258 | if filer_version < "3": 259 | # As of django-filer 3.0 mptt is not needed as a dependency 260 | default_settings["INSTALLED_APPS"].append("mptt") 261 | if "filer" in default_settings["INSTALLED_APPS"] and "easy_thumbnails" not in default_settings["INSTALLED_APPS"]: 262 | default_settings["INSTALLED_APPS"].append("easy_thumbnails") 263 | 264 | default_settings["TEMPLATES"] = [ 265 | { 266 | "NAME": "django", 267 | "BACKEND": "django.template.backends.django.DjangoTemplates", 268 | "OPTIONS": { 269 | "context_processors": [ 270 | template_processor.replace("django.core", "django.template") 271 | for template_processor in default_settings.pop("TEMPLATE_CONTEXT_PROCESSORS") 272 | ], 273 | "loaders": default_settings.pop("TEMPLATE_LOADERS"), 274 | }, 275 | }, 276 | ] 277 | if "TEMPLATE_DIRS" in default_settings: 278 | default_settings["TEMPLATES"][0]["DIRS"] = default_settings.pop("TEMPLATE_DIRS") 279 | 280 | # Support for custom user models 281 | if "AUTH_USER_MODEL" in os.environ: 282 | custom_user_app = os.environ["AUTH_USER_MODEL"].rpartition(".")[0] 283 | custom_user_model = ".".join(os.environ["AUTH_USER_MODEL"].split(".")[-2:]) 284 | if "cms" in default_settings["INSTALLED_APPS"]: 285 | default_settings["INSTALLED_APPS"].insert(default_settings["INSTALLED_APPS"].index("cms"), custom_user_app) 286 | else: 287 | default_settings["INSTALLED_APPS"].insert( 288 | default_settings["INSTALLED_APPS"].index("django.contrib.auth") + 1, 289 | custom_user_app, 290 | ) 291 | default_settings["AUTH_USER_MODEL"] = custom_user_model 292 | 293 | if args["test"]: 294 | default_settings["SESSION_ENGINE"] = "django.contrib.sessions.backends.cache" 295 | if application not in default_settings["INSTALLED_APPS"]: 296 | default_settings["INSTALLED_APPS"].append(application) 297 | 298 | if not migrate: 299 | default_settings["MIGRATION_MODULES"] = DisableMigrations() 300 | 301 | if "MIDDLEWARE" not in default_settings: 302 | default_settings["MIDDLEWARE"] = default_settings["MIDDLEWARE_CLASSES"] 303 | del default_settings["MIDDLEWARE_CLASSES"] 304 | if not default_settings.get("SECRET_KEY", None): 305 | default_settings["SECRET_KEY"] = "".join(random.choice(string.ascii_lowercase) for i in range(32)) 306 | default_settings["DEFAULT_AUTO_FIELD"] = "django.db.models.BigAutoField" 307 | 308 | _reset_django(settings) 309 | settings.configure(**default_settings) 310 | django.setup() 311 | reload_urls(settings, cms_apps=False) 312 | return settings 313 | 314 | 315 | def reload_urls(settings, urlconf=None, cms_apps=True): 316 | if "cms.urls" in sys.modules: 317 | six.moves.reload_module(sys.modules["cms.urls"]) 318 | if urlconf is None: 319 | urlconf = settings.ROOT_URLCONF 320 | if urlconf in sys.modules: 321 | six.moves.reload_module(sys.modules[urlconf]) 322 | clear_url_caches() 323 | if cms_apps: 324 | from cms.appresolver import clear_app_resolvers, get_app_patterns 325 | 326 | clear_app_resolvers() 327 | get_app_patterns() 328 | 329 | 330 | def _create_db(migrate_cmd=False): 331 | call_command("migrate") 332 | 333 | 334 | def get_user_model(): 335 | from django.contrib.auth import get_user_model 336 | 337 | return get_user_model() 338 | 339 | 340 | def create_user( 341 | username, 342 | email, 343 | password, 344 | is_staff=False, 345 | is_superuser=False, 346 | base_cms_permissions=False, 347 | permissions=None, 348 | ): 349 | from django.contrib.auth.models import Permission 350 | 351 | User = get_user_model() # NOQA 352 | 353 | try: 354 | if User.USERNAME_FIELD == "email": 355 | user = User.objects.get(**{User.USERNAME_FIELD: email}) 356 | else: 357 | user = User.objects.get(**{User.USERNAME_FIELD: username}) 358 | except User.DoesNotExist: 359 | user = User() 360 | 361 | if User.USERNAME_FIELD != "email": 362 | setattr(user, User.USERNAME_FIELD, username) 363 | 364 | try: 365 | user.email = email 366 | except AttributeError: 367 | pass 368 | user.set_password(password) 369 | if is_superuser: 370 | user.is_superuser = True 371 | if is_superuser or is_staff: 372 | user.is_staff = True 373 | user.is_active = True 374 | user.save() 375 | if user.is_staff and not is_superuser and base_cms_permissions: 376 | user.user_permissions.add(Permission.objects.get(codename="add_text")) 377 | user.user_permissions.add(Permission.objects.get(codename="delete_text")) 378 | user.user_permissions.add(Permission.objects.get(codename="change_text")) 379 | user.user_permissions.add(Permission.objects.get(codename="publish_page")) 380 | 381 | user.user_permissions.add(Permission.objects.get(codename="add_page")) 382 | user.user_permissions.add(Permission.objects.get(codename="change_page")) 383 | user.user_permissions.add(Permission.objects.get(codename="delete_page")) 384 | if is_staff and not is_superuser and permissions: 385 | for permission in permissions: 386 | user.user_permissions.add(Permission.objects.get(codename=permission)) 387 | return user 388 | 389 | 390 | def get_user_model_labels(): 391 | User = get_user_model() # NOQA 392 | 393 | user_orm_label = "{}.{}".format(User._meta.app_label, User._meta.object_name) 394 | user_model_label = "{}.{}".format(User._meta.app_label, User._meta.model_name) 395 | return user_orm_label, user_model_label 396 | 397 | 398 | class UserLoginContext: 399 | def __init__(self, testcase, user, password=None): 400 | self.testcase = testcase 401 | self.user = user 402 | if password is None: 403 | password = getattr(user, get_user_model().USERNAME_FIELD) 404 | self.password = password 405 | 406 | def __enter__(self): 407 | loginok = self.testcase.client.login( 408 | username=getattr(self.user, get_user_model().USERNAME_FIELD), 409 | password=self.password, 410 | ) 411 | self.testcase.assertTrue(loginok) 412 | self.testcase._login_context = self 413 | 414 | def __exit__(self, exc, value, tb): 415 | self.testcase._login_context = None 416 | self.testcase.client.logout() 417 | 418 | 419 | def ensure_unicoded_and_unique(args_list, application): 420 | """ 421 | Iterate over args_list, make it unicode if needed and ensure that there 422 | are no duplicates. 423 | Returns list of unicoded arguments in the same order. 424 | """ 425 | unicoded_args = [] 426 | for argument in args_list: 427 | argument = argument if not isinstance(argument, str) else argument 428 | if argument not in unicoded_args or argument == application: 429 | unicoded_args.append(argument) 430 | return unicoded_args 431 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------