oxutils 0.1.2__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/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.5.dist-info}/METADATA +92 -11
- oxutils-0.1.5.dist-info/RECORD +88 -0
- {oxutils-0.1.2.dist-info → oxutils-0.1.5.dist-info}/WHEEL +1 -1
- oxutils/locale/fr/LC_MESSAGES/django.mo +0 -0
- oxutils-0.1.2.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/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
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oxutils
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Production-ready utilities for Django applications in the Oxiliere ecosystem
|
|
5
5
|
Keywords: django,utilities,jwt,s3,audit,logging,celery,structlog
|
|
6
6
|
Author: Edimedia Mutoke
|
|
@@ -13,7 +13,6 @@ Classifier: Intended Audience :: Developers
|
|
|
13
13
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
17
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
18
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
@@ -22,7 +21,6 @@ Requires-Dist: celery>=5.5.3
|
|
|
22
21
|
Requires-Dist: cryptography>=46.0.3
|
|
23
22
|
Requires-Dist: django-auditlog>=3.3.0
|
|
24
23
|
Requires-Dist: django-celery-results>=2.6.0
|
|
25
|
-
Requires-Dist: django-cid>=3.0
|
|
26
24
|
Requires-Dist: django-extensions>=4.1
|
|
27
25
|
Requires-Dist: django-ninja>=1.5.0
|
|
28
26
|
Requires-Dist: django-ninja-extra>=0.30.6
|
|
@@ -32,7 +30,7 @@ Requires-Dist: jwcrypto>=1.5.6
|
|
|
32
30
|
Requires-Dist: pydantic-settings>=2.12.0
|
|
33
31
|
Requires-Dist: pyjwt>=2.10.1
|
|
34
32
|
Requires-Dist: requests>=2.32.5
|
|
35
|
-
Requires-Python: >=3.
|
|
33
|
+
Requires-Python: >=3.12
|
|
36
34
|
Project-URL: Changelog, https://github.com/oxiliere/oxutils/blob/main/CHANGELOG.md
|
|
37
35
|
Project-URL: Documentation, https://github.com/oxiliere/oxutils/tree/main/docs
|
|
38
36
|
Project-URL: Homepage, https://github.com/oxiliere/oxutils
|
|
@@ -45,9 +43,9 @@ Description-Content-Type: text/markdown
|
|
|
45
43
|
**Production-ready utilities for Django applications in the Oxiliere ecosystem.**
|
|
46
44
|
|
|
47
45
|
[](https://pypi.org/project/oxutils/)
|
|
48
|
-
[](https://www.python.org/)
|
|
49
47
|
[](https://www.djangoproject.com/)
|
|
50
|
-
[](tests/)
|
|
51
49
|
[](LICENSE)
|
|
52
50
|
[](https://github.com/astral-sh/ruff)
|
|
53
51
|
|
|
@@ -55,11 +53,15 @@ Description-Content-Type: text/markdown
|
|
|
55
53
|
|
|
56
54
|
- 🔐 **JWT Authentication** - RS256 with JWKS caching
|
|
57
55
|
- 📦 **S3 Storage** - Static, media, private, and log backends
|
|
58
|
-
- 📝 **Structured Logging** - JSON logs with
|
|
56
|
+
- 📝 **Structured Logging** - JSON logs with automatic request tracking
|
|
59
57
|
- 🔍 **Audit System** - Change tracking with S3 export
|
|
60
58
|
- ⚙️ **Celery Integration** - Pre-configured task processing
|
|
61
59
|
- 🛠️ **Django Mixins** - UUID, timestamps, user tracking
|
|
62
60
|
- ⚡ **Custom Exceptions** - Standardized API errors
|
|
61
|
+
- 🎨 **Context Processors** - Site name and domain for templates
|
|
62
|
+
- 💱 **Currency Module** - Multi-source exchange rates (BCC/OXR)
|
|
63
|
+
- 📄 **PDF Generation** - WeasyPrint integration for Django
|
|
64
|
+
- 🏢 **Multi-Tenant** - PostgreSQL schema-based isolation
|
|
63
65
|
|
|
64
66
|
---
|
|
65
67
|
|
|
@@ -82,12 +84,12 @@ uv add oxutils
|
|
|
82
84
|
from oxutils.conf import UTILS_APPS, AUDIT_MIDDLEWARE
|
|
83
85
|
|
|
84
86
|
INSTALLED_APPS = [
|
|
85
|
-
*UTILS_APPS, # structlog, auditlog,
|
|
87
|
+
*UTILS_APPS, # structlog, auditlog, celery_results
|
|
86
88
|
# your apps...
|
|
87
89
|
]
|
|
88
90
|
|
|
89
91
|
MIDDLEWARE = [
|
|
90
|
-
*AUDIT_MIDDLEWARE, #
|
|
92
|
+
*AUDIT_MIDDLEWARE, # RequestMiddleware, Auditlog
|
|
91
93
|
# your middleware...
|
|
92
94
|
]
|
|
93
95
|
```
|
|
@@ -126,10 +128,22 @@ class Product(BaseModelMixin): # UUID + timestamps + is_active
|
|
|
126
128
|
# Custom Exceptions
|
|
127
129
|
from oxutils.exceptions import NotFoundException
|
|
128
130
|
raise NotFoundException(detail="User not found")
|
|
131
|
+
|
|
132
|
+
# Context Processors
|
|
133
|
+
# settings.py
|
|
134
|
+
TEMPLATES = [{
|
|
135
|
+
'OPTIONS': {
|
|
136
|
+
'context_processors': [
|
|
137
|
+
'oxutils.context.site_name_processor.site_name',
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
}]
|
|
141
|
+
# Now {{ site_name }} and {{ site_domain }} are available in templates
|
|
129
142
|
```
|
|
130
143
|
|
|
131
144
|
## Documentation
|
|
132
145
|
|
|
146
|
+
### Core Modules
|
|
133
147
|
- **[Settings](docs/settings.md)** - Configuration reference
|
|
134
148
|
- **[JWT](docs/jwt.md)** - Authentication
|
|
135
149
|
- **[S3](docs/s3.md)** - Storage backends
|
|
@@ -138,9 +152,14 @@ raise NotFoundException(detail="User not found")
|
|
|
138
152
|
- **[Mixins](docs/mixins.md)** - Model/service mixins
|
|
139
153
|
- **[Celery](docs/celery.md)** - Task processing
|
|
140
154
|
|
|
155
|
+
### Additional Modules
|
|
156
|
+
- **[Currency](docs/currency.md)** - Exchange rates management
|
|
157
|
+
- **[PDF](docs/pdf.md)** - PDF generation with WeasyPrint
|
|
158
|
+
- **[Oxiliere](docs/oxiliere.md)** - Multi-tenant architecture
|
|
159
|
+
|
|
141
160
|
## Requirements
|
|
142
161
|
|
|
143
|
-
- Python 3.
|
|
162
|
+
- Python 3.12+
|
|
144
163
|
- Django 5.0+
|
|
145
164
|
- PostgreSQL (recommended)
|
|
146
165
|
|
|
@@ -150,7 +169,7 @@ raise NotFoundException(detail="User not found")
|
|
|
150
169
|
git clone https://github.com/oxiliere/oxutils.git
|
|
151
170
|
cd oxutils
|
|
152
171
|
uv sync
|
|
153
|
-
uv run pytest #
|
|
172
|
+
uv run pytest # 201 tests passing, 4 skipped
|
|
154
173
|
```
|
|
155
174
|
|
|
156
175
|
### Creating Migrations
|
|
@@ -165,6 +184,19 @@ uv run make_migrations.py
|
|
|
165
184
|
|
|
166
185
|
See [MIGRATIONS.md](MIGRATIONS.md) for detailed documentation.
|
|
167
186
|
|
|
187
|
+
## Optional Dependencies
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Multi-tenant support
|
|
191
|
+
uv add oxutils[oxiliere]
|
|
192
|
+
|
|
193
|
+
# PDF generation
|
|
194
|
+
uv add oxutils[pdf]
|
|
195
|
+
|
|
196
|
+
# Development tools
|
|
197
|
+
uv add oxutils[dev]
|
|
198
|
+
```
|
|
199
|
+
|
|
168
200
|
## Advanced Examples
|
|
169
201
|
|
|
170
202
|
### JWT with Django Ninja
|
|
@@ -199,6 +231,55 @@ export = export_logs_from_date(from_date=from_date)
|
|
|
199
231
|
print(f"Exported to {export.data.url}")
|
|
200
232
|
```
|
|
201
233
|
|
|
234
|
+
### Currency Exchange Rates
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
from oxutils.currency.models import CurrencyState
|
|
238
|
+
|
|
239
|
+
# Sync rates from BCC (with OXR fallback)
|
|
240
|
+
state = CurrencyState.sync()
|
|
241
|
+
|
|
242
|
+
# Get latest rates
|
|
243
|
+
latest = CurrencyState.objects.latest()
|
|
244
|
+
usd_rate = latest.currencies.get(code='USD').rate
|
|
245
|
+
eur_rate = latest.currencies.get(code='EUR').rate
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### PDF Generation
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
from oxutils.pdf.printer import Printer
|
|
252
|
+
from oxutils.pdf.views import WeasyTemplateView
|
|
253
|
+
|
|
254
|
+
# Standalone PDF generation
|
|
255
|
+
printer = Printer(
|
|
256
|
+
template_name='invoice.html',
|
|
257
|
+
context={'invoice': invoice},
|
|
258
|
+
stylesheets=['css/invoice.css']
|
|
259
|
+
)
|
|
260
|
+
pdf_bytes = printer.write_pdf()
|
|
261
|
+
|
|
262
|
+
# Class-based view
|
|
263
|
+
class InvoicePDFView(WeasyTemplateView):
|
|
264
|
+
template_name = 'invoice.html'
|
|
265
|
+
pdf_filename = 'invoice.pdf'
|
|
266
|
+
pdf_stylesheets = ['css/invoice.css']
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Multi-Tenant Setup
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
# settings.py
|
|
273
|
+
TENANT_MODEL = "oxiliere.Tenant"
|
|
274
|
+
MIDDLEWARE = [
|
|
275
|
+
'oxutils.oxiliere.middleware.TenantMainMiddleware', # First!
|
|
276
|
+
# other middleware...
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
# All requests must include X-Organization-ID header
|
|
280
|
+
# Data is automatically isolated per tenant schema
|
|
281
|
+
```
|
|
282
|
+
|
|
202
283
|
## License
|
|
203
284
|
|
|
204
285
|
Apache 2.0 License - see [LICENSE](LICENSE)
|