pax_global_header00006660000000000000000000000064150012016130014477gustar00rootroot0000000000000052 comment=69e1aa1b6c00b45ccc9c755b0dca5cfad8c73fb4 django-modeltranslation-0.19.14/000077500000000000000000000000001500120161300164725ustar00rootroot00000000000000django-modeltranslation-0.19.14/.github/000077500000000000000000000000001500120161300200325ustar00rootroot00000000000000django-modeltranslation-0.19.14/.github/workflows/000077500000000000000000000000001500120161300220675ustar00rootroot00000000000000django-modeltranslation-0.19.14/.github/workflows/release.yml000066400000000000000000000007771500120161300242450ustar00rootroot00000000000000name: Release on: push: tags: - v* env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: Release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: astral-sh/setup-uv@v5 with: python-version: "3.10" - name: Build artifacts run: | uvx --from build python -m build --installer uv - name: Create release uses: softprops/action-gh-release@v2 with: files: dist/* django-modeltranslation-0.19.14/.github/workflows/test.yml000066400000000000000000000061331500120161300235740ustar00rootroot00000000000000# Docs: # - https://docs.github.com/en/actions/guides/about-service-containers # Example # - https://github.com/actions/example-services/blob/main/.github/workflows/postgres-service.yml name: CI on: push: branches: - master pull_request: jobs: Check: runs-on: ubuntu-latest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} strategy: matrix: python: ["3.10"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} uses: astral-sh/setup-uv@v5 with: enable-cache: true python-version: ${{ matrix.python }} - name: Install deps run: uv sync --group dev - name: Run linters run: | source .venv/bin/activate make lint - name: Run type checking run: | source .venv/bin/activate make typecheck - name: Test package install run: | python -m build --installer uv pip install dist/*.whl pip install dist/*.tar.gz Test: runs-on: ubuntu-latest strategy: matrix: python: ["3.9", "3.10", "3.11", "3.12", "3.13"] django: ["4.2", "5.0", "5.1", "5.2"] database: ["sqlite", "postgres", "mysql"] exclude: - python: 3.9 django: 5.0 - python: 3.9 django: 5.1 - python: 3.9 django: 5.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DJANGO: ${{ matrix.django }} DB: ${{ matrix.database }} DB_HOST: 127.0.0.1 MYSQL_DATABASE: "modeltranslation" MYSQL_USER: "root" MYSQL_PASSWORD: "password" POSTGRES_DB: "modeltranslation" POSTGRES_USER: "modeltranslation" POSTGRES_PASSWORD: "modeltranslation" services: mariadb: image: mariadb:10 ports: - 3306:3306 env: MYSQL_DATABASE: "modeltranslation" MYSQL_USER: "modeltranslation" MYSQL_PASSWORD: "password" MYSQL_ROOT_PASSWORD: "password" options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 postgres: image: postgres ports: - 5432:5432 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 env: POSTGRES_DB: "modeltranslation" POSTGRES_USER: "modeltranslation" POSTGRES_PASSWORD: "modeltranslation" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} uses: astral-sh/setup-uv@v5 with: python-version: ${{ matrix.python }} - name: Set up env run: | if [[ $DB == mysql ]]; then uv pip install -q mysqlclient fi if [[ $DB == postgres ]]; then uv pip install -q psycopg2-binary fi uv pip install django_stubs_ext typing-extensions coverage pytest pytest-django pytest-cov parameterized $(./get-django-version.py ${{ matrix.django }}) - name: Run tests run: | pytest --cov-report term django-modeltranslation-0.19.14/.gitignore000066400000000000000000000005031500120161300204600ustar00rootroot00000000000000*.py[cod] __pycache__ # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 MANIFEST # Installer logs pip-log.txt # Development pipfiles and venv Pipfile Pipfile.lock .venv # Unit test / coverage reports .coverage .tox nosetests.xml # Sphinx _build django-modeltranslation-0.19.14/.versionrc.json000066400000000000000000000002671500120161300214620ustar00rootroot00000000000000{ "host": "github.com", "owner": "deschler", "repository": "django-modeltranslation", "bumpFiles": [ { "filename": "VERSION", "type": "plain-text" } ] } django-modeltranslation-0.19.14/AUTHORS.rst000066400000000000000000000020311500120161300203450ustar00rootroot00000000000000Authors ======= Core Committers --------------- * Peter Eschler (retired) * Dirk Eschler * Jacek Tomaszewski Contributors ------------ * Carl J. Meyer * Jaap Roes * Bojan Mihelac * Sébastien Fievet * Bruno Tavares * Zach Mathew (of django-linguo_, initial author of ``MultilingualManager``) * Mihai Sucan * Benoît Bryon * Wojtek Ruszczewski * Chris Adams * Dominique Lederer * Braden MacDonald * Karol Fuksiewicz * Konrad Wojas * Bas Peschier * Oleg Prans * Francesc Arpí Roca * Mathieu Leplatre * Thom Wiggers * Warnar Boekkooi * Alex Marandon * Fabio Caccamo * Vladimir Sinitsin * Luca Corti * Morgan Aubert * Mathias Ettinger * Daniel Loeb * Stephen McDonald * Lukas Lundgren * zenoamaro * oliphunt * Venelin Stoykov * Stratos Moros * Benjamin Toueg * Emilie Zawadzki * Virgílio N Santos * PetrDlouhy * dmarcelino * GreyZmeem * Hugo Defrance * And many more ... (if you miss your name here, please let us know!) .. _django-linguo: https://github.com/zmathew/django-linguo django-modeltranslation-0.19.14/CHANGELOG.md000066400000000000000000001277411500120161300203170ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. ## [0.19.14](https://github.com/deschler/django-modeltranslation/compare/v0.19.13...v0.19.14) (2025-04-20) ### Features * **dev:** Migrate project to uv ([#783](https://github.com/deschler/django-modeltranslation/issues/783)) ([b188f37](https://github.com/deschler/django-modeltranslation/commit/b188f37aaed6b4aa7bc9dd63201f3176bba4bbfc)) ### Bug Fixes * Fix duplicate constraint generation ([#789](https://github.com/deschler/django-modeltranslation/issues/789)) ([236ddec](https://github.com/deschler/django-modeltranslation/commit/236ddec46531f29a457a1b9f810323d5262d3835)), closes [#785](https://github.com/deschler/django-modeltranslation/issues/785) ### [0.19.13](https://github.com/deschler/django-modeltranslation/compare/v0.19.12...v0.19.13) (2025-03-22) ### Features * Add another test model and set up django-admin ([063eb16](https://github.com/deschler/django-modeltranslation/commit/063eb16f544b3e273a2cf7478bb8983e6b4942c4)) ### Bug Fixes * Add add unique constraints on all translated fields ([2c0ae62](https://github.com/deschler/django-modeltranslation/commit/2c0ae623055287590b5a727da522dde30cd26566)), closes [#261](https://github.com/deschler/django-modeltranslation/issues/261) ### [0.19.12](https://github.com/deschler/django-modeltranslation/compare/v0.19.11...v0.19.12) (2024-12-26) ### Bug Fixes * Preserve `blank` value for optional fields ([f197adc](https://github.com/deschler/django-modeltranslation/commit/f197adc6674131dcc23aea0d0d4a8704d8e4cb7c)) ### [0.19.11](https://github.com/deschler/django-modeltranslation/compare/v0.19.10...v0.19.11) (2024-11-11) ### Features * Support django 5.1 ([#756](https://github.com/deschler/django-modeltranslation/issues/756)) ([7a60ca1](https://github.com/deschler/django-modeltranslation/commit/7a60ca10955a452448f0a34981713c6c632ca1db)), closes [#719](https://github.com/deschler/django-modeltranslation/issues/719) ### [0.19.10](https://github.com/deschler/django-modeltranslation/compare/v0.19.9...v0.19.10) (2024-10-17) ### Bug Fixes * Fix indonesian language support ([a1106d1](https://github.com/deschler/django-modeltranslation/commit/a1106d1fe1a4484fd352e4b17ddebad55c3ecbcc)), closes [#763](https://github.com/deschler/django-modeltranslation/issues/763) ### [0.19.9](https://github.com/deschler/django-modeltranslation/compare/v0.19.8...v0.19.9) (2024-09-05) ### [0.19.8](https://github.com/deschler/django-modeltranslation/compare/v0.19.7...v0.19.8) (2024-09-04) ### Bug Fixes * Fix django-cms compatibility ([d420b6a](https://github.com/deschler/django-modeltranslation/commit/d420b6a9db87e133a34d0462af4c699b6debed96)), closes [#748](https://github.com/deschler/django-modeltranslation/issues/748) * Fix type error for Python 3.8 ([#754](https://github.com/deschler/django-modeltranslation/issues/754)) ([5cc37c2](https://github.com/deschler/django-modeltranslation/commit/5cc37c256f377f918e3f4b788b900a700a5f22db)), closes [#753](https://github.com/deschler/django-modeltranslation/issues/753) ### [0.19.7](https://github.com/deschler/django-modeltranslation/compare/v0.19.6...v0.19.7) (2024-08-12) ### Features * add changelog to project urls in package metadata ([#752](https://github.com/deschler/django-modeltranslation/issues/752)) ([303c947](https://github.com/deschler/django-modeltranslation/commit/303c947930a3348bf4037617677dc89fb157e1e2)) ### [0.19.6](https://github.com/deschler/django-modeltranslation/compare/v0.19.5...v0.19.6) (2024-08-07) ### Bug Fixes * Support multiple translation fields in `get_translation_fields` ([56c5784](https://github.com/deschler/django-modeltranslation/commit/56c578400fd6bd29bd8b088bc3e5ba9f6b4fa9a4)) ### [0.19.5](https://github.com/deschler/django-modeltranslation/compare/v0.19.4...v0.19.5) (2024-07-05) ### Bug Fixes * **types:** Use Union instead of | for some types ([13af637](https://github.com/deschler/django-modeltranslation/commit/13af637d87dc9eca2775b46bf2b04da7e741c805)), closes [#744](https://github.com/deschler/django-modeltranslation/issues/744) ### [0.19.4](https://github.com/deschler/django-modeltranslation/compare/v0.19.3...v0.19.4) (2024-06-20) ### Features * Add global `MODELTRANSLATION_REQUIRED_LANGUAGES` setting ([0bbdb5f](https://github.com/deschler/django-modeltranslation/commit/0bbdb5fe8fa053de2bc54d31b668b3621a9dda78)), closes [#743](https://github.com/deschler/django-modeltranslation/issues/743) ### [0.19.3](https://github.com/deschler/django-modeltranslation/compare/v0.19.2...v0.19.3) (2024-06-01) ### Bug Fixes * **types:** Make admin classes generic as their super classes ([#737](https://github.com/deschler/django-modeltranslation/issues/737)) ([d2c16fe](https://github.com/deschler/django-modeltranslation/commit/d2c16feba9d9f00f16f9406e2a466cd0cc832433)) ### Breaking changes * Dropped support for python 3.8 and removed it from CI ### [0.19.2](https://github.com/deschler/django-modeltranslation/compare/v0.19.1...v0.19.2) (2024-05-27) ### [0.19.1](https://github.com/deschler/django-modeltranslation/compare/v0.19.0...v0.19.1) (2024-05-27) ### Bug Fixes * Removed protocol from admin javascript links. ([ed8f2bc](https://github.com/deschler/django-modeltranslation/commit/ed8f2bcf747435e242ce5e0b01287b5162d59476)), closes [#740](https://github.com/deschler/django-modeltranslation/issues/740) ## [0.19.0](https://github.com/deschler/django-modeltranslation/compare/v0.18.13...v0.19.0) (2024-05-26) ### ⚠ BREAKING CHANGES * **types:** Rename `fields` (dict with set of TranslationField) to `all_fields`, on the TranslationOptions instance. ### Features * Support F and Concat expressions in annotate() ([a0aeb58](https://github.com/deschler/django-modeltranslation/commit/a0aeb58b470d7b0607bf7e3a4e9dd49e1862dcc3)), closes [#735](https://github.com/deschler/django-modeltranslation/issues/735) ### Bug Fixes * **types:** Export public variables ([47f8083](https://github.com/deschler/django-modeltranslation/commit/47f80835764be1607ec7463b55c7de8496bc0152)) * **types:** Fix `fields` type ([#739](https://github.com/deschler/django-modeltranslation/issues/739)) ([b97c22c](https://github.com/deschler/django-modeltranslation/commit/b97c22c197686379be5d6237cfd61a92c10aefb5)) ### [0.18.13](https://github.com/deschler/django-modeltranslation/compare/v0.18.13-beta1.1...v0.18.13) (2024-05-17) ### Features * Add build_lang helper in utils ([bdee9ff](https://github.com/deschler/django-modeltranslation/commit/bdee9ff5b906f682cfc8c4a774074a8b2aacf463)) * Add types ([a9e95e8](https://github.com/deschler/django-modeltranslation/commit/a9e95e8c78550aba70712e524fb289b87bdf1b48)), closes [#716](https://github.com/deschler/django-modeltranslation/issues/716) ### Bug Fixes * Remove deprecated test config starting from Django 5.0 ([b016af5](https://github.com/deschler/django-modeltranslation/commit/b016af5d4a2bdb9a0dfebf1492d6997f2aa9861d)) ### [0.18.13-beta1.1](https://github.com/deschler/django-modeltranslation/compare/v0.18.13-beta.0...v0.18.13-beta1.1) (2023-11-17) ### Bug Fixes * Fixed bug in tabbed_translation_fields.js ([641fbe8](https://github.com/deschler/django-modeltranslation/commit/641fbe89ab674c03dcb41f584e7bb569e3c141a9)), closes [#597](https://github.com/deschler/django-modeltranslation/issues/597) * **ci:** Replace flake8 with ruff ([2061f6c](https://github.com/deschler/django-modeltranslation/commit/2061f6c264d7cb889ae14d2d52a7547df6d58663)) ### [0.18.13-beta.0](https://github.com/deschler/django-modeltranslation/compare/v0.18.13-beta1.0...v0.18.13-beta.0) (2023-09-13) ### [0.18.13-beta1.0](https://github.com/deschler/django-modeltranslation/compare/v0.18.12...v0.18.13-beta1.0) (2023-09-13) ### Bug Fixes * Apply force_str only to Promise ([e7640c7](https://github.com/deschler/django-modeltranslation/commit/e7640c71197f3c7b34386847c746663123fad07b)), closes [#701](https://github.com/deschler/django-modeltranslation/issues/701) ### [0.18.12](https://github.com/deschler/django-modeltranslation/compare/v0.18.11...v0.18.12) (2023-09-08) ### Features * Support language-specific field defaults ([2657de7](https://github.com/deschler/django-modeltranslation/commit/2657de7c2ebd6523a31ab04ba9453c715b0c34f3)), closes [#700](https://github.com/deschler/django-modeltranslation/issues/700) [#698](https://github.com/deschler/django-modeltranslation/issues/698) ### [0.18.11](https://github.com/deschler/django-modeltranslation/compare/v0.18.10...v0.18.11) (2023-07-16) ### Features * extend update_fields with translation fields in Model.save() ([#687](https://github.com/deschler/django-modeltranslation/issues/687)) ([d86c6de](https://github.com/deschler/django-modeltranslation/commit/d86c6defc864b3493955a41f95a85fc5aa8d5649)) ### [0.18.10](https://github.com/deschler/django-modeltranslation/compare/v0.18.10-beta.0...v0.18.10) (2023-06-02) ### Bug Fixes * Add support for JSONField ([25f7305](https://github.com/deschler/django-modeltranslation/commit/25f73058f5f176a61c5368b7aee563874309687e)), closes [#685](https://github.com/deschler/django-modeltranslation/issues/685) ### [0.18.10-beta.1](https://github.com/deschler/django-modeltranslation/compare/v0.18.10-beta.0...v0.18.10-beta.1) (2023-06-02) ### Bug Fixes * Add support for JSONField ([25f7305](https://github.com/deschler/django-modeltranslation/commit/25f73058f5f176a61c5368b7aee563874309687e)), closes [#685](https://github.com/deschler/django-modeltranslation/issues/685) ### [0.18.10-beta.1](https://github.com/deschler/django-modeltranslation/compare/v0.18.10-beta.0...v0.18.10-beta.1) (2023-06-02) ### [0.18.10-beta.0](https://github.com/deschler/django-modeltranslation/compare/v0.18.9...v0.18.10-beta.0) (2023-05-30) ### Bug Fixes * Fix update_or_create for Django 4.2 ([d5eefa8](https://github.com/deschler/django-modeltranslation/commit/d5eefa8bd193cd8aee1cd1f97561d2a7e9dc0801)), closes [#682](https://github.com/deschler/django-modeltranslation/issues/682) [#683](https://github.com/deschler/django-modeltranslation/issues/683) ### [0.18.9](https://github.com/deschler/django-modeltranslation/compare/v0.18.8...v0.18.9) (2023-02-09) ### Bug Fixes * Fix handling of expressions in `values()`/`values_list()` ([d65ff60](https://github.com/deschler/django-modeltranslation/commit/d65ff60007d4088b1f483edd2df85f407be3b5de)), closes [#670](https://github.com/deschler/django-modeltranslation/issues/670) ### [0.18.8](https://github.com/deschler/django-modeltranslation/compare/v0.18.8-beta.1...v0.18.8) (2023-02-01) ### [0.18.8-beta.1](https://github.com/deschler/django-modeltranslation/compare/v0.18.8-beta.0...v0.18.8-beta.1) (2023-01-27) ### Features * Add support for ManyToManyFields 🧑‍🤝‍🧑 ([#668](https://github.com/deschler/django-modeltranslation/issues/668)) ([f69e317](https://github.com/deschler/django-modeltranslation/commit/f69e3172bc6254a4ddd8def7500632d0046b30eb)) ### Bug Fixes * **docs:** Update documentation regarding inheritance ([#665](https://github.com/deschler/django-modeltranslation/issues/665)) ([ca31a21](https://github.com/deschler/django-modeltranslation/commit/ca31a21f014b04978188562a0e0e1b58d95923e6)), closes [#663](https://github.com/deschler/django-modeltranslation/issues/663) ### [0.18.8-beta.0](https://github.com/deschler/django-modeltranslation/compare/v0.18.7...v0.18.8-beta.0) (2022-11-22) ### Bug Fixes * Fix admin widget for fk fields ([#662](https://github.com/deschler/django-modeltranslation/issues/662)) ([fcfbd5c](https://github.com/deschler/django-modeltranslation/commit/fcfbd5ce059e4858a2c8d4803d094285282ad2c9)), closes [#660](https://github.com/deschler/django-modeltranslation/issues/660) ### [0.18.7](https://github.com/deschler/django-modeltranslation/compare/v0.18.6...v0.18.7) (2022-11-08) ### [0.18.6](https://github.com/deschler/django-modeltranslation/compare/v0.18.5...v0.18.6) (2022-11-07) ### Bug Fixes * Fix unexpected ordering after `values()`/`values_list()` followed by `order_by()`. ([09ce0e0](https://github.com/deschler/django-modeltranslation/commit/09ce0e076ba323432275e28eb16fdb19f37df3e0)), closes [#655](https://github.com/deschler/django-modeltranslation/issues/655) [#656](https://github.com/deschler/django-modeltranslation/issues/656) ### [0.18.5](https://github.com/deschler/django-modeltranslation/compare/v0.18.4...v0.18.5) (2022-10-12) ### Features * Support UserAdmin add_fieldsets ([d414cd3](https://github.com/deschler/django-modeltranslation/commit/d414cd3e0709622a66260088d2da0ade94a01be1)), closes [#654](https://github.com/deschler/django-modeltranslation/issues/654) ### Bug Fixes * Fix working in strict mode. ([#649](https://github.com/deschler/django-modeltranslation/issues/649)) ([8ef8afd](https://github.com/deschler/django-modeltranslation/commit/8ef8afd2d7aad71ba185f17c0db95494616f3730)) ### [0.18.4](https://github.com/deschler/django-modeltranslation/compare/v0.18.3...v0.18.4) (2022-07-22) ### Bug Fixes * Update django compatibility ([582b612](https://github.com/deschler/django-modeltranslation/commit/582b612ab5d422bf2cd1f45a28748db60819e85c)) ### [0.18.3](https://github.com/deschler/django-modeltranslation/compare/v0.18.3-beta.1...v0.18.3) (2022-07-19) ### Bug Fixes * Remove six (old compat layer for python2) ([86b67c2](https://github.com/deschler/django-modeltranslation/commit/86b67c271e5fcba94e396acc9efd5e52ced2d1e2)) ### [0.18.3-beta.1](https://github.com/deschler/django-modeltranslation/compare/v0.18.3-beta.0...v0.18.3-beta.1) (2022-07-13) ### Features * **dev:** Migrate to pytest ([d3e2396](https://github.com/deschler/django-modeltranslation/commit/d3e2396be6757f0d0b3ee4e06777c37f17d3834b)) ### [0.18.3-beta.0](https://github.com/deschler/django-modeltranslation/compare/v0.18.2...v0.18.3-beta.0) (2022-07-10) ### Features * Support `named` argument for `values_list` ([#644](https://github.com/deschler/django-modeltranslation/issues/644)) ([39bbe82](https://github.com/deschler/django-modeltranslation/commit/39bbe821b31278b21e0bf3528d036343338bb0f7)) ### [0.18.2](https://github.com/deschler/django-modeltranslation/compare/v0.18.1...v0.18.2) (2022-05-15) ### Features * Update test matrix; Drop python 3.6, add Python 3.10 ([#638](https://github.com/deschler/django-modeltranslation/issues/638)) ([29deb95](https://github.com/deschler/django-modeltranslation/commit/29deb95bf30c0e31c6a031f754677182cdd461a2)) ### [0.18.1](https://github.com/deschler/django-modeltranslation/compare/v0.18.0...v0.18.1) (2022-05-15) ### Bug Fixes * Fix install (included missing VERSION) ([ab66e8d](https://github.com/deschler/django-modeltranslation/commit/ab66e8d2f79c5e7e6f517e53a1698f5113d711bf)), closes [#637](https://github.com/deschler/django-modeltranslation/issues/637) ## [0.18.0](https://github.com/deschler/django-modeltranslation/compare/v0.17.7...v0.18.0) (2022-05-14) ### ⚠ BREAKING CHANGES * Replaced `VERSION` in tuple format by `__version__` as a string ### Bug Fixes * Add django version check for default_app_config ([79d2e08](https://github.com/deschler/django-modeltranslation/commit/79d2e089eff2f6bcfd150d3ac6e165bfefa475cb)) * Fix django version detect during install ([876f2e7](https://github.com/deschler/django-modeltranslation/commit/876f2e715804e5cba9f8dde0b8a75ff3179e908c)) * Store version as plain text file to simplify bumping ([#636](https://github.com/deschler/django-modeltranslation/issues/636)) ([6b4bb73](https://github.com/deschler/django-modeltranslation/commit/6b4bb733d971363c223d9d4ff307a0f9be131315)) ### [0.17.7](https://github.com/deschler/django-modeltranslation/compare/v0.17.6...v0.17.7) (2022-05-04) ### Bug Fixes * Do not include annotation fields when selecting specific fields ([#634](https://github.com/deschler/django-modeltranslation/issues/634)) ([defc37c](https://github.com/deschler/django-modeltranslation/commit/defc37c7a539dff1e4af96e7d13856519befe585)) ### [0.17.6](https://github.com/deschler/django-modeltranslation/compare/v0.17.5...v0.17.6) (2022-04-29) ### Bug Fixes * Preserve annotate() fields in queryset ([#633](https://github.com/deschler/django-modeltranslation/issues/633)) ([6f2688f](https://github.com/deschler/django-modeltranslation/commit/6f2688f52c56107da361c7c6197bcf38d8b99f42)) ### [0.17.5](https://github.com/deschler/django-modeltranslation/compare/v0.17.4...v0.17.5) (2022-01-30) ### [0.17.4](https://github.com/deschler/django-modeltranslation/compare/v0.17.3...v0.17.4) (2022-01-28) ### Features * semi-configurable selection of elements to generate tabs in admin ([#607](https://github.com/deschler/django-modeltranslation/issues/607)) ([eb05201](https://github.com/deschler/django-modeltranslation/commit/eb052018bf930146d667be3e47f26d69afb3c2c3)) ### [0.17.3](https://github.com/deschler/django-modeltranslation/compare/v0.17.2...v0.17.3) (2021-06-28) ### [0.17.2](https://github.com/deschler/django-modeltranslation/compare/v0.17.1...v0.17.2) (2021-05-31) ### Bug Fixes * **docs:** Fixed legacy python 2 print statements ([10ec4ed](https://github.com/deschler/django-modeltranslation/commit/10ec4ed8694d949815ccf4ada679a1cb72f24675)) * **MultilingualQuerySet:** Make _clone signature match default django _clone ([c65adb0](https://github.com/deschler/django-modeltranslation/commit/c65adb058d6c60c077138e5099342f31aac1690b)), closes [#483](https://github.com/deschler/django-modeltranslation/issues/483) ### [0.17.1](https://github.com/deschler/django-modeltranslation/compare/v0.16.2...v0.17.1) (2021-04-15) ### Bug Fixes * Fixed .latest() ORM method with django 3.2 ([eaf613b](https://github.com/deschler/django-modeltranslation/commit/eaf613be1733314ad3b639e1702b0f7423af7899)), closes [#591](https://github.com/deschler/django-modeltranslation/issues/591) ## [0.17.0](https://github.com/deschler/django-modeltranslation/compare/v0.16.2...v0.17.0) (2021-04-15) ### Features * Add Django 3.2 support ### [0.16.2](https://github.com/deschler/django-modeltranslation/compare/v0.16.1...v0.16.2) (2021-02-18) ### Bug Fixes * Fix loading for Inline Admin ([c8ea228](https://github.com/deschler/django-modeltranslation/commit/c8ea22877b3f4070ffb4d3d4e602d7ef09ab0860)) ### [0.16.1](https://github.com/deschler/django-modeltranslation/compare/v0.16.0...v0.16.1) (2020-11-23) ### Bug Fixes * missing jquery operator ([7c750de](https://github.com/deschler/django-modeltranslation/commit/7c750def728e163d5bde88fedd1124bd7e9a8122)) ## [0.16.0](https://github.com/deschler/django-modeltranslation/compare/v0.15.2...v0.16.0) (2020-10-12) ### ⚠ BREAKING CHANGES * **js:** It's 2020 already, drop backward compatibility with jquery-ui 1.10. ### Features * **tabbed-translation-fields:** Make tab with errors visible by default. ([4c2e284](https://github.com/deschler/django-modeltranslation/commit/4c2e284d871044a443817aabfbe3c956799ffe06)) ### Bug Fixes * Fix error detection; add red dot for tab with errors. ([9a93cf6](https://github.com/deschler/django-modeltranslation/commit/9a93cf6b4d4ec24e754159f71cf9d9eda811673e)) * **dev:** Fix install in editable mode. ([aaa2dcf](https://github.com/deschler/django-modeltranslation/commit/aaa2dcf5987e19c2da8460bc73a0681a291f0dc5)) * **js:** It's 2020 already, drop backward compatibility with jquery-ui 1.10. ([d8f432a](https://github.com/deschler/django-modeltranslation/commit/d8f432a5cadd60871101081c87569e3d390474e6)) ### [0.15.2](https://github.com/deschler/django-modeltranslation/compare/v0.15.1...v0.15.2) (2020-09-08) ### Features * Adds a language option to the update_translation_fields commands ([ac91740](https://github.com/deschler/django-modeltranslation/commit/ac91740a5c3d718b8695514da8a0dd7b90aa1ee6)), closes [#563](https://github.com/deschler/django-modeltranslation/issues/563) ### [0.15.1](https://github.com/deschler/django-modeltranslation/compare/v0.15.0...v0.15.1) (2020-07-10) ### Bug Fixes * **admin:** Fix custom widget initialization problem ([48e7f59](https://github.com/deschler/django-modeltranslation/commit/48e7f598955a09dc4130a0074cb953ecd05d1a01)) ## [0.15.0](https://github.com/deschler/django-modeltranslation/compare/0.14.4...0.15.0) (2020-04-22) ### Features * Use poetry as venv manager ([a5b402c](https://github.com/deschler/django-modeltranslation/commit/a5b402c51673a78a1aa160247746695070e08a2f)) * Drop old python versions (<3.6) * Drop old django versions (<2.2) ### Bug Fixes * add NewMultilingualManager __eq__() ([205a8f6](https://github.com/deschler/django-modeltranslation/commit/205a8f6c2f411b8b20235bbf89b88d3781919cbd)) ## 0.14.0 (2019-11-14) ### Bug Fixes * Django 3.0 support (#521) * Tests when django files not writable (#527) ## 0.13-3 (2019-07-22) ### Bug Fixes * Broken "Add another inline" (#475) ## 0.13-2 (2019-07-01) ### Bug Fixes * Outdated formfield_for_dbfield signature (#510) ## 0.13-1 (2019-04-18) * REMOVED: Python 3.5 from test matrix * REMOVED: Django 2.0 from test matrix * FIXED: TabbedTranslationAdmin in django 2.2 (#506) * ADDED: Django 2.2 to test matrix ## 0.13-0 (2019-02-21) * ADDED: Django 2.0 and 2.1 support * ADDED: Python 3.7 support * REMOVED: Python 3.4 from test matrix ## 0.13-beta3 (2019-02-17) * FIXED: Patching parent model managers on multi-table inheritance (#467) ## 0.13-beta2 (2019-02-13) * ADDED: Django 2.1 support * ADDED: Python 3.7 support * FIXED: JS errors in admin with new jQuery ## 0.13-beta1 (2018-04-16) * FIXED: Reverse relations and select_related for Django 2.0. (resolves issues #436 and #457, thanks to GreyZmeem and dmarcelino) * FIXED: Multiple fixes for Django 2.0. (resolves issues #436 and #451, thanks PetrDlouhy) * ADDED: Add primary support to DISTINCT statement (resolves issue #368, thanks Virgílio N Santos) * CHANGED: Check if 'descendants' list has values (resolves issue #445, thanks Emilie Zawadzki) ## 0.12.2 (2018-01-26) * FIXED: order_by with expression (resolves issue #398, thanks Benjamin Toueg) ## 0.12.1 (2017-04-05) * FIXED: Issue in loaddata management command in combination with Django 1.11. (resolves issue #401) ## 0.12 (2016-09-20) * ADDED: Support for Django 1.10. (resolves issue #360, thanks Jacek Tomaszewski and Primož Kerin) * CHANGED: Original field value became more unreliable and undetermined; please make sure you're not using it anywhere. See http://django-modeltranslation.readthedocs.io/en/latest/usage.html#the-state-of-the-original-field * CHANGED: Let register decorator return decorated class (resolves issue #360, thanks spacediver) * FIXED: Deferred classes signal connection. (resolves issue #379, thanks Jacek Tomaszewski) * FIXED: values_list + annotate combo bug. (resolves issue #374, thanks Jacek Tomaszewski) * FIXED: Several flake8 and travis related issues. (resolves issues #363, thanks Matthias K) ## 0.11 (2016-01-31) Released without changes. ## 0.11rc2 (2015-12-15) * FIXED: Custom manager in migrations. (resolves issues #330, #339 and #350, thanks Jacek Tomaszewski) ## 0.11rc1 (2015-12-07) * ADDED: Support for Django 1.9 (resolves issue #349, thanks Jacek Tomaszewski) ## 0.10.2 (2015-10-27) * FIXED: Proxy model inheritance for Django >=1.8 (resolves issues #304, thanks Stratos Moros) ## 0.10.1 (2015-09-04) * FIXED: FallbackValuesListQuerySet.iterator which broke ORM datetimes (resolves issue #324, thanks Venelin Stoykov) ## 0.10.0 (2015-07-03) * ADDED: CSS support for bi-directional languages to TranslationAdmin using mt-bidi class. (resolves issue #317, thanks oliphunt) * ADDED: A decorator to handle registration of models. (resolves issue #318, thanks zenoamaro) * FIXED: Handled annotation fields when using values_list. (resolves issue #321, thanks Lukas Lundgren) ## 0.9.1 (2015-05-14) * FIXED: Handled deprecation of _meta._fill_fields_cache() for Django 1.8 in add_translation_fields. (resolves issue #304, thanks Mathias Ettinger and Daniel Loeb) * FIXED: Handled deprecation of transaction.commit_unless_managed for Django 1.8 in sync_translation_fields management command. (resolves issue #310) * FIXED: Fixed translatable fields discovery with the new _meta API and generic relations for Django 1.8. (resolves issue #309, thanks Morgan Aubert) ## 0.9 (2015-04-16) * ADDED: Support for Django 1.8 and the new meta API. (resolves issue #299, thanks Luca Corti and Jacek Tomaszewski) ## 0.8.1 (2015-04-02) * FIXED: Using a queryset with select related. (resolves issue #298, thanks Vladimir Sinitsin) * FIXED: Added missing jquery browser plugin. (resolves issue #270, thanks Fabio Caccamo) * FIXED: Deprecated imports with Django >= 1.7 (resolves issue #283, thanks Alex Marandon) ## 0.8 (2014-10-06) * FIXED: JavaScript scoping issue with two jQuery versions in tabbed translation fields. (resolves issue #267, thanks Wojtek Ruszczewski) * ADDED: Patch db_column of translation fields in migration files. (resolves issue #264, thanks Thom Wiggers and Jacek Tomaszewski) * ADDED: Fallback to values and values_list. (resolves issue #258, thanks Jacek Tomaszewski) ## 0.8b2 (2014-07-18) * ADDED: Explicit support for Python 3.4 (should have already worked for older versions that supported Python 3). (resolves issue #254) * ADDED: Support for Django 1.7 migrations. * FIXED: Dict iteration Exception under Python 3. (resolves issue #256, thanks Jacek Tomaszewski) * FIXED: Reduce usage under Python 3. (thanks Jacek Tomaszewski) * FIXED: Support for AppConfigs in INSTALLED_APPS (resolves issue #252, thanks Warnar Boekkooi, Jacek Tomaszewski) * FIXED: Rewrite field names in select_related. Fix deffered models registry. Rewrite spanned queries on all levels for defer/only. (resolves issue #248, thanks Jacek Tomaszewski) ## 0.8b1 (2014-06-22) * ADDED: Detect custom get_queryset on managers. (resolves issue #242, thanks Jacek Tomaszewski) * ADDED: Support for Django 1.7 and the new app-loading refactor. (resolves issue #237) * ADDED: Added required_languages TranslationOptions (resolves issue #143) * FIXED: Fixed sync_translation_fields to be compatible with PostgreSQL. (resolves issue #247, thanks Jacek Tomaszewski) * FIXED: Manager .values() with no fields specified behaves as expected. (resolves issue #247) * FIXED: Fieldset headers are not capitalized when group_fieldsets is enabled. (resolves issue #234, thanks Jacek Tomaszewski) * FIXED: Exclude for nullable field manager rewriting. (resolves issue #231, thanks Jacek Tomaszewski) * FIXED: Use AVAILABLE_LANGUAGES in sync_translation_fields management command to detect missing fields. (resolves issue #227, thanks Mathieu Leplatre) * FIXED: Take db_column into account while syncing fields (resolves issue #225, thanks Mathieu Leplatre) * CHANGED: Moved to get_queryset, which resolves a deprecation warning. (resolves issue #244, thanks Thom Wiggers) * CHANGED: Considered iframes in tabbed_translation_fields.js to support third party apps like django-summernote. (resolves issue #229, thanks Francesc Arpí Roca) * CHANGED: Removed the http protocol from jquery-ui url in admin Media class. (resolves issue #224, thanks Francesc Arpí Roca) ## 0.7.3 (2014-01-05) * ADDED: Documentation for TranslationOptions fields reference and south/sync_translation_fields. * FIXED: Some python3 compatibility issues. (thanks Jacek Tomaszewski, resolves issue #220) * FIXED: Clearing translated FileFields does not work with easy_thumbnails. (thanks Jacek Tomaszewski, resolves issue #219) * FIXED: Compatibility with nested inlines. (thanks abstraktor, resolves issue #218) * FIXED: Admin inlines recursion problem in Django 1.6. (thanks Oleg Prans, resolves issue #214) * FIXED: Empty FileField handling. (thanks Jacek Tomaszewski, resolves issue #215) ## 0.7.2 (2013-11-11) * ADDED: Documentation about empty_values. (thanks Jacek Tomaszewski, resolves issue #211) * FIXED: Proxy model handling. (thanks Jacek Tomaszewsk) * FIXED: Abstract managers patching. (thanks Jacek Tomaszewski, resolves issue #212) ## 0.7.1 (2013-11-07) Packaged from revision f7c7ea174344f3dc0cf56ac3bf6e92878ed6baea * ADDED: Configurable formfields. The ultimate approach to nullable CharFields. (thanks Jacek Tomaszewski, resolves issue #211, ref #163, #187) * FIXED: Recursion problem with fieldset handling in Django 1.6. (thanks to Bas Peschier, resolves issue #214) ## 0.7 (2013-10-19) Packaged from revision 89f5e6712aaf5d5ec7e2d61940dc1a71fb08ca94 * ADDED: A setting to control which language are slug fields based on (thanks to Konrad Wojas, resolves issue #194) * ADDED: A noinput option to the sync_translation_fields management command. (thanks to cuchac, resolves issues #179 and #184) * ADDED: Support for Python 3.2 and 3.3. (thanks to Karol Fuksiewicz, resolves issue #174) * ADDED: Convenient admin classes which already contain proper Media definitions. (resolves issue #171) * ADDED: Only, defer, values, values_list, dates, raw_values methods to MultilingualManager. (resolves issue #166 adn #173) * ADDED: Support for ForeignKey and OneToOneField. (thanks to Braden MacDonald and Jacek Tomaszewski, resolves issue #161) * ADDED: An auto-population option to the loaddata command. (resolves issue #160) * ADDED: A MODELTRANSLATION_LOADDATA_RETAIN_LOCALE setting for loaddata command to leave locale alone. (resolves issue #151) * FIXED: Compatibility with Django 1.6 development version. (resolves issue #169) * FIXED: Handling of 3rd party apps' ModelForms. (resolves issue #167) * FIXED: Triggering field fallback on its default value rather than empty string only. Also enhance nullable fields in forms with proper widgets to preserve ``None``. (thanks to Wojtek Ruszczewski, resolves issue #163) * FIXED: Admin prepopulated_fields is now handled properly. (thanks to Rafleze, resolves issue #181 and #190) * FIXED: Form saving when translated field is excluded (e.g. in admin) (resolves issue #183) * FIXED: Multilingual clones are Multilingual too. (resolved issue #189) * CHANGED: Every model's manager is patched as MultiLingual, not only objects. (resolved issue #198) * CHANGED: Display "make null" checkboxes in model forms. * CHANGED: MODELTRANSLATION_DEBUG setting defaults to False instead of settings.DEBUG. * CHANGED: Drop support for Python 2.5 and Django 1.3. ## 0.6.1 (2013-03-17) Packaged from revision fc8a3034897b8b818c74f41c43a92001e536d970 * FIXED: Joined query does not use translated fields. (resolves issue #162) ## 0.6 (2013-03-01) Packaged from revision ea0e2db68900371146d39dcdf88b29091ee5222f * ADDED: A new ENABLE_FALLBACKS setting and a context manager for switching fallbacks temporarily. (thanks to Wojtek Ruszczewski, resolves issue #152) * ADDED: Major refactoring of the tabbed translation fields javascript. Adds support for tabular inlines and includes proper handling of stacked inlines, which have never been officially supported, but were not actively prevented from being tabbified. (resolves issue #66) * ADDED: New group_fieldsets option for TranslationAdmin. When activated translation fields and untranslated fields are automatically grouped into fieldsets. (based on original implementation by Chris Adams, resolves issues #38) * FIXED: Tests to run properly in the scope of a Django project. (thanks to Wojtek Ruszczewski, resolves issue #153) * FIXED: Broken tab activation when using jquery-ui 1.10, keeping support for older jquery-ui versions and the jquery version shipped by Django. (thanks to Dominique Lederer, resolves issue #146) * FIXED: Wrong admin field css class for en-us language. (resolves issue #141) * FIXED: Added missing hook for admin readonly_fields. (resolves issue #140) * FIXED: Keys used in tabbed translation fields to group translations are not unique for inlines. (resolves issue #121) * FIXED: The prepopulated_fields TranslationAdmin option only works on the first defined field to prepopulate from and made the option aware of the current language. (resolves issue #57) * CHANGED: Removed deprecated MODELTRANSLATION_TRANSLATION_REGISTRY setting. * CHANGED: Refactored auto population manager functionality. Switched to a populate method in favour of the old _populate keyword and added a new contextmanager to switch the population mode on demand. (thanks to Wojtek Ruszczewski, resolves issue #145) * CHANGED: Major refactoring of translation field inheritance and TranslationOptions. (thanks to Wojtek Ruszczewski, resolves issues #50 and #136) ## 0.5 (2013-02-10) Packaged from revision bedd18ea9e338b133d06f2ed5e7ebfc2e21fd276 * ADDED: Merged autodiscover tests from django-modeltranslation-wrapper. * ADDED: Rewrite method to MultilingualManager and optimized create. * FIXED: grouped_translations are computed twice in tabbed translations. (thanks to Wojtek Ruszczewski, resolves issue #135) * FIXED: CSS classes in tabbed translation fields when fieldname has a leading underscore. (thanks to Wojtek Ruszczewski, resolves issue #134) * FIXED: Rewriting of descending ('-' prefixed) ordering fields in MultilingualManager. (thanks to Wojtek Ruszczewski, resolves issue #133) * FIXED: Download url in setup.py. (thanks to Benoît Bryon, resolves issue #130) * FIXED: The update_translation_fields management command does nothing. (resolves issue #123) * FIXED: MultilingualQuerySet custom inheritance. * CHANGED: Don't raise an exception if TranslationField is accessed via class to allow descriptor introspection. (resolves issue #131) ## 0.5b1 (2013-01-07) Packaged from revision da928dd431fcf112e2e9c4c154c5b69e7dadc3b3. * ADDED: Possibility to turn off query rewriting in MultilingualManager. (thanks to Jacek Tomaszewski) * FIXED: Fixed update_translation_fields management command. (thanks to Jacek Tomaszewski, resolves issues #123 and #124) * CHANGED: Major test refactoring. (thanks to Jacek Tomaszewski, resolves issues #100 and #119) ## 0.5a1 (2012-12-05) Packaged from revision da4aeba0ea20ddbee67aa49bc90af507997ac386. * ADDED: Increased the number of supported fields. Essentially all Django model fields and subclasses of them should work, except related fields (ForeignKey, ManyToManyField, OneToOneField) and AutoField which are not supported. * ADDED: A subclass of TranslationOptions inherits fields from its bases. (thanks to Bruno Tavares and Jacek Tomaszewski, resolves issue #110) * ADDED: Support for fallback languages. Allows fine grained configuration through project settings and TranslationOptions on model basis. (thanks to Jacek Tomaszewski, resolves issue #104) * ADDED: Multilingual manager which is aware of the current language. (thanks to Jacek Tomaszewski, resolves issues #45, #78 and #84) * CHANGED: Version code to use a PEP386 compliant version number. * CHANGED: Constructor rewrites fields to be language aware. (thanks to Jacek Tomaszewski, resolves issues #33 and #58) * FIXED: Lacking support for readonly_fields in TranslationAdmin. (thanks to sbrandtb, resolves issue #111) * FIXED: Model's db_column option is not applied to the translation field. (resolves issue #83) * FIXED: Admin prevents saving a cleared field. The fix deactivates rule3 and implies the new language aware manager and constructor rewrite. (resolves issue #85) ## 0.4.1 (2012-11-13) Packaged from revision d9bf9709e9647fb2af51fc559bbe356010bd51ca. * FIXED: Pypi wants to install beta version. Happened because pypi treats 0.4.0-beta2 as latest release. This also effectively resulted in a downgrade when using 'pip --upgrade' and 0.4.0 was already installed. (thanks to jmagnusson for the report, resolves issue #103) ## 0.4.0 (2012-11-11) Packaged from revision c44f9cfee59f1b440f022422f917f247e16bbc6b. * CHANGED: Refactored tests to allow test runs with other apps. Includes a "backport" of override_settings to ensure Django 1.3 support. (thanks to Jacek Tomaszewski) * CHANGED: Modeltranslation related css class prefix to 'mt'. * FIXED: Race condition during initialization. (resolves issue #91) * FIXED: Tabs don't properly support two-part language codes. (resolves issue #63) ## 0.4.0-beta2 (2012-10-17) Packaged from revision 7b8cafbde7b14afc8e85235e9b087889a6bfa86e. * FIXED: Release doesn't include rst files. ## 0.4.0-beta1 (2012-10-17) Packaged from revision 09a0c4434a676c6fd753e6dcde95056c424db62e. * CHANGED: Refactored documentation using sphinx. (resolves issue #81) * FIXED: Setting MODELTRANSLATION_TRANSLATION_FILES should be optional. (resolves issue #86) ## 0.4.0-alpha1 (2012-10-12) Packaged from revision 170. * ADDED: Support for FileField and ImageField. (thanks to Bruno Tavares, resolves issue #30) * ADDED: New management command sync_database_fields to sync the database after a new model has been registered or a new language has been added. (thanks to Sébastien Fievet and the authors of django-transmeta, resolves issue #62) * CHANGED: Excluded tabular inlines from jQuery tabs, as they are currently not supported. * CHANGED: Use app-level translation files in favour of a single project-level one. Adds an autoregister feature similiar to the one provided by Django's admin. A new setting MODELTRANSLATION_TRANSLATION_FILES keeps backwards compatibility with older versions. See documentation for details. This is basically a merge from both django-modeltranslation-wrapper and hyperweek's branch at github. (thanks to Jacek Tomaszewski, Sébastien Fievet and Maxime Haineault, resolves issues #19, #58 and #71) * CHANGED: Moved tests to separate folder and added tests for TranslationAdmin. To run the tests the settings provided in model.tests.modeltranslation have to be used (settings.LANGUAGES override doesn't work for TranslationAdmin). * CHANGED: Major refactoring of the admin integration. Subclassed BaseModelAdmin and InlineModelAdmin. Patching options in init doesn't seem to be thread safe. Instead used provided hooks like get_form, get_formset and get_fieldsets. This should resolve several problems with the exclude and fieldsets options and properly support options in inlines. (resolves issue #72) * FIXED: Non-unicode verbose field names showing up empty in forms. (resolves issue #35) * FIXED: Dynamic TranslationOptions model name. * FIXED: Widgets for translated fields are not properly copied from original fields. (thanks to boris-chervenkov, resolves issue #74) * FIXED: Removed XMLField test which is deprecated since Django 1.3 and broke tests in Django 1.4. (resolves issue #75) ## 0.3.3 (2012-02-23) Packaged from revision 129. * CHANGED: jQuery search path in tabbed_translation_fields.js. This allows use of a version of jQuery other than the one provided by Django. Users who want to force the use of Django's jQuery can include force_jquery.js. * FIXED: Another attempt to include static files during installation. (resolves reopened issue #61) ## 0.3.2 (2011-06-16) Packaged from revision 122. * FIXED: Static files not included during installation. (resolves issue #61) ## 0.3.1 (2011-06-07) Packaged from revision 121. * CHANGED: Renamed media folder to static. ## 0.3 (2011-06-03) Packaged from revision 113. * ADDED: Support for multi-table inheritance. (thanks to Sébastien Fievet, resolves issues #50 and #51) * ADDED: Jquery-ui based admin support for tabbed translation fields. (thanks to jaap and adamsc, resolves issue #39) * ADDED: CSS class to identify a translation field and the default translation field in admin. (thanks to jaap) * ADDED: Configurable default value per field instance. (thanks to bmihelac, resolves issue #28) * ADDED: Setting to override the default language. (thanks to jaap, resolves issue #2) * CHANGED: Improved performance of update_translation_fields command. (thanks to adamsc, resolves issue #43) * CHANGED: Factored out settings into a separate settings.py and consistently used an app specific settings prefix. * CHANGED: Refactored creation of translation fields and added handling of supported fields. (resolves issue #37) * FIXED: Clearing the default translation field in admin does not clear the original field. (resolves issue #47) * FIXED: In some setups appears "This field is required" error for the original field. (resolves issue #5) * FIXED: Translations are not saved for tinymce HTMLField when using jquery tabs. (thanks to kottenator, resolves issue #41) * FIXED: Fieldname isn't ensured to be string. (resolves issue #41) * FIXED: Kept backwards compatibility with Django-1.0. (thanks to jaap, resolves issue #34) * FIXED: Regression in south_field_triple caused by r55. (thanks to jaap, resolves issue #29) * FIXED: TranslationField pre_save does not get the default language correctly. (thanks to jaap, resolves issue #31) ## 0.2 (2010-06-15) Packaged from revision 57. * ADDED: Support for admin prepopulated_fields. (resolves issue #21) * ADDED: Support for admin list_editable. (thanks carl.j.meyer, resolves issue #20) * ADDED: Preserve the formfield widget of the translated field. (thanks piquadrat) * ADDED: Initial support for django-south. (thanks andrewgodwin, resolves issue #11) * ADDED: Support for admin inlines, common and generic. (resolves issue #12 and issue #18) * FIXED: Admin form validation errors with empty translated values and unique=True. (thanks to adamsc, resolves issue #26) * FIXED: Mangling of untranslated prepopulated fields. (thanks to carl.j.meyer, resolves issue #25) * FIXED: Verbose names of translated fields are not translated. (thanks to carl.j.meyer, resolves issue #24) * FIXED: Race condition between model import and translation registration in production by ensuring that models are registered for translation before TranslationAdmin runs. (thanks to carl.j.meyer, resolves issue #19) * FIXED: Added workaround for swallowed ImportErrors by printing a traceback explicitly. (resolves issue #17) * FIXED: Only print debug statements to stdout if the runserver or runserver_plus management commands are used. (resolves issue #16) * FIXED: Removed print statements so that modeltranslation is usable with mod_wsgi. (resolves issue #7) * FIXED: Broken admin fields and fieldsets. (thanks simoncelen, resolves issue #9) * FIXED: Creation of db fields with invalid python language code. (resolves issue #4) * FIXED: Tests to run from any project. (thanks carl.j.meyer, resolves issue #6) * FIXED: Removed unused dependency to content type which can break syncdb. (thanks carl.j.meyer, resolves issue #1) ## 0.1 (2009-02-22) Initial release packaged from revision 19. django-modeltranslation-0.19.14/LICENSE.txt000066400000000000000000000030071500120161300203150ustar00rootroot00000000000000Copyright (c) 2012, 2011, 2010, 2009, Peter Eschler, Dirk Eschler All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-modeltranslation-0.19.14/MANIFEST.in000066400000000000000000000002341500120161300202270ustar00rootroot00000000000000include *.txt *.rst VERSION prune modeltranslation/tests recursive-include docs *.rst conf.py Makefile make.bat recursive-include modeltranslation/static * django-modeltranslation-0.19.14/Makefile000066400000000000000000000003771500120161300201410ustar00rootroot00000000000000release: commit-and-tag-version publish: clean python -m build twine upload dist/* git push --follow-tags clean: rm -rf dist lint: ruff check modeltranslation ruff format --check modeltranslation *.py typecheck: mypy --pretty modeltranslation django-modeltranslation-0.19.14/README.rst000066400000000000000000000041461500120161300201660ustar00rootroot00000000000000================ Modeltranslation ================ .. image:: https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg :target: https://stand-with-ukraine.pp.ua :alt: Stand With Ukraine ----- .. image:: http://img.shields.io/coveralls/deschler/django-modeltranslation.svg?style=flat-square :target: https://coveralls.io/r/deschler/django-modeltranslation .. image:: https://img.shields.io/pypi/v/django-modeltranslation.svg?style=flat-square :target: https://pypi.python.org/pypi/django-modeltranslation/ :alt: Latest PyPI version .. image:: https://img.shields.io/pypi/pyversions/django-modeltranslation.svg?style=flat-square :target: https://pypi.python.org/pypi/django-modeltranslation/ :alt: Supported Python versions .. image:: https://img.shields.io/gitter/room/django-modeltranslation/community?color=4DB798&style=flat-square :alt: Join the chat at https://gitter.im/django-modeltranslation/community :target: https://gitter.im/django-modeltranslation/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge The modeltranslation application is used to translate dynamic content of existing Django models to an arbitrary number of languages without having to change the original model classes. It uses a registration approach (comparable to Django's admin app) to be able to add translations to existing or new projects and is fully integrated into the Django admin backend. The advantage of a registration approach is the ability to add translations to models on a per-app basis. You can use the same app in different projects, may they use translations or not, and you never have to touch the original model class. Features ======== - Add translations without changing existing models or views - Translation fields are stored in the same table (no expensive joins) - Supports inherited models (abstract and multi-table inheritance) - Handle more than just text fields - Django admin integration - Flexible fallbacks, auto-population and more! For the latest documentation, visit https://django-modeltranslation.readthedocs.io/en/latest/. django-modeltranslation-0.19.14/VERSION000066400000000000000000000000071500120161300175370ustar00rootroot000000000000000.19.14django-modeltranslation-0.19.14/docs/000077500000000000000000000000001500120161300174225ustar00rootroot00000000000000django-modeltranslation-0.19.14/docs/modeltranslation/000077500000000000000000000000001500120161300230015ustar00rootroot00000000000000django-modeltranslation-0.19.14/docs/modeltranslation/Makefile000066400000000000000000000130001500120161300244330ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-modeltranslation.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-modeltranslation.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/django-modeltranslation" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-modeltranslation" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." django-modeltranslation-0.19.14/docs/modeltranslation/_static/000077500000000000000000000000001500120161300244275ustar00rootroot00000000000000django-modeltranslation-0.19.14/docs/modeltranslation/_static/.gitignore000066400000000000000000000000151500120161300264130ustar00rootroot00000000000000!.gitignore django-modeltranslation-0.19.14/docs/modeltranslation/admin.rst000066400000000000000000000266371500120161300246410ustar00rootroot00000000000000.. _admin: Django Admin Integration ======================== In order to be able to edit the translations via the ``django.contrib.admin`` application you need to register a special admin class for the translated models. The admin class must derive from ``modeltranslation.admin.TranslationAdmin`` which does some funky patching on all your models registered for translation. Taken the :ref:`news example ` the most simple case would look like: .. code-block:: python from django.contrib import admin from news.models import News from modeltranslation.admin import TranslationAdmin class NewsAdmin(TranslationAdmin): pass admin.site.register(News, NewsAdmin) Tweaks Applied to the Admin --------------------------- formfield_for_dbfield ********************* The ``TranslationBaseModelAdmin`` class, which ``TranslationAdmin`` and all inline related classes in modeltranslation derive from, implements a special method which is ``formfield_for_dbfield(self, db_field, **kwargs)``. This method does the following: 1. Copies the widget of the original field to each of its translation fields. 2. Checks if the original field was required and if so makes the default translation field required instead. get_form/get_fieldsets ****************************************** In addition the ``TranslationBaseModelAdmin`` class overrides ``get_form`` and ``get_fieldsets`` to make the options ``fields``, ``exclude`` and ``fieldsets`` work in a transparent way. It basically does: 1. Removes the original field from every admin form by adding it to ``exclude`` under the hood. 2. Replaces the - now removed - original fields with their corresponding translation fields. Taken the ``fieldsets`` option as an example, where the ``title`` field is registered for translation but not the ``news`` field: .. code-block:: python class NewsAdmin(TranslationAdmin): fieldsets = [ (u'News', {'fields': ('title', 'news',)}) ] In this case ``get_fieldsets`` will return a patched fieldset which contains the translation fields of ``title``, but not the original field: .. code-block:: python >>> a = NewsAdmin(NewsModel, site) >>> a.get_fieldsets(request) [(u'News', {'fields': ('title_de', 'title_en', 'news',)})] .. _translationadmin_in_combination_with_other_admin_classes: TranslationAdmin in Combination with Other Admin Classes -------------------------------------------------------- If there already exists a custom admin class for a translated model and you don't want or can't edit that class directly there is another solution. Taken a reusable blog app which defines a model ``Entry`` and a corresponding admin class called ``EntryAdmin``. This app is not yours and you don't want to touch it at all. In the most common case you simply make use of Python's support for multiple inheritance like this: .. code-block:: python class MyTranslatedEntryAdmin(EntryAdmin, TranslationAdmin): pass The class is then registered for the ``admin.site`` (not to be confused with modeltranslation's ``translator``). If ``EntryAdmin`` is already registered through the blog app, it has to be unregistered first: .. code-block:: python admin.site.unregister(Entry) admin.site.register(Entry, MyTranslatedEntryAdmin) Admin Classes that Override ``formfield_for_dbfield`` ***************************************************** In a more complex setup the original ``EntryAdmin`` might override ``formfield_for_dbfield`` itself: .. code-block:: python class EntryAdmin(model.Admin): def formfield_for_dbfield(self, db_field, **kwargs): # does some funky stuff with the formfield here Unfortunately the first example won't work anymore because Python can only execute one of the ``formfield_for_dbfield`` methods. Since both admin classes implement this method Python must make a decision and it chooses the first class ``EntryAdmin``. The functionality from ``TranslationAdmin`` will not be executed and translation in the admin will not work for this class. But don't panic, here's a solution: .. code-block:: python class MyTranslatedEntryAdmin(EntryAdmin, TranslationAdmin): def formfield_for_dbfield(self, db_field, **kwargs): field = super(MyTranslatedEntryAdmin, self).formfield_for_dbfield(db_field, **kwargs) self.patch_translation_field(db_field, field, **kwargs) return field This implements the ``formfield_for_dbfield`` such that both functionalities will be executed. The first line calls the superclass method which in this case will be the one of ``EntryAdmin`` because it is the first class inherited from. The ``TranslationAdmin`` capsulates its functionality in the ``patch_translation_field`` method and the ``formfield_for_dbfield`` implementation of the ``TranslationAdmin`` class simply calls it. You can copy this behaviour by calling it from a custom admin class and that's done in the example above. After that the ``field`` is fully patched for translation and finally returned. Admin Inlines ------------- .. versionadded:: 0.2 Support for tabular and stacked inlines, common and generic ones. A translated inline must derive from one of the following classes: * ``modeltranslation.admin.TranslationTabularInline`` * ``modeltranslation.admin.TranslationStackedInline`` * ``modeltranslation.admin.TranslationGenericTabularInline`` * ``modeltranslation.admin.TranslationGenericStackedInline`` Just like ``TranslationAdmin`` these classes implement a special method ``formfield_for_dbfield`` which does all the patching. For our example we assume that there is a new model called ``Image``. The definition is left out for simplicity. Our ``News`` model inlines the new model: .. code-block:: python from django.contrib import admin from news.models import Image, News from modeltranslation.admin import TranslationTabularInline class ImageInline(TranslationTabularInline): model = Image class NewsAdmin(admin.ModelAdmin): list_display = ('title',) inlines = [ImageInline,] admin.site.register(News, NewsAdmin) .. note:: In this example only the ``Image`` model is registered in ``translation.py``. It's not a requirement that ``NewsAdmin`` derives from ``TranslationAdmin`` in order to inline a model which is registered for translation. Complex Example with Admin Inlines ********************************** In this more complex example we assume that the ``News`` and ``Image`` models are registered in ``translation.py``. The ``News`` model has an own custom admin class called ``NewsAdmin`` and the ``Image`` model an own generic stacked inline class called ``ImageInline``. Furthermore we assume that ``NewsAdmin`` overrides ``formfield_for_dbfield`` itself and the admin class is already registered through the news app. .. note:: The example uses the technique described in `TranslationAdmin in combination with other admin classes`__. __ translationadmin_in_combination_with_other_admin_classes_ Bringing it all together our code might look like this: .. code-block:: python from django.contrib import admin from news.admin import ImageInline from news.models import Image, News from modeltranslation.admin import TranslationAdmin, TranslationGenericStackedInline class TranslatedImageInline(ImageInline, TranslationGenericStackedInline): model = Image class TranslatedNewsAdmin(NewsAdmin, TranslationAdmin): inlines = [TranslatedImageInline,] def formfield_for_dbfield(self, db_field, **kwargs): field = super(TranslatedNewsAdmin, self).formfield_for_dbfield(db_field, **kwargs) self.patch_translation_field(db_field, field, **kwargs) return field admin.site.unregister(News) admin.site.register(News, NewsAdmin) Using Tabbed Translation Fields ------------------------------- .. versionadded:: 0.3 Modeltranslation supports separation of translation fields via jquery-ui tabs. The proposed way to include it is through the inner ``Media`` class of a ``TranslationAdmin`` class like this: .. code-block:: python class NewsAdmin(TranslationAdmin): class Media: js = ( 'modeltranslation/js/force_jquery.js', '//ajax.googleapis.com/ajax/libs/jqueryui/1.8.24/jquery-ui.min.js', 'modeltranslation/js/tabbed_translation_fields.js', ) css = { 'screen': ('modeltranslation/css/tabbed_translation_fields.css',), } .. note:: Here we stick to the jquery library shipped with Django. The ``force_jquery.js`` script is necessary when using Django's built-in ``django.jQuery`` object. Otherwise the *normal* ``jQuery`` object won't be available to the included (non-namespaced) jquery-ui library. Standard jquery-ui theming can be used to customize the look of tabs, the provided css file is supposed to work well with a default Django admin. As an alternative, if want to use a more recent version of jquery, you can do so by including this in your ``Media`` class instead: .. code-block:: python class NewsAdmin(TranslationAdmin): class Media: js = ( '//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js', '//ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js', 'modeltranslation/js/tabbed_translation_fields.js', ) css = { 'screen': ('modeltranslation/css/tabbed_translation_fields.css',), } Tabbed Translation Fields Admin Classes *************************************** .. versionadded:: 0.7 To ease the inclusion of the required static files for tabbed translation fields, the following admin classes are provided: * ``TabbedDjangoJqueryTranslationAdmin`` (aliased to ``TabbedTranslationAdmin``) * ``TabbedExternalJqueryTranslationAdmin`` Rather than inheriting from ``TranslationAdmin``, simply subclass one of these classes like this: .. code-block:: python class NewsAdmin(TabbedTranslationAdmin): pass ``TranslationAdmin`` Options ---------------------------- ``TranslationAdmin.group_fieldsets`` ************************************ .. versionadded:: 0.6 When this option is activated untranslated and translation fields are grouped into separate fieldsets. The first fieldset contains the untranslated fields, followed by a fieldset for each translation field. The translation field fieldsets use the original field's ``verbose_name`` as a label. Activating the option is a simple way to reduce the visual clutter one might experience when mixing these different types of fields. The ``group_fieldsets`` option expects a boolean. By default fields are not grouped into fieldsets (``group_fieldsets = False``). A few simple policies are applied: * A ``fieldsets`` option takes precedence over the ``group_fieldsets`` option. * Other default ``ModelAdmin`` options like ``exclude`` are respected. .. code-block:: python class NewsAdmin(TranslationAdmin): group_fieldsets = True .. _admin-formfield: Formfields with None-checkbox ***************************** There is the special widget which allow to choose whether empty field value should be stores as empty string or ``None`` (see :ref:`forms-formfield-both`). In ``TranslationAdmin`` some fields can use this widget regardless of their ``empty_values`` setting:: class NewsAdmin(TranslationAdmin): both_empty_values_fields = ('title', 'text') django-modeltranslation-0.19.14/docs/modeltranslation/authors.rst000077700000000000000000000000001500120161300275202../../AUTHORS.rstustar00rootroot00000000000000django-modeltranslation-0.19.14/docs/modeltranslation/caveats.rst000066400000000000000000000063521500120161300251670ustar00rootroot00000000000000.. _caveats: Caveats ======= Accessing Translated Fields Outside Views ----------------------------------------- Since the modeltranslation mechanism relies on the current language as it is returned by the ``get_language`` function care must be taken when accessing translated fields outside a view function. Within a view function the language is set by Django based on a flexible model described at `How Django discovers language preference`_ which is normally used only by Django's static translation system. .. _How Django discovers language preference: https://docs.djangoproject.com/en/dev/topics/i18n/translation/#how-django-discovers-language-preference When a translated field is accessed in a view function or in a template, it uses the ``django.utils.translation.get_language`` function to determine the current language and return the appropriate value. Outside a view (or a template), i.e. in normal Python code, a call to the ``get_language`` function still returns a value, but it might not what you expect. Since no request is involved, Django's machinery for discovering the user's preferred language is not activated. For this reason modeltranslation adds a thin wrapper (``modeltranslation.utils.get_language``) around the function which guarantees that the returned language is listed in the ``LANGUAGES`` setting. The unittests use the ``django.utils.translation.trans_real`` functions to activate and deactive a specific language outside a view function. Using in combination with ``django-audit-log`` ---------------------------------------------- ``django-audit-log`` is a package that allows you to track changes to your model instances (`documentation`_). As ``django-audit-log`` behind the scenes automatically creates "shadow" models for your tracked models, you have to remember to register these shadow models for translation as well as your regular models. Here's an example: .. code:: python from modeltranslation.translator import register, TranslationOptions from my_app import models @register(models.MyModel) @register(models.MyModel.audit_log.model) class MyModelTranslationOptions(TranslationOptions): """Translation options for MyModel.""" fields = ( 'text', 'title', ) If you forget to register the shadow models, you will get an error like: .. code:: TypeError: 'text_es' is an invalid keyword argument for this function Using in combination with ``django-rest-framework`` ------------------------------------------------- When creating a new viewset , make sure to override ``get_queryset`` method, using ``queryset`` as a property won't work because it is being evaluated once, before any language was set. Translating ``ManyToManyField`` fields ------------------------------------------------- Translated ``ManyToManyField`` fields do not support fallbacks. This is because the field descriptor returns a ``Manager`` when accessed. If falbacks were enabled we could find ourselves using the manager of a different language than the current one without realizing it. This can lead to using the ``.set()`` method on the wrong language. Due to this behavior the fallbacks on M2M fields have been disabled. .. _documentation: https://django-audit-log.readthedocs.io/ django-modeltranslation-0.19.14/docs/modeltranslation/changelog.rst000066400000000000000000000000741500120161300254630ustar00rootroot00000000000000ChangeLog ========= .. literalinclude:: ../../CHANGELOG.md django-modeltranslation-0.19.14/docs/modeltranslation/commands.rst000066400000000000000000000072761500120161300253500ustar00rootroot00000000000000.. _commands: Management Commands =================== .. _commands-update_translation_fields: The ``update_translation_fields`` Command ----------------------------------------- In case modeltranslation was installed in an existing project and you have specified to translate fields of models which are already synced to the database, you have to update your database schema (see :ref:`db-fields`). Unfortunately the newly added translation fields on the model will be empty then, and your templates will show the translated value of the fields (see :ref:`Rule 1 `) which will be empty in this case. To correctly initialize the default translation field you can use the ``update_translation_fields`` command: .. code-block:: console $ python manage.py update_translation_fields Taken the news example used throughout the documentation this command will copy the value from the news object's ``title`` field to the translation field ``title_de``. It only does so if the translation field is empty otherwise nothing is copied. On default, only the *default language* will have its translation field populated, but you can provide a ``--language`` option to specify any other language listed in ``settings.py``. .. note:: Unless you configured modeltranslation to :ref:`override the default language ` the command will examine your ``settings.LANGUAGES`` variable and the first language declared there will be used as the default language. All translated models (as specified in the translation files) from all apps will be populated with initial data. Optionally, an app label and model name may be passed to populate only a subset of translated models. .. code-block:: console $ python manage.py update_translation_fields myapp .. code-block:: console $ python manage.py update_translation_fields myapp mymodel .. _commands-sync_translation_fields: The ``sync_translation_fields`` Command --------------------------------------- .. versionadded:: 0.4 .. code-block:: console $ python manage.py sync_translation_fields This command compares the database and translated models definitions (finding new translation fields) and provides SQL statements to alter tables. You should run this command after adding a new language to your ``settings.LANGUAGES`` or a new field to the ``TranslationOptions`` of a registered model. However, if you are using South in your project, in most cases it's recommended to use migration instead of ``sync_translation_fields``. See :ref:`db-fields` for detailed info and use cases. The ``loaddata`` Command ------------------------ .. versionadded:: 0.7 An extended version of Django's original ``loaddata`` command which adds an optional ``populate`` keyword. If the keyword is specified, the normal loading command will be run under the selected auto-population modes. By default no auto-population is performed. .. code-block:: console $ python manage.py loaddata --populate=all fixtures.json Allowed modes are listed :ref:`here `. To choose ``False`` (turn off auto-population) specify ``'0'`` or ``'false'``: .. code-block:: console $ python manage.py loaddata --populate=false fixtures.json $ python manage.py loaddata --populate=0 fixtures.json .. note:: If ``populate`` is not specified, the current auto-population mode is used. *Current* means the one set by :ref:`settings `. Moreover, this ``loaddata`` command version can override the nasty habit of changing locale to `en-us`. By default, it will retain the proper locale. To get the old behaviour back, set :ref:`settings-modeltranslation_loaddata_retain_locale` to ``False``. django-modeltranslation-0.19.14/docs/modeltranslation/conf.py000066400000000000000000000227431500120161300243100ustar00rootroot00000000000000# fmt: off # django-modeltranslation documentation build configuration file, created by # sphinx-quickstart on Wed Oct 17 10:26:58 2012. # # This file is execfile()d with the current directory set to its containing # dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) try: from importlib.metadata import version # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout # the built documents. # # The full PEP386-compliant version number version, including # normalized alpha/beta/rc/dev tags (e.g. '0.5a1'). release = version("django-modeltranslation") # The short X.Y version (e.g.'0.5'). version = '.'.join(i for i in release.split('.')[:2]) except ImportError: version = 'dev' release = 'dev' # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'django-modeltranslation' copyright = '2009-2019, Peter Eschler, Dirk Eschler, Jacek Tomaszewski' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # Todos produce output # FIXME: Doesn't seem to work todo_include_todos = True # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'django-modeltranslationdoc' # -- Options for LaTeX output ------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'django-modeltranslation.tex', 'django-modeltranslation Documentation', 'Dirk Eschler', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'django-modeltranslation', 'django-modeltranslation Documentation', ['Dirk Eschler'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ----------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'django-modeltranslation', 'django-modeltranslation Documentation', 'Dirk Eschler', 'django-modeltranslation', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # -- Options for Epub output -------------------------------------------------- # Bibliographic Dublin Core info. epub_title = 'django-modeltranslation' epub_author = 'Dirk Eschler' epub_publisher = 'Dirk Eschler' epub_copyright = '2009-2019, Peter Eschler, Dirk Eschler, Jacek Tomaszewski' # The language of the text. It defaults to the language option # or en if the language is not set. #epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. #epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. #epub_identifier = '' # A unique identification for the text. #epub_uid = '' # A tuple containing the cover image and cover page html template filenames. #epub_cover = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. #epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. #epub_post_files = [] # A list of files that should not be packed into the epub file. #epub_exclude_files = [] # The depth of the table of contents in toc.ncx. #epub_tocdepth = 3 # Allow duplicate toc entries. #epub_tocdup = True django-modeltranslation-0.19.14/docs/modeltranslation/contribute.rst000066400000000000000000000116461500120161300257210ustar00rootroot00000000000000.. _contribute: How to Contribute ================= There are various ways how you can contribute to the project. Contributing Code ----------------- The preferred way for code contributions are pull requests at `Github`_, usually created against master. Use [Convential commits](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages. .. note:: In order to be properly blamed for a contribution, please verify that the email you commit with is connected to your Github account (see `help.github.com`_ for details). Coding Style ************ Please make sure that your code follows the `PEP 8`_ style guide. The only exception we make is to allow a maximum line length of 100. Furthermore your code has to validate against `ruff`_. It is recommended to use `ruff`_ which combines all the checks, and `ruff format` for code formatting. .. code-block:: console $ make lint The ``# noqa`` marks for `ruff`_ should be used sparsely. Django and Python Versions ************************** We always try to support **at least** the two latest major versions of Django, as well as Django's development version. While we can not guarantee the latter to be supported in early development stages of a new Django version, we aim to achieve support once it has seen its first release candidate. The supported Python versions can be derived from the supported Django versions. Example (from the past) where we support Python 2.5, 2.6 and 2.7: * Django 1.3 (old stable) supports Python 2.5, 2.6, 2.7 * Django 1.4 (current stable) supports Python 2.5, 2.6, 2.7 * Django 1.5 (dev) supports Python 2.6, 2.7 Python 3 is supported since 0.7 release. Although 0.6 release supported Django 1.5 (which started Python 3 compliance), it was not Python 3 ready yet. Unittests ********* To test Modeltranslation, you can use the comprehensive test suite that comes with the package. First, make sure you have installed the project's requirements using uv. Once the requirements are installed, you can run the tests using pytest. This will run all of the tests in the test suite and report any failures or errors. .. code-block:: console $ pip install uv $ uv sync --group dev --group lsp --no-install-project $ uv run pytest Non trivial changes and new features should always be accompanied by a unittest. Pull requests which add unittests for uncovered code or rare edge cases are also appreciated. Continuous Integration ********************** The project uses `Github Actions`_ for continuous integration tests. Hooks provided by Github are active, so that each push and pull request is automatically run against our `Github Actions Workflows`_, checking code against different databases, Python and Django versions. This includes automatic tracking of test coverage through `Coveralls`_. .. image:: http://img.shields.io/coveralls/deschler/django-modeltranslation.png?style=flat :target: https://coveralls.io/r/deschler/django-modeltranslation Contributing Documentation -------------------------- Documentation is a crucial part of any open source project. We try to make it as useful as possible for both, new and experienced developers. If you feel that something is unclear or lacking, your help to improve it is highly appreciated. Even if you don't feel comfortable enough to document modeltranslation's usage or internals, you still have a chance to contribute. None of the core committers is a native english speaker and bad grammar or misspellings happen. If you find any of these kind or just simple typos, nobody will feel offended for getting an English lesson. The documentation is written using `reStructuredText`_ and `Sphinx`_. You should try to keep a maximum line length of 80 characters. Unlike for code contribution this isn't a forced rule and easily exceeded by something like a long url. Using the Issue Tracker ----------------------- When you have found a bug or want to request a new feature for modeltranslation, please create a ticket using the project's `issue tracker`_. Your report should include as many details as possible, like a traceback in case you get one. Please do not use the issue tracker for general questions, we run a dedicated `mailing list`_ for this. .. _help.github.com: https://help.github.com/articles/why-are-my-commits-linked-to-the-wrong-user .. _PEP 8: http://www.python.org/dev/peps/pep-0008/ .. _ruff: https://pypi.python.org/pypi/ruff .. _Github: https://github.com/deschler/django-modeltranslation .. _Github Actions: https://travis-ci.org/deschler/django-modeltranslation .. _Github Actions Workflows: https://github.com/deschler/django-modeltranslation/blob/master/.github/workflows .. _Coveralls: https://coveralls.io/r/deschler/django-modeltranslation .. _reStructuredText: http://docutils.sourceforge.net/rst.html .. _Sphinx: http://sphinx-doc.org/ .. _issue tracker: https://github.com/deschler/django-modeltranslation/issues .. _mailing list: http://groups.google.com/group/django-modeltranslation django-modeltranslation-0.19.14/docs/modeltranslation/forms.rst000066400000000000000000000072751500120161300246740ustar00rootroot00000000000000.. _forms: ModelForms ========== ``ModelForms`` for multilanguage models are defined and handled as typical ``ModelForms``. Please note, however, that they shouldn't be defined next to models (see :ref:`a note `). Editing multilanguage models with all translation fields in the admin backend is quite sensible. However, presenting all model fields to the user on the frontend may be not the right way. Here comes the ``TranslationModelForm`` which strip out all translation fields:: from news.models import News from modeltranslation.forms import TranslationModelForm class MyForm(TranslationModelForm): class Meta: model = News Such a form will contain only original fields (title, text - see :ref:`example `). Of course, upon saving, provided values would be set on proper attributes, depending on the user current language. .. _formfield_nullability: Formfields and nullability -------------------------- .. versionadded:: 0.7.1 .. note:: Please remember that all translation fields added to model definition are nullable (``null=True``), regardless of the original field nullability. In most cases formfields for translation fields behave as expected. However, there is one annoying problem with ``models.CharField`` - probably the most commonly translated field type. The problem is that default formfield for ``CharField`` stores empty values as empty strings (``''``), even if the field is nullable (see django `ticket #9590 `_). Thus formfields for translation fields are patched by modeltranslation. The following rules apply: .. _formfield_rules: - If the original field is not nullable, an empty value is saved as ``''``; - If the original field is nullable, an empty value is saved as ``None``. To deal with complex cases, these rules can be overridden per model or even per field using ``TranslationOptions``:: class NewsTranslationOptions(TranslationOptions): fields = ('title', 'text',) empty_values = None class ProjectTranslationOptions(TranslationOptions): fields = ('name', 'slug', 'description',) empty_values = {'name': '', 'slug': None} If a field is not mentioned while using dict syntax, the :ref:`default rules ` apply. This configuration is especially useful for fields with unique constraints:: class Category(models.Model): name = models.CharField(max_length=40) slug = models.SlugField(max_length=30, unique=True) Because the ``slug`` field is not nullable, its translation fields would store empty values as ``''`` and that would result in an error when two or more ``Categories`` are saved with ``slug_en`` empty - unique constraints wouldn't be satisfied. Instead, ``None`` should be stored, as several ``None`` values in the database don't violate uniqueness:: class CategoryTranslationOptions(TranslationOptions): fields = ('name', 'slug') empty_values = {'slug': None} .. _forms-formfield-both: None-checkbox widget ******************** Maybe there is a situation where you want to store both - empty strings and ``None`` values - in a field. For such a scenario there is a third configuration value: ``'both'``:: class NewsTranslationOptions(TranslationOptions): fields = ('title', 'text',) empty_values = {'title': None, 'text': 'both'} It results in a special widget with a None-checkbox to null a field. It's not recommended in frontend as users may be confused what this `None` is. The only useful place for this widget might be the admin backend; see :ref:`admin-formfield`. To sum it up, the valid values for ``empty_values`` are: ``None``, ``''`` and ``'both'``. django-modeltranslation-0.19.14/docs/modeltranslation/index.rst000066400000000000000000000005571500120161300246510ustar00rootroot00000000000000.. django-modeltranslation documentation master file .. _ref-topics-modeltranslation: .. include:: readme.rst Table of Contents ================= .. toctree:: :maxdepth: 2 installation registration usage forms admin commands caveats .. toctree:: :maxdepth: 1 contribute related_projects changelog .. include:: authors.rst django-modeltranslation-0.19.14/docs/modeltranslation/installation.rst000066400000000000000000000267731500120161300262530ustar00rootroot00000000000000.. _installation: Installation ============ Requirements ------------ Which Modeltranslation version is required for given Django-Python combination to work? ======= ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== Python Django ------- ----------------------------------------------------------- version 1.8 1.9 1.10 1.11 2.0 2.1 2.2 3.0 3.2 4.0 4.1 4.2 ======= ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== 2.7 |9| |11| |12| |13| 3.2 |9| 3.3 |9| 3.4 |9| |11| |12| |13| |13| 3.5 |9| |11| |12| |13| |13| |13| 3.6 |13| |13| |13| |15| |15| |17| |17| |17| |18| 3.7 |13| |13| |15| |15| |17| |17| |17| |18| 3.8 |13| |13| |15| |15| |17| |17| |17| |18| 3.9 |13| |13| |15| |15| |17| |17| |17| |18| 3.10 |13| |13| |15| |15| |17| |17| |17| |18| 3.11 |13| |13| |15| |15| |17| |17| |17| |18| ======= ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== (``-X`` denotes "up to version X", whereas ``X+`` means "from version X upwards") .. |9| replace:: 0.9+ .. |11| replace:: 0.11+ .. |12| replace:: 0.12+ .. |13| replace:: 0.13+ .. |15| replace:: 0.15+ .. |17| replace:: 0.17+ .. |18| replace:: 0.18+ Using Pip --------- .. code-block:: console $ pip install django-modeltranslation Using the Source ---------------- Get a source tarball from `pypi`_, unpack, then install with: .. code-block:: console $ python setup.py install .. note:: As an alternative, if you don't want to mess with any packaging tool, unpack the tarball and copy/move the modeltranslation directory to a path listed in your ``PYTHONPATH`` environment variable. .. _pypi: http://pypi.python.org/pypi/django-modeltranslation/ Setup ===== To setup the application please follow these steps. Each step is described in detail in the following sections: 1. Add ``modeltranslation`` to the ``INSTALLED_APPS`` variable of your project's ``settings.py``. 2. Set ``USE_I18N = True`` in ``settings.py``. 3. Configure your ``LANGUAGES`` in ``settings.py``. 4. Create a ``translation.py`` in your app directory and register ``TranslationOptions`` for every model you want to translate. 5. Sync the database using ``python manage.py makemigrations`` and ``python manage.py migrate``. .. note:: This only applies if the models registered in ``translation.py`` haven't been synced to the database before. If they have, please read :ref:`db-fields`. Configuration ============= Required Settings ----------------- The following variables have to be added to or edited in the project's ``settings.py``: ``INSTALLED_APPS`` ^^^^^^^^^^^^^^^^^^ Make sure that the ``modeltranslation`` app is listed in your ``INSTALLED_APPS`` variable:: INSTALLED_APPS = ( ... 'modeltranslation', 'django.contrib.admin', # optional .... ) .. important:: If you want to use the admin integration, ``modeltranslation`` must be put before ``django.contrib.admin`` (only applies when using Django 1.7 or above). .. important:: If you want to use the ``django-debug-toolbar`` together with modeltranslation, use `explicit setup `_. Otherwise tweak the order of ``INSTALLED_APPS``: try to put ``debug_toolbar`` as first entry in ``INSTALLED_APPS`` (in Django < 1.7) or after ``modeltranslation`` (in Django >= 1.7). However, only `explicit setup` is guaranteed to succeed. .. _settings-languages: ``LANGUAGES`` ^^^^^^^^^^^^^ The ``LANGUAGES`` variable must contain all languages used for translation. The first language is treated as the *default language*. Modeltranslation uses the list of languages to add localized fields to the models registered for translation. To use the languages ``de`` and ``en`` in your project, set the ``LANGUAGES`` variable like this (where ``de`` is the default language):: gettext = lambda s: s LANGUAGES = ( ('de', gettext('German')), ('en', gettext('English')), ) .. note:: The ``gettext`` lambda function is not a feature of modeltranslation, but rather required for Django to be able to (statically) translate the verbose names of the languages using the standard ``i18n`` solution. .. note:: If, for some reason, you don't want to translate objects to exactly the same languages as the site would be displayed into, you can set ``MODELTRANSLATION_LANGUAGES`` (see below). For any language in ``LANGUAGES`` not present in ``MODELTRANSLATION_LANGUAGES``, the *default language* will be used when accessing translated content. For any language in ``MODELTRANSLATION_LANGUAGES`` not present in ``LANGUAGES``, probably nobody will see translated content, since the site wouldn't be accessible in that language. .. warning:: Modeltranslation does not enforce the ``LANGUAGES`` setting to be defined in your project. When it isn't present (and neither is ``MODELTRANSLATION_LANGUAGES``), it defaults to Django's `global LANGUAGES setting `_ instead, and that are quite a few languages! Advanced Settings ----------------- Modeltranslation also has some advanced settings to customize its behaviour. .. _settings-modeltranslation_default_language: ``MODELTRANSLATION_DEFAULT_LANGUAGE`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 0.3 Default: ``None`` To override the default language as described in :ref:`settings-languages`, you can define a language in ``MODELTRANSLATION_DEFAULT_LANGUAGE``. Note that the value has to be in ``settings.LANGUAGES``, otherwise an ``ImproperlyConfigured`` exception will be raised. Example:: MODELTRANSLATION_DEFAULT_LANGUAGE = 'en' ``MODELTRANSLATION_LANGUAGES`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 0.8 Default: same as ``LANGUAGES`` Allow to set languages the content will be translated into. If not set, by default all languages listed in ``LANGUAGES`` will be used. Example:: LANGUAGES = ( ('en', 'English'), ('de', 'German'), ('pl', 'Polish'), ) MODELTRANSLATION_LANGUAGES = ('en', 'de') .. note:: This setting may become useful if your users shall produce content for a restricted set of languages, while your application is translated into a greater number of locales. .. _settings-modeltranslation_fallback_languages: ``MODELTRANSLATION_FALLBACK_LANGUAGES`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 0.5 Default: ``(DEFAULT_LANGUAGE,)`` By default modeltranslation will :ref:`fallback ` to the computed value of the ``DEFAULT_LANGUAGE``. This is either the first language found in the ``LANGUAGES`` setting or the value defined through ``MODELTRANSLATION_DEFAULT_LANGUAGE`` which acts as an override. This setting allows for a more fine grained tuning of the fallback behaviour by taking additional languages into account. The language order is defined as a tuple or list of language codes. Example:: MODELTRANSLATION_FALLBACK_LANGUAGES = ('en', 'de') Using a dict syntax it is also possible to define fallbacks by language. A ``default`` key is required in this case to define the default behaviour of unlisted languages. Example:: MODELTRANSLATION_FALLBACK_LANGUAGES = {'default': ('en', 'de'), 'fr': ('de',)} .. note:: Each language has to be in the ``LANGUAGES`` setting, otherwise an ``ImproperlyConfigured`` exception is raised. .. _settings-modeltranslation_prepopulate_language: ``MODELTRANSLATION_PREPOPULATE_LANGUAGE`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 0.7 Default: ``current active language`` By default modeltranslation will use the current request language for prepopulating admin fields specified in the ``prepopulated_fields`` admin property. This is often used to automatically fill slug fields. This setting allows you to pin this functionality to a specific language. Example:: MODELTRANSLATION_PREPOPULATE_LANGUAGE = 'en' .. note:: The language has to be in the ``LANGUAGES`` setting, otherwise an ``ImproperlyConfigured`` exception is raised. ``MODELTRANSLATION_TRANSLATION_FILES`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 0.4 Default: ``()`` (empty tuple) Modeltranslation uses an autoregister feature similar to the one in Django's admin. The autoregistration process will look for a ``translation.py`` file in the root directory of each application that is in ``INSTALLED_APPS``. The setting ``MODELTRANSLATION_TRANSLATION_FILES`` is provided to extend the modules that are taken into account. Syntax:: MODELTRANSLATION_TRANSLATION_FILES = ( '.translation', '.translation', ) Example:: MODELTRANSLATION_TRANSLATION_FILES = ( 'news.translation', 'projects.translation', ) .. note:: Modeltranslation up to version 0.3 used a single project wide registration file which was defined through ``MODELTRANSLATION_TRANSLATION_REGISTRY = '.translation'``. In version 0.4 and 0.5, for backwards compatibility, the module defined through this setting was automatically added to ``MODELTRANSLATION_TRANSLATION_FILES``. A ``DeprecationWarning`` was issued in this case. In version 0.6 ``MODELTRANSLATION_TRANSLATION_REGISTRY`` is handled no more. ``MODELTRANSLATION_CUSTOM_FIELDS`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Default: ``()`` (empty tuple) .. versionadded:: 0.3 Modeltranslation supports the fields listed in the :ref:`supported_field_matrix`. In most cases subclasses of the supported fields will work fine, too. Unsupported fields will throw an ``ImproperlyConfigured`` exception. The list of supported fields can be extended by defining a tuple of field names in your ``settings.py``. Example:: MODELTRANSLATION_CUSTOM_FIELDS = ('MyField', 'MyOtherField',) .. warning:: This just prevents modeltranslation from throwing an ``ImproperlyConfigured`` exception. Any unsupported field will most likely fail in one way or another. The feature is considered experimental and might be replaced by a more sophisticated mechanism in future versions. .. _settings-modeltranslation_auto_populate: ``MODELTRANSLATION_AUTO_POPULATE`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Default: ``False`` .. versionadded:: 0.5 This setting controls if the :ref:`multilingual_manager` should automatically populate language field values in its ``create`` and ``get_or_create`` method, and in model constructors, so that these two blocks of statements can be considered equivalent:: News.objects.populate(True).create(title='-- no translation yet --') with auto_populate(True): q = News(title='-- no translation yet --') # same effect with MODELTRANSLATION_AUTO_POPULATE == True: News.objects.create(title='-- no translation yet --') q = News(title='-- no translation yet --') Possible modes are listed :ref:`here `. ``MODELTRANSLATION_DEBUG`` ^^^^^^^^^^^^^^^^^^^^^^^^^^ Default: ``False`` .. versionadded:: 0.4 .. versionchanged:: 0.7 Used for modeltranslation related debug output. Currently setting it to ``False`` will just prevent Django's development server from printing the ``Registered xx models for translation`` message to stdout. ``MODELTRANSLATION_ENABLE_FALLBACKS`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Default: ``True`` .. versionadded:: 0.6 Control if :ref:`fallback ` (both language and value) will occur. django-modeltranslation-0.19.14/docs/modeltranslation/make.bat000066400000000000000000000120121500120161300244020ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-modeltranslation.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-modeltranslation.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end django-modeltranslation-0.19.14/docs/modeltranslation/readme.rst000077700000000000000000000000001500120161300271002../../README.rstustar00rootroot00000000000000django-modeltranslation-0.19.14/docs/modeltranslation/registration.rst000066400000000000000000000366571500120161300262660ustar00rootroot00000000000000.. _registration: Registering Models for Translation ================================== Modeltranslation can translate model fields of any model class. For each model to translate, a translation option class containing the fields to translate is registered with a special object called the ``translator``. Registering models and their fields for translation requires the following steps: 1. Create a ``translation.py`` in your app directory. 2. Create a translation option class for every model to translate. 3. Register the model and the translation option class at ``modeltranslation.translator.translator``. The modeltranslation application reads the ``translation.py`` file in your app directory, thereby triggering the registration of the translation options found in the file. A translation option is a class that declares which fields of a model to translate. The class must derive from ``modeltranslation.translator.TranslationOptions`` and it must provide a ``fields`` attribute storing the list of fieldnames. The option class must be registered with the ``modeltranslation.translator.translator`` instance. To illustrate this, let's have a look at a simple example using a ``News`` model. The news in this example only contains a ``title`` and a ``text`` field. Instead of a news, this could be any Django model class:: class News(models.Model): title = models.CharField(max_length=255) text = models.TextField() In order to tell modeltranslation to translate the ``title`` and ``text`` fields, create a ``translation.py`` file in your news app directory and add the following:: from modeltranslation.translator import translator, TranslationOptions from .models import News class NewsTranslationOptions(TranslationOptions): fields = ('title', 'text') translator.register(News, NewsTranslationOptions) Note that this does not require to change the ``News`` model in any way, it's only imported. The ``NewsTranslationOptions`` derives from ``TranslationOptions`` and provides the ``fields`` attribute. Finally the model and its translation options are registered at the ``translator`` object. .. versionadded:: 0.10 If you prefer, ``register`` is also available as a decorator, much like the one Django introduced for its admin in version 1.7. Usage is similar to the standard ``register``, just provide arguments as you normally would, except the options class which will be the decorated one:: from modeltranslation.translator import register, TranslationOptions from news.models import News @register(News) class NewsTranslationOptions(TranslationOptions): fields = ('title', 'text',) At this point you are mostly done and the model classes registered for translation will have been added some auto-magical fields. The next section explains how things are working under the hood. .. _TO_field_inheritance: ``TranslationOptions`` fields inheritance ----------------------------------------- .. versionadded:: 0.5 A subclass of any ``TranslationOptions`` will inherit fields from its bases (similar to the way Django models inherit fields, but with a different syntax). When dealing with abstract base classes, this can be handy:: from modeltranslation.translator import translator, TranslationOptions from news.models import News, NewsWithImage class AbstractNewsTranslationOptions(TranslationOptions): fields = ('title', 'text',) class NewsWithImageTranslationOptions(AbstractNewsTranslationOptions): fields = ('image',) translator.register(News, NewsTranslationOptions) translator.register(NewsWithImage, NewsWithImageTranslationOptions) The above example adds the fields ``title`` and ``text`` from the ``AbstractNewsTranslationOptions`` class to ``NewsWithImageTranslationOptions``, or to say it in code:: assert NewsWithImageTranslationOptions.fields == ('title', 'text', 'image') Of course multiple inheritance and inheritance chains (A > B > C) also work as expected. However, if the base class is not abstract, inheriting the ``TranslationOptions`` will cause errors, because the base ``TranslationOptions`` already took care of adding fields to the model. The example below illustrates how to add translation fields to a child model with a non-abstract base:: from modeltranslation.translator import translator, TranslationOptions from news.models import News, NewsWithImage class NewsTranslationOptions(TranslationOptions): fields = ('title', 'text',) class NewsWithImageTranslationOptions(TranslationOptions): fields = ('image',) translator.register(News, NewsTranslationOptions) translator.register(NewsWithImage, NewsWithImageTranslationOptions) This will add the translated fields ``title`` and ``text`` to the ``News`` model and further add the translated field ``image`` to the ``NewsWithImage`` model. .. note:: When upgrading from a previous modeltranslation version (<0.5), please review your ``TranslationOptions`` classes and see if introducing `fields inheritance` broke the project (if you had always subclassed ``TranslationOptions`` only, there is no risk). Changes Automatically Applied to the Model Class ------------------------------------------------ After registering the ``News`` model for translation a SQL dump of the news app will look like this: .. code-block:: console $ ./manage.py sqlall news BEGIN; CREATE TABLE `news_news` ( `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `title` varchar(255) NOT NULL, `title_de` varchar(255) NULL, `title_en` varchar(255) NULL, `text` longtext NULL, `text_de` longtext NULL, `text_en` longtext NULL, ) ; CREATE INDEX `news_news_page_id` ON `news_news` (`page_id`); COMMIT; Note the ``title_de``, ``title_en``, ``text_de`` and ``text_en`` fields which are not declared in the original ``News`` model class but rather have been added by the modeltranslation app. These are called *translation fields*. There will be one for every language in your project's ``settings.py``. The names of these additional fields are built using the original name of the translated field and appending one of the language identifiers found in the ``settings.LANGUAGES``. As these fields are added to the registered model class as fully valid Django model fields, they will appear in the db schema for the model although it has not been specified on the model explicitly. .. _register-precautions: Precautions regarding registration approach ******************************************* Be aware that registration approach (as opposed to base-class approach) to models translation has a few caveats, though (despite many pros). First important thing to note is the fact that translatable models are being patched - that means their fields list is not final until the modeltranslation code executes. In normal circumstances it shouldn't affect anything - as long as ``models.py`` contain only models' related code. For example: consider a project where a ``ModelForm`` is declared in ``models.py`` just after its model. When the file is executed, the form gets prepared - but it will be frozen with old fields list (without translation fields). That's because the ``ModelForm`` will be created before modeltranslation would add new fields to the model (``ModelForm`` gather fields info at class creation time, not instantiation time). Proper solution is to define the form in ``forms.py``, which wouldn't be imported alongside with ``models.py`` (and rather imported from views file or urlconf). Generally, for seamless integration with modeltranslation (and as sensible design anyway), the ``models.py`` should contain only bare models and model related logic. .. _db-fields: Committing fields to database ***************************** If you are starting a fresh project and have considered your translation needs in the beginning then simply sync your database (``./manage.py syncdb`` or ``./manage.py schemamigration myapp --initial`` if using South) and you are ready to use the translated models. In case you are translating an existing project and your models have already been synced to the database you will need to alter the tables in your database and add these additional translation fields. If you are using South, you're done: simply create a new migration (South will detect newly added translation fields) and apply it. If not, you can use a little helper: :ref:`commands-sync_translation_fields` which can execute schema-ALTERing SQL to add new fields. Use either of these two solutions, not both. If you are adding translation fields to a third-party app, things get more complicated. In order to be able to update the app in the future, and to feel comfortable, you should use the ``sync_translation_fields`` command. Although it's possible to introduce new fields in a migration, it's nasty and involves copying migration files, using ``MIGRATION_MODULES`` setting, so we don't recommend it. Invoking ``sync_translation_fields`` is plain easier. Note that all added fields are by default declared ``blank=True`` and ``null=True`` no matter if the original field is required or not. In other words - all translations are optional, unless an explicit option is provided - see :ref:`required_langs`. To populate the default translation fields added by modeltranslation with values from existing database fields, you can use the ``update_translation_fields`` command. See :ref:`commands-update_translation_fields` for more info on this. .. _migrations: Migrations (Django 1.7) ^^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 0.8 Modeltranslation supports the migration system introduced by Django 1.7. Besides the normal workflow as described in Django's `Migration docs`_, you should do a migration whenever one of the following changes have been made to your project: - Added or removed a language through ``settings.LANGUAGES`` or ``settings.MODELTRANSLATION LANGUAGES``. - Registered or unregistered a field through ``TranslationOptions.fields``. It doesn't matter if you are starting a fresh project or change an existing one, it's always: 1. ``python manage.py makemigrations`` to create a new migration with the added or removed fields. 2. ``python manage.py migrate`` to apply the changes. .. As opposed to the statement made in :ref:`db-fields`, using the .. :ref:`sync_translation_fields ` .. management command together with the new migration system is not recommended. .. note:: Support for migrations is implemented through ``fields.TranslationField.deconstruct(self)`` and respects changes to the ``null`` option. .. _required_langs: Required fields --------------- .. versionadded:: 0.8 By default, all translation fields are optional (not required). This can be changed using a special attribute on ``TranslationOptions``:: class NewsTranslationOptions(TranslationOptions): fields = ('title', 'text',) required_languages = ('en', 'de') It's quite self-explanatory: for German and English, all translation fields are required. For other languages - optional. A more fine-grained control is available:: class NewsTranslationOptions(TranslationOptions): fields = ('title', 'text',) required_languages = {'de': ('title', 'text'), 'default': ('title',)} For German, all fields (both ``title`` and ``text``) are required; for all other languages - only ``title`` is required. The ``'default'`` is optional. .. note:: Requirement is enforced by ``blank=False``. Please remember that it will trigger validation only in modelforms and admin (as always in Django). Manual model validation can be performed via the ``full_clean()`` model method. The required fields are still ``null=True``, though. .. versionadded:: 0.19.4 To set required_languages for all models, use `MODELTRANSLATION_REQUIRED_LANGUAGES` setting, which accepts the same values as `required_languages` class variable. ``TranslationOptions`` attributes reference ------------------------------------------- Quick cheatsheet with links to proper docs sections and examples showing expected syntax. Classes inheriting from ``TranslationOptions`` can have following attributes defined: .. attribute:: TranslationOptions.fields (required) List of translatable model fields. See :ref:`registration`. Some fields can be implicitly added through inheritance, see :ref:`TO_field_inheritance`. .. attribute:: TranslationOptions.fallback_languages Control order of languages for fallback purposes. See :ref:`fallback_lang`. :: fallback_languages = {'default': ('en', 'de', 'fr'), 'uk': ('ru',)} .. attribute:: TranslationOptions.fallback_values Set the value that should be used if no fallback language yielded a value. See :ref:`fallback_val`. :: fallback_values = _('-- sorry, no translation provided --') fallback_values = {'title': _('Object not translated'), 'text': '---'} .. attribute:: TranslationOptions.fallback_undefined Set what value should be considered "no value". See :ref:`fallback_undef`. :: fallback_undefined = None fallback_undefined = {'title': 'no title', 'text': None} .. attribute:: TranslationOptions.empty_values Override the value that should be saved in forms on empty fields. See :ref:`formfield_nullability`. :: empty_values = '' empty_values = {'title': '', 'slug': None, 'desc': 'both'} .. attribute:: TranslationOptions.required_languages Control which translation fields are required. See :ref:`required_langs`. :: required_languages = ('en', 'de') required_languages = {'de': ('title','text'), 'default': ('title',)} .. _supported_field_matrix: Supported Fields Matrix ----------------------- While the main purpose of modeltranslation is to translate text-like fields, translating other fields can be useful in several situations. The table lists all model fields available in Django and gives an overview about their current support status: =============================== === === === Model Field 0.4 0.5 0.7 =============================== === === === ``AutoField`` |n| |n| |n| ``BigIntegerField`` |n| |i| |i| ``BooleanField`` |n| |y| |y| ``CharField`` |y| |y| |y| ``CommaSeparatedIntegerField`` |n| |y| |y| ``DateField`` |n| |y| |y| ``DateTimeField`` |n| |y| |y| ``DecimalField`` |n| |y| |y| ``EmailField`` |i| |i| |i| ``FileField`` |y| |y| |y| ``FilePathField`` |i| |i| |i| ``FloatField`` |n| |y| |y| ``ImageField`` |y| |y| |y| ``IntegerField`` |n| |y| |y| ``IPAddressField`` |n| |y| |y| ``GenericIPAddressField`` |n| |y| |y| ``NullBooleanField`` |n| |y| |y| ``PositiveIntegerField`` |n| |i| |i| ``PositiveSmallIntegerField`` |n| |i| |i| ``SlugField`` |i| |i| |i| ``SmallIntegerField`` |n| |i| |i| ``TextField`` |y| |y| |y| ``TimeField`` |n| |y| |y| ``URLField`` |i| |i| |i| ``ForeignKey`` |n| |n| |y| ``OneToOneField`` |n| |n| |y| ``ManyToManyField`` |n| |n| |n| =============================== === === === .. |y| replace:: Yes .. |i| replace:: Yes\* .. |n| replace:: No .. |u| replace:: ? \* Implicitly supported (as subclass of a supported field) .. _Migration docs: https://docs.djangoproject.com/en/dev/topics/migrations/#workflow django-modeltranslation-0.19.14/docs/modeltranslation/related_projects.rst000066400000000000000000000033121500120161300270630ustar00rootroot00000000000000.. _related_projects: Related Projects ================ .. note:: This list is horribly outdated and only covers apps that where available when modeltranslation was initially developed. A more complete list can be found at `djangopackages.com`_. `django-multilingual`_ ---------------------- A library providing support for multilingual content in Django models. It is not possible to reuse existing models without modifying them. `django-multilingual-model`_ ---------------------------- A much simpler version of the above `django-multilingual`. It works very similar to the `django-multilingual` approach. `transdb`_ ---------- Django's field that stores labels in more than one language in database. This approach uses a specialized ``Field`` class, which means one has to change existing models. `i18ndynamic`_ -------------- This approach is not developed any more. `django-pluggable-model-i18n`_ ------------------------------ This app utilizes a new approach to multilingual models based on the same concept the new admin interface uses. A translation for an existing model can be added by registering a translation class for that model. This is more or less what modeltranslation does, unfortunately it is far from being finished. .. _djangopackages.com: http://www.djangopackages.com/grids/g/model-translation/ .. _django-multilingual: http://code.google.com/p/django-multilingual/ .. _django-multilingual-model: http://code.google.com/p/django-multilingual-model/ .. _django-transdb: http://code.google.com/p/transdb/ .. _i18ndynamic: http://code.google.com/p/i18ndynamic/ .. _django-pluggable-model-i18n: http://code.google.com/p/django-pluggable-model-i18n/ django-modeltranslation-0.19.14/docs/modeltranslation/usage.rst000066400000000000000000000333711500120161300246460ustar00rootroot00000000000000.. _usage: Accessing Translated and Translation Fields =========================================== Modeltranslation changes the behaviour of the translated fields. To explain this consider the news example from the :ref:`registration` chapter again. The original ``News`` model looked like this:: class News(models.Model): title = models.CharField(max_length=255) text = models.TextField() Now that it is registered with modeltranslation the model looks like this - note the additional fields automatically added by the app:: class News(models.Model): title = models.CharField(max_length=255) # original/translated field title_de = models.CharField(null=True, blank=True, max_length=255) # default translation field title_en = models.CharField(null=True, blank=True, max_length=255) # translation field text = models.TextField() # original/translated field text_de = models.TextField(null=True, blank=True) # default translation field text_en = models.TextField(null=True, blank=True) # translation field The example above assumes that the default language is ``de``, therefore the ``title_de`` and ``text_de`` fields are marked as the *default translation fields*. If the default language is ``en``, the ``title_en`` and ``text_en`` fields would be the *default translation fields*. .. _rules: Rules for Translated Field Access --------------------------------- .. versionchanged:: 0.5 So now when it comes to setting and getting the value of the original and the translation fields the following rules apply: **Rule 1** Reading the value from the original field returns the value translated to the current language. **Rule 2** Assigning a value to the original field updates the value in the associated current language translation field. **Rule 3** If both fields - the original and the current language translation field - are updated at the same time, the current language translation field wins. .. note:: This can only happen in the model's constructor or ``objects.create``. There is no other situation which can be considered *changing several fields at the same time*. Examples for Translated Field Access ------------------------------------ Because the whole point of using the modeltranslation app is translating dynamic content, the fields marked for translation are somehow special when it comes to accessing them. The value returned by a translated field is depending on the current language setting. "Language setting" is referring to the Django `set_language`_ view and the corresponding ``get_lang`` function. Assuming the current language is ``de`` in the news example from above, the translated ``title`` field will return the value from the ``title_de`` field:: # Assuming the current language is "de" n = News.objects.all()[0] t = n.title # returns german translation # Assuming the current language is "en" t = n.title # returns english translation This feature is implemented using Python descriptors making it happen without the need to touch the original model classes in any way. The descriptor uses the ``django.utils.i18n.get_language`` function to determine the current language. .. todo:: Add more examples. .. _multilingual_manager: Multilingual Manager -------------------- .. versionadded:: 0.5 Every model registered for translation is patched so that all its managers become subclasses of ``MultilingualManager`` (of course, if a custom manager was defined on the model, its functions will be retained). ``MultilingualManager`` simplifies language-aware queries, especially on third-party apps, by rewriting query field names. Every model's manager is patched, not only ``objects`` (even managers inherited from abstract base classes). For example:: # Assuming the current language is "de", # these queries returns the same objects news1 = News.objects.filter(title__contains='enigma') news2 = News.objects.filter(title_de__contains='enigma') assert news1 == news2 It works as follow: if the translation field name is used (``title``), it is changed into the current language field name (``title_de`` or ``title_en``, depending on the current active language). Any language-suffixed names are left untouched (so ``title_en`` wouldn't change, no matter what the current language is). Rewriting of field names works with operators (like ``__in``, ``__ge``) as well as with relationship spanning. Moreover, it is also handled on ``Q`` and ``F`` expressions. These manager methods perform rewriting: - ``filter()``, ``exclude()``, ``get()`` - ``order_by()`` - ``update()`` - ``only()``, ``defer()`` - ``values()``, ``values_list()``, with :ref:`fallback ` mechanism - ``dates()`` - ``select_related()`` - ``create()``, with optional auto-population_ feature In order not to introduce differences between ``X.objects.create(...)`` and ``X(...)``, model constructor is also patched and performs rewriting of field names prior to regular initialization. If one wants to turn rewriting of field names off, this can be easily achieved with ``rewrite(mode)`` method. ``mode`` is a boolean specifying whether rewriting should be applied. It can be changed several times inside a query. So ``X.objects.rewrite(False)`` turns rewriting off. ``MultilingualManager`` offers one additional method: ``raw_values``. It returns actual values from the database, without field names rewriting. Useful for checking translated field database value. Auto-population *************** .. versionchanged:: 0.6 There is special manager method ``populate(mode)`` which can trigger ``create()`` or ``get_or_create()`` to populate all translation (language) fields with values from translated (original) ones. It can be very convenient when working with many languages. So:: x = News.objects.populate(True).create(title='bar') is equivalent of:: x = News.objects.create(title_en='bar', title_de='bar') ## title_?? for every language Moreover, some fields can be explicitly assigned different values:: x = News.objects.populate(True).create(title='-- no translation yet --', title_de='enigma') It will result in ``title_de == 'enigma'`` and other ``title_?? == '-- no translation yet --'``. There is another way of altering the current population status, an ``auto_populate`` context manager:: from modeltranslation.utils import auto_populate with auto_populate(True): x = News.objects.create(title='bar') Auto-population takes place also in model constructor, what is extremely useful when loading non-translated fixtures. Just remember to use the context manager:: with auto_populate(): # True can be ommited call_command('loaddata', 'fixture.json') # Some fixture loading z = News(title='bar') print(z.title_en, z.title_de) # prints 'bar bar' There is a more convenient way than calling ``populate`` manager method or entering ``auto_populate`` manager context all the time: :ref:`settings-modeltranslation_auto_populate` setting. It controls the default population behaviour. .. _auto-population-modes: Auto-population modes ^^^^^^^^^^^^^^^^^^^^^ There are four different population modes: ``False`` [set by default] Auto-population turned off ``True`` or ``'all'`` [default argument to population altering methods] Auto-population turned on, copying translated field value to all other languages (unless a translation field value is provided) ``'default'`` Auto-population turned on, copying translated field value to default language field (unless its value is provided) ``'required'`` Acts like ``'default'``, but copy value only if the original field is non-nullable .. _fallback: Falling back ------------ Modeltranslation provides a mechanism to control behaviour of data access in case of empty translation values. This mechanism affects field access, as well as ``values()`` and ``values_list()`` manager methods. Consider the ``News`` example: a creator of some news hasn't specified its German title and content, but only English ones. Then if a German visitor is viewing the site, we would rather show him English title/content of the news than display empty strings. This is called *fallback*. :: news.title_en = 'English title' news.title_de = '' print(news.title) # If current active language is German, it should display the title_de field value (''). # But if fallback is enabled, it would display 'English title' instead. # Similarly for manager news.save() print(News.objects.filter(pk=news.pk).values_list('title', flat=True)[0]) # As above: if current active language is German and fallback to English is enabled, # it would display 'English title'. There are several ways of controlling fallback, described below. .. _fallback_lang: Fallback languages ****************** .. versionadded:: 0.5 :ref:`settings-modeltranslation_fallback_languages` setting allows to set the order of *fallback languages*. By default that's the ``DEFAULT_LANGUAGE``. For example, setting :: MODELTRANSLATION_FALLBACK_LANGUAGES = ('en', 'de', 'fr') means: if current active language field value is unset, try English value. If it is also unset, try German, and so on - until some language yields a non-empty value of the field. There is also an option to define a fallback by language, using dict syntax:: MODELTRANSLATION_FALLBACK_LANGUAGES = { 'default': ('en', 'de', 'fr'), 'fr': ('de',), 'uk': ('ru',) } The ``default`` key is required and its value denote languages which are always tried at the end. With such a setting: - for `uk` the order of fallback languages is: ``('ru', 'en', 'de', 'fr')`` - for `fr` the order of fallback languages is: ``('de', 'en')`` - Note, that `fr` obviously is not a fallback, since its active language and `de` would be tried before `en` - for `en` and `de` the fallback order is ``('de', 'fr')`` and ``('en', 'fr')``, respectively - for any other language the order of fallback languages is just ``('en', 'de', 'fr')`` What is more, fallback languages order can be overridden per model, using ``TranslationOptions``:: class NewsTranslationOptions(TranslationOptions): fields = ('title', 'text',) fallback_languages = {'default': ('fa', 'km')} # use Persian and Khmer as fallback for News Dict syntax is only allowed there. .. versionadded:: 0.6 Even more, all fallbacks may be switched on or off for just some exceptional block of code using:: from modeltranslation.utils import fallbacks with fallbacks(False): # Work with values for the active language only .. _fallback_val: Fallback values *************** .. versionadded:: 0.4 But what if current language and all fallback languages yield no field value? Then modeltranslation will use the field's *fallback value*, if one was defined. Fallback values are defined in ``TranslationOptions``, for example:: class NewsTranslationOptions(TranslationOptions): fields = ('title', 'text',) fallback_values = _('-- sorry, no translation provided --') In this case, if title is missing in active language and any of fallback languages, news title will be ``'-- sorry, no translation provided --'`` (maybe translated, since gettext is used). Empty text will be handled in same way. Fallback values can be also customized per model field:: class NewsTranslationOptions(TranslationOptions): fields = ('title', 'text',) fallback_values = { 'title': _('-- sorry, this news was not translated --'), 'text': _('-- please contact our translator (translator@example.com) --') } If current language and all fallback languages yield no field value, and no fallback values are defined, then modeltranslation will use the field's default value. .. _fallback_undef: Fallback undefined ****************** .. versionadded:: 0.7 Another question is what do we consider "no value", on what value should we fall back to other translations? For text fields the empty string can usually be considered as the undefined value, but other fields may have different concepts of empty or missing values. Modeltranslation defaults to using the field's default value as the undefined value (the empty string for non-nullable ``CharFields``). This requires calling ``get_default`` for every field access, which in some cases may be expensive. If you'd like to fall back on a different value or your default is expensive to calculate, provide a custom undefined value (for a field or model):: class NewsTranslationOptions(TranslationOptions): fields = ('title', 'text',) fallback_undefined = { 'title': 'no title', 'text': None } The State of the Original Field ------------------------------- .. versionchanged:: 0.5 .. versionchanged:: 0.12 As defined by the :ref:`rules`, accessing the original field is guaranteed to work on the associated translation field of the current language. This applies to both, read and write operations. The actual field value (which *can* still be accessed through ``instance.__dict__['original_field_name']``) however has to be considered **undetermined** once the field has been registered for translation. Attempts to keep the value in sync with either the default or current language's field value has raised a boatload of unpredictable side effects in older versions of modeltranslation. Since version 0.12 the original field is expected to have even more undetermined value. It's because Django 1.10 changed the way deferred fields work. .. warning:: Do not rely on the underlying value of the *original field* in any way! .. todo:: Perhaps outline effects this might have on the ``update_translation_field`` management command. .. _set_language: https://docs.djangoproject.com/en/dev/topics/i18n/translation/#set-language-redirect-view django-modeltranslation-0.19.14/get-django-version.py000077500000000000000000000003461500120161300225540ustar00rootroot00000000000000#!/usr/bin/env python import sys version = sys.argv[1] if version.startswith("http"): print(version) else: next_version = version[:-1] + "%d" % (int(version[-1]) + 1) print("Django>=%s,<%s" % (version, next_version)) django-modeltranslation-0.19.14/modeltranslation/000077500000000000000000000000001500120161300220515ustar00rootroot00000000000000django-modeltranslation-0.19.14/modeltranslation/__init__.py000066400000000000000000000001511500120161300241570ustar00rootroot00000000000000from modeltranslation._typing import monkeypatch # monkeypatch generic classes at runtime monkeypatch() django-modeltranslation-0.19.14/modeltranslation/_compat.py000066400000000000000000000042441500120161300240510ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Callable import django from typing import Iterable from typing import Optional if TYPE_CHECKING: from django.db.models import QuerySet from django.db.models.fields.reverse_related import ForeignObjectRel _django_version = django.VERSION[:2] def is_hidden(field: ForeignObjectRel) -> bool: return field.hidden def clear_ForeignObjectRel_caches(field: ForeignObjectRel): """ Django 5.1 Introduced caching for `accessor_name` props. We need to clear this cache when creating Translated field. https://github.com/django/django/commit/5e80390add100e0c7a1ac8e51739f94c5d706ea3#diff-e65b05ecbbe594164125af53550a43ef8a174f80811608012bc8e9e4ed575749 """ caches = ("accessor_name",) for name in caches: field.__dict__.pop(name, None) def build_refresh_from_db( old_refresh_from_db: Callable[ [Any, Optional[str], Optional[Iterable[str]], QuerySet[Any] | None], None ], ): from modeltranslation.manager import append_translated def refresh_from_db( self: Any, using: str | None = None, fields: Iterable[str] | None = None, from_queryset: QuerySet[Any] | None = None, ) -> None: if fields is not None: fields = append_translated(self.__class__, fields) return old_refresh_from_db(self, using, fields, from_queryset) return refresh_from_db if _django_version <= (5, 0): def is_hidden(field: ForeignObjectRel) -> bool: return field.is_hidden() # Django versions below 5.1 do not have `from_queryset` argument. def build_refresh_from_db( # type: ignore[misc] old_refresh_from_db: Callable[[Any, Optional[str], Optional[Iterable[str]]], None], ): from modeltranslation.manager import append_translated def refresh_from_db( self: Any, using: str | None = None, fields: Iterable[str] | None = None, ) -> None: if fields is not None: fields = append_translated(self.__class__, fields) return old_refresh_from_db(self, using, fields) return refresh_from_db django-modeltranslation-0.19.14/modeltranslation/_typing.py000066400000000000000000000026571500120161300241060ustar00rootroot00000000000000from __future__ import annotations import sys from typing import Literal, TypeVar, Union from django.contrib import admin from django.contrib.admin.options import BaseModelAdmin if sys.version_info >= (3, 11): from typing import Self, TypeAlias # noqa: F401 else: from typing_extensions import Self, TypeAlias # noqa: F401 AutoPopulate: TypeAlias = "bool | Literal['all', 'default', 'required']" _K = TypeVar("_K") # See https://github.com/typeddjango/django-stubs/blob/082955/django-stubs/utils/datastructures.pyi#L12-L14 _ListOrTuple: TypeAlias = Union[list[_K], tuple[_K, ...]] # https://github.com/typeddjango/django-stubs/tree/master/django_stubs_ext # For generic classes to work at runtime we need to define `__class_getitem__`. # We're defining it here, instead of relying on django_stubs_ext, because # we don't want every user setting up django_stubs_ext just for this feature. def monkeypatch() -> None: classes = [ admin.ModelAdmin, BaseModelAdmin, ] def class_getitem(cls: type, key: str | type | TypeVar): if isinstance(key, str) and hasattr(cls, key): # Fix django-cms compatibility: # https://github.com/django-cms/django-cms/issues/7948 raise KeyError(f"Key '{key}' found as attribute, use getattr to access it.") return cls for cls in classes: cls.__class_getitem__ = classmethod(class_getitem) # type: ignore[attr-defined] django-modeltranslation-0.19.14/modeltranslation/admin.py000066400000000000000000000462431500120161300235240ustar00rootroot00000000000000from __future__ import annotations from copy import deepcopy from typing import Any, TypeVar, TYPE_CHECKING from collections.abc import Iterable, Sequence from django import forms from django.db.models import Field, Model from django.contrib import admin from django.contrib.admin.options import BaseModelAdmin, InlineModelAdmin, flatten_fieldsets from django.contrib.contenttypes.admin import GenericStackedInline, GenericTabularInline from django.forms.models import BaseInlineFormSet from django.http.request import HttpRequest from modeltranslation import settings as mt_settings from modeltranslation.translator import translator from modeltranslation.utils import ( build_css_class, build_localized_fieldname, get_language, get_language_bidi, get_translation_fields, unique, ) from modeltranslation.widgets import ClearableWidgetWrapper from modeltranslation._typing import _ListOrTuple if TYPE_CHECKING: # We depend here or `django-stubs` internal `_FieldsetSpec`, # in case it changes, change import or define this internally. from django.contrib.admin.options import _FieldsetSpec _ModelT = TypeVar("_ModelT", bound=Model) class TranslationBaseModelAdmin(BaseModelAdmin[_ModelT]): _orig_was_required: dict[str, bool] = {} both_empty_values_fields = () def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.trans_opts = translator.get_options_for_model(self.model) self._patch_prepopulated_fields() def _get_declared_fieldsets( self, request: HttpRequest, obj: _ModelT | None = None ) -> _FieldsetSpec | None: # Take custom modelform fields option into account if not self.fields and hasattr(self.form, "_meta") and self.form._meta.fields: self.fields = self.form._meta.fields # type: ignore[assignment] # takes into account non-standard add_fieldsets attribute used by UserAdmin fieldsets = ( self.add_fieldsets if getattr(self, "add_fieldsets", None) and obj is None else self.fieldsets ) if fieldsets: return self._patch_fieldsets(fieldsets) elif self.fields: return [(None, {"fields": self.replace_orig_field(self.get_fields(request, obj))})] return None def _patch_fieldsets(self, fieldsets: _FieldsetSpec) -> _FieldsetSpec: fieldsets_new = list(fieldsets) for name, dct in fieldsets: if "fields" in dct: dct["fields"] = self.replace_orig_field(dct["fields"]) return fieldsets_new def formfield_for_dbfield( self, db_field: Field, request: HttpRequest, **kwargs: Any ) -> forms.Field | None: if field := super().formfield_for_dbfield(db_field, request, **kwargs): self.patch_translation_field(db_field, field, request, **kwargs) return field def patch_translation_field( self, db_field: Field, field: forms.Field, request: HttpRequest, **kwargs: Any ) -> None: if db_field.name in self.trans_opts.all_fields: if field.required: field.required = False field.blank = True self._orig_was_required["%s.%s" % (db_field.model._meta, db_field.name)] = True # For every localized field copy the widget from the original field # and add a css class to identify a modeltranslation widget. try: orig_field = db_field.translated_field except AttributeError: pass else: orig_formfield = self.formfield_for_dbfield(orig_field, request, **kwargs) if orig_formfield is None: return field.widget = deepcopy(orig_formfield.widget) attrs = field.widget.attrs # if any widget attrs are defined on the form they should be copied try: # this is a class: field.widget = deepcopy(self.form._meta.widgets[orig_field.name]) # type: ignore[index] if isinstance(field.widget, type): # if not initialized field.widget = field.widget(attrs) # initialize form widget with attrs except (AttributeError, TypeError, KeyError): pass # field.widget = deepcopy(orig_formfield.widget) if orig_field.name in self.both_empty_values_fields: from modeltranslation.forms import NullableField, NullCharField form_class = field.__class__ if issubclass(form_class, NullCharField): # NullableField don't work with NullCharField form_class.__bases__ = tuple( b for b in form_class.__bases__ if b != NullCharField ) field.__class__ = type( "Nullable%s" % form_class.__name__, (NullableField, form_class), {} ) if ( db_field.empty_value == "both" or orig_field.name in self.both_empty_values_fields ) and isinstance(field.widget, (forms.TextInput, forms.Textarea)): field.widget = ClearableWidgetWrapper(field.widget) css_classes = self._get_widget_from_field(field).attrs.get("class", "").split(" ") css_classes.append("mt") # Add localized fieldname css class css_classes.append(build_css_class(db_field.name, "mt-field")) # Add mt-bidi css class if language is bidirectional if get_language_bidi(db_field.language): css_classes.append("mt-bidi") if db_field.language == mt_settings.DEFAULT_LANGUAGE: # Add another css class to identify a default modeltranslation widget css_classes.append("mt-default") if orig_formfield.required or self._orig_was_required.get( "%s.%s" % (orig_field.model._meta, orig_field.name) ): # In case the original form field was required, make the # default translation field required instead. orig_formfield.required = False orig_formfield.blank = True field.required = True field.blank = False # Hide clearable widget for required fields if isinstance(field.widget, ClearableWidgetWrapper): field.widget = field.widget.widget self._get_widget_from_field(field).attrs["class"] = " ".join(css_classes) def _get_widget_from_field(self, field: forms.Field) -> Any: # retrieve "nested" widget in case of related field if isinstance(field.widget, admin.widgets.RelatedFieldWidgetWrapper): return field.widget.widget else: return field.widget def _exclude_original_fields(self, exclude: _ListOrTuple[str] | None = None) -> tuple[str, ...]: if exclude is None: exclude = tuple() if exclude: exclude_new = tuple(exclude) return exclude_new + tuple(self.trans_opts.all_fields.keys()) return tuple(self.trans_opts.all_fields.keys()) def replace_orig_field(self, option: Iterable[str | Sequence[str]]) -> _ListOrTuple[str]: """ Replaces each original field in `option` that is registered for translation by its translation fields. Returns a new list with replaced fields. If `option` contains no registered fields, it is returned unmodified. >>> self = TranslationAdmin() # PyFlakes >>> print(self.trans_opts.fields.keys()) ['title',] >>> get_translation_fields(self.trans_opts.fields.keys()[0]) ['title_de', 'title_en'] >>> self.replace_orig_field(['title', 'url']) ['title_de', 'title_en', 'url'] Note that grouped fields are flattened. We do this because: 1. They are hard to handle in the jquery-ui tabs implementation 2. They don't scale well with more than a few languages 3. It's better than not handling them at all (okay that's weak) >>> self.replace_orig_field((('title', 'url'), 'email', 'text')) ['title_de', 'title_en', 'url_de', 'url_en', 'email_de', 'email_en', 'text'] """ if option: option_new = list(option) for opt in option: if opt in self.trans_opts.all_fields: index = option_new.index(opt) option_new[index : index + 1] = get_translation_fields(opt) # type: ignore[arg-type] elif isinstance(opt, (tuple, list)) and ( [o for o in opt if o in self.trans_opts.all_fields] ): index = option_new.index(opt) option_new[index : index + 1] = self.replace_orig_field(opt) option = option_new return option # type: ignore[return-value] def _patch_prepopulated_fields(self) -> None: def localize(sources: Sequence[str], lang: str) -> tuple[str, ...]: "Append lang suffix (if applicable) to field list" def append_lang(source: str) -> str: if source in self.trans_opts.all_fields: return build_localized_fieldname(source, lang) return source return tuple(map(append_lang, sources)) prepopulated_fields: dict[str, Sequence[str]] = {} for dest, sources in self.prepopulated_fields.items(): if dest in self.trans_opts.all_fields: for lang in mt_settings.AVAILABLE_LANGUAGES: key = build_localized_fieldname(dest, lang) prepopulated_fields[key] = localize(sources, lang) else: lang = mt_settings.PREPOPULATE_LANGUAGE or get_language() prepopulated_fields[dest] = localize(sources, lang) self.prepopulated_fields = prepopulated_fields def _get_form_or_formset( self, request: HttpRequest, obj: Model | None, **kwargs: Any ) -> dict[str, Any]: """ Generic code shared by get_form and get_formset. """ exclude = self.get_exclude(request, obj) # type: ignore[arg-type] if exclude is None: exclude = [] else: exclude = list(exclude) exclude.extend(self.get_readonly_fields(request, obj)) # type: ignore[arg-type] if not exclude and hasattr(self.form, "_meta") and self.form._meta.exclude: # Take the custom ModelForm's Meta.exclude into account only if the # ModelAdmin doesn't define its own. exclude.extend(self.form._meta.exclude) # If exclude is an empty list we pass None to be consistent with the # default on modelform_factory exclude = self.replace_orig_field(exclude) or None exclude = self._exclude_original_fields(exclude) kwargs.update({"exclude": exclude}) return kwargs def _get_fieldsets_pre_form_or_formset( self, request: HttpRequest, obj: _ModelT | None = None ) -> _FieldsetSpec | None: """ Generic get_fieldsets code, shared by TranslationAdmin and TranslationInlineModelAdmin. """ return self._get_declared_fieldsets(request, obj) def _get_fieldsets_post_form_or_formset( self, request: HttpRequest, form: type[forms.ModelForm], obj: _ModelT | None = None ) -> list: """ Generic get_fieldsets code, shared by TranslationAdmin and TranslationInlineModelAdmin. """ base_fields = self.replace_orig_field(form.base_fields.keys()) fields = list(base_fields) + list(self.get_readonly_fields(request, obj)) return [(None, {"fields": self.replace_orig_field(fields)})] def get_readonly_fields( self, request: HttpRequest, obj: _ModelT | None = None ) -> _ListOrTuple[str]: """ Hook to specify custom readonly fields. """ return self.replace_orig_field(self.readonly_fields) class TranslationAdmin(TranslationBaseModelAdmin[_ModelT], admin.ModelAdmin[_ModelT]): # TODO: Consider addition of a setting which allows to override the fallback to True group_fieldsets = False def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._patch_list_editable() def _patch_list_editable(self) -> None: if self.list_editable: editable_new = list(self.list_editable) display_new = list(self.list_display) for field in self.list_editable: if field in self.trans_opts.all_fields: index = editable_new.index(field) display_index = display_new.index(field) translation_fields = get_translation_fields(field) editable_new[index : index + 1] = translation_fields display_new[display_index : display_index + 1] = translation_fields self.list_editable = editable_new self.list_display = display_new def _group_fieldsets(self, fieldsets: list) -> list: # Fieldsets are not grouped by default. The function is activated by # setting TranslationAdmin.group_fieldsets to True. If the admin class # already defines a fieldset, we leave it alone and assume the author # has done whatever grouping for translated fields they desire. if self.group_fieldsets is True: flattened_fieldsets = flatten_fieldsets(fieldsets) # Create a fieldset to group each translated field's localized fields fields = sorted(f for f in self.opts.get_fields() if f.concrete) # type: ignore[type-var] untranslated_fields = [ f.name for f in fields if ( # Exclude the primary key field f is not self.opts.auto_field # Exclude non-editable fields and f.editable # Exclude the translation fields and not hasattr(f, "translated_field") # Honour field arguments. We rely on the fact that the # passed fieldsets argument is already fully filtered # and takes options like exclude into account. and f.name in flattened_fieldsets ) ] # TODO: Allow setting a label fieldsets = ( [ ( "", {"fields": untranslated_fields}, ) ] if untranslated_fields else [] ) temp_fieldsets = {} for orig_field, trans_fields in self.trans_opts.all_fields.items(): trans_fieldnames = [f.name for f in sorted(trans_fields, key=lambda x: x.name)] if any(f in trans_fieldnames for f in flattened_fieldsets): # Extract the original field's verbose_name for use as this # fieldset's label - using gettext_lazy in your model # declaration can make that translatable. label = self.model._meta.get_field(orig_field).verbose_name.capitalize() # type: ignore[union-attr] temp_fieldsets[orig_field] = ( label, {"fields": trans_fieldnames, "classes": ("mt-fieldset",)}, ) fields_order = unique( f.translated_field.name for f in self.opts.fields if hasattr(f, "translated_field") and f.name in flattened_fieldsets ) for field_name in fields_order: fieldsets.append(temp_fieldsets.pop(field_name)) assert not temp_fieldsets # cleaned return fieldsets def get_form( self, request: HttpRequest, obj: _ModelT | None = None, **kwargs: Any ) -> type[forms.ModelForm]: kwargs = self._get_form_or_formset(request, obj, **kwargs) return super().get_form(request, obj, **kwargs) def get_fieldsets(self, request: HttpRequest, obj: _ModelT | None = None) -> _FieldsetSpec: return self._get_fieldsets_pre_form_or_formset(request, obj) or self._group_fieldsets( self._get_fieldsets_post_form_or_formset( request, self.get_form(request, obj, fields=None), obj ) ) _ChildModelT = TypeVar("_ChildModelT", bound=Model) _ParentModelT = TypeVar("_ParentModelT", bound=Model) class TranslationInlineModelAdmin( TranslationBaseModelAdmin[_ChildModelT], InlineModelAdmin[_ChildModelT, _ParentModelT] ): def get_formset( self, request: HttpRequest, obj: _ParentModelT | None = None, **kwargs: Any ) -> type[BaseInlineFormSet]: kwargs = self._get_form_or_formset(request, obj, **kwargs) return super().get_formset(request, obj, **kwargs) def get_fieldsets(self, request: HttpRequest, obj: _ChildModelT | None = None): # FIXME: If fieldsets are declared on an inline some kind of ghost # fieldset line with just the original model verbose_name of the model # is displayed above the new fieldsets. declared_fieldsets = self._get_fieldsets_pre_form_or_formset(request, obj) if declared_fieldsets: return declared_fieldsets form = self.get_formset(request, obj, fields=None).form # type: ignore[arg-type] return self._get_fieldsets_post_form_or_formset(request, form, obj) class TranslationTabularInline( TranslationInlineModelAdmin[_ChildModelT, _ParentModelT], admin.TabularInline[_ChildModelT, _ParentModelT], ): pass class TranslationStackedInline( TranslationInlineModelAdmin[_ChildModelT, _ParentModelT], admin.StackedInline[_ChildModelT, _ParentModelT], ): pass class TranslationGenericTabularInline( TranslationInlineModelAdmin[_ChildModelT, _ParentModelT], GenericTabularInline ): pass class TranslationGenericStackedInline( TranslationInlineModelAdmin[_ChildModelT, _ParentModelT], GenericStackedInline ): pass class TabbedDjangoJqueryTranslationAdmin(TranslationAdmin[_ModelT]): """ Convenience class which includes the necessary media files for tabbed translation fields. Reuses Django's internal jquery version. """ class Media: js = ( "admin/js/jquery.init.js", "modeltranslation/js/force_jquery.js", mt_settings.JQUERY_UI_URL, "modeltranslation/js/tabbed_translation_fields.js", ) css = { "all": ("modeltranslation/css/tabbed_translation_fields.css",), } class TabbedExternalJqueryTranslationAdmin(TranslationAdmin[_ModelT]): """ Convenience class which includes the necessary media files for tabbed translation fields. Loads recent jquery version from a cdn. """ class Media: js = ( mt_settings.JQUERY_URL, mt_settings.JQUERY_UI_URL, "modeltranslation/js/tabbed_translation_fields.js", ) css = { "screen": ("modeltranslation/css/tabbed_translation_fields.css",), } TabbedTranslationAdmin = TabbedDjangoJqueryTranslationAdmin django-modeltranslation-0.19.14/modeltranslation/apps.py000066400000000000000000000004501500120161300233650ustar00rootroot00000000000000from django.apps import AppConfig class ModeltranslationConfig(AppConfig): name = "modeltranslation" verbose_name = "Modeltranslation" def ready(self) -> None: from modeltranslation.models import handle_translation_registrations handle_translation_registrations() django-modeltranslation-0.19.14/modeltranslation/decorators.py000066400000000000000000000025031500120161300245700ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Callable, TypeVar from collections.abc import Iterable from django.db.models import Model if TYPE_CHECKING: from modeltranslation.translator import TranslationOptions _TranslationOptionsTypeT = TypeVar("_TranslationOptionsTypeT", bound=type[TranslationOptions]) def register( model_or_iterable: type[Model] | Iterable[type[Model]], **options: Any ) -> Callable[[_TranslationOptionsTypeT], _TranslationOptionsTypeT]: """ Registers the given model(s) with the given translation options. The model(s) should be Model classes, not instances. Fields declared for translation on a base class are inherited by subclasses. If the model or one of its subclasses is already registered for translation, this will raise an exception. @register(Author) class AuthorTranslation(TranslationOptions): pass """ from modeltranslation.translator import TranslationOptions, translator def wrapper(opts_class: _TranslationOptionsTypeT) -> _TranslationOptionsTypeT: if not issubclass(opts_class, TranslationOptions): raise ValueError("Wrapped class must subclass TranslationOptions.") translator.register(model_or_iterable, opts_class, **options) return opts_class return wrapper django-modeltranslation-0.19.14/modeltranslation/fields.py000066400000000000000000000522161500120161300236770ustar00rootroot00000000000000from __future__ import annotations import copy from typing import Any, cast from collections.abc import Sequence from django import forms from django.core.exceptions import ImproperlyConfigured from django.db.models import Model, fields from django.utils.encoding import force_str from django.utils.functional import Promise from django.utils.translation import override from modeltranslation import settings as mt_settings from modeltranslation.thread_context import fallbacks_enabled from modeltranslation.utils import ( build_localized_fieldname, build_localized_intermediary_model, build_localized_verbose_name, get_language, resolution_order, ) from modeltranslation.widgets import ClearableWidgetWrapper from ._typing import Self from ._compat import is_hidden, clear_ForeignObjectRel_caches SUPPORTED_FIELDS = ( fields.CharField, # Above implies also CommaSeparatedIntegerField, EmailField, FilePathField, SlugField # and URLField as they are subclasses of CharField. fields.TextField, fields.json.JSONField, fields.IntegerField, # Above implies also BigIntegerField, SmallIntegerField, PositiveIntegerField and # PositiveSmallIntegerField, as they are subclasses of IntegerField. fields.BooleanField, fields.NullBooleanField, fields.FloatField, fields.DecimalField, fields.IPAddressField, fields.GenericIPAddressField, fields.DateField, fields.DateTimeField, fields.TimeField, fields.files.FileField, fields.files.ImageField, fields.related.ForeignKey, # Above implies also OneToOneField fields.related.ManyToManyField, ) class NONE: """ Used for fallback options when they are not provided (``None`` can be given as a fallback or undefined value) or to mark that a nullable value is not yet known and needs to be computed (e.g. field default). """ pass def create_translation_field(model: type[Model], field_name: str, lang: str, empty_value: Any): """ Translation field factory. Returns a ``TranslationField`` based on a fieldname and a language. The list of supported fields can be extended by defining a tuple of field names in the projects settings.py like this:: MODELTRANSLATION_CUSTOM_FIELDS = ('MyField', 'MyOtherField',) If the class is neither a subclass of fields in ``SUPPORTED_FIELDS``, nor in ``CUSTOM_FIELDS`` an ``ImproperlyConfigured`` exception will be raised. """ if empty_value not in ("", "both", None, NONE): raise ImproperlyConfigured("%s is not a valid empty_value." % empty_value) field = cast(fields.Field, model._meta.get_field(field_name)) cls_name = field.__class__.__name__ if not (isinstance(field, SUPPORTED_FIELDS) or cls_name in mt_settings.CUSTOM_FIELDS): raise ImproperlyConfigured("%s is not supported by modeltranslation." % cls_name) translation_class = field_factory(field.__class__) return translation_class(translated_field=field, language=lang, empty_value=empty_value) def field_factory(baseclass: type[fields.Field]) -> type[TranslationField]: class TranslationFieldSpecific(TranslationField, baseclass): # type: ignore[valid-type, misc] pass # Reflect baseclass name of returned subclass TranslationFieldSpecific.__name__ = "Translation%s" % baseclass.__name__ return TranslationFieldSpecific class TranslationField: """ The translation field functions as a proxy to the original field which is wrapped. For every field defined in the model's ``TranslationOptions`` localized versions of that field are added to the model depending on the languages given in ``settings.LANGUAGES``. If for example there is a model ``News`` with a field ``title`` which is registered for translation and the ``settings.LANGUAGES`` contains the ``de`` and ``en`` languages, the fields ``title_de`` and ``title_en`` will be added to the model class. These fields are realized using this descriptor. The translation field needs to know which language it contains therefore that needs to be specified when the field is created. """ def __init__( self, translated_field: fields.Field, language: str, empty_value: Any, *args: Any, **kwargs: Any, ) -> None: from modeltranslation.translator import translator # Update the dict of this field with the content of the original one # This might be a bit radical?! Seems to work though... self.__dict__.update(translated_field.__dict__) # Store the originally wrapped field for later self.translated_field = translated_field self.language = language self.empty_value = empty_value if empty_value is NONE: self.empty_value = None if translated_field.null else "" # Default behaviour is that all translations are optional if not isinstance(self, fields.BooleanField): # TODO: Do we really want to enforce null *at all*? Shouldn't this # better honour the null setting of the translated field? self.null = True # We preserve original blank value for translated fields, # when they're in 'required_languages'. # So they will be only required if original field itself was required. original_blank = self.blank self.blank = True # Take required_languages translation option into account. trans_opts = translator.get_options_for_model(self.model) if trans_opts.required_languages: required_languages = trans_opts.required_languages if isinstance(required_languages, (tuple, list)): # All fields if self.language in required_languages: # self.null = False self.blank = original_blank else: # Certain fields only # Try current language - if not present, try 'default' key try: req_fields = required_languages[self.language] except KeyError: req_fields = required_languages.get("default", ()) if self.name in req_fields: # TODO: We might have to handle the whole thing through the # FieldsAggregationMetaClass, as fields can be inherited. # self.null = False self.blank = original_blank # Adjust the name of this field to reflect the language self.attname = build_localized_fieldname(self.translated_field.name, language) self.name = self.attname if self.translated_field.db_column: self.db_column = build_localized_fieldname(self.translated_field.db_column, language) self.column = self.db_column # Copy the verbose name and append a language suffix # (will show up e.g. in the admin). self.verbose_name = build_localized_verbose_name(translated_field.verbose_name, language) if self.remote_field: clear_ForeignObjectRel_caches(self.remote_field) # M2M support - if isinstance(self.translated_field, fields.related.ManyToManyField) and hasattr( self.remote_field, "through" ): # Since fields cannot share the same remote_field object: self.remote_field = copy.copy(self.remote_field) # To support multiple relations to self, must provide a non null language scoped related_name if self.remote_field.symmetrical and ( self.remote_field.model == "self" or self.remote_field.model == self.model._meta.object_name or self.remote_field.model == self.model ): self.remote_field.related_name = "%s_rel_+" % self.name elif is_hidden(self.remote_field): # Even if the backwards relation is disabled, django internally uses it, need to use a language scoped related_name self.remote_field.related_name = "_%s_%s_+" % ( self.model.__name__.lower(), self.name, ) else: # Default case with standard related_name must also include language scope if self.remote_field.related_name is None: # For implicit related_name use different query field name loc_related_query_name = build_localized_fieldname( self.related_query_name(), self.language ) self.related_query_name = lambda: loc_related_query_name self.remote_field.related_name = "%s_set" % ( build_localized_fieldname(self.model.__name__.lower(), language), ) else: self.remote_field.related_name = build_localized_fieldname( self.remote_field.get_accessor_name(), language ) # Patch intermediary model with language scope to create correct db table self.remote_field.through = build_localized_intermediary_model( self.remote_field.through, language ) self.remote_field.field = self if hasattr(self.remote_field.model._meta, "_related_objects_cache"): del self.remote_field.model._meta._related_objects_cache elif self.remote_field and not is_hidden(self.remote_field): current = self.remote_field.get_accessor_name() # Since fields cannot share the same rel object: self.remote_field = copy.copy(self.remote_field) if self.remote_field.related_name is None: # For implicit related_name use different query field name loc_related_query_name = build_localized_fieldname( self.related_query_name(), self.language ) self.related_query_name = lambda: loc_related_query_name self.remote_field.related_name = build_localized_fieldname(current, self.language) self.remote_field.field = self if hasattr(self.remote_field.model._meta, "_related_objects_cache"): del self.remote_field.model._meta._related_objects_cache # Django 1.5 changed definition of __hash__ for fields to be fine with hash requirements. # It spoiled our machinery, since TranslationField has the same creation_counter as its # original field and fields didn't get added to sets. # So here we override __eq__ and __hash__ to fix the issue while retaining fine with # http://docs.python.org/2.7/reference/datamodel.html#object.__hash__ def __eq__(self, other: object) -> bool: if isinstance(other, fields.Field): return self.creation_counter == other.creation_counter and self.language == getattr( other, "language", None ) return super().__eq__(other) def __ne__(self, other: object) -> bool: return not self.__eq__(other) def __hash__(self) -> int: return hash((self.creation_counter, self.language)) def get_default(self) -> Any: with override(self.language): default = super().get_default() # we must *force evaluation* at this point, otherwise the lazy translatable # string is returned and will be evaluated only later when the main language # is activated again (because this context block has exited). # # force_str passes protected types as-is, which includes None, int, float, # datetime... if isinstance(default, Promise): default = force_str(default, strings_only=True) return default def formfield(self, *args: Any, **kwargs: Any) -> forms.Field: """ Returns proper formfield, according to empty_values setting (only for ``forms.CharField`` subclasses). There are 3 different formfields: - CharField that stores all empty values as empty strings; - NullCharField that stores all empty values as None (Null); - NullableField that can store both None and empty string. By default, if no empty_values was specified in model's translation options, NullCharField would be used if the original field is nullable, CharField otherwise. This can be overridden by setting empty_values to '' or None. Setting 'both' will result in NullableField being used. Textual widgets (subclassing ``TextInput`` or ``Textarea``) used for nullable fields are enriched with a clear checkbox, allowing ``None`` values to be preserved rather than saved as empty strings. The ``forms.CharField`` somewhat surprising behaviour is documented as a "won't fix": https://code.djangoproject.com/ticket/9590. """ formfield = super().formfield(*args, **kwargs) if isinstance(formfield, forms.CharField): if self.empty_value is None: from modeltranslation.forms import NullCharField form_class = formfield.__class__ kwargs["form_class"] = type( "Null%s" % form_class.__name__, (NullCharField, form_class), {} ) formfield = super().formfield(*args, **kwargs) elif self.empty_value == "both": from modeltranslation.forms import NullableField form_class = formfield.__class__ kwargs["form_class"] = type( "Nullable%s" % form_class.__name__, (NullableField, form_class), {} ) formfield = super().formfield(*args, **kwargs) if isinstance(formfield.widget, (forms.TextInput, forms.Textarea)): formfield.widget = ClearableWidgetWrapper(formfield.widget) return formfield def save_form_data(self, instance: Model, data: Any, check: bool = True) -> None: # Allow 3rd-party apps forms to be saved using only translated field name. # When translated field (e.g. 'name') is specified and translation field (e.g. 'name_en') # not, we assume that form was saved without knowledge of modeltranslation and we make # things right: # Translated field is saved first, settings respective translation field value. Then # translation field is being saved without value - and we handle this here (only for # active language). # Questionable fields are stored in special variable, which is later handled by clean_fields # method on the model. if check and self.language == get_language() and getattr(instance, self.name) and not data: if not hasattr(instance, "_mt_form_pending_clear"): instance._mt_form_pending_clear = {} instance._mt_form_pending_clear[self.name] = data else: super().save_form_data(instance, data) def deconstruct(self) -> tuple[str, str, Sequence[Any], dict[str, Any]]: name, path, args, kwargs = self.translated_field.deconstruct() if self.null is True: kwargs.update({"null": True}) if "db_column" in kwargs: kwargs["db_column"] = self.db_column return self.name, path, args, kwargs def clone(self) -> Self: from django.utils.module_loading import import_string name, path, args, kwargs = self.deconstruct() cls = import_string(path) return cls(*args, **kwargs) class TranslationFieldDescriptor: """ A descriptor used for the original translated field. """ def __init__( self, field: fields.Field, fallback_languages: dict[str, tuple[str, ...]] | None = None, fallback_value: Any = NONE, fallback_undefined: Any = NONE, ) -> None: """ Stores fallback options and the original field, so we know it's name and default. """ self.field = field self.fallback_languages = fallback_languages self.fallback_value = fallback_value self.fallback_undefined = fallback_undefined def __set__(self, instance, value): """ Updates the translation field for the current language. """ # In order for deferred fields to work, we also need to set the base value instance.__dict__[self.field.name] = value if isinstance(self.field, fields.related.ForeignKey): instance.__dict__[self.field.get_attname()] = None if value is None else value.pk if getattr(instance, "_mt_init", False) or getattr(instance, "_mt_disable", False): # When assignment takes place in model instance constructor, don't set value. # This is essential for only/defer to work, but I think it's sensible anyway. # Setting the localized field may also be disabled by setting _mt_disable. return loc_field_name = build_localized_fieldname(self.field.name, get_language()) setattr(instance, loc_field_name, value) def meaningful_value(self, val, undefined): """ Check if val is considered non-empty. """ if isinstance(val, fields.files.FieldFile): return val.name and not ( isinstance(undefined, fields.files.FieldFile) and val == undefined ) return val is not None and val != undefined def __get__(self, instance, owner): """ Returns value from the translation field for the current language, or value for some another language according to fallback languages, or the custom fallback value, or field's default value. """ if instance is None: return self default = NONE undefined = self.fallback_undefined if undefined is NONE: default = self.field.get_default() undefined = default langs = resolution_order(get_language(), self.fallback_languages) for lang in langs: loc_field_name = build_localized_fieldname(self.field.name, lang) val = getattr(instance, loc_field_name, None) if self.meaningful_value(val, undefined): return val if fallbacks_enabled() and self.fallback_value is not NONE: return self.fallback_value else: if default is NONE: default = self.field.get_default() # Some fields like FileField behave strange, as their get_default() doesn't return # instance of attr_class, but rather None or ''. # Normally this case is handled in the descriptor, but since we have overridden it, we # must mock it up. if isinstance(self.field, fields.files.FileField) and not isinstance( default, self.field.attr_class ): return self.field.attr_class(instance, self.field, default) return default class TranslatedRelationIdDescriptor: """ A descriptor used for the original '_id' attribute of a translated ForeignKey field. """ def __init__( self, field_name: str, fallback_languages: dict[str, tuple[str, ...]] | None ) -> None: self.field_name = field_name # The name of the original field (excluding '_id') self.fallback_languages = fallback_languages def __set__(self, instance, value): lang = get_language() loc_field_name = build_localized_fieldname(self.field_name, lang) # Localized field name with '_id' loc_attname = instance._meta.get_field(loc_field_name).get_attname() setattr(instance, loc_attname, value) base_attname = instance._meta.get_field(self.field_name).get_attname() instance.__dict__[base_attname] = value def __get__(self, instance, owner): if instance is None: return self langs = resolution_order(get_language(), self.fallback_languages) for lang in langs: loc_field_name = build_localized_fieldname(self.field_name, lang) # Localized field name with '_id' loc_attname = instance._meta.get_field(loc_field_name).get_attname() val = getattr(instance, loc_attname, None) if val is not None: return val return None class TranslatedManyToManyDescriptor: """ A descriptor used to return correct related manager without language fallbacks. """ def __init__( self, field_name: str, fallback_languages: dict[str, tuple[str, ...]] | None ) -> None: self.field_name = field_name # The name of the original field self.fallback_languages = fallback_languages def __get__(self, instance, owner): # TODO: do we really need to handle fallbacks with m2m relations? loc_field_name = build_localized_fieldname(self.field_name, get_language()) loc_attname = (instance or owner)._meta.get_field(loc_field_name).get_attname() return getattr((instance or owner), loc_attname) def __set__(self, instance, value): loc_field_name = build_localized_fieldname(self.field_name, get_language()) loc_attname = instance._meta.get_field(loc_field_name).get_attname() setattr(instance, loc_attname, value) django-modeltranslation-0.19.14/modeltranslation/forms.py000066400000000000000000000031111500120161300235450ustar00rootroot00000000000000from __future__ import annotations from typing import Any from django import forms from django.core import validators from modeltranslation.fields import TranslationField class TranslationModelForm(forms.ModelForm): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) for f in self._meta.model._meta.fields: if f.name in self.fields and isinstance(f, TranslationField): del self.fields[f.name] class NullCharField(forms.CharField): """ CharField subclass that returns ``None`` when ``CharField`` would return empty string. """ def to_python(self, value: Any | None) -> str | None: if value in validators.EMPTY_VALUES: return None return super().to_python(value) class NullableField(forms.Field): """ Form field mixin that ensures that ``None`` is not cast to anything (like the empty string with ``CharField`` and its derivatives). """ def to_python(self, value: Any | None) -> Any | None: if value is None: return value return super().to_python(value) # Django 1.6 def _has_changed(self, initial, data): return self.has_changed(initial, data) def has_changed(self, initial, data): if (initial is None and data is not None) or (initial is not None and data is None): return True obj = super() if hasattr(obj, "has_changed"): return obj.has_changed(initial, data) else: # Django < 1.9 compat return obj._has_changed(initial, data) django-modeltranslation-0.19.14/modeltranslation/management/000077500000000000000000000000001500120161300241655ustar00rootroot00000000000000django-modeltranslation-0.19.14/modeltranslation/management/__init__.py000066400000000000000000000000001500120161300262640ustar00rootroot00000000000000django-modeltranslation-0.19.14/modeltranslation/management/commands/000077500000000000000000000000001500120161300257665ustar00rootroot00000000000000django-modeltranslation-0.19.14/modeltranslation/management/commands/__init__.py000066400000000000000000000000001500120161300300650ustar00rootroot00000000000000django-modeltranslation-0.19.14/modeltranslation/management/commands/loaddata.py000066400000000000000000000045671500120161300301250ustar00rootroot00000000000000from __future__ import annotations from typing import Any from argparse import Action, Namespace from django.core.management.base import CommandParser from django.core.management.commands.loaddata import Command as LoadDataCommand # Because this command is used (instead of default loaddata), then settings have been imported # and we can safely import MT modules from modeltranslation import settings as mt_settings from modeltranslation.utils import auto_populate from modeltranslation._typing import AutoPopulate ALLOWED = (None, False, "all", "default", "required") ALLOWED_FOR_PRINT = ", ".join(str(i) for i in (0,) + ALLOWED[1:]) # For pretty-printing def check_mode( option: Command.CheckAction, opt_str: str | None, value: str, parser: CommandParser, namespace: Namespace | None = None, ) -> None: if value == "0" or value.lower() == "false": value = False # type: ignore[assignment] if value not in ALLOWED: raise ValueError("%s option can be only one of: %s" % (opt_str, ALLOWED_FOR_PRINT)) setattr(namespace or parser.values, option.dest, value) # type: ignore[attr-defined] class Command(LoadDataCommand): leave_locale_alone = mt_settings.LOADDATA_RETAIN_LOCALE # Django 1.6 class CheckAction(Action): def __call__( self, parser: CommandParser, # type: ignore[override] namespace: Namespace, value: str, # type: ignore[override] option_string: str | None = None, ) -> None: check_mode(self, option_string, value, parser, namespace) def add_arguments(self, parser: CommandParser) -> None: super().add_arguments(parser) parser.add_argument( "--populate", action=self.CheckAction, type=str, dest="populate", metavar="MODE", help=( "Using this option will cause fixtures to be loaded under auto-population MODE. " + "Allowed values are: %s" % ALLOWED_FOR_PRINT ), ) def handle(self, *fixture_labels: Any, **options: Any) -> str | None: mode: AutoPopulate | None = options.get("populate") if mode is not None: with auto_populate(mode): return super().handle(*fixture_labels, **options) else: return super().handle(*fixture_labels, **options) django-modeltranslation-0.19.14/modeltranslation/management/commands/sync_translation_fields.py000066400000000000000000000133431500120161300332640ustar00rootroot00000000000000""" Detect new translatable fields in all models and sync database structure. You will need to execute this command in two cases: 1. When you add new languages to settings.LANGUAGES. 2. When you add new translatable fields to your models. Credits: Heavily inspired by django-transmeta's sync_transmeta_db command. """ from __future__ import annotations from typing import Any, cast from collections.abc import Iterator from django.core.management.base import BaseCommand, CommandParser from django.core.management.color import no_style from django.db import connection from django.db.models import Model, Field from modeltranslation.settings import AVAILABLE_LANGUAGES from modeltranslation.translator import translator from modeltranslation.utils import build_localized_fieldname def ask_for_confirmation(sql_sentences: list[str], model_full_name: str, interactive: bool) -> bool: print('\nSQL to synchronize "%s" schema:' % model_full_name) for sentence in sql_sentences: print(" %s" % sentence) while True: prompt = "\nAre you sure that you want to execute the previous SQL: (y/n) [n]: " if interactive: answer = input(prompt).strip() else: answer = "y" if answer == "": return False elif answer not in ("y", "n", "yes", "no"): print("Please answer yes or no") elif answer == "y" or answer == "yes": return True else: return False def print_missing_langs(missing_langs: list[str], field_name: str, model_name: str) -> None: print( 'Missing languages in "%s" field from "%s" model: %s' % (field_name, model_name, ", ".join(missing_langs)) ) class Command(BaseCommand): help = ( "Detect new translatable fields or new available languages and" " sync database structure. Does not remove columns of removed" " languages or undeclared fields." ) def add_arguments(self, parser: CommandParser) -> None: ( parser.add_argument( "--noinput", action="store_false", dest="interactive", default=True, help="Do NOT prompt the user for input of any kind.", ), ) def handle(self, *args: Any, **options: Any) -> None: """ Command execution. """ self.cursor = connection.cursor() self.introspection = connection.introspection self.interactive = options["interactive"] found_missing_fields = False models = translator.get_registered_models(abstract=False) for model in models: db_table = model._meta.db_table model_name = model._meta.model_name model_full_name = "%s.%s" % (model._meta.app_label, model_name) opts = translator.get_options_for_model(model) for field_name, fields in opts.local_fields.items(): # Take `db_column` attribute into account try: field = list(fields)[0] except IndexError: # Ignore IndexError for ProxyModel # maybe there is better way to handle this continue column_name = field.db_column if field.db_column else field_name missing_langs = list(self.get_missing_languages(column_name, db_table)) if missing_langs: found_missing_fields = True print_missing_langs(missing_langs, field_name, model_full_name) sql_sentences = self.get_sync_sql(field_name, missing_langs, model) execute_sql = ask_for_confirmation( sql_sentences, model_full_name, self.interactive ) if execute_sql: print("Executing SQL...") for sentence in sql_sentences: self.cursor.execute(sentence) print("Done") else: print("SQL not executed") if not found_missing_fields: print("No new translatable fields detected") def get_table_fields(self, db_table: str) -> list[str]: """ Gets table fields from schema. """ db_table_desc = self.introspection.get_table_description(self.cursor, db_table) return [t[0] for t in db_table_desc] def get_missing_languages(self, field_name: str, db_table: str) -> Iterator[str]: """ Gets only missing fields. """ db_table_fields = self.get_table_fields(db_table) for lang_code in AVAILABLE_LANGUAGES: if build_localized_fieldname(field_name, lang_code) not in db_table_fields: yield lang_code def get_sync_sql( self, field_name: str, missing_langs: list[str], model: type[Model] ) -> list[str]: """ Returns SQL needed for sync schema for a new translatable field. """ qn = connection.ops.quote_name style = no_style() sql_output: list[str] = [] db_table = model._meta.db_table for lang in missing_langs: new_field = build_localized_fieldname(field_name, lang) f = cast(Field, model._meta.get_field(new_field)) col_type = f.db_type(connection=connection) field_sql = [style.SQL_FIELD(qn(f.column)), style.SQL_COLTYPE(col_type)] # type: ignore[arg-type] # column creation stmt = "ALTER TABLE %s ADD COLUMN %s" % (qn(db_table), " ".join(field_sql)) if not f.null: stmt += " " + style.SQL_KEYWORD("NOT NULL") sql_output.append(stmt + ";") return sql_output django-modeltranslation-0.19.14/modeltranslation/management/commands/update_translation_fields.py000066400000000000000000000100601500120161300335630ustar00rootroot00000000000000from typing import Any from django.core.management.base import BaseCommand, CommandParser, CommandError from django.db.models import F, ManyToManyField, Q from modeltranslation.settings import AVAILABLE_LANGUAGES, DEFAULT_LANGUAGE from modeltranslation.translator import translator from modeltranslation.utils import build_localized_fieldname COMMASPACE = ", " class Command(BaseCommand): help = ( "Updates empty values of translation fields using" " values from original fields (in all translated models)." ) def add_arguments(self, parser: CommandParser) -> None: parser.add_argument( "app_label", nargs="?", help="App label of an application to update empty values.", ) parser.add_argument( "model_name", nargs="?", help="Model name to update empty values of only this model.", ) parser.add_argument( "--language", action="store", help=( "Language translation field the be updated. Default language field if not provided" ), ) def handle(self, *args: Any, **options: Any) -> None: verbosity: int = options["verbosity"] if verbosity > 0: self.stdout.write("Using default language: %s" % DEFAULT_LANGUAGE) # get all models excluding proxy- and not managed models models = translator.get_registered_models(abstract=False) models = [m for m in models if not m._meta.proxy and m._meta.managed] # optionally filter by given app_label app_label = options["app_label"] if app_label: models = [m for m in models if m._meta.app_label == app_label] # optionally filter by given model_name model_name = options["model_name"] if model_name: model_name = model_name.lower() models = [m for m in models if m._meta.model_name == model_name] # optionally defining the translation field language lang = options.get("language") or DEFAULT_LANGUAGE if lang not in AVAILABLE_LANGUAGES: raise CommandError( "Cannot find language '%s'. Options are %s." % (lang, COMMASPACE.join(AVAILABLE_LANGUAGES)) ) else: lang = lang.replace("-", "_") if verbosity > 0: self.stdout.write( "Working on models: %s" % ", ".join( ["{app_label}.{object_name}".format(**m._meta.__dict__) for m in models] ) ) for model in models: if verbosity > 0: self.stdout.write("Updating data of model '%s'" % model) opts = translator.get_options_for_model(model) for field_name in opts.all_fields.keys(): def_lang_fieldname = build_localized_fieldname(field_name, lang) # We'll only update fields which do not have an existing value q = Q(**{f"{def_lang_fieldname}__isnull": True}) field = model._meta.get_field(field_name) if isinstance(field, ManyToManyField): trans_field = getattr(model, def_lang_fieldname) if not trans_field.through.objects.exists(): field_names = [f.name for f in trans_field.through._meta.fields] trans_field.through.objects.bulk_create( trans_field.through( **{f: v for f, v in dict(inst.__dict__) if f in field_names} ) for inst in getattr(model, field_name).through.objects.all() ) continue if field.empty_strings_allowed: # type: ignore[union-attr] q |= Q(**{def_lang_fieldname: ""}) model._default_manager.filter(q).rewrite(False).order_by().update( # type: ignore[attr-defined] **{def_lang_fieldname: F(field_name)} ) django-modeltranslation-0.19.14/modeltranslation/manager.py000066400000000000000000000615401500120161300240430ustar00rootroot00000000000000""" The idea of MultilingualManager is taken from django-linguo by Zach Mathew https://github.com/zmathew/django-linguo """ from __future__ import annotations import itertools from functools import reduce from typing import Any, Literal, TypeVar, cast, overload from collections.abc import Container, Iterator, Sequence, Iterable from django.contrib.admin.utils import get_model_from_relation from django.core.exceptions import FieldDoesNotExist from django.db import models from django.db.backends.utils import CursorWrapper from django.db.models import Field, Model, F from django.db.models.expressions import Col from django.db.models.functions import Concat, ConcatPair from django.db.models.lookups import Lookup from django.db.models.query import QuerySet, ValuesIterable from django.db.models.utils import create_namedtuple_class from django.utils.tree import Node from modeltranslation._typing import Self, AutoPopulate from modeltranslation.fields import TranslationField from modeltranslation.thread_context import auto_populate_mode from modeltranslation.utils import ( auto_populate, build_localized_fieldname, get_language, resolution_order, ) _C2F_CACHE: dict[tuple[type[Model], str], Field] = {} _F2TM_CACHE: dict[type[Model], dict[str, type[Model]]] = {} def get_translatable_fields_for_model(model: type[Model]) -> list[str] | None: from modeltranslation.translator import NotRegistered, translator try: return translator.get_options_for_model(model).get_field_names() except NotRegistered: return None def rewrite_lookup_key(model: type[Model], lookup_key: str) -> str: try: pieces = lookup_key.split("__", 1) original_key = pieces[0] translatable_fields = get_translatable_fields_for_model(model) if translatable_fields is not None: # If we are doing a lookup on a translatable field, # we want to rewrite it to the actual field name # For example, we want to rewrite "name__startswith" to "name_fr__startswith" if pieces[0] in translatable_fields: pieces[0] = build_localized_fieldname(pieces[0], get_language()) if len(pieces) > 1: # Check if we are doing a lookup to a related trans model fields_to_trans_models = get_fields_to_translatable_models(model) # Check ``original key``, as pieces[0] may have been already rewritten. if original_key in fields_to_trans_models: transmodel = fields_to_trans_models[original_key] pieces[1] = rewrite_lookup_key(transmodel, pieces[1]) return "__".join(pieces) except AttributeError: return lookup_key def append_fallback(model: type[Model], fields: Sequence[str]) -> tuple[set[str], set[str]]: """ If translated field is encountered, add also all its fallback fields. Returns tuple: (set_of_new_fields_to_use, set_of_translated_field_names) """ fields_set = set(fields) trans: set[str] = set() from modeltranslation.translator import translator opts = translator.get_options_for_model(model) for key, _ in opts.all_fields.items(): if key in fields_set: langs = resolution_order(get_language(), getattr(model, key).fallback_languages) fields_set = fields_set.union(build_localized_fieldname(key, lang) for lang in langs) fields_set.remove(key) trans.add(key) return fields_set, trans def append_translated(model: type[Model], fields: Iterable[str]) -> set[str]: "If translated field is encountered, add also all its translation fields." fields_set = set(fields) from modeltranslation.translator import translator opts = translator.get_options_for_model(model) for key, translated in opts.all_fields.items(): if key in fields_set: fields_set = fields_set.union(f.name for f in translated) return fields_set def append_lookup_key(model: type[Model], lookup_key: str) -> set[str]: "Transform spanned__lookup__key into all possible translation versions, on all levels" pieces = lookup_key.split("__", 1) fields = append_translated(model, (pieces[0],)) if len(pieces) > 1: # Check if we are doing a lookup to a related trans model fields_to_trans_models = get_fields_to_translatable_models(model) if pieces[0] in fields_to_trans_models: transmodel = fields_to_trans_models[pieces[0]] rest = append_lookup_key(transmodel, pieces[1]) fields = {"__".join(pr) for pr in itertools.product(fields, rest)} else: fields = {"%s__%s" % (f, pieces[1]) for f in fields} return fields def append_lookup_keys(model: type[Model], fields: Sequence[str]) -> set[str]: new_fields = [] for field in fields: try: new_field: Container[str] = append_lookup_key(model, field) except AttributeError: new_field = (field,) new_fields.append(new_field) return reduce(set.union, new_fields, set()) # type: ignore[arg-type] def rewrite_order_lookup_key(model: type[Model], lookup_key: str) -> str: try: if lookup_key.startswith("-"): return "-" + rewrite_lookup_key(model, lookup_key[1:]) else: return rewrite_lookup_key(model, lookup_key) except AttributeError: return lookup_key def get_fields_to_translatable_models(model: type[Model]) -> dict[str, type[Model]]: if model in _F2TM_CACHE: return _F2TM_CACHE[model] results: list[tuple[str, type[Model]]] = [] for f in model._meta.get_fields(): if f.is_relation and f.related_model: # The new get_field() will find GenericForeignKey relations. # In that case the 'related_model' attribute is set to None # so it is necessary to check for this value before trying to # get translatable fields. related_model = get_model_from_relation(f) # type: ignore[arg-type] if get_translatable_fields_for_model(related_model) is not None: results.append((f.name, related_model)) _F2TM_CACHE[model] = dict(results) return _F2TM_CACHE[model] def get_field_by_colum_name(model: type[Model], col: str) -> Field: # First, try field with the column name try: field = cast(Field, model._meta.get_field(col)) if field.column == col: return field except FieldDoesNotExist: pass field = _C2F_CACHE.get((model, col), None) # type: ignore[arg-type] if field: return field # D'oh, need to search through all of them. for field in model._meta.fields: if field.column == col: _C2F_CACHE[(model, col)] = field return field assert False, "No field found for column %s" % col _T = TypeVar("_T", bound=Model, covariant=True) class MultilingualQuerySet(QuerySet[_T]): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._post_init() def _post_init(self) -> None: self._rewrite = True self._populate = None if self.model and self.query.default_ordering and (not self.query.order_by): if self.model._meta.ordering: # If we have default ordering specified on the model, set it now so that # it can be rewritten. Otherwise sql.compiler will grab it directly from _meta ordering = [] for key in self.model._meta.ordering: ordering.append(rewrite_order_lookup_key(self.model, key)) self.query.add_ordering(*ordering) def __reduce__(self): return multilingual_queryset_factory, (self.__class__.__bases__[0],), self.__getstate__() def _clone(self) -> Self: return self.__clone() def __clone(self, **kwargs: Any) -> Self: # This method is private, so outside code can use default _clone without `kwargs`, # and we're here can use private version with `kwargs`. # Refs: https://github.com/deschler/django-modeltranslation/issues/483 kwargs.setdefault("_rewrite", self._rewrite) kwargs.setdefault("_populate", self._populate) if hasattr(self, "translation_fields"): kwargs.setdefault("translation_fields", self.translation_fields) if hasattr(self, "original_fields"): kwargs.setdefault("original_fields", self.original_fields) cloned = super()._clone() cloned.__dict__.update(kwargs) return cloned def rewrite(self, mode: bool = True) -> Self: return self.__clone(_rewrite=mode) def populate(self, mode: AutoPopulate = "all") -> Self: """ Overrides the translation fields population mode for this query set. """ return self.__clone(_populate=mode) def _rewrite_applied_operations(self) -> None: """ Rewrite fields in already applied filters/ordering. Useful when converting any QuerySet into MultilingualQuerySet. """ self._rewrite_where(self.query.where) self._rewrite_order() self._rewrite_select_related() # This method was not present in django-linguo def select_related(self, *fields: Any, **kwargs: Any) -> Self: if not self._rewrite: return super().select_related(*fields, **kwargs) # TO CONSIDER: whether this should rewrite only current language, or all languages? # fk -> [fk, fk_en] (with en=active) VS fk -> [fk, fk_en, fk_de, fk_fr ...] (for all langs) # new_args = append_lookup_keys(self.model, fields) new_args: list[str | None] = [] for key in fields: if key is None: new_args.append(None) else: new_args.append(rewrite_lookup_key(self.model, key)) return super().select_related(*new_args, **kwargs) # This method was not present in django-linguo def _rewrite_col(self, col: Col) -> None: """Django >= 1.7 column name rewriting""" if isinstance(col, Col): new_name = rewrite_lookup_key(self.model, col.target.name) if col.target.name != new_name: new_field = self.model._meta.get_field(new_name) if col.target is col.source: col.source = new_field col.target = new_field elif hasattr(col, "col"): self._rewrite_col(col.col) elif hasattr(col, "lhs"): self._rewrite_col(col.lhs) def _rewrite_where(self, q: Lookup | Node) -> None: """ Rewrite field names inside WHERE tree. """ if isinstance(q, Lookup): self._rewrite_col(q.lhs) if isinstance(q, Node): for child in q.children: self._rewrite_where(child) # type: ignore[arg-type] def _rewrite_order(self) -> None: self.query.order_by = [ rewrite_order_lookup_key(self.model, field_name) for field_name in self.query.order_by ] def _rewrite_select_related(self) -> None: if isinstance(self.query.select_related, dict): new = {} for field_name, value in self.query.select_related.items(): new[rewrite_order_lookup_key(self.model, field_name)] = value self.query.select_related = new # This method was not present in django-linguo def _rewrite_q(self, q: Node | tuple[str, Any]) -> Any: """Rewrite field names inside Q call.""" if isinstance(q, tuple) and len(q) == 2: return rewrite_lookup_key(self.model, q[0]), q[1] if isinstance(q, Node): q.children = list(map(self._rewrite_q, q.children)) # type: ignore[arg-type] return q # This method was not present in django-linguo def _rewrite_f(self, q: models.F | Node) -> models.F | Node: """ Rewrite field names inside F call. """ if isinstance(q, models.F): q.name = rewrite_lookup_key(self.model, q.name) return q if isinstance(q, Node): q.children = list(map(self._rewrite_f, q.children)) # type: ignore[arg-type] # Django >= 1.8 if hasattr(q, "lhs"): q.lhs = self._rewrite_f(q.lhs) if hasattr(q, "rhs"): q.rhs = self._rewrite_f(q.rhs) return q def _rewrite_filter_or_exclude(self, args: Any, kwargs: Any) -> tuple[Any, Any]: if not self._rewrite: return args, kwargs args = tuple(map(self._rewrite_q, args)) for key, val in list(kwargs.items()): new_key = rewrite_lookup_key(self.model, key) del kwargs[key] kwargs[new_key] = self._rewrite_f(val) return args, kwargs def _filter_or_exclude(self, negate: bool, args: Any, kwargs: Any) -> Self: args, kwargs = self._rewrite_filter_or_exclude(args, kwargs) return super()._filter_or_exclude(negate, args, kwargs) def _get_original_fields(self) -> list[str]: source = ( self.model._meta.concrete_fields if hasattr(self.model._meta, "concrete_fields") else self.model._meta.fields ) return [f.attname for f in source if not isinstance(f, TranslationField)] def order_by(self, *field_names: Any) -> Self: """ Change translatable field names in an ``order_by`` argument to translation fields for the current language. """ if not self._rewrite: return super().order_by(*field_names) new_args = [] for key in field_names: new_args.append(rewrite_order_lookup_key(self.model, key)) return super().order_by(*new_args) def distinct(self, *field_names: Any) -> Self: """ Change translatable field names in an ``distinct`` argument to translation fields for the current language. """ if not self._rewrite: return super().distinct(*field_names) new_args = [] for key in field_names: new_args.append(rewrite_order_lookup_key(self.model, key)) return super().distinct(*new_args) def update(self, **kwargs: Any) -> int: if not self._rewrite: return super().update(**kwargs) for key, val in list(kwargs.items()): new_key = rewrite_lookup_key(self.model, key) del kwargs[key] kwargs[new_key] = self._rewrite_f(val) return super().update(**kwargs) update.alters_data = True def _update(self, values: list[tuple[Field, type[Model] | None, Any]]) -> CursorWrapper: """ This method is called in .save() method to update an existing record. Here we force to update translation fields as well if the original field only is passed in `save()` in argument `update_fields`. """ # TODO: Should the original field (field without lang code suffix) be updated # when only the default translation field (`field_`) is passed in `update_fields`? # Currently, we don't synchronize values of the original and default translation fields in that case. field_names_to_update = {field.name for field, *_ in values} translation_values: list[tuple[Field, type[Model] | None, Any]] = [] for field, model, value in values: translation_field_name = rewrite_lookup_key(self.model, field.name) if translation_field_name not in field_names_to_update: translatable_field = cast(Field, self.model._meta.get_field(translation_field_name)) translation_values.append((translatable_field, model, value)) values += translation_values return super()._update(values) # This method was not present in django-linguo @property def _populate_mode(self) -> AutoPopulate: # Populate can be set using a global setting or a manager method. if self._populate is None: return auto_populate_mode() return self._populate # This method was not present in django-linguo def create(self, **kwargs: Any) -> _T: """ Allows to override population mode with a ``populate`` method. """ with auto_populate(self._populate_mode): return super().create(**kwargs) # This method was not present in django-linguo def get_or_create(self, *args: Any, **kwargs: Any) -> tuple[_T, bool]: """ Allows to override population mode with a ``populate`` method. """ with auto_populate(self._populate_mode): return super().get_or_create(*args, **kwargs) # This method was not present in django-linguo def defer(self, *fields: Any) -> Self: fields = append_lookup_keys(self.model, fields) # type: ignore[assignment] return super().defer(*fields) # This method was not present in django-linguo def only(self, *fields: Any) -> Self: fields = append_lookup_keys(self.model, fields) # type: ignore[assignment] return super().only(*fields) # This method was not present in django-linguo def raw_values(self, *fields: str, **expressions: Any) -> Self: return super().values(*fields, **expressions) def _values(self, *original: str, **kwargs: Any) -> Self: selects_all = kwargs.pop("selects_all", False) if not kwargs.pop("prepare", False): return super()._values(*original, **kwargs) new_fields, translation_fields = append_fallback(self.model, original) annotation_keys = set(self.query.annotation_select.keys()) if selects_all else set() new_fields.update(annotation_keys) clone = super()._values(*list(new_fields), **kwargs) clone.original_fields = tuple(original) clone.translation_fields = translation_fields return clone # This method was not present in django-linguo def values(self, *fields: str, **expressions: Any) -> Self: if not self._rewrite: return super().values(*fields, **expressions) selects_all = not fields if not fields: # Emulate original queryset behaviour: get all fields that are not translation fields fields = self._get_original_fields() # type: ignore[assignment] fields += tuple(expressions) clone = self._values(*fields, prepare=True, selects_all=selects_all, **expressions) clone._iterable_class = FallbackValuesIterable return clone # This method was not present in django-linguo def values_list(self, *fields: str, flat: bool = False, named: bool = False) -> Self: if not self._rewrite: return super().values_list(*fields, flat=flat, named=named) if flat and named: raise TypeError("'flat' and 'named' can't be used together.") if flat and len(fields) > 1: raise TypeError( "'flat' is not valid when values_list is called with more than one field." ) selects_all = not fields if not fields: # Emulate original queryset behaviour: get all fields that are not translation fields fields = self._get_original_fields() # type: ignore[assignment] field_names = {f for f in fields if not hasattr(f, "resolve_expression")} _fields = [] expressions = {} counter = 1 for field in fields: if hasattr(field, "resolve_expression"): field_id_prefix = getattr(field, "default_alias", field.__class__.__name__.lower()) while True: field_id = field_id_prefix + str(counter) counter += 1 if field_id not in field_names: break expressions[field_id] = field _fields.append(field_id) else: _fields.append(field) clone = self._values(*_fields, prepare=True, selects_all=selects_all, **expressions) clone._iterable_class = ( FallbackNamedValuesListIterable if named else FallbackFlatValuesListIterable if flat else FallbackValuesListIterable ) return clone # This method was not present in django-linguo def dates(self, field_name: str, *args: Any, **kwargs: Any) -> Self: if not self._rewrite: return super().dates(field_name, *args, **kwargs) new_key = rewrite_lookup_key(self.model, field_name) return super().dates(new_key, *args, **kwargs) def _rewrite_concat(self, concat: Concat | ConcatPair): new_source_expressions = [] for exp in concat.source_expressions: if isinstance(exp, (Concat, ConcatPair)): exp = self._rewrite_concat(exp) if isinstance(exp, F): exp = self._rewrite_f(exp) new_source_expressions.append(exp) concat.set_source_expressions(new_source_expressions) return concat def annotate(self, *args: Any, **kwargs: Any) -> Self: if not self._rewrite: return super().annotate(*args, **kwargs) for key, val in list(kwargs.items()): if isinstance(val, models.F): kwargs[key] = self._rewrite_f(val) if isinstance(val, Concat): kwargs[key] = self._rewrite_concat(val) return super().annotate(*args, **kwargs) class FallbackValuesIterable(ValuesIterable): queryset: MultilingualQuerySet[Model] class X: # This stupid class is needed as object use __slots__ and has no __dict__. pass def __iter__(self) -> Iterator[dict[str, Any]]: instance = self.X() fields = self.queryset.original_fields fields += tuple(f for f in self.queryset.query.annotation_select if f not in fields) for row in super().__iter__(): instance.__dict__.update(row) for key in self.queryset.translation_fields: row[key] = getattr(self.queryset.model, key).__get__(instance, None) # Restore original ordering. yield {k: row[k] for k in fields} class FallbackValuesListIterable(FallbackValuesIterable): def __iter__(self) -> Iterator[tuple[Any, ...]]: for row in super().__iter__(): yield tuple(row.values()) class FallbackNamedValuesListIterable(FallbackValuesIterable): def __iter__(self) -> Iterator[tuple[Any, ...]]: for row in super().__iter__(): names, values = row.keys(), row.values() tuple_class = create_namedtuple_class(*names) new = tuple.__new__ yield new(tuple_class, values) class FallbackFlatValuesListIterable(FallbackValuesListIterable): def __iter__(self) -> Iterator[Any]: for row in super().__iter__(): yield row[0] @overload def multilingual_queryset_factory( old_cls: type[Any], instantiate: Literal[False] ) -> type[MultilingualQuerySet]: ... @overload def multilingual_queryset_factory( old_cls: type[Any], instantiate: Literal[True] = ... ) -> MultilingualQuerySet: ... def multilingual_queryset_factory( old_cls: type[Any], instantiate: bool = True ) -> type[MultilingualQuerySet] | MultilingualQuerySet: if old_cls == models.query.QuerySet: NewClass = MultilingualQuerySet else: class NewClass(old_cls, MultilingualQuerySet): # type: ignore[no-redef] pass NewClass.__name__ = "Multilingual%s" % old_cls.__name__ return NewClass() if instantiate else NewClass class MultilingualQuerysetManager(models.Manager[_T]): """ This class gets hooked in MRO just before plain Manager, so that every call to get_queryset returns MultilingualQuerySet. """ def get_queryset(self) -> MultilingualQuerySet[_T]: qs = super().get_queryset() return self._patch_queryset(qs) def _patch_queryset(self, qs: QuerySet[_T]) -> MultilingualQuerySet[_T]: qs.__class__ = multilingual_queryset_factory(qs.__class__, instantiate=False) qs = cast(MultilingualQuerySet[_T], qs) qs._post_init() qs._rewrite_applied_operations() return qs class MultilingualManager(MultilingualQuerysetManager[_T]): def rewrite(self, *args: Any, **kwargs: Any): return self.get_queryset().rewrite(*args, **kwargs) def populate(self, *args: Any, **kwargs: Any): return self.get_queryset().populate(*args, **kwargs) def raw_values(self, *args: Any, **kwargs: Any): return self.get_queryset().raw_values(*args, **kwargs) def get_queryset(self) -> MultilingualQuerySet[_T]: """ This method is repeated because some managers that don't use super() or alter queryset class may return queryset that is not subclass of MultilingualQuerySet. """ qs = super().get_queryset() if isinstance(qs, MultilingualQuerySet): # Is already patched by MultilingualQuerysetManager - in most of the cases # when custom managers use super() properly in get_queryset. return qs return self._patch_queryset(qs) django-modeltranslation-0.19.14/modeltranslation/models.py000066400000000000000000000061071500120161300237120ustar00rootroot00000000000000from typing import Any def autodiscover() -> None: """ Auto-discover INSTALLED_APPS translation.py modules and fail silently when not present. This forces an import on them to register. Also import explicit modules. """ import copy import os import sys from importlib import import_module from django.apps import apps from django.utils.module_loading import module_has_submodule from modeltranslation.settings import DEBUG, TRANSLATION_FILES from modeltranslation.translator import translator mods = [(app_config.name, app_config.module) for app_config in apps.get_app_configs()] for app, mod in mods: # Attempt to import the app's translation module. module = "%s.translation" % app before_import_registry = copy.copy(translator._registry) try: import_module(module) except ImportError: # Reset the model registry to the state before the last import as # this import will have to reoccur on the next request and this # could raise NotRegistered and AlreadyRegistered exceptions translator._registry = before_import_registry # Decide whether to bubble up this error. If the app just # doesn't have an translation module, we can ignore the error # attempting to import it, otherwise we want it to bubble up. if module_has_submodule(mod, "translation"): raise for module in TRANSLATION_FILES: import_module(module) # This executes 'after imports' scheduled operations translator.execute_lazy_operations() # In debug mode, print a list of registered models and pid to stdout. # Note: Differing model order is fine, we don't rely on a particular # order, as far as base classes are registered before subclasses. if DEBUG: try: if sys.argv[1] in ("runserver", "runserver_plus"): models = translator.get_registered_models() names = ", ".join(m.__name__ for m in models) print( "modeltranslation: Registered %d models for translation" " (%s) [pid: %d]." % (len(models), names, os.getpid()) ) except IndexError: pass def handle_translation_registrations(*args: Any, **kwargs: Any) -> None: """ Ensures that any configuration of the TranslationOption(s) are handled when importing modeltranslation. This makes it possible for scripts/management commands that affect models but know nothing of modeltranslation. """ from modeltranslation.settings import ENABLE_REGISTRATIONS if not ENABLE_REGISTRATIONS: # If the user really wants to disable this, they can, possibly at their # own expense. This is generally only required in cases where other # apps generate import errors and requires extra work on the user's # part to make things work. return # Trigger autodiscover, causing any TranslationOption initialization # code to execute. autodiscover() django-modeltranslation-0.19.14/modeltranslation/py.typed000066400000000000000000000000001500120161300235360ustar00rootroot00000000000000django-modeltranslation-0.19.14/modeltranslation/settings.py000066400000000000000000000070201500120161300242620ustar00rootroot00000000000000from __future__ import annotations from django.conf import settings from django.core.exceptions import ImproperlyConfigured from ._typing import AutoPopulate, _ListOrTuple TRANSLATION_FILES: tuple[str, ...] = tuple( getattr(settings, "MODELTRANSLATION_TRANSLATION_FILES", ()) ) AVAILABLE_LANGUAGES: list[str] = list( getattr( settings, "MODELTRANSLATION_LANGUAGES", (val for val, label in settings.LANGUAGES), ) ) _default_language: str | None = getattr(settings, "MODELTRANSLATION_DEFAULT_LANGUAGE", None) if _default_language and _default_language not in AVAILABLE_LANGUAGES: raise ImproperlyConfigured("MODELTRANSLATION_DEFAULT_LANGUAGE not in LANGUAGES setting.") DEFAULT_LANGUAGE = _default_language or AVAILABLE_LANGUAGES[0] REQUIRED_LANGUAGES: _ListOrTuple[str] = getattr(settings, "MODELTRANSLATION_REQUIRED_LANGUAGES", ()) # Fixed base language for prepopulated fields (slugs) # (If not set, the current request language will be used) PREPOPULATE_LANGUAGE: str | None = getattr(settings, "MODELTRANSLATION_PREPOPULATE_LANGUAGE", None) if PREPOPULATE_LANGUAGE and PREPOPULATE_LANGUAGE not in AVAILABLE_LANGUAGES: raise ImproperlyConfigured("MODELTRANSLATION_PREPOPULATE_LANGUAGE not in LANGUAGES setting.") # Load allowed CUSTOM_FIELDS from django settings CUSTOM_FIELDS: tuple[str, ...] = getattr(settings, "MODELTRANSLATION_CUSTOM_FIELDS", ()) # Don't change this setting unless you really know what you are doing ENABLE_REGISTRATIONS: bool = getattr( settings, "MODELTRANSLATION_ENABLE_REGISTRATIONS", settings.USE_I18N ) # Modeltranslation specific debug setting DEBUG: bool = getattr(settings, "MODELTRANSLATION_DEBUG", False) AUTO_POPULATE: AutoPopulate = getattr(settings, "MODELTRANSLATION_AUTO_POPULATE", False) # FALLBACK_LANGUAGES should be in either format: # MODELTRANSLATION_FALLBACK_LANGUAGES = ('en', 'de') # MODELTRANSLATION_FALLBACK_LANGUAGES = {'default': ('en', 'de'), 'fr': ('de',)} # By default we fallback to the default language _fallback_languages = getattr(settings, "MODELTRANSLATION_FALLBACK_LANGUAGES", (DEFAULT_LANGUAGE,)) if isinstance(_fallback_languages, (tuple, list)): _fallback_languages = {"default": tuple(_fallback_languages)} # To please mypy, explicitly annotate: FALLBACK_LANGUAGES: dict[str, tuple[str, ...]] = _fallback_languages if "default" not in FALLBACK_LANGUAGES: raise ImproperlyConfigured( 'MODELTRANSLATION_FALLBACK_LANGUAGES does not contain "default" key.' ) for key, value in FALLBACK_LANGUAGES.items(): if key != "default" and key not in AVAILABLE_LANGUAGES: raise ImproperlyConfigured( 'MODELTRANSLATION_FALLBACK_LANGUAGES: "%s" not in LANGUAGES setting.' % key ) if not isinstance(value, (tuple, list)): raise ImproperlyConfigured( 'MODELTRANSLATION_FALLBACK_LANGUAGES: value for key "%s" is not list nor tuple.' % key ) for lang in value: if lang not in AVAILABLE_LANGUAGES: raise ImproperlyConfigured( 'MODELTRANSLATION_FALLBACK_LANGUAGES: "%s" not in LANGUAGES setting.' % lang ) ENABLE_FALLBACKS: bool = getattr(settings, "MODELTRANSLATION_ENABLE_FALLBACKS", True) LOADDATA_RETAIN_LOCALE: bool = getattr(settings, "MODELTRANSLATION_LOADDATA_RETAIN_LOCALE", True) JQUERY_URL: str = getattr( settings, "JQUERY_URL", "//ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js", ) JQUERY_UI_URL: str = getattr( settings, "JQUERY_UI_URL", "//ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js", ) django-modeltranslation-0.19.14/modeltranslation/static/000077500000000000000000000000001500120161300233405ustar00rootroot00000000000000django-modeltranslation-0.19.14/modeltranslation/static/modeltranslation/000077500000000000000000000000001500120161300267175ustar00rootroot00000000000000django-modeltranslation-0.19.14/modeltranslation/static/modeltranslation/css/000077500000000000000000000000001500120161300275075ustar00rootroot00000000000000tabbed_translation_fields.css000066400000000000000000000102321500120161300353250ustar00rootroot00000000000000django-modeltranslation-0.19.14/modeltranslation/static/modeltranslation/css/* * jQuery UI CSS Framework * Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about) * Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses. * http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/themes/base/jquery.ui.core.css */ /* backward compatibility: .ui-tabs-selected: jquery ui < 1.10 .ui-tabs-active classes jquery ui >= 1.10 */ /* Layout helpers ----------------------------------*/ .ui-helper-hidden { display: none; } .ui-helper-hidden-accessible { position: absolute; left: -99999999px; } .ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; } .ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } .ui-helper-clearfix { display: inline-block; } /* required comment for clearfix to work in Opera \*/ * html .ui-helper-clearfix { height:1%; } .ui-helper-clearfix { display:block; } .ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); } .ui-state-disabled { cursor: default !important; } .ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; } /* http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/themes/base/jquery.ui.tabs.css */ .ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ .ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; } .ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; } .ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; } .ui-tabs .ui-tabs-nav li.ui-tabs-active, .ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; } .ui-tabs .ui-tabs-nav li.ui-tabs-active a, .ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; } .ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ .ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; } .ui-tabs .ui-tabs-hide { position: absolute; display: none; } /* custom tabs theme */ .ui-tabs { padding: 0; } .ui-tabs .ui-tabs-nav { padding: 5px 0 0 10px; border-bottom: 1px solid #EEEEEE; } .ui-tabs .ui-tabs-nav li { margin: 0; } .ui-tabs .ui-tabs-nav li.required { font-weight: bold; } .ui-tabs .ui-tabs-nav li a { border: 1px solid #CCCCCC; background: #eeeeee repeat-x; border-bottom-width: 0; color: #666666; padding: 4px 10px 4px 10px; margin-top: 2px; -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; border-top-left-radius: 4px; border-top-right-radius: 4px; font-size: 12px; } .ui-tabs .ui-tabs-nav li.ui-tabs-active a, .ui-tabs .ui-tabs-nav li.ui-tabs-selected a { background: #7CA0C7 repeat-x; color: #fff; padding: 6px 10px 4px 10px; margin-top: 0; } .ui-tabs .ui-tabs-panel { padding: 0; } .ui-tabs .ui-tab-has-errors a::after { content: "•"; color: #ba2121; position: absolute; font-size: 1.8rem; } .inline-group .tabular .ui-tabs .ui-tabs-panel { padding: 5px; } .inline-group .tabular .ui-tabs .ui-tabs-nav { padding-left: 4px; font-family: "Lucida Grande", "DejaVu Sans", "Bitstream Vera Sans", Verdana,Arial, sans-serif; } .inline-group .tabular tr td { vertical-align: bottom; } .inline-group .tabular tr.has_original td.original, .inline-group .tabular tr td.delete { vertical-align: top; } .inline-group .tabular .datetime > input { margin-right: 5px; } .inline-group .tabular .datetime br { display: none; } django-modeltranslation-0.19.14/modeltranslation/static/modeltranslation/js/000077500000000000000000000000001500120161300273335ustar00rootroot00000000000000django-modeltranslation-0.19.14/modeltranslation/static/modeltranslation/js/clearable_inputs.js000066400000000000000000000007541500120161300332130ustar00rootroot00000000000000var jQuery, $, django; (function () { "use strict"; (jQuery || $ || django.jQuery)(function ($) { $(".clearable-input").each(function () { var clear = $(this).children().last(); $(this) .find("input, select, textarea") .not(clear) .change(function () { if (typeof clear.prop == "undefined") // older jQuery clear.removeAttr("checked"); else clear.prop("checked", false); }); }); }); })(); django-modeltranslation-0.19.14/modeltranslation/static/modeltranslation/js/force_jquery.js000066400000000000000000000001011500120161300323560ustar00rootroot00000000000000if (typeof jQuery === 'undefined') { jQuery = django.jQuery; } tabbed_translation_fields.js000066400000000000000000000447551500120161300350160ustar00rootroot00000000000000django-modeltranslation-0.19.14/modeltranslation/static/modeltranslation/js/*jslint white: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true */ var google, django, gettext; (function () { var jQuery = window.jQuery || $ || django.jQuery; /* Add a new selector to jQuery that excludes parent items which match a given selector */ jQuery.expr[":"].parents = function (a, i, m) { return jQuery(a).parents(m[3]).length < 1; }; jQuery(function ($) { var detectTemplate = function() { if (document.querySelector("#jazzmin-theme, #jazzy-navbar, #jazzy-tabs, #jazzy-actions")) { return "jazzmin"; } return "default"; } var selectorMapping = { "default": { "mainHeader": () => $("#content").find("h1"), "tabContainer": (el) => $(el).closest(".form-row"), "tabUlClass": "", "tabLiClass": "", "tabAClass": "", "tabErrorClass": "ui-tab-has-errors" }, "jazzmin": { "mainHeader": () => $("#content-main").find(".card-title:first"), "tabContainer": (el) => $(el).closest(".form-group"), "tabUlClass": "nav nav-tabs", "tabLiClass": "nav-item", "tabAClass": "nav-link", "tabErrorClass": "ui-tab-has-errors" } } const selectors = selectorMapping[detectTemplate()]; var TranslationField = function (options) { this.el = options.el; this.cls = options.cls; this.id = ""; this.origFieldname = ""; this.lang = ""; this.groupId = ""; this.init = function () { var clsBits = this.cls .substring(TranslationField.cssPrefix.length, this.cls.length) .split("-"); this.origFieldname = clsBits[0]; this.lang = clsBits[1]; this.id = $(this.el).attr("id"); this.groupId = this.buildGroupId(); }; this.buildGroupId = function () { /** * Returns a unique group identifier with respect to the admin's way * of handling inline field name attributes. Essentially that's the * translation field id without the language prefix. * * Examples ('id parameter': 'return value'): * * 'id_name_de': * 'id_name' * 'id_name_zh_tw': * 'id_name' * 'id_name_set-2-name_de': * 'id_name_set-2-name' * 'id_name_set-2-name_zh_tw': * 'id_name_set-2-name' * 'id_name_set-2-0-name_de': * 'id_name_set-2-0-name' * 'id_name_set-2-0-name_zh_tw': * 'id_name_set-2-0-name' * 'id_news-data2-content_type-object_id-0-name_de': * 'id_news-data2-content_type-object_id-0-name' * 'id_news-data2-content_type-object_id-0-name_zh_cn': * id_news-data2-content_type-object_id-0-name' * 'id_news-data2-content_type-object_id-0-1-name_de': * 'id_news-data2-content_type-object_id-0-1-name' * 'id_news-data2-content_type-object_id-0-1-name_zh_cn': * id_news-data2-content_type-object_id-0-1-name' */ // TODO: We should be able to simplify this, the modeltranslation specific // field classes are already build to be easily splittable, so we could use them // to slice off the language code. var idBits = this.id.split("-"), idPrefix = "id_" + this.origFieldname; if (idBits.length === 3) { // Handle standard inlines idPrefix = idBits[0] + "-" + idBits[1] + "-" + idPrefix; } else if (idBits.length === 4) { // Handle standard inlines with model used by inline more than once idPrefix = idBits[0] + "-" + idBits[1] + "-" + idBits[2] + "-" + idPrefix; } else if (idBits.length === 5 && idBits[3] != "__prefix__") { // Handle nested inlines (https://github.com/Soaa-/django-nested-inlines) idPrefix = idBits[0] + "-" + idBits[1] + "-" + idBits[2] + "-" + idBits[3] + "-" + this.origFieldname; } else if (idBits.length === 6) { // Handle generic inlines idPrefix = idBits[0] + "-" + idBits[1] + "-" + idBits[2] + "-" + idBits[3] + "-" + idBits[4] + "-" + this.origFieldname; } else if (idBits.length === 7) { // Handle generic inlines with model used by inline more than once idPrefix = idBits[0] + "-" + idBits[1] + "-" + idBits[2] + "-" + idBits[3] + "-" + idBits[4] + "-" + idBits[5] + "-" + this.origFieldname; } return idPrefix; }; this.init(); }; TranslationField.cssPrefix = "mt-field-"; var TranslationFieldGrouper = function (options) { this.$fields = options.$fields; this.groupedTranslations = {}; this.init = function () { // Handle fields inside collapsed groups as added by zinnia this.$fields = this.$fields.add("fieldset.collapse-closed .mt"); this.groupedTranslations = this.getGroupedTranslations(); }; this.getGroupedTranslations = function () { /** * Returns a grouped set of all model translation fields. * The returned datastructure will look something like this: * * { * 'id_name_de': { * 'en': HTMLInputElement, * 'de': HTMLInputElement, * 'zh_tw': HTMLInputElement * }, * 'id_name_set-2-name': { * 'en': HTMLTextAreaElement, * 'de': HTMLTextAreaElement, * 'zh_tw': HTMLTextAreaElement * }, * 'id_news-data2-content_type-object_id-0-name': { * 'en': HTMLTextAreaElement, * 'de': HTMLTextAreaElement, * 'zh_tw': HTMLTextAreaElement * } * } * * The keys are unique group identifiers as returned by * TranslationField.buildGroupId() to handle inlines properly. */ var self = this, cssPrefix = TranslationField.cssPrefix; this.$fields.each(function (idx, el) { $.each($(el).attr("class").split(" "), function (idx, cls) { if (cls.substring(0, cssPrefix.length) === cssPrefix) { var tfield = new TranslationField({ el: el, cls: cls }); if (!self.groupedTranslations[tfield.groupId]) { self.groupedTranslations[tfield.groupId] = {}; } self.groupedTranslations[tfield.groupId][tfield.lang] = el; } }); }); return this.groupedTranslations; }; this.init(); }; function createTabs(groupedTranslations) { var tabs = []; $.each(groupedTranslations, function (groupId, lang) { if (groupId.includes("__prefix__")) return; var tabsContainer = $("
"), tabsList = $(`
    `), insertionPoint, activeTab = 0; tabsContainer.append(tabsList); $.each(lang, function (lang, el) { var container = selectors["tabContainer"](el), label = $("label", container), fieldLabel = container.find("label"), tabId = "tab_" + $(el).attr("id"), panel, tab; // Remove language and brackets from field label, they are // displayed in the tab already. if (fieldLabel.html()) { fieldLabel.html(fieldLabel.html().replace(/ \[.+\]/, "")); } if (!insertionPoint) { insertionPoint = { insert: container.prev().length ? "after" : container.next().length ? "prepend" : "append", el: container.prev().length ? container.prev() : container.parent(), }; } container.find("script").remove(); panel = $('
    ').append(container); tab = $( `
  • ' + lang.replace("_", "-") + "
  • " ); tabsList.append(tab); tabsContainer.append(panel); if (container.hasClass("errors")) { activeTab = tabsList.find("li").length - 1; tab.addClass(selectors["tabErrorClass"]); } }); insertionPoint.el[insertionPoint.insert](tabsContainer); tabsContainer.tabs({ active: activeTab, }); tabs.push(tabsContainer); }); return tabs; } function handleAddAnotherInline() { // TODO: Refactor $(".mt") .parents(".inline-group") .not(".tabular") .find(".add-row a") .click(function () { var grouper = new TranslationFieldGrouper({ $fields: $(this).parent().prev().prev().find(".mt").add( // Support django-nested-admin stacked inlines $(this) .parent() .prev(".djn-items") .children(".djn-item") .last() .find(".mt") ), }); var tabs = createTabs(grouper.groupedTranslations); // Update the main switch as it is not aware of the newly created tabs MainSwitch.update(tabs); // Activate the language tab selected in the main switch MainSwitch.activateTab(tabs); }); } var TabularInlineGroup = function (options) { this.id = options.id; this.$id = null; this.$table = null; this.translationColumns = []; // TODO: Make use of this to flag required tabs this.requiredColumns = []; this.init = function () { this.$id = $("#" + this.id); this.$table = $(this.$id).find("table"); }; this.getAllGroupedTranslations = function () { var grouper = new TranslationFieldGrouper({ $fields: this.$table.find(".mt").filter("input, textarea, select"), }); //this.requiredColumns = this.getRequiredColumns(); this.initTable(); return grouper.groupedTranslations; }; this.getGroupedTranslations = function ($fields) { var grouper = new TranslationFieldGrouper({ $fields: $fields, }); return grouper.groupedTranslations; }; this.initTable = function () { var self = this; // The table header requires special treatment. In case an inline // is declared with extra=0, the translation fields are not visible. var thGrouper = new TranslationFieldGrouper({ $fields: this.$table.find(".mt").filter("input, textarea, select"), }); this.translationColumns = this.getTranslationColumns( thGrouper.groupedTranslations ); // The markup of tabular inlines is kinda weird. There is an additional // leading td.original per row, so we have one td more than ths. this.$table.find("th").each(function (idx) { // Hide table heads from which translation fields have been moved out. if ($.inArray(idx + 1, self.translationColumns) !== -1) { // FIXME: Why does this break when we use remove instead of hide? $(this).hide(); } // Remove language and brackets from table header, // they are displayed in the tab already. if ( $(this).html() && $.inArray(idx + 1, self.translationColumns) === -1 ) { $(this).html( $(this) .html() .replace(/ \[.+\]/, "") ); } }); }; this.getTranslationColumns = function (groupedTranslations) { var translationColumns = []; // Get table column indexes which have translation fields, but omit the first // one per group, because that's where we insert our tab container. $.each(groupedTranslations, function (groupId, lang) { var i = 0; $.each(lang, function (lang, el) { var column = $(el).closest("td").prevAll().length; if (i > 0 && $.inArray(column, translationColumns) === -1) { translationColumns.push(column); } i += 1; }); }); return translationColumns; }; this.getRequiredColumns = function () { var requiredColumns = []; // Get table column indexes which have required fields, but omit the first // one per group, because that's where we insert our tab container. this.$table.find("th.required").each(function () { requiredColumns.push($(this).index() + 1); }); return requiredColumns; }; this.init(); }; function handleTabularAddAnotherInline(tabularInlineGroup) { tabularInlineGroup.$table.find(".add-row a").click(function () { var tabs = createTabularTabs( tabularInlineGroup.getGroupedTranslations( $(this).parent().parent().prev().prev().find(".mt") ) ); // Update the main switch as it is not aware of the newly created tabs MainSwitch.update(tabs); // Activate the language tab selected in the main switch MainSwitch.activateTab(tabs); }); } function createTabularTabs(groupedTranslations) { var tabs = []; $.each(groupedTranslations, function (groupId, lang) { if (groupId.includes("__prefix__")) return; var tabsContainer = $(""), tabsList = $("
      "), insertionPoint, activeTab = 0; tabsContainer.append(tabsList); $.each(lang, function (lang, el) { var $container = $(el).closest("td"), $panel, $tab, tabId = "tab_" + $(el).attr("id"); if (!insertionPoint) { insertionPoint = { insert: $container.prev().length ? "after" : $container.next().length ? "prepend" : "append", el: $container.prev().length ? $container.prev() : $container.parent(), }; } $panel = $('
      ').append($container); // Turn the moved tds into divs var attrs = {}; $.each($container[0].attributes, function (idx, attr) { attrs[attr.nodeName] = attr.nodeValue; }); $container.replaceWith(function () { return $("
      ", attrs).append($(this).contents()); }); // TODO: Setting the required state based on the default field is naive. // The user might have tweaked his admin. We somehow have to keep track of the // column indexes _before_ the tds have been moved around. $tab = $( "' + lang.replace("_", "-") + "" ); tabsList.append($tab); tabsContainer.append($panel); if ($container.hasClass("errors")) { activeTab = tabsList.find("li").length - 1; tab.addClass(selectors["tabErrorClass"]); } }); insertionPoint.el[insertionPoint.insert](tabsContainer); tabsContainer.tabs({ active: activeTab, }); tabs.push(tabsContainer); }); return tabs; } var MainSwitch = { languages: [], $select: $("