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.
- 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/s3/storages.py +4 -4
- oxutils/settings.py +15 -13
- 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.1.dist-info → oxutils-0.1.5.dist-info}/METADATA +92 -11
- oxutils-0.1.5.dist-info/RECORD +88 -0
- {oxutils-0.1.1.dist-info → oxutils-0.1.5.dist-info}/WHEEL +1 -1
- oxutils/locale/fr/LC_MESSAGES/django.mo +0 -0
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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()}
|
|
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()}
|
|
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()}
|
|
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.
|
|
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.
|
|
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.
|
|
222
|
+
domain = self.private_s3_custom_domain
|
|
221
223
|
else:
|
|
222
|
-
domain = self.
|
|
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
oxutils/users/apps.py
ADDED
|
File without changes
|
oxutils/users/models.py
ADDED
|
@@ -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
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
|