From 12efe25b1a73a1af7f3de833f3097b2cac6707d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrkan=20=C4=B0ndibay?= Date: Thu, 2 Mar 2023 08:42:57 +0300 Subject: [PATCH] Removes reserved tenant_id dependency (#151) * Removes reserved tenant_id dependency * Adds documentation compile tests into pipeline * Fixes documentation issues to make it compliant with latest library version * Updates documentation to describe the usage of newly introduced TenantMeta option --- .coverage | Bin 53248 -> 0 bytes .../workflows/django-multitenant-tests.yml | 5 + CHANGELOG.md | 74 + README.md | 160 ++- coverage.xml | 1200 ----------------- django_multitenant/mixins.py | 16 +- django_multitenant/tests/base.py | 8 - ...siness_tenant_alter_account_id_and_more.py | 108 ++ .../{0024_data_load.py => 0025_data_load.py} | 2 +- ...rchase_store_alter_account_id_and_more.py} | 2 +- ...ute.py => 0027_many_to_many_distribute.py} | 2 +- django_multitenant/tests/models.py | 39 +- .../tests/single_node_settings.py | 5 - django_multitenant/tests/test_models.py | 43 +- django_multitenant/tests/urls.py | 20 - docs/source/_static/.gitignore | 4 + docs/source/_templates/.gitignore | 4 + docs/source/migration_mt_django.rst | 87 +- requirements/static-analysis-requirements.txt | 54 +- requirements/static-analysis.in | 3 + 20 files changed, 451 insertions(+), 1385 deletions(-) delete mode 100644 .coverage create mode 100644 CHANGELOG.md delete mode 100644 coverage.xml create mode 100644 django_multitenant/tests/migrations/0024_business_tenant_alter_account_id_and_more.py rename django_multitenant/tests/migrations/{0024_data_load.py => 0025_data_load.py} (96%) rename django_multitenant/tests/migrations/{0024_product_purchase_store_alter_account_id_and_more.py => 0026_product_purchase_store_alter_account_id_and_more.py} (98%) rename django_multitenant/tests/migrations/{0025_many_to_many_distribute.py => 0027_many_to_many_distribute.py} (96%) delete mode 100644 django_multitenant/tests/single_node_settings.py delete mode 100644 django_multitenant/tests/urls.py create mode 100644 docs/source/_static/.gitignore create mode 100644 docs/source/_templates/.gitignore diff --git a/.coverage b/.coverage deleted file mode 100644 index c4a0845cca37ececc4128a8b3dec9993d1b6f3b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53248 zcmeI4dvF`Y8Nl^8y`@{f0~)X z4DINKG5v0=?_2HN?zj8fZ};w#yxOpSZCI6so_HcEsY0uv$Y3-Yt`Y=;!Qg((}cytXjEom9TN;)oWJ?={%v?DZsI4k+4!o z#D|3Kazf|{M`R%!3x$J{8jke}YM-oKomAuyz0pA)-0J9si&=S^62l=VrOLff#r8xv zDkXLa1M;qg?1GFnRovTbC8Va*E$CA-*K;EB>Zl{9-CDMk~c)pVP%Bt~9XFv7WTw*Mz`#WPeHRNpl^qdy-V&vx8fWiC7<|zW z!hKt@-R7;YH{QO3ISR!ic866Zn{do~@=1%NR$_XU)aFR$40LCa&FWoQldn6O>qLJhVNcmOo435&m`cwG zx-QX|Wkx4x&h!+x=tl7Ao4;LB`-G3Ton}MBn=DrEvhsWlr%e$3R9g3LHL)h&Ud8A~ zVbP~A^Nc3boarX7k#{W`lp?U2Y?s0bn6j`2r|$p)y)Y!f#Zq@XsWShiZ<%}2d(U)x z(-lm=*Vw~V^V#Jvy|0ZcN*Hdd^tPpZLCY1I`Xq&}LQ0d+9gjz3DVDxBK#A1~*bFNI zYs1IyN?p@+D7$DKy@O4xP1?3uW$C^fsKX71n+_IjNoWA_4`4 zCYY28Owo(#)GE!Kr7MzIH=v^>`l+N*vYkH4rRt3~?}7!!R5CLe)9Z|=z)d|WY0b{O zHO+96i%w4a%Q{$a;b|w=Pbj+_9Q1A40$tqFBiNm$pk0-D)^X0K0ltxZXn;RFkN^@u z0!RP}AOR$R1dsp{Kmter2_S*b9|5z`WVF%v-$edmApZb?2NFO6NB{{S0VIF~kN^@u z0!RP}AOR#WI|;Z==31iti-(UG%{AW4KLNBa_APF6La$6@+(5?3x!Ku4Y&8-<0!RP} zAOR$R1dsp{Kmter2_OL^pb>DJYmM5c04Aro#*z6DfPVkqXt-b?50Pv5^Za-DWu6nB zVb2xrQTKrBg6r$966c7s#qqRbwf(3)X#1yax6NU_*E-Mgm}MDvn7hvWwt0`)W!i7L z%=lB|jZo1nIf9LxA+@HZFCLX!dXtF(DMmMqc1ycjR5>QaRHY@OChP%<#Y3{F$SU1=s=#KH4fDXrfG0J8p8wyy3d)yl_=Mxj z+zYl_tovNwa6V|d+~%}4x+DArvd%EUAK;gJPI_*2|IxT@mW2NXLx@YQF&1p3E*<|j zZsbx63T7k||IbTssosKB`l;go2K`Lti~r~9XEI;>FAQ?2xISP%DgLkT?`k62OuhP$CzWBdV zKa=_5f3L!&!upJ6-uS;l$8P32ZBG1OuA|93@qd|qCiBJrr8=6-6aSa!XEI;>U#z3a z=@>5a8F@PXFN$+1NuODA$@rh>*TsDCKd+z3eDS|WKa=_5f46=n^Tq$JAeUOLhwtQu zGh4V+d%lUbzL6)MQ=TK9=R8kCC3qkKB!C2v01`j~NB{{S0VIF~kU-%GI2t&kqpYEMY>Knb zg=ERx{U3I@1V~j1j2{^38s=*t zU03tuWY^SLvOHmOR?~CU55HgAqpXqp&vsFMwQN`g>dpRUZ$~(q}on?@!C>uX8b;M*ZrMabtCye$Inks1?Guw-4 zs(Adsu)T<;ikinfc0yC6W31lJ(-gn|$duhfQ=aCrEA4KYa_>Jo>~=w_+SNC4*H~?* z(dvYEywgU!J1_nds~tF0qTM#5ja|G9_c-oX{4uZ>;v_7&&0oIZE-bZo=s z=;`Dy&OLfmKGweO)pPAPA3wPDQ2(*mD>1KO>#>8c-_Uz}=;$LSmJK%z9UJ_~UF#nC z+mUBRJKsIky5{Da4;?=q+Zr7@(SK-nxfNQo(E8K|zxM1L*!$>U?+N9^(Wu#Kp*^se ztQ^&xNic)H+Wg{uQ{R8<{5|LI*?0cf$;1=CZ*P0{r5`MKVBhcFa1#?;A(&qJ_SE$T z)3qNNA{Ha(aPTJj{eJ@~GmsC-2jpGyCV8FwnY>J1B)=g~lY`_@_+7vU$$jKIpsSI1OsEmL#mFjZ5-RCP5|RaHz?Rx;)FGF4H*R5^?4jb&x* zU1=#(B_&K17c*5<#1tV+@jO!=4^wV8Q!W=%PA5|i2UB)CQ#Kn@Rx48$3sW4&l-bOb z$;8120POpJL(wc69Be8QKmter2_OL^fCP{L5RQ?Li%2>Anff&7*{ zOP+##0FRMJ$PdZ)$N{ns_5$2VzDf3yTgWb=!hV2$B9oiQR_e@KRAh zML88^RFqOtLPaqZMN|+fcq%+pxT$ba;iSSrg`El;6;>)NRB%+7sW4GNfB*kK5k!?` 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 @@ - - - - - - /home/gurkanindibay/tenants/django-multitenant/django_multitenant/tests - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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