oxutils 0.1.1__py3-none-any.whl → 0.1.5__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.
Files changed (56) hide show
  1. oxutils/__init__.py +1 -1
  2. oxutils/audit/settings.py +1 -16
  3. oxutils/audit/utils.py +22 -0
  4. oxutils/conf.py +1 -3
  5. oxutils/constants.py +2 -0
  6. oxutils/context/__init__.py +0 -0
  7. oxutils/context/site_name_processor.py +11 -0
  8. oxutils/currency/__init__.py +0 -0
  9. oxutils/currency/admin.py +57 -0
  10. oxutils/currency/apps.py +7 -0
  11. oxutils/currency/controllers.py +79 -0
  12. oxutils/currency/enums.py +7 -0
  13. oxutils/currency/migrations/0001_initial.py +41 -0
  14. oxutils/currency/migrations/__init__.py +0 -0
  15. oxutils/currency/models.py +100 -0
  16. oxutils/currency/schemas.py +38 -0
  17. oxutils/currency/tests.py +3 -0
  18. oxutils/currency/utils.py +69 -0
  19. oxutils/functions.py +5 -2
  20. oxutils/logger/receivers.py +0 -2
  21. oxutils/oxiliere/__init__.py +0 -0
  22. oxutils/oxiliere/admin.py +3 -0
  23. oxutils/oxiliere/apps.py +6 -0
  24. oxutils/oxiliere/cacheops.py +7 -0
  25. oxutils/oxiliere/caches.py +33 -0
  26. oxutils/oxiliere/controllers.py +36 -0
  27. oxutils/oxiliere/enums.py +10 -0
  28. oxutils/oxiliere/management/__init__.py +0 -0
  29. oxutils/oxiliere/management/commands/__init__.py +0 -0
  30. oxutils/oxiliere/management/commands/init_oxiliere_system.py +86 -0
  31. oxutils/oxiliere/middleware.py +97 -0
  32. oxutils/oxiliere/migrations/__init__.py +0 -0
  33. oxutils/oxiliere/models.py +55 -0
  34. oxutils/oxiliere/permissions.py +104 -0
  35. oxutils/oxiliere/schemas.py +65 -0
  36. oxutils/oxiliere/settings.py +17 -0
  37. oxutils/oxiliere/tests.py +3 -0
  38. oxutils/oxiliere/utils.py +76 -0
  39. oxutils/pdf/__init__.py +10 -0
  40. oxutils/pdf/printer.py +81 -0
  41. oxutils/pdf/utils.py +94 -0
  42. oxutils/pdf/views.py +208 -0
  43. oxutils/s3/storages.py +4 -4
  44. oxutils/settings.py +15 -13
  45. oxutils/users/__init__.py +0 -0
  46. oxutils/users/admin.py +3 -0
  47. oxutils/users/apps.py +6 -0
  48. oxutils/users/migrations/__init__.py +0 -0
  49. oxutils/users/models.py +88 -0
  50. oxutils/users/tests.py +3 -0
  51. oxutils/users/utils.py +15 -0
  52. {oxutils-0.1.1.dist-info → oxutils-0.1.5.dist-info}/METADATA +92 -11
  53. oxutils-0.1.5.dist-info/RECORD +88 -0
  54. {oxutils-0.1.1.dist-info → oxutils-0.1.5.dist-info}/WHEEL +1 -1
  55. oxutils/locale/fr/LC_MESSAGES/django.mo +0 -0
  56. oxutils-0.1.1.dist-info/RECORD +0 -45
