oxutils 0.1.2__py3-none-any.whl → 0.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oxutils/__init__.py +1 -1
- oxutils/audit/settings.py +1 -16
- oxutils/audit/utils.py +22 -0
- oxutils/conf.py +1 -3
- oxutils/constants.py +2 -0
- oxutils/context/__init__.py +0 -0
- oxutils/context/site_name_processor.py +11 -0
- oxutils/currency/__init__.py +0 -0
- oxutils/currency/admin.py +57 -0
- oxutils/currency/apps.py +7 -0
- oxutils/currency/controllers.py +79 -0
- oxutils/currency/enums.py +7 -0
- oxutils/currency/migrations/0001_initial.py +41 -0
- oxutils/currency/migrations/__init__.py +0 -0
- oxutils/currency/models.py +100 -0
- oxutils/currency/schemas.py +38 -0
- oxutils/currency/tests.py +3 -0
- oxutils/currency/utils.py +69 -0
- oxutils/functions.py +5 -2
- oxutils/logger/receivers.py +0 -2
- oxutils/oxiliere/__init__.py +0 -0
- oxutils/oxiliere/admin.py +3 -0
- oxutils/oxiliere/apps.py +6 -0
- oxutils/oxiliere/cacheops.py +7 -0
- oxutils/oxiliere/caches.py +33 -0
- oxutils/oxiliere/controllers.py +36 -0
- oxutils/oxiliere/enums.py +10 -0
- oxutils/oxiliere/management/__init__.py +0 -0
- oxutils/oxiliere/management/commands/__init__.py +0 -0
- oxutils/oxiliere/management/commands/init_oxiliere_system.py +86 -0
- oxutils/oxiliere/middleware.py +97 -0
- oxutils/oxiliere/migrations/__init__.py +0 -0
- oxutils/oxiliere/models.py +55 -0
- oxutils/oxiliere/permissions.py +104 -0
- oxutils/oxiliere/schemas.py +65 -0
- oxutils/oxiliere/settings.py +17 -0
- oxutils/oxiliere/tests.py +3 -0
- oxutils/oxiliere/utils.py +76 -0
- oxutils/pdf/__init__.py +10 -0
- oxutils/pdf/printer.py +81 -0
- oxutils/pdf/utils.py +94 -0
- oxutils/pdf/views.py +208 -0
- oxutils/settings.py +2 -0
- oxutils/users/__init__.py +0 -0
- oxutils/users/admin.py +3 -0
- oxutils/users/apps.py +6 -0
- oxutils/users/migrations/__init__.py +0 -0
- oxutils/users/models.py +88 -0
- oxutils/users/tests.py +3 -0
- oxutils/users/utils.py +15 -0
- {oxutils-0.1.2.dist-info → oxutils-0.1.6.dist-info}/METADATA +99 -11
- oxutils-0.1.6.dist-info/RECORD +88 -0
- {oxutils-0.1.2.dist-info → oxutils-0.1.6.dist-info}/WHEEL +1 -1
- oxutils/locale/fr/LC_MESSAGES/django.mo +0 -0
- oxutils-0.1.2.dist-info/RECORD +0 -45
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from django.core.management.base import BaseCommand
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.db import transaction
|
|
5
|
+
from django.contrib.auth import get_user_model
|
|
6
|
+
from django_tenants.utils import get_tenant_model
|
|
7
|
+
from oxutils.oxiliere.models import Domain, TenantUser
|
|
8
|
+
from oxutils.oxiliere.utils import oxid_to_schema_name
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Command(BaseCommand):
|
|
13
|
+
help = 'Initialise le tenant système Oxiliere'
|
|
14
|
+
|
|
15
|
+
@transaction.atomic
|
|
16
|
+
def handle(self, *args, **options):
|
|
17
|
+
TenantModel = get_tenant_model()
|
|
18
|
+
UserModel = get_user_model()
|
|
19
|
+
|
|
20
|
+
# Configuration du tenant système depuis settings
|
|
21
|
+
system_slug = getattr(settings, 'OXI_SYSTEM_TENANT', 'tenant_oxisystem')
|
|
22
|
+
schema_name = oxid_to_schema_name(system_slug)
|
|
23
|
+
system_domain = getattr(settings, 'OXI_SYSTEM_DOMAIN', 'system.oxiliere.com')
|
|
24
|
+
owner_email = getattr(settings, 'OXI_SYSTEM_OWNER_EMAIL', 'dev@oxiliere.com')
|
|
25
|
+
owner_oxi_id = uuid.uuid4()
|
|
26
|
+
|
|
27
|
+
self.stdout.write(self.style.WARNING(f'Initialisation du tenant système...'))
|
|
28
|
+
|
|
29
|
+
# Vérifier si le tenant système existe déjà
|
|
30
|
+
if TenantModel.objects.filter(schema_name=schema_name).exists():
|
|
31
|
+
self.stdout.write(self.style.ERROR(f'Le tenant système "{schema_name}" existe déjà!'))
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
# Créer le tenant système
|
|
35
|
+
self.stdout.write(f'Création du tenant système: {schema_name}')
|
|
36
|
+
tenant = TenantModel.objects.create(
|
|
37
|
+
name='Oxiliere System',
|
|
38
|
+
schema_name=schema_name,
|
|
39
|
+
oxi_id=system_slug,
|
|
40
|
+
subscription_plan='system',
|
|
41
|
+
subscription_status='active',
|
|
42
|
+
)
|
|
43
|
+
self.stdout.write(self.style.SUCCESS(f'✓ Tenant système créé: {tenant.name} ({tenant.schema_name})'))
|
|
44
|
+
|
|
45
|
+
# Créer le domaine pour le tenant système
|
|
46
|
+
self.stdout.write(f'Création du domaine: {system_domain}')
|
|
47
|
+
domain = Domain.objects.create(
|
|
48
|
+
domain=system_domain,
|
|
49
|
+
tenant=tenant,
|
|
50
|
+
is_primary=True
|
|
51
|
+
)
|
|
52
|
+
self.stdout.write(self.style.SUCCESS(f'✓ Domaine créé: {domain.domain}'))
|
|
53
|
+
|
|
54
|
+
self.stdout.write(f'Création du superuser: {owner_email}')
|
|
55
|
+
try:
|
|
56
|
+
superuser = UserModel.objects.get(email=owner_email)
|
|
57
|
+
self.stdout.write(self.style.WARNING(f'⚠ Superuser existe déjà: {superuser.email}'))
|
|
58
|
+
except UserModel.DoesNotExist:
|
|
59
|
+
superuser = UserModel.objects.create_superuser(
|
|
60
|
+
email=owner_email,
|
|
61
|
+
oxi_id=owner_oxi_id,
|
|
62
|
+
first_name='System',
|
|
63
|
+
last_name='Admin'
|
|
64
|
+
)
|
|
65
|
+
self.stdout.write(self.style.SUCCESS(f'✓ Superuser créé: {superuser.email}'))
|
|
66
|
+
|
|
67
|
+
# Lier le superuser au tenant système
|
|
68
|
+
self.stdout.write('Liaison du superuser au tenant système')
|
|
69
|
+
tenant_user, created = TenantUser.objects.get_or_create(
|
|
70
|
+
tenant=tenant,
|
|
71
|
+
user=superuser,
|
|
72
|
+
defaults={
|
|
73
|
+
'is_owner': True,
|
|
74
|
+
'is_admin': True,
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
if created:
|
|
78
|
+
self.stdout.write(self.style.SUCCESS(f'✓ Superuser lié au tenant système'))
|
|
79
|
+
else:
|
|
80
|
+
self.stdout.write(self.style.WARNING(f'⚠ Liaison existe déjà'))
|
|
81
|
+
|
|
82
|
+
self.stdout.write(self.style.SUCCESS('\n=== Initialisation terminée avec succès ==='))
|
|
83
|
+
self.stdout.write(f'Tenant: {tenant.name}')
|
|
84
|
+
self.stdout.write(f'Schema: {tenant.schema_name}')
|
|
85
|
+
self.stdout.write(f'Domain: {domain.domain}')
|
|
86
|
+
self.stdout.write(f'Superuser: {owner_email}')
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
from django.db import connection
|
|
3
|
+
from django.http import Http404
|
|
4
|
+
from django.urls import set_urlconf
|
|
5
|
+
from django.utils.module_loading import import_string
|
|
6
|
+
from django.utils.deprecation import MiddlewareMixin
|
|
7
|
+
|
|
8
|
+
from django_tenants.utils import (
|
|
9
|
+
get_public_schema_name,
|
|
10
|
+
get_public_schema_urlconf
|
|
11
|
+
)
|
|
12
|
+
from oxutils.constants import ORGANIZATION_HEADER_KEY
|
|
13
|
+
|
|
14
|
+
class TenantMainMiddleware(MiddlewareMixin):
|
|
15
|
+
TENANT_NOT_FOUND_EXCEPTION = Http404
|
|
16
|
+
"""
|
|
17
|
+
This middleware should be placed at the very top of the middleware stack.
|
|
18
|
+
Selects the proper database schema using the request host. Can fail in
|
|
19
|
+
various ways which is better than corrupting or revealing data.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def get_org_id_from_request(request):
|
|
24
|
+
""" Extracts organization ID from request header X-Organization-ID.
|
|
25
|
+
"""
|
|
26
|
+
custom = 'HTTP_' + ORGANIZATION_HEADER_KEY.upper().replace('-', '_')
|
|
27
|
+
return request.headers.get(ORGANIZATION_HEADER_KEY) or request.META.get(custom)
|
|
28
|
+
|
|
29
|
+
def get_tenant(self, tenant_model, oxi_id):
|
|
30
|
+
""" Get tenant by oxi_id instead of domain.
|
|
31
|
+
"""
|
|
32
|
+
return tenant_model.objects.get(oxi_id=oxi_id)
|
|
33
|
+
|
|
34
|
+
def process_request(self, request):
|
|
35
|
+
# Connection needs first to be at the public schema, as this is where
|
|
36
|
+
# the tenant metadata is stored.
|
|
37
|
+
|
|
38
|
+
connection.set_schema_to_public()
|
|
39
|
+
|
|
40
|
+
oxi_id = self.get_org_id_from_request(request)
|
|
41
|
+
if not oxi_id:
|
|
42
|
+
from django.http import HttpResponseBadRequest
|
|
43
|
+
return HttpResponseBadRequest('Missing X-Organization-ID header')
|
|
44
|
+
|
|
45
|
+
tenant_model = connection.tenant_model
|
|
46
|
+
try:
|
|
47
|
+
tenant = self.get_tenant(tenant_model, oxi_id)
|
|
48
|
+
except tenant_model.DoesNotExist:
|
|
49
|
+
default_tenant = self.no_tenant_found(request, oxi_id)
|
|
50
|
+
return default_tenant
|
|
51
|
+
|
|
52
|
+
request.tenant = tenant
|
|
53
|
+
connection.set_tenant(request.tenant)
|
|
54
|
+
self.setup_url_routing(request)
|
|
55
|
+
|
|
56
|
+
def no_tenant_found(self, request, oxi_id):
|
|
57
|
+
""" What should happen if no tenant is found.
|
|
58
|
+
This makes it easier if you want to override the default behavior """
|
|
59
|
+
if hasattr(settings, 'DEFAULT_NOT_FOUND_TENANT_VIEW'):
|
|
60
|
+
view_path = settings.DEFAULT_NOT_FOUND_TENANT_VIEW
|
|
61
|
+
view = import_string(view_path)
|
|
62
|
+
if hasattr(view, 'as_view'):
|
|
63
|
+
response = view.as_view()(request)
|
|
64
|
+
else:
|
|
65
|
+
response = view(request)
|
|
66
|
+
if hasattr(response, 'render'):
|
|
67
|
+
response.render()
|
|
68
|
+
return response
|
|
69
|
+
elif hasattr(settings, 'SHOW_PUBLIC_IF_NO_TENANT_FOUND') and settings.SHOW_PUBLIC_IF_NO_TENANT_FOUND:
|
|
70
|
+
self.setup_url_routing(request=request, force_public=True)
|
|
71
|
+
else:
|
|
72
|
+
raise self.TENANT_NOT_FOUND_EXCEPTION('No tenant for X-Organization-ID "%s"' % oxi_id)
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def setup_url_routing(request, force_public=False):
|
|
76
|
+
"""
|
|
77
|
+
Sets the correct url conf based on the tenant
|
|
78
|
+
:param request:
|
|
79
|
+
:param force_public
|
|
80
|
+
"""
|
|
81
|
+
public_schema_name = get_public_schema_name()
|
|
82
|
+
if has_multi_type_tenants():
|
|
83
|
+
tenant_types = get_tenant_types()
|
|
84
|
+
if (not hasattr(request, 'tenant') or
|
|
85
|
+
((force_public or request.tenant.schema_name == get_public_schema_name()) and
|
|
86
|
+
'URLCONF' in tenant_types[public_schema_name])):
|
|
87
|
+
request.urlconf = get_public_schema_urlconf()
|
|
88
|
+
else:
|
|
89
|
+
tenant_type = request.tenant.get_tenant_type()
|
|
90
|
+
request.urlconf = tenant_types[tenant_type]['URLCONF']
|
|
91
|
+
set_urlconf(request.urlconf)
|
|
92
|
+
|
|
93
|
+
else:
|
|
94
|
+
# Do we have a public-specific urlconf?
|
|
95
|
+
if (hasattr(settings, 'PUBLIC_SCHEMA_URLCONF') and
|
|
96
|
+
(force_public or request.tenant.schema_name == get_public_schema_name())):
|
|
97
|
+
request.urlconf = settings.PUBLIC_SCHEMA_URLCONF
|
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from django_tenants.models import TenantMixin, DomainMixin
|
|
4
|
+
from oxutils.models import (
|
|
5
|
+
TimestampMixin,
|
|
6
|
+
BaseModelMixin,
|
|
7
|
+
)
|
|
8
|
+
from oxutils.oxiliere.enums import TenantStatus
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Tenant(TenantMixin, TimestampMixin):
|
|
14
|
+
name = models.CharField(max_length=100)
|
|
15
|
+
oxi_id = models.UUIDField(unique=True)
|
|
16
|
+
subscription_plan = models.CharField(max_length=255, null=True, blank=True)
|
|
17
|
+
subscription_status = models.CharField(max_length=255, null=True, blank=True)
|
|
18
|
+
subscription_end_date = models.DateTimeField(null=True, blank=True)
|
|
19
|
+
status = models.CharField(
|
|
20
|
+
max_length=20,
|
|
21
|
+
choices=TenantStatus.choices,
|
|
22
|
+
default=TenantStatus.ACTIVE
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# default true, schema will be automatically created and synced when it is saved
|
|
26
|
+
auto_create_schema = True
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Domain(DomainMixin):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TenantUser(BaseModelMixin):
|
|
35
|
+
tenant = models.ForeignKey(
|
|
36
|
+
settings.TENANT_MODEL, on_delete=models.CASCADE
|
|
37
|
+
)
|
|
38
|
+
user = models.ForeignKey(
|
|
39
|
+
settings.AUTH_USER_MODEL, on_delete=models.CASCADE
|
|
40
|
+
)
|
|
41
|
+
is_owner = models.BooleanField(default=False)
|
|
42
|
+
is_admin = models.BooleanField(default=False)
|
|
43
|
+
status = models.CharField(
|
|
44
|
+
max_length=20,
|
|
45
|
+
choices=TenantStatus.choices,
|
|
46
|
+
default=TenantStatus.ACTIVE
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
class Meta:
|
|
50
|
+
constraints = [
|
|
51
|
+
models.UniqueConstraint(
|
|
52
|
+
fields=['tenant', 'user'],
|
|
53
|
+
name='unique_tenant_user'
|
|
54
|
+
)
|
|
55
|
+
]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from ninja.permissions import BasePermission
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from oxutils.oxiliere.models import TenantUser
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TenantPermission(BasePermission):
|
|
7
|
+
"""
|
|
8
|
+
Vérifie que l'utilisateur a accès au tenant actuel.
|
|
9
|
+
L'utilisateur doit être authentifié et avoir un lien avec le tenant.
|
|
10
|
+
"""
|
|
11
|
+
def has_permission(self, request, view):
|
|
12
|
+
if not request.user or not request.user.is_authenticated:
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
if not hasattr(request, 'tenant'):
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
# Vérifier que l'utilisateur a accès à ce tenant
|
|
19
|
+
return TenantUser.objects.filter(
|
|
20
|
+
tenant=request.tenant,
|
|
21
|
+
user=request.user
|
|
22
|
+
).exists()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TenantOwnerPermission(BasePermission):
|
|
26
|
+
"""
|
|
27
|
+
Vérifie que l'utilisateur est propriétaire (owner) du tenant actuel.
|
|
28
|
+
"""
|
|
29
|
+
def has_permission(self, request, view):
|
|
30
|
+
if not request.user or not request.user.is_authenticated:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
if not hasattr(request, 'tenant'):
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
# Vérifier que l'utilisateur est owner du tenant
|
|
37
|
+
return TenantUser.objects.filter(
|
|
38
|
+
tenant=request.tenant,
|
|
39
|
+
user=request.user,
|
|
40
|
+
is_owner=True
|
|
41
|
+
).exists()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TenantAdminPermission(BasePermission):
|
|
45
|
+
"""
|
|
46
|
+
Vérifie que l'utilisateur est admin ou owner du tenant actuel.
|
|
47
|
+
"""
|
|
48
|
+
def has_permission(self, request, view):
|
|
49
|
+
if not request.user or not request.user.is_authenticated:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
if not hasattr(request, 'tenant'):
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
# Vérifier que l'utilisateur est admin ou owner du tenant
|
|
56
|
+
return TenantUser.objects.filter(
|
|
57
|
+
tenant=request.tenant,
|
|
58
|
+
user=request.user,
|
|
59
|
+
is_admin=True
|
|
60
|
+
).exists() or TenantUser.objects.filter(
|
|
61
|
+
tenant=request.tenant,
|
|
62
|
+
user=request.user,
|
|
63
|
+
is_owner=True
|
|
64
|
+
).exists()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TenantUserPermission(BasePermission):
|
|
68
|
+
"""
|
|
69
|
+
Vérifie que l'utilisateur est un membre du tenant actuel.
|
|
70
|
+
Alias de TenantPermission pour plus de clarté sémantique.
|
|
71
|
+
"""
|
|
72
|
+
def has_permission(self, request, view):
|
|
73
|
+
if not request.user or not request.user.is_authenticated:
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
if not hasattr(request, 'tenant'):
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
return TenantUser.objects.filter(
|
|
80
|
+
tenant=request.tenant,
|
|
81
|
+
user=request.user
|
|
82
|
+
).exists()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class OxiliereServicePermission(BasePermission):
|
|
86
|
+
"""
|
|
87
|
+
Vérifie que la requête provient d'un service interne Oxiliere.
|
|
88
|
+
Utilise un token de service ou une clé API spéciale.
|
|
89
|
+
"""
|
|
90
|
+
def has_permission(self, request, view):
|
|
91
|
+
# Vérifier le header de service
|
|
92
|
+
service_token = request.headers.get('X-Oxiliere-Service-Token')
|
|
93
|
+
|
|
94
|
+
if not service_token:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
# Comparer avec le token configuré dans settings
|
|
98
|
+
expected_token = getattr(settings, 'OXILIERE_SERVICE_TOKEN', None)
|
|
99
|
+
|
|
100
|
+
if not expected_token:
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
return service_token == expected_token
|
|
104
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from ninja import Schema
|
|
3
|
+
from django.db import transaction
|
|
4
|
+
from django.contrib.auth import get_user_model
|
|
5
|
+
from django_tenants.utils import get_tenant_model
|
|
6
|
+
from oxutils.oxiliere.models import TenantUser
|
|
7
|
+
from oxutils.oxiliere.utils import oxid_to_schema_name
|
|
8
|
+
import structlog
|
|
9
|
+
|
|
10
|
+
logger = structlog.get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TenantSchema(Schema):
|
|
14
|
+
name: str
|
|
15
|
+
oxi_id: str
|
|
16
|
+
subscription_plan: Optional[str]
|
|
17
|
+
subscription_status: Optional[str]
|
|
18
|
+
subscription_end_date: Optional[str]
|
|
19
|
+
status: Optional[str]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TenantOwnerSchema(Schema):
|
|
23
|
+
oxi_id: str
|
|
24
|
+
email: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CreateTenantSchema(Schema):
|
|
28
|
+
tenant: TenantSchema
|
|
29
|
+
owner: TenantOwnerSchema
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@transaction.atomic
|
|
33
|
+
def create_tenant(self):
|
|
34
|
+
UserModel = get_user_model()
|
|
35
|
+
TenantModel = get_tenant_model()
|
|
36
|
+
|
|
37
|
+
if TenantModel.objects.filter(oxi_id=self.tenant.oxi_id).exists():
|
|
38
|
+
logger.info("tenant_exists", oxi_id=self.tenant.oxi_id)
|
|
39
|
+
raise ValueError("Tenant with oxi_id {} already exists".format(self.tenant.oxi_id))
|
|
40
|
+
|
|
41
|
+
user = UserModel.objects.get_or_create(
|
|
42
|
+
oxi_id=self.owner.oxi_id,
|
|
43
|
+
defaults={
|
|
44
|
+
'email': self.owner.email,
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
tenant = TenantModel.objects.create(
|
|
49
|
+
name=self.tenant.name,
|
|
50
|
+
schema_name=oxid_to_schema_name(self.tenant.oxi_id),
|
|
51
|
+
oxi_id=self.tenant.oxi_id,
|
|
52
|
+
subscription_plan=self.tenant.subscription_plan,
|
|
53
|
+
subscription_status=self.tenant.subscription_status,
|
|
54
|
+
subscription_end_date=self.tenant.subscription_end_date,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
TenantUser.objects.create(
|
|
58
|
+
tenant=tenant,
|
|
59
|
+
user=user,
|
|
60
|
+
is_owner=True,
|
|
61
|
+
is_admin=True,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
logger.info("tenant_created", oxi_id=self.tenant.oxi_id)
|
|
65
|
+
return tenant
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
TENANT_LIMIT_SET_CALLS = True
|
|
4
|
+
|
|
5
|
+
CACHEOPS_PREFIX = 'oxutils.oxiliere.cacheops.cacheops_prefix'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
REDIS_URLS = os.getenv("REDIS_URLS", "redis://127.0.0.1:6379").split(",")
|
|
9
|
+
|
|
10
|
+
CACHES = {
|
|
11
|
+
"default": {
|
|
12
|
+
"BACKEND": "django.core.cache.backends.redis.RedisCache",
|
|
13
|
+
"LOCATION": REDIS_URLS,
|
|
14
|
+
'KEY_FUNCTION': 'django_tenants.cache.make_key',
|
|
15
|
+
'REVERSE_KEY_FUNCTION': 'django_tenants.cache.reverse_key',
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# utils.py
|
|
2
|
+
|
|
3
|
+
def oxid_to_schema_name(oxid: str) -> str:
|
|
4
|
+
"""
|
|
5
|
+
Convertit un OXI-ID (slug) en nom de schéma PostgreSQL valide.
|
|
6
|
+
|
|
7
|
+
Règles PostgreSQL pour les noms de schéma:
|
|
8
|
+
- Doit commencer par une lettre (a-z) ou underscore (_)
|
|
9
|
+
- Peut contenir uniquement des lettres, chiffres et underscores
|
|
10
|
+
- Maximum 63 caractères
|
|
11
|
+
- Sensible à la casse mais conventionnellement en minuscules
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
oxid: Slug de l'organisation (ex: "my-company", "acme-corp")
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Nom de schéma valide (format: tenant_mycompany, tenant_acmecorp)
|
|
18
|
+
"""
|
|
19
|
+
if not oxid:
|
|
20
|
+
raise ValueError("oxi_id cannot be empty")
|
|
21
|
+
|
|
22
|
+
# Nettoyer le slug: remplacer les tirets par underscores et convertir en minuscules
|
|
23
|
+
clean_id = str(oxid).replace('-', '_').lower()
|
|
24
|
+
|
|
25
|
+
# Supprimer tous les caractères non-alphanumériques sauf underscore
|
|
26
|
+
import re
|
|
27
|
+
clean_id = re.sub(r'[^a-z0-9_]', '', clean_id)
|
|
28
|
+
|
|
29
|
+
# Préfixer avec 'tenant_' pour s'assurer que ça commence par une lettre
|
|
30
|
+
# et pour éviter les conflits avec les schémas système de PostgreSQL
|
|
31
|
+
schema_name = f"tenant_{clean_id}"
|
|
32
|
+
|
|
33
|
+
# Vérifier la longueur (PostgreSQL limite à 63 caractères)
|
|
34
|
+
if len(schema_name) > 63:
|
|
35
|
+
raise ValueError(f"Schema name too long: {len(schema_name)} characters (max 63)")
|
|
36
|
+
|
|
37
|
+
return schema_name
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def update_tenant_user(oxi_org_id: str, oxi_user_id: str, data: dict):
|
|
41
|
+
if not data or isinstance(data, dict) == False: return
|
|
42
|
+
if not oxi_org_id or not oxi_user_id: return
|
|
43
|
+
|
|
44
|
+
from oxutils.oxiliere.caches import get_tenant_user
|
|
45
|
+
|
|
46
|
+
TENANT_USER_FIELDS = ['is_owner', 'is_admin', 'status', 'is_active']
|
|
47
|
+
tenant_user = get_tenant_user(oxi_org_id, oxi_user_id)
|
|
48
|
+
changes = False
|
|
49
|
+
|
|
50
|
+
for key, value in data.items():
|
|
51
|
+
if key in TENANT_USER_FIELDS:
|
|
52
|
+
setattr(tenant_user, key, value)
|
|
53
|
+
changes = True
|
|
54
|
+
|
|
55
|
+
if changes:
|
|
56
|
+
tenant_user.save()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def update_tenant(oxi_id: str, data: dict):
|
|
60
|
+
if not data or isinstance(data, dict) == False: return
|
|
61
|
+
if not oxi_id: return
|
|
62
|
+
|
|
63
|
+
from oxutils.oxiliere.caches import get_tenant_by_oxi_id
|
|
64
|
+
|
|
65
|
+
TENANT_FIELDS = ['name', 'status', 'subscription_plan', 'subscription_status', 'subscription_end_date']
|
|
66
|
+
|
|
67
|
+
tenant = get_tenant_by_oxi_id(oxi_id)
|
|
68
|
+
changes = False
|
|
69
|
+
|
|
70
|
+
for key, value in data.items():
|
|
71
|
+
if key in TENANT_FIELDS:
|
|
72
|
+
setattr(tenant, key, value)
|
|
73
|
+
changes = True
|
|
74
|
+
|
|
75
|
+
if changes:
|
|
76
|
+
tenant.save()
|
oxutils/pdf/__init__.py
ADDED
oxutils/pdf/printer.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import weasyprint
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from django.template.loader import render_to_string
|
|
4
|
+
|
|
5
|
+
from oxutils.pdf.utils import django_url_fetcher
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Printer:
|
|
9
|
+
template_name = None
|
|
10
|
+
pdf_stylesheets = []
|
|
11
|
+
pdf_options = {}
|
|
12
|
+
|
|
13
|
+
def __init__(self, template_name=None, context=None, stylesheets=None, options=None, base_url=None):
|
|
14
|
+
self.template_name = template_name or self.template_name
|
|
15
|
+
self.context = context or {}
|
|
16
|
+
self._stylesheets = stylesheets or self.pdf_stylesheets
|
|
17
|
+
self._options = options.copy() if options else self.pdf_options.copy()
|
|
18
|
+
self._base_url = base_url
|
|
19
|
+
|
|
20
|
+
def get_context_data(self, **kwargs):
|
|
21
|
+
context = self.context.copy()
|
|
22
|
+
context.update(kwargs)
|
|
23
|
+
return context
|
|
24
|
+
|
|
25
|
+
def get_pdf_filename(self):
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
def get_base_url(self):
|
|
29
|
+
if self._base_url:
|
|
30
|
+
return self._base_url
|
|
31
|
+
return getattr(settings, 'WEASYPRINT_BASEURL', '/')
|
|
32
|
+
|
|
33
|
+
def get_url_fetcher(self):
|
|
34
|
+
return django_url_fetcher
|
|
35
|
+
|
|
36
|
+
def get_font_config(self):
|
|
37
|
+
return weasyprint.text.fonts.FontConfiguration()
|
|
38
|
+
|
|
39
|
+
def get_css(self, base_url, url_fetcher, font_config):
|
|
40
|
+
return [
|
|
41
|
+
weasyprint.CSS(
|
|
42
|
+
value,
|
|
43
|
+
base_url=base_url,
|
|
44
|
+
url_fetcher=url_fetcher,
|
|
45
|
+
font_config=font_config,
|
|
46
|
+
)
|
|
47
|
+
for value in self._stylesheets
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def render_html(self, **kwargs):
|
|
51
|
+
context = self.get_context_data(**kwargs)
|
|
52
|
+
return render_to_string(self.template_name, context)
|
|
53
|
+
|
|
54
|
+
def get_document(self, **kwargs):
|
|
55
|
+
base_url = self.get_base_url()
|
|
56
|
+
url_fetcher = self.get_url_fetcher()
|
|
57
|
+
font_config = self.get_font_config()
|
|
58
|
+
|
|
59
|
+
html_content = self.render_html(**kwargs)
|
|
60
|
+
html = weasyprint.HTML(
|
|
61
|
+
string=html_content,
|
|
62
|
+
base_url=base_url,
|
|
63
|
+
url_fetcher=url_fetcher,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self._options.setdefault(
|
|
67
|
+
'stylesheets',
|
|
68
|
+
self.get_css(base_url, url_fetcher, font_config),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return html.render(
|
|
72
|
+
font_config=font_config,
|
|
73
|
+
**self._options,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def write_pdf(self, output=None, **kwargs):
|
|
77
|
+
document = self.get_document(**kwargs)
|
|
78
|
+
return document.write_pdf(target=output, **self._options)
|
|
79
|
+
|
|
80
|
+
def write_object(self, file_obj, **kwargs):
|
|
81
|
+
return self.write_pdf(output=file_obj, **kwargs)
|