diff --git a/.coverage b/.coverage
deleted file mode 100644
index c4a0845c..00000000
Binary files a/.coverage and /dev/null differ
diff --git a/.github/workflows/django-multitenant-tests.yml b/.github/workflows/django-multitenant-tests.yml
index 5d8ab7f2..3e6b20ce 100644
--- a/.github/workflows/django-multitenant-tests.yml
+++ b/.github/workflows/django-multitenant-tests.yml
@@ -21,6 +21,11 @@ jobs:
- name: Prospector checks
run: |
make lint
+
+ - name: Documentation Checks
+ run: |
+ cd docs
+ sphinx-build -W -b html source builds
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..795c6182
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,74 @@
+### Django-Multitenant v3.1.0(March 1, 2023) ###
+
+* Adds support for Django 4.1
+
+* Adds support for setting tenant automatically for ManyToMany related model
+
+* Fixes invalid error message problem in case of invalid field name
+
+* Adds support for getting models using apps.get_model
+
+* Removes reserved tenant_id limitation by introducing TenantMeta usage
+
+* Introduces ReadTheDocs documentation
+
+### Django-Multitenant v3.0.0(December 8, 2021) ###
+
+* Adds support for Django 4.0
+
+* Drops support for the following EOLed Django and Python versions:
+ 1. Python 2.7
+ 2. Django 1.11
+ 3. Django 3.1
+
+### Django-Multitenant v2.4.0(November 11, 2021) ###
+
+* Backwards migration for `Distribute` migration using `undistribute_table()`
+
+* Adds tests for Django 3.2 and Python 3.9
+
+* Fixes migrations on Django 3.0+
+
+* Fixes aggregations using `annotate`
+
+### Django-Multitenant v2.0.9 (May 18, 2019) ###
+
+* Fixes the process of running old migrations when the model has been deleted from the code.
+
+### Django-Multitenant v2.0.8 (May 18, 2019) ###
+
+* Add tests to confirm the join condition in subqueries includes tenant column.
+
+### Django-Multitenant v2.0.7 (May 18, 2019) ###
+
+* Fixes create with current tenant
+
+### Django-Multitenant v2.0.6 (May 18, 2019) ###
+
+* Fix recursive loop in warning for fields when joining without current_tenant set
+
+### Django-Multitenant v2.0.5 (May 18, 2019) ###
+
+* Adds support for custom query_set in TenantManager
+
+* Cleans the delete code to ensure deleting rows only related to current tenant
+
+### Django-Multitenant v2.0.4 (May 18, 2019) ###
+
+* Adds support for multiple tenant
+
+### Django-Multitenant v1.1.0 (January 26, 2018) ###
+
+* Add TenantForeignKey to emulate composite foreign keys between tenant related models.
+
+* Split apart library into multiple files. Importing the utility function `get_current_tenant` would cause errors due to the import statement triggering evaluation of the TenantModel class. This would cause problems if TenantModel were evaluated before the database backend was initialized.
+
+* Added a simple TenantOneToOneField which does not try to enforce a uniqueness constraint on the ID column, but preserves all the relationship semantics of using a traditional OneToOneField in Django.
+
+* Overrode Django's DatabaseSchemaEditor to produce foreign key constraints on composite foreign keys consisting of both the ID and tenant ID columns for any foreign key between TenantModels
+
+* Monkey-patched Django's DeleteQuery implementation to include tenant_id in its SQL queries.
+
+### Django-Multitenant v1.0.1 (November 7, 2017) ###
+
+* Some bug fixes.
\ No newline at end of file
diff --git a/README.md b/README.md
index 57dba8b6..bd96a42c 100644
--- a/README.md
+++ b/README.md
@@ -39,32 +39,44 @@ In order to use this library you can either use Mixins or have your models inher
from django_multitenant.fields import *
from django_multitenant.models import *
```
-1. All models should inherit the TenantModel class.
+2. All models should inherit the TenantModel class.
`Ex: class Product(TenantModel):`
-1. Define a static variable named tenant_id and specify the tenant column using this variable.
- `Ex: tenant_id='store_id'`
-1. All foreign keys to TenantModel subclasses should use TenantForeignKey in place of
+3. Define a static variable named tenant_id and specify the tenant column using this variable.You can define tenant_id in three ways. Any of them is acceptavle
+ * Using TenantMeta.tenant_field_name variable
+ * Using TenantMeta.tenant_id variable
+ * Using tenant_id field
+
+
+
+ > **Warning**
+ > Using tenant_id field directly in the class is not suggested since it may cause collision if class has a field named with 'tenant'
+
+
+4. All foreign keys to TenantModel subclasses should use TenantForeignKey in place of
models.ForeignKey
-1. A sample model implementing the above 2 steps:
- ```python
- class Store(TenantModel):
- tenant_id = 'id'
- name = models.CharField(max_length=50)
- address = models.CharField(max_length=255)
- email = models.CharField(max_length=50)
-
- class Product(TenantModel):
- store = models.ForeignKey(Store)
- tenant_id='store_id'
- name = models.CharField(max_length=255)
- description = models.TextField()
- class Meta(object):
- unique_together = ["id", "store"]
- class Purchase(TenantModel):
- store = models.ForeignKey(Store)
- tenant_id='store_id'
- product_purchased = TenantForeignKey(Product)
- ```
+5. A sample model implementing the above 2 steps:
+ ```python
+ class Store(TenantModel):
+ name = models.CharField(max_length=50)
+ address = models.CharField(max_length=255)
+ email = models.CharField(max_length=50)
+ class TenantMeta:
+ tenant_field_name = "id"
+
+ class Product(TenantModel):
+ store = models.ForeignKey(Store)
+ name = models.CharField(max_length=255)
+ description = models.TextField()
+ class Meta:
+ unique_together = ["id", "store"]
+ class TenantMeta:
+ tenant_field_name = "store_id"
+ class Purchase(TenantModel):
+ store = models.ForeignKey(Store)
+ product_purchased = TenantForeignKey(Product)
+ class TenantMeta:
+ tenant_field_name = "store_id"
+ ```
### Changes in Models using mixins:
@@ -81,36 +93,36 @@ In order to use this library you can either use Mixins or have your models inher
1. Referenced table in TenenatForeignKey should include a unique key including tenant_id and primary key
```
Ex:
- class Meta(object):
+ class Meta:
unique_together = ["id", "store"]
```
1. A sample model implementing the above 3 steps:
- ```python
-
- class ProductManager(TenantManagerMixin, models.Manager):
- pass
-
- class Product(TenantModelMixin, models.Model):
- store = models.ForeignKey(Store)
- tenant_id='store_id'
- name = models.CharField(max_length=255)
- description = models.TextField()
-
- objects = ProductManager()
-
- class Meta(object):
- unique_together = ["id", "store"]
-
- class PurchaseManager(TenantManagerMixin, models.Manager):
- pass
-
- class Purchase(TenantModelMixin, models.Model):
- store = models.ForeignKey(Store)
- tenant_id='store_id'
- product_purchased = TenantForeignKey(Product)
+ ```python
- objects = PurchaseManager()
- ```
+ class ProductManager(TenantManagerMixin, models.Manager):
+ pass
+
+ class Product(TenantModelMixin, models.Model):
+ store = models.ForeignKey(Store)
+ tenant_id='store_id'
+ name = models.CharField(max_length=255)
+ description = models.TextField()
+
+ objects = ProductManager()
+
+ class Meta:
+ unique_together = ["id", "store"]
+
+ class PurchaseManager(TenantManagerMixin, models.Manager):
+ pass
+
+ class Purchase(TenantModelMixin, models.Model):
+ store = models.ForeignKey(Store)
+ tenant_id='store_id'
+ product_purchased = TenantForeignKey(Product)
+
+ objects = PurchaseManager()
+ ```
@@ -167,31 +179,31 @@ In order to use this library you can either use Mixins or have your models inher
## Supported APIs:
1. Most of the APIs under Model.objects.*.
1. Model.save() injects tenant_id for tenant inherited models.
- ```python
+ ```python
s=Store.objects.all()[0]
- set_current_tenant(s)
-
- #All the below API calls would add suitable tenant filters.
- #Simple get_queryset()
- Product.objects.get_queryset()
-
- #Simple join
- Purchase.objects.filter(id=1).filter(store__name='The Awesome Store').filter(product__description='All products are awesome')
-
- #Update
- Purchase.objects.filter(id=1).update(id=1)
-
- #Save
- p=Product(8,1,'Awesome Shoe','These shoes are awesome')
- p.save()
-
- #Simple aggregates
- Product.objects.count()
- Product.objects.filter(store__name='The Awesome Store').count()
-
- #Subqueries
- Product.objects.filter(name='Awesome Shoe');
- Purchase.objects.filter(product__in=p);
+ set_current_tenant(s)
+
+ #All the below API calls would add suitable tenant filters.
+ #Simple get_queryset()
+ Product.objects.get_queryset()
+
+ #Simple join
+ Purchase.objects.filter(id=1).filter(store__name='The Awesome Store').filter(product__description='All products are awesome')
+
+ #Update
+ Purchase.objects.filter(id=1).update(id=1)
+
+ #Save
+ p=Product(8,1,'Awesome Shoe','These shoes are awesome')
+ p.save()
+
+ #Simple aggregates
+ Product.objects.count()
+ Product.objects.filter(store__name='The Awesome Store').count()
+
+ #Subqueries
+ Product.objects.filter(name='Awesome Shoe');
+ Purchase.objects.filter(product__in=p);
```
diff --git a/coverage.xml b/coverage.xml
deleted file mode 100644
index 35b80e11..00000000
--- a/coverage.xml
+++ /dev/null
@@ -1,1200 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/django_multitenant/mixins.py b/django_multitenant/mixins.py
index 18832dff..c314d49a 100644
--- a/django_multitenant/mixins.py
+++ b/django_multitenant/mixins.py
@@ -96,7 +96,6 @@ def bulk_create(self, objs, **kwargs):
class TenantModelMixin:
# Abstract model which all the models related to tenant inherit.
- tenant_id = ""
def __init__(self, *args, **kwargs):
# Adds tenant_id filters in the delete and update queries.
@@ -196,7 +195,20 @@ def save(self, *args, **kwargs):
@property
def tenant_field(self):
- return self.tenant_id
+ if hasattr(self, "TenantMeta") and "tenant_field_name" in dir(self.TenantMeta):
+ return self.TenantMeta.tenant_field_name
+ if hasattr(self, "TenantMeta") and "tenant_id" in dir(self.TenantMeta):
+ return self.TenantMeta.tenant_id
+ if hasattr(self, "tenant"):
+ raise AttributeError(
+ "Tenant field exists which may cause collision with tenant_id field. Please rename the tenant field. "
+ )
+ if hasattr(self, "tenant_id"):
+ return self.tenant_id
+
+ raise AttributeError(
+ "tenant_id field not found. Please add tenant_id field to the model."
+ )
@property
def tenant_value(self):
diff --git a/django_multitenant/tests/base.py b/django_multitenant/tests/base.py
index c751aff2..5eac6592 100644
--- a/django_multitenant/tests/base.py
+++ b/django_multitenant/tests/base.py
@@ -160,14 +160,6 @@ def subtasks(self):
return subtasks
- @fixture
- def unscoped(self):
- pass
-
- @fixture
- def aliased_tasks(self):
- pass
-
@fixture
def organization(self):
return Organization.objects.create(name="organization")
diff --git a/django_multitenant/tests/migrations/0024_business_tenant_alter_account_id_and_more.py b/django_multitenant/tests/migrations/0024_business_tenant_alter_account_id_and_more.py
new file mode 100644
index 00000000..c2195611
--- /dev/null
+++ b/django_multitenant/tests/migrations/0024_business_tenant_alter_account_id_and_more.py
@@ -0,0 +1,108 @@
+# Generated by Django 4.1.5 on 2023-02-17 11:14
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django_multitenant.fields
+import django_multitenant.mixins
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("tests", "0023_auto_20200412_0603"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="Business",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("bk_biz_id", models.IntegerField(verbose_name="business ID")),
+ (
+ "bk_biz_name",
+ models.CharField(max_length=100, verbose_name="business name"),
+ ),
+ ],
+ bases=(django_multitenant.mixins.TenantModelMixin, models.Model),
+ ),
+ migrations.CreateModel(
+ name="Tenant",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=100, verbose_name="tenant name")),
+ ],
+ options={
+ "abstract": False,
+ },
+ bases=(django_multitenant.mixins.TenantModelMixin, models.Model),
+ ),
+ migrations.CreateModel(
+ name="Template",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=100, verbose_name="name")),
+ (
+ "business",
+ django_multitenant.fields.TenantForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="tests.business",
+ ),
+ ),
+ (
+ "tenant",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="tests.tenant",
+ ),
+ ),
+ ],
+ options={
+ "abstract": False,
+ },
+ bases=(django_multitenant.mixins.TenantModelMixin, models.Model),
+ ),
+ migrations.AddField(
+ model_name="business",
+ name="tenant",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="tests.tenant",
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name="business",
+ constraint=models.UniqueConstraint(
+ fields=("id", "tenant_id"), name="unique_business_tenant"
+ ),
+ ),
+ ]
diff --git a/django_multitenant/tests/migrations/0024_data_load.py b/django_multitenant/tests/migrations/0025_data_load.py
similarity index 96%
rename from django_multitenant/tests/migrations/0024_data_load.py
rename to django_multitenant/tests/migrations/0025_data_load.py
index 5ba81ca3..04f7524a 100644
--- a/django_multitenant/tests/migrations/0024_data_load.py
+++ b/django_multitenant/tests/migrations/0025_data_load.py
@@ -6,7 +6,7 @@
class Migration(migrations.Migration):
dependencies = [
- ("tests", "0023_auto_20200412_0603"),
+ ("tests", "0024_business_tenant_alter_account_id_and_more"),
]
def forwards_func(apps, schema_editor):
diff --git a/django_multitenant/tests/migrations/0024_product_purchase_store_alter_account_id_and_more.py b/django_multitenant/tests/migrations/0026_product_purchase_store_alter_account_id_and_more.py
similarity index 98%
rename from django_multitenant/tests/migrations/0024_product_purchase_store_alter_account_id_and_more.py
rename to django_multitenant/tests/migrations/0026_product_purchase_store_alter_account_id_and_more.py
index c5cc86e0..d0799038 100644
--- a/django_multitenant/tests/migrations/0024_product_purchase_store_alter_account_id_and_more.py
+++ b/django_multitenant/tests/migrations/0026_product_purchase_store_alter_account_id_and_more.py
@@ -9,7 +9,7 @@
class Migration(migrations.Migration):
dependencies = [
- ("tests", "0023_auto_20200412_0603"),
+ ("tests", "0025_data_load"),
]
operations = [
diff --git a/django_multitenant/tests/migrations/0025_many_to_many_distribute.py b/django_multitenant/tests/migrations/0027_many_to_many_distribute.py
similarity index 96%
rename from django_multitenant/tests/migrations/0025_many_to_many_distribute.py
rename to django_multitenant/tests/migrations/0027_many_to_many_distribute.py
index 6b673fbd..d55ea64f 100644
--- a/django_multitenant/tests/migrations/0025_many_to_many_distribute.py
+++ b/django_multitenant/tests/migrations/0027_many_to_many_distribute.py
@@ -7,7 +7,7 @@
class Migration(migrations.Migration):
dependencies = [
- ("tests", "0024_product_purchase_store_alter_account_id_and_more"),
+ ("tests", "0026_product_purchase_store_alter_account_id_and_more"),
]
operations = []
diff --git a/django_multitenant/tests/models.py b/django_multitenant/tests/models.py
index bd2829af..4a6a0a9f 100644
--- a/django_multitenant/tests/models.py
+++ b/django_multitenant/tests/models.py
@@ -177,7 +177,9 @@ class Revenue(TenantModel):
class Organization(TenantModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=255)
- tenant_id = "id"
+
+ class TenantMeta:
+ tenant_field_name = "id"
class Record(TenantModel):
@@ -185,7 +187,8 @@ class Record(TenantModel):
name = models.CharField(max_length=255)
organization = TenantForeignKey(Organization, on_delete=models.CASCADE)
- tenant_id = "organization_id"
+ class TenantMeta:
+ tenant_id = "organization_id"
class TenantNotIdModel(TenantModel):
@@ -211,6 +214,38 @@ class MigrationTestReferenceModel(models.Model):
name = models.CharField(max_length=255)
+class Tenant(TenantModel):
+ tenant_id = "id"
+ name = models.CharField("tenant name", max_length=100)
+
+
+class Business(TenantModel):
+ tenant = models.ForeignKey(Tenant, blank=True, null=True, on_delete=models.SET_NULL)
+ bk_biz_id = models.IntegerField("business ID")
+ bk_biz_name = models.CharField("business name", max_length=100)
+
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(
+ fields=["id", "tenant_id"], name="unique_business_tenant"
+ )
+ ]
+
+ class TenantMeta:
+ tenant_field_name = "tenant_id"
+
+
+class Template(TenantModel):
+ tenant = models.ForeignKey(Tenant, blank=True, null=True, on_delete=models.SET_NULL)
+ business = TenantForeignKey(
+ Business, blank=True, null=True, on_delete=models.SET_NULL
+ )
+ name = models.CharField("name", max_length=100)
+
+ class TenantMeta:
+ tenant_field_name = "tenant_id"
+
+
class Store(TenantModel):
tenant_id = "id"
name = models.CharField(max_length=50)
diff --git a/django_multitenant/tests/single_node_settings.py b/django_multitenant/tests/single_node_settings.py
deleted file mode 100644
index c3800505..00000000
--- a/django_multitenant/tests/single_node_settings.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from .settings import DATABASES
-
-DATABASES["default"]["PORT"] = 5604
-
-USE_CITUS = False
diff --git a/django_multitenant/tests/test_models.py b/django_multitenant/tests/test_models.py
index 8dfbf872..f493c897 100644
--- a/django_multitenant/tests/test_models.py
+++ b/django_multitenant/tests/test_models.py
@@ -1,8 +1,7 @@
from datetime import date
import re
-
-import django
import pytest
+
from django.conf import settings
from django.db.models import Count
from django.db.utils import NotSupportedError, DataError
@@ -248,10 +247,14 @@ def test_bulk_create_tenant_not_set(self):
with self.assertRaises(DataError):
Project.objects.bulk_create(projects)
+ @pytest.mark.skipif(
+ not settings.USE_CITUS,
+ reason=(
+ """ If table is distributed, we can't update the tenant column.
+ If Citus is not enabled in settings, there is no reason to run this test."""
+ ),
+ )
def test_update_tenant_project(self):
- if not settings.USE_CITUS:
- return
-
from .models import Project
account = self.account_fr
@@ -456,22 +459,6 @@ def test_exclude_tenant_not_set(self):
tasks = Task.objects.exclude(project__isnull=True)
self.assertEqual(tasks.count(), 150)
- @pytest.mark.skipif(
- django.VERSION >= (3, 2),
- reason="Django 3.2 changed the generated query to one that's not supported by Citus",
- )
- def test_exclude_related(self):
- from .models import Project, Manager, ProjectManager
-
- project = self.projects[0]
- project_managers = self.project_managers
- account = project.account
- manager = Manager.objects.create(name="Louise", account=account)
- ProjectManager.objects.create(account=account, project=project, manager=manager)
-
- excluded = Project.objects.exclude(projectmanagers__manager__name="Louise")
- self.assertEqual(excluded.count(), 29)
-
def test_delete_cascade_distributed(self):
from .models import Task, Project, SubTask
@@ -829,3 +816,17 @@ def test_many_to_many_through_saves(self):
purchase = Purchase.objects.create(store=store)
purchase.save()
purchase.product_purchased.add(product, through_defaults={"date": date.today()})
+
+ def test_tenant_id_columns(self):
+ from .models import Template, Tenant, Business
+
+ tenant = Tenant.objects.create(name="tenant")
+ tenant.save()
+ business = Business.objects.create(
+ bk_biz_name="business", bk_biz_id=1, tenant=tenant
+ )
+ business.save()
+ template = Template.objects.create(name="template", business=business)
+ template.save()
+
+ Template.objects.filter(business__tenant=tenant).first()
diff --git a/django_multitenant/tests/urls.py b/django_multitenant/tests/urls.py
deleted file mode 100644
index fb86d9cb..00000000
--- a/django_multitenant/tests/urls.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-import django
-from django.contrib import admin
-
-# django.conf.urls.url is deprecated since Django 3.1, re_path is the
-# replacement:
-# https://docs.djangoproject.com/en/3.1/ref/urls/#url
-if django.VERSION >= (3, 1):
- from django.urls import re_path as url
-else:
- from django.conf.urls import url
-
-
-# TODO
-# Add view to verify objects in context
-
-
-urlpatterns = [
- url(r"^admin/", admin.site.urls),
-]
diff --git a/docs/source/_static/.gitignore b/docs/source/_static/.gitignore
new file mode 100644
index 00000000..5e7d2734
--- /dev/null
+++ b/docs/source/_static/.gitignore
@@ -0,0 +1,4 @@
+# Ignore everything in this directory
+*
+# Except this file
+!.gitignore
diff --git a/docs/source/_templates/.gitignore b/docs/source/_templates/.gitignore
new file mode 100644
index 00000000..5e7d2734
--- /dev/null
+++ b/docs/source/_templates/.gitignore
@@ -0,0 +1,4 @@
+# Ignore everything in this directory
+*
+# Except this file
+!.gitignore
diff --git a/docs/source/migration_mt_django.rst b/docs/source/migration_mt_django.rst
index d08eeb95..e535a9e5 100644
--- a/docs/source/migration_mt_django.rst
+++ b/docs/source/migration_mt_django.rst
@@ -10,7 +10,7 @@ This process will be in 5 steps:
- Introducing the tenant column to models missing it that we want to distribute
- Changing the primary keys of distributed tables to include the tenant column
-- Updating the models to use the :code:`TenantModelMixin`
+- Updating the models to use the :code:`TenantModel`
- Distributing the data
- Updating the Django Application to scope queries
@@ -319,7 +319,7 @@ And finally apply the changes by creating a new migration to generate these cons
python manage.py makemigrations
-3. Updating the models to use TenantModelMixin and TenantForeignKey
+3. Updating the models to use TenantModel and TenantForeignKey
--------------------------------------------------------------------
Next, we'll use the `django-multitenant `_ library to add account_id to foreign keys, and make application queries easier later on.
@@ -338,54 +338,52 @@ In settings.py, change the database engine to the customized engine provided by
'ENGINE': 'django_multitenant.backends.postgresql'
-**3.1 Introducing the TenantModelMixin and TenantManager**
+**3.1 Introducing the TenantModel**
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-The models will now not only inherit from ``models.Model`` but also from the ``TenantModelMixin``.
+The models will now inherit from ``TenantModel`` which is the base model for tenant-based models .
To do that in your :code:`models.py` file you will need to do the following imports
.. code-block:: python
- from django_multitenant.mixins import *
+ from django_multitenant.models import TenantModel
Previously our example models inherited from just models.Model, but now we need
-to change them to also inherit from TenantModelMixin. The models in real
-projects may inherit from other mixins too like ``django.contrib.gis.db``,
-which is fine.
+to change them to inherit from TenantModel.
You will also, at this point, introduce the tenant_id to define which column is
the distribution column.
.. code-block:: python
- class TenantManager(TenantManagerMixin, models.Manager):
+ class TenantManager(TenantModel):
pass
- class Account(TenantModelMixin, models.Model):
+ class Account(TenantModel):
...
- tenant_id = 'id'
- objects = TenantManager()
+ class TenantMeta:
+ tenant_field_name = 'id'
- class Manager(TenantModelMixin, models.Model):
+ class Manager(TenantModel):
...
- tenant_id = 'account_id'
- objects = TenantManager()
+ class TenantMeta:
+ tenant_field_name = 'account_id'
- class Project(TenantModelMixin, models.Model):
+ class Project(TenantModel):
...
- tenant_id = 'account_id'
- objects = TenantManager()
+ class TenantMeta:
+ tenant_field_name = 'account_id'
- class Task(TenantModelMixin, models.Model):
+ class Task(TenantModel):
...
- tenant_id = 'account_id'
- objects = TenantManager()
+ class TenantMeta:
+ tenant_field_name = 'account_id'
- class ProjectManager(TenantModelMixin, models.Model):
+ class ProjectManager(TenantModel):
...
- tenant_id = 'account_id'
- objects = TenantManager()
+ class TenantMeta:
+ tenant_field_name = 'account_id'
**3.2 Handling ForeignKey constraints**
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -402,53 +400,50 @@ Finally your models should look like this:
from django.db import models
from django_multitenant.fields import TenantForeignKey
- from django_multitenant.mixins import *
+ from django_multitenant.models import TenantModel
class Country(models.Model): # This table is a reference table
name = models.CharField(max_length=255)
- class TenantManager(TenantManagerMixin, models.Manager):
- pass
-
- class Account(TenantModelMixin, models.Model):
+ class Account(TenantModel):
name = models.CharField(max_length=255)
domain = models.CharField(max_length=255)
subdomain = models.CharField(max_length=255)
country = models.ForeignKey(Country, on_delete=models.SET_NULL) # No changes needed
- tenant_id = 'id'
- objects = TenantManager()
+ class TenantMeta:
+ tenant_field_name = "id"
- class Manager(TenantModelMixin, models.Model):
+ class Manager(TenantModel):
name = models.CharField(max_length=255)
account = models.ForeignKey(Account, related_name='managers',
on_delete=models.CASCADE)
- tenant_id = 'account_id'
- objects = TenantManager()
+ class TenantMeta:
+ tenant_field_name = 'account_id'
- class Project(TenantModelMixin, models.Model):
+ class Project(TenantModel):
account = models.ForeignKey(Account, related_name='projects',
on_delete=models.CASCADE)
managers = models.ManyToManyField(Manager, through='ProjectManager')
- tenant_id = 'account_id'
- objects = TenantManager()
+ class TenantMeta:
+ tenant_field_name = 'account_id'
- class Task(TenantModelMixin, models.Model):
+ class Task(TenantModel):
name = models.CharField(max_length=255)
project = TenantForeignKey(Project, on_delete=models.CASCADE,
related_name='tasks')
account = models.ForeignKey(Account, on_delete=models.CASCADE)
- tenant_id = 'account_id'
- objects = TenantManager()
+ class TenantMeta:
+ tenant_field_name = 'account_id'
- class ProjectManager(TenantModelMixin, models.Model):
+ class ProjectManager(TenantModel):
project = TenantForeignKey(Project, on_delete=models.CASCADE)
manager = TenantForeignKey(Manager, on_delete=models.CASCADE)
account = models.ForeignKey(Account, on_delete=models.CASCADE)
- tenant_id = 'account_id'
- objects = TenantManager()
+ class TenantMeta:
+ tenant_field_name = 'account_id'
**3.3 Handling ManyToMany constraints**
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -457,13 +452,13 @@ In the second section of this article, we introduced the fact that with citus, `
.. code-block:: python
- class ProjectManager(TenantModelMixin, models.Model):
+ class ProjectManager(TenantModel):
project = TenantForeignKey(Project, on_delete=models.CASCADE)
manager = TenantForeignKey(Manager, on_delete=models.CASCADE)
account = models.ForeignKey(Account, on_delete=models.CASCADE)
- tenant_id = 'account_id'
- objects = TenantManager()
+ class TenantMeta:
+ tenant_field_name = 'account_id'
After installing the library, changing the engine, and updating the models, run
:code:`python manage.py makemigrations`. This will produce a migration to make the foreign keys composite when necessary.
diff --git a/requirements/static-analysis-requirements.txt b/requirements/static-analysis-requirements.txt
index ab4dcdb8..f91ccc3d 100644
--- a/requirements/static-analysis-requirements.txt
+++ b/requirements/static-analysis-requirements.txt
@@ -4,6 +4,8 @@
#
# pip-compile --output-file=requirements/static-analysis-requirements.txt --resolver=backtracking requirements/static-analysis.in
#
+alabaster==0.7.13
+ # via sphinx
asgiref==3.6.0
# via django
astroid==2.13.2
@@ -14,6 +16,8 @@ astroid==2.13.2
# requirements-detector
attrs==22.2.0
# via pytest
+babel==2.12.1
+ # via sphinx
backports-zoneinfo==0.2.1
# via django
bandit==1.7.4
@@ -35,7 +39,9 @@ dill==0.3.6
django==4.1.7
# via -r requirements/static-analysis.in
docutils==0.19
- # via pyroma
+ # via
+ # pyroma
+ # sphinx
dodgy==0.2.1
# via prospector
exam==0.10.6
@@ -56,12 +62,20 @@ gitpython==3.1.30
# prospector
idna==3.4
# via requests
+imagesize==1.4.1
+ # via sphinx
+importlib-metadata==6.0.0
+ # via sphinx
iniconfig==2.0.0
# via pytest
isort==5.11.4
# via pylint
+jinja2==3.1.2
+ # via sphinx
lazy-object-proxy==1.9.0
# via astroid
+markupsafe==2.1.2
+ # via jinja2
mccabe==0.7.0
# via
# flake8
@@ -81,6 +95,7 @@ packaging==21.3
# prospector
# pytest
# requirements-detector
+ # sphinx
pathspec==0.11.0
# via black
pbr==5.11.1
@@ -112,7 +127,9 @@ pyflakes==2.5.0
# flake8
# prospector
pygments==2.14.0
- # via pyroma
+ # via
+ # pyroma
+ # sphinx
pylint==2.15.10
# via
# -r requirements/static-analysis.in
@@ -148,12 +165,16 @@ pytest-cov==4.0.0
# via -r requirements/static-analysis.in
pytest-django==4.5.2
# via -r requirements/static-analysis.in
+pytz==2022.7.1
+ # via babel
pyyaml==6.0
# via
# bandit
# prospector
requests==2.28.2
- # via pyroma
+ # via
+ # pyroma
+ # sphinx
requirements-detector==1.0.3
# via prospector
setoptconf-tmp==0.3.1
@@ -161,7 +182,30 @@ setoptconf-tmp==0.3.1
smmap==5.0.0
# via gitdb
snowballstemmer==2.2.0
- # via pydocstyle
+ # via
+ # pydocstyle
+ # sphinx
+sphinx==6.1.3
+ # via
+ # -r requirements/static-analysis.in
+ # sphinx-rtd-theme
+ # sphinxnotes-strike
+sphinx-rtd-theme==0.5.1
+ # via -r requirements/static-analysis.in
+sphinxcontrib-applehelp==1.0.4
+ # via sphinx
+sphinxcontrib-devhelp==1.0.2
+ # via sphinx
+sphinxcontrib-htmlhelp==2.0.1
+ # via sphinx
+sphinxcontrib-jsmath==1.0.1
+ # via sphinx
+sphinxcontrib-qthelp==1.0.3
+ # via sphinx
+sphinxcontrib-serializinghtml==1.1.5
+ # via sphinx
+sphinxnotes-strike==1.2
+ # via -r requirements/static-analysis.in
sqlparse==0.4.3
# via django
stevedore==4.1.1
@@ -199,6 +243,8 @@ wheel==0.38.4
# via pyroma
wrapt==1.14.1
# via astroid
+zipp==3.15.0
+ # via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
# setuptools
diff --git a/requirements/static-analysis.in b/requirements/static-analysis.in
index 57400441..66617dac 100644
--- a/requirements/static-analysis.in
+++ b/requirements/static-analysis.in
@@ -7,3 +7,6 @@ pytest-cov
pytest-django
prospector[with_everything]
pylint < 2.16
+Sphinx
+sphinxnotes.strike
+sphinx_rtd_theme