├── .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] "
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]
53 | author: github-actions[bot]
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 | [](https://github.com/tj-django/django-view-breadcrumbs/actions/workflows/test.yml) [](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) [](https://results.pre-commit.ci/latest/github/tj-django/django-view-breadcrumbs/main) [](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) [](https://badge.fury.io/py/django-view-breadcrumbs)
4 |
5 |  
6 | [](https://pepy.tech/project/django-view-breadcrumbs)
7 |
8 |
9 |
10 | [](#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 |
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 |
175 |
176 | ## [Translation support](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/)
177 |
178 | ### Example
179 |
180 | 
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 |
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 |
--------------------------------------------------------------------------------
/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 | "{}",
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 |
4 |
5 |
6 | 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 |
31 | {% endblock %}
32 |
33 |
--------------------------------------------------------------------------------
/demo/templates/demo/test-create.html:
--------------------------------------------------------------------------------
1 | {% extends "demo/index.html" %}
2 | {% load i18n %}
3 | {% block body %}
4 |
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/demo/templates/demo/test-custom.html:
--------------------------------------------------------------------------------
1 | {% extends "demo/index.html" %}
2 | {% block body %}
3 |
4 | {% for pathname, path_url in view_paths %}
5 | -
6 | {{ pathname }}
7 |
8 | {% endfor %}
9 |
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 |
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 |
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 | [](https://github.com/tj-django/django-view-breadcrumbs/actions/workflows/test.yml) [](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) [](https://results.pre-commit.ci/latest/github/tj-django/django-view-breadcrumbs/main) [](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) [](https://badge.fury.io/py/django-view-breadcrumbs)
4 |
5 |  
6 | [](https://pepy.tech/project/django-view-breadcrumbs)
7 |
8 |
9 |
10 | [](#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 |
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 |
175 |
176 | ## [Translation support](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/)
177 |
178 | ### Example
179 |
180 | 
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 |
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 |
2 | {% for url, label in breadcrumbs %}
3 | -
4 | {% if url and forloop.counter != breadcrumbs_total %}
5 | {{ label|safe }}
6 | {% else %}
7 | {{ label|safe }}
8 | {% endif %}
9 | {% if not forloop.last %}
10 | /
11 | {% endif %}
12 |
13 | {% endfor %}
14 |
15 |
--------------------------------------------------------------------------------
/view_breadcrumbs/templates/view_breadcrumbs/bootstrap3.html:
--------------------------------------------------------------------------------
1 |
2 | {% for url, label in breadcrumbs %}
3 | -
4 | {% if url and forloop.counter != breadcrumbs_total %}
5 | {{ label|safe }}
6 | {% else %}
7 | {{ label|safe }}
8 | {% endif %}
9 |
10 | {% endfor %}
11 |
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 |
--------------------------------------------------------------------------------