oxutils/pdf/utils.py ADDED
@@ -0,0 +1,94 @@
1
+ import logging
2
+ import mimetypes
3
+ import os
4
+ from functools import lru_cache
5
+ from pathlib import Path
6
+ from urllib.parse import urlparse
7
+
8
+ import weasyprint
9
+
10
+ from django.conf import settings
11
+ from django.contrib.staticfiles.finders import find
12
+ from django.contrib.staticfiles.storage import staticfiles_storage
13
+ from django.core.files.storage import default_storage
14
+ from django.urls import get_script_prefix
15
+
16
+
17
+ log = logging.getLogger(__name__)
18
+
19
+
20
+ @lru_cache(maxsize=None)
21
+ def get_reversed_hashed_files():
22
+ return {v: k for k, v in staticfiles_storage.hashed_files.items()}
23
+
24
+
25
+ def django_url_fetcher(url, *args, **kwargs):
26
+ # attempt to load file:// paths to Django MEDIA or STATIC files directly from disk
27
+ if url.startswith('file:'):
28
+ log.debug('Attempt to fetch from %s', url)
29
+ mime_type, encoding = mimetypes.guess_type(url)
30
+ url_path = urlparse(url).path
31
+ data = {
32
+ 'mime_type': mime_type,
33
+ 'encoding': encoding,
34
+ 'filename': Path(url_path).name,
35
+ }
36
+
37
+ default_media_url = settings.MEDIA_URL in ('', get_script_prefix())
38
+ if not default_media_url and url_path.startswith(settings.MEDIA_URL):
39
+ log.debug('URL contains MEDIA_URL (%s)', settings.MEDIA_URL)
40
+ cleaned_media_root = str(settings.MEDIA_ROOT)
41
+ if not cleaned_media_root.endswith('/'):
42
+ cleaned_media_root += '/'
43
+ absolute_path = url_path.replace(settings.MEDIA_URL, cleaned_media_root, 1)
44
+ log.debug('Cleaned path: %s', absolute_path)
45
+ data['file_obj'] = default_storage.open(absolute_path, 'rb')
46
+ data['redirected_url'] = 'file://' + absolute_path
47
+ return data
48
+
49
+ # path looks like a static file based on configured STATIC_URL
50
+ elif settings.STATIC_URL and url_path.startswith(settings.STATIC_URL):
51
+ log.debug('URL contains STATIC_URL (%s)', settings.STATIC_URL)
52
+ # strip the STATIC_URL prefix to get the relative filesystem path
53
+ relative_path = url_path.replace(settings.STATIC_URL, '', 1)
54
+ # detect hashed files storage and get path with un-hashed filename
55
+ if not settings.DEBUG and hasattr(staticfiles_storage, 'hashed_files'):
56
+ log.debug('Hashed static files storage detected')
57
+ relative_path = get_reversed_hashed_files()[relative_path]
58
+ data['filename'] = Path(relative_path).name
59
+ log.debug('Cleaned path: %s', relative_path)
60
+ # find the absolute path using the static file finders
61
+ absolute_path = find(relative_path)
62
+ log.debug('Static file finder returned: %s', absolute_path)
63
+ if absolute_path:
64
+ log.debug('Loading static file: %s', absolute_path)
65
+ data['file_obj'] = open(absolute_path, 'rb') # noqa: PTH123
66
+ data['redirected_url'] = 'file://' + absolute_path
67
+ return data
68
+
69
+ # Fall back to weasyprint default fetcher for http/s: and file: paths
70
+ # that did not match MEDIA_URL or STATIC_URL.
71
+ log.debug('Forwarding to weasyprint.default_url_fetcher: %s', url)
72
+ return weasyprint.default_url_fetcher(url, *args, **kwargs)
73
+
74
+
75
+ def get_stylesheet_path(relative_path):
76
+ if settings.DEBUG:
77
+ path = find(relative_path)
78
+ if path:
79
+ return path
80
+ print('Le DEBUG est False')
81
+ static_root_path = os.path.join(settings.STATIC_ROOT, relative_path)
82
+ if os.path.exists(static_root_path):
83
+ return static_root_path
84
+
85
+ return None
86
+
87
+
88
+ def get_stylesheets(*relative_paths):
89
+ stylesheets = []
90
+ for path in relative_paths:
91
+ resolved = get_stylesheet_path(path)
92
+ if resolved:
93
+ stylesheets.append(resolved)
94
+ return stylesheets
oxutils/pdf/views.py ADDED
@@ -0,0 +1,208 @@
1
+ import weasyprint
2
+
3
+ from django.conf import settings
4
+ from django.template.response import TemplateResponse
5
+ from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
6
+
7
+ from oxutils.pdf.utils import django_url_fetcher
8
+
9
+
10
+ class WeasyTemplateResponse(TemplateResponse):
11
+ def __init__(
12
+ self,
13
+ request,
14
+ template,
15
+ context=None,
16
+ content_type=None,
17
+ status=None,
18
+ charset=None,
19
+ using=None,
20
+ headers=None,
21
+ filename=None,
22
+ attachment=True,
23
+ stylesheets=None,
24
+ options=None,
25
+ ):
26
+ """
27
+ An HTTP response class with template and context rendered to a PDF document.
28
+
29
+ Django TemplateResponse arguments:
30
+
31
+ :param request: the request object
32
+ :param template: template to use to render the response
33
+ :param context: context to use to render the response
34
+ :param content_type: content type of the response (default: 'application/pdf')
35
+ :param status: status code of the response (default: 200)
36
+ :param charset: character set of the response (default: settings.DEFAULT_CHARSET)
37
+ :param using: template engine to use (default: 'django')
38
+ :param headers: dictionary of headers to use in the response
39
+
40
+ WeasyPrint specific arguments:
41
+
42
+ :param filename: set `Content-Disposition` to use this filename
43
+ :param attachment: set `Content-Disposition` 'attachment';
44
+ A `filename` must be given to enable this even if set to `True`.
45
+ (default: `True`)
46
+ :param stylesheets: list of additional stylesheets
47
+ :param options: dictionary of options passed to WeasyPrint
48
+ """
49
+ self._stylesheets = stylesheets or []
50
+ self._options = options.copy() if options else {}
51
+
52
+ kwargs = dict(
53
+ context=context,
54
+ content_type=content_type or WeasyTemplateResponseMixin.content_type,
55
+ status=status,
56
+ charset=charset,
57
+ using=using,
58
+ headers=headers,
59
+ )
60
+ super().__init__(request, template, **kwargs)
61
+
62
+ if filename:
63
+ display = 'attachment' if attachment else 'inline'
64
+ self['Content-Disposition'] = f'{display};filename="{filename}"'
65
+
66
+ def get_base_url(self):
67
+ """
68
+ Determine base URL to fetch CSS or other files referenced with relative
69
+ paths in the HTML files using the `WEASYPRINT_BASEURL` setting or
70
+ fall back to using the root path of the URL used in the request.
71
+ """
72
+ return getattr(
73
+ settings, 'WEASYPRINT_BASEURL',
74
+ self._request.build_absolute_uri('/')
75
+ )
76
+
77
+ def get_url_fetcher(self):
78
+ """
79
+ Determine the URL fetcher to fetch CSS, images, fonts, etc. from.
80
+ """
81
+ return django_url_fetcher
82
+
83
+ def get_font_config(self):
84
+ """
85
+ A FreeType font configuration to handle @font-config rules.
86
+ """
87
+ return weasyprint.text.fonts.FontConfiguration()
88
+
89
+ def get_css(self, base_url, url_fetcher, font_config):
90
+ """
91
+ Load additional stylesheets.
92
+ """
93
+ return [
94
+ weasyprint.CSS(
95
+ value,
96
+ base_url=base_url,
97
+ url_fetcher=url_fetcher,
98
+ font_config=font_config,
99
+ )
100
+ for value
101
+ in self._stylesheets
102
+ ]
103
+
104
+ def get_document(self):
105
+ """
106
+ Returns a :class:`~document.Document` object which provides
107
+ access to individual pages and various meta-data.
108
+
109
+ See :meth:`weasyprint.HTML.render` and
110
+ :meth:`weasyprint.document.Document.write_pdf` on how to generate a
111
+ PDF file.
112
+ """
113
+ base_url = self.get_base_url()
114
+ url_fetcher = self.get_url_fetcher()
115
+ font_config = self.get_font_config()
116
+
117
+ html = weasyprint.HTML(
118
+ string=super().rendered_content,
119
+ base_url=base_url,
120
+ url_fetcher=url_fetcher,
121
+ )
122
+
123
+ self._options.setdefault(
124
+ 'stylesheets',
125
+ self.get_css(base_url, url_fetcher, font_config),
126
+ )
127
+
128
+ return html.render(
129
+ font_config=font_config,
130
+ **self._options,
131
+ )
132
+
133
+ @property
134
+ def rendered_content(self):
135
+ """
136
+ Returns rendered PDF pages.
137
+ """
138
+ document = self.get_document()
139
+ return document.write_pdf(**self._options)
140
+
141
+
142
+ class WeasyTemplateResponseMixin(TemplateResponseMixin):
143
+ """
144
+ Mixin for a CBV creating a ``WeasyTemplateResponse`` using the configured template.
145
+ """
146
+ response_class = WeasyTemplateResponse
147
+ content_type = 'application/pdf'
148
+ pdf_filename = None
149
+ pdf_attachment = True
150
+ pdf_stylesheets = []
151
+ pdf_options = {}
152
+
153
+ def get_pdf_filename(self):
154
+ """
155
+ Returns :attr:`pdf_filename` value by default.
156
+
157
+ If left blank the browser will display the PDF inline.
158
+ Otherwise it will pop up the "Save as.." dialog.
159
+
160
+ :rtype: :func:`str`
161
+ """
162
+ return self.pdf_filename
163
+
164
+ def get_pdf_stylesheets(self):
165
+ """
166
+ Returns a list of stylesheet filenames to use when rendering.
167
+
168
+ :rtype: :func:`list`
169
+ """
170
+ return self.pdf_stylesheets
171
+
172
+ def get_pdf_options(self):
173
+ """
174
+ Returns dictionary of WeasyPrint options.
175
+ """
176
+ return self.pdf_options
177
+
178
+ def render_to_response(self, context, **response_kwargs):
179
+ """
180
+ Renders PDF document and prepares response by calling on
181
+ :attr:`response_class` (default: :class:`WeasyTemplateResponse`).
182
+
183
+ :returns: Django HTTP response
184
+ :rtype: :class:`django.http.HttpResponse`
185
+ """
186
+ response_kwargs.update({
187
+ 'attachment': self.pdf_attachment,
188
+ 'filename': self.get_pdf_filename(),
189
+ 'stylesheets': self.get_pdf_stylesheets(),
190
+ 'options': self.get_pdf_options(),
191
+ })
192
+ return super().render_to_response(
193
+ context, **response_kwargs
194
+ )
195
+
196
+
197
+ class WeasyTemplateView(WeasyTemplateResponseMixin, ContextMixin, View):
198
+ """
199
+ Concrete view for serving PDF files.
200
+
201
+ .. code-block:: python
202
+
203
+ class HelloPDFView(WeasyTemplateView):
204
+ template_name = "hello.html"
205
+ """
206
+ def get(self, request, *args, **kwargs):
207
+ context = self.get_context_data(**kwargs)
208
+ return self.render_to_response(context)
oxutils/s3/storages.py CHANGED
@@ -53,7 +53,7 @@ class PublicMediaStorage(S3Boto3Storage):
53
53
  self.access_key = oxi_settings.default_s3_access_key_id
