├── .all-contributorsrc ├── .bumpversion.cfg ├── .coveragerc ├── .editorconfig ├── .fussyfox.yml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ └── feature_request.yaml ├── dependabot.yml └── workflows │ ├── auto-approve.yml │ ├── auto-merge.yml │ ├── codacy-analysis.yml │ ├── codeql.yml │ ├── deploy.yml │ ├── gh-pages.yml │ ├── greetings.yml │ ├── pre-commit-auto-merge.yml │ ├── test.yml │ └── update-doc-assets.yml ├── .gitignore ├── .pep8speaks.yml ├── .pre-commit-config.yaml ├── .pypirc ├── .pyup.yml ├── .whitesource ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── conftest.py ├── custom ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20210804_0828.py │ ├── 0003_alter_library_options.py │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── demo ├── __init__.py ├── apps.py ├── filterset.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20190619_1122.py │ ├── 0003_auto_20210325_2151.py │ └── __init__.py ├── models.py ├── tables.py ├── templates │ └── demo │ │ ├── index.html │ │ ├── test-create.html │ │ ├── test-custom.html │ │ ├── test-detail.html │ │ ├── test-list.html │ │ ├── test-multi-table.html │ │ ├── test-table-list.html │ │ └── test-update.html ├── urls.py └── views.py ├── docs ├── .nojekyll ├── CHANGELOG.md ├── README.md └── index.html ├── manage.py ├── pytest.ini ├── renovate.json ├── requirements.txt ├── setup.py ├── tox.ini └── view_breadcrumbs ├── __init__.py ├── apps.py ├── constants.py ├── generic ├── __init__.py ├── base.py ├── create.py ├── delete.py ├── detail.py ├── list.py └── update.py ├── locale ├── en_US │ └── LC_MESSAGES │ │ └── django.po └── fr │ └── LC_MESSAGES │ └── django.po ├── templates └── view_breadcrumbs │ ├── bootstrap2.html │ ├── bootstrap3.html │ ├── bootstrap4.html │ └── bootstrap5.html ├── templatetags ├── __init__.py └── view_breadcrumbs.py ├── tests ├── __init__.py └── unit │ ├── __init__.py │ └── test_breadcrumbs.py └── utils.py /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "KrunchMuffin", 10 | "name": "Derek", 11 | "avatar_url": "https://avatars3.githubusercontent.com/u/1037197?v=4", 12 | "profile": "https://fansourcedpoisontour.com", 13 | "contributions": [ 14 | "doc" 15 | ] 16 | }, 17 | { 18 | "login": "sveetch", 19 | "name": "David THENON", 20 | "avatar_url": "https://avatars.githubusercontent.com/u/1572165?v=4", 21 | "profile": "http://www.emencia.com", 22 | "contributions": [ 23 | "code" 24 | ] 25 | } 26 | ], 27 | "contributorsPerLine": 7, 28 | "projectName": "django-view-breadcrumbs", 29 | "projectOwner": "tj-django", 30 | "repoType": "github", 31 | "repoHost": "https://github.com", 32 | "skipCi": true 33 | } 34 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2.5.1 3 | commit = True 4 | tag = False 5 | 6 | [bumpversion:file:setup.py] 7 | search = version="{current_version}" 8 | replace = version="{new_version}" 9 | 10 | [bumpversion:file:view_breadcrumbs/locale/en_US/LC_MESSAGES/django.po] 11 | search = {current_version} 12 | replace = {new_version} 13 | 14 | [bumpversion:file:view_breadcrumbs/locale/fr/LC_MESSAGES/django.po] 15 | search = {current_version} 16 | replace = {new_version} 17 | 18 | [bdist_wheel] 19 | universal = 1 20 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = view_breadcrumbs 3 | omit = 4 | view_breadcrumbs/test*, 5 | view_breadcrumbs/templatetags/* 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{html,css,scss,json,yml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [Makefile] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /.fussyfox.yml: -------------------------------------------------------------------------------- 1 | - bandit 2 | - black 3 | - flake8 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: jackton1 4 | patreon: # Replace with a single Patreon username 5 | open_collective: tj-django 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: [] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug 2 | description: Create a report to help us improve 3 | title: "[BUG] " 4 | labels: [bug, needs triage] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this bug report! 11 | - type: checkboxes 12 | attributes: 13 | label: Is there an existing issue for this? 14 | description: Please search to see if an issue already exists for the bug you encountered. 15 | options: 16 | - label: I have searched the existing issues 17 | required: true 18 | - type: checkboxes 19 | attributes: 20 | label: Does this issue exist in the latest version? 21 | description: Please view all releases to confirm that this issue hasn't already been fixed. 22 | options: 23 | - label: I'm using the latest release 24 | required: true 25 | - type: textarea 26 | id: what-happened 27 | attributes: 28 | label: Describe the bug? 29 | description: A clear and concise description of what the bug is 30 | placeholder: Tell us what you see! 31 | validations: 32 | required: true 33 | - type: textarea 34 | id: reproduce 35 | attributes: 36 | label: To Reproduce 37 | description: Steps to reproduce the behavior? 38 | placeholder: | 39 | 1. In this environment... 40 | 2. With this config... 41 | 3. Run '...' 42 | 4. See error... 43 | validations: 44 | required: true 45 | - type: dropdown 46 | id: os 47 | attributes: 48 | label: What OS are you seeing the problem on? 49 | multiple: true 50 | options: 51 | - Ubuntu 52 | - macOS 53 | - Windows 54 | - Other 55 | validations: 56 | required: false 57 | - type: textarea 58 | id: expected 59 | attributes: 60 | label: Expected behavior? 61 | description: A clear and concise description of what you expected to happen. 62 | placeholder: Tell us what you expected! 63 | validations: 64 | required: true 65 | - type: textarea 66 | id: logs 67 | attributes: 68 | label: Relevant log output 69 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 70 | placeholder: | 71 | This can be achieved by: 72 | 1. Re-running the workflow with debug logging enabled. 73 | 2. Copy or download the log archive. 74 | 3. Paste the contents here or upload the file in a subsequent comment. 75 | render: shell 76 | - type: textarea 77 | attributes: 78 | label: Anything else? 79 | description: | 80 | Links? or References? 81 | 82 | Anything that will give us more context about the issue you are encountering! 83 | 84 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 85 | validations: 86 | required: false 87 | - type: checkboxes 88 | id: terms 89 | attributes: 90 | label: Code of Conduct 91 | description: By submitting this issue, you agree to follow our [Code of Conduct](../blob/main/CODE_OF_CONDUCT.md) 92 | options: 93 | - label: I agree to follow this project's Code of Conduct 94 | required: true 95 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | title: "[Feature] <title>" 4 | labels: [enhancement] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this feature request! 11 | - type: checkboxes 12 | attributes: 13 | label: Is this feature missing in the latest version? 14 | description: Please upgrade to the latest version to verify that this feature is still missing. 15 | options: 16 | - label: I'm using the latest release 17 | required: true 18 | - type: textarea 19 | id: what-happened 20 | attributes: 21 | label: Is your feature request related to a problem? Please describe. 22 | description: | 23 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 24 | placeholder: Tell us what you see! 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: requests 29 | attributes: 30 | label: Describe the solution you'd like? 31 | description: A clear and concise description of what you want to happen. 32 | validations: 33 | required: true 34 | - type: textarea 35 | id: alternative 36 | attributes: 37 | label: Describe alternatives you've considered? 38 | description: A clear and concise description of any alternative solutions or features you've considered. 39 | validations: 40 | required: false 41 | - type: textarea 42 | attributes: 43 | label: Anything else? 44 | description: | 45 | Links? or References? 46 | 47 | Add any other context or screenshots about the feature request here. 48 | 49 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 50 | validations: 51 | required: false 52 | - type: checkboxes 53 | id: terms 54 | attributes: 55 | label: Code of Conduct 56 | description: By submitting this issue, you agree to follow our [Code of Conduct](./CODE_OF_CONDUCT.md) 57 | options: 58 | - label: I agree to follow this project's Code of Conduct 59 | required: true 60 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | labels: 9 | - "dependencies" 10 | - "dependabot" 11 | - "automerge" 12 | - package-ecosystem: github-actions 13 | directory: "/" 14 | schedule: 15 | interval: daily 16 | open-pull-requests-limit: 10 17 | labels: 18 | - "dependencies" 19 | - "dependabot" 20 | - "automerge" 21 | -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | name: Auto approve 2 | 3 | on: 4 | pull_request_target 5 | 6 | jobs: 7 | auto-approve: 8 | runs-on: ubuntu-latest 9 | if: | 10 | ( 11 | github.event.pull_request.user.login == 'dependabot[bot]' || 12 | github.event.pull_request.user.login == 'dependabot' || 13 | github.event.pull_request.user.login == 'dependabot-preview[bot]' || 14 | github.event.pull_request.user.login == 'dependabot-preview' || 15 | github.event.pull_request.user.login == 'renovate[bot]' || 16 | github.event.pull_request.user.login == 'renovate' || 17 | github.event.pull_request.user.login == 'github-actions[bot]' || 18 | github.event.pull_request.user.login == 'pre-commit-ci' || 19 | github.event.pull_request.user.login == 'pre-commit-ci[bot]' 20 | ) 21 | && 22 | ( 23 | github.actor == 'dependabot[bot]' || 24 | github.actor == 'dependabot' || 25 | github.actor == 'dependabot-preview[bot]' || 26 | github.actor == 'dependabot-preview' || 27 | github.actor == 'renovate[bot]' || 28 | github.actor == 'renovate' || 29 | github.actor == 'github-actions[bot]' || 30 | github.actor == 'pre-commit-ci' || 31 | github.actor == 'pre-commit-ci[bot]' 32 | ) 33 | steps: 34 | - uses: hmarr/auto-approve-action@v3 35 | with: 36 | github-token: ${{ secrets.PAT_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: automerge 2 | on: 3 | check_suite: 4 | types: 5 | - completed 6 | 7 | jobs: 8 | automerge: 9 | runs-on: ubuntu-latest 10 | if: | 11 | github.actor == 'dependabot[bot]' || 12 | github.actor == 'dependabot' || 13 | github.actor == 'dependabot-preview[bot]' || 14 | github.actor == 'dependabot-preview' || 15 | github.actor == 'renovate[bot]' || 16 | github.actor == 'renovate' 17 | steps: 18 | - name: automerge 19 | uses: pascalgn/automerge-action@v0.16.3 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} 22 | MERGE_METHOD: "rebase" 23 | UPDATE_METHOD: "rebase" 24 | MERGE_RETRIES: "6" 25 | MERGE_RETRY_SLEEP: "100000" 26 | MERGE_LABELS: "" 27 | -------------------------------------------------------------------------------- /.github/workflows/codacy-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow checks out code, performs a Codacy security scan 2 | # and integrates the results with the 3 | # GitHub Advanced Security code scanning feature. For more information on 4 | # the Codacy security scan action usage and parameters, see 5 | # https://github.com/codacy/codacy-analysis-cli-action. 6 | # For more information on Codacy Analysis CLI in general, see 7 | # https://github.com/codacy/codacy-analysis-cli. 8 | 9 | name: Codacy Security Scan 10 | 11 | on: 12 | push: 13 | branches: [ main ] 14 | pull_request: 15 | # The branches below must be a subset of the branches above 16 | branches: [ main ] 17 | schedule: 18 | - cron: '15 16 * * 2' 19 | 20 | jobs: 21 | codacy-security-scan: 22 | name: Codacy Security Scan 23 | runs-on: ubuntu-latest 24 | steps: 25 | # Checkout the repository to the GitHub Actions runner 26 | - name: Checkout code 27 | uses: actions/checkout@v4 28 | 29 | # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis 30 | - name: Run Codacy Analysis CLI 31 | uses: codacy/codacy-analysis-cli-action@v4.4.0 32 | with: 33 | # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository 34 | # You can also omit the token and run the tools that support default configurations 35 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 36 | verbose: true 37 | output: results.sarif 38 | format: sarif 39 | # Adjust severity of non-security issues 40 | gh-code-scanning-compat: true 41 | # Force 0 exit code to allow SARIF file generation 42 | # This will handover control about PR rejection to the GitHub side 43 | max-allowed-issues: 2147483647 44 | 45 | # Upload the SARIF file generated in the previous step 46 | - name: Upload SARIF results file 47 | uses: github/codeql-action/upload-sarif@v3 48 | with: 49 | sarif_file: results.sarif 50 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: "22 12 * * 1" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ python ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v3 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v3 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | 15 | - name: Run semver-diff 16 | id: semver-diff 17 | uses: tj-actions/semver-diff@v3.0.1 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: '3.11.x' 23 | 24 | - name: Upgrade pip 25 | run: | 26 | pip install -U pip 27 | 28 | - name: Install dependencies 29 | run: make install-deploy 30 | 31 | - name: Setup git 32 | run: | 33 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 34 | git config --local user.name "github-actions[bot]" 35 | 36 | - name: bumpversion 37 | run: | 38 | make increase-version PART="${{ steps.semver-diff.outputs.release_type }}" 39 | 40 | - name: Build and publish 41 | run: make release 42 | env: 43 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 44 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 45 | 46 | - name: Run git-cliff 47 | uses: tj-actions/git-cliff@v1 48 | with: 49 | output: CHANGELOG.md 50 | 51 | - name: Create Pull Request 52 | uses: peter-evans/create-pull-request@v6 53 | with: 54 | base: "main" 55 | title: "Upgraded ${{ steps.semver-diff.outputs.old_version }} → ${{ steps.semver-diff.outputs.new_version }}" 56 | branch: "chore/upgrade-${{ steps.semver-diff.outputs.old_version }}-to-${{ steps.semver-diff.outputs.new_version }}" 57 | commit-message: "Upgraded from ${{ steps.semver-diff.outputs.old_version }} → ${{ steps.semver-diff.outputs.new_version }}" 58 | body: "View [CHANGES](https://github.com/${{ github.repository }}/compare/${{ steps.semver-diff.outputs.old_version }}...${{ steps.semver-diff.outputs.new_version }})" 59 | token: ${{ secrets.PAT_TOKEN }} 60 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Deploy 17 | uses: peaceiris/actions-gh-pages@v4.0.0 18 | with: 19 | github_token: ${{ secrets.GITHUB_TOKEN }} 20 | publish_dir: ./docs 21 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request_target, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: "Thanks for reporting this issue, don't forget to star this project if you haven't already to help us reach a wider audience." 13 | pr-message: "Thanks for implementing a fix, could you ensure that the test covers your changes if applicable." 14 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Pre-commit auto-merge 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | dependabot: 13 | runs-on: ubuntu-latest 14 | if: ${{ github.head_ref == 'pre-commit-ci-update-config' }} 15 | steps: 16 | - name: Enable auto-merge for Pre-commit PRs 17 | run: gh pr merge --auto --squash "$PR_URL" 18 | env: 19 | PR_URL: ${{github.event.pull_request.html_url}} 20 | GITHUB_TOKEN: ${{secrets.PAT_TOKEN}} 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | - '**' 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.platform }} 15 | strategy: 16 | fail-fast: true 17 | matrix: 18 | platform: [ubuntu-latest, macos-latest, windows-latest] 19 | python-version: [3.6, 3.7, 3.8, 3.9, '3.10', 3.11] 20 | exclude: 21 | - platform: ubuntu-latest 22 | python-version: 3.6 23 | - platform: macos-latest 24 | python-version: 3.11 25 | - platform: windows-latest 26 | python-version: 3.6 27 | - platform: windows-latest 28 | python-version: 3.11 29 | 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - name: Set up Python ${{ matrix.python-version }} 35 | uses: actions/setup-python@v5 36 | with: 37 | python-version: ${{ matrix.python-version }} 38 | 39 | - uses: actions/cache@v4.0.2 40 | id: pip-cache 41 | with: 42 | path: ~/.cache/pip 43 | key: ${{ runner.os }}-${{ matrix.platform }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }} 44 | restore-keys: | 45 | ${{ runner.os }}-${{ matrix.platform }}-pip-${{ matrix.python-version }}- 46 | 47 | - uses: actions/cache@v4.0.2 48 | id: pytest-cache 49 | with: 50 | path: | 51 | .pytest_cache 52 | key: ${{ runner.os }}-pytest-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}-{{ hashFiles('pytest.ini') }} 53 | restore-keys: | 54 | ${{ runner.os }}-pytest-${{ matrix.python-version }}- 55 | 56 | - name: Install dependencies 57 | run: | 58 | make install-test 59 | 60 | - name: Run test 61 | run: make tox 62 | env: 63 | CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} 64 | PLATFORM: ${{ matrix.platform }} 65 | 66 | - name: "Upload coverage to Codecov" 67 | uses: codecov/codecov-action@v4 68 | with: 69 | token: ${{ secrets.CODECOV_TOKEN }} 70 | fail_ci_if_error: true 71 | -------------------------------------------------------------------------------- /.github/workflows/update-doc-assets.yml: -------------------------------------------------------------------------------- 1 | name: Sync doc assets. 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | sync-assets: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4.1.3 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Run test 17 | uses: tj-actions/remark@v3 18 | 19 | - name: Verify Changed files 20 | uses: tj-actions/verify-changed-files@v19 21 | id: verify_changed_files 22 | with: 23 | files: | 24 | README.md 25 | 26 | - name: README.md changed 27 | if: steps.verify_changed_files.outputs.files_changed == 'true' 28 | run: | 29 | echo "README.md has uncommited changes" 30 | exit 1 31 | 32 | - name: Create Pull Request 33 | if: failure() 34 | uses: peter-evans/create-pull-request@v6 35 | with: 36 | base: "main" 37 | title: "Updated README.md" 38 | branch: "chore/update-readme" 39 | commit-message: "Updated README.md" 40 | body: "Updated README.md" 41 | token: ${{ secrets.PAT_TOKEN }} 42 | 43 | - name: Copy doc assets 44 | run: | 45 | cp -f README.md docs/README.md 46 | cp -f CHANGELOG.md docs/CHANGELOG.md 47 | 48 | - name: Create Pull Request 49 | uses: peter-evans/create-pull-request@v6.0.4 50 | with: 51 | commit-message: Synced README changes to docs 52 | committer: github-actions[bot] <github-actions[bot]@users.noreply@github.com> 53 | author: github-actions[bot] <github-actions[bot]@users.noreply.github.com> 54 | branch: chore/update-docs 55 | base: main 56 | delete-branch: true 57 | title: Updated docs 58 | body: | 59 | Updated docs 60 | - Auto-generated by github-actions[bot] 61 | assignees: jackton1 62 | reviewers: jackton1 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | .idea/ 3 | __pycache__ 4 | .pytest_cache 5 | *.pyc 6 | .tox/ 7 | build/ 8 | dist/ 9 | 10 | *.db 11 | 12 | .DS_Store 13 | .coverage 14 | 15 | *.mo 16 | coverage.xml 17 | 18 | venv/ 19 | .venv/ 20 | -------------------------------------------------------------------------------- /.pep8speaks.yml: -------------------------------------------------------------------------------- 1 | scanner: 2 | diff_only: True # If False, the entire file touched by the Pull Request is scanned for errors. If True, only the diff is scanned. 3 | linter: pycodestyle 4 | 5 | pycodestyle: # Same as scanner.linter value. Other option is flake8 6 | max-line-length: 100 # Default is 79 in PEP 8 7 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/PyCQA/autoflake 3 | rev: v2.3.1 4 | hooks: 5 | - id: autoflake 6 | args: ['--in-place', '--remove-all-unused-imports', '--remove-unused-variable'] 7 | 8 | - repo: https://github.com/pycqa/isort 9 | rev: 5.13.2 10 | hooks: 11 | - id: isort 12 | args: ["--profile", "black", "--filter-files"] 13 | 14 | - repo: https://github.com/pre-commit/pre-commit-hooks 15 | rev: v4.6.0 16 | hooks: 17 | - id: trailing-whitespace 18 | exclude: ^docs/.*|.*.md 19 | - id: end-of-file-fixer 20 | exclude: ^docs/.*|.*.md 21 | 22 | - repo: https://github.com/psf/black 23 | rev: 24.4.0 24 | hooks: 25 | - id: black 26 | language_version: python3 27 | 28 | - repo: https://github.com/adrienverge/yamllint.git 29 | rev: v1.35.1 30 | hooks: 31 | - id: yamllint 32 | args: ["-d", "relaxed"] 33 | 34 | - repo: https://github.com/pycqa/flake8 35 | rev: 7.0.0 36 | hooks: 37 | - id: flake8 38 | -------------------------------------------------------------------------------- /.pypirc: -------------------------------------------------------------------------------- 1 | [distutils] 2 | index-servers = 3 | pypi 4 | 5 | [pypi] 6 | repository: https://upload.pypi.org/legacy/ 7 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | # autogenerated pyup.io config file 2 | # see https://pyup.io/docs/configuration/ for all available options 3 | 4 | schedule: '' 5 | update: insecure 6 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "baseBranches": [] 4 | }, 5 | "checkRunSettings": { 6 | "vulnerableCheckRunConclusionLevel": "failure", 7 | "displayMode": "diff" 8 | }, 9 | "issueSettings": { 10 | "minSeverityLevel": "LOW" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | jtonye@ymail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Tonye Jack 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include view_breadcrumbs *.py *.html *.css *.js *.po *.mo 2 | recursive-include django_view_breadcrumbs *.py 3 | exclude *.json 4 | include *.md 5 | exclude *.png 6 | exclude *.py 7 | exclude *.txt 8 | exclude *.pypirc 9 | exclude *.all-contributorsrc 10 | exclude *.yml 11 | exclude *.pyup.yml 12 | exclude *.travis.yml 13 | exclude .bumpversion.cfg 14 | exclude .coveragerc 15 | include LICENSE 16 | exclude Makefile 17 | exclude pytest.ini 18 | exclude tox.ini 19 | prune view_breadcrumbs/tests 20 | prune demo 21 | 22 | # added by check-manifest 23 | recursive-include docs *.html 24 | recursive-include docs *.md 25 | recursive-exclude docs *.nojekyll 26 | recursive-include docs *.png 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Self-Documented Makefile see https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 2 | 3 | .DEFAULT_GOAL := help 4 | 5 | PYTHON := /usr/bin/env python3 6 | MANAGE_PY := $(PYTHON) manage.py 7 | PYTHON_PIP := /usr/bin/env pip3 8 | PIP_COMPILE := /usr/bin/env pip-compile 9 | PART := patch 10 | PACKAGE_VERSION = $(shell $(PYTHON) setup.py --version) 11 | 12 | # Put it first so that "make" without argument is like "make help". 13 | help: 14 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-32s-\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 15 | 16 | .PHONY: help 17 | 18 | guard-%: ## Checks that env var is set else exits with non 0 mainly used in CI; 19 | @if [ -z '${${*}}' ]; then echo 'Environment variable $* not set' && exit 1; fi 20 | 21 | # -------------------------------------------------------- 22 | # ------- Python package (pip) management commands ------- 23 | # -------------------------------------------------------- 24 | clean-build: ## Clean project build artifacts. 25 | @echo "Removing build assets..." 26 | @$(PYTHON) setup.py clean 27 | @rm -rf build/ 28 | @rm -rf dist/ 29 | @rm -rf *.egg-info 30 | 31 | install-wheel: clean-build ## Install wheel 32 | @echo "Installing wheel" 33 | @$(PYTHON_PIP) install wheel 34 | 35 | install: clean-build install-wheel ## Install project dependencies. 36 | @echo "Installing project in dependencies..." 37 | @$(PYTHON_PIP) install -r requirements.txt 38 | 39 | install-deploy: clean-build install-wheel ## Install deploy extra dependencies. 40 | @echo "Installing deploy extra requirements..." 41 | @$(PYTHON_PIP) install -e .'[deploy]' 42 | 43 | install-lint: clean-build install-wheel ## Install lint extra dependencies. 44 | @echo "Installing lint extra requirements..." 45 | @$(PYTHON_PIP) install -e .'[lint]' 46 | 47 | install-test: clean-build install-wheel ## Install test extra dependencies. 48 | @echo "Installing test extra requirements..." 49 | @$(PYTHON_PIP) install -e .'[test]' 50 | 51 | install-dev: clean-build install-wheel ## Install development extra dependencies. 52 | @echo "Installing development requirements..." 53 | @$(PYTHON_PIP) install -e .'[development]' -r requirements.txt 54 | 55 | update-requirements: ## Updates the requirement.txt adding missing package dependencies 56 | @echo "Syncing the package requirements.txt..." 57 | @$(PIP_COMPILE) 58 | 59 | # ---------------------------------------------------------- 60 | # --------- Django manage.py commands ---------------------- 61 | # ---------------------------------------------------------- 62 | run: compilemessages makemessages ## Run the run_server using default host and port 63 | @$(MANAGE_PY) runserver 127.0.0.1:8090 64 | 65 | migrate: ## Run the migrations 66 | @$(MANAGE_PY) migrate 67 | 68 | migrations: ## Generate the migrations 69 | @$(MANAGE_PY) makemigrations 70 | 71 | makemessages: clean-build ## Runs over the entire source tree of the current directory and pulls out all strings marked for translation. 72 | @$(MANAGE_PY) makemessages --locale=en_US --ignore=".tox*" --ignore="venv*" 73 | @$(MANAGE_PY) makemessages --locale=fr --ignore=".tox*" --ignore="venv*" 74 | 75 | compilemessages: clean-build ## Compiles .po files created by makemessages to .mo files for use with the built-in gettext support. 76 | @$(MANAGE_PY) compilemessages --ignore=".tox*" --ignore="venv*" 77 | 78 | # ---------------------------------------------------------- 79 | # ---------- Release the project to PyPI ------------------- 80 | # ---------------------------------------------------------- 81 | increase-version: guard-PART ## Increase project version 82 | @bump2version $(PART) 83 | @git switch -c main 84 | 85 | dist: clean install-deploy ## builds source and wheel package 86 | @pip install twine==3.4.1 87 | @python setup.py sdist bdist_wheel 88 | 89 | release: dist ## package and upload a release 90 | @twine upload dist/* 91 | 92 | # ---------------------------------------------------------- 93 | # --------- Run project Test ------------------------------- 94 | # ---------------------------------------------------------- 95 | test: install-test 96 | @pytest -v 97 | 98 | tox: install-test ## Run tox test 99 | @tox 100 | 101 | clean-test-all: clean-build ## Clean build and test assets. 102 | @rm -rf .tox/ 103 | @rm -rf .pytest_cache/ 104 | @rm -f test.db 105 | 106 | lint: 107 | isort . 108 | flake8 . 109 | 110 | create-docs: 111 | @npx docsify init ./docs 112 | 113 | serve-docs: 114 | @npx docsify serve ./docs 115 | 116 | clean: clean-test-all clean-build 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-view-breadcrumbs 2 | 3 | [![Test](https://github.com/tj-django/django-view-breadcrumbs/actions/workflows/test.yml/badge.svg)](https://github.com/tj-django/django-view-breadcrumbs/actions/workflows/test.yml) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/537b0ce56e744f078f17cc8ccd4200d8)](https://www.codacy.com/gh/tj-django/django-view-breadcrumbs/dashboard?utm_source=github.com\&utm_medium=referral\&utm_content=tj-django/django-view-breadcrumbs\&utm_campaign=Badge_Grade) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/tj-django/django-view-breadcrumbs/main.svg)](https://results.pre-commit.ci/latest/github/tj-django/django-view-breadcrumbs/main) [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/537b0ce56e744f078f17cc8ccd4200d8)](https://www.codacy.com/gh/tj-django/django-view-breadcrumbs/dashboard?utm_source=github.com\&utm_medium=referral\&utm_content=tj-django/django-view-breadcrumbs\&utm_campaign=Badge_Coverage) [![PyPI version](https://badge.fury.io/py/django-view-breadcrumbs.svg)](https://badge.fury.io/py/django-view-breadcrumbs) 4 | 5 | ![PyPI - Django Version](https://img.shields.io/pypi/djversions/django-view-breadcrumbs) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-view-breadcrumbs) 6 | [![Downloads](https://static.pepy.tech/badge/django-view-breadcrumbs)](https://pepy.tech/project/django-view-breadcrumbs) 7 | 8 | <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> 9 | 10 | [![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-) 11 | 12 | <!-- ALL-CONTRIBUTORS-BADGE:END --> 13 | 14 | ## Table of Contents 15 | 16 | * [Background](#background) 17 | * [Installation](#installation) 18 | * [Add `view_breadcrumbs` to your INSTALLED\_APPS](#add-view_breadcrumbs-to-your-installed_apps) 19 | * [Breadcrumb mixin classes provided.](#breadcrumb-mixin-classes-provided) 20 | * [Settings](#settings) 21 | * [Customization](#customization) 22 | * [BREADCRUMBS\_TEMPLATE](#breadcrumbs_template) 23 | * [Site wide](#site-wide) 24 | * [Overriding the breadcrumb template for a single view](#overriding-the-breadcrumb-template-for-a-single-view) 25 | * [BREADCRUMBS\_HOME\_LABEL](#breadcrumbs_home_label) 26 | * [Site wide](#site-wide-1) 27 | * [Overriding the Home label for a specific view](#overriding-the-home-label-for-a-specific-view) 28 | * [Translation support](#translation-support) 29 | * [Example](#example) 30 | * [Usage](#usage) 31 | * [View Configuration](#view-configuration) 32 | * [django-tables-2](#django-tables-2) 33 | * [URL Configuration](#url-configuration) 34 | * [Examples](#examples) 35 | * [Sample crumbs: `Posts`](#sample-crumbs-posts) 36 | * [Sample crumbs: `Home / Posts / Test - Post`](#sample-crumbs--home--posts--test---post) 37 | * [Custom crumbs: `Home / My Test Breadcrumb`](#custom-crumbs-home--my-test-breadcrumb) 38 | * [Using multiple apps](#using-multiple-apps) 39 | * [Running locally](#running-locally) 40 | * [Credits](#credits) 41 | * [Contributors ✨](#contributors-) 42 | 43 | ## Background 44 | 45 | This package provides a set of breadcrumb mixin classes that can be added to any django class based view and requires adding just `{% render_breadcrumbs %}` to the base template. 46 | 47 | <img width="1438" alt="breadcrumbs" src="https://user-images.githubusercontent.com/17484350/128493747-776706bf-d46c-4b57-ba54-c64fcc71ada7.png"> 48 | 49 | In the `base.html` template add the `render_breadcrumbs` tag and any template that inherits the base should have breadcrumbs included. 50 | 51 | **Example:** 52 | 53 | my_app 54 | |--templates 55 | |--base.html 56 | |--create.html 57 | 58 | `base.html` 59 | 60 | ```jinja2 61 | {% load view_breadcrumbs %} 62 | 63 | {% block breadcrumbs %} 64 | {% render_breadcrumbs %} {# Optionally provide a custom template e.g {% render_breadcrumbs "view_breadcrumbs/bootstrap5.html" %} #} 65 | {% endblock %} 66 | ``` 67 | 68 | And your `create.html`. 69 | 70 | ```jinja2 71 | {% extends "base.html" %} 72 | ``` 73 | 74 | ## Installation 75 | 76 | ```bash 77 | $ pip install django-view-breadcrumbs 78 | 79 | ``` 80 | 81 | ### Add `view_breadcrumbs` to your INSTALLED\_APPS 82 | 83 | ```python 84 | 85 | INSTALLED_APPS = [ 86 | ..., 87 | "view_breadcrumbs", 88 | ..., 89 | ] 90 | ``` 91 | 92 | ## Breadcrumb mixin classes provided. 93 | 94 | * `BaseBreadcrumbMixin` - Subclasses requires a `crumbs` class property. 95 | * `CreateBreadcrumbMixin` - For create views `Home / Posts / Add Post` 96 | * `DetailBreadcrumbMixin` - For detail views `Home / Posts / Post 1` 97 | * `ListBreadcrumbMixin` - For list views `Home / Posts` 98 | * `UpdateBreadcrumbMixin` - For Update views `Home / Posts / Post 1 / Update Post 1` 99 | * `DeleteBreadcrumbMixin` - For Delete views this has a link to the list view to be used as the success URL. 100 | 101 | ## Settings 102 | 103 | > NOTE :warning: 104 | > 105 | > * Make sure that `"django.template.context_processors.request"` is added to your TEMPLATE OPTIONS setting. 106 | 107 | ```python 108 | TEMPLATES = [ 109 | { 110 | "BACKEND": "django.template.backends.django.DjangoTemplates", 111 | "APP_DIRS": True, 112 | "OPTIONS": { 113 | "context_processors": [ 114 | "django.template.context_processors.debug", 115 | "django.template.context_processors.request", # <- This context processor is required 116 | "django.contrib.auth.context_processors.auth", 117 | "django.contrib.messages.context_processors.messages", 118 | ], 119 | }, 120 | }, 121 | ] 122 | ``` 123 | 124 | Modify the defaults using the following: 125 | 126 | | Name | Default | Description | Options | 127 | |----------------------------|---------------------------------------------|-------------|---------------------| 128 | | `BREADCRUMBS_TEMPLATE` | `"view_breadcrumbs/bootstrap5.html"` | Template used to render breadcrumbs. | [Predefined Templates](https://github.com/tj-django/django-view-breadcrumbs/tree/main/view_breadcrumbs/templates/view_breadcrumbs) | 129 | | `BREADCRUMBS_HOME_LABEL` | `Home` | Default label for the root path | | 130 | 131 | ### Customization 132 | 133 | #### BREADCRUMBS\_TEMPLATE 134 | 135 | ##### Site wide 136 | 137 | ```python 138 | BREADCRUMBS_TEMPLATE = "my_app/breadcrumbs.html" 139 | ``` 140 | 141 | ##### Overriding the breadcrumb template for a single view 142 | 143 | Update the `base.html` 144 | 145 | ```jinja2 146 | {% render_breadcrumbs "my_app/breadcrumbs.html" %} 147 | ``` 148 | 149 | #### BREADCRUMBS\_HOME\_LABEL 150 | 151 | ##### Site wide 152 | 153 | ```python 154 | BREADCRUMBS_HOME_LABEL = "My new home" 155 | ``` 156 | 157 | ##### Overriding the Home label for a specific view 158 | 159 | ```python 160 | from django.utils.translation import gettext_lazy as _ 161 | from view_breadcrumbs import DetailBreadcrumbMixin 162 | from django.views.generic import DetailView 163 | from demo.models import TestModel 164 | 165 | 166 | class TestDetailView(DetailBreadcrumbMixin, DetailView): 167 | model = TestModel 168 | home_label = _("My new home") 169 | template_name = "demo/test-detail.html" 170 | ``` 171 | 172 | *Renders* 173 | 174 | <img width="436" alt="custom-root-breadcrumb" src="https://user-images.githubusercontent.com/17484350/128493798-c71a8071-e913-4875-80b6-c7b414ac4e24.png"> 175 | 176 | ## [Translation support](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/) 177 | 178 | ### Example 179 | 180 | ![translated-crumbs](https://user-images.githubusercontent.com/17484350/128493830-7e50a6a9-3648-48cb-b198-4646ee2b03cf.png) 181 | 182 | ## Usage 183 | 184 | `django-view-breadcrumbs` includes generic mixins that can be added to a class based view. 185 | 186 | Using the generic breadcrumb mixin each breadcrumb will be added to the view dynamically 187 | and can be overridden by providing a `crumbs` property. 188 | 189 | ### View Configuration 190 | 191 | > NOTE: :warning: 192 | > 193 | > * Model based views should use a pattern `view_name=model_verbose_name_{action}` 194 | 195 | | Actions | View Class | View name | Sample Breadcrumb | Example | 196 | |-----------|-------------|-------------|-------------------|----------| 197 | | `list` | [`ListView`](https://docs.djangoproject.com/en/3.2/ref/class-based-views/generic-display/#listview) | `{model.verbose_name}_list` | `Home / Posts` | [Posts Example](https://github.com/tj-django/django-view-breadcrumbs#sample-crumbs-posts) | 198 | | `create` | [`CreateView`](https://docs.djangoproject.com/en/3.2/ref/class-based-views/generic-editing/#createview) | `{model.verbose_name}_create` | `Home / Posts / Add Post` | | 199 | | `detail` | [`DetailView`](https://docs.djangoproject.com/en/3.2/ref/class-based-views/generic-display/#detailview) | `{model.verbose_name}_detail` | `Home / Posts / Test - Post` | | 200 | | `change` | [`UpdateView`](https://docs.djangoproject.com/en/3.2/ref/class-based-views/generic-editing/#updateview) | `{model.verbose_name}_update` | `Home / Posts / Test - Post / Update Test - Post` | | 201 | | `delete` | [`DeleteView`](https://docs.djangoproject.com/en/3.2/ref/class-based-views/generic-editing/#deleteview) | `{model.verbose_name}_delete` | N/A | 202 | | N/A | [`TemplateView`](https://docs.djangoproject.com/en/3.2/ref/class-based-views/base/#templateview) | N/A | N/A | See: [Custom View](#custom-crumbs-home--my-test-breadcrumb) | 203 | | N/A | [`FormView`](https://docs.djangoproject.com/en/3.2/ref/class-based-views/generic-editing/#formview) | N/A | N/A | See: [Custom View](#custom-crumbs-home--my-test-breadcrumb) | 204 | | N/A | [`AboutView`](https://docs.djangoproject.com/en/3.2/topics/class-based-views/#subclassing-generic-views) | N/A | N/A | See: [Custom View](#custom-crumbs-home--my-test-breadcrumb) | 205 | | N/A | [`View`](https://docs.djangoproject.com/en/3.2/ref/class-based-views/base/#view) | N/A | N/A | See: [Custom View](#custom-crumbs-home--my-test-breadcrumb) | 206 | 207 | #### [django-tables-2](https://django-tables2.readthedocs.io/en/latest/index.html#) 208 | 209 | | Actions | View Class | View name | Sample Breadcrumb | Example | 210 | |-----------|-------------|-------------|-------------------|----------| 211 | | N/A | [`SingleTableMixin`](https://django-tables2.readthedocs.io/en/latest/pages/generic-mixins.html?highlight=SingleTableMixin#a-single-table-using-singletablemixin) | N/A | N/A | See: [demo table view](https://github.com/tj-django/django-view-breadcrumbs/blob/main/demo/views.py#L154-L162) | 212 | | N/A | [`MultiTableMixin`](https://django-tables2.readthedocs.io/en/latest/pages/generic-mixins.html?highlight=SingleTableMixin#multiple-tables-using-multitablemixin) | N/A | N/A | See: [demo table view](https://github.com/tj-django/django-view-breadcrumbs/blob/main/demo/views.py#L166-L173) | 213 | | N/A | [`SingleTableView`](https://django-tables2.readthedocs.io/en/latest/pages/api-reference.html?highlight=SingleTableView#singletableview) | N/A | N/A | Same implementation as `SingleTableMixin` | 214 | 215 | For more examples see: [demo app](https://github.com/tj-django/django-view-breadcrumbs/tree/main/demo) 216 | 217 | ### URL Configuration 218 | 219 | Based on the table of actions listed above there's a strict `view_name` requirement that needs to be adhered to in order for breadcrumbs to work. 220 | 221 | This can be manually entered in your `urls.py` or you can optionally use the following class properties instead of hardcoding the `view_name`. 222 | 223 | ```python 224 | ... 225 | path("tests/", TestListsView.as_view(), name=TestListsView.list_view_name), 226 | path( 227 | "tests/<slug:slug>/", 228 | TestDetailView.as_view(), 229 | name=TestDetailView.detail_view_name, 230 | ), 231 | path( 232 | "tests/<slug:slug>/update/", 233 | TestUpdateView.as_view(), 234 | name=TestUpdateView.update_view_name, 235 | ), 236 | path( 237 | "tests/<slug:slug>/delete/", 238 | TestDeleteView.as_view(), 239 | name=TestDeleteView.delete_view_name, 240 | ), 241 | ... 242 | ``` 243 | 244 | ### Examples 245 | 246 | #### Sample crumbs: `Posts` 247 | 248 | In your urls.py 249 | 250 | ```python 251 | urlpatterns = [ 252 | ... 253 | path("posts/", views.PostList.as_view(), name="post_list"), 254 | ... 255 | # OR 256 | ... 257 | path("posts/", views.PostList.as_view(), name=views.PostList.list_view_name), 258 | ... 259 | ] 260 | ``` 261 | 262 | > All crumbs use the home root path `/` as the base this can be excluded by specifying `add_home = False` 263 | 264 | ```python 265 | from django.views.generic import ListView 266 | from view_breadcrumbs import ListBreadcrumbMixin 267 | 268 | 269 | class PostList(ListBreadcrumbMixin, ListView): 270 | model = Post 271 | template_name = "app/post/list.html" 272 | add_home = False 273 | ``` 274 | 275 | #### Sample crumbs: `Home / Posts / Test - Post` 276 | 277 | In your `urls.py` 278 | 279 | ```python 280 | urlpatterns = [ 281 | ... 282 | path("posts/<slug:slug>/", views.PostDetail.as_view(), name="post_detail"), 283 | ... 284 | # OR 285 | ... 286 | path("posts/<slug:slug>/", views.PostDetail.as_view(), name=views.PostDetail.detail_view_name), 287 | ... 288 | ] 289 | 290 | ``` 291 | 292 | `views.py` 293 | 294 | ```python 295 | from django.views.generic import DetailView 296 | from view_breadcrumbs import DetailBreadcrumbMixin 297 | 298 | 299 | class PostDetail(DetailBreadcrumbMixin, DetailView): 300 | model = Post 301 | template_name = "app/post/detail.html" 302 | breadcrumb_use_pk = False 303 | ``` 304 | 305 | #### Custom crumbs: `Home / My Test Breadcrumb` 306 | 307 | URL configuration. 308 | 309 | ```python 310 | urlpatterns = [ 311 | path("my-custom-view/", views.CustomView.as_view(), name="custom_view"), 312 | ] 313 | ``` 314 | 315 | views.py 316 | 317 | ```python 318 | from django.urls import reverse 319 | from django.views.generic import View 320 | from view_breadcrumbs import BaseBreadcrumbMixin 321 | from demo.models import TestModel 322 | 323 | 324 | class CustomView(BaseBreadcrumbMixin, View): 325 | model = TestModel 326 | template_name = "app/test/custom.html" 327 | crumbs = [("My Test Breadcrumb", reverse("custom_view"))] # OR reverse_lazy 328 | ``` 329 | 330 | **OR** 331 | 332 | ```python 333 | from django.urls import reverse 334 | from django.views.generic import View 335 | from view_breadcrumbs import BaseBreadcrumbMixin 336 | from demo.models import TestModel 337 | from django.utils.functional import cached_property 338 | 339 | 340 | class CustomView(BaseBreadcrumbMixin, View): 341 | template_name = "app/test/custom.html" 342 | 343 | @cached_property 344 | def crumbs(self): 345 | return [("My Test Breadcrumb", reverse("custom_view"))] 346 | 347 | ``` 348 | 349 | > Refer to the [demo app](https://github.com/tj-django/django-view-breadcrumbs/tree/main/demo) for more examples. 350 | 351 | ### Using multiple apps 352 | 353 | To reference models from a different application you need to override the `app_name` class attribute. 354 | 355 | Example: 356 | Using a `Library` model that is imported from a `custom` application that you want to render in a `demo` app view. 357 | 358 | ```python 359 | INSTALLED_APPS = [ 360 | ... 361 | "demo", 362 | "custom", 363 | ... 364 | ] 365 | ``` 366 | 367 | `demo/views.py` 368 | 369 | ```python 370 | class LibraryDetailView(DetailBreadcrumbMixin, DetailView): 371 | model = Library 372 | app_name = "demo" 373 | ... 374 | ``` 375 | 376 | ## Running locally 377 | 378 | ```bash 379 | $ git clone git@github.com:tj-django/django-view-breadcrumbs.git 380 | $ make install-dev 381 | $ make migrate 382 | $ make run 383 | ``` 384 | 385 | Spins up a django server running the demo app. 386 | 387 | Visit `http://127.0.0.1:8090` 388 | 389 | ## Credits 390 | 391 | * [django-bootstrap-breadcrumbs](https://github.com/prymitive/bootstrap-breadcrumbs) 392 | 393 | To file a bug or submit a patch, please head over to [django-view-breadcrumbs on github](https://github.com/tj-django/django-view-breadcrumbs/issues). 394 | 395 | If you feel generous and want to show some extra appreciation: 396 | 397 | Support me with a :star: 398 | 399 | [![Buy me a coffee][buymeacoffee-shield]][buymeacoffee] 400 | 401 | [buymeacoffee]: https://www.buymeacoffee.com/jackton1 402 | 403 | [buymeacoffee-shield]: https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png 404 | 405 | ## Contributors ✨ 406 | 407 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 408 | 409 | <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> 410 | 411 | <!-- prettier-ignore-start --> 412 | 413 | <!-- markdownlint-disable --> 414 | 415 | <table> 416 | <tr> 417 | <td align="center"><a href="https://fansourcedpoisontour.com"><img src="https://avatars3.githubusercontent.com/u/1037197?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Derek</b></sub></a><br /><a href="https://github.com/tj-django/django-view-breadcrumbs/commits?author=KrunchMuffin" title="Documentation">📖</a></td> 418 | <td align="center"><a href="http://www.emencia.com"><img src="https://avatars.githubusercontent.com/u/1572165?v=4?s=100" width="100px;" alt=""/><br /><sub><b>David THENON</b></sub></a><br /><a href="https://github.com/tj-django/django-view-breadcrumbs/commits?author=sveetch" title="Code">💻</a></td> 419 | </tr> 420 | </table> 421 | 422 | <!-- markdownlint-restore --> 423 | 424 | <!-- prettier-ignore-end --> 425 | 426 | <!-- ALL-CONTRIBUTORS-LIST:END --> 427 | 428 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 429 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from django import setup 5 | from django.conf import settings 6 | 7 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 8 | TEST_DIR = os.path.join(BASE_DIR, "demo", "templates") 9 | 10 | 11 | def pytest_configure(debug=False): 12 | base_settings = dict( 13 | SECRET_KEY="rprowrjp4293u2904u290422;jk4l", 14 | DEBUG=debug, 15 | DATABASES={ 16 | "default": { 17 | "ENGINE": "django.db.backends.sqlite3", 18 | "NAME": "test.db", 19 | } 20 | }, 21 | INSTALLED_APPS=["view_breadcrumbs", "demo", "custom"], 22 | ROOT_URLCONF="demo.urls", 23 | USE_I18N=True, 24 | USE_L10N=True, 25 | # Provide a lists of languages which your site supports. 26 | LANGUAGES=( 27 | ("en", "English"), 28 | ("fr", "French"), 29 | ), 30 | # Set the default language for your site. 31 | LANGUAGE_CODE="en", 32 | # Tell Django where the project's translation files should be. 33 | LOCALE_PATHS=(os.path.join(BASE_DIR, "view_breadcrumbs", "locale"),), 34 | TEMPLATES=[ 35 | { 36 | "BACKEND": "django.template.backends.django.DjangoTemplates", 37 | "DIRS": [TEST_DIR], 38 | "APP_DIRS": True, 39 | "OPTIONS": { 40 | "context_processors": [ 41 | "django.template.context_processors.debug", 42 | "django.template.context_processors.request", 43 | "django.contrib.auth.context_processors.auth", 44 | "django.contrib.messages.context_processors.messages", 45 | ], 46 | }, 47 | }, 48 | ], 49 | ) 50 | 51 | if debug: 52 | base_settings.update( 53 | { 54 | "ALLOWED_HOSTS": ["127.0.0.1", "localhost"], 55 | "INSTALLED_APPS": [ 56 | "django.contrib.auth", 57 | "django.contrib.contenttypes", 58 | "django.contrib.sessions", 59 | "view_breadcrumbs", 60 | "demo", 61 | "custom", 62 | "django_tables2", 63 | "django_bootstrap5", 64 | "django_filters", 65 | ], 66 | } 67 | ) 68 | settings.configure(**base_settings) 69 | setup() 70 | if not debug: 71 | create_db() 72 | 73 | 74 | def create_db(): 75 | if sys.version_info > (3, 5): 76 | from django.db import connection 77 | 78 | with connection.cursor() as c: 79 | c.executescript( 80 | """ 81 | BEGIN; 82 | -- 83 | -- Create model TestModel 84 | -- 85 | DROP TABLE IF EXISTS "demo_testmodel"; 86 | CREATE TABLE "demo_testmodel" ( 87 | "created_at" datetime NOT NULL, 88 | "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 89 | "update_at" datetime NOT NULL, 90 | "name" varchar(50) NOT NULL 91 | ); 92 | COMMIT; 93 | """ 94 | ) 95 | -------------------------------------------------------------------------------- /custom/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tj-django/django-view-breadcrumbs/de91af7c1eb6a9be69f85fe38e146d92bcc29bf0/custom/__init__.py -------------------------------------------------------------------------------- /custom/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /custom/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CustomConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "custom" 7 | -------------------------------------------------------------------------------- /custom/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.6 on 2021-08-04 08:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | initial = True 8 | 9 | dependencies = [] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="Library", 14 | fields=[ 15 | ( 16 | "id", 17 | models.BigAutoField( 18 | auto_created=True, 19 | primary_key=True, 20 | serialize=False, 21 | verbose_name="ID", 22 | ), 23 | ), 24 | ("name", models.CharField(max_length=255)), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /custom/migrations/0002_auto_20210804_0828.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.6 on 2021-08-04 08:28 2 | 3 | from django.db import migrations 4 | 5 | 6 | def forwards_func(apps, schema_editor): 7 | Library = apps.get_model("custom", "Library") 8 | 9 | Library.objects.bulk_create( 10 | [Library(name="test library {}".format(i)) for i in range(5)] 11 | ) 12 | 13 | 14 | class Migration(migrations.Migration): 15 | dependencies = [ 16 | ("custom", "0001_initial"), 17 | ] 18 | 19 | operations = [migrations.RunPython(forwards_func, migrations.RunPython.noop)] 20 | -------------------------------------------------------------------------------- /custom/migrations/0003_alter_library_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.6 on 2021-08-04 11:26 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("custom", "0002_auto_20210804_0828"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name="library", 14 | options={"verbose_name": "library", "verbose_name_plural": "libraries"}, 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /custom/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tj-django/django-view-breadcrumbs/de91af7c1eb6a9be69f85fe38e146d92bcc29bf0/custom/migrations/__init__.py -------------------------------------------------------------------------------- /custom/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class Library(models.Model): 6 | name = models.CharField(max_length=255) 7 | 8 | class Meta: 9 | verbose_name = _("library") 10 | verbose_name_plural = _("libraries") 11 | 12 | def __str__(self): 13 | return str(_(self.name)) 14 | -------------------------------------------------------------------------------- /custom/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /custom/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tj-django/django-view-breadcrumbs/de91af7c1eb6a9be69f85fe38e146d92bcc29bf0/demo/__init__.py -------------------------------------------------------------------------------- /demo/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DemoConfig(AppConfig): 5 | name = "demo" 6 | default_auto_field = "django.db.models.AutoField" 7 | -------------------------------------------------------------------------------- /demo/filterset.py: -------------------------------------------------------------------------------- 1 | from django_filters import FilterSet 2 | 3 | from demo.models import TestModel 4 | 5 | 6 | class TestModelFilterSet(FilterSet): 7 | class Meta: 8 | model = TestModel 9 | fields = ["name", "update_at"] 10 | -------------------------------------------------------------------------------- /demo/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.5 on 2018-05-07 04:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | initial = True 8 | 9 | dependencies = [] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="TestModel", 14 | fields=[ 15 | ( 16 | "id", 17 | models.AutoField( 18 | auto_created=True, 19 | primary_key=True, 20 | serialize=False, 21 | verbose_name="ID", 22 | ), 23 | ), 24 | ("created_at", models.DateTimeField(auto_now_add=True)), 25 | ("update_at", models.DateTimeField(auto_now=True)), 26 | ("name", models.CharField(max_length=50)), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /demo/migrations/0002_auto_20190619_1122.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.8 on 2019-06-19 11:22 2 | 3 | from django.db import migrations 4 | 5 | 6 | def forwards_create_dummy_test(apps, schema_editor): 7 | TestModel = apps.get_model("demo", "TestModel") 8 | 9 | TestModel.objects.bulk_create( 10 | [TestModel(name="new test {}".format(i)) for i in range(5)] 11 | ) 12 | 13 | 14 | class Migration(migrations.Migration): 15 | dependencies = [ 16 | ("demo", "0001_initial"), 17 | ] 18 | 19 | operations = [ 20 | migrations.RunPython(forwards_create_dummy_test, migrations.RunPython.noop) 21 | ] 22 | -------------------------------------------------------------------------------- /demo/migrations/0003_auto_20210325_2151.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-03-25 21:51 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("demo", "0002_auto_20190619_1122"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name="testmodel", 14 | options={ 15 | "verbose_name": "test model", 16 | "verbose_name_plural": "test models", 17 | }, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /demo/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tj-django/django-view-breadcrumbs/de91af7c1eb6a9be69f85fe38e146d92bcc29bf0/demo/migrations/__init__.py -------------------------------------------------------------------------------- /demo/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class TestModel(models.Model): 6 | created_at = models.DateTimeField(auto_now_add=True) 7 | update_at = models.DateTimeField(auto_now=True) 8 | name = models.CharField(max_length=50) 9 | 10 | class Meta: 11 | verbose_name = _("test model") 12 | verbose_name_plural = _("test models") 13 | 14 | def __str__(self): 15 | return str(_(self.name)) 16 | 17 | __repr__ = __str__ 18 | -------------------------------------------------------------------------------- /demo/tables.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.utils.html import format_html 3 | from django_tables2 import Table 4 | 5 | from demo.models import TestModel 6 | 7 | 8 | class TestModelTable(Table): 9 | class Meta: 10 | model = TestModel 11 | template_name = "django_tables2/bootstrap.html" 12 | fields = ("id", "name") 13 | 14 | @staticmethod 15 | def render_name(value, record): 16 | return format_html( 17 | "<a href={}>{}</b>", 18 | reverse("demo:testmodel_detail", kwargs={"pk": record.pk}), 19 | value, 20 | ) 21 | -------------------------------------------------------------------------------- /demo/templates/demo/index.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load view_breadcrumbs %} 3 | <!DOCTYPE html> 4 | <html lang="en"> 5 | <head> 6 | <title>Test app 7 | 8 | 9 | 10 | {% block styles %} 11 | 12 | 13 | {% endblock %} 14 | 15 | 16 | {% block breadcrumbs %} 17 |
18 | {% render_breadcrumbs 'view_breadcrumbs/bootstrap5.html' %} 19 |
20 | {% endblock %} 21 | {% block body %} 22 |
23 | 30 |
31 | {% endblock %} 32 | 33 | -------------------------------------------------------------------------------- /demo/templates/demo/test-create.html: -------------------------------------------------------------------------------- 1 | {% extends "demo/index.html" %} 2 | {% load i18n %} 3 | {% block body %} 4 |
5 | {% csrf_token %} 6 | {{ form }} 7 | 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /demo/templates/demo/test-custom.html: -------------------------------------------------------------------------------- 1 | {% extends "demo/index.html" %} 2 | {% block body %} 3 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /demo/templates/demo/test-detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'demo/test-custom.html' %} 2 | {% load i18n l10n view_breadcrumbs %} 3 | {% block body %} 4 |

{% trans "Name" %}: {% trans object.name %}

5 | {% if object.created_at %} 6 |

{% trans "Created at" %}: {{ object.created_at|localize }}

7 | {% endif %} 8 | {% if object.update_at %} 9 |

{% trans "Updated at" %}: {{ object.update_at|localize }}

10 | {% endif %} 11 | {% trans "Edit" %} 12 |
13 | {% csrf_token %} 14 | 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /demo/templates/demo/test-list.html: -------------------------------------------------------------------------------- 1 | {% extends 'demo/test-custom.html' %} 2 | {% load i18n l10n view_breadcrumbs %} 3 | {% block body %} 4 | {{ block.super }} 5 | {% trans "Create" %} 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /demo/templates/demo/test-multi-table.html: -------------------------------------------------------------------------------- 1 | {% extends "demo/index.html" %} 2 | {% load django_tables2 %} 3 | 4 | {% block body %} 5 | {% for table in tables %} 6 |

Table {{ forloop.counter }}

7 | {% render_table table %} 8 | {% endfor %} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /demo/templates/demo/test-table-list.html: -------------------------------------------------------------------------------- 1 | {% extends "demo/index.html" %} 2 | {% load django_tables2 django_bootstrap5 %} 3 | {% block styles %} 4 | 5 | 6 | {% endblock %} 7 | {% block body %} 8 | {% if filter %} 9 |
10 | {% bootstrap_form filter.form layout='inline' %} 11 | {% bootstrap_button 'filter' %} 12 |
13 | {% endif %} 14 | {% render_table table %} 15 | 16 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /demo/templates/demo/test-update.html: -------------------------------------------------------------------------------- 1 | {% extends "demo/test-create.html" %} 2 | -------------------------------------------------------------------------------- /demo/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | Examples: 3 | For the test LIST and CRUD views. 4 | [ 5 | path('test/', views.TestListsView.as_view(), name='test_list'), 6 | path('test/add/', views.TestCreateView.as_view(), name='test_add'), 7 | path('test//change', views.TestUpdateView.as_view(), name='test_change'), 8 | ... 9 | ] 10 | """ 11 | 12 | try: 13 | from django.urls import include 14 | from django.urls import re_path as path 15 | except ImportError: 16 | from django.conf.urls import url as path, include 17 | 18 | from .views import ( 19 | LibraryCreateView, 20 | LibraryDeleteView, 21 | LibraryDetailView, 22 | LibraryListsView, 23 | LibraryUpdateView, 24 | TestCreateView, 25 | TestDeleteView, 26 | TestDetailView, 27 | TestHomeView, 28 | TestListsView, 29 | TestModelMultiTableView, 30 | TestModelSingleTableView, 31 | TestUpdateView, 32 | TestView, 33 | ) 34 | 35 | app_name = "demo" 36 | 37 | 38 | test_patterns = ( 39 | [ 40 | # Home view 41 | path("^$", TestHomeView.as_view(), name="test_root"), 42 | # Custom view 43 | path("^test/custom/$", TestView.as_view(), name="test_view"), 44 | # CRUD views. 45 | path("^tests/$", TestListsView.as_view(), name=TestListsView.list_view_name), 46 | path( 47 | "^tests/add/$", 48 | TestCreateView.as_view(), 49 | name=TestCreateView.create_view_name, 50 | ), 51 | path( 52 | "^tests/(?P[0-9]+)/$", 53 | TestDetailView.as_view(), 54 | name=TestDetailView.detail_view_name, 55 | ), 56 | path( 57 | "^tests/(?P[0-9]+)/update/$", 58 | TestUpdateView.as_view(), 59 | name=TestUpdateView.update_view_name, 60 | ), 61 | path( 62 | "^tests/(?P[0-9]+)/delete/$", 63 | TestDeleteView.as_view(), 64 | name=TestDeleteView.delete_view_name, 65 | ), 66 | path( 67 | "^libraryies/$", 68 | LibraryListsView.as_view(), 69 | name=LibraryListsView.list_view_name, 70 | ), 71 | path( 72 | "^libraryies/(?P[0-9]+)/$", 73 | LibraryDetailView.as_view(), 74 | name=LibraryDetailView.detail_view_name, 75 | ), 76 | path( 77 | "^libraries/add/$", 78 | LibraryCreateView.as_view(), 79 | name=LibraryCreateView.create_view_name, 80 | ), 81 | path( 82 | "^libraries/(?P[0-9]+)/update/$", 83 | LibraryUpdateView.as_view(), 84 | name=LibraryUpdateView.update_view_name, 85 | ), 86 | path( 87 | "^libraries/(?P[0-9]+)/delete/$", 88 | LibraryDeleteView.as_view(), 89 | name=LibraryDeleteView.delete_view_name, 90 | ), 91 | path( 92 | "^tests/lists/$", 93 | TestModelSingleTableView.as_view(), 94 | name="test_model_table", 95 | ), 96 | path( 97 | "^tests/lists/multiple/$", 98 | TestModelMultiTableView.as_view(), 99 | name="test_model_multi_table", 100 | ), 101 | ], 102 | app_name, 103 | ) 104 | 105 | 106 | urlpatterns = [path("", include(test_patterns, namespace=app_name))] 107 | -------------------------------------------------------------------------------- /demo/views.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.utils.functional import cached_property 3 | from django.utils.translation import gettext_lazy as _ 4 | from django.views.generic import ( 5 | CreateView, 6 | DeleteView, 7 | DetailView, 8 | ListView, 9 | TemplateView, 10 | UpdateView, 11 | ) 12 | from django_filters.views import FilterView 13 | from django_tables2 import MultiTableMixin, SingleTableMixin 14 | 15 | from custom.models import Library 16 | from demo.filterset import TestModelFilterSet 17 | from demo.models import TestModel 18 | from demo.tables import TestModelTable 19 | from view_breadcrumbs import ( 20 | BaseBreadcrumbMixin, 21 | CreateBreadcrumbMixin, 22 | DeleteBreadcrumbMixin, 23 | DetailBreadcrumbMixin, 24 | ListBreadcrumbMixin, 25 | UpdateBreadcrumbMixin, 26 | ) 27 | from view_breadcrumbs.generic.base import BaseModelBreadcrumbMixin 28 | from view_breadcrumbs.templatetags.view_breadcrumbs import detail_instance_view_url 29 | 30 | 31 | class TestHomeView(BaseBreadcrumbMixin, TemplateView): 32 | template_name = "demo/index.html" 33 | crumbs = [] 34 | 35 | 36 | class TestView(ListBreadcrumbMixin, ListView): 37 | model = TestModel 38 | template_name = "demo/test-custom.html" 39 | crumbs = [(_("My Test Breadcrumb"), "test_view")] 40 | 41 | def get_context_data(self, **kwargs): 42 | context = super().get_context_data(**kwargs) 43 | context["view_paths"] = [ 44 | (_("List tests"), reverse("demo:testmodel_list")), 45 | ] 46 | return context 47 | 48 | 49 | class TestListsView(ListBreadcrumbMixin, ListView): 50 | model = TestModel 51 | template_name = "demo/test-list.html" 52 | 53 | def get_context_data(self, **kwargs): 54 | context = super().get_context_data(**kwargs) 55 | view_paths = [] 56 | 57 | for instance in self.object_list: 58 | view_paths.append( 59 | ( 60 | instance.name, 61 | detail_instance_view_url(instance), 62 | ), 63 | ) 64 | context["view_paths"] = view_paths 65 | return context 66 | 67 | 68 | class TestCreateView(CreateBreadcrumbMixin, CreateView): 69 | model = TestModel 70 | template_name = "demo/test-create.html" 71 | fields = ["name"] 72 | 73 | def get_success_url(self) -> str: 74 | return self.list_view_url 75 | 76 | 77 | class TestDetailView(DetailBreadcrumbMixin, DetailView): 78 | model = TestModel 79 | home_label = _("My new home") 80 | template_name = "demo/test-detail.html" 81 | 82 | 83 | class TestUpdateView(UpdateBreadcrumbMixin, UpdateView): 84 | model = TestModel 85 | template_name = "demo/test-update.html" 86 | fields = ["name"] 87 | 88 | def get_success_url(self) -> str: 89 | return self.detail_view_url(self.object) 90 | 91 | 92 | class TestDeleteView(DeleteBreadcrumbMixin, DeleteView): 93 | model = TestModel 94 | 95 | def get_success_url(self) -> str: 96 | return self.list_view_url 97 | 98 | 99 | class LibraryListsView(ListBreadcrumbMixin, ListView): 100 | model = Library 101 | template_name = "demo/test-list.html" 102 | app_name = "demo" 103 | 104 | def get_context_data(self, **kwargs): 105 | context = super().get_context_data(**kwargs) 106 | view_paths = [] 107 | 108 | for instance in self.object_list: 109 | view_paths.append( 110 | ( 111 | instance.name, 112 | detail_instance_view_url(instance, app_name=self.app_name), 113 | ), 114 | ) 115 | context["view_paths"] = view_paths 116 | return context 117 | 118 | 119 | class LibraryDetailView(DetailBreadcrumbMixin, DetailView): 120 | model = Library 121 | home_label = _("My new home") 122 | app_name = "demo" 123 | template_name = "demo/test-detail.html" 124 | 125 | 126 | class LibraryCreateView(CreateBreadcrumbMixin, CreateView): 127 | model = Library 128 | template_name = "demo/test-create.html" 129 | app_name = "demo" 130 | fields = ["name"] 131 | 132 | def get_success_url(self) -> str: 133 | return self.list_view_url 134 | 135 | 136 | class LibraryUpdateView(UpdateBreadcrumbMixin, UpdateView): 137 | model = Library 138 | template_name = "demo/test-update.html" 139 | app_name = "demo" 140 | fields = ["name"] 141 | 142 | def get_success_url(self) -> str: 143 | return self.detail_view_url(self.object) 144 | 145 | 146 | class LibraryDeleteView(DeleteBreadcrumbMixin, DeleteView): 147 | model = Library 148 | app_name = "demo" 149 | 150 | def get_success_url(self) -> str: 151 | return self.list_view_url 152 | 153 | 154 | class TestModelSingleTableView(BaseModelBreadcrumbMixin, SingleTableMixin, FilterView): 155 | model = TestModel 156 | table_class = TestModelTable 157 | filterset_class = TestModelFilterSet 158 | template_name = "demo/test-table-list.html" 159 | 160 | @cached_property 161 | def crumbs(self): 162 | return [(self.model_name_title_plural, "/")] 163 | 164 | 165 | class TestModelMultiTableView(BaseBreadcrumbMixin, MultiTableMixin, TemplateView): 166 | template_name = "demo/test-multi-table.html" 167 | tables = [ 168 | TestModelTable(TestModel.objects.all()), 169 | TestModelTable(TestModel.objects.all(), exclude=("id",)), 170 | ] 171 | 172 | table_pagination = {"per_page": 10} 173 | 174 | @cached_property 175 | def crumbs(self): 176 | return [("Multi Tables", "/")] 177 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tj-django/django-view-breadcrumbs/de91af7c1eb6a9be69f85fe38e146d92bcc29bf0/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # django-view-breadcrumbs 2 | 3 | [![Test](https://github.com/tj-django/django-view-breadcrumbs/actions/workflows/test.yml/badge.svg)](https://github.com/tj-django/django-view-breadcrumbs/actions/workflows/test.yml) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/537b0ce56e744f078f17cc8ccd4200d8)](https://www.codacy.com/gh/tj-django/django-view-breadcrumbs/dashboard?utm_source=github.com\&utm_medium=referral\&utm_content=tj-django/django-view-breadcrumbs\&utm_campaign=Badge_Grade) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/tj-django/django-view-breadcrumbs/main.svg)](https://results.pre-commit.ci/latest/github/tj-django/django-view-breadcrumbs/main) [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/537b0ce56e744f078f17cc8ccd4200d8)](https://www.codacy.com/gh/tj-django/django-view-breadcrumbs/dashboard?utm_source=github.com\&utm_medium=referral\&utm_content=tj-django/django-view-breadcrumbs\&utm_campaign=Badge_Coverage) [![PyPI version](https://badge.fury.io/py/django-view-breadcrumbs.svg)](https://badge.fury.io/py/django-view-breadcrumbs) 4 | 5 | ![PyPI - Django Version](https://img.shields.io/pypi/djversions/django-view-breadcrumbs) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-view-breadcrumbs) 6 | [![Downloads](https://static.pepy.tech/badge/django-view-breadcrumbs)](https://pepy.tech/project/django-view-breadcrumbs) 7 | 8 | 9 | 10 | [![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-) 11 | 12 | 13 | 14 | ## Table of Contents 15 | 16 | * [Background](#background) 17 | * [Installation](#installation) 18 | * [Add `view_breadcrumbs` to your INSTALLED\_APPS](#add-view_breadcrumbs-to-your-installed_apps) 19 | * [Breadcrumb mixin classes provided.](#breadcrumb-mixin-classes-provided) 20 | * [Settings](#settings) 21 | * [Customization](#customization) 22 | * [BREADCRUMBS\_TEMPLATE](#breadcrumbs_template) 23 | * [Site wide](#site-wide) 24 | * [Overriding the breadcrumb template for a single view](#overriding-the-breadcrumb-template-for-a-single-view) 25 | * [BREADCRUMBS\_HOME\_LABEL](#breadcrumbs_home_label) 26 | * [Site wide](#site-wide-1) 27 | * [Overriding the Home label for a specific view](#overriding-the-home-label-for-a-specific-view) 28 | * [Translation support](#translation-support) 29 | * [Example](#example) 30 | * [Usage](#usage) 31 | * [View Configuration](#view-configuration) 32 | * [django-tables-2](#django-tables-2) 33 | * [URL Configuration](#url-configuration) 34 | * [Examples](#examples) 35 | * [Sample crumbs: `Posts`](#sample-crumbs-posts) 36 | * [Sample crumbs: `Home / Posts / Test - Post`](#sample-crumbs--home--posts--test---post) 37 | * [Custom crumbs: `Home / My Test Breadcrumb`](#custom-crumbs-home--my-test-breadcrumb) 38 | * [Using multiple apps](#using-multiple-apps) 39 | * [Running locally](#running-locally) 40 | * [Credits](#credits) 41 | * [Contributors ✨](#contributors-) 42 | 43 | ## Background 44 | 45 | This package provides a set of breadcrumb mixin classes that can be added to any django class based view and requires adding just `{% render_breadcrumbs %}` to the base template. 46 | 47 | breadcrumbs 48 | 49 | In the `base.html` template add the `render_breadcrumbs` tag and any template that inherits the base should have breadcrumbs included. 50 | 51 | **Example:** 52 | 53 | my_app 54 | |--templates 55 | |--base.html 56 | |--create.html 57 | 58 | `base.html` 59 | 60 | ```jinja2 61 | {% load view_breadcrumbs %} 62 | 63 | {% block breadcrumbs %} 64 | {% render_breadcrumbs %} {# Optionally provide a custom template e.g {% render_breadcrumbs "view_breadcrumbs/bootstrap5.html" %} #} 65 | {% endblock %} 66 | ``` 67 | 68 | And your `create.html`. 69 | 70 | ```jinja2 71 | {% extends "base.html" %} 72 | ``` 73 | 74 | ## Installation 75 | 76 | ```bash 77 | $ pip install django-view-breadcrumbs 78 | 79 | ``` 80 | 81 | ### Add `view_breadcrumbs` to your INSTALLED\_APPS 82 | 83 | ```python 84 | 85 | INSTALLED_APPS = [ 86 | ..., 87 | "view_breadcrumbs", 88 | ..., 89 | ] 90 | ``` 91 | 92 | ## Breadcrumb mixin classes provided. 93 | 94 | * `BaseBreadcrumbMixin` - Subclasses requires a `crumbs` class property. 95 | * `CreateBreadcrumbMixin` - For create views `Home / Posts / Add Post` 96 | * `DetailBreadcrumbMixin` - For detail views `Home / Posts / Post 1` 97 | * `ListBreadcrumbMixin` - For list views `Home / Posts` 98 | * `UpdateBreadcrumbMixin` - For Update views `Home / Posts / Post 1 / Update Post 1` 99 | * `DeleteBreadcrumbMixin` - For Delete views this has a link to the list view to be used as the success URL. 100 | 101 | ## Settings 102 | 103 | > NOTE :warning: 104 | > 105 | > * Make sure that `"django.template.context_processors.request"` is added to your TEMPLATE OPTIONS setting. 106 | 107 | ```python 108 | TEMPLATES = [ 109 | { 110 | "BACKEND": "django.template.backends.django.DjangoTemplates", 111 | "APP_DIRS": True, 112 | "OPTIONS": { 113 | "context_processors": [ 114 | "django.template.context_processors.debug", 115 | "django.template.context_processors.request", # <- This context processor is required 116 | "django.contrib.auth.context_processors.auth", 117 | "django.contrib.messages.context_processors.messages", 118 | ], 119 | }, 120 | }, 121 | ] 122 | ``` 123 | 124 | Modify the defaults using the following: 125 | 126 | | Name | Default | Description | Options | 127 | |----------------------------|---------------------------------------------|-------------|---------------------| 128 | | `BREADCRUMBS_TEMPLATE` | `"view_breadcrumbs/bootstrap5.html"` | Template used to render breadcrumbs. | [Predefined Templates](https://github.com/tj-django/django-view-breadcrumbs/tree/main/view_breadcrumbs/templates/view_breadcrumbs) | 129 | | `BREADCRUMBS_HOME_LABEL` | `Home` | Default label for the root path | | 130 | 131 | ### Customization 132 | 133 | #### BREADCRUMBS\_TEMPLATE 134 | 135 | ##### Site wide 136 | 137 | ```python 138 | BREADCRUMBS_TEMPLATE = "my_app/breadcrumbs.html" 139 | ``` 140 | 141 | ##### Overriding the breadcrumb template for a single view 142 | 143 | Update the `base.html` 144 | 145 | ```jinja2 146 | {% render_breadcrumbs "my_app/breadcrumbs.html" %} 147 | ``` 148 | 149 | #### BREADCRUMBS\_HOME\_LABEL 150 | 151 | ##### Site wide 152 | 153 | ```python 154 | BREADCRUMBS_HOME_LABEL = "My new home" 155 | ``` 156 | 157 | ##### Overriding the Home label for a specific view 158 | 159 | ```python 160 | from django.utils.translation import gettext_lazy as _ 161 | from view_breadcrumbs import DetailBreadcrumbMixin 162 | from django.views.generic import DetailView 163 | from demo.models import TestModel 164 | 165 | 166 | class TestDetailView(DetailBreadcrumbMixin, DetailView): 167 | model = TestModel 168 | home_label = _("My new home") 169 | template_name = "demo/test-detail.html" 170 | ``` 171 | 172 | *Renders* 173 | 174 | custom-root-breadcrumb 175 | 176 | ## [Translation support](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/) 177 | 178 | ### Example 179 | 180 | ![translated-crumbs](https://user-images.githubusercontent.com/17484350/128493830-7e50a6a9-3648-48cb-b198-4646ee2b03cf.png) 181 | 182 | ## Usage 183 | 184 | `django-view-breadcrumbs` includes generic mixins that can be added to a class based view. 185 | 186 | Using the generic breadcrumb mixin each breadcrumb will be added to the view dynamically 187 | and can be overridden by providing a `crumbs` property. 188 | 189 | ### View Configuration 190 | 191 | > NOTE: :warning: 192 | > 193 | > * Model based views should use a pattern `view_name=model_verbose_name_{action}` 194 | 195 | | Actions | View Class | View name | Sample Breadcrumb | Example | 196 | |-----------|-------------|-------------|-------------------|----------| 197 | | `list` | [`ListView`](https://docs.djangoproject.com/en/3.2/ref/class-based-views/generic-display/#listview) | `{model.verbose_name}_list` | `Home / Posts` | [Posts Example](https://github.com/tj-django/django-view-breadcrumbs#sample-crumbs-posts) | 198 | | `create` | [`CreateView`](https://docs.djangoproject.com/en/3.2/ref/class-based-views/generic-editing/#createview) | `{model.verbose_name}_create` | `Home / Posts / Add Post` | | 199 | | `detail` | [`DetailView`](https://docs.djangoproject.com/en/3.2/ref/class-based-views/generic-display/#detailview) | `{model.verbose_name}_detail` | `Home / Posts / Test - Post` | | 200 | | `change` | [`UpdateView`](https://docs.djangoproject.com/en/3.2/ref/class-based-views/generic-editing/#updateview) | `{model.verbose_name}_update` | `Home / Posts / Test - Post / Update Test - Post` | | 201 | | `delete` | [`DeleteView`](https://docs.djangoproject.com/en/3.2/ref/class-based-views/generic-editing/#deleteview) | `{model.verbose_name}_delete` | N/A | 202 | | N/A | [`TemplateView`](https://docs.djangoproject.com/en/3.2/ref/class-based-views/base/#templateview) | N/A | N/A | See: [Custom View](#custom-crumbs-home--my-test-breadcrumb) | 203 | | N/A | [`FormView`](https://docs.djangoproject.com/en/3.2/ref/class-based-views/generic-editing/#formview) | N/A | N/A | See: [Custom View](#custom-crumbs-home--my-test-breadcrumb) | 204 | | N/A | [`AboutView`](https://docs.djangoproject.com/en/3.2/topics/class-based-views/#subclassing-generic-views) | N/A | N/A | See: [Custom View](#custom-crumbs-home--my-test-breadcrumb) | 205 | | N/A | [`View`](https://docs.djangoproject.com/en/3.2/ref/class-based-views/base/#view) | N/A | N/A | See: [Custom View](#custom-crumbs-home--my-test-breadcrumb) | 206 | 207 | #### [django-tables-2](https://django-tables2.readthedocs.io/en/latest/index.html#) 208 | 209 | | Actions | View Class | View name | Sample Breadcrumb | Example | 210 | |-----------|-------------|-------------|-------------------|----------| 211 | | N/A | [`SingleTableMixin`](https://django-tables2.readthedocs.io/en/latest/pages/generic-mixins.html?highlight=SingleTableMixin#a-single-table-using-singletablemixin) | N/A | N/A | See: [demo table view](https://github.com/tj-django/django-view-breadcrumbs/blob/main/demo/views.py#L154-L162) | 212 | | N/A | [`MultiTableMixin`](https://django-tables2.readthedocs.io/en/latest/pages/generic-mixins.html?highlight=SingleTableMixin#multiple-tables-using-multitablemixin) | N/A | N/A | See: [demo table view](https://github.com/tj-django/django-view-breadcrumbs/blob/main/demo/views.py#L166-L173) | 213 | | N/A | [`SingleTableView`](https://django-tables2.readthedocs.io/en/latest/pages/api-reference.html?highlight=SingleTableView#singletableview) | N/A | N/A | Same implementation as `SingleTableMixin` | 214 | 215 | For more examples see: [demo app](https://github.com/tj-django/django-view-breadcrumbs/tree/main/demo) 216 | 217 | ### URL Configuration 218 | 219 | Based on the table of actions listed above there's a strict `view_name` requirement that needs to be adhered to in order for breadcrumbs to work. 220 | 221 | This can be manually entered in your `urls.py` or you can optionally use the following class properties instead of hardcoding the `view_name`. 222 | 223 | ```python 224 | ... 225 | path("tests/", TestListsView.as_view(), name=TestListsView.list_view_name), 226 | path( 227 | "tests//", 228 | TestDetailView.as_view(), 229 | name=TestDetailView.detail_view_name, 230 | ), 231 | path( 232 | "tests//update/", 233 | TestUpdateView.as_view(), 234 | name=TestUpdateView.update_view_name, 235 | ), 236 | path( 237 | "tests//delete/", 238 | TestDeleteView.as_view(), 239 | name=TestDeleteView.delete_view_name, 240 | ), 241 | ... 242 | ``` 243 | 244 | ### Examples 245 | 246 | #### Sample crumbs: `Posts` 247 | 248 | In your urls.py 249 | 250 | ```python 251 | urlpatterns = [ 252 | ... 253 | path("posts/", views.PostList.as_view(), name="post_list"), 254 | ... 255 | # OR 256 | ... 257 | path("posts/", views.PostList.as_view(), name=views.PostList.list_view_name), 258 | ... 259 | ] 260 | ``` 261 | 262 | > All crumbs use the home root path `/` as the base this can be excluded by specifying `add_home = False` 263 | 264 | ```python 265 | from django.views.generic import ListView 266 | from view_breadcrumbs import ListBreadcrumbMixin 267 | 268 | 269 | class PostList(ListBreadcrumbMixin, ListView): 270 | model = Post 271 | template_name = "app/post/list.html" 272 | add_home = False 273 | ``` 274 | 275 | #### Sample crumbs: `Home / Posts / Test - Post` 276 | 277 | In your `urls.py` 278 | 279 | ```python 280 | urlpatterns = [ 281 | ... 282 | path("posts//", views.PostDetail.as_view(), name="post_detail"), 283 | ... 284 | # OR 285 | ... 286 | path("posts//", views.PostDetail.as_view(), name=views.PostDetail.detail_view_name), 287 | ... 288 | ] 289 | 290 | ``` 291 | 292 | `views.py` 293 | 294 | ```python 295 | from django.views.generic import DetailView 296 | from view_breadcrumbs import DetailBreadcrumbMixin 297 | 298 | 299 | class PostDetail(DetailBreadcrumbMixin, DetailView): 300 | model = Post 301 | template_name = "app/post/detail.html" 302 | breadcrumb_use_pk = False 303 | ``` 304 | 305 | #### Custom crumbs: `Home / My Test Breadcrumb` 306 | 307 | URL configuration. 308 | 309 | ```python 310 | urlpatterns = [ 311 | path("my-custom-view/", views.CustomView.as_view(), name="custom_view"), 312 | ] 313 | ``` 314 | 315 | views.py 316 | 317 | ```python 318 | from django.urls import reverse 319 | from django.views.generic import View 320 | from view_breadcrumbs import BaseBreadcrumbMixin 321 | from demo.models import TestModel 322 | 323 | 324 | class CustomView(BaseBreadcrumbMixin, View): 325 | model = TestModel 326 | template_name = "app/test/custom.html" 327 | crumbs = [("My Test Breadcrumb", reverse("custom_view"))] # OR reverse_lazy 328 | ``` 329 | 330 | **OR** 331 | 332 | ```python 333 | from django.urls import reverse 334 | from django.views.generic import View 335 | from view_breadcrumbs import BaseBreadcrumbMixin 336 | from demo.models import TestModel 337 | from django.utils.functional import cached_property 338 | 339 | 340 | class CustomView(BaseBreadcrumbMixin, View): 341 | template_name = "app/test/custom.html" 342 | 343 | @cached_property 344 | def crumbs(self): 345 | return [("My Test Breadcrumb", reverse("custom_view"))] 346 | 347 | ``` 348 | 349 | > Refer to the [demo app](https://github.com/tj-django/django-view-breadcrumbs/tree/main/demo) for more examples. 350 | 351 | ### Using multiple apps 352 | 353 | To reference models from a different application you need to override the `app_name` class attribute. 354 | 355 | Example: 356 | Using a `Library` model that is imported from a `custom` application that you want to render in a `demo` app view. 357 | 358 | ```python 359 | INSTALLED_APPS = [ 360 | ... 361 | "demo", 362 | "custom", 363 | ... 364 | ] 365 | ``` 366 | 367 | `demo/views.py` 368 | 369 | ```python 370 | class LibraryDetailView(DetailBreadcrumbMixin, DetailView): 371 | model = Library 372 | app_name = "demo" 373 | ... 374 | ``` 375 | 376 | ## Running locally 377 | 378 | ```bash 379 | $ git clone git@github.com:tj-django/django-view-breadcrumbs.git 380 | $ make install-dev 381 | $ make migrate 382 | $ make run 383 | ``` 384 | 385 | Spins up a django server running the demo app. 386 | 387 | Visit `http://127.0.0.1:8090` 388 | 389 | ## Credits 390 | 391 | * [django-bootstrap-breadcrumbs](https://github.com/prymitive/bootstrap-breadcrumbs) 392 | 393 | To file a bug or submit a patch, please head over to [django-view-breadcrumbs on github](https://github.com/tj-django/django-view-breadcrumbs/issues). 394 | 395 | If you feel generous and want to show some extra appreciation: 396 | 397 | Support me with a :star: 398 | 399 | [![Buy me a coffee][buymeacoffee-shield]][buymeacoffee] 400 | 401 | [buymeacoffee]: https://www.buymeacoffee.com/jackton1 402 | 403 | [buymeacoffee-shield]: https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png 404 | 405 | ## Contributors ✨ 406 | 407 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 |

Derek

📖

David THENON

💻
421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 429 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | django-view-breadcrumbs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
Please wait...
14 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | 4 | if __name__ == "__main__": 5 | try: 6 | from django.core.management import execute_from_command_line 7 | 8 | from conftest import pytest_configure 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | pytest_configure(debug=True) 16 | execute_from_command_line(sys.argv) 17 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = view_breadcrumbs 3 | python_files = tests.py test_*.py *_tests.py 4 | python_classes = *TestCase TestCase* 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "enabled": true, 6 | "prHourlyLimit": 10, 7 | "prConcurrentLimit": 5, 8 | "rebaseWhen": "behind-base-branch", 9 | "addLabels": [ 10 | "dependencies" 11 | ], 12 | "assignees": [ 13 | "jackton1" 14 | ], 15 | "assignAutomerge": true, 16 | "dependencyDashboard": true, 17 | "dependencyDashboardAutoclose": true, 18 | "lockFileMaintenance": { 19 | "enabled": true, 20 | "automerge": true 21 | }, 22 | "packageRules": [ 23 | { 24 | "matchUpdateTypes": ["major", "minor", "patch", "pin", "digest"], 25 | "automerge": true, 26 | "rebaseWhen": "behind-base-branch", 27 | "addLabels": [ 28 | "automerge" 29 | ] 30 | }, 31 | { 32 | "description": "docker images", 33 | "matchLanguages": [ 34 | "docker" 35 | ], 36 | "matchUpdateTypes": ["major", "minor", "patch", "pin", "digest"], 37 | "rebaseWhen": "behind-base-branch", 38 | "addLabels": [ 39 | "automerge" 40 | ], 41 | "automerge": true 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile 6 | # 7 | asgiref==3.8.1 8 | # via django 9 | django==5.0.4 10 | # via django-view-breadcrumbs (setup.py) 11 | pytz==2024.1 12 | # via django 13 | sqlparse==0.5.0 14 | # via django 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages, setup 4 | 5 | install_requires = ["Django"] 6 | 7 | test_requires = [ 8 | "tox", 9 | "tox-gh-actions", 10 | "coverage", 11 | "pytest", 12 | "pluggy>=0.7", 13 | "mock", 14 | "codacy-coverage", 15 | ] 16 | 17 | doc_requires = [ 18 | "Sphinx", 19 | ] 20 | 21 | deploy_requires = [ 22 | "bump2version", 23 | "readme_renderer[md]", 24 | "git-changelog", 25 | "twine", 26 | ] 27 | 28 | local_dev_requires = [ 29 | "pip-tools", 30 | "django_tables2", 31 | "django_bootstrap5", 32 | "django-filter", 33 | ] 34 | 35 | extras_require = { 36 | "development": [ 37 | local_dev_requires, 38 | install_requires, 39 | test_requires, 40 | doc_requires, 41 | ], 42 | "docs": doc_requires, 43 | "test": test_requires, 44 | "deploy": deploy_requires, 45 | } 46 | 47 | BASE_DIR = os.path.dirname(__file__) 48 | README_PATH = os.path.join(BASE_DIR, "README.md") 49 | 50 | LONG_DESCRIPTION_TYPE = "text/markdown" 51 | if os.path.isfile(README_PATH): 52 | with open(README_PATH) as f: 53 | LONG_DESCRIPTION = f.read() 54 | else: 55 | LONG_DESCRIPTION = "" 56 | 57 | 58 | setup( 59 | name="django-view-breadcrumbs", 60 | python_requires=">=3.6", 61 | version="2.5.1", 62 | author="Tonye Jack", 63 | author_email="jtonye@ymail.com", 64 | long_description=LONG_DESCRIPTION, 65 | long_description_content_type=LONG_DESCRIPTION_TYPE, 66 | packages=find_packages(exclude=["demo", "demo.migrations.*"]), 67 | classifiers=[ 68 | "Development Status :: 5 - Production/Stable", 69 | "Intended Audience :: Developers", 70 | "Topic :: Internet :: WWW/HTTP", 71 | "License :: OSI Approved :: BSD License", 72 | "Operating System :: OS Independent", 73 | "Programming Language :: Python :: 3.6", 74 | "Programming Language :: Python :: 3.7", 75 | "Programming Language :: Python :: 3.8", 76 | "Programming Language :: Python :: 3.9", 77 | "Programming Language :: Python :: 3.10", 78 | "Framework :: Django :: 1.11", 79 | "Framework :: Django :: 2.0", 80 | "Framework :: Django :: 2.1", 81 | "Framework :: Django :: 2.2", 82 | "Framework :: Django :: 3.0", 83 | "Framework :: Django :: 3.1", 84 | "Framework :: Django :: 3.2", 85 | "Framework :: Django :: 4.0", 86 | "Framework :: Django :: 4.1", 87 | "Framework :: Django :: 4.2", 88 | ], 89 | keywords=[ 90 | "django breadcrumbs", 91 | "breadcrumbs", 92 | "django generic views breadcrumb", 93 | ], 94 | include_package_data=True, 95 | install_requires=install_requires, 96 | tests_require=test_requires, 97 | extras_require=extras_require, 98 | url="https://github.com/tj-django/django-view-breadcrumbs", 99 | description="Django generic view breadcrumbs", 100 | zip_safe=False, 101 | ) 102 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 2.9.1 3 | skipsdist = false 4 | envlist = 5 | yamllint 6 | flake8 7 | py35-django{11,20,21}-{linux,macos,windows} 8 | py36-django{11,20,21,22,30,31,32,main}-{linux,macos,windows} 9 | py37-django{11,20,21,22,30,31,32,main}-{linux,macos,windows} 10 | py38-django{11,20,21,22,30,31,32,40,41,main}-{linux,macos,windows} 11 | py39-django{11,20,21,22,30,31,32,40,41,42,main}-{linux,macos,windows} 12 | py310-django{11,20,21,22,30,31,32,40,41,42,main}-{linux,macos,windows} 13 | py311-django{11,20,21,22,30,31,32,40,41,42,main}-{linux} 14 | skip_missing_interpreters = true 15 | 16 | [gh-actions] 17 | python = 18 | 2.7: py27 19 | 3.6: py36 20 | 3.7: py37 21 | 3.8: py38 22 | 3.9: py39 23 | 3.10: py310 24 | 3.11: py311 25 | 26 | [gh-actions:env] 27 | PLATFORM = 28 | ubuntu-latest: linux 29 | macos-latest: macos 30 | windows-latest: windows 31 | 32 | [testenv] 33 | setenv = DJANGO_SETTINGS_MODULE=django_view_breadcrumbs.settings 34 | passenv = * 35 | extras = 36 | development 37 | test 38 | deps = 39 | django11: Django>=1.11.0,<2.0 40 | django20: Django>=2.0,<2.1 41 | django21: Django>=2.1,<2.2 42 | django22: Django>=2.2,<2.3 43 | django30: Django>=3.0,<3.1 44 | django31: Django>=3.1,<3.2 45 | django32: Django>=3.2,<3.3 46 | django40: Django>=4.0,<4.1 47 | django41: Django>=4.1,<4.2 48 | django42: Django>=4.2,<4.3 49 | main: https://github.com/django/django/archive/main.tar.gz 50 | usedevelop = true 51 | commands = 52 | coverage run -m pytest -v 53 | coverage report -m 54 | coverage xml 55 | - python-codacy-coverage -r coverage.xml 56 | 57 | [testenv:flake8] 58 | deps = flake8 59 | commands = 60 | flake8 . 61 | 62 | [testenv:yamllint] 63 | deps = yamllint 64 | changedir = {toxinidir} 65 | commands = 66 | yamllint --strict -f standard .github 67 | 68 | [check-manifest] 69 | ignore = 70 | demo 71 | demo/migrations 72 | 73 | [flake8] 74 | exclude = 75 | .tox, 76 | demo/migrations/*, 77 | .git/* 78 | max-line-length = 120 79 | -------------------------------------------------------------------------------- /view_breadcrumbs/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for view_breadcrumbs.""" 2 | 3 | __author__ = """Tonye Jack""" 4 | __email__ = "jtonye@ymail.com" 5 | __version__ = "2.0.0" 6 | 7 | from .generic import ( 8 | BaseBreadcrumbMixin, 9 | CreateBreadcrumbMixin, 10 | DeleteBreadcrumbMixin, 11 | DetailBreadcrumbMixin, 12 | ListBreadcrumbMixin, 13 | UpdateBreadcrumbMixin, 14 | ) 15 | 16 | __all__ = [ 17 | "BaseBreadcrumbMixin", 18 | "CreateBreadcrumbMixin", 19 | "DetailBreadcrumbMixin", 20 | "ListBreadcrumbMixin", 21 | "UpdateBreadcrumbMixin", 22 | "DeleteBreadcrumbMixin", 23 | ] 24 | -------------------------------------------------------------------------------- /view_breadcrumbs/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ViewBreadcrumbsConfig(AppConfig): 5 | name = "view_breadcrumbs" 6 | -------------------------------------------------------------------------------- /view_breadcrumbs/constants.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext_lazy as _ 2 | 3 | LIST_VIEW_SUFFIX = _("list") 4 | CREATE_VIEW_SUFFIX = _("create") 5 | UPDATE_VIEW_SUFFIX = _("update") 6 | DELETE_VIEW_SUFFIX = _("delete") 7 | DETAIL_VIEW_SUFFIX = _("detail") 8 | -------------------------------------------------------------------------------- /view_breadcrumbs/generic/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import BaseBreadcrumbMixin # noqa 2 | from .create import CreateBreadcrumbMixin # noqa 3 | from .delete import DeleteBreadcrumbMixin # noqa 4 | from .detail import DetailBreadcrumbMixin # noqa 5 | from .list import ListBreadcrumbMixin # noqa 6 | from .update import UpdateBreadcrumbMixin # noqa 7 | -------------------------------------------------------------------------------- /view_breadcrumbs/generic/base.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from view_breadcrumbs.constants import ( 7 | CREATE_VIEW_SUFFIX, 8 | DELETE_VIEW_SUFFIX, 9 | DETAIL_VIEW_SUFFIX, 10 | LIST_VIEW_SUFFIX, 11 | UPDATE_VIEW_SUFFIX, 12 | ) 13 | 14 | from ..templatetags.view_breadcrumbs import ( 15 | CONTEXT_KEY, 16 | append_breadcrumb, 17 | clear_breadcrumbs, 18 | ) 19 | from ..utils import get_verbose_name, get_verbose_name_plural 20 | 21 | log = logging.getLogger(__name__) 22 | 23 | 24 | def add_breadcrumb(context, label, view_name, **kwargs): 25 | return append_breadcrumb(context, label, view_name, (), kwargs) 26 | 27 | 28 | class BaseBreadcrumbMixin(object): 29 | add_home = True 30 | model = None 31 | home_path = "/" 32 | home_label = None 33 | 34 | @property 35 | def crumbs(self): 36 | raise NotImplementedError( 37 | _( 38 | "%(class_name)s should have a crumbs property." 39 | % {"class_name": type(self).__name__} 40 | ) 41 | ) 42 | 43 | def update_breadcrumbs(self, context): 44 | crumbs = self.crumbs 45 | if self.add_home: 46 | home_label = self.home_label or _( 47 | getattr(settings, "BREADCRUMBS_HOME_LABEL", _("Home")) 48 | ) 49 | crumbs = [(home_label, self.home_path)] + crumbs 50 | for crumb in crumbs: 51 | try: 52 | label, view_name = crumb 53 | except (TypeError, ValueError): 54 | raise ValueError( 55 | _("Breadcrumb requires a tuple of label and view name.") 56 | ) 57 | else: 58 | if hasattr(self, "object") and self.object: 59 | if callable(label): 60 | label = label(self.object) 61 | if callable(view_name): 62 | view_name = view_name(self.object) 63 | add_breadcrumb(context, label, view_name) 64 | 65 | def get_context_data(self, **kwargs): 66 | ctx = {"request": self.request} 67 | if CONTEXT_KEY in self.request.META: 68 | clear_breadcrumbs(ctx) 69 | self.update_breadcrumbs(ctx) 70 | 71 | return super(BaseBreadcrumbMixin, self).get_context_data(**kwargs) 72 | 73 | 74 | class BaseModelBreadcrumbMixin(BaseBreadcrumbMixin): 75 | breadcrumb_use_pk = True 76 | app_name = None 77 | 78 | list_view_suffix = LIST_VIEW_SUFFIX 79 | create_view_suffix = CREATE_VIEW_SUFFIX 80 | update_view_suffix = UPDATE_VIEW_SUFFIX 81 | delete_view_suffix = DELETE_VIEW_SUFFIX 82 | detail_view_suffix = DETAIL_VIEW_SUFFIX 83 | 84 | @property 85 | def model_name_title(self): 86 | return get_verbose_name(self.model).title() 87 | 88 | @property 89 | def model_name_title_plural(self): 90 | return get_verbose_name_plural(self.model).title() 91 | -------------------------------------------------------------------------------- /view_breadcrumbs/generic/create.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from ..utils import action_view_name, classproperty 5 | from .list import ListBreadcrumbMixin 6 | 7 | 8 | class CreateBreadcrumbMixin(ListBreadcrumbMixin): 9 | # Home / object List / Add object 10 | add_format_string = _("Add %(model)s") 11 | 12 | @classproperty 13 | def create_view_name(self): 14 | return action_view_name( 15 | model=self.model, 16 | action=self.create_view_suffix, 17 | app_name=self.app_name, 18 | full=False, 19 | ) 20 | 21 | @property 22 | def __create_view_name(self): 23 | return action_view_name( 24 | model=self.model, action=self.create_view_suffix, app_name=self.app_name 25 | ) 26 | 27 | @property 28 | def create_view_url(self): 29 | return reverse(self.__create_view_name) 30 | 31 | @property 32 | def crumbs(self): 33 | return super(CreateBreadcrumbMixin, self).crumbs + [ 34 | ( 35 | _(self.add_format_string % {"model": self.model_name_title}), 36 | self.create_view_url, 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /view_breadcrumbs/generic/delete.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | 3 | from ..utils import action_view_name, classproperty 4 | from .list import ListBreadcrumbMixin 5 | 6 | 7 | class DeleteBreadcrumbMixin(ListBreadcrumbMixin): 8 | @classproperty 9 | def delete_view_name(self): 10 | return action_view_name( 11 | model=self.model, 12 | action=self.delete_view_suffix, 13 | app_name=self.app_name, 14 | full=False, 15 | ) 16 | 17 | @property 18 | def __delete_view_name(self): 19 | return action_view_name( 20 | model=self.model, action=self.detail_view_suffix, app_name=self.app_name 21 | ) 22 | 23 | def delete_view_url(self, instance): 24 | if self.breadcrumb_use_pk: 25 | return reverse( 26 | self.__delete_view_name, kwargs={self.pk_url_kwarg: instance.pk} 27 | ) 28 | 29 | return reverse( 30 | self.__delete_view_name, 31 | kwargs={self.slug_url_kwarg: getattr(instance, self.slug_field)}, 32 | ) 33 | -------------------------------------------------------------------------------- /view_breadcrumbs/generic/detail.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from django.urls import reverse 4 | from django.utils.encoding import force_str 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | from ..utils import action_view_name, classproperty 8 | from .list import ListBreadcrumbMixin 9 | 10 | 11 | def _detail_view_label(instance, format_string): 12 | return _(force_str(format_string) % {"instance": force_str(instance)}) 13 | 14 | 15 | class DetailBreadcrumbMixin(ListBreadcrumbMixin): 16 | # Home / object List / str(object) 17 | detail_format_string = _("%(instance)s") 18 | 19 | @classproperty 20 | def detail_view_name(self): 21 | return action_view_name( 22 | model=self.model, 23 | action=self.detail_view_suffix, 24 | app_name=self.app_name, 25 | full=False, 26 | ) 27 | 28 | @property 29 | def __detail_view_name(self): 30 | return action_view_name( 31 | model=self.model, action=self.detail_view_suffix, app_name=self.app_name 32 | ) 33 | 34 | def detail_view_url(self, instance): 35 | if self.breadcrumb_use_pk: 36 | return reverse( 37 | self.__detail_view_name, kwargs={self.pk_url_kwarg: instance.pk} 38 | ) 39 | 40 | return reverse( 41 | self.__detail_view_name, 42 | kwargs={self.slug_url_kwarg: getattr(instance, self.slug_field)}, 43 | ) 44 | 45 | @property 46 | def crumbs(self): 47 | return super(DetailBreadcrumbMixin, self).crumbs + [ 48 | ( 49 | partial(_detail_view_label, format_string=self.detail_format_string), 50 | self.detail_view_url, 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /view_breadcrumbs/generic/list.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | 3 | from ..utils import action_view_name, classproperty 4 | from .base import BaseModelBreadcrumbMixin 5 | 6 | 7 | class ListBreadcrumbMixin(BaseModelBreadcrumbMixin): 8 | # Home / object List 9 | 10 | @classproperty 11 | def list_view_name(self): 12 | return action_view_name( 13 | model=self.model, 14 | action=self.list_view_suffix, 15 | app_name=self.app_name, 16 | full=False, 17 | ) 18 | 19 | @property 20 | def __list_view_name(self): 21 | return action_view_name( 22 | model=self.model, action=self.list_view_suffix, app_name=self.app_name 23 | ) 24 | 25 | @property 26 | def list_view_url(self): 27 | return reverse(self.__list_view_name) 28 | 29 | @property 30 | def crumbs(self): 31 | return [(self.model_name_title_plural, self.list_view_url)] 32 | -------------------------------------------------------------------------------- /view_breadcrumbs/generic/update.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from django.urls import reverse 4 | from django.utils.encoding import force_str 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | from ..utils import action_view_name, classproperty 8 | from .detail import DetailBreadcrumbMixin 9 | 10 | 11 | def _update_view_label(instance, format_string): 12 | return _(force_str(format_string) % {"instance": force_str(instance)}) 13 | 14 | 15 | class UpdateBreadcrumbMixin(DetailBreadcrumbMixin): 16 | # Home / object List / object / Update object 17 | update_format_str = _("Update: %(instance)s") 18 | 19 | @classproperty 20 | def update_view_name(self): 21 | return action_view_name( 22 | model=self.model, 23 | action=self.update_view_suffix, 24 | app_name=self.app_name, 25 | full=False, 26 | ) 27 | 28 | @property 29 | def __update_view_name(self): 30 | return action_view_name( 31 | model=self.model, action=self.update_view_suffix, app_name=self.app_name 32 | ) 33 | 34 | def update_view_url(self, instance): 35 | if self.breadcrumb_use_pk: 36 | return reverse( 37 | self.__update_view_name, kwargs={self.pk_url_kwarg: instance.pk} 38 | ) 39 | 40 | return reverse( 41 | self.__update_view_name, 42 | kwargs={self.slug_url_kwarg: getattr(instance, self.slug_field)}, 43 | ) 44 | 45 | @property 46 | def crumbs(self): 47 | return super(UpdateBreadcrumbMixin, self).crumbs + [ 48 | ( 49 | partial(_update_view_label, format_string=self.update_format_str), 50 | self.update_view_url, 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /view_breadcrumbs/locale/en_US/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # django-view-breadcrumbs 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # Tonye Jack jtonye@ymail.com, YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: 2.5.1\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-01-15 16:51-0500\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Tonye Jack jtonye@ymail.com\n" 14 | "Language: English US\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: custom/models.py:11 20 | msgid "library" 21 | msgstr "" 22 | 23 | #: custom/models.py:12 24 | msgid "libraries" 25 | msgstr "" 26 | 27 | #: demo/models.py:13 28 | msgid "test model" 29 | msgstr "" 30 | 31 | #: demo/models.py:14 32 | msgid "test models" 33 | msgstr "" 34 | 35 | #: demo/templates/demo/index.html:24 36 | msgid "Custom view" 37 | msgstr "" 38 | 39 | #: demo/templates/demo/index.html:25 40 | msgid "CRUD views" 41 | msgstr "" 42 | 43 | #: demo/templates/demo/index.html:26 44 | msgid "Libraries" 45 | msgstr "" 46 | 47 | #: demo/templates/demo/index.html:27 48 | msgid "Table views" 49 | msgstr "" 50 | 51 | #: demo/templates/demo/index.html:28 52 | msgid "Multi Table views" 53 | msgstr "" 54 | 55 | #: demo/templates/demo/test-create.html:7 56 | msgid "Submit" 57 | msgstr "" 58 | 59 | #: demo/templates/demo/test-detail.html:4 60 | msgid "Name" 61 | msgstr "" 62 | 63 | #: demo/templates/demo/test-detail.html:6 64 | msgid "Created at" 65 | msgstr "" 66 | 67 | #: demo/templates/demo/test-detail.html:9 68 | msgid "Updated at" 69 | msgstr "" 70 | 71 | #: demo/templates/demo/test-detail.html:11 72 | msgid "Edit" 73 | msgstr "" 74 | 75 | #: demo/templates/demo/test-detail.html:14 76 | msgid "Delete" 77 | msgstr "" 78 | 79 | #: demo/templates/demo/test-list.html:5 80 | msgid "Create" 81 | msgstr "" 82 | 83 | #: demo/views.py:39 84 | msgid "My Test Breadcrumb" 85 | msgstr "" 86 | 87 | #: demo/views.py:44 88 | msgid "List tests" 89 | msgstr "" 90 | 91 | #: demo/views.py:79 demo/views.py:121 92 | msgid "My new home" 93 | msgstr "" 94 | 95 | #: view_breadcrumbs/constants.py:3 96 | msgid "list" 97 | msgstr "" 98 | 99 | #: view_breadcrumbs/constants.py:4 100 | msgid "create" 101 | msgstr "" 102 | 103 | #: view_breadcrumbs/constants.py:5 104 | msgid "update" 105 | msgstr "" 106 | 107 | #: view_breadcrumbs/constants.py:6 108 | msgid "delete" 109 | msgstr "" 110 | 111 | #: view_breadcrumbs/constants.py:7 112 | msgid "detail" 113 | msgstr "" 114 | 115 | #: view_breadcrumbs/generic/base.py:38 116 | #, python-format 117 | msgid "%(class_name)s should have a crumbs property." 118 | msgstr "" 119 | 120 | #: view_breadcrumbs/generic/base.py:47 121 | msgid "Home" 122 | msgstr "" 123 | 124 | #: view_breadcrumbs/generic/base.py:55 125 | msgid "Breadcrumb requires a tuple of label and view name." 126 | msgstr "" 127 | 128 | #: view_breadcrumbs/generic/create.py:10 129 | #, python-format 130 | msgid "Add %(model)s" 131 | msgstr "" 132 | 133 | #: view_breadcrumbs/generic/create.py:35 134 | msgid "model" 135 | msgstr "" 136 | 137 | #: view_breadcrumbs/generic/detail.py:12 view_breadcrumbs/generic/update.py:12 138 | msgid "instance" 139 | msgstr "" 140 | 141 | #: view_breadcrumbs/generic/detail.py:17 142 | #, python-format 143 | msgid "%(instance)s" 144 | msgstr "" 145 | 146 | #: view_breadcrumbs/generic/update.py:17 147 | #, python-format 148 | msgid "Update: %(instance)s" 149 | msgstr "" 150 | 151 | #: view_breadcrumbs/utils.py:45 152 | #, python-format 153 | msgid "%(model)s is not installed or missing from the app registry." 154 | msgstr "" 155 | -------------------------------------------------------------------------------- /view_breadcrumbs/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # django-view-breadcrumbs 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # Tonye Jack jtonye@ymail.com, YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: 2.5.1\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-01-15 16:51-0500\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Tonye Jack jtonye@ymail.com\n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: French\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 | 21 | #: custom/models.py:11 22 | msgid "library" 23 | msgstr "" 24 | 25 | #: custom/models.py:12 26 | msgid "libraries" 27 | msgstr "" 28 | 29 | #: demo/models.py:13 30 | msgid "test model" 31 | msgstr "modèle de test" 32 | 33 | #: demo/models.py:14 34 | msgid "test models" 35 | msgstr "modèles de test" 36 | 37 | #: demo/templates/demo/index.html:24 38 | msgid "Custom view" 39 | msgstr "Vue personnalisée" 40 | 41 | #: demo/templates/demo/index.html:25 42 | msgid "CRUD views" 43 | msgstr "Vues CRUD" 44 | 45 | #: demo/templates/demo/index.html:26 46 | msgid "Libraries" 47 | msgstr "" 48 | 49 | #: demo/templates/demo/index.html:27 50 | msgid "Table views" 51 | msgstr "Vues de table" 52 | 53 | #: demo/templates/demo/index.html:28 54 | msgid "Multi Table views" 55 | msgstr "Vues multi-tables" 56 | 57 | #: demo/templates/demo/test-create.html:7 58 | msgid "Submit" 59 | msgstr "Nous faire parvenir" 60 | 61 | #: demo/templates/demo/test-detail.html:4 62 | msgid "Name" 63 | msgstr "Nom" 64 | 65 | #: demo/templates/demo/test-detail.html:6 66 | msgid "Created at" 67 | msgstr "Créé à" 68 | 69 | #: demo/templates/demo/test-detail.html:9 70 | msgid "Updated at" 71 | msgstr "Mis à jour à" 72 | 73 | #: demo/templates/demo/test-detail.html:11 74 | msgid "Edit" 75 | msgstr "Éditer" 76 | 77 | #: demo/templates/demo/test-detail.html:14 78 | msgid "Delete" 79 | msgstr "Effacer" 80 | 81 | #: demo/templates/demo/test-list.html:5 82 | msgid "Create" 83 | msgstr "Créer" 84 | 85 | #: demo/views.py:39 86 | msgid "My Test Breadcrumb" 87 | msgstr "Mon fil d'Ariane de test" 88 | 89 | #: demo/views.py:44 90 | msgid "List tests" 91 | msgstr "Liste des tests" 92 | 93 | #: demo/views.py:79 demo/views.py:121 94 | msgid "My new home" 95 | msgstr "Ma nouvelle maison" 96 | 97 | #: view_breadcrumbs/constants.py:3 98 | msgid "list" 99 | msgstr "liste" 100 | 101 | #: view_breadcrumbs/constants.py:4 102 | msgid "create" 103 | msgstr "créer" 104 | 105 | #: view_breadcrumbs/constants.py:5 106 | msgid "update" 107 | msgstr "mettre à jour" 108 | 109 | #: view_breadcrumbs/constants.py:6 110 | msgid "delete" 111 | msgstr "effacer" 112 | 113 | #: view_breadcrumbs/constants.py:7 114 | msgid "detail" 115 | msgstr "détail" 116 | 117 | #: view_breadcrumbs/generic/base.py:38 118 | #, python-format 119 | msgid "%(class_name)s should have a crumbs property." 120 | msgstr "%(class_name)s doit avoir une propriété crumbs." 121 | 122 | #: view_breadcrumbs/generic/base.py:47 123 | msgid "Home" 124 | msgstr "Domicile" 125 | 126 | #: view_breadcrumbs/generic/base.py:55 127 | msgid "Breadcrumb requires a tuple of label and view name." 128 | msgstr "Le fil d'Ariane nécessite un tuple d'étiquette et de nom de vue." 129 | 130 | #: view_breadcrumbs/generic/create.py:10 131 | #, python-format 132 | msgid "Add %(model)s" 133 | msgstr "Ajouter %(model)s" 134 | 135 | #: view_breadcrumbs/generic/create.py:35 136 | msgid "model" 137 | msgstr "" 138 | 139 | #: view_breadcrumbs/generic/detail.py:12 view_breadcrumbs/generic/update.py:12 140 | msgid "instance" 141 | msgstr "" 142 | 143 | #: view_breadcrumbs/generic/detail.py:17 144 | #, python-format 145 | msgid "%(instance)s" 146 | msgstr "" 147 | 148 | #: view_breadcrumbs/generic/update.py:17 149 | #, python-format 150 | msgid "Update: %(instance)s" 151 | msgstr "Mise à jour: %(instance)s" 152 | 153 | #: view_breadcrumbs/utils.py:45 154 | #, python-format 155 | msgid "%(model)s is not installed or missing from the app registry." 156 | msgstr "%(model)s n'est pas installé ou absent du registre de l'application." 157 | -------------------------------------------------------------------------------- /view_breadcrumbs/templates/view_breadcrumbs/bootstrap2.html: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /view_breadcrumbs/templates/view_breadcrumbs/bootstrap3.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /view_breadcrumbs/templates/view_breadcrumbs/bootstrap4.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /view_breadcrumbs/templates/view_breadcrumbs/bootstrap5.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /view_breadcrumbs/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tj-django/django-view-breadcrumbs/de91af7c1eb6a9be69f85fe38e146d92bcc29bf0/view_breadcrumbs/templatetags/__init__.py -------------------------------------------------------------------------------- /view_breadcrumbs/templatetags/view_breadcrumbs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | :copyright: Copyright 2013 by Łukasz Mierzwa 4 | :contact: l.mierzwa@gmail.com 5 | """ 6 | 7 | 8 | from __future__ import unicode_literals 9 | 10 | import logging 11 | from functools import wraps 12 | from inspect import ismethod 13 | 14 | from django import VERSION, template 15 | from django.conf import settings 16 | from django.db.models import Model 17 | from django.template.loader import render_to_string 18 | from django.utils.encoding import smart_str 19 | 20 | from view_breadcrumbs.constants import ( 21 | CREATE_VIEW_SUFFIX, 22 | DELETE_VIEW_SUFFIX, 23 | DETAIL_VIEW_SUFFIX, 24 | LIST_VIEW_SUFFIX, 25 | UPDATE_VIEW_SUFFIX, 26 | ) 27 | from view_breadcrumbs.utils import action_view_name 28 | 29 | if VERSION >= (2, 0): 30 | from django.urls import NoReverseMatch, Resolver404, resolve, reverse 31 | else: 32 | from django.core.urlresolvers import NoReverseMatch, Resolver404, resolve, reverse 33 | 34 | logger = logging.getLogger(__name__) 35 | 36 | register = template.Library() 37 | 38 | 39 | CONTEXT_KEY = "DJANGO_VIEW_BREADCRUMB_LINKS" 40 | 41 | 42 | def log_request_not_found(): 43 | if VERSION < (1, 8): # pragma: nocover 44 | logger.error( 45 | "request object not found in context! Check if " 46 | "'django.core.context_processors.request' is in " 47 | "TEMPLATE_CONTEXT_PROCESSORS" 48 | ) 49 | else: # pragma: nocover 50 | logger.error( 51 | "request object not found in context! Check if " 52 | "'django.template.context_processors.request' is in the " 53 | "'context_processors' option of your template settings." 54 | ) 55 | 56 | 57 | def requires_request(func): 58 | @wraps(func) 59 | def wrapped(context, *args, **kwargs): 60 | if "request" in context: 61 | return func(context, *args, **kwargs) 62 | 63 | log_request_not_found() 64 | return "" 65 | 66 | return wrapped 67 | 68 | 69 | @requires_request 70 | def append_breadcrumb(context, label, viewname, args, kwargs): 71 | context["request"].META[CONTEXT_KEY] = context["request"].META.get( 72 | CONTEXT_KEY, [] 73 | ) + [(label, viewname, args, kwargs)] 74 | 75 | 76 | @register.simple_tag(takes_context=True) 77 | @requires_request 78 | def render_breadcrumbs(context, *args): 79 | """ 80 | Render breadcrumbs html using bootstrap css classes. 81 | """ 82 | 83 | try: 84 | template_path = args[0] 85 | except IndexError: 86 | template_path = getattr( 87 | settings, "BREADCRUMBS_TEMPLATE", "view_breadcrumbs/bootstrap5.html" 88 | ) 89 | 90 | links = [] 91 | for label, viewname, view_args, view_kwargs in context["request"].META.get( 92 | CONTEXT_KEY, [] 93 | ): 94 | if ( 95 | isinstance(viewname, Model) 96 | and hasattr(viewname, "get_absolute_url") 97 | and ismethod(viewname.get_absolute_url) 98 | ): 99 | url = viewname.get_absolute_url(*view_args, **view_kwargs) 100 | else: 101 | try: 102 | try: 103 | # 'resolver_match' introduced in Django 1.5 104 | current_app = context["request"].resolver_match.namespace 105 | except AttributeError: 106 | try: 107 | resolver_match = resolve(context["request"].path) 108 | current_app = resolver_match.namespace 109 | except Resolver404: 110 | current_app = None 111 | url = reverse( 112 | viewname=viewname, 113 | args=view_args, 114 | kwargs=view_kwargs, 115 | current_app=current_app, 116 | ) 117 | except NoReverseMatch: 118 | url = viewname 119 | links.append((url, smart_str(label) if label else label)) 120 | 121 | if not links: 122 | return "" 123 | 124 | if VERSION > (1, 8): # pragma: nocover 125 | # RequestContext is deprecated in recent django 126 | # https://docs.djangoproject.com/en/1.10/ref/templates/upgrading/ 127 | context = context.flatten() 128 | 129 | context["breadcrumbs"] = links 130 | context["breadcrumbs_total"] = len(links) 131 | 132 | return render_to_string(template_path, context) 133 | 134 | 135 | @register.simple_tag(takes_context=True) 136 | @requires_request 137 | def clear_breadcrumbs(context, *args): 138 | """ 139 | Removes all currently added breadcrumbs. 140 | """ 141 | 142 | context["request"].META.pop(CONTEXT_KEY, None) 143 | return "" 144 | 145 | 146 | def _get_model(model): 147 | if model is None: 148 | raise ValueError("Invalid model") 149 | 150 | if isinstance(model, str): 151 | from django.apps import apps 152 | 153 | model = apps.get_model(model) 154 | 155 | return model 156 | 157 | 158 | def _view_url(model, suffix, app_name=None): 159 | view_name = action_view_name( 160 | model=_get_model(model), app_name=app_name, action=suffix 161 | ) 162 | return reverse(view_name) 163 | 164 | 165 | @register.simple_tag() 166 | def list_view_url(model, app_name=None, suffix=LIST_VIEW_SUFFIX): 167 | return _view_url(model=model, app_name=app_name, suffix=suffix) 168 | 169 | 170 | @register.simple_tag() 171 | def create_view_url(model, app_name=None, suffix=CREATE_VIEW_SUFFIX): 172 | return _view_url(model=model, app_name=app_name, suffix=suffix) 173 | 174 | 175 | def _object_url( 176 | instance, 177 | suffix, 178 | use_pk=True, 179 | pk_url_kwarg="pk", 180 | slug_url_kwarg="slug", 181 | app_name=None, 182 | slug_field="slug", 183 | ): 184 | model = instance.__class__ 185 | view_name = action_view_name(model=model, action=suffix, app_name=app_name) 186 | 187 | if use_pk: 188 | return reverse(view_name, kwargs={pk_url_kwarg: instance.pk}) 189 | 190 | return reverse( 191 | view_name, 192 | kwargs={slug_url_kwarg: getattr(instance, slug_field)}, 193 | ) 194 | 195 | 196 | @register.simple_tag(takes_context=True) 197 | def update_view_url( 198 | context, 199 | use_pk=True, 200 | pk_url_kwarg="pk", 201 | slug_url_kwarg="slug", 202 | slug_field="slug", 203 | app_name=None, 204 | suffix=UPDATE_VIEW_SUFFIX, 205 | ): 206 | return _object_url( 207 | instance=context["object"], 208 | use_pk=use_pk, 209 | pk_url_kwarg=pk_url_kwarg, 210 | slug_url_kwarg=slug_url_kwarg, 211 | slug_field=slug_field, 212 | app_name=app_name or getattr(context["view"], "app_name", None), 213 | suffix=suffix, 214 | ) 215 | 216 | 217 | @register.simple_tag() 218 | def update_instance_view_url( 219 | instance, 220 | use_pk=True, 221 | pk_url_kwarg="pk", 222 | slug_url_kwarg="slug", 223 | slug_field="slug", 224 | app_name=None, 225 | suffix=UPDATE_VIEW_SUFFIX, 226 | ): 227 | return _object_url( 228 | instance=instance, 229 | use_pk=use_pk, 230 | pk_url_kwarg=pk_url_kwarg, 231 | slug_url_kwarg=slug_url_kwarg, 232 | slug_field=slug_field, 233 | app_name=app_name, 234 | suffix=suffix, 235 | ) 236 | 237 | 238 | @register.simple_tag(takes_context=True) 239 | def delete_view_url( 240 | context, 241 | use_pk=True, 242 | pk_url_kwarg="pk", 243 | slug_url_kwarg="slug", 244 | slug_field="slug", 245 | suffix=DELETE_VIEW_SUFFIX, 246 | app_name=None, 247 | ): 248 | return _object_url( 249 | instance=context["object"], 250 | use_pk=use_pk, 251 | pk_url_kwarg=pk_url_kwarg, 252 | slug_url_kwarg=slug_url_kwarg, 253 | slug_field=slug_field, 254 | app_name=app_name or getattr(context["view"], "app_name", None), 255 | suffix=suffix, 256 | ) 257 | 258 | 259 | @register.simple_tag() 260 | def delete_instance_view_url( 261 | instance, 262 | use_pk=True, 263 | pk_url_kwarg="pk", 264 | slug_url_kwarg="slug", 265 | slug_field="slug", 266 | suffix=DELETE_VIEW_SUFFIX, 267 | app_name=None, 268 | ): 269 | return _object_url( 270 | instance=instance, 271 | use_pk=use_pk, 272 | pk_url_kwarg=pk_url_kwarg, 273 | slug_url_kwarg=slug_url_kwarg, 274 | slug_field=slug_field, 275 | app_name=app_name, 276 | suffix=suffix, 277 | ) 278 | 279 | 280 | @register.simple_tag(takes_context=True) 281 | def detail_view_url( 282 | context, 283 | use_pk=True, 284 | pk_url_kwarg="pk", 285 | slug_url_kwarg="slug", 286 | slug_field="slug", 287 | suffix=DETAIL_VIEW_SUFFIX, 288 | app_name=None, 289 | ): 290 | return _object_url( 291 | instance=context["object"], 292 | use_pk=use_pk, 293 | pk_url_kwarg=pk_url_kwarg, 294 | slug_url_kwarg=slug_url_kwarg, 295 | slug_field=slug_field, 296 | suffix=suffix, 297 | app_name=app_name or getattr(context["view"], "app_name", None), 298 | ) 299 | 300 | 301 | @register.simple_tag() 302 | def detail_instance_view_url( 303 | instance, 304 | use_pk=True, 305 | pk_url_kwarg="pk", 306 | slug_url_kwarg="slug", 307 | slug_field="slug", 308 | suffix=DETAIL_VIEW_SUFFIX, 309 | app_name=None, 310 | ): 311 | return _object_url( 312 | instance=instance, 313 | use_pk=use_pk, 314 | pk_url_kwarg=pk_url_kwarg, 315 | slug_url_kwarg=slug_url_kwarg, 316 | slug_field=slug_field, 317 | suffix=suffix, 318 | app_name=app_name, 319 | ) 320 | -------------------------------------------------------------------------------- /view_breadcrumbs/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tj-django/django-view-breadcrumbs/de91af7c1eb6a9be69f85fe38e146d92bcc29bf0/view_breadcrumbs/tests/__init__.py -------------------------------------------------------------------------------- /view_breadcrumbs/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tj-django/django-view-breadcrumbs/de91af7c1eb6a9be69f85fe38e146d92bcc29bf0/view_breadcrumbs/tests/unit/__init__.py -------------------------------------------------------------------------------- /view_breadcrumbs/tests/unit/test_breadcrumbs.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.test import RequestFactory, TestCase, override_settings 3 | from django.utils.encoding import force_str 4 | from django.views.generic.base import View 5 | from django.views.generic.detail import SingleObjectMixin 6 | from django.views.generic.list import MultipleObjectMixin 7 | 8 | from demo.models import TestModel 9 | from demo.views import TestView 10 | from view_breadcrumbs import DeleteBreadcrumbMixin 11 | from view_breadcrumbs.generic import ( 12 | BaseBreadcrumbMixin, 13 | CreateBreadcrumbMixin, 14 | DetailBreadcrumbMixin, 15 | ListBreadcrumbMixin, 16 | UpdateBreadcrumbMixin, 17 | ) 18 | from view_breadcrumbs.templatetags.view_breadcrumbs import CONTEXT_KEY 19 | 20 | 21 | class BaseBreadcrumbTestCase(TestCase): 22 | breadcrumb_mixin_cls = BaseBreadcrumbMixin 23 | view_attrs = {} 24 | 25 | @classmethod 26 | def make_crumb_cls(cls, class_name, bases, attrs): 27 | attrs["request"] = RequestFactory().request() 28 | return type(class_name, bases, attrs) 29 | 30 | def test_no_crumbs_property_raise_exception(self): 31 | TestViewClass = self.make_crumb_cls( 32 | "CustomView", 33 | (self.breadcrumb_mixin_cls, View), 34 | {**self.view_attrs, "crumbs": BaseBreadcrumbMixin.crumbs}, 35 | ) 36 | 37 | with self.assertRaises(NotImplementedError) as exc: 38 | crumbs = TestViewClass().crumbs 39 | self.assertIsNone(crumbs) 40 | 41 | self.assertEqual( 42 | str(exc.exception), 43 | "{} should have a crumbs property.".format(TestViewClass.__name__), 44 | ) 45 | 46 | def test_custom_crumbs_property_is_valid(self): 47 | expected_crumbs = [("My Test Breadcrumb", "/")] 48 | 49 | TestViewClass = self.make_crumb_cls( 50 | "CustomView", 51 | (self.breadcrumb_mixin_cls, View), 52 | {"crumbs": expected_crumbs}, 53 | ) 54 | crumbs = TestViewClass().crumbs 55 | 56 | self.assertEqual(crumbs, expected_crumbs) 57 | 58 | def test_view_crumbs_is_valid(self): 59 | expected_crumbs = [("My Test Breadcrumb", "test_view")] 60 | crumbs = TestView().crumbs 61 | 62 | self.assertEqual(crumbs, expected_crumbs) 63 | 64 | 65 | class ActionTestMixin(object): 66 | object_mixin = None 67 | view_name = None 68 | 69 | def _get_view(self): 70 | # TODO: Move this to use the default django client. 71 | instance = TestModel.objects.create(name="Test") 72 | 73 | TestViewClass = self.make_crumb_cls( 74 | "CustomView", 75 | (self.breadcrumb_mixin_cls, self.object_mixin, View), 76 | self.view_attrs, 77 | ) 78 | view = TestViewClass() 79 | if isinstance(view, MultipleObjectMixin): 80 | view.object_list = view.get_queryset() 81 | else: 82 | view.kwargs = {"pk": instance.pk} 83 | view.object = view.get_object() 84 | 85 | return view 86 | 87 | @override_settings(BREADCRUMBS_HOME_LABEL="Custom Home") 88 | def test_custom_home_label(self): 89 | view = self._get_view() 90 | view.get_context_data() 91 | 92 | labels = [force_str(paths[0]) for paths in view.request.META[CONTEXT_KEY]] 93 | 94 | self.assertEqual(settings.BREADCRUMBS_HOME_LABEL, "Custom Home") 95 | self.assertIn("Custom Home", labels) 96 | 97 | def test_valid_view_name(self): 98 | view = self._get_view() 99 | 100 | self.assertIsNotNone(getattr(view, "{}_view_name".format(self.view_name))) 101 | 102 | def test_valid_view_url(self): 103 | view = self._get_view() 104 | view_url = getattr(view, "{}_view_url".format(self.view_name)) 105 | 106 | if isinstance(view_url, str): 107 | self.assertIsNotNone(view_url) 108 | else: 109 | self.assertIsNotNone(view_url(view.object)) 110 | 111 | 112 | class ListViewBreadcrumbTestCase(ActionTestMixin, BaseBreadcrumbTestCase): 113 | breadcrumb_mixin_cls = ListBreadcrumbMixin 114 | view_attrs = {"model": TestModel} 115 | object_mixin = MultipleObjectMixin 116 | view_name = "list" 117 | 118 | 119 | class DetailViewBreadcrumbTestCase(ActionTestMixin, BaseBreadcrumbTestCase): 120 | breadcrumb_mixin_cls = DetailBreadcrumbMixin 121 | view_attrs = {"model": TestModel} 122 | object_mixin = SingleObjectMixin 123 | view_name = "detail" 124 | 125 | 126 | class CreateBreadcrumbMixinTestCase(ActionTestMixin, BaseBreadcrumbTestCase): 127 | breadcrumb_mixin_cls = CreateBreadcrumbMixin 128 | view_attrs = {"model": TestModel} 129 | object_mixin = SingleObjectMixin 130 | view_name = "create" 131 | 132 | 133 | class UpdateBreadcrumbMixinTestCase(ActionTestMixin, BaseBreadcrumbTestCase): 134 | breadcrumb_mixin_cls = UpdateBreadcrumbMixin 135 | view_attrs = {"model": TestModel} 136 | object_mixin = SingleObjectMixin 137 | view_name = "update" 138 | 139 | 140 | class DeleteBreadcrumbMixinTestCase(ActionTestMixin, BaseBreadcrumbTestCase): 141 | breadcrumb_mixin_cls = DeleteBreadcrumbMixin 142 | view_attrs = {"model": TestModel} 143 | object_mixin = SingleObjectMixin 144 | view_name = "delete" 145 | -------------------------------------------------------------------------------- /view_breadcrumbs/utils.py: -------------------------------------------------------------------------------- 1 | from django.utils.encoding import force_str 2 | from django.utils.translation import override 3 | 4 | 5 | class classproperty: 6 | """ 7 | Decorator that converts a method with a single cls argument into a property 8 | that can be accessed directly from the class. 9 | """ 10 | 11 | def __init__(self, method=None): 12 | self.fget = method 13 | 14 | def __get__(self, instance, cls=None): 15 | return self.fget(cls) 16 | 17 | def getter(self, method): 18 | self.fget = method 19 | return self 20 | 21 | 22 | def get_verbose_name(model): 23 | return force_str(model._meta.verbose_name) 24 | 25 | 26 | def get_verbose_name_plural(model): 27 | return force_str(model._meta.verbose_name_plural) 28 | 29 | 30 | def get_app_label(model): 31 | return force_str(model._meta.app_label) 32 | 33 | 34 | def get_model_name(model): 35 | return force_str(model._meta.model_name) 36 | 37 | 38 | def get_model_info(model): 39 | return get_app_label(model), get_model_name(model) 40 | 41 | 42 | def action_view_name(*, model, action, app_name=None, full=True): 43 | if app_name is None: 44 | app_name, model_name = get_model_info(model) 45 | else: 46 | model_name = get_model_name(model) 47 | 48 | with override(None): 49 | if full: 50 | return "%(app_name)s:%(model_name)s_%(action)s" % { 51 | "app_name": app_name, 52 | "model_name": model_name.lower().replace(" ", "_"), 53 | "action": action, 54 | } 55 | 56 | return "%(model_name)s_%(action)s" % { 57 | "model_name": model_name.lower().replace(" ", "_"), 58 | "action": action, 59 | } 60 | --------------------------------------------------------------------------------