{"id":19660657,"url":"https://github.com/openwisp/django-swappable-models","last_synced_at":"2025-12-11T23:10:23.741Z","repository":{"id":13072626,"uuid":"15753348","full_name":"openwisp/django-swappable-models","owner":"openwisp","description":"Swapper - The unofficial Django swappable models API. Maintained by the OpenWISP project.","archived":false,"fork":false,"pushed_at":"2025-12-01T04:22:49.000Z","size":91,"stargazers_count":245,"open_issues_count":1,"forks_count":31,"subscribers_count":19,"default_branch":"master","last_synced_at":"2025-12-11T00:38:56.718Z","etag":null,"topics":["django","django-models","foreign-keys","migrations","reusable-app","swappable-models","swapper"],"latest_commit_sha":null,"homepage":"http://openwisp.org","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/openwisp.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGES.rst","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["openwisp"],"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"lfx_crowdfunding":null,"polar":null,"buy_me_a_coffee":null,"thanks_dev":null,"custom":["https://openwisp.org/sponsorship/"]}},"created_at":"2014-01-09T01:21:15.000Z","updated_at":"2025-12-07T15:48:30.000Z","dependencies_parsed_at":"2024-11-29T03:12:10.792Z","dependency_job_id":"18bc4942-bc16-4562-8d43-0bdaf57972cf","html_url":"https://github.com/openwisp/django-swappable-models","commit_stats":{"total_commits":88,"total_committers":13,"mean_commits":6.769230769230769,"dds":"0.30681818181818177","last_synced_commit":"bae85fa332834a69fdbd2cf291a5b2595e6999af"},"previous_names":["wq/django-swappable-models"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/openwisp/django-swappable-models","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openwisp%2Fdjango-swappable-models","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openwisp%2Fdjango-swappable-models/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openwisp%2Fdjango-swappable-models/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openwisp%2Fdjango-swappable-models/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/openwisp","download_url":"https://codeload.github.com/openwisp/django-swappable-models/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openwisp%2Fdjango-swappable-models/sbom","scorecard":{"id":710801,"data":{"date":"2025-08-11","repo":{"name":"github.com/openwisp/django-swappable-models","commit":"7b38f9c4d8e54ad39d1a8689091bc0f9857bd31f"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":5,"checks":[{"name":"Code-Review","score":6,"reason":"Found 16/24 approved changesets -- score normalized to 6","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/ci.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:26: update your workflow using https://app.stepsecurity.io/secureworkflow/openwisp/django-swappable-models/ci.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:31: update your workflow using https://app.stepsecurity.io/secureworkflow/openwisp/django-swappable-models/ci.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pypi.yml:20: update your workflow using https://app.stepsecurity.io/secureworkflow/openwisp/django-swappable-models/pypi.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pypi.yml:22: update your workflow using https://app.stepsecurity.io/secureworkflow/openwisp/django-swappable-models/pypi.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/pypi.yml:32: update your workflow using https://app.stepsecurity.io/secureworkflow/openwisp/django-swappable-models/pypi.yml/master?enable=pin","Warn: pipCommand not pinned by hash: .github/workflows/ci.yml:38","Warn: pipCommand not pinned by hash: .github/workflows/ci.yml:39","Warn: pipCommand not pinned by hash: .github/workflows/ci.yml:40","Warn: pipCommand not pinned by hash: .github/workflows/pypi.yml:27","Warn: pipCommand not pinned by hash: .github/workflows/pypi.yml:28","Info:   0 out of   4 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   1 third-party GitHubAction dependencies pinned","Info:   0 out of   5 pipCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Packaging","score":10,"reason":"packaging workflow detected","details":["Info: Project packages its releases by way of GitHub Actions.: .github/workflows/pypi.yml:11"],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"SAST","score":4,"reason":"SAST tool is not run on all commits -- score normalized to 4","details":["Warn: 11 commits out of 24 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-22T08:09:57.535Z","repository_id":13072626,"created_at":"2025-08-22T08:09:57.535Z","updated_at":"2025-08-22T08:09:57.535Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":27654710,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-11T02:00:11.302Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["django","django-models","foreign-keys","migrations","reusable-app","swappable-models","swapper"],"created_at":"2024-11-11T16:04:47.501Z","updated_at":"2025-12-11T23:10:23.713Z","avatar_url":"https://github.com/openwisp.png","language":"Python","funding_links":["https://github.com/sponsors/openwisp","https://openwisp.org/sponsorship/"],"categories":[],"sub_categories":[],"readme":"# Swapper\n\n[![Dependency monitoring](https://img.shields.io/librariesio/release/github/openwisp/django-swappable-models)](https://libraries.io/github/openwisp/django-swappable-models)\n\n#### Django Swappable Models - No longer only for auth.User!\n\nSwapper is an unofficial API for the [undocumented] but very powerful Django\nfeature: swappable models. Swapper facilitates implementing\narbitrary swappable models in your own reusable apps.\n\n[![Build Status](https://github.com/openwisp/django-swappable-models/actions/workflows/ci.yml/badge.svg)](https://github.com/openwisp/django-swappable-models/actions/workflows/ci.yml)\n[![Latest PyPI Release](https://img.shields.io/pypi/v/swapper.svg)](https://pypi.org/project/swapper)\n[![Release Notes](https://img.shields.io/github/release/openwisp/django-swappable-models.svg)](https://github.com/wq/django-swappable-models/releases)\n[![License](https://img.shields.io/pypi/l/swapper.svg)](https://github.com/openwisp/django-swappable-models/blob/master/LICENSE)\n[![GitHub Stars](https://img.shields.io/github/stars/openwisp/django-swappable-models.svg)](https://github.com/openwisp/django-swappable-models/stargazers)\n[![GitHub Forks](https://img.shields.io/github/forks/openwisp/django-swappable-models.svg)](https://github.com/openwisp/django-swappable-models/network)\n\n## Motivation\n\nSuppose your reusable app has two related tables:\n\n```python\nfrom django.db import models\nclass Parent(models.Model):\n    name = models.TextField()\n\nclass Child(models.Model):\n    name = models.TextField()\n    parent = models.ForeignKey(Parent)\n```\n\nSuppose further that you want to allow the user to subclass either or both of\nthese models and supplement them with their own additional fields. You could use\nAbstract classes (e.g. `BaseParent` and `BaseChild`) for this, but then you\nwould either need to:\n\n1.  Avoid putting the foreign key on `BaseChild` and tell the user they need to\n    do it.\n2.  Put the foreign key on `BaseChild`, but make `Parent` a concrete model that\n    can't be swapped\n3.  Use swappable models, together with `ForeignKeys` that read the swappable\n    settings.\n\nThis third approach is taken by Django to facilitate [swapping the auth.User model]. The `auth.User` swappable code was implemented in a generic way that allows it to be used for any model. Although this capability is currently [undocumented] while any remaining issues are being sorted out, it has proven to be very stable and useful in our experience.\n\nSwapper is essentially a simple API wrapper around this existing functionality. Note that Swapper is primarily a tool for library authors; users of your reusable app generally should not need to know about Swapper in order to use it. (See the notes on [End User Documentation](#end-user-documentation) below.)\n\n### Real-World Examples\n\nSwapper is used extensively in several OpenWISP packages to facilitate customization and extension. Notable examples include:\n\n- [openwisp-users]\n- [openwisp-controller]\n- [openwisp-radius]\n\nThe use of swapper in these packages promotes [Software Reusability][reusability], one of the core values of the OpenWISP project.\n\n## Creating a Reusable App\n\nFirst, make sure you have `swapper` installed. If you are publishing your reusable app as a Python package, be sure to add `swapper` to your project's dependencies (e.g. `setup.py`) to ensure that users of your app don't have errors integrating it.\n\n```bash\npip3 install swapper\n```\n\nExtending the above example, you might create two abstract base classes and corresponding default implementations:\n\n```python\n# reusableapp/models.py\nfrom django.db import models\nimport swapper\n\nclass BaseParent(models.Model):\n    # minimal base implementation ...\n    class Meta:\n        abstract = True\n\nclass Parent(BaseParent):\n    # default (swappable) implementation ...\n    class Meta:\n       swappable = swapper.swappable_setting('reusableapp', 'Parent')\n\nclass BaseChild(models.Model):\n    parent = models.ForeignKey(swapper.get_model_name('reusableapp', 'Parent'))\n    # minimal base implementation ...\n    class Meta:\n        abstract = True\n\nclass Child(BaseChild):\n    # default (swappable) implementation ...\n    class Meta:\n       swappable = swapper.swappable_setting('reusableapp', 'Child')\n```\n\n### Loading Swapped Models\n\nIn your reusable views and other functions, always use the swapper instead of importing swappable models directly. This is because you might not know whether the user of your app is using your default implementation or their own version.\n\n```python\n# reusableapp/views.py\n\n# Might work, might not\n# from .models import Parent\n\nimport swapper\nParent = swapper.load_model(\"reusableapp\", \"Parent\")\nChild = swapper.load_model(\"reusableapp\", \"Child\")\n\ndef view(request, *args, **kwargs):\n    qs = Parent.objects.all()\n    # ...\n```\n\n\u003e Note: `swapper.load_model()` is the general equivalent of [get_user_model()] and subject to the same constraints: e.g. it should not be used until after the model system has fully initialized.\n\n### Migration Scripts\n\nSwapper can also be used in migration scripts to facilitate dependency ordering and foreign key references. To use this feature in your library, generate a migration script with `makemigrations` and make the following changes. In general, users of your library should not need to make any similar changes to their own migration scripts. The one exception is if you have multiple levels of swappable models with foreign keys pointing to each other.\n\n```diff\n  # reusableapp/migrations/0001_initial.py\n\n  from django.db import models, migrations\n\u003c from django.conf import settings\n\u003e import swapper\n\n  class Migration(migrations.Migration):\n\n      dependencies = [\n\u003c          migrations.swappable_dependency(settings.REUSABLEAPP_PARENT_MODEL),\n\u003e          swapper.dependency('reusableapp', 'Parent')\n      ]\n\n      operations = [\n          migrations.CreateModel(\n              name='Child',\n              fields=[\n                  ('id', models.AutoField(auto_created=True, serialize=False, primary_key=True, verbose_name='ID')),\n              ],\n              options={\n\u003c                 'swappable': 'REUSABLEAPP_CHILD_MODEL',\n\u003e                 'swappable': swapper.swappable_setting('reusableapp', 'Child'),\n              },\n              bases=(models.Model,),\n          ),\n          migrations.CreateModel(\n              name='Parent',\n              fields=[\n                  ('id', models.AutoField(auto_created=True, serialize=False, primary_key=True, verbose_name='ID')),\n              ],\n              options={\n\u003c                 'swappable': 'REUSABLEAPP_PARENT_MODEL',\n\u003e                 'swappable': swapper.swappable_setting('reusableapp', 'Parent'),\n              },\n              bases=(models.Model,),\n          ),\n          migrations.AddField(\n              model_name='child',\n              name='parent',\n\u003c             field=models.ForeignKey(to=settings.REUSABLEAPP_PARENT_MODEL),\n\u003e             field=models.ForeignKey(to=swapper.get_model_name('reusableapp', 'Parent')),\n              preserve_default=True,\n          ),\n      ]\n```\n\n## End User Documentation\n\nWith the above setup, the user of your app can override one or both models in their own app. You might provide them with an example like this:\n\n```python\n# myapp/models.py\nfrom reusableapp.models import BaseParent\nclass Parent(BaseParent):\n    # custom implementation ...\n```\n\nThen, tell your users to update their settings to trigger the swap.\n\n```python\n# myproject/settings.py\nREUSABLEAPP_PARENT_MODEL = \"myapp.Parent\"\n```\n\nThe goal is to make this process just as easy for your end user as [swapping the auth.User model] is. As with `auth.User`, there are some important caveats that you may want to inform your users about.\n\nThe biggest issue is that your users will probably need to define the swapped model settings **before creating any migrations** for their implementation of `myapp`. Due to key assumptions made within Django's migration infrastructure, it is difficult to start out with a default (non-swapped) model and then later to switch to a swapped implementation without doing some migration hacking. This is somewhat awkward - as your users will most likely want to try out your default implementation before deciding to customize it. Unfortunately, there isn't an easy workaround due to how the swappable setting is currently implemented in Django core. This will likely be addressed in future Django versions (see [#10] and [Django ticket #25313]).\n\n## API Documentation\n\nHere is the full API for `swapper`, which you may find useful in creating your reusable app code. End users of your library should generally not need to reference this API.\n\n| function                                      | purpose                                                                                                                                                                                                                                                                                                                                                                        |\n| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `swappable_setting(app_label, model)`         | Generates a swappable setting name for the provided model (e.g. `\"REUSABLEAPP_PARENT_MODEL\"`)                                                                                                                                                                                                                                                                                  |\n| `is_swapped(app_label, model)`                | Determines whether or not a given model has been swapped. (Returns the model name if swapped, otherwise `False`)                                                                                                                                                                                                                                                               |\n| `get_model_name(app_label, model)`            | Gets the name of the model the swappable model has been swapped for (or the name of the original model if not swapped.)                                                                                                                                                                                                                                                        |\n| `get_model_names(app_label, models)`          | Match a list of model names to their swapped versions. All of the models should be from the same app (though their swapped versions need not be).                                                                                                                                                                                                                              |\n| `load_model(app_label, model, required=True)` | Load the swapped model class for a swappable model (or the original model if it hasn't been swapped). If your code can function without the specified model, set `required = False`.                                                                                                                                                                                           |\n| `dependency(app_label, model, version=None)`  | Generate a dependency tuple for use in migrations. Use `version` only when depending on the first migration of the target dependency doesn't work (eg: when a specific migration needs to be depended upon), we recommend avoid using `version='__latest__'` because it can have serious [drawbacks] when new migrations are added to the module which is being depended upon. |\n| `set_app_prefix(app_label, prefix)`           | Set a custom prefix for swappable settings (the default is the upper case `app_label`). This can be useful if the app has a long name or is part of a larger framework. This should be set at the top of your models.py.                                                                                                                                                       |\n| `join(app_label, model)`, `split(model)`      | Utilities for splitting and joining `\"app.Model\"` strings and `(\"app\", \"Model\")` tuples.                                                                                                                                                                                                                                                                                       |\n\n[undocumented]: https://code.djangoproject.com/ticket/19103\n[swapping the auth.User model]: https://docs.djangoproject.com/en/4.0/topics/auth/customizing/#auth-custom-user\n[openwisp-users]: https://github.com/openwisp/openwisp-users#extend-openwisp-users\n[openwisp-controller]: https://github.com/openwisp/openwisp-controller#extending-openwisp-controller\n[openwisp-radius]: https://openwisp-radius.readthedocs.io/en/latest/developer/how_to_extend.html\n[reusability]: https://openwisp.io/docs/general/values.html#software-reusability-means-long-term-sustainability\n[get_user_model()]: https://docs.djangoproject.com/en/4.0/topics/auth/customizing/#referencing-the-user-model\n[#10]: https://github.com/openwisp/django-swappable-models/issues/10\n[Django ticket #25313]: https://code.djangoproject.com/ticket/25313\n[drawbacks]: https://code.djangoproject.com/ticket/23071\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopenwisp%2Fdjango-swappable-models","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fopenwisp%2Fdjango-swappable-models","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopenwisp%2Fdjango-swappable-models/lists"}