54
54
  self.secret_key = oxi_settings.default_s3_secret_access_key
55
55
  self.bucket_name = oxi_settings.default_s3_storage_bucket_name
56
- self.custom_domain = oxi_settings.default_s3_s3_custom_domain
56
+ self.custom_domain = oxi_settings.default_s3_custom_domain
57
57
 
58
58
  self.location = oxi_settings.default_s3_location
59
59
  self.default_acl = oxi_settings.default_s3_default_acl
@@ -79,7 +79,7 @@ class PrivateMediaStorage(S3Boto3Storage):
79
79
  self.access_key = oxi_settings.private_s3_access_key_id
80
80
  self.secret_key = oxi_settings.private_s3_secret_access_key
81
81
  self.bucket_name = oxi_settings.private_s3_storage_bucket_name
82
- self.custom_domain = oxi_settings.private_s3_s3_custom_domain
82
+ self.custom_domain = oxi_settings.private_s3_custom_domain
83
83
  self.location = oxi_settings.private_s3_location
84
84
  self.default_acl = oxi_settings.private_s3_default_acl
85
85
  self.file_overwrite = False
@@ -107,12 +107,12 @@ class LogStorage(S3Boto3Storage):
107
107
  self.access_key = oxi_settings.private_s3_access_key_id
108
108
  self.secret_key = oxi_settings.private_s3_secret_access_key
