# Multi Tenant App for Django
Shared schema multi tenant system for Django.

## Setup
`pip install accrete`  

- Add 'accrete' to INSTALLED_APPS
    
    ```python
    # settings.py
  
    INSTALLED_APPS = [
        ...
        'accrete',
        'django...',
    ]
    ```
- Add accrete.middleware.TenantMiddleware after the auth middleware
    ```python
    # settings.py
  
    MIDDLEWARE = [
        ...
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'accrete.middleware.TenantMiddleware',
        ...
    ]
    ```
- Run migrations

## Overview
Tenants and their Members are separated by a ForeignKey to accrete.models.Tenant 
and authenticated by request parameters and headers. On Models inheriting from 
accrete.modeles.TenantModel and Forms/ModelForms inheriting from 
accrete.form.(Model)Form, querysets are filtered automatically. 
The authenticated tenant and member are stored in a contextvar and can be 
retrieved and set from anywhere in the code.

## Models
### accrete.models.TenantModel
Abstract Base Model that links to accrete.models.Tenant.  
Sets the objects attribute to accrete.managers.TenantManager.  
Adds safety and integrity checks to the save method.

Inherit form this model to enable tenant separation.  
```python
# models.py

from accrete.models import TenantModel

class MyModel(TenantModel):
    ...
```

### accrete.models.Tenant  
Model representing a tenant.  The active tenant for a request/process is stored
in a contexvar and can be retrieved and set by accrete.tenant.get_tenant and 
accrete.tenant.set_tenant respectively.

### accrete.models.Member  
This model links the user model to the tenant model, allowing users to 
be associated with multiple tenants. The member, like the tenant, is authenticated
by the accrete.middleware.TenantMiddleware and stored in a contextvar.  
The active member can be retived by accrete.tenant.get_mamber and set by
accrete.tenant.set_member. Using set_member also sets the appropriate tenant.

### accret.models.AccessGroup  
Arbitrary authorization groups that can be set on tenant or members.  
These groups are checked on views decorated by accrete.decorator.tenant_reuired 
or subclassed from accrete.mixins.TenantRequiredMixin.

## Manager
##### accrete.managers.TenantManager
Filters QuerySets by the active Tenant.  
Ensures the active tenant is set on the instances on bulk operations.

## Middleware
### tenant.middleware.TenantMiddleware
This Middleware adds the Tenant(request.tenant) and 
Member(request.member) attributes to the request object.  
On responses the X-TENANT-ID header and tenant_id url parameter is added with
the active Tenant-ID as the value.

If the user is a member of multiple tenants, the request is parsed for a 
tenant_id in this order.

- The "tenant_id" URL Parameter in the GET data
- The Header X-TENANT-ID

If no tenant could be assigned the two attributes are set to None.  
Additionally, the user is checked for membership of the found tenant. If the
user has is_staff set to True, the tenant is set regardless of membership.

The Middleware must be added 
to the MIDDLEWARE setting after your authentication Middleware as it needs 
access to request.user.is_authenticated().

## Views
##### tenant.views.TenantRequiredMixin 
Adds tenant and optionally access right checks to the dispatch method.  
This Mixin is meant as a substitute to 
django.contrib.auth.mixins.LoginRequiredMixin as TenantRequiredMixin inherits
LoginRequiredMixin.

##### tenant.decorator.tenant_required  
Substitute for django.contrib.auth.decorators.login_required  
Checks if a tenant is set on the request and redirects to the 
TENANT_NOT_SET_URL specified in the settings.

The decorator itself is wrapped by login_required and can pass the arguments
redirect_field_name and login_url to login_required.

## Forms
##### accrete.forms.Form
Form class that filters the queryset of every field with a queryset attribute.

##### accrete.forms.ModelForm
Same behavior as tenant.forms.Form. Additionally, sets the tenant 
on the instance on save if needed.

## Fields
##### accrete.fields.TranslatedCharField
Extends JsonField to store strings in different languages with the language code as the key.  
By default, the language specified in settings.LANGUAGE_CODE is used to store values.
```python
from django.db import models
from django.utils import translation
from accrete.utils.models import translated_db_value


class MyModel(models.Model):
    name = TranslatedCharField(...)

    
# Create instance with the language set in settings.LANGUAGE_CODE
instance = MyModel(name='Name in default language')
instance.save()

# Switch to german
translation.activate('de-de')
instance.name = 'Name auf Deutsch'
instance.save()

# Switch back to english
translation.activate('en-us')
instance.refresh_from_db()
print(instance.name)
>>> 'Name in default language'

# Utility to get the saved json as a dict
print(translated_db_value(instance, 'name'))
>>> {'en-us': 'Name in default language', 'de-de': 'Name auf Deutsch'}

# Create an instance with multiple translations at once
translation.activate('de-de')
instance = MyModel(name={'en-us': 'Name in default language', 'de-de': 'Name auf Deutsch'})
instance.save()
print(instance.name)
>>> 'Name auf Deutsch'

```

## Channels
##### accrete.channels.TenantMiddleware
Support for django-channels. Sets the tenant and member contextvar.  
Must be inside auth middleware.

```python
# asgi.py

application = ProtocolTypeRouter({
    "http": django_asgi_app,
    "websocket": AllowedHostsOriginValidator(
        AuthMiddlewareStack(
            TenantMiddleware(
                URLRouter([
                    path('path/to/consumer/', Consumer.as_asgi()),
                ])
            )
        )
    ),
})

```

## Consumer
##### accrete.consumer.WebsocketTenantConsumer
Checks on connect if a tenant is set.

##### accrete.consumer.JsonWebsocketTenantConsumer
Checks on connect if a tenant is set.

## Settings

- ACCRETE_TENANT_NOT_SET_URL
Redirect to this URL when no tenant could be set for an authenticated user.


- ACCRETE_GROUP_NOT_SET_URL  
Redirect to this URL when a tenant or member tries to access a URL 
without having the needed access rights.

## Tenant Utils
### Tenant
##### accrete.tenant.set_tenant(tenant: accrete.models.Tenant)
Stores the tenant in contextvar.

##### accrete.tenant.get_tenant
Retrieves the tenant from contextvar.

##### accrete.tenant.set_member(member: accrete.models.Member)
Stores the member and associated tenant contextvar.

##### accrete.tenant.set_member(member: accrete.models.Member)
Retrieves the member from contextvar.

##### accrete.tenant.unscoped()
Context Manager to temporally disable tenant isolation.

##### accrete.tenant.unscope()
Decorator to disable tenant isolation.

##### accrete.tenant.per_tenant(include: Q = None, exclude: Q = None)
Decorator to run the decorated function per tenant.
Tenant to run on can be filtered by the include or exclude arguments.

## Contrib
Additional Apps

| App               | Description                                                    |  
|-------------------|----------------------------------------------------------------|
| country           | Countries with translatable names                              |
| log               | Global auditlog, runs at post_save signal                      |
| sequence          | Configurable sequences with support for subsequences per year. |
| system_mail       | Celery mail queue                                              |
| ui                | User Interface based on HTMX, Alpine.js and Bulma              |
| user              | Custom User model                                              |
| user_registration | User registration using system_mail to send confirmation mails |
