├── .all-contributorsrc
├── .bumpversion.cfg
├── .coveragerc
├── .editorconfig
├── .fussyfox.yml
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.yaml
│ └── feature_request.yaml
└── workflows
│ ├── auto-approve.yml
│ ├── auto-merge.yml
│ ├── codacy-analysis.yml
│ ├── deploy.yml
│ ├── gh-pages.yml
│ ├── greetings.yml
│ ├── migration-fixer.yml
│ ├── test.yml
│ └── update-doc-assets.yml
├── .gitignore
├── .pep8speaks.yml
├── .pre-commit-config.yaml
├── .pyup.yml
├── .whitesource
├── .yamllint
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Duplicate-action.png
├── Duplicate-button.png
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.md
├── django_clone
├── __init__.py
├── settings.py
└── wsgi.py
├── docs
├── .nojekyll
├── CHANGELOG.md
├── Duplicate-action.png
├── Duplicate-button.png
├── README.md
├── _sidebar.md
└── index.html
├── manage.py
├── model_clone
├── __init__.py
├── admin.py
├── apps.py
├── locale
│ ├── en_US
│ │ └── LC_MESSAGES
│ │ │ └── django.po
│ └── fr
│ │ └── LC_MESSAGES
│ │ └── django.po
├── mixin.py
├── models.py
├── signals.py
├── templates
│ ├── admin
│ │ └── change_form_object_tools.html
│ └── clone
│ │ └── change_form.html
├── tests
│ ├── __init__.py
│ ├── test_clone_mixin.py
│ ├── test_clone_signals.py
│ ├── test_create_copy_of_instance.py
│ └── test_utils.py
└── utils.py
├── renovate.json
├── requirements.txt
├── sample
├── __init__.py
├── admin.py
├── apps.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ └── create_default_user.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_library.py
│ ├── 0003_book_created_at.py
│ ├── 0004_auto_20191122_0848.py
│ ├── 0005_page.py
│ ├── 0006_assignment.py
│ ├── 0007_auto_20200829_0213.py
│ ├── 0008_page_created_at.py
│ ├── 0009_auto_20210407_1546.py
│ ├── 0010_furniture_house_room.py
│ ├── 0011_auto_20210414_1744.py
│ ├── 0012_backcover_cover.py
│ ├── 0013_edition.py
│ ├── 0014_auto_20210422_1449.py
│ ├── 0015_auto_20210423_0935.py
│ ├── 0016_product.py
│ ├── 0017_auto_20210624_1117.py
│ ├── 0018_auto_20210628_2301.py
│ ├── 0019_saletag_sale_tag_unique_name.py
│ ├── 0020_auto_20210717_2230.py
│ ├── 0021_book_custom_slug.py
│ ├── 0022_ending_sentence.py
│ ├── 0023_alter_edition_book.py
│ ├── 0024_alter_edition_book.py
│ ├── 0025_auto_20221029_0402.py
│ └── __init__.py
├── models.py
├── signals.py
└── urls.py
├── sample_assignment
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_alter_contract_id.py
│ └── __init__.py
└── models.py
├── sample_company
├── __init__.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_alter_companydepot_id.py
│ └── __init__.py
└── models.py
├── sample_driver
├── __init__.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_alter_driver_id.py
│ ├── 0003_driverflag_driver_flags.py
│ └── __init__.py
└── models.py
├── setup.cfg
├── setup.py
└── tox.ini
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "README.md"
4 | ],
5 | "imageSize": 100,
6 | "commit": false,
7 | "contributors": [
8 | {
9 | "login": "gerbyzation",
10 | "name": "Gerben Neven",
11 | "avatar_url": "https://avatars1.githubusercontent.com/u/2500973?v=4",
12 | "profile": "http://gerritneven.nl",
13 | "contributions": [
14 | "bug",
15 | "test",
16 | "code"
17 | ]
18 | },
19 | {
20 | "login": "SebastianKapunkt",
21 | "name": "Sebastian Kapunkt",
22 | "avatar_url": "https://avatars1.githubusercontent.com/u/2536081?v=4",
23 | "profile": "http://sebastian-kindt.com",
24 | "contributions": [
25 | "code",
26 | "bug",
27 | "test"
28 | ]
29 | },
30 | {
31 | "login": "andresp99999",
32 | "name": "Andrés Portillo",
33 | "avatar_url": "https://avatars0.githubusercontent.com/u/1036725?v=4",
34 | "profile": "https://github.com/andresp99999",
35 | "contributions": [
36 | "bug"
37 | ]
38 | },
39 | {
40 | "login": "renovate-bot",
41 | "name": "WhiteSource Renovate",
42 | "avatar_url": "https://avatars0.githubusercontent.com/u/25180681?v=4",
43 | "profile": "https://renovate.whitesourcesoftware.com",
44 | "contributions": [
45 | "maintenance"
46 | ]
47 | },
48 | {
49 | "login": "yuekui",
50 | "name": "Yuekui",
51 | "avatar_url": "https://avatars2.githubusercontent.com/u/2561450?v=4",
52 | "profile": "https://github.com/yuekui",
53 | "contributions": [
54 | "code",
55 | "bug",
56 | "test",
57 | "doc",
58 | "maintenance"
59 | ]
60 | },
61 | {
62 | "login": "diesieben07",
63 | "name": "Take Weiland",
64 | "avatar_url": "https://avatars.githubusercontent.com/u/1915984?v=4",
65 | "profile": "https://github.com/diesieben07",
66 | "contributions": [
67 | "test",
68 | "bug",
69 | "code"
70 | ]
71 | },
72 | {
73 | "login": "ptrck",
74 | "name": "Patrick",
75 | "avatar_url": "https://avatars.githubusercontent.com/u/1259703?v=4",
76 | "profile": "https://github.com/ptrck",
77 | "contributions": [
78 | "bug",
79 | "code"
80 | ]
81 | },
82 | {
83 | "login": "Akollek",
84 | "name": "Amiel Kollek",
85 | "avatar_url": "https://avatars.githubusercontent.com/u/5873158?v=4",
86 | "profile": "https://github.com/Akollek",
87 | "contributions": [
88 | "code",
89 | "bug",
90 | "test"
91 | ]
92 | },
93 | {
94 | "login": "erictheise",
95 | "name": "Eric Theise",
96 | "avatar_url": "https://avatars.githubusercontent.com/u/317680?v=4",
97 | "profile": "https://erictheise.com/",
98 | "contributions": [
99 | "doc"
100 | ]
101 | },
102 | {
103 | "login": "DanielSchaffer",
104 | "name": "Daniel Schaffer",
105 | "avatar_url": "https://avatars.githubusercontent.com/u/334487?v=4",
106 | "profile": "https://github.com/DanielSchaffer",
107 | "contributions": [
108 | "code",
109 | "test"
110 | ]
111 | },
112 | {
113 | "login": "DamianB-BitFlipper",
114 | "name": "Damian Barabonkov",
115 | "avatar_url": "https://avatars.githubusercontent.com/u/4206989?v=4",
116 | "profile": "http://damianb.dev",
117 | "contributions": [
118 | "code",
119 | "test"
120 | ]
121 | }
122 | ],
123 | "contributorsPerLine": 7,
124 | "projectName": "django-clone",
125 | "projectOwner": "tj-django",
126 | "repoType": "github",
127 | "repoHost": "https://github.com",
128 | "skipCi": true,
129 | "commitConvention": "angular",
130 | "commitType": "docs"
131 | }
132 |
--------------------------------------------------------------------------------
/.bumpversion.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 5.3.3
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:model_clone/__init__.py]
11 | search = __version__ = "{current_version}"
12 | replace = __version__ = "{new_version}"
13 |
14 | [bdist_wheel]
15 | universal = 1
16 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | source = model_clone
3 | omit =
4 | model_clone/test*,
5 | model_clone/admin*
6 |
7 | [report]
8 | precision = 1
9 | exclude_lines =
10 | pragma: no cover
11 | pass
12 | def __repr__
13 | if self.debug:
14 | if settings.DEBUG
15 | # Don't complain if tests don't hit defensive assertion code:
16 | raise AssertionError
17 | raise StopIteration
18 | raise NotImplementedError
19 | except ImportError
20 | except IntegrityError
21 | # Don't complain if non-runnable code isn't run:
22 | if __name__ == .__main__.:
23 |
--------------------------------------------------------------------------------
/.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 | - flake8
2 | # - pydocstyle
3 | # - bandit TODO: Switch to use github action
4 | # - isort
5 |
--------------------------------------------------------------------------------
/.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/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@v4
35 | with:
36 | github-token: ${{ secrets.PAT_TOKEN }}
37 |
--------------------------------------------------------------------------------
/.github/workflows/auto-merge.yml:
--------------------------------------------------------------------------------
1 | name: Auto-merge
2 | on:
3 | pull_request_target:
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.actor == 'dependabot[bot]' }}
15 | steps:
16 | - name: Dependabot metadata
17 | id: metadata
18 | uses: dependabot/fetch-metadata@v2
19 | with:
20 | github-token: "${{ secrets.GITHUB_TOKEN }}"
21 | - name: Enable auto-merge for Dependabot PRs
22 | if: |
23 | (
24 | steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
25 | steps.metadata.outputs.update-type == 'version-update:semver-patch'
26 | )
27 | run: gh pr merge --auto --squash "$PR_URL"
28 | env:
29 | PR_URL: ${{github.event.pull_request.html_url}}
30 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
31 | pre-commit-ci:
32 | runs-on: ubuntu-latest
33 | if: ${{ github.event.pull_request.user.login == 'pre-commit-ci[bot]' }}
34 | steps:
35 | - name: Enable auto-merge for Pre-commit PRs
36 | run: gh pr merge --auto --squash "$PR_URL"
37 | env:
38 | PR_URL: ${{github.event.pull_request.html_url}}
39 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
40 |
--------------------------------------------------------------------------------
/.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@11bd71901bbe5b1630ceea73d27597364c9af683 # 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.5
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/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Upload Python Package
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | permissions:
8 | contents: write
9 | pull-requests: write
10 |
11 | jobs:
12 | deploy:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
16 | with:
17 | fetch-depth: 0
18 |
19 | - name: Run semver-diff
20 | id: semver-diff
21 | uses: tj-actions/semver-diff@v3.0.1
22 |
23 | - name: Set up Python
24 | uses: actions/setup-python@v5
25 | with:
26 | python-version: '3.13.x'
27 |
28 | - name: Upgrade pip
29 | run: |
30 | pip install -U pip
31 |
32 | - name: Install dependencies
33 | run: make install-deploy
34 |
35 | - name: Setup git
36 | run: |
37 | git config --local user.email "github-actions[bot]@users.noreply.github.com"
38 | git config --local user.name "github-actions[bot]"
39 |
40 | - name: bumpversion
41 | run: |
42 | make increase-version PART="${{ steps.semver-diff.outputs.release_type }}"
43 |
44 | - name: Build and publish
45 | run: make release
46 | env:
47 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
48 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
49 |
50 | - name: Generate CHANGELOG
51 | uses: tj-actions/github-changelog-generator@v1.21
52 |
53 | - name: Create Pull Request
54 | uses: peter-evans/create-pull-request@v7
55 | with:
56 | base: "main"
57 | title: "Upgraded ${{ steps.semver-diff.outputs.old_version }} → ${{ steps.semver-diff.outputs.new_version }}"
58 | branch: "chore/upgrade-${{ steps.semver-diff.outputs.old_version }}-to-${{ steps.semver-diff.outputs.new_version }}"
59 | commit-message: "Upgraded from ${{ steps.semver-diff.outputs.old_version }} → ${{ steps.semver-diff.outputs.new_version }}"
60 | body: "View [CHANGES](https://github.com/${{ github.repository }}/compare/${{ steps.semver-diff.outputs.old_version }}...${{ steps.semver-diff.outputs.new_version }})"
61 | token: ${{ secrets.GITHUB_TOKEN }}
62 |
--------------------------------------------------------------------------------
/.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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
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/migration-fixer.yml:
--------------------------------------------------------------------------------
1 | name: Fix django migrations
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | fix-migrations:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
13 | with:
14 | fetch-depth: 0
15 | persist-credentials: false
16 |
17 | - name: Set up Python
18 | uses: actions/setup-python@v5
19 | with:
20 | python-version: '3.13.x'
21 | cache: 'pip'
22 | cache-dependency-path: '**/requirements.txt'
23 |
24 | - name: Install dependencies
25 | run: |
26 | make install-test
27 |
28 | - name: Run django-migration-fixer
29 | uses: tj-django/django-migration-fixer@v1.3.6
30 |
31 | - name: Verify Changed files
32 | uses: tj-actions/verify-changed-files@v20
33 | id: verify-changed-files
34 | with:
35 | files: |
36 | sample/migrations
37 | sample_assignment/migrations
38 | sample_driver/migrations
39 | sample_company/migrations
40 |
41 | - name: Commit migration changes
42 | if: steps.verify-changed-files.outputs.files_changed == 'true'
43 | run: |
44 | git config --local user.email "github-actions[bot]@users.noreply.github.com"
45 | git config --local user.name "github-actions[bot]"
46 | git add sample/migrations sample_assignment/migrations sample_driver/migrations sample_company/migrations
47 | git commit -m "Update migrations"
48 |
49 | - name: Push migration changes
50 | if: steps.verify-changed-files.outputs.files_changed == 'true'
51 | uses: ad-m/github-push-action@master
52 | with:
53 | github_token: ${{ secrets.PAT_TOKEN }}
54 | branch: ${{ github.head_ref }}
55 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 | paths:
11 | - '**'
12 |
13 | jobs:
14 | cleanup-runs:
15 | runs-on: ubuntu-latest
16 | if: "!startsWith(github.ref, 'refs/tags/') && !endsWith(github.ref, 'main')"
17 | steps:
18 | - uses: rokroskar/workflow-run-cleanup-action@v0.3.3
19 | env:
20 | GITHUB_TOKEN: ${{ github.token }}
21 |
22 | build:
23 | runs-on: ${{ matrix.platform }}
24 | strategy:
25 | fail-fast: false
26 | matrix:
27 | platform: [ubuntu-latest, macos-latest, windows-latest]
28 | python-version: [3.7, 3.8, 3.9, '3.10', 3.11]
29 | exclude:
30 | - platform: macos-latest
31 | python-version: 3.11
32 | - platform: macos-latest
33 | python-version: 3.7
34 | - platform: windows-latest
35 | python-version: 3.11
36 | - platform: ubuntu-latest
37 | python-version: 3.7
38 |
39 | steps:
40 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
41 |
42 | - name: Set up Python ${{ matrix.python-version }}
43 | uses: actions/setup-python@v5
44 | with:
45 | python-version: ${{ matrix.python-version }}
46 | cache: 'pip'
47 | cache-dependency-path: '**/requirements.txt'
48 |
49 | - name: Install dependencies
50 | run: |
51 | make install-test
52 |
53 | - name: Run test
54 | run: make tox
55 | env:
56 | CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
57 | PLATFORM: ${{ matrix.platform }}
58 |
59 | - name: "Upload coverage to Codecov"
60 | if: ${{ runner.os == 'Linux' && matrix.python-version == '3.9' }}
61 | uses: codecov/codecov-action@v5.4.3
62 | with:
63 | token: ${{ secrets.CODECOV_TOKEN }}
64 | fail_ci_if_error: true
65 |
--------------------------------------------------------------------------------
/.github/workflows/update-doc-assets.yml:
--------------------------------------------------------------------------------
1 | name: Sync doc assets
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: write
10 | pull-requests: write
11 |
12 | jobs:
13 | sync-doc-assets:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
17 | with:
18 | fetch-depth: 0
19 |
20 | - name: Run test
21 | uses: tj-actions/remark@v3
22 |
23 | - name: Verify Changed files
24 | uses: tj-actions/verify-changed-files@v20
25 | id: verify_changed_files
26 | with:
27 | files: |
28 | README.md
29 |
30 | - name: README.md changed
31 | if: steps.verify_changed_files.outputs.files_changed == 'true'
32 | run: |
33 | echo "README.md has uncommitted changes"
34 | exit 1
35 |
36 | - name: Create Pull Request
37 | if: failure()
38 | uses: peter-evans/create-pull-request@v7
39 | with:
40 | base: "main"
41 | labels: merge when passing
42 | title: "Updated README.md"
43 | branch: "chore/update-readme"
44 | commit-message: "Updated README.md"
45 | body: "Updated README.md"
46 | token: ${{ secrets.GITHUB_TOKEN }}
47 |
48 | - name: Copy doc assets
49 | run: |
50 | cp -f README.md docs/README.md
51 | cp -f CHANGELOG.md docs/CHANGELOG.md
52 | cp -f *.png docs
53 |
54 | - name: Create Pull Request
55 | uses: peter-evans/create-pull-request@v7.0.8
56 | with:
57 | commit-message: Synced README changes to docs
58 | committer: github-actions[bot]
59 | author: github-actions[bot]
60 | branch: chore/update-docs
61 | labels: merge when passing
62 | base: main
63 | delete-branch: true
64 | title: Updated docs
65 | body: |
66 | Updated docs
67 | - Auto-generated by github-actions[bot]
68 | assignees: jackton1
69 | reviewers: jackton1
70 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Python template
2 | # Byte-compiled / optimized / DLL files
3 | __pycache__/
4 | *.py[cod]
5 | *$py.class
6 |
7 | # C extensions
8 | *.so
9 |
10 | # Distribution / packaging
11 | .Python
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 | cover/
54 |
55 | # Translations
56 | *.mo
57 | *.pot
58 |
59 | # Django stuff:
60 | *.log
61 | local_settings.py
62 | db.sqlite3
63 | db.sqlite3-journal
64 |
65 | # Flask stuff:
66 | instance/
67 | .webassets-cache
68 |
69 | # Scrapy stuff:
70 | .scrapy
71 |
72 | # Sphinx documentation
73 | docs/_build/
74 |
75 | # PyBuilder
76 | .pybuilder/
77 | target/
78 |
79 | # Jupyter Notebook
80 | .ipynb_checkpoints
81 |
82 | # IPython
83 | profile_default/
84 | ipython_config.py
85 |
86 | # pyenv
87 | # For a library or package, you might want to ignore these files since the code is
88 | # intended to run in multiple environments; otherwise, check them in:
89 | # .python-version
90 |
91 | # pipenv
92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
95 | # install all needed dependencies.
96 | #Pipfile.lock
97 |
98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
99 | __pypackages__/
100 |
101 | # Celery stuff
102 | celerybeat-schedule
103 | celerybeat.pid
104 |
105 | # SageMath parsed files
106 | *.sage.py
107 |
108 | # Environments
109 | .env
110 | .venv
111 | env/
112 | venv/
113 | ENV/
114 | env.bak/
115 | venv.bak/
116 |
117 | # Spyder project settings
118 | .spyderproject
119 | .spyproject
120 |
121 | # Rope project settings
122 | .ropeproject
123 |
124 | # mkdocs documentation
125 | /site
126 |
127 | # mypy
128 | .mypy_cache/
129 | .dmypy.json
130 | dmypy.json
131 |
132 | # Pyre type checker
133 | .pyre/
134 |
135 | # pytype static type analyzer
136 | .pytype/
137 |
138 | # Cython debug symbols
139 | cython_debug/
140 |
141 | # JetBrains PyCharm
142 | .idea/
143 |
144 | # Test Results
145 | test-reports/
146 |
147 | .DS_Store
148 |
--------------------------------------------------------------------------------
/.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: 6.0.1
10 | hooks:
11 | - id: isort
12 | args: ["--profile", "black", "--filter-files"]
13 |
14 | - repo: https://github.com/pre-commit/pre-commit-hooks
15 | rev: v5.0.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: 25.1.0
24 | hooks:
25 | - id: black
26 | language_version: python3
27 |
28 | - repo: https://github.com/adrienverge/yamllint.git
29 | rev: v1.37.1
30 | hooks:
31 | - id: yamllint
32 | args: ["-d", "relaxed"]
33 |
34 | - repo: https://github.com/pycqa/flake8
35 | rev: 7.2.0
36 | hooks:
37 | - id: flake8
38 |
--------------------------------------------------------------------------------
/.pyup.yml:
--------------------------------------------------------------------------------
1 | # autogenerated pyup.io config file
2 | # see https://pyup.io/docs/configuration/ for all available options
3 |
4 | schedule: ''
5 | update: False
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 |
--------------------------------------------------------------------------------
/.yamllint:
--------------------------------------------------------------------------------
1 | extends: default
2 |
3 | rules:
4 | document-start: disable
5 | colons: disable
6 | truthy: disable
7 | comments-indentation: disable
8 | comments: disable
9 |
10 | line-length:
11 | max: 136
12 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Duplicate-action.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tj-django/django-clone/a9d90db6c3f54fb31cd029df117e1eee8f061253/Duplicate-action.png
--------------------------------------------------------------------------------
/Duplicate-button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tj-django/django-clone/a9d90db6c3f54fb31cd029df117e1eee8f061253/Duplicate-button.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Tonye Jack
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include model_clone/templates *.html
2 | recursive-include model_clone *.py
3 | recursive-exclude model_clone/tests *.py
4 | prune django_clone
5 | prune sample_assignment
6 | prune sample_company
7 | prune sample_driver
8 | prune sample
9 | prune .github
10 | prune .circleci
11 | global-exclude __pycache__
12 | global-exclude *.py[co]
13 |
14 | include *.md
15 | exclude *.example
16 | exclude *.json
17 | exclude .yamllint
18 | exclude *.png
19 | exclude *.egg-info
20 | include *.py
21 | include *.txt
22 | exclude .bumpversion.cfg
23 | exclude .coveragerc
24 | include LICENSE
25 | exclude Makefile
26 | exclude tox.ini
27 | exclude manage.py
28 | exclude .tox
29 | exclude .all-contributorsrc
30 | exclude .pypirc
31 | exclude *.yml
32 |
33 | recursive-exclude docs *.html
34 | recursive-exclude docs *.md
35 | recursive-exclude docs *.nojekyll
36 | recursive-exclude docs *.png
37 |
--------------------------------------------------------------------------------
/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 python
6 | PYTHON_VERSION := $(PYTHON) --version
7 | MANAGE_PY := $(PYTHON) manage.py
8 | PYTHON_PIP := /usr/bin/env pip
9 | PIP_COMPILE := /usr/bin/env pip-compile
10 | PART := patch
11 | PACKAGE_VERSION = $(shell $(PYTHON) setup.py --version)
12 |
13 | # Put it first so that "make" without argument is like "make help".
14 | help:
15 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-32s-\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
16 |
17 | .PHONY: help
18 |
19 | guard-%: ## Checks that env var is set else exits with non 0 mainly used in CI;
20 | @if [ -z '${${*}}' ]; then echo 'Environment variable $* not set' && exit 1; fi
21 |
22 | # --------------------------------------------------------
23 | # ------- Python package (pip) management commands -------
24 | # --------------------------------------------------------
25 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
26 |
27 | clean-build: ## remove build artifacts
28 | @rm -fr build/
29 | @rm -fr dist/
30 | @rm -fr .eggs/
31 | @find . -name '*.egg-info' -exec rm -fr {} +
32 | @find . -name '*.egg' -exec rm -f {} +
33 |
34 | clean-pyc: ## remove Python file artifacts
35 | @find . -name '*.pyc' -exec rm -f {} +
36 | @find . -name '*.pyo' -exec rm -f {} +
37 | @find . -name '*~' -exec rm -f {} +
38 | @find . -name '__pycache__' -exec rm -fr {} +
39 |
40 | clean-test: ## remove test and coverage artifacts
41 | @rm -fr .tox/
42 | @rm -f .coverage
43 | @rm -fr htmlcov/
44 | @rm -fr .pytest_cache
45 |
46 | install-wheel: ## Install wheel
47 | @echo "Installing wheel..."
48 | @pip install wheel
49 |
50 | install: clean requirements.txt install-wheel ## Install project dependencies.
51 | @echo "Installing project in dependencies..."
52 | @$(PYTHON_PIP) install -r requirements.txt
53 |
54 | install-lint: clean setup.py install-wheel ## Install lint extra dependencies.
55 | @echo "Installing lint extra requirements..."
56 | @$(PYTHON_PIP) install -e .'[lint]'
57 |
58 | install-test: clean setup.py install-wheel ## Install test extra dependencies.
59 | @echo "Installing test extra requirements..."
60 | @$(PYTHON_PIP) install -e .'[test]'
61 |
62 | install-deploy: clean setup.py install-wheel ## Install deploy extra dependencies.
63 | @echo "Installing deploy extra requirements..."
64 | @$(PYTHON_PIP) install -e .'[deploy]'
65 |
66 | install-dev: clean setup.py install-wheel ## Install development extra dependencies.
67 | @echo "Installing development requirements..."
68 | @$(PYTHON_PIP) install -e .'[development]'
69 |
70 | update-requirements: ## Updates the requirements.txt adding missing package dependencies
71 | @echo "Syncing the package requirements.txt..."
72 | @$(PIP_COMPILE)
73 |
74 | # --------------------------------------------------------
75 | # ------- Django manage.py commands ---------------------
76 | # --------------------------------------------------------
77 | migrations:
78 | @$(MANAGE_PY) makemigrations
79 |
80 | migrate:
81 | @$(MANAGE_PY) migrate
82 |
83 | default-user: migrate
84 | @echo "Creating a default user..."
85 | @$(MANAGE_PY) create_default_user
86 | @echo "Username: admin@admin.com"
87 | @echo "Password: admin"
88 |
89 | run: default-user
90 | @echo "Starting server..."
91 | @$(MANAGE_PY) runserver
92 |
93 | makemessages: clean-build ## Runs over the entire source tree of the current directory and pulls out all strings marked for translation.
94 | @$(MANAGE_PY) makemessages --locale=en_US --ignore=sample --ignore=django_clone
95 | @$(MANAGE_PY) makemessages --locale=fr --ignore=sample --ignore=django_clone
96 |
97 | compilemessages: makemessages ## Compiles .po files created by makemessages to .mo files for use with the built-in gettext support.
98 | @$(MANAGE_PY) compilemessages --ignore=.tox --ignore=sample --ignore=django_clone
99 |
100 | test:
101 | @echo "Running `$(PYTHON_VERSION)` test..."
102 | @$(MANAGE_PY) test
103 |
104 | # ----------------------------------------------------------
105 | # ---------- Release the project to PyPI -------------------
106 | # ----------------------------------------------------------
107 | increase-version: guard-PART ## Increase project version
108 | @bump2version $(PART)
109 | @git switch -c main
110 |
111 | dist: clean ## builds source and wheel package
112 | @pip install build twine
113 | @python -m build
114 |
115 | release: dist ## package and upload a release
116 | @twine upload dist/*
117 |
118 | # ----------------------------------------------------------
119 | # --------- Run project Test -------------------------------
120 | # ----------------------------------------------------------
121 | tox: install-test ## Run tox test
122 | @tox
123 |
124 | clean-test-all: clean-build ## Clean build and test assets.
125 | @rm -rf .tox/
126 | @rm -rf test-results
127 | @rm -rf .pytest_cache/
128 | @rm -f test.db
129 | @rm -f ".coverage.*" .coverage coverage.xml
130 |
131 |
132 | # -----------------------------------------------------------
133 | # --------- Fix lint errors ---------------------------------
134 | # -----------------------------------------------------------
135 | lint-fix: ## Run black with inplace for model_clone and sample/models.py.
136 | @pip install black autopep8
137 | @black model_clone sample/models.py sample_driver sample_assignment sample_company --line-length=95
138 | @autopep8 -ir model_clone sample/models.py sample_driver sample_assignment sample_company --max-line-length=95
139 |
140 | # -----------------------------------------------------------
141 | # --------- Docs ---------------------------------------
142 | # -----------------------------------------------------------
143 | serve-docs:
144 | @npm i -g docsify-cli
145 | @npx docsify serve ./docs
146 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | | Python | Django | Downloads | Code Style |
6 | |:---------:|:-------:|:-----------:|:--------------:|
7 | | [](https://pypi.org/project/django-clone) | [](https://docs.djangoproject.com/en/dev/releases/) | [](https://pepy.tech/project/django-clone) | [](https://github.com/psf/black) |
8 |
9 | | PyPI | Test | Vulnerabilities | Coverage | Code Quality | Pre-Commit |
10 | |:---------------:|:----:|:---------------:|:--------:|:-------------:|:-------------:|
11 | | [](https://badge.fury.io/py/django-clone) | [](https://github.com/tj-django/django-clone/actions?query=workflow%3ATest) | [](https://snyk.io/test/github/tj-django/django-clone?targetFile=requirements.txt) | [](https://www.codacy.com/gh/tj-django/django-clone?utm_source=github.com\&utm_medium=referral\&utm_content=tj-django/django-clone\&utm_campaign=Badge_Coverage)
[](https://codecov.io/gh/tj-django/django-clone)| [](https://www.codacy.com/gh/tj-django/django-clone?utm_source=github.com\&utm_medium=referral\&utm_content=tj-django/django-clone\&utm_campaign=Badge_Grade) | [](https://results.pre-commit.ci/latest/github/tj-django/django-clone/main) |
12 |
13 | ## django-clone
14 |
15 | Create copies of a model instance with explicit control on how the instance should be duplicated (limiting fields or related objects copied) with unique field detection.
16 |
17 | This solves the problem introduced by using `instance.pk = None` and `instance.save()` which results in copying more object state than required.
18 |
19 | ## Features
20 |
21 | * 100% test coverage.
22 | * More control over how a model instance should be duplicated
23 | * Multi Database support i.e Create duplicates on one or more databases.
24 | * Restrict fields used for creating a duplicate instance.
25 | * Detects unique fields and naively adds a suffix `copy {count}` to each duplicate instance (for supported fields only).
26 | * Optionally differentiate between a duplicate instance and the original by appending a **copy** suffix to non unique fields (for supported fields only).
27 |
28 | ## Table of Contents
29 |
30 | * [Installation](#installation)
31 | * [pip](#pip)
32 | * [poetry](#poetry)
33 | * [Usage](#usage)
34 | * [Subclassing the `CloneModel`](#subclassing-the-clonemodel)
35 | * [Using the `CloneMixin`](#using-the-clonemixin)
36 | * [Using the `CloneModel`](#using-the-clonemodel)
37 | * [Duplicating a model instance](#duplicating-a-model-instance)
38 | * [Bulk cloning a model](#bulk-cloning-a-model)
39 | * [Creating clones without subclassing `CloneMixin`.](#creating-clones-without-subclassing-clonemixin)
40 | * [CloneMixin attributes](#clonemixin-attributes)
41 | * [Explicit (include only these fields)](#explicit-include-only-these-fields)
42 | * [Implicit (include all except these fields)](#implicit-include-all-except-these-fields)
43 | * [Django Admin](#django-admin)
44 | * [Duplicating Models from the Django Admin view.](#duplicating-models-from-the-django-admin-view)
45 | * [List View](#list-view)
46 | * [Change View](#change-view)
47 | * [CloneModelAdmin class attributes](#clonemodeladmin-class-attributes)
48 | * [Advanced Usage](#advanced-usage)
49 | * [Signals](#signals)
50 | * [`pre_clone_save`, `post_clone_save`](#pre_clone_save-post_clone_save)
51 | * [Multi-database support](#multi-database-support)
52 | * [Compatibility](#compatibility)
53 | * [Running locally](#running-locally)
54 | * [Found a Bug?](#found-a-bug)
55 | * [Contributors ✨](#contributors-)
56 |
57 | ## Installation
58 |
59 | ### pip
60 |
61 | ```bash
62 | pip install django-clone
63 | ```
64 |
65 | ### poetry
66 |
67 | ```bash
68 | poetry add django-clone
69 | ```
70 |
71 | ## Usage
72 |
73 | ### Subclassing the `CloneModel`
74 |
75 | 
76 |
77 | ### Using the `CloneMixin`
78 |
79 | 
80 |
81 | ### Using the `CloneModel`
82 |
83 | 
84 |
85 | ### Duplicating a model instance
86 |
87 | 
88 |
89 | ### Bulk cloning a model
90 |
91 | 
92 |
93 | ### Creating clones without subclassing `CloneMixin`.
94 |
95 | > **NOTE:** :warning:
96 | >
97 | > * This method won't copy over related objects like Many to Many/One to Many relationships.
98 | > * Ensure that required fields skipped from being cloned are passed in using the `attrs` kwargs.
99 |
100 | 
101 |
102 | ### CloneMixin attributes
103 |
104 | | Attribute | Description |
105 | |:------------------------------:|:------------:|
106 | | `DUPLICATE_SUFFIX` | Suffix to append to duplicates
(NOTE: This requires `USE_DUPLICATE_SUFFIX_FOR_NON_UNIQUE_FIELDS`
to be enabled and supports string fields). |
107 | `USE_DUPLICATE_SUFFIX_FOR_NON_UNIQUE_FIELDS` | Enable appending the `DUPLICATE_SUFFIX` to new cloned instances. |
108 | `UNIQUE_DUPLICATE_SUFFIX` | Suffix to append to unique fields |
109 | `USE_UNIQUE_DUPLICATE_SUFFIX` | Enable appending the `UNIQUE_DUPLICATE_SUFFIX` to new cloned instances. |
110 | `MAX_UNIQUE_DUPLICATE_QUERY_ATTEMPTS` | The max query attempt while generating unique values for a case of unique conflicts. |
111 |
112 | #### Explicit (include only these fields)
113 |
114 | | Attribute | Description |
115 | |:------------------------------:|:------------:|
116 | | `_clone_fields` | Restrict the list of fields to copy from the instance (By default: Copies all fields excluding auto-created/non editable model fields) |
117 | | `_clone_m2m_fields` | Restricted Many to many fields (i.e Test.tags) |
118 | | `_clone_m2o_or_o2m_fields` | Restricted Many to One/One to Many fields |
119 | | `_clone_o2o_fields` | Restricted One to One fields |
120 | | `_clone_linked_m2m_fields` | Restricted Many to Many fields that should be linked to the new instance |
121 |
122 | #### Implicit (include all except these fields)
123 |
124 | | Attribute | Description |
125 | |:--------------------:|:-----------:|
126 | | `_clone_excluded_fields` | Excluded model fields. |
127 | `_clone_excluded_m2m_fields` | Excluded many to many fields. |
128 | `_clone_excluded_m2o_or_o2m_fields` | Excluded Many to One/One to Many fields. |
129 | `_clone_excluded_o2o_fields` | Excluded one to one fields. |
130 |
131 | > **NOTE:** :warning:
132 | >
133 | > * Ensure to either set `_clone_excluded_*` or `_clone_*`. Using both would raise errors.
134 |
135 | ### Django Admin
136 |
137 | #### Duplicating Models from the Django Admin view.
138 |
139 | 
140 |
141 | ##### List View
142 |
143 | 
144 |
145 | ##### Change View
146 |
147 | 
148 |
149 | #### CloneModelAdmin class attributes
150 |
151 | 
152 |
153 | > **NOTE:** :warning:
154 | >
155 | > * Ensure that `model_clone` is placed before `django.contrib.admin`
156 |
157 | ```python
158 | INSTALLED_APPS = [
159 | 'model_clone',
160 | 'django.contrib.admin',
161 | '...',
162 | ]
163 | ```
164 |
165 | ## Advanced Usage
166 |
167 | ### Signals
168 |
169 | #### `pre_clone_save`, `post_clone_save`
170 |
171 | 
172 |
173 | ### Multi-database support
174 |
175 | 
176 |
177 | ## Compatibility
178 |
179 | | Python | Supported version |
180 | |--------------|--------------------|
181 | | Python2.x | `<=2.5.3` |
182 | | Python3.5 | `<=2.9.6` |
183 | | Python3.6+ | `<=5.3.3` |
184 | | Python3.7+ | All versions |
185 |
186 | | Django | Supported version |
187 | |--------------|---------------------|
188 | | 1.11 | `<=2.7.2` |
189 | | 2.x | All versions |
190 | | 3.x | All versions |
191 | | 4.x | All versions |
192 |
193 | ## Running locally
194 |
195 | ```shell
196 | $ git clone git@github.com:tj-django/django-clone.git
197 | $ make default-user
198 | $ make run
199 | ```
200 |
201 | Spins up a django server running the demo app.
202 |
203 | Visit http://127.0.0.1:8000
204 |
205 | ## Found a Bug?
206 |
207 | To file a bug or submit a patch, please head over to [django-clone on github](https://github.com/tj-django/django-clone/issues).
208 |
209 | If you feel generous and want to show some extra appreciation:
210 |
211 | Support me with a :star:
212 |
213 | [![Buy me a coffee][buymeacoffee-shield]][buymeacoffee]
214 |
215 | [buymeacoffee]: https://www.buymeacoffee.com/jackton1
216 |
217 | [buymeacoffee-shield]: https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png
218 |
219 | ## Contributors ✨
220 |
221 | Thanks goes to these wonderful people:
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
256 |
--------------------------------------------------------------------------------
/django_clone/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tj-django/django-clone/a9d90db6c3f54fb31cd029df117e1eee8f061253/django_clone/__init__.py
--------------------------------------------------------------------------------
/django_clone/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for django_clone project.
3 |
4 | Generated by 'django-admin startproject' using Django 2.0.2.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.0/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/2.0/ref/settings/
11 | """
12 |
13 | import os
14 | import sys
15 |
16 | from django.utils.translation import gettext_lazy as _
17 |
18 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
19 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
20 |
21 |
22 | # Quick-start development settings - unsuitable for production
23 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
24 |
25 | # SECURITY WARNING: keep the secret key used in production secret!
26 | SECRET_KEY = "hp8o!fg=xgdw0bb%!ts+m7z08q#@w48_=o21_0w+!pmkp&w7"
27 |
28 | # SECURITY WARNING: don't run with debug turned on in production!
29 | DEBUG = True
30 |
31 | ROOT_URLCONF = "sample.urls"
32 |
33 | WSGI_APPLICATION = "django_clone.wsgi.application"
34 |
35 | INSTALLED_APPS = [
36 | "model_clone",
37 | "django.contrib.admin",
38 | "django.contrib.auth",
39 | "django.contrib.contenttypes",
40 | "django.contrib.sessions",
41 | "django.contrib.messages",
42 | "django.contrib.staticfiles",
43 | "django.contrib.humanize",
44 | "sample",
45 | "sample_assignment",
46 | "sample_company",
47 | "sample_driver",
48 | ]
49 |
50 | if sys.version_info >= (3, 6) and "--fix" in sys.argv:
51 | INSTALLED_APPS += ["migration_fixer"]
52 |
53 |
54 | TEMPLATES = [
55 | {
56 | "BACKEND": "django.template.backends.django.DjangoTemplates",
57 | "DIRS": [],
58 | "APP_DIRS": True,
59 | "OPTIONS": {
60 | "context_processors": [
61 | "django.template.context_processors.debug",
62 | "django.template.context_processors.request",
63 | "django.template.context_processors.tz",
64 | "django.contrib.auth.context_processors.auth",
65 | "django.contrib.messages.context_processors.messages",
66 | ],
67 | },
68 | },
69 | ]
70 |
71 |
72 | MIDDLEWARE = [
73 | "django.middleware.security.SecurityMiddleware",
74 | "django.contrib.sessions.middleware.SessionMiddleware",
75 | "django.middleware.common.CommonMiddleware",
76 | "django.middleware.csrf.CsrfViewMiddleware",
77 | "django.contrib.auth.middleware.AuthenticationMiddleware",
78 | "django.contrib.messages.middleware.MessageMiddleware",
79 | "django.middleware.clickjacking.XFrameOptionsMiddleware",
80 | ]
81 |
82 |
83 | # Database
84 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases
85 |
86 | DATABASES = {
87 | "default": {
88 | "ENGINE": "django.db.backends.sqlite3",
89 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
90 | },
91 | "replica": {
92 | "ENGINE": "django.db.backends.sqlite3",
93 | "NAME": os.path.join(BASE_DIR, "replica.sqlite3"),
94 | },
95 | }
96 |
97 | # Password validation
98 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
99 |
100 | AUTH_PASSWORD_VALIDATORS = [
101 | {
102 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
103 | },
104 | {
105 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
106 | },
107 | {
108 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
109 | },
110 | {
111 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
112 | },
113 | ]
114 |
115 |
116 | # Internationalization
117 | # https://docs.djangoproject.com/en/2.0/topics/i18n/
118 |
119 | LANGUAGE_CODE = "en-us"
120 |
121 | TIME_ZONE = "UTC"
122 |
123 | USE_I18N = True
124 |
125 | USE_L10N = True
126 |
127 | USE_TZ = True
128 |
129 |
130 | # Static files (CSS, JavaScript, Images)
131 | # https://docs.djangoproject.com/en/2.0/howto/static-files/
132 |
133 | STATIC_URL = "/static/"
134 |
135 | try:
136 | import xmlrunner # noqa
137 | except ImportError:
138 | pass
139 | else:
140 | # https://github.com/xmlrunner/unittest-xml-reporting#django
141 | TEST_RUNNER = "xmlrunner.extra.djangotestrunner.XMLTestRunner"
142 | TEST_OUTPUT_VERBOSE = 2
143 | TEST_OUTPUT_DESCRIPTIONS = True
144 | TEST_OUTPUT_DIR = "test-reports"
145 |
146 | LANGUAGES = (
147 | ("en", _("English")),
148 | ("fr", _("French")),
149 | )
150 |
151 | # Set the default language for your site.
152 | LANGUAGE_CODE = "en"
153 |
154 | # Tell Django where the project's translation files should be.
155 | LOCALE_PATHS = (os.path.join(BASE_DIR, "model_clone", "locale"),)
156 |
--------------------------------------------------------------------------------
/django_clone/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for django_antd project.
3 | It exposes the WSGI callable as a module-level variable named ``application``.
4 | For more information on this file, see
5 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
6 | """
7 |
8 | import os
9 |
10 | from django.core.wsgi import get_wsgi_application
11 |
12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_clone.settings")
13 |
14 | application = get_wsgi_application()
15 |
--------------------------------------------------------------------------------
/docs/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tj-django/django-clone/a9d90db6c3f54fb31cd029df117e1eee8f061253/docs/.nojekyll
--------------------------------------------------------------------------------
/docs/Duplicate-action.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tj-django/django-clone/a9d90db6c3f54fb31cd029df117e1eee8f061253/docs/Duplicate-action.png
--------------------------------------------------------------------------------
/docs/Duplicate-button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tj-django/django-clone/a9d90db6c3f54fb31cd029df117e1eee8f061253/docs/Duplicate-button.png
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | | Python | Django | Downloads | Code Style |
6 | |:---------:|:-------:|:-----------:|:--------------:|
7 | | [](https://pypi.org/project/django-clone) | [](https://docs.djangoproject.com/en/dev/releases/) | [](https://pepy.tech/project/django-clone) | [](https://github.com/psf/black) |
8 |
9 | | PyPI | Test | Vulnerabilities | Coverage | Code Quality | Pre-Commit |
10 | |:---------------:|:----:|:---------------:|:--------:|:-------------:|:-------------:|
11 | | [](https://badge.fury.io/py/django-clone) | [](https://github.com/tj-django/django-clone/actions?query=workflow%3ATest) | [](https://snyk.io/test/github/tj-django/django-clone?targetFile=requirements.txt) | [](https://www.codacy.com/gh/tj-django/django-clone?utm_source=github.com\&utm_medium=referral\&utm_content=tj-django/django-clone\&utm_campaign=Badge_Coverage)
[](https://codecov.io/gh/tj-django/django-clone)| [](https://www.codacy.com/gh/tj-django/django-clone?utm_source=github.com\&utm_medium=referral\&utm_content=tj-django/django-clone\&utm_campaign=Badge_Grade) | [](https://results.pre-commit.ci/latest/github/tj-django/django-clone/main) |
12 |
13 | ## django-clone
14 |
15 | Create copies of a model instance with explicit control on how the instance should be duplicated (limiting fields or related objects copied) with unique field detection.
16 |
17 | This solves the problem introduced by using `instance.pk = None` and `instance.save()` which results in copying more object state than required.
18 |
19 | ## Features
20 |
21 | * 100% test coverage.
22 | * More control over how a model instance should be duplicated
23 | * Multi Database support i.e Create duplicates on one or more databases.
24 | * Restrict fields used for creating a duplicate instance.
25 | * Detects unique fields and naively adds a suffix `copy {count}` to each duplicate instance (for supported fields only).
26 | * Optionally differentiate between a duplicate instance and the original by appending a **copy** suffix to non unique fields (for supported fields only).
27 |
28 | ## Table of Contents
29 |
30 | * [Installation](#installation)
31 | * [pip](#pip)
32 | * [poetry](#poetry)
33 | * [Usage](#usage)
34 | * [Subclassing the `CloneModel`](#subclassing-the-clonemodel)
35 | * [Using the `CloneMixin`](#using-the-clonemixin)
36 | * [Using the `CloneModel`](#using-the-clonemodel)
37 | * [Duplicating a model instance](#duplicating-a-model-instance)
38 | * [Bulk cloning a model](#bulk-cloning-a-model)
39 | * [Creating clones without subclassing `CloneMixin`.](#creating-clones-without-subclassing-clonemixin)
40 | * [CloneMixin attributes](#clonemixin-attributes)
41 | * [Explicit (include only these fields)](#explicit-include-only-these-fields)
42 | * [Implicit (include all except these fields)](#implicit-include-all-except-these-fields)
43 | * [Django Admin](#django-admin)
44 | * [Duplicating Models from the Django Admin view.](#duplicating-models-from-the-django-admin-view)
45 | * [List View](#list-view)
46 | * [Change View](#change-view)
47 | * [CloneModelAdmin class attributes](#clonemodeladmin-class-attributes)
48 | * [Advanced Usage](#advanced-usage)
49 | * [Signals](#signals)
50 | * [`pre_clone_save`, `post_clone_save`](#pre_clone_save-post_clone_save)
51 | * [Multi-database support](#multi-database-support)
52 | * [Compatibility](#compatibility)
53 | * [Running locally](#running-locally)
54 | * [Found a Bug?](#found-a-bug)
55 | * [Contributors ✨](#contributors-)
56 |
57 | ## Installation
58 |
59 | ### pip
60 |
61 | ```bash
62 | pip install django-clone
63 | ```
64 |
65 | ### poetry
66 |
67 | ```bash
68 | poetry add django-clone
69 | ```
70 |
71 | ## Usage
72 |
73 | ### Subclassing the `CloneModel`
74 |
75 | 
76 |
77 | ### Using the `CloneMixin`
78 |
79 | 
80 |
81 | ### Using the `CloneModel`
82 |
83 | 
84 |
85 | ### Duplicating a model instance
86 |
87 | 
88 |
89 | ### Bulk cloning a model
90 |
91 | 
92 |
93 | ### Creating clones without subclassing `CloneMixin`.
94 |
95 | > **NOTE:** :warning:
96 | >
97 | > * This method won't copy over related objects like Many to Many/One to Many relationships.
98 | > * Ensure that required fields skipped from being cloned are passed in using the `attrs` kwargs.
99 |
100 | 
101 |
102 | ### CloneMixin attributes
103 |
104 | | Attribute | Description |
105 | |:------------------------------:|:------------:|
106 | | `DUPLICATE_SUFFIX` | Suffix to append to duplicates
(NOTE: This requires `USE_DUPLICATE_SUFFIX_FOR_NON_UNIQUE_FIELDS`
to be enabled and supports string fields). |
107 | `USE_DUPLICATE_SUFFIX_FOR_NON_UNIQUE_FIELDS` | Enable appending the `DUPLICATE_SUFFIX` to new cloned instances. |
108 | `UNIQUE_DUPLICATE_SUFFIX` | Suffix to append to unique fields |
109 | `USE_UNIQUE_DUPLICATE_SUFFIX` | Enable appending the `UNIQUE_DUPLICATE_SUFFIX` to new cloned instances. |
110 | `MAX_UNIQUE_DUPLICATE_QUERY_ATTEMPTS` | The max query attempt while generating unique values for a case of unique conflicts. |
111 |
112 | #### Explicit (include only these fields)
113 |
114 | | Attribute | Description |
115 | |:------------------------------:|:------------:|
116 | | `_clone_fields` | Restrict the list of fields to copy from the instance (By default: Copies all fields excluding auto-created/non editable model fields) |
117 | | `_clone_m2m_fields` | Restricted Many to many fields (i.e Test.tags) |
118 | | `_clone_m2o_or_o2m_fields` | Restricted Many to One/One to Many fields |
119 | | `_clone_o2o_fields` | Restricted One to One fields |
120 | | `_clone_linked_m2m_fields` | Restricted Many to Many fields that should be linked to the new instance |
121 |
122 | #### Implicit (include all except these fields)
123 |
124 | | Attribute | Description |
125 | |:--------------------:|:-----------:|
126 | | `_clone_excluded_fields` | Excluded model fields. |
127 | `_clone_excluded_m2m_fields` | Excluded many to many fields. |
128 | `_clone_excluded_m2o_or_o2m_fields` | Excluded Many to One/One to Many fields. |
129 | `_clone_excluded_o2o_fields` | Excluded one to one fields. |
130 |
131 | > **NOTE:** :warning:
132 | >
133 | > * Ensure to either set `_clone_excluded_*` or `_clone_*`. Using both would raise errors.
134 |
135 | ### Django Admin
136 |
137 | #### Duplicating Models from the Django Admin view.
138 |
139 | 
140 |
141 | ##### List View
142 |
143 | 
144 |
145 | ##### Change View
146 |
147 | 
148 |
149 | #### CloneModelAdmin class attributes
150 |
151 | 
152 |
153 | > **NOTE:** :warning:
154 | >
155 | > * Ensure that `model_clone` is placed before `django.contrib.admin`
156 |
157 | ```python
158 | INSTALLED_APPS = [
159 | 'model_clone',
160 | 'django.contrib.admin',
161 | '...',
162 | ]
163 | ```
164 |
165 | ## Advanced Usage
166 |
167 | ### Signals
168 |
169 | #### `pre_clone_save`, `post_clone_save`
170 |
171 | 
172 |
173 | ### Multi-database support
174 |
175 | 
176 |
177 | ## Compatibility
178 |
179 | | Python | Supported version |
180 | |--------------|--------------------|
181 | | Python2.x | `<=2.5.3` |
182 | | Python3.5 | `<=2.9.6` |
183 | | Python3.6+ | `<=5.3.3` |
184 | | Python3.7+ | All versions |
185 |
186 | | Django | Supported version |
187 | |--------------|---------------------|
188 | | 1.11 | `<=2.7.2` |
189 | | 2.x | All versions |
190 | | 3.x | All versions |
191 | | 4.x | All versions |
192 |
193 | ## Running locally
194 |
195 | ```shell
196 | $ git clone git@github.com:tj-django/django-clone.git
197 | $ make default-user
198 | $ make run
199 | ```
200 |
201 | Spins up a django server running the demo app.
202 |
203 | Visit http://127.0.0.1:8000
204 |
205 | ## Found a Bug?
206 |
207 | To file a bug or submit a patch, please head over to [django-clone on github](https://github.com/tj-django/django-clone/issues).
208 |
209 | If you feel generous and want to show some extra appreciation:
210 |
211 | Support me with a :star:
212 |
213 | [![Buy me a coffee][buymeacoffee-shield]][buymeacoffee]
214 |
215 | [buymeacoffee]: https://www.buymeacoffee.com/jackton1
216 |
217 | [buymeacoffee-shield]: https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png
218 |
219 | ## Contributors ✨
220 |
221 | Thanks goes to these wonderful people:
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
256 |
--------------------------------------------------------------------------------
/docs/_sidebar.md:
--------------------------------------------------------------------------------
1 | * [Installation](#installation)
2 | * [Usage](#usage)
3 | * [Subclassing the `CloneModel`](#subclassing-the-clonemodel)
4 | * [Using the `CloneMixin`](#using-the-clonemixin)
5 | * [Duplicating a model instance](#duplicating-a-model-instance)
6 | * [Bulk cloning a model](#bulk-cloning-a-model)
7 | * [Creating clones without subclassing `CloneMixin`.](#creating-clones-without-subclassing-clonemixin)
8 | * [CloneMixin attributes](#clonemixin-attributes)
9 | * [Explicit (include only these fields)](#explicit-include-only-these-fields)
10 | * [Implicit (include all except these fields)](#implicit-include-all-except-these-fields)
11 | * [Django Admin](#django-admin)
12 | * [Duplicating Models from the Django Admin view.](#duplicating-models-from-the-django-admin-view)
13 | * [List View](#list-view)
14 | * [Change View](#change-view)
15 | * [CloneModelAdmin class attributes](#clonemodeladmin-class-attributes)
16 | * [Advanced Usage](#advanced-usage)
17 | * [Signals](#signals)
18 | * [pre\_clone\_save, post\_clone\_save](#pre_clone_save-post_clone_save)
19 | * [Clone Many to Many fields](#clone-many-to-many-fields)
20 | * [Using the `CloneModel`](#using-the-clonemodel)
21 | * [Using the `CloneMixin`](#using-the-clonemixin-1)
22 | * [Multi database support](#multi-database-support)
23 | * [Compatibility](#compatibility)
24 | * [Running locally](#running-locally)
25 | * [Found a Bug?](#found-a-bug)
26 | * [Contributors ✨](#contributors-)
27 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | django-clone
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Please wait...
15 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_clone.settings")
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError:
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 | )
15 | execute_from_command_line(sys.argv)
16 |
--------------------------------------------------------------------------------
/model_clone/__init__.py:
--------------------------------------------------------------------------------
1 | """Top-level package for django-clone."""
2 |
3 | __author__ = """Tonye Jack"""
4 | __email__ = "jtonye@ymail.com"
5 | __version__ = "5.3.3"
6 |
7 | from model_clone.admin import CloneModelAdmin, CloneModelAdminMixin
8 | from model_clone.mixin import CloneMixin
9 | from model_clone.utils import create_copy_of_instance
10 |
11 | __all__ = [
12 | "CloneMixin",
13 | "CloneModelAdmin",
14 | "CloneModelAdminMixin",
15 | "create_copy_of_instance",
16 | ]
17 |
--------------------------------------------------------------------------------
/model_clone/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib.admin import ModelAdmin
2 | from django.contrib.admin.exceptions import DisallowedModelAdminToField
3 | from django.contrib.admin.options import TO_FIELD_VAR
4 | from django.contrib.admin.utils import unquote
5 | from django.http import HttpResponseRedirect
6 | from django.urls import reverse
7 | from django.utils.translation import gettext_lazy as _
8 |
9 | from model_clone.mixin import CloneMixin
10 |
11 |
12 | class CloneModelAdminMixin(object):
13 | """Mixin to handle duplication of models."""
14 |
15 | include_duplicate_action = True
16 | include_duplicate_object_link = True
17 |
18 | def change_view(self, request, object_id, form_url="", extra_context=None):
19 | extra_context = extra_context or {}
20 | extra_context["include_duplicate_object_link"] = (
21 | self.include_duplicate_object_link
22 | )
23 | if self.include_duplicate_object_link:
24 | to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR))
25 | if to_field and not self.to_field_allowed(request, to_field):
26 | raise DisallowedModelAdminToField(
27 | "The field %s cannot be referenced." % to_field
28 | )
29 |
30 | obj = self.get_object(request, unquote(object_id), to_field)
31 |
32 | if obj is not None and request.GET.get("duplicate") == "true":
33 | clone = obj.make_clone()
34 | clone.save()
35 | self.message_user(
36 | request,
37 | _("Duplication successful, redirected to cloned: {}").format(clone),
38 | )
39 | cloned_admin_url = reverse(
40 | "admin:{0}_{1}_change".format(
41 | clone._meta.app_label, clone._meta.model_name
42 | ),
43 | args=(clone.pk,),
44 | )
45 | return HttpResponseRedirect(cloned_admin_url)
46 |
47 | return super().changeform_view(request, object_id, form_url, extra_context)
48 |
49 | def make_clone(self, request, queryset):
50 | clone_obj_ids = []
51 |
52 | for obj in queryset:
53 | clone = obj.make_clone()
54 | clone.save()
55 | clone_obj_ids.append(str(clone.pk))
56 |
57 | if clone_obj_ids:
58 | self.message_user(
59 | request,
60 | _("Successfully created: {} new duplicates").format(len(clone_obj_ids)),
61 | )
62 |
63 | make_clone.short_description = _("Duplicate selected %(verbose_name_plural)s")
64 |
65 | def _get_base_actions(self):
66 | """Return the list of actions, prior to any request-based filtering."""
67 | actions = list(super()._get_base_actions())
68 | # Add the make clone action
69 | if self.include_duplicate_action and issubclass(self.model, CloneMixin):
70 | actions.extend(self.get_action(action) for action in ["make_clone"])
71 |
72 | return actions
73 |
74 |
75 | class CloneModelAdmin(CloneModelAdminMixin, ModelAdmin):
76 | """Clone model admin view."""
77 |
--------------------------------------------------------------------------------
/model_clone/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class ModelCloneConfig(AppConfig):
5 | name = "model_clone"
6 |
--------------------------------------------------------------------------------
/model_clone/locale/en_US/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the PACKAGE package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: PACKAGE VERSION\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2021-06-24 11:44+0000\n"
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 | "Last-Translator: Tonye Jack \n"
14 | "Language-Team: LANGUAGE \n"
15 | "Language: \n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=UTF-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 |
20 | #: model_clone/admin.py:37
21 | msgid "Duplication successful, redirected to cloned: {}"
22 | msgstr ""
23 |
24 | #: model_clone/admin.py:60
25 | msgid "Successfully created: {} new duplicates"
26 | msgstr ""
27 |
28 | #: model_clone/admin.py:63
29 | #, python-format
30 | msgid "Duplicate selected %(verbose_name_plural)s"
31 | msgstr ""
32 |
33 | #: model_clone/templates/admin/change_form_object_tools.html:5
34 | #: model_clone/templates/clone/change_form.html:7
35 | msgid "Duplicate"
36 | msgstr ""
37 |
--------------------------------------------------------------------------------
/model_clone/locale/fr/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the PACKAGE package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: PACKAGE VERSION\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2021-06-24 11:44+0000\n"
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 | "Last-Translator: Tonye Jack \n"
14 | "Language-Team: LANGUAGE \n"
15 | "Language: \n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=UTF-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n"
20 |
21 | #: model_clone/admin.py:37
22 | msgid "Duplication successful, redirected to cloned: {}"
23 | msgstr ""
24 |
25 | #: model_clone/admin.py:60
26 | msgid "Successfully created: {} new duplicates"
27 | msgstr ""
28 |
29 | #: model_clone/admin.py:63
30 | #, python-format
31 | msgid "Duplicate selected %(verbose_name_plural)s"
32 | msgstr ""
33 |
34 | #: model_clone/templates/admin/change_form_object_tools.html:5
35 | #: model_clone/templates/clone/change_form.html:7
36 | msgid "Duplicate"
37 | msgstr ""
38 |
--------------------------------------------------------------------------------
/model_clone/mixin.py:
--------------------------------------------------------------------------------
1 | import itertools
2 | from itertools import repeat
3 | from typing import List # noqa
4 |
5 | from conditional import conditional
6 | from django.core.checks import Error
7 | from django.core.exceptions import ValidationError
8 | from django.db import IntegrityError, connections, models, transaction
9 | from django.db.models import SlugField
10 | from django.utils.text import slugify
11 |
12 | from model_clone.apps import ModelCloneConfig
13 | from model_clone.signals import post_clone_save, pre_clone_save
14 | from model_clone.utils import (
15 | clean_value,
16 | context_mutable_attribute,
17 | get_fields_and_unique_fields_from_cls,
18 | get_unique_default,
19 | get_unique_value,
20 | get_value,
21 | transaction_autocommit,
22 | )
23 |
24 |
25 | class CloneMixin(object):
26 | """CloneMixin mixin to duplicate an object using the model's class.
27 |
28 | :param _clone_fields: Restricted List of fields to copy from the instance.
29 | :type _clone_fields: collections.Iterable
30 | :param _clone_m2m_fields: Many to many fields (Example: TestModel.tags).
31 | :type _clone_m2m_fields: collections.Iterable
32 | :param _clone_m2o_or_o2m_fields: Many to one/One to many fields.
33 | :type _clone_m2o_or_o2m_fields: collections.Iterable
34 | :param _clone_o2o_fields: One to One fields.
35 | :type _clone_o2o_fields: collections.Iterable
36 |
37 | :param _clone_linked_m2m_fields: List of fields to link to the cloned instance.
38 | :type _clone_linked_m2m_fields: collections.Iterable
39 |
40 | :param _clone_excluded_fields: Excluded model fields.
41 | :type _clone_excluded_fields: collections.Iterable
42 | :param _clone_excluded_m2m_fields: Excluded many to many fields.
43 | :type _clone_excluded_m2m_fields: collections.Iterable
44 | :param _clone_excluded_m2o_or_o2m_fields: Excluded many to many
45 | and one to many fields.
46 | :type _clone_excluded_m2o_or_o2m_fields: collections.Iterable
47 | :param _clone_excluded_o2o_fields: Excluded one to one fields.
48 | :type _clone_excluded_o2o_fields: collections.Iterable
49 |
50 | :Example:
51 | >>> from django.conf import settings
52 | >>> from django.db import models
53 | >>>
54 | >>> # Using explicit fields
55 | >>>
56 | >>> class TestModel1(CloneMixin, models.Model):
57 | >>> field_1 = models.CharField(max_length=200)
58 | >>> tags = models.ManyToManyField(Tags)
59 | >>> audiences = models.ManyToManyField(Audience)
60 | >>> user = models.ForiegnKey(
61 | >>> settings.AUTH_USER_MODEL,
62 | >>> on_delete=models.CASCADE,
63 | >>> )
64 | >>>
65 | >>> _clone_m2m_fields = ['tags', 'audiences']
66 | >>> _clone_m2o_or_o2m_fields = ['user']
67 | >>> ...
68 | >>>
69 | >>> # Using implicit all except fields.
70 | >>>
71 | >>> class TestModel2(CloneMixin, models.Model):
72 | >>> field_1 = models.CharField(max_length=200)
73 | >>> tags = models.ManyToManyField(Tags)
74 | >>> audiences = models.ManyToManyField(Audience)
75 | >>> user = models.ForiegnKey(
76 | >>> settings.AUTH_USER_MODEL,
77 | >>> on_delete=models.CASCADE,
78 | >>> null=True,
79 | >>> )
80 |
81 | >>> # Clones any other m2m field excluding "audiences".
82 | >>> _clone_excluded_m2m_fields = ['audiences']
83 | >>> # Clones all other many to one or one to many fields excluding "user".
84 | >>> _clone_excluded_m2o_or_o2m_fields = ['user']
85 | >>> ...
86 | """
87 |
88 | # Included fields
89 | _clone_fields = [] # type: List[str]
90 | _clone_m2m_fields = [] # type: List[str]
91 | _clone_m2o_or_o2m_fields = [] # type: List[str]
92 | _clone_o2o_fields = [] # type: List[str]
93 |
94 | # Included / linked (not copied) fields
95 | _clone_linked_m2m_fields = [] # type: List[str]
96 |
97 | # Excluded fields
98 | _clone_excluded_fields = [] # type: List[str]
99 | _clone_excluded_m2m_fields = [] # type: List[str]
100 | _clone_excluded_m2o_or_o2m_fields = [] # type: List[str]
101 | _clone_excluded_o2o_fields = [] # type: List[str]
102 |
103 | DUPLICATE_SUFFIX = "copy" # type: str
104 | USE_DUPLICATE_SUFFIX_FOR_NON_UNIQUE_FIELDS = False # type: bool
105 | UNIQUE_DUPLICATE_SUFFIX = "copy" # type: str
106 | USE_UNIQUE_DUPLICATE_SUFFIX = True # type: bool
107 | MAX_UNIQUE_DUPLICATE_QUERY_ATTEMPTS = 100 # type: int
108 |
109 | @classmethod
110 | def check(cls, **kwargs): # pragma: no cover
111 | errors = super(CloneMixin, cls).check(**kwargs)
112 |
113 | if cls.USE_UNIQUE_DUPLICATE_SUFFIX and not cls.UNIQUE_DUPLICATE_SUFFIX:
114 | errors.append(
115 | Error(
116 | "UNIQUE_DUPLICATE_SUFFIX is required.",
117 | hint=(
118 | "Please provide UNIQUE_DUPLICATE_SUFFIX"
119 | f" for {cls.__name__} or set USE_UNIQUE_DUPLICATE_SUFFIX=False"
120 | ),
121 | obj=cls,
122 | id=f"{ModelCloneConfig.name}.E001",
123 | )
124 | )
125 |
126 | if cls.USE_DUPLICATE_SUFFIX_FOR_NON_UNIQUE_FIELDS and not cls.DUPLICATE_SUFFIX:
127 | errors.append(
128 | Error(
129 | "UNIQUE_DUPLICATE_SUFFIX is required.",
130 | hint=(
131 | f"Please provide DUPLICATE_SUFFIX for {cls.__name__} "
132 | "or set USE_DUPLICATE_SUFFIX_FOR_NON_UNIQUE_FIELDS=False"
133 | ),
134 | obj=cls,
135 | id=f"{ModelCloneConfig.name}.E001",
136 | )
137 | )
138 |
139 | if all([cls._clone_fields, cls._clone_excluded_fields]):
140 | errors.append(
141 | Error(
142 | "Conflicting configuration.",
143 | hint=(
144 | 'Please provide either "_clone_fields"'
145 | f' or "_clone_excluded_fields" for model {cls.__name__}'
146 | ),
147 | obj=cls,
148 | id=f"{ModelCloneConfig.name}.E002",
149 | )
150 | )
151 |
152 | if all([cls._clone_m2m_fields, cls._clone_excluded_m2m_fields]):
153 | errors.append(
154 | Error(
155 | "Conflicting configuration.",
156 | hint=(
157 | 'Please provide either "_clone_m2m_fields"'
158 | f' or "_clone_excluded_m2m_fields" for model {cls.__name__}'
159 | ),
160 | obj=cls,
161 | id=f"{ModelCloneConfig.name}.E002",
162 | )
163 | )
164 |
165 | if all(
166 | [
167 | cls._clone_m2o_or_o2m_fields,
168 | cls._clone_excluded_m2o_or_o2m_fields,
169 | ]
170 | ):
171 | errors.append(
172 | Error(
173 | "Conflicting configuration.",
174 | hint=(
175 | 'Please provide either "_clone_m2o_or_o2m_fields" or '
176 | f'"_clone_excluded_m2o_or_o2m_fields" for model {cls.__name__}'
177 | ),
178 | obj=cls,
179 | id=f"{ModelCloneConfig.name}.E002",
180 | )
181 | )
182 |
183 | if all([cls._clone_o2o_fields, cls._clone_excluded_o2o_fields]):
184 | errors.append(
185 | Error(
186 | "Conflicting configuration.",
187 | hint=(
188 | 'Please provide either "_clone_o2o_fields" or '
189 | f'"_clone_excluded_o2o_fields" for model {cls.__name__}'
190 | ),
191 | obj=cls,
192 | id=f"{ModelCloneConfig.name}.E002",
193 | )
194 | )
195 |
196 | errors.extend(cls.__check_has_invalid_linked_m2m_fields())
197 |
198 | return errors
199 |
200 | @classmethod
201 | def __check_has_invalid_linked_m2m_fields(cls):
202 | errors = []
203 |
204 | for field_name in cls._clone_linked_m2m_fields:
205 | field = cls._meta.get_field(field_name)
206 | through_field = getattr(field, "field", getattr(field, "remote_field"))
207 | through_model = through_field.through
208 | if not through_model._meta.auto_created:
209 | errors.append(
210 | Error(
211 | f"Invalid configuration for _clone_linked_m2m_fields: {field_name}",
212 | hint=(
213 | 'Use "_clone_m2m_fields" instead of "_clone_linked_m2m_fields"'
214 | f" for m2m fields that are not auto-created for model {cls.__name__}"
215 | ),
216 | obj=cls,
217 | id=f"{ModelCloneConfig.name}.E003",
218 | )
219 | )
220 |
221 | if field_name in cls._clone_excluded_m2m_fields:
222 | errors.append(
223 | Error(
224 | f"Invalid configuration for _clone_excluded_m2m_fields: {field_name}",
225 | hint=(
226 | "Fields that are linked with _clone_linked_m2m_fields "
227 | f"cannot be excluded in _clone_excluded_m2m_fields for model "
228 | f"{cls.__name__}"
229 | ),
230 | obj=cls,
231 | id=f"{ModelCloneConfig.name}.E002",
232 | )
233 | )
234 |
235 | if field_name in cls._clone_m2m_fields:
236 | errors.append(
237 | Error(
238 | f"Invalid configuration for _clone_m2m_fields: {field_name}",
239 | hint=(
240 | "Fields that are linked with _clone_linked_m2m_fields "
241 | f"cannot be included in _clone_m2m_fields for model {cls.__name__}"
242 | ),
243 | obj=cls,
244 | id=f"{ModelCloneConfig.name}.E002",
245 | )
246 | )
247 |
248 | return errors
249 |
250 | @transaction.atomic
251 | def make_clone(self, attrs=None, sub_clone=False, using=None, parent=None):
252 | """Creates a clone of the django model instance.
253 |
254 | :param attrs: Dictionary of attributes to be replaced on the cloned object.
255 | :type attrs: dict
256 | :param sub_clone: Internal boolean used to detect cloning sub objects.
257 | :type sub_clone: bool
258 | :rtype: :obj:`django.db.models.Model`
259 | :param using: The database alias used to save the created instances.
260 | :type using: str
261 | :return: The model instance that has been cloned.
262 | :param parent: The parent object that is being cloned.
263 | :type parent: :obj:`django.db.models.Model`
264 | """
265 | using = using or self._state.db or self.__class__._default_manager.db
266 | attrs = attrs or {}
267 | if not self.pk:
268 | raise ValidationError(
269 | "{}: Instance must be saved before it can be cloned.".format(
270 | self.__class__.__name__
271 | )
272 | )
273 | if sub_clone:
274 | duplicate = self # pragma: no cover
275 | duplicate.pk = None # pragma: no cover
276 | else:
277 | duplicate = self._create_copy_of_instance(self, using=using, parent=parent)
278 |
279 | for name, value in attrs.items():
280 | setattr(duplicate, name, value)
281 |
282 | duplicate = self.__duplicate_m2o_fields(duplicate, using=using)
283 |
284 | pre_clone_save.send(sender=self.__class__, instance=duplicate)
285 |
286 | duplicate.save(using=using)
287 |
288 | duplicate = self.__duplicate_o2o_fields(duplicate, using=using)
289 | duplicate = self.__duplicate_o2m_fields(duplicate, using=using)
290 | duplicate = self.__duplicate_m2m_fields(duplicate, using=using)
291 | duplicate = self.__duplicate_linked_m2m_fields(duplicate)
292 |
293 | post_clone_save.send(sender=self.__class__, instance=duplicate)
294 |
295 | return duplicate
296 |
297 | def bulk_clone(
298 | self, count, attrs=None, batch_size=None, using=None, auto_commit=False
299 | ):
300 | using = using or self._state.db or self.__class__._default_manager.db
301 | ops = connections[using].ops
302 | objs = range(count)
303 | clones = []
304 | batch_size = batch_size or max(ops.bulk_batch_size([], list(objs)), 1)
305 |
306 | with conditional(
307 | auto_commit,
308 | transaction_autocommit(using=using),
309 | ):
310 | # If count exceeds the MAX_UNIQUE_DUPLICATE_QUERY_ATTEMPTS
311 | with conditional(
312 | self.MAX_UNIQUE_DUPLICATE_QUERY_ATTEMPTS < count,
313 | context_mutable_attribute(
314 | self,
315 | "MAX_UNIQUE_DUPLICATE_QUERY_ATTEMPTS",
316 | count,
317 | ),
318 | ):
319 | if not self.MAX_UNIQUE_DUPLICATE_QUERY_ATTEMPTS >= count:
320 | raise AssertionError(
321 | "An Unknown error has occurred: Expected ({}) >= ({})".format(
322 | self.MAX_UNIQUE_DUPLICATE_QUERY_ATTEMPTS, count
323 | ),
324 | )
325 | clones = list(repeat(self.make_clone(attrs=attrs), batch_size))
326 |
327 | return clones
328 |
329 | @staticmethod
330 | def _create_copy_of_instance(
331 | instance, using=None, force=False, sub_clone=False, parent=None
332 | ):
333 | """Create a copy of a model instance.
334 |
335 | :param instance: The instance to be duplicated.
336 | :type instance: `django.db.models.Model`
337 | :param using: The database alias used to save the created instances.
338 | :type using: str
339 | :param force: Flag to skip using the current model clone declared attributes.
340 | :type force: bool
341 | :param sub_clone: Flag to skip cloning one to one fields for sub clones.
342 | :type sub_clone: bool
343 | :return: A new transient instance.
344 | :rtype: `django.db.models.Model`
345 | :param parent: The parent object that is being cloned.
346 | :type parent: :obj:`django.db.models.Model`
347 | """
348 | cls = instance.__class__
349 | clone_fields = getattr(cls, "_clone_fields", CloneMixin._clone_fields)
350 | clone_excluded_fields = getattr(
351 | cls, "_clone_excluded_fields", CloneMixin._clone_excluded_fields
352 | )
353 | clone_o2o_fields = getattr(
354 | cls, "_clone_o2o_fields", CloneMixin._clone_o2o_fields
355 | )
356 | clone_excluded_o2o_fields = getattr(
357 | cls, "_clone_excluded_o2o_fields", CloneMixin._clone_excluded_o2o_fields
358 | )
359 | unique_duplicate_suffix = getattr(
360 | cls, "UNIQUE_DUPLICATE_SUFFIX", CloneMixin.UNIQUE_DUPLICATE_SUFFIX
361 | )
362 | use_unique_duplicate_suffix = getattr(
363 | cls,
364 | "USE_UNIQUE_DUPLICATE_SUFFIX",
365 | CloneMixin.USE_UNIQUE_DUPLICATE_SUFFIX,
366 | )
367 | max_unique_duplicate_query_attempts = getattr(
368 | cls,
369 | "MAX_UNIQUE_DUPLICATE_QUERY_ATTEMPTS",
370 | CloneMixin.MAX_UNIQUE_DUPLICATE_QUERY_ATTEMPTS,
371 | )
372 | duplicate_suffix = getattr(
373 | cls,
374 | "DUPLICATE_SUFFIX",
375 | CloneMixin.DUPLICATE_SUFFIX,
376 | )
377 | use_duplicate_suffix_for_non_unique_fields = getattr(
378 | cls,
379 | "USE_DUPLICATE_SUFFIX_FOR_NON_UNIQUE_FIELDS",
380 | CloneMixin.USE_DUPLICATE_SUFFIX_FOR_NON_UNIQUE_FIELDS,
381 | )
382 |
383 | fields, unique_fields = get_fields_and_unique_fields_from_cls(
384 | model=cls,
385 | force=force,
386 | clone_fields=clone_fields,
387 | clone_excluded_fields=clone_excluded_fields,
388 | clone_o2o_fields=clone_o2o_fields,
389 | clone_excluded_o2o_fields=clone_excluded_o2o_fields,
390 | )
391 |
392 | new_instance = cls()
393 |
394 | for f in fields:
395 | if not f.editable:
396 | f.pre_save(new_instance, add=True)
397 | continue
398 |
399 | value = f.value_from_object(instance)
400 |
401 | if all(
402 | [
403 | not f.auto_created,
404 | f.concrete,
405 | f.editable,
406 | f.name in unique_fields,
407 | ]
408 | ):
409 | # Do not try to get unique value for enum type field
410 | if all(
411 | [
412 | isinstance(f, (models.CharField, models.TextField)),
413 | not f.choices,
414 | isinstance(value, str),
415 | ]
416 | ):
417 | value = clean_value(value, unique_duplicate_suffix)
418 |
419 | if f.has_default():
420 | value = f.get_default()
421 |
422 | if not callable(f.default) and isinstance(value, str):
423 | value = get_unique_default(
424 | model=cls,
425 | fname=f.attname,
426 | value=value,
427 | transform=(
428 | slugify if isinstance(f, SlugField) else str
429 | ),
430 | suffix=unique_duplicate_suffix,
431 | max_length=f.max_length,
432 | max_attempts=max_unique_duplicate_query_attempts,
433 | )
434 |
435 | elif use_unique_duplicate_suffix and isinstance(value, str):
436 | value = get_unique_value(
437 | model=cls,
438 | fname=f.attname,
439 | value=value,
440 | transform=(slugify if isinstance(f, SlugField) else str),
441 | suffix=unique_duplicate_suffix,
442 | max_length=f.max_length,
443 | max_attempts=max_unique_duplicate_query_attempts,
444 | using=using,
445 | )
446 |
447 | elif isinstance(f, models.OneToOneField) and not sub_clone:
448 | if parent is not None:
449 | value = parent.pk
450 | else:
451 | sub_instance = (
452 | getattr(instance, f.name, None) or f.get_default()
453 | )
454 |
455 | if sub_instance is not None:
456 | sub_instance = CloneMixin._create_copy_of_instance(
457 | sub_instance,
458 | force=True,
459 | sub_clone=True,
460 | using=using,
461 | )
462 | sub_instance.save(using=using)
463 | value = sub_instance.pk
464 | elif all(
465 | [
466 | use_duplicate_suffix_for_non_unique_fields,
467 | f.concrete,
468 | f.editable,
469 | f.name not in unique_fields,
470 | ]
471 | ):
472 | if (
473 | isinstance(f, (models.CharField, models.TextField))
474 | and not f.choices
475 | ):
476 | value = get_value(
477 | value=value,
478 | transform=(slugify if isinstance(f, SlugField) else str),
479 | suffix=duplicate_suffix,
480 | max_length=f.max_length,
481 | )
482 |
483 | setattr(new_instance, f.attname, value)
484 |
485 | return new_instance
486 |
487 | def __duplicate_o2o_fields(self, duplicate, using=None):
488 | """Duplicate one to one fields.
489 | :param duplicate: The transient instance that should be duplicated.
490 | :type duplicate: `django.db.models.Model`
491 | :param using: The database alias used to save the created instances.
492 | :type using: str
493 | :return: The duplicate instance with all the one to one fields duplicated.
494 | """
495 | for f in self._meta.related_objects:
496 | if f.one_to_one:
497 | if any(
498 | [
499 | f.name in self._clone_o2o_fields
500 | and f not in self._meta.concrete_fields,
501 | self._clone_excluded_o2o_fields
502 | and f.name not in self._clone_excluded_o2o_fields
503 | and f not in self._meta.concrete_fields,
504 | ]
505 | ):
506 | rel_object = getattr(self, f.name, None)
507 | if rel_object:
508 | if hasattr(rel_object, "make_clone"):
509 | new_rel_object = rel_object.make_clone(
510 | using=using,
511 | parent=duplicate,
512 | )
513 | else:
514 | new_rel_object = CloneMixin._create_copy_of_instance(
515 | rel_object,
516 | force=True,
517 | using=using,
518 | parent=duplicate,
519 | )
520 |
521 | setattr(new_rel_object, f.remote_field.name, duplicate)
522 | new_rel_object.save(using=using)
523 |
524 | return duplicate
525 |
526 | def __duplicate_o2m_fields(self, duplicate, using=None):
527 | """Duplicate one to many fields.
528 |
529 | :param duplicate: The transient instance that should be duplicated.
530 | :type duplicate: `django.db.models.Model`
531 | :param using: The database alias used to save the created instances.
532 | :type using: str
533 | :return: The duplicate instance with all the transient one to many duplicated instances.
534 | """
535 |
536 | for f in itertools.chain(
537 | self._meta.related_objects, self._meta.concrete_fields
538 | ):
539 | if f.one_to_many:
540 | if any(
541 | [
542 | f.get_accessor_name() in self._clone_m2o_or_o2m_fields,
543 | self._clone_excluded_m2o_or_o2m_fields
544 | and f.get_accessor_name()
545 | not in self._clone_excluded_m2o_or_o2m_fields,
546 | ]
547 | ):
548 | for item in getattr(self, f.get_accessor_name()).all():
549 | if hasattr(item, "make_clone"):
550 | try:
551 | item.make_clone(
552 | attrs={f.remote_field.name: duplicate},
553 | using=using,
554 | )
555 | except IntegrityError:
556 | item.make_clone(
557 | attrs={f.remote_field.name: duplicate},
558 | sub_clone=True,
559 | using=using,
560 | )
561 | else:
562 | new_item = CloneMixin._create_copy_of_instance(
563 | item,
564 | force=True,
565 | sub_clone=True,
566 | using=using,
567 | )
568 | setattr(new_item, f.remote_field.name, duplicate)
569 |
570 | new_item.save(using=using)
571 |
572 | return duplicate
573 |
574 | def __duplicate_m2o_fields(self, duplicate, using=None):
575 | """Duplicate many to one fields.
576 |
577 | :param duplicate: The transient instance that should be duplicated.
578 | :type duplicate: `django.db.models.Model`
579 | :param using: The database alias used to save the created instances.
580 | :type using: str
581 | :return: The duplicate instance with all the many to one fields duplicated.
582 | """
583 | for f in self._meta.concrete_fields:
584 | if f.many_to_one:
585 | if any(
586 | [
587 | f.name in self._clone_m2o_or_o2m_fields,
588 | self._clone_excluded_m2o_or_o2m_fields
589 | and f.name not in self._clone_excluded_m2o_or_o2m_fields,
590 | ]
591 | ):
592 | item = getattr(self, f.name)
593 | if hasattr(item, "make_clone"):
594 | try:
595 | item_clone = item.make_clone(using=using)
596 | except IntegrityError:
597 | item_clone = item.make_clone(sub_clone=True, using=using)
598 | elif item is None:
599 | item_clone = None
600 | else:
601 | item_clone = CloneMixin._create_copy_of_instance(
602 | item,
603 | force=True,
604 | sub_clone=True,
605 | using=using,
606 | )
607 | item_clone.save(using=using)
608 |
609 | setattr(duplicate, f.name, item_clone)
610 |
611 | return duplicate
612 |
613 | def __duplicate_m2m_fields(self, duplicate, using=None):
614 | """Duplicate many to many fields.
615 |
616 | :param duplicate: The transient instance that should be duplicated.
617 | :type duplicate: `django.db.models.Model`
618 | :param using: The database alias used to save the created instances.
619 | :type using: str
620 | :return: The duplicate instance with all the many to many fields duplicated.
621 | """
622 | fields = set()
623 |
624 | for f in self._meta.many_to_many:
625 | if any(
626 | [
627 | f.name in self._clone_m2m_fields,
628 | self._clone_excluded_m2m_fields
629 | and f.name not in self._clone_excluded_m2m_fields,
630 | ]
631 | ):
632 | fields.add(f)
633 |
634 | for f in self._meta.related_objects:
635 | if f.many_to_many:
636 | if any(
637 | [
638 | f.get_accessor_name() in self._clone_m2m_fields,
639 | self._clone_excluded_m2m_fields
640 | and f.get_accessor_name()
641 | not in self._clone_excluded_m2m_fields,
642 | ]
643 | ):
644 | fields.add(f)
645 |
646 | # Clone many to many fields
647 | for field in fields:
648 | if hasattr(field, "field"):
649 | # ManyToManyRel
650 | field_name = field.field.m2m_reverse_field_name()
651 | through = field.through
652 | source = getattr(self, field.get_accessor_name())
653 | destination = getattr(duplicate, field.get_accessor_name())
654 | else:
655 | through = field.remote_field.through
656 | field_name = field.m2m_field_name()
657 | source = getattr(self, field.attname)
658 | destination = getattr(duplicate, field.attname)
659 |
660 | if all(
661 | [
662 | through,
663 | not through._meta.auto_created,
664 | ]
665 | ):
666 | objs = through.objects.filter(**{field_name: self.pk})
667 | for item in objs:
668 | if hasattr(through, "make_clone"):
669 | try:
670 | item.make_clone(
671 | attrs={field_name: duplicate},
672 | using=using,
673 | )
674 | except IntegrityError:
675 | item.make_clone(
676 | attrs={field_name: duplicate},
677 | sub_clone=True,
678 | using=using,
679 | )
680 | else:
681 | item.pk = None
682 | setattr(item, field_name, duplicate)
683 | item.save(using=using)
684 | else:
685 | items_clone = []
686 | for item in source.all():
687 | if hasattr(item, "make_clone"):
688 | try:
689 | item_clone = item.make_clone(
690 | using=using,
691 | )
692 | except IntegrityError:
693 | item_clone = item.make_clone(
694 | sub_clone=True,
695 | using=using,
696 | )
697 | else:
698 | item_clone = CloneMixin._create_copy_of_instance(
699 | item,
700 | force=True,
701 | sub_clone=True,
702 | using=using,
703 | )
704 |
705 | item_clone.save(using=using)
706 |
707 | items_clone.append(item_clone)
708 |
709 | destination.set(items_clone)
710 |
711 | return duplicate
712 |
713 | def __duplicate_linked_m2m_fields(self, duplicate):
714 | """Duplicate many to many fields.
715 |
716 | :param duplicate: The transient instance that should be duplicated.
717 | :type duplicate: `django.db.models.Model`
718 | :return: The duplicate instance objects from all the many-to-many fields duplicated.
719 | """
720 |
721 | for field in self._meta.many_to_many:
722 | if all(
723 | [
724 | field.attname not in self._clone_m2m_fields,
725 | field.attname not in self._clone_excluded_m2m_fields,
726 | field.attname in self._clone_linked_m2m_fields,
727 | ]
728 | ):
729 | source = getattr(self, field.attname)
730 | destination = getattr(duplicate, field.attname)
731 | destination.set(list(source.all()))
732 |
733 | return duplicate
734 |
--------------------------------------------------------------------------------
/model_clone/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from model_clone.mixin import CloneMixin
4 |
5 |
6 | class CloneModel(CloneMixin, models.Model):
7 | class Meta:
8 | abstract = True
9 |
--------------------------------------------------------------------------------
/model_clone/signals.py:
--------------------------------------------------------------------------------
1 | from django.db.models.signals import ModelSignal
2 |
3 | pre_clone_save = ModelSignal(use_caching=True)
4 | post_clone_save = ModelSignal(use_caching=True)
5 |
--------------------------------------------------------------------------------
/model_clone/templates/admin/change_form_object_tools.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/change_form_object_tools.html" %}
2 | {% load i18n %}
3 | {% block object-tools-items %}
4 | {% if include_duplicate_object_link %}
5 | {% trans "Duplicate" %}
6 | {% endif %}
7 | {{ block.super }}
8 | {% endblock %}
9 |
--------------------------------------------------------------------------------
/model_clone/templates/clone/change_form.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/change_form.html" %}
2 | {% load l10n i18n %}
3 |
4 | {% block submit_buttons_bottom %}
5 | {{ block.super }}
6 |
7 |
8 |
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/model_clone/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tj-django/django-clone/a9d90db6c3f54fb31cd029df117e1eee8f061253/model_clone/tests/__init__.py
--------------------------------------------------------------------------------
/model_clone/tests/test_clone_signals.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import get_user_model
2 | from django.test import TestCase
3 | from django.utils import timezone
4 | from django.utils.text import slugify
5 |
6 | from sample.models import Book, Edition
7 |
8 | User = get_user_model()
9 |
10 |
11 | class CloneSignalsTestCase(TestCase):
12 | REPLICA_DB_ALIAS = "replica"
13 | databases = {
14 | "default",
15 | "replica",
16 | }
17 |
18 | @classmethod
19 | def setUpTestData(cls):
20 | cls.user = User.objects.create(username="user")
21 |
22 | def test_signals(self):
23 | name = "New Book"
24 | first_published_at = timezone.datetime(
25 | 1970, 1, 1, tzinfo=timezone.get_default_timezone()
26 | )
27 | book = Book.objects.create(
28 | name=name,
29 | created_by=self.user,
30 | slug=slugify(name),
31 | published_at=first_published_at,
32 | )
33 | self.assertEqual(book.published_at, first_published_at)
34 | edition = Edition.objects.create(seq=1, book=book)
35 | cloned_edition = edition.make_clone()
36 | self.assertEqual(cloned_edition.seq, 2)
37 | book.refresh_from_db()
38 | self.assertNotEqual(book.published_at, first_published_at)
39 |
--------------------------------------------------------------------------------
/model_clone/tests/test_create_copy_of_instance.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import get_user_model
2 | from django.core.exceptions import ValidationError
3 | from django.db import DEFAULT_DB_ALIAS, IntegrityError
4 | from django.test import TestCase
5 | from django.utils.text import slugify
6 |
7 | from model_clone import create_copy_of_instance
8 | from sample.models import Book, Library
9 |
10 | User = get_user_model()
11 |
12 |
13 | class CreateCopyOfInstanceTestCase(TestCase):
14 | REPLICA_DB_ALIAS = "replica"
15 | databases = {
16 | "default",
17 | "replica",
18 | }
19 |
20 | @classmethod
21 | def setUpTestData(cls):
22 | cls.user1 = User.objects.create(username="user 1")
23 | cls.user2 = User.objects.create(username="user 2")
24 |
25 | def test_cloning_model_with_custom_id(self):
26 | instance = Library.objects.create(name="First library", user=self.user1)
27 | clone = create_copy_of_instance(instance, attrs={"user": self.user2})
28 |
29 | self.assertNotEqual(instance.pk, clone.pk)
30 | self.assertEqual(clone.user, self.user2)
31 |
32 | def test_cloning_model_with_a_different_db_alias_is_valid(self):
33 | new_user = User(username="new user 1")
34 | new_user.save(using=self.REPLICA_DB_ALIAS)
35 | instance = Library(name="First library", user=self.user1)
36 | instance.save(using=DEFAULT_DB_ALIAS)
37 | clone = create_copy_of_instance(
38 | instance, attrs={"user": new_user}, using=self.REPLICA_DB_ALIAS
39 | )
40 |
41 | self.assertNotEqual(instance.pk, clone.pk)
42 | self.assertEqual(clone.user, new_user)
43 | self.assertNotEqual(instance._state.db, clone._state.db)
44 |
45 | def test_cloning_unique_fk_field_without_a_fallback_value_is_invalid(self):
46 | name = "New Library"
47 | instance = Library.objects.create(name=name, user=self.user1)
48 |
49 | with self.assertRaises(ValidationError):
50 | create_copy_of_instance(instance)
51 |
52 | def test_cloning_excluded_field_without_a_fallback_value_is_invalid(self):
53 | name = "New Library"
54 | instance = Book.objects.create(
55 | name=name, created_by=self.user1, slug=slugify(name)
56 | )
57 |
58 | with self.assertRaises(IntegrityError):
59 | create_copy_of_instance(
60 | instance, exclude={"slug"}, attrs={"created_by": self.user2}
61 | )
62 |
63 | def test_raises_error_when_create_copy_of_instance_uses_an_invalid_attrs_value(
64 | self,
65 | ):
66 | instance = Library.objects.create(name="First library", user=self.user1)
67 |
68 | with self.assertRaises(ValueError):
69 | create_copy_of_instance(instance, attrs="user")
70 |
71 | def test_cloning_an_invalid_object_is_invalid(self):
72 | class InvalidObj:
73 | def __init__(self):
74 | pass
75 |
76 | instance = InvalidObj()
77 |
78 | with self.assertRaises(ValueError):
79 | create_copy_of_instance(instance, attrs={"created_by": self.user2})
80 |
--------------------------------------------------------------------------------
/model_clone/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | from model_clone.utils import clean_value
4 |
5 |
6 | class CleanValueTestCase(TestCase):
7 | def test_clean_value_with_single_digit(self):
8 | """Test cleaning value with single digit suffix."""
9 | result = clean_value("Test Copy 1", "Copy")
10 | self.assertEqual(result, "Test")
11 |
12 | def test_clean_value_with_multi_digit(self):
13 | """Test cleaning value with multi-digit suffix."""
14 | result = clean_value("Test Copy 10", "Copy")
15 | self.assertEqual(result, "Test")
16 |
17 | result = clean_value("Test Copy 123", "Copy")
18 | self.assertEqual(result, "Test")
19 |
20 | def test_clean_value_with_hyphen_separator(self):
21 | """Test cleaning value with hyphen separator."""
22 | result = clean_value("test-copy-1", "copy")
23 | self.assertEqual(result, "test")
24 |
25 | result = clean_value("test-copy-42", "copy")
26 | self.assertEqual(result, "test")
27 |
28 | def test_clean_value_case_insensitive(self):
29 | """Test that cleaning is case insensitive."""
30 | result = clean_value("Test COPY 1", "copy")
31 | self.assertEqual(result, "Test")
32 |
33 | result = clean_value("test copy 1", "COPY")
34 | self.assertEqual(result, "test")
35 |
36 | def test_clean_value_with_regex_special_characters(self):
37 | """Test cleaning value when suffix contains regex special characters."""
38 | result = clean_value("Test (Copy) 1", "(Copy)")
39 | self.assertEqual(result, "Test")
40 |
41 | result = clean_value("Test Copy+ 2", "Copy+")
42 | self.assertEqual(result, "Test")
43 |
44 | result = clean_value("Test Copy* 5", "Copy*")
45 | self.assertEqual(result, "Test")
46 |
47 | result = clean_value("Test Copy? 3", "Copy?")
48 | self.assertEqual(result, "Test")
49 |
50 | result = clean_value("Test Copy[1] 4", "Copy[1]")
51 | self.assertEqual(result, "Test")
52 |
53 | def test_clean_value_with_dots_and_brackets(self):
54 | """Test cleaning with complex regex characters."""
55 | result = clean_value("Test Copy.exe 1", "Copy.exe")
56 | self.assertEqual(result, "Test")
57 |
58 | result = clean_value("Test (v2.0) 15", "(v2.0)")
59 | self.assertEqual(result, "Test")
60 |
61 | def test_clean_value_no_match(self):
62 | """Test that value is unchanged when pattern doesn't match."""
63 | result = clean_value("Test Copy", "Copy")
64 | self.assertEqual(result, "Test Copy")
65 |
66 | result = clean_value("Test Different 1", "Copy")
67 | self.assertEqual(result, "Test Different 1")
68 |
69 | result = clean_value("Test Copy A", "Copy")
70 | self.assertEqual(result, "Test Copy A")
71 |
72 | def test_clean_value_partial_match(self):
73 | """Test that partial matches are not cleaned."""
74 | result = clean_value("Test Copying 1", "Copy")
75 | self.assertEqual(result, "Test Copying 1")
76 |
77 | result = clean_value("Test Copy Something 1", "Copy")
78 | self.assertEqual(result, "Test Copy Something 1")
79 |
80 | def test_clean_value_middle_of_string(self):
81 | """Test that pattern in middle of string is not cleaned."""
82 | result = clean_value("Test Copy 1 More", "Copy")
83 | self.assertEqual(result, "Test Copy 1 More")
84 |
85 | def test_clean_value_with_empty_suffix(self):
86 | """Test behavior with empty suffix."""
87 | result = clean_value("Test 1", "")
88 | self.assertEqual(result, "Test")
89 |
90 | def test_clean_value_with_space_before_suffix(self):
91 | """Test cleaning when there's a space before suffix."""
92 | result = clean_value("Test Copy 25", "Copy")
93 | self.assertEqual(result, "Test")
94 |
95 | def test_clean_value_with_hyphen_before_suffix(self):
96 | """Test cleaning when there's a hyphen before suffix."""
97 | result = clean_value("test-copy-99", "copy")
98 | self.assertEqual(result, "test")
99 |
100 | def test_clean_value_zero_digit(self):
101 | """Test cleaning with zero as digit."""
102 | result = clean_value("Test Copy 0", "Copy")
103 | self.assertEqual(result, "Test")
104 |
105 | def test_clean_value_leading_zeros(self):
106 | """Test cleaning with leading zeros in digits."""
107 | result = clean_value("Test Copy 001", "Copy")
108 | self.assertEqual(result, "Test")
109 |
110 | result = clean_value("Test Copy 010", "Copy")
111 | self.assertEqual(result, "Test")
112 |
113 | def test_clean_value_complex_example(self):
114 | """Test with a complex real-world example."""
115 | result = clean_value("my-awesome-slug-copy-42", "copy")
116 | self.assertEqual(result, "my-awesome-slug")
117 |
118 | result = clean_value("Product (v1.0) Copy 123", "(v1.0) Copy")
119 | self.assertEqual(result, "Product")
120 |
--------------------------------------------------------------------------------
/model_clone/utils.py:
--------------------------------------------------------------------------------
1 | import contextlib
2 | import re
3 |
4 | import six
5 | from django.db import models, transaction
6 | from django.db.transaction import TransactionManagementError
7 |
8 |
9 | def create_copy_of_instance(
10 | instance, attrs=None, exclude=(), save_new=True, using=None
11 | ):
12 | """
13 | Clone an instance of `django.db.models.Model`.
14 |
15 | :param instance: The model instance to clone.
16 | :type instance: django.db.models.Model
17 | :param exclude: List or set of fields to exclude from unique validation.
18 | :type exclude: list|set
19 | :param save_new: Save the model instance after duplication calling .save().
20 | :type save_new: bool
21 | :param using: The database alias used to save the created instances.
22 | :type using: str
23 | :param attrs: Kwargs of field and value to set on the duplicated instance.
24 | :type attrs: dict
25 | :return: The new duplicated instance.
26 | :rtype: django.db.models.Model
27 |
28 | :example:
29 | >>> from django.contrib.auth import get_user_model
30 | >>> from sample.models import Book
31 | >>> instance = Book.objects.create(name='The Beautiful Life')
32 | >>> instance.pk
33 | 1
34 | >>> instance.name
35 | "The Beautiful Life"
36 | >>> duplicate = create_copy_of_instance(instance, attrs={'name': 'Duplicate Book 2'})
37 | >>> duplicate.pk
38 | 2
39 | >>> duplicate.name
40 | "Duplicate Book 2"
41 | """
42 |
43 | if not isinstance(instance, models.Model):
44 | raise ValueError("Invalid: Expected an instance of django.db.models.Model")
45 |
46 | defaults = {}
47 | attrs = attrs or {}
48 | default_db_alias = instance._state.db or instance.__class__._default_manager.db
49 | using = using or default_db_alias
50 | fields = instance.__class__._meta.concrete_fields
51 |
52 | if not isinstance(attrs, dict):
53 | try:
54 | attrs = dict(attrs)
55 | except (TypeError, ValueError):
56 | raise ValueError(
57 | "Invalid: Expected attrs to be a dict or iterable of key and value tuples."
58 | )
59 |
60 | for f in fields:
61 | if all(
62 | [
63 | not f.auto_created,
64 | not f.primary_key,
65 | f.concrete,
66 | f.editable,
67 | f not in instance.__class__._meta.related_objects,
68 | f not in instance.__class__._meta.many_to_many,
69 | ]
70 | ):
71 | # Prevent duplicates
72 | if f.name not in attrs:
73 | defaults[f.attname] = getattr(instance, f.attname, f.get_default())
74 |
75 | defaults.update(attrs)
76 |
77 | new_obj = instance.__class__(**defaults)
78 |
79 | exclude = set(
80 | [
81 | f.name
82 | for f in instance._meta.fields
83 | if any(
84 | [
85 | all([f.name not in defaults, f.attname not in defaults]),
86 | f.has_default(),
87 | f.null,
88 | ]
89 | )
90 | ]
91 | + list(exclude)
92 | )
93 |
94 | # Bug with django using full_clean on a different db
95 | if using == default_db_alias:
96 | # Validate the new instance on the same database
97 | new_obj.full_clean(exclude=exclude)
98 |
99 | if save_new:
100 | new_obj.save(using=using)
101 |
102 | return new_obj
103 |
104 |
105 | def unpack_unique_constraints(opts, only_fields=()):
106 | """
107 | Unpack unique constraint fields.
108 |
109 | :param opts: Model options
110 | :type opts: `django.db.models.options.Options`
111 | :param only_fields: Fields that should be considered.
112 | :type only_fields: `collections.Iterable`
113 | :return: Flat list of fields.
114 | """
115 | fields = []
116 | constraints = getattr(
117 | opts, "total_unique_constraints", getattr(opts, "constraints", [])
118 | )
119 | for constraint in constraints:
120 | fields.extend([f for f in constraint.fields if f in only_fields])
121 | return fields
122 |
123 |
124 | def unpack_unique_together(opts, only_fields=()):
125 | """
126 | Unpack unique together fields.
127 |
128 | :param opts: Model options
129 | :type opts: `django.db.models.options.Options`
130 | :param only_fields: Fields that should be considered.
131 | :type only_fields: `collections.Iterable`
132 | :return: Flat list of fields.
133 | """
134 | fields = []
135 | for field in opts.unique_together:
136 | fields.extend(list([f for f in field if f in only_fields]))
137 | return fields
138 |
139 |
140 | def clean_value(value, suffix):
141 | """
142 | Strip out copy suffix from a string value.
143 |
144 | :param value: Current value e.g "Test Copy" or "test-copy" for slug fields.
145 | :type value: `str`
146 | :param suffix: The suffix value to be replaced with an empty string.
147 | :type suffix: `str`
148 | :return: Stripped string without the suffix.
149 | :rtype: `str`
150 | """
151 | # type: (str, str) -> str
152 | escaped_suffix = re.escape(suffix)
153 | return re.sub(r"([\s-]?){}[\s-]\d+$".format(escaped_suffix), "", value, flags=re.I)
154 |
155 |
156 | @contextlib.contextmanager
157 | def transaction_autocommit(using=None):
158 | """
159 | Context manager to enable autocommit.
160 |
161 | :param using: The database alias used to save the created instances.
162 | :type using: str
163 | """
164 | try:
165 | transaction.set_autocommit(True, using=using)
166 | yield
167 | except TransactionManagementError:
168 | raise
169 |
170 |
171 | @contextlib.contextmanager
172 | def context_mutable_attribute(obj, key, value):
173 | """
174 | Context manager that modifies an obj temporarily.
175 |
176 | :param obj: The object to modify.
177 | :type obj: `object`
178 | :param key: The attribute name to modify.
179 | :type key: `str`
180 | :param value: The value to set on the attribute.
181 | :type value: `object`
182 | """
183 | attribute_exists = hasattr(obj, key)
184 | default = getattr(obj, key, None)
185 | try:
186 | setattr(obj, key, value)
187 | yield
188 | finally:
189 | if attribute_exists:
190 | setattr(obj, key, default)
191 | else:
192 | delattr(obj, key) # pragma: no cover
193 |
194 |
195 | def get_value(value, suffix, transform, max_length, index=None):
196 | """
197 | Append a suffix to a string value and pass it directly to a
198 | transformation function.
199 |
200 | :param value: Current value e.g "Test Copy" or "test-copy" for slug fields.
201 | :type value: `str`
202 | :param suffix: The suffix value to be replaced with an empty string.
203 | :type suffix: `str`
204 | :param transform: The transformation function to apply to the value.
205 | :type transform: `callable`
206 | :param max_length: The maximum length of the value.
207 | :type max_length: `int`
208 | :param index: The index of the copy.
209 | :type index: `int`
210 | :return: The transformed value.
211 | """
212 | if index is None:
213 | duplicate_suffix = " {}".format(suffix.strip())
214 | else:
215 | duplicate_suffix = " {} {}".format(suffix.strip(), index)
216 |
217 | total_length = len(value + duplicate_suffix)
218 |
219 | if max_length is not None and total_length > max_length:
220 | # Truncate the value to max_length - suffix length.
221 | value = value[: max_length - len(duplicate_suffix)]
222 |
223 | return transform("{}{}".format(value, duplicate_suffix))
224 |
225 |
226 | def generate_value(value, suffix, transform, max_length, max_attempts):
227 | """
228 | Given a fixed max attempt generate a unique value.
229 |
230 | :param value: Current value e.g "Test Copy" or "test-copy" for slug fields.
231 | :type value: `str`
232 | :param suffix: The suffix value to be replaced with an empty string.
233 | :type suffix: `str`
234 | :param transform: The transformation function to apply to the value.
235 | :type transform: `callable`
236 | :param max_length: The maximum length of the value.
237 | :type max_length: `int`
238 | :param max_attempts: The maximum number of attempts to generate a unique value.
239 | :type max_attempts: `int`
240 | :return: The unique value.
241 | """
242 |
243 | for i in range(1, max_attempts):
244 | yield get_value(value, suffix, transform, max_length, i)
245 |
246 | raise StopIteration(
247 | "CloneError: max unique attempts for {} exceeded ({})".format(
248 | value, max_attempts
249 | )
250 | )
251 |
252 |
253 | def get_unique_value(
254 | model,
255 | fname,
256 | value="",
257 | transform=lambda v: v,
258 | suffix="",
259 | max_length=None,
260 | max_attempts=100,
261 | using=None,
262 | ):
263 | """
264 | Generate a unique value using current value and query the model
265 | for existing objects with the new value.
266 | """
267 | qs = model._default_manager.using(using or model._default_manager.db).all()
268 |
269 | if not qs.filter(**{fname: value}).exists():
270 | return value
271 |
272 | it = generate_value(value, suffix, transform, max_length, max_attempts)
273 | new = six.next(it)
274 | kwargs = {fname: new}
275 |
276 | while qs.filter(**kwargs).exists():
277 | new = six.next(it)
278 | kwargs[fname] = new
279 |
280 | return new
281 |
282 |
283 | def get_fields_and_unique_fields_from_cls(
284 | model,
285 | force,
286 | clone_fields,
287 | clone_excluded_fields,
288 | clone_o2o_fields,
289 | clone_excluded_o2o_fields,
290 | ):
291 | """Get a list of all fields and unique fields from a model class.
292 |
293 | Skip the clone_* properties if force is ``True``.
294 | """
295 | fields = []
296 |
297 | for f in model._meta.concrete_fields:
298 | valid = False
299 | if not getattr(f, "primary_key", False):
300 | if clone_fields and not force and not getattr(f, "one_to_one", False):
301 | valid = f.name in clone_fields
302 | elif (
303 | clone_excluded_fields
304 | and not force
305 | and not getattr(f, "one_to_one", False)
306 | ):
307 | valid = f.name not in clone_excluded_fields
308 | elif clone_o2o_fields and not force and getattr(f, "one_to_one", False):
309 | valid = f.name in clone_o2o_fields
310 | elif (
311 | clone_excluded_o2o_fields
312 | and not force
313 | and getattr(f, "one_to_one", False)
314 | ):
315 | valid = f.name not in clone_excluded_o2o_fields # pragma: no cover
316 | else:
317 | valid = True
318 |
319 | if valid:
320 | fields.append(f)
321 |
322 | unique_field_names = unpack_unique_together(
323 | opts=model._meta,
324 | only_fields=[f.attname for f in fields],
325 | )
326 |
327 | unique_constraint_field_names = unpack_unique_constraints(
328 | opts=model._meta,
329 | only_fields=[f.attname for f in fields],
330 | )
331 |
332 | unique_fields = [
333 | f.name
334 | for f in fields
335 | if not f.auto_created
336 | and (
337 | f.unique
338 | or f.name in unique_field_names
339 | or f.name in unique_constraint_field_names
340 | )
341 | ]
342 |
343 | return fields, unique_fields
344 |
345 |
346 | def get_unique_default(
347 | model,
348 | fname,
349 | value,
350 | transform=lambda v: v,
351 | suffix="",
352 | max_length=None,
353 | max_attempts=100,
354 | ):
355 | """Get a unique value using the value and adding a suffix if needed."""
356 |
357 | qs = model._default_manager.all()
358 |
359 | if not qs.filter(**{fname: value}).exists():
360 | return value
361 |
362 | it = generate_value(
363 | value,
364 | suffix,
365 | transform,
366 | max_length,
367 | max_attempts,
368 | )
369 |
370 | new = six.next(it)
371 | kwargs = {fname: new}
372 |
373 | while qs.filter(**kwargs).exists():
374 | new = six.next(it)
375 | kwargs[fname] = new
376 |
377 | return new
378 |
--------------------------------------------------------------------------------
/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 | "merge-when-passing"
40 | ],
41 | "automerge": true
42 | }
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | #
2 | # This file is autogenerated by pip-compile with Python 3.11
3 | # by the following command:
4 | #
5 | # pip-compile
6 | #
7 | asgiref==3.8.1
8 | # via django
9 | conditional==2.0
10 | # via django-clone (setup.py)
11 | django==5.2.2
12 | # via django-clone (setup.py)
13 | six==1.17.0
14 | # via django-clone (setup.py)
15 | sqlparse==0.5.3
16 | # via django
17 |
--------------------------------------------------------------------------------
/sample/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = "sample.apps.SampleConfig"
2 |
--------------------------------------------------------------------------------
/sample/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from model_clone import CloneModelAdmin
4 | from sample.models import Author, Book, Library, Page
5 |
6 |
7 | class PageInline(admin.StackedInline):
8 | model = Page
9 | fields = ["content"]
10 |
11 |
12 | @admin.register(Book)
13 | class BookAdmin(CloneModelAdmin):
14 | pass
15 |
16 |
17 | @admin.register(Author)
18 | class AuthorAdmin(CloneModelAdmin):
19 | list_display = ["first_name", "last_name", "sex", "age"]
20 |
21 |
22 | @admin.register(Library)
23 | class LibraryAdmin(CloneModelAdmin):
24 | model = Library
25 | fields = ["name", "user"]
26 |
--------------------------------------------------------------------------------
/sample/apps.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=unused-import
2 | from django.apps import AppConfig
3 |
4 |
5 | class SampleConfig(AppConfig):
6 | default_auto_field = "django.db.models.BigAutoField"
7 | name = "sample"
8 |
9 | def ready(self):
10 | from . import signals # noqa: F401
11 |
--------------------------------------------------------------------------------
/sample/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tj-django/django-clone/a9d90db6c3f54fb31cd029df117e1eee8f061253/sample/management/__init__.py
--------------------------------------------------------------------------------
/sample/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tj-django/django-clone/a9d90db6c3f54fb31cd029df117e1eee8f061253/sample/management/commands/__init__.py
--------------------------------------------------------------------------------
/sample/management/commands/create_default_user.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django.contrib.auth import get_user_model
4 | from django.core.management.base import BaseCommand
5 |
6 | User = get_user_model()
7 |
8 |
9 | class Command(BaseCommand):
10 | def handle(self, *args, **options):
11 | email = os.getenv("ADMIN_EMAIL", "admin@admin.com")
12 | password = os.getenv("ADMIN_PASSWORD", "admin")
13 |
14 | if not User.objects.filter(email=email).exists():
15 | User.objects.create_superuser(
16 | username=email,
17 | email=email,
18 | first_name="admin",
19 | last_name="admin",
20 | password=password,
21 | )
22 | self.stdout.write("Created superuser.")
23 | else:
24 | self.stderr.write("User already exists.")
25 |
--------------------------------------------------------------------------------
/sample/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2 on 2019-09-26 09:58
2 |
3 | import django.db.models.deletion
4 | from django.conf import settings
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 | initial = True
10 |
11 | dependencies = [
12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name="Author",
18 | fields=[
19 | (
20 | "id",
21 | models.AutoField(
22 | auto_created=True,
23 | primary_key=True,
24 | serialize=False,
25 | verbose_name="ID",
26 | ),
27 | ),
28 | ("first_name", models.CharField(max_length=200)),
29 | ("last_name", models.CharField(max_length=200)),
30 | ("age", models.PositiveIntegerField()),
31 | (
32 | "sex",
33 | models.CharField(
34 | choices=[("F", "Female"), ("M", "Male")], max_length=1
35 | ),
36 | ),
37 | (
38 | "created_by",
39 | models.ForeignKey(
40 | on_delete=django.db.models.deletion.PROTECT,
41 | to=settings.AUTH_USER_MODEL,
42 | ),
43 | ),
44 | ],
45 | ),
46 | migrations.CreateModel(
47 | name="Book",
48 | fields=[
49 | (
50 | "id",
51 | models.AutoField(
52 | auto_created=True,
53 | primary_key=True,
54 | serialize=False,
55 | verbose_name="ID",
56 | ),
57 | ),
58 | ("name", models.CharField(max_length=2000)),
59 | ("slug", models.SlugField(unique=True)),
60 | (
61 | "authors",
62 | models.ManyToManyField(related_name="books", to="sample.Author"),
63 | ),
64 | (
65 | "created_by",
66 | models.ForeignKey(
67 | on_delete=django.db.models.deletion.PROTECT,
68 | to=settings.AUTH_USER_MODEL,
69 | ),
70 | ),
71 | ],
72 | ),
73 | ]
74 |
--------------------------------------------------------------------------------
/sample/migrations/0002_library.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.7 on 2019-11-04 13:46
2 | import uuid
3 |
4 | import django.db.models.deletion
5 | from django.conf import settings
6 | from django.db import migrations, models
7 |
8 | import model_clone.mixin
9 |
10 |
11 | class Migration(migrations.Migration):
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ("sample", "0001_initial"),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name="Library",
20 | fields=[
21 | (
22 | "id",
23 | models.UUIDField(
24 | default=uuid.uuid4, primary_key=True, serialize=False
25 | ),
26 | ),
27 | ("name", models.CharField(max_length=100)),
28 | (
29 | "user",
30 | models.OneToOneField(
31 | on_delete=django.db.models.deletion.PROTECT,
32 | to=settings.AUTH_USER_MODEL,
33 | ),
34 | ),
35 | ],
36 | bases=(model_clone.mixin.CloneMixin, models.Model),
37 | ),
38 | ]
39 |
--------------------------------------------------------------------------------
/sample/migrations/0003_book_created_at.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2 on 2019-11-21 23:37
2 |
3 | import django.utils.timezone
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("sample", "0002_library"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="book",
15 | name="created_at",
16 | field=models.DateTimeField(
17 | auto_now_add=True, default=django.utils.timezone.now
18 | ),
19 | preserve_default=False,
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/sample/migrations/0004_auto_20191122_0848.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2 on 2019-11-22 08:48
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("sample", "0003_book_created_at"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="author",
14 | name="first_name",
15 | field=models.CharField(max_length=200, unique=True),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/sample/migrations/0005_page.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0 on 2019-12-03 09:47
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 | import model_clone.mixin
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("sample", "0004_auto_20191122_0848"),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name="Page",
17 | fields=[
18 | (
19 | "id",
20 | models.AutoField(
21 | auto_created=True,
22 | primary_key=True,
23 | serialize=False,
24 | verbose_name="ID",
25 | ),
26 | ),
27 | ("content", models.CharField(max_length=20000)),
28 | (
29 | "book",
30 | models.ForeignKey(
31 | on_delete=django.db.models.deletion.CASCADE,
32 | to="sample.Book",
33 | ),
34 | ),
35 | ],
36 | options={
37 | "abstract": False,
38 | },
39 | bases=(model_clone.mixin.CloneMixin, models.Model),
40 | ),
41 | ]
42 |
--------------------------------------------------------------------------------
/sample/migrations/0006_assignment.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.6 on 2020-05-16 17:33
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 | import model_clone.mixin
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("sample_company", "0001_initial"),
12 | ("sample_assignment", "0001_initial"),
13 | ("sample_driver", "0001_initial"),
14 | ("sample", "0005_page"),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name="Assignment",
20 | fields=[
21 | (
22 | "id",
23 | models.AutoField(
24 | auto_created=True,
25 | primary_key=True,
26 | serialize=False,
27 | verbose_name="ID",
28 | ),
29 | ),
30 | ("created_at", models.DateTimeField(auto_now_add=True, null=True)),
31 | ("updated_at", models.DateTimeField(auto_now=True, null=True)),
32 | (
33 | "title",
34 | models.CharField(
35 | blank=True, default="", max_length=100, verbose_name="Job title"
36 | ),
37 | ),
38 | ("assignment_date", models.DateField(blank=True, null=True)),
39 | (
40 | "assignment_status",
41 | models.CharField(
42 | blank=True,
43 | choices=[(1, "Complete"), (2, "Incomplete")],
44 | default="O",
45 | max_length=2,
46 | verbose_name="Assignment status",
47 | ),
48 | ),
49 | (
50 | "location",
51 | models.CharField(
52 | max_length=25, default="", verbose_name="Location"
53 | ),
54 | ),
55 | (
56 | "driver_type",
57 | models.CharField(
58 | choices=[(1, "Commercial"), (2, "Residential")],
59 | max_length=2,
60 | default="",
61 | verbose_name="Driver type",
62 | ),
63 | ),
64 | (
65 | "car_type",
66 | models.CharField(
67 | choices=[(1, "Large"), (2, "Small")],
68 | max_length=2,
69 | default="",
70 | verbose_name="Car type",
71 | ),
72 | ),
73 | (
74 | "compensation",
75 | models.DecimalField(
76 | decimal_places=2,
77 | max_digits=5,
78 | null=True,
79 | verbose_name="Compensation",
80 | ),
81 | ),
82 | (
83 | "hours",
84 | models.IntegerField(verbose_name="Amount of hours"),
85 | ),
86 | (
87 | "spots_available",
88 | models.IntegerField(null=True, verbose_name="Spots available"),
89 | ),
90 | (
91 | "description",
92 | models.TextField(blank=True, verbose_name="Assignment description"),
93 | ),
94 | (
95 | "applied_drivers",
96 | models.ManyToManyField(
97 | blank=True,
98 | related_name="driver_applications",
99 | to="sample_driver.Driver",
100 | verbose_name="Driver applications",
101 | ),
102 | ),
103 | (
104 | "chosen_drivers",
105 | models.ManyToManyField(
106 | blank=True,
107 | to="sample_driver.Driver",
108 | verbose_name="Chosen drivers",
109 | ),
110 | ),
111 | (
112 | "company",
113 | models.ForeignKey(
114 | blank=True,
115 | null=True,
116 | on_delete=django.db.models.deletion.CASCADE,
117 | to="sample_company.CompanyDepot",
118 | verbose_name="Company",
119 | ),
120 | ),
121 | (
122 | "contract",
123 | models.ForeignKey(
124 | blank=True,
125 | null=True,
126 | on_delete=django.db.models.deletion.SET_NULL,
127 | related_name="assignment_contracts",
128 | to="sample_assignment.Contract",
129 | verbose_name="Choose contract",
130 | ),
131 | ),
132 | ],
133 | options={
134 | "verbose_name": "Assigment",
135 | "verbose_name_plural": "Assignments",
136 | },
137 | bases=(model_clone.mixin.CloneMixin, models.Model),
138 | ),
139 | ]
140 |
--------------------------------------------------------------------------------
/sample/migrations/0007_auto_20200829_0213.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.4 on 2020-08-29 02:13
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("sample", "0006_assignment"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterUniqueTogether(
13 | name="author",
14 | unique_together={("first_name", "last_name", "sex")},
15 | ),
16 | ]
17 |
--------------------------------------------------------------------------------
/sample/migrations/0008_page_created_at.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.7 on 2021-04-07 11:58
2 |
3 | import django.utils.timezone
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("sample", "0007_auto_20200829_0213"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="page",
15 | name="created_at",
16 | field=models.DateTimeField(default=django.utils.timezone.now),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/sample/migrations/0009_auto_20210407_1546.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.7 on 2021-04-07 15:46
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("sample", "0008_page_created_at"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterModelOptions(
13 | name="library",
14 | options={"verbose_name_plural": "libraries"},
15 | ),
16 | ]
17 |
--------------------------------------------------------------------------------
/sample/migrations/0010_furniture_house_room.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.7 on 2021-04-10 13:50
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 | import model_clone.mixin
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("sample", "0009_auto_20210407_1546"),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name="House",
17 | fields=[
18 | (
19 | "id",
20 | models.AutoField(
21 | auto_created=True,
22 | primary_key=True,
23 | serialize=False,
24 | verbose_name="ID",
25 | ),
26 | ),
27 | ("name", models.CharField(max_length=255)),
28 | ],
29 | bases=(model_clone.mixin.CloneMixin, models.Model),
30 | ),
31 | migrations.CreateModel(
32 | name="Room",
33 | fields=[
34 | (
35 | "id",
36 | models.AutoField(
37 | auto_created=True,
38 | primary_key=True,
39 | serialize=False,
40 | verbose_name="ID",
41 | ),
42 | ),
43 | ("name", models.CharField(max_length=255)),
44 | (
45 | "house",
46 | models.ForeignKey(
47 | on_delete=django.db.models.deletion.CASCADE,
48 | related_name="rooms",
49 | to="sample.house",
50 | ),
51 | ),
52 | ],
53 | bases=(model_clone.mixin.CloneMixin, models.Model),
54 | ),
55 | migrations.CreateModel(
56 | name="Furniture",
57 | fields=[
58 | (
59 | "id",
60 | models.AutoField(
61 | auto_created=True,
62 | primary_key=True,
63 | serialize=False,
64 | verbose_name="ID",
65 | ),
66 | ),
67 | ("name", models.CharField(max_length=255)),
68 | (
69 | "room",
70 | models.ForeignKey(
71 | on_delete=django.db.models.deletion.PROTECT,
72 | related_name="furniture",
73 | to="sample.room",
74 | ),
75 | ),
76 | ],
77 | bases=(model_clone.mixin.CloneMixin, models.Model),
78 | ),
79 | ]
80 |
--------------------------------------------------------------------------------
/sample/migrations/0011_auto_20210414_1744.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.7 on 2021-04-14 17:44
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("sample", "0010_furniture_house_room"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="author",
14 | name="first_name",
15 | field=models.CharField(blank=True, max_length=200, unique=True),
16 | ),
17 | migrations.AlterField(
18 | model_name="author",
19 | name="last_name",
20 | field=models.CharField(blank=True, max_length=200),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/sample/migrations/0012_backcover_cover.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.7 on 2021-04-14 17:49
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 | import model_clone.mixin
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("sample", "0011_auto_20210414_1744"),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name="Cover",
17 | fields=[
18 | (
19 | "id",
20 | models.AutoField(
21 | auto_created=True,
22 | primary_key=True,
23 | serialize=False,
24 | verbose_name="ID",
25 | ),
26 | ),
27 | ("content", models.CharField(max_length=200)),
28 | (
29 | "book",
30 | models.OneToOneField(
31 | on_delete=django.db.models.deletion.CASCADE, to="sample.book"
32 | ),
33 | ),
34 | ],
35 | options={
36 | "abstract": False,
37 | },
38 | bases=(model_clone.mixin.CloneMixin, models.Model),
39 | ),
40 | migrations.CreateModel(
41 | name="BackCover",
42 | fields=[
43 | (
44 | "id",
45 | models.AutoField(
46 | auto_created=True,
47 | primary_key=True,
48 | serialize=False,
49 | verbose_name="ID",
50 | ),
51 | ),
52 | ("content", models.CharField(max_length=200)),
53 | (
54 | "book",
55 | models.OneToOneField(
56 | on_delete=django.db.models.deletion.CASCADE, to="sample.book"
57 | ),
58 | ),
59 | ],
60 | ),
61 | ]
62 |
--------------------------------------------------------------------------------
/sample/migrations/0013_edition.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.5 on 2021-04-20 00:23
2 |
3 | import django.db.models.deletion
4 | import django.utils.timezone
5 | from django.db import migrations, models
6 |
7 | import model_clone.mixin
8 |
9 |
10 | class Migration(migrations.Migration):
11 | dependencies = [
12 | ("sample", "0012_backcover_cover"),
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name="Edition",
18 | fields=[
19 | (
20 | "id",
21 | models.AutoField(
22 | auto_created=True,
23 | primary_key=True,
24 | serialize=False,
25 | verbose_name="ID",
26 | ),
27 | ),
28 | ("seq", models.PositiveIntegerField()),
29 | ("created_at", models.DateTimeField(default=django.utils.timezone.now)),
30 | (
31 | "book",
32 | models.ForeignKey(
33 | on_delete=django.db.models.deletion.CASCADE,
34 | related_name="editions",
35 | to="sample.book",
36 | ),
37 | ),
38 | ],
39 | bases=(model_clone.mixin.CloneMixin, models.Model),
40 | ),
41 | ]
42 |
--------------------------------------------------------------------------------
/sample/migrations/0014_auto_20210422_1449.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-04-22 14:49
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 | import model_clone.mixin
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("sample", "0013_edition"),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name="Tag",
17 | fields=[
18 | (
19 | "id",
20 | models.BigAutoField(
21 | auto_created=True,
22 | primary_key=True,
23 | serialize=False,
24 | verbose_name="ID",
25 | ),
26 | ),
27 | ("name", models.CharField(max_length=255)),
28 | ],
29 | options={
30 | "abstract": False,
31 | },
32 | bases=(model_clone.mixin.CloneMixin, models.Model),
33 | ),
34 | migrations.AlterField(
35 | model_name="assignment",
36 | name="id",
37 | field=models.BigAutoField(
38 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
39 | ),
40 | ),
41 | migrations.AlterField(
42 | model_name="author",
43 | name="id",
44 | field=models.BigAutoField(
45 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
46 | ),
47 | ),
48 | migrations.AlterField(
49 | model_name="backcover",
50 | name="id",
51 | field=models.BigAutoField(
52 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
53 | ),
54 | ),
55 | migrations.AlterField(
56 | model_name="book",
57 | name="id",
58 | field=models.BigAutoField(
59 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
60 | ),
61 | ),
62 | migrations.AlterField(
63 | model_name="cover",
64 | name="id",
65 | field=models.BigAutoField(
66 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
67 | ),
68 | ),
69 | migrations.AlterField(
70 | model_name="edition",
71 | name="id",
72 | field=models.BigAutoField(
73 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
74 | ),
75 | ),
76 | migrations.AlterField(
77 | model_name="furniture",
78 | name="id",
79 | field=models.BigAutoField(
80 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
81 | ),
82 | ),
83 | migrations.AlterField(
84 | model_name="house",
85 | name="id",
86 | field=models.BigAutoField(
87 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
88 | ),
89 | ),
90 | migrations.AlterField(
91 | model_name="page",
92 | name="id",
93 | field=models.BigAutoField(
94 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
95 | ),
96 | ),
97 | migrations.AlterField(
98 | model_name="room",
99 | name="id",
100 | field=models.BigAutoField(
101 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
102 | ),
103 | ),
104 | migrations.CreateModel(
105 | name="BookTag",
106 | fields=[
107 | (
108 | "id",
109 | models.BigAutoField(
110 | auto_created=True,
111 | primary_key=True,
112 | serialize=False,
113 | verbose_name="ID",
114 | ),
115 | ),
116 | (
117 | "book",
118 | models.ForeignKey(
119 | on_delete=django.db.models.deletion.CASCADE, to="sample.book"
120 | ),
121 | ),
122 | (
123 | "tag",
124 | models.ForeignKey(
125 | on_delete=django.db.models.deletion.PROTECT, to="sample.tag"
126 | ),
127 | ),
128 | ],
129 | options={
130 | "unique_together": {("book", "tag")},
131 | },
132 | ),
133 | migrations.AddField(
134 | model_name="book",
135 | name="tags",
136 | field=models.ManyToManyField(through="sample.BookTag", to="sample.Tag"),
137 | ),
138 | ]
139 |
--------------------------------------------------------------------------------
/sample/migrations/0015_auto_20210423_0935.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-04-23 09:35
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 | import model_clone.mixin
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("sample", "0014_auto_20210422_1449"),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name="SaleTag",
17 | fields=[
18 | (
19 | "id",
20 | models.BigAutoField(
21 | auto_created=True,
22 | primary_key=True,
23 | serialize=False,
24 | verbose_name="ID",
25 | ),
26 | ),
27 | ("name", models.CharField(max_length=255)),
28 | ],
29 | options={
30 | "abstract": False,
31 | },
32 | bases=(model_clone.mixin.CloneMixin, models.Model),
33 | ),
34 | migrations.CreateModel(
35 | name="BookSaleTag",
36 | fields=[
37 | (
38 | "id",
39 | models.BigAutoField(
40 | auto_created=True,
41 | primary_key=True,
42 | serialize=False,
43 | verbose_name="ID",
44 | ),
45 | ),
46 | (
47 | "book",
48 | models.ForeignKey(
49 | on_delete=django.db.models.deletion.CASCADE, to="sample.book"
50 | ),
51 | ),
52 | (
53 | "sale_tag",
54 | models.ForeignKey(
55 | on_delete=django.db.models.deletion.PROTECT, to="sample.saletag"
56 | ),
57 | ),
58 | ],
59 | options={
60 | "unique_together": {("book", "sale_tag")},
61 | },
62 | bases=(model_clone.mixin.CloneMixin, models.Model),
63 | ),
64 | migrations.AddField(
65 | model_name="book",
66 | name="sale_tags",
67 | field=models.ManyToManyField(
68 | through="sample.BookSaleTag", to="sample.SaleTag"
69 | ),
70 | ),
71 | ]
72 |
--------------------------------------------------------------------------------
/sample/migrations/0016_product.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.3 on 2021-05-17 12:29
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("sample", "0015_auto_20210423_0935"),
9 | ]
10 |
11 | operations = [
12 | migrations.CreateModel(
13 | name="Product",
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.TextField(unique=True)),
25 | ],
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/sample/migrations/0017_auto_20210624_1117.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.4 on 2021-06-24 11:17
2 |
3 | import uuid
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 | dependencies = [
10 | ("sample", "0016_product"),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name="author",
16 | name="uuid",
17 | field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
18 | ),
19 | migrations.AddField(
20 | model_name="library",
21 | name="uuid",
22 | field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/sample/migrations/0018_auto_20210628_2301.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.8 on 2021-06-28 23:01
2 |
3 | from django.db import migrations, models
4 | from django.utils import timezone
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("sample", "0017_auto_20210624_1117"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name="author",
15 | name="last_name",
16 | field=models.CharField(blank=True, default="Unknown", max_length=200),
17 | ),
18 | migrations.AlterField(
19 | model_name="author",
20 | name="sex",
21 | field=models.CharField(
22 | choices=[("U", "Unknown"), ("F", "Female"), ("M", "Male")],
23 | default="U",
24 | max_length=1,
25 | ),
26 | ),
27 | migrations.AddField(
28 | model_name="book",
29 | name="published_at",
30 | field=models.DateTimeField(blank=True, default=timezone.now, null=True),
31 | ),
32 | ]
33 |
--------------------------------------------------------------------------------
/sample/migrations/0019_saletag_sale_tag_unique_name.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.5 on 2021-07-17 20:26
2 |
3 | import django
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("sample", "0018_auto_20210628_2301"),
10 | ]
11 |
12 | operations = (
13 | [
14 | migrations.AddConstraint(
15 | model_name="saletag",
16 | constraint=models.UniqueConstraint(
17 | fields=("name",), name="sale_tag_unique_name"
18 | ),
19 | ),
20 | ]
21 | if django.VERSION >= (2, 2)
22 | else [
23 | migrations.AlterField(
24 | model_name="saletag",
25 | name="name",
26 | field=models.CharField(max_length=255, unique=True),
27 | ),
28 | ]
29 | )
30 |
--------------------------------------------------------------------------------
/sample/migrations/0020_auto_20210717_2230.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.5 on 2021-07-17 22:30
2 | import django
3 | from django.db import migrations, models
4 |
5 | import sample.models
6 |
7 |
8 | class Migration(migrations.Migration):
9 | dependencies = [
10 | ("sample", "0019_saletag_sale_tag_unique_name"),
11 | ]
12 |
13 | operations = (
14 | [
15 | migrations.AlterField(
16 | model_name="tag",
17 | name="name",
18 | field=models.CharField(
19 | default=sample.models.get_unique_tag_name, max_length=255
20 | ),
21 | ),
22 | migrations.AddConstraint(
23 | model_name="tag",
24 | constraint=models.UniqueConstraint(
25 | fields=("name",), name="tag_unique_name"
26 | ),
27 | ),
28 | ]
29 | if django.VERSION >= (2, 2)
30 | else [
31 | migrations.AlterField(
32 | model_name="tag",
33 | name="name",
34 | field=models.CharField(
35 | default=sample.models.get_unique_tag_name,
36 | max_length=255,
37 | unique=True,
38 | ),
39 | ),
40 | ]
41 | )
42 |
--------------------------------------------------------------------------------
/sample/migrations/0021_book_custom_slug.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.6 on 2021-08-12 01:39
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("sample", "0020_auto_20210717_2230"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="book",
14 | name="custom_slug",
15 | field=models.SlugField(default=""),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/sample/migrations/0022_ending_sentence.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.9 on 2021-11-09 09:02
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 | import model_clone.mixin
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("sample", "0021_book_custom_slug"),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name="Sentence",
17 | fields=[
18 | (
19 | "id",
20 | models.BigAutoField(
21 | auto_created=True,
22 | primary_key=True,
23 | serialize=False,
24 | verbose_name="ID",
25 | ),
26 | ),
27 | ("value", models.TextField()),
28 | ],
29 | bases=(model_clone.mixin.CloneMixin, models.Model),
30 | ),
31 | migrations.CreateModel(
32 | name="Ending",
33 | fields=[
34 | (
35 | "id",
36 | models.BigAutoField(
37 | auto_created=True,
38 | primary_key=True,
39 | serialize=False,
40 | verbose_name="ID",
41 | ),
42 | ),
43 | (
44 | "sentence",
45 | models.OneToOneField(
46 | on_delete=django.db.models.deletion.CASCADE,
47 | related_name="ending",
48 | to="sample.sentence",
49 | ),
50 | ),
51 | ],
52 | bases=(model_clone.mixin.CloneMixin, models.Model),
53 | ),
54 | ]
55 |
--------------------------------------------------------------------------------
/sample/migrations/0023_alter_edition_book.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.13 on 2022-06-24 13:32
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("sample", "0022_ending_sentence"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name="edition",
15 | name="book",
16 | field=models.ForeignKey(
17 | null=True,
18 | on_delete=django.db.models.deletion.CASCADE,
19 | related_name="editions",
20 | to="sample.book",
21 | ),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/sample/migrations/0024_alter_edition_book.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.13 on 2022-06-24 13:35
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("sample", "0023_alter_edition_book"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name="edition",
15 | name="book",
16 | field=models.ForeignKey(
17 | blank=True,
18 | null=True,
19 | on_delete=django.db.models.deletion.CASCADE,
20 | related_name="editions",
21 | to="sample.book",
22 | ),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/sample/migrations/0025_auto_20221029_0402.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.16 on 2022-10-29 04:02
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("sample", "0024_alter_edition_book"),
9 | ]
10 |
11 | operations = [
12 | migrations.CreateModel(
13 | name="Editor",
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, unique=True)),
25 | ],
26 | ),
27 | migrations.AddField(
28 | model_name="book",
29 | name="editors",
30 | field=models.ManyToManyField(related_name="books", to="sample.Editor"),
31 | ),
32 | ]
33 |
--------------------------------------------------------------------------------
/sample/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tj-django/django-clone/a9d90db6c3f54fb31cd029df117e1eee8f061253/sample/migrations/__init__.py
--------------------------------------------------------------------------------
/sample/models.py:
--------------------------------------------------------------------------------
1 | from uuid import uuid4
2 |
3 | import django
4 | from django.conf import settings
5 | from django.db import models
6 | from django.utils import timezone
7 | from django.utils.translation import gettext as _
8 |
9 | from model_clone.utils import get_unique_default
10 |
11 | if django.VERSION >= (2, 2):
12 | from django.db.models import UniqueConstraint
13 |
14 | from model_clone import CloneMixin
15 | from model_clone.models import CloneModel
16 |
17 |
18 | class Author(CloneModel):
19 | first_name = models.CharField(max_length=200, blank=True, unique=True)
20 | last_name = models.CharField(default="Unknown", max_length=200, blank=True)
21 | age = models.PositiveIntegerField()
22 |
23 | uuid = models.UUIDField(default=uuid4, unique=True, editable=False)
24 |
25 | SEX_CHOICES = [
26 | ("U", "Unknown"),
27 | ("F", "Female"),
28 | ("M", "Male"),
29 | ]
30 | sex = models.CharField(choices=SEX_CHOICES, max_length=1, default="U")
31 | created_by = models.ForeignKey(
32 | settings.AUTH_USER_MODEL,
33 | on_delete=models.PROTECT,
34 | )
35 |
36 | def __str__(self):
37 | return _("{} {}".format(self.first_name, self.last_name))
38 |
39 | @property
40 | def full_name(self):
41 | return "{} {}".format(self.first_name, self.last_name)
42 |
43 | class Meta:
44 | unique_together = (("first_name", "last_name", "sex"),)
45 |
46 |
47 | def get_unique_tag_name():
48 | return get_unique_default(
49 | model=Tag,
50 | fname="name",
51 | value="test-tag",
52 | )
53 |
54 |
55 | class Tag(CloneModel):
56 | name = models.CharField(
57 | max_length=255, default=get_unique_tag_name, unique=django.VERSION < (2, 2)
58 | )
59 |
60 | if django.VERSION >= (2, 2):
61 |
62 | class Meta:
63 | constraints = [
64 | UniqueConstraint(fields=["name"], name="tag_unique_name"),
65 | ]
66 |
67 | def __str__(self):
68 | return _(self.name)
69 |
70 |
71 | class SaleTag(CloneModel):
72 | name = models.CharField(max_length=255, unique=django.VERSION < (2, 2))
73 |
74 | if django.VERSION >= (2, 2):
75 |
76 | class Meta:
77 | constraints = [
78 | UniqueConstraint(fields=["name"], name="sale_tag_unique_name"),
79 | ]
80 |
81 | def __str__(self):
82 | return _(self.name)
83 |
84 |
85 | class Editor(models.Model):
86 | name = models.CharField(max_length=255, unique=True)
87 |
88 | def __str__(self):
89 | return _(self.name)
90 |
91 |
92 | class Book(CloneModel):
93 | name = models.CharField(max_length=2000)
94 | slug = models.SlugField(unique=True)
95 | custom_slug = models.SlugField(default="")
96 | authors = models.ManyToManyField(Author, related_name="books")
97 | editors = models.ManyToManyField(Editor, related_name="books")
98 | created_by = models.ForeignKey(
99 | settings.AUTH_USER_MODEL,
100 | on_delete=models.PROTECT,
101 | )
102 | created_at = models.DateTimeField(auto_now_add=True)
103 | tags = models.ManyToManyField(Tag, through="BookTag")
104 | sale_tags = models.ManyToManyField(SaleTag, through="BookSaleTag")
105 | published_at = models.DateTimeField(null=True, blank=True, default=timezone.now)
106 |
107 | def __str__(self):
108 | return _(self.name)
109 |
110 |
111 | class BookTag(models.Model):
112 | book = models.ForeignKey(Book, on_delete=models.CASCADE)
113 | tag = models.ForeignKey(Tag, on_delete=models.PROTECT)
114 |
115 | class Meta:
116 | unique_together = [
117 | ("book", "tag"),
118 | ]
119 |
120 |
121 | class BookSaleTag(CloneModel):
122 | book = models.ForeignKey(Book, on_delete=models.CASCADE)
123 | sale_tag = models.ForeignKey(SaleTag, on_delete=models.PROTECT)
124 |
125 | class Meta:
126 | unique_together = [
127 | ("book", "sale_tag"),
128 | ]
129 |
130 |
131 | class Page(models.Model):
132 | content = models.CharField(max_length=20000)
133 | book = models.ForeignKey(Book, on_delete=models.CASCADE)
134 | created_at = models.DateTimeField(default=timezone.now)
135 |
136 |
137 | class Edition(CloneModel):
138 | seq = models.PositiveIntegerField()
139 | book = models.ForeignKey(
140 | Book, on_delete=models.CASCADE, null=True, related_name="editions", blank=True
141 | )
142 | created_at = models.DateTimeField(default=timezone.now)
143 |
144 |
145 | class Library(CloneModel):
146 | id = models.UUIDField(primary_key=True, default=uuid4)
147 | name = models.CharField(max_length=100)
148 | user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
149 |
150 | uuid = models.UUIDField(default=uuid4, unique=True, editable=False)
151 |
152 | _clone_fields = ["name"]
153 | _clone_o2o_fields = ["user"]
154 |
155 | class Meta:
156 | verbose_name_plural = _("libraries")
157 |
158 | def __str__(self):
159 | return _(self.name)
160 |
161 |
162 | class Assignment(CloneMixin, models.Model):
163 | created_at = models.DateTimeField(auto_now_add=True, null=True)
164 | updated_at = models.DateTimeField(auto_now=True, null=True)
165 | title = models.CharField(
166 | max_length=100, default="", blank=True, verbose_name=_("Job title")
167 | )
168 | assignment_date = models.DateField(blank=True, null=True)
169 | company = models.ForeignKey(
170 | "sample_company.CompanyDepot",
171 | null=True,
172 | blank=True,
173 | on_delete=models.CASCADE,
174 | verbose_name=_("Company"),
175 | )
176 | applied_drivers = models.ManyToManyField(
177 | "sample_driver.Driver",
178 | blank=True,
179 | verbose_name=_("Driver applications"),
180 | related_name="driver_applications",
181 | )
182 | chosen_drivers = models.ManyToManyField(
183 | "sample_driver.Driver",
184 | blank=True,
185 | verbose_name=_("Chosen drivers"),
186 | )
187 |
188 | contract = models.ForeignKey(
189 | "sample_assignment.Contract",
190 | null=True,
191 | blank=True,
192 | on_delete=models.SET_NULL,
193 | verbose_name=_("Choose contract"),
194 | related_name="assignment_contracts",
195 | )
196 |
197 | ASSIGNMENT_STATUS = [
198 | (1, "Complete"),
199 | (2, "Incomplete"),
200 | ]
201 |
202 | assignment_status = models.CharField(
203 | max_length=2,
204 | choices=ASSIGNMENT_STATUS,
205 | default="O",
206 | verbose_name=_("Assignment status"),
207 | blank=True,
208 | )
209 | location = models.CharField(max_length=25, default="", verbose_name=_("Location"))
210 |
211 | DRIVER_TYPES = [
212 | (1, "Commercial"),
213 | (2, "Residential"),
214 | ]
215 |
216 | driver_type = models.CharField(
217 | max_length=2, choices=DRIVER_TYPES, default="", verbose_name=_("Driver type")
218 | )
219 | CAR_TYPES = [
220 | (1, "Large"),
221 | (2, "Small"),
222 | ]
223 | car_type = models.CharField(
224 | max_length=2, choices=CAR_TYPES, default="", verbose_name=_("Car type")
225 | )
226 | compensation = models.DecimalField(
227 | null=True, max_digits=5, decimal_places=2, verbose_name=_("Compensation")
228 | )
229 | hours = models.IntegerField(verbose_name=_("Amount of hours"))
230 | spots_available = models.IntegerField(null=True, verbose_name=_("Spots available"))
231 | description = models.TextField(blank=True, verbose_name=_("Assignment description"))
232 |
233 | # Model clone settings
234 | _clone_excluded_m2m_fields = ["applied_drivers", "chosen_drivers"]
235 |
236 | class Meta:
237 | verbose_name = _("Assigment")
238 | verbose_name_plural = _("Assignments")
239 |
240 | def __str__(self):
241 | return self.title
242 |
243 |
244 | class House(CloneMixin, models.Model):
245 | name = models.CharField(max_length=255)
246 |
247 | _clone_fields = ["name"]
248 | _clone_m2o_or_o2m_fields = ["rooms"]
249 |
250 | def __str__(self):
251 | return self.name
252 |
253 |
254 | class Room(CloneMixin, models.Model):
255 | name = models.CharField(max_length=255)
256 |
257 | house = models.ForeignKey(House, related_name="rooms", on_delete=models.CASCADE)
258 |
259 | _clone_fields = ["name"]
260 | _clone_m2o_or_o2m_fields = ["furniture"]
261 |
262 | def __str__(self):
263 | return self.name
264 |
265 |
266 | class Furniture(CloneMixin, models.Model):
267 | name = models.CharField(max_length=255)
268 |
269 | room = models.ForeignKey(Room, related_name="furniture", on_delete=models.PROTECT)
270 |
271 | _clone_fields = ["name"]
272 |
273 | def __str__(self):
274 | return self.name
275 |
276 |
277 | class Cover(CloneModel):
278 | content = models.CharField(max_length=200)
279 | book = models.OneToOneField(Book, on_delete=models.CASCADE)
280 |
281 |
282 | class BackCover(models.Model):
283 | content = models.CharField(max_length=200)
284 | book = models.OneToOneField(Book, on_delete=models.CASCADE)
285 |
286 |
287 | class Product(CloneMixin, models.Model):
288 | name = models.TextField(unique=True)
289 |
290 |
291 | class Sentence(CloneMixin, models.Model):
292 | value = models.TextField()
293 |
294 | _clone_o2o_fields = {"ending"}
295 |
296 |
297 | class Ending(CloneMixin, models.Model):
298 | sentence = models.OneToOneField(
299 | Sentence, on_delete=models.CASCADE, related_name="ending"
300 | )
301 |
--------------------------------------------------------------------------------
/sample/signals.py:
--------------------------------------------------------------------------------
1 | from django.dispatch import receiver
2 | from django.utils import timezone
3 |
4 | from model_clone.signals import post_clone_save, pre_clone_save
5 |
6 | from .models import Edition
7 |
8 |
9 | @receiver(pre_clone_save, sender=Edition)
10 | def increase_seq(sender, instance, **kwargs):
11 | instance.seq += 1
12 |
13 |
14 | @receiver(post_clone_save, sender=Edition)
15 | def update_book_published_at(sender, instance, **kwargs):
16 | if instance.book:
17 | instance.book.published_at = timezone.now()
18 | instance.book.save(update_fields=["published_at"])
19 |
--------------------------------------------------------------------------------
/sample/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.views.generic import RedirectView
3 |
4 | try:
5 | from django.urls import re_path as path
6 | except ImportError:
7 | from django.conf.urls import url as path
8 |
9 | app_name = "sample"
10 |
11 | urlpatterns = [
12 | path("^admin/", admin.site.urls, name="admin"),
13 | path("", RedirectView.as_view(pattern_name="admin:index")),
14 | ]
15 |
--------------------------------------------------------------------------------
/sample_assignment/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tj-django/django-clone/a9d90db6c3f54fb31cd029df117e1eee8f061253/sample_assignment/__init__.py
--------------------------------------------------------------------------------
/sample_assignment/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from sample_assignment.models import Contract
4 |
5 |
6 | @admin.register(Contract)
7 | class ContractAdmin(admin.ModelAdmin):
8 | pass
9 |
--------------------------------------------------------------------------------
/sample_assignment/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class AssignmentConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "sample_assignment"
7 |
--------------------------------------------------------------------------------
/sample_assignment/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.6 on 2020-05-16 17:33
2 |
3 | from django.db import migrations, models
4 |
5 | import model_clone.mixin
6 |
7 |
8 | class Migration(migrations.Migration):
9 | initial = True
10 |
11 | dependencies = []
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name="Contract",
16 | fields=[
17 | (
18 | "id",
19 | models.AutoField(
20 | auto_created=True,
21 | primary_key=True,
22 | serialize=False,
23 | verbose_name="ID",
24 | ),
25 | ),
26 | ("title", models.CharField(max_length=255)),
27 | ],
28 | bases=(model_clone.mixin.CloneMixin, models.Model),
29 | ),
30 | ]
31 |
--------------------------------------------------------------------------------
/sample_assignment/migrations/0002_alter_contract_id.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-04-22 14:49
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("sample_assignment", "0001_initial"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="contract",
14 | name="id",
15 | field=models.BigAutoField(
16 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
17 | ),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/sample_assignment/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tj-django/django-clone/a9d90db6c3f54fb31cd029df117e1eee8f061253/sample_assignment/migrations/__init__.py
--------------------------------------------------------------------------------
/sample_assignment/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 |
4 | class Contract(models.Model):
5 | title = models.CharField(max_length=255)
6 |
7 | def __str__(self):
8 | return self.title
9 |
--------------------------------------------------------------------------------
/sample_company/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tj-django/django-clone/a9d90db6c3f54fb31cd029df117e1eee8f061253/sample_company/__init__.py
--------------------------------------------------------------------------------
/sample_company/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class CompanyConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "sample_company"
7 |
--------------------------------------------------------------------------------
/sample_company/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.6 on 2020-05-16 17:33
2 |
3 | from django.db import migrations, models
4 |
5 | import model_clone.mixin
6 |
7 |
8 | class Migration(migrations.Migration):
9 | initial = True
10 |
11 | dependencies = []
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name="CompanyDepot",
16 | fields=[
17 | (
18 | "id",
19 | models.AutoField(
20 | auto_created=True,
21 | primary_key=True,
22 | serialize=False,
23 | verbose_name="ID",
24 | ),
25 | ),
26 | ("name", models.CharField(max_length=255)),
27 | ],
28 | bases=(model_clone.mixin.CloneMixin, models.Model),
29 | ),
30 | ]
31 |
--------------------------------------------------------------------------------
/sample_company/migrations/0002_alter_companydepot_id.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-04-22 14:49
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("sample_company", "0001_initial"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="companydepot",
14 | name="id",
15 | field=models.BigAutoField(
16 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
17 | ),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/sample_company/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tj-django/django-clone/a9d90db6c3f54fb31cd029df117e1eee8f061253/sample_company/migrations/__init__.py
--------------------------------------------------------------------------------
/sample_company/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from model_clone import CloneMixin
4 |
5 |
6 | class CompanyDepot(CloneMixin, models.Model):
7 | name = models.CharField(max_length=255)
8 |
--------------------------------------------------------------------------------
/sample_driver/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tj-django/django-clone/a9d90db6c3f54fb31cd029df117e1eee8f061253/sample_driver/__init__.py
--------------------------------------------------------------------------------
/sample_driver/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class DriverConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "sample_driver"
7 |
--------------------------------------------------------------------------------
/sample_driver/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.6 on 2020-05-16 17:33
2 |
3 | from django.db import migrations, models
4 |
5 | import model_clone.mixin
6 |
7 |
8 | class Migration(migrations.Migration):
9 | initial = True
10 |
11 | dependencies = []
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name="Driver",
16 | fields=[
17 | (
18 | "id",
19 | models.AutoField(
20 | auto_created=True,
21 | primary_key=True,
22 | serialize=False,
23 | verbose_name="ID",
24 | ),
25 | ),
26 | ("name", models.CharField(max_length=255)),
27 | ("age", models.SmallIntegerField()),
28 | ],
29 | bases=(model_clone.mixin.CloneMixin, models.Model),
30 | ),
31 | ]
32 |
--------------------------------------------------------------------------------
/sample_driver/migrations/0002_alter_driver_id.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-04-22 14:49
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("sample_driver", "0001_initial"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="driver",
14 | name="id",
15 | field=models.BigAutoField(
16 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
17 | ),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/sample_driver/migrations/0003_driverflag_driver_flags.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.5 on 2023-01-27 19:49
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("sample_driver", "0002_alter_driver_id"),
9 | ]
10 |
11 | operations = [
12 | migrations.CreateModel(
13 | name="DriverFlag",
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, unique=True)),
25 | ],
26 | ),
27 | migrations.AddField(
28 | model_name="driver",
29 | name="flags",
30 | field=models.ManyToManyField(to="sample_driver.driverflag"),
31 | ),
32 | ]
33 |
--------------------------------------------------------------------------------
/sample_driver/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tj-django/django-clone/a9d90db6c3f54fb31cd029df117e1eee8f061253/sample_driver/migrations/__init__.py
--------------------------------------------------------------------------------
/sample_driver/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from model_clone import CloneMixin
4 |
5 |
6 | class DriverFlag(models.Model):
7 | name = models.CharField(max_length=255, unique=True)
8 |
9 | def __str__(self):
10 | return self.name
11 |
12 |
13 | class Driver(CloneMixin, models.Model):
14 | name = models.CharField(max_length=255)
15 | age = models.SmallIntegerField()
16 | flags = models.ManyToManyField(DriverFlag)
17 |
18 | _clone_linked_m2m_fields = ["flags"]
19 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude =
3 | .tox,
4 | sample/migrations,
5 | sample/views.py,
6 | sample/urls.py,
7 | django_clone/settings.py,
8 | manage.py,
9 | venv,
10 | max-line-length = 100
11 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import io
2 | import os
3 |
4 | from setuptools import find_namespace_packages, setup
5 |
6 | install_requires = [
7 | "django",
8 | "conditional>=1.3",
9 | "six",
10 | ]
11 |
12 | test_requires = [
13 | "tox",
14 | "tox-gh-actions",
15 | "pluggy>=0.7",
16 | "mock",
17 | "unittest-xml-reporting",
18 | "codacy-coverage",
19 | "django-migration-fixer",
20 | ]
21 |
22 | deploy_requires = [
23 | "bump2version",
24 | "readme_renderer[md]",
25 | "git-changelog",
26 | ]
27 |
28 |
29 | local_dev_requires = [
30 | "pip-tools",
31 | "check-manifest",
32 | ]
33 |
34 | extras_require = {
35 | "development": [
36 | local_dev_requires,
37 | install_requires,
38 | test_requires,
39 | deploy_requires,
40 | ],
41 | "test": test_requires,
42 | "deploy": deploy_requires,
43 | }
44 |
45 | BASE_DIR = os.path.dirname(__file__)
46 | README_PATH = os.path.join(BASE_DIR, "README.md")
47 | LONG_DESCRIPTION_TYPE = "text/markdown"
48 |
49 | if os.path.isfile(README_PATH):
50 | with io.open(README_PATH, encoding="utf-8") as f:
51 | LONG_DESCRIPTION = f.read()
52 | else:
53 | LONG_DESCRIPTION = ""
54 |
55 | setup(
56 | name="django-clone",
57 | version="5.3.3",
58 | description="Create a clone of a django model instance.",
59 | python_requires=">=3.6",
60 | long_description=LONG_DESCRIPTION,
61 | long_description_content_type=LONG_DESCRIPTION_TYPE,
62 | author="Tonye Jack",
63 | author_email="jtonye@ymail.com",
64 | maintainer="Tonye Jack",
65 | maintainer_email="jtonye@ymail.com",
66 | url="https://github.com/tj-django/django-clone.git",
67 | license="MIT/Apache-2.0",
68 | zip_safe=False,
69 | include_package_data=True,
70 | keywords=[
71 | "django",
72 | "django-clone",
73 | "django clone",
74 | "django object clone",
75 | "clone-django",
76 | "model cloning",
77 | "django instance duplication",
78 | "django duplication",
79 | ],
80 | classifiers=[
81 | "Development Status :: 5 - Production/Stable",
82 | "Intended Audience :: Developers",
83 | "License :: OSI Approved :: MIT License",
84 | "License :: OSI Approved :: Apache Software License",
85 | "Natural Language :: English",
86 | "Topic :: Internet :: WWW/HTTP",
87 | "Operating System :: OS Independent",
88 | "Programming Language :: Python :: 3.6",
89 | "Programming Language :: Python :: 3.7",
90 | "Programming Language :: Python :: 3.8",
91 | "Programming Language :: Python :: 3.9",
92 | "Programming Language :: Python :: 3.10",
93 | "Programming Language :: Python :: 3.11",
94 | "Programming Language :: Python :: Implementation :: CPython",
95 | "Programming Language :: Python :: Implementation :: PyPy",
96 | "Framework :: Django :: 2.0",
97 | "Framework :: Django :: 2.1",
98 | "Framework :: Django :: 2.2",
99 | "Framework :: Django :: 3.0",
100 | "Framework :: Django :: 3.1",
101 | "Framework :: Django :: 3.2",
102 | "Framework :: Django :: 4.0",
103 | "Framework :: Django :: 4.1",
104 | "Framework :: Django :: 4.2",
105 | ],
106 | install_requires=install_requires,
107 | tests_require=["coverage"],
108 | extras_require=extras_require,
109 | packages=find_namespace_packages(
110 | include=[
111 | "model_clone.templates.admin",
112 | "model_clone",
113 | "model_clone.templates.clone",
114 | ],
115 | ),
116 | )
117 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | minversion = 3.8.0
3 | skipsdist = false
4 | envlist =
5 | py36-django{20,21,22,30,31,32,main}-{linux,macos,windows}
6 | py37-django{20,21,22,30,31,32,main}-{linux,macos,windows}
7 | py38-django{21,22,30,31,32,40,41,42,main}-{linux,macos,windows}
8 | py39-django{21,22,30,31,32,40,41,42,main}-{linux,macos,windows}
9 | py310-django{22,30,31,32,40,41,42,main}-{linux,macos,windows}
10 | py311-django{22,30,31,32,40,41,42,main}-{linux}
11 | skip_missing_interpreters = true
12 |
13 | [gh-actions]
14 | python =
15 | 3.6: py36
16 | 3.7: py37
17 | 3.8: py38
18 | 3.9: py39
19 | 3.10: py310
20 | 3.11: py311
21 |
22 | [gh-actions:env]
23 | PLATFORM =
24 | ubuntu-latest: linux
25 | macos-latest: macos
26 | windows-latest: windows
27 |
28 | [testenv]
29 | whitelist_externals = make
30 | setenv = DJANGO_SETTINGS_MODULE=django_clone.settings
31 | passenv = *
32 | extras =
33 | development
34 | test
35 | deps =
36 | django20: Django>=2.0,<2.1
37 | django21: Django>=2.1,<2.2
38 | django22: Django>=2.2,<2.3
39 | django30: Django>=3.0,<3.1
40 | django31: Django>=3.1,<3.2
41 | django32: Django>=3.2,<3.3
42 | django40: Django>=4.0,<4.1
43 | django41: Django>=4.1,<4.2
44 | django42: Django>=4.2,<4.3
45 | main: https://github.com/django/django/archive/main.tar.gz
46 | mock
47 | coverage
48 | pytest-django
49 | codacy-coverage
50 | usedevelop = true
51 | commands =
52 | coverage run manage.py test
53 | coverage xml
54 | - python-codacy-coverage -r coverage.xml
55 |
--------------------------------------------------------------------------------