109
109
  self.bucket_name = oxi_settings.private_s3_storage_bucket_name
110
- self.custom_domain = oxi_settings.private_s3_s3_custom_domain
110
+ self.custom_domain = oxi_settings.private_s3_custom_domain
111
111
  else:
112
112
  self.access_key = oxi_settings.log_s3_access_key_id
113
113
  self.secret_key = oxi_settings.log_s3_secret_access_key
114
114
  self.bucket_name = oxi_settings.log_s3_storage_bucket_name
115
- self.custom_domain = oxi_settings.log_s3_s3_custom_domain
115
+ self.custom_domain = oxi_settings.log_s3_custom_domain
116
116
 
117
117
  self.location = f'{oxi_settings.log_s3_location}/{oxi_settings.service_name}'
118
118
  self.default_acl = oxi_settings.log_s3_default_acl
oxutils/settings.py CHANGED
@@ -21,6 +21,8 @@ class OxUtilsSettings(BaseSettings):
21
21
 
22
22
  # Service
23
23
  service_name: Optional[str] = 'Oxutils'
24
+ site_name: Optional[str] = 'Oxiliere'
25
+ site_domain: Optional[str] = 'oxiliere.com'
24
26
 
25
27
  # Auth JWT Settings (JWT_SIGNING_KEY)
26
28
  jwt_signing_key: Optional[str] = None
@@ -52,7 +54,7 @@ class OxUtilsSettings(BaseSettings):
52
54
  default_s3_secret_access_key: Optional[str] = None
53
55
  default_s3_storage_bucket_name: Optional[str] = None
54
56
  default_s3_default_acl: str = Field('public-read')
55
- default_s3_s3_custom_domain: Optional[str] = None
57
+ default_s3_custom_domain: Optional[str] = None
56
58
  default_s3_location: str = Field('media')
57
59
  default_s3_storage: str = Field('oxutils.s3.storages.PublicMediaStorage')
58
60
 
@@ -62,7 +64,7 @@ class OxUtilsSettings(BaseSettings):
62
64
  private_s3_secret_access_key: Optional[str] = None
63
65
  private_s3_storage_bucket_name: Optional[str] = None
64
66
  private_s3_default_acl: str = Field('private')
65
- private_s3_s3_custom_domain: Optional[str] = None
67
+ private_s3_custom_domain: Optional[str] = None
66
68
  private_s3_location: str = Field('private')
67
69
  private_s3_storage: str = Field('oxutils.s3.storages.PrivateMediaStorage')
68
70
 
@@ -73,7 +75,7 @@ class OxUtilsSettings(BaseSettings):
73
75
  log_s3_secret_access_key: Optional[str] = None
74
76
  log_s3_storage_bucket_name: Optional[str] = None
75
77
  log_s3_default_acl: str = Field('private')
76
- log_s3_s3_custom_domain: Optional[str] = None
78
+ log_s3_custom_domain: Optional[str] = None
77
79
  log_s3_location: str = Field('oxi_logs')
78
80
  log_s3_storage: str = Field('oxutils.s3.storages.LogStorage')
79
81
 
@@ -102,7 +104,7 @@ class OxUtilsSettings(BaseSettings):
102
104
  self.default_s3_access_key_id,
103
105
  self.default_s3_secret_access_key,
104
106
  self.default_s3_storage_bucket_name,
105
- self.default_s3_s3_custom_domain
107
+ self.default_s3_custom_domain
106
108
  )
107
109
  elif not self.use_static_s3:
108
110
  raise ValueError(
@@ -116,7 +118,7 @@ class OxUtilsSettings(BaseSettings):
116
118
  self.private_s3_access_key_id,
117
119
  self.private_s3_secret_access_key,
118
120
  self.private_s3_storage_bucket_name,
119
- self.private_s3_s3_custom_domain
121
+ self.private_s3_custom_domain
120
122
  )
121
123
 
122
124
  # Validate log S3
@@ -127,7 +129,7 @@ class OxUtilsSettings(BaseSettings):
127
129
  self.log_s3_access_key_id,
128
130
  self.log_s3_secret_access_key,
129
131
  self.log_s3_storage_bucket_name,
130
- self.log_s3_s3_custom_domain
132
+ self.log_s3_custom_domain
131
133
  )
132
134
  elif not self.use_private_s3:
133
135
  raise ValueError(
@@ -166,11 +168,11 @@ class OxUtilsSettings(BaseSettings):
166
168
  """Validate required S3 configuration fields."""
167
169
  missing_fields = []
168
170
  if not access_key:
169
- missing_fields.append(f'OXI_{name.upper()}_ACCESS_KEY_ID')
171
+ missing_fields.append(f'OXI_{name.upper()}_S3_ACCESS_KEY_ID')
170
172
  if not secret_key:
171
- missing_fields.append(f'OXI_{name.upper()}_SECRET_ACCESS_KEY')
173
+ missing_fields.append(f'OXI_{name.upper()}_S3_SECRET_ACCESS_KEY')
172
174
  if not bucket:
173
- missing_fields.append(f'OXI_{name.upper()}_STORAGE_BUCKET_NAME')
175
+ missing_fields.append(f'OXI_{name.upper()}_S3_STORAGE_BUCKET_NAME')
174
176
  if not domain:
175
177
  missing_fields.append(f'OXI_{name.upper()}_S3_CUSTOM_DOMAIN')
176
178
 
@@ -194,7 +196,7 @@ class OxUtilsSettings(BaseSettings):
194
196
  # Use static S3 credentials but keep default_s3 specific values (location, etc.)
195
197
  domain = self.static_s3_custom_domain
196
198
  else:
197
- domain = self.default_s3_s3_custom_domain
199
+ domain = self.default_s3_custom_domain
198
200
  return f'https://{domain}/{self.default_s3_location}/'
199
201
 
200
202
  raise ImproperlyConfigured(
@@ -207,7 +209,7 @@ class OxUtilsSettings(BaseSettings):
207
209
  raise ImproperlyConfigured(
208
210
  "Private S3 is not enabled. Set OXI_USE_PRIVATE_S3=True."
209
211
  )
210
- return f'https://{self.private_s3_s3_custom_domain}/{self.private_s3_location}/'
212
+ return f'https://{self.private_s3_custom_domain}/{self.private_s3_location}/'
211
213
 
212
214
  def get_log_storage_url(self) -> str:
213
215
  """Get log storage URL."""
@@ -217,9 +219,9 @@ class OxUtilsSettings(BaseSettings):
217
219
  )
218
220
  if self.use_private_s3_as_log:
219
221
  # Use private S3 credentials but keep log_s3 specific values (location, etc.)
220
- domain = self.private_s3_s3_custom_domain
222
+ domain = self.private_s3_custom_domain
221
223
  else:
222
- domain = self.log_s3_s3_custom_domain
224
+ domain = self.log_s3_custom_domain
223
225
  return f'https://{domain}/{self.log_s3_location}/{self.service_name}/'
224
226
 
225
227
 
File without changes
oxutils/users/admin.py ADDED
@@ -0,0 +1,3 @@
1
+ from django.contrib import admin
2
+
3
+ # Register your models here.
oxutils/users/apps.py ADDED
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class UsersConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'users'
File without changes
@@ -0,0 +1,88 @@
1
+ from django.db import models
2
+
3
+ from django.contrib.auth.models import AbstractUser, BaseUserManager
4
+ from django.db import models
5
+ from django.utils.translation import gettext_lazy as _
6
+ from safedelete.models import SafeDeleteModel
7
+ from safedelete.models import SOFT_DELETE_CASCADE
8
+ from auditlog.registry import auditlog
9
+ from oxutils.models import BaseModelMixin
10
+
11
+
12
+
13
+ class UserManager(BaseUserManager):
14
+ """
15
+ Gestionnaire personnalisé pour le modèle User
16
+ """
17
+ def create_user(self, email, oxi_id, **extra_fields):
18
+ """
19
+ Crée et sauvegarde un utilisateur avec l'email et l'oxi_id donnés.
20
+ """
21
+ if not email:
22
+ raise ValueError(_('The Email field must be set'))
23
+ if not oxi_id:
24
+ raise ValueError(_('The oxi_id field must be set'))
25
+
26
+ email = self.normalize_email(email)
27
+ user = self.model(email=email, oxi_id=oxi_id, **extra_fields)
28
+ user.save(using=self._db)
29
+ return user
30
+
31
+ def create_superuser(self, email, oxi_id, **extra_fields):
32
+ """
33
+ Crée et sauvegarde un superutilisateur avec l'email et l'oxi_id donnés.
34
+ """
35
+ extra_fields.setdefault('is_staff', True)
36
+ extra_fields.setdefault('is_superuser', True)
37
+ extra_fields.setdefault('is_active', True)
38
+
39
+ if extra_fields.get('is_staff') is not True:
40
+ raise ValueError(_('Superuser must have is_staff=True.'))
41
+ if extra_fields.get('is_superuser') is not True:
42
+ raise ValueError(_('Superuser must have is_superuser=True.'))
43
+
44
+ return self.create_user(email, oxi_id, **extra_fields)
45
+
46
+
47
+ class User(AbstractUser, SafeDeleteModel, BaseModelMixin):
48
+ """
49
+ Modèle d'utilisateur personnalisé qui utilise l'email comme identifiant unique
50
+ et intègre la suppression sécurisée (soft delete)
51
+ """
52
+ _safedelete_policy = SOFT_DELETE_CASCADE
53
+
54
+ # Suppression du champ username qui est obligatoire dans AbstractUser
55
+ username = None
56
+ password = None # Don't need password
57
+
58
+ oxi_id = models.UUIDField(unique=True) # id venant de auth.oxi.com
59
+ email = models.EmailField(unique=True)
60
+ is_active = models.BooleanField(default=True)
61
+ subscription_plan = models.CharField(max_length=255, null=True, blank=True)
62
+ subscription_status = models.CharField(max_length=255, null=True, blank=True)
63
+ subscription_end_date = models.DateTimeField(null=True, blank=True)
64
+
65
+ USERNAME_FIELD = "email"
66
+ REQUIRED_FIELDS = []
67
+
68
+ def __str__(self):
69
+ return self.email
70
+
71
+ objects = UserManager()
72
+
73
+ class Meta:
74
+ verbose_name = _('utilisateur')
75
+ verbose_name_plural = _('utilisateurs')
76
+ ordering = ['-created_at']
77
+ indexes = [
78
+ models.Index(fields=['oxi_id']),
79
+ models.Index(fields=['email']),
80
+ ]
81
+
82
+ def __str__(self):
83
+ return self.email
84
+
85
+
86
+
87
+ # Enregistrement du modèle User pour l'audit logging
88
+ auditlog.register(User)
oxutils/users/tests.py ADDED
@@ -0,0 +1,3 @@
1
+ from django.test import TestCase
2
+
3
+ # Create your tests here.
oxutils/users/utils.py ADDED
@@ -0,0 +1,15 @@
1
+ from django.contrib.auth import get_user_model
2
+
3
+
4
+ def update_user(oxid: str, data: dict):
5
+ user = get_user_model().objects.get(oxi_id=oxid)
6
+ changes = False
7
+ if data:
8
+ for key, value in data.items():
9
+ if hasattr(user, key):
10
+ setattr(user, key, value)
11
+ changes = True
12
+ if changes:
13
+ user.save()
14
+
15
+ return user