djgentelella 0.5.4__py3-none-any.whl → 0.5.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.
- djgentelella/__init__.py +1 -1
- djgentelella/fields/secure.py +109 -0
- djgentelella/locale/es/LC_MESSAGES/django.mo +0 -0
- djgentelella/locale/es/LC_MESSAGES/django.po +151 -102
- djgentelella/models_utils.py +87 -0
- djgentelella/permission_management/__init__.py +27 -2
- djgentelella/serializers/__init__.py +14 -0
- djgentelella/serializers/secure.py +8 -0
- djgentelella/static/djgentelella.readonly.vendors.min.js +1 -1
- djgentelella/static/vendors/storymapjs/storymap.js +1 -1
- {djgentelella-0.5.4.dist-info → djgentelella-0.5.6.dist-info}/METADATA +2 -1
- {djgentelella-0.5.4.dist-info → djgentelella-0.5.6.dist-info}/RECORD +16 -14
- {djgentelella-0.5.4.dist-info → djgentelella-0.5.6.dist-info}/WHEEL +1 -1
- djgentelella/migrations/0019_alter_chunkedupload_status.py +0 -18
- {djgentelella-0.5.4.dist-info → djgentelella-0.5.6.dist-info}/AUTHORS +0 -0
- {djgentelella-0.5.4.dist-info → djgentelella-0.5.6.dist-info}/LICENSE.txt +0 -0
- {djgentelella-0.5.4.dist-info → djgentelella-0.5.6.dist-info}/top_level.txt +0 -0
djgentelella/__init__.py
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import io
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from base64 import b64encode, b64decode
|
|
6
|
+
|
|
7
|
+
from Crypto.Cipher import AES
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
from django.db import models
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_key(size=32):
|
|
13
|
+
key = os.urandom(size)
|
|
14
|
+
base64_encoded = base64.b64encode(key)
|
|
15
|
+
return base64_encoded.decode("utf-8")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_salt_session(size=16):
|
|
19
|
+
key = settings.SECRET_KEY.encode()
|
|
20
|
+
if len(key) > size:
|
|
21
|
+
return key[:size]
|
|
22
|
+
return key
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def salt_encrypt(message, session_key=None):
|
|
26
|
+
if type(message) == str:
|
|
27
|
+
message = message.encode()
|
|
28
|
+
session_key = get_salt_session()
|
|
29
|
+
file_out = io.BytesIO()
|
|
30
|
+
cipher_aes = AES.new(session_key, AES.MODE_EAX)
|
|
31
|
+
ciphertext, tag = cipher_aes.encrypt_and_digest(message)
|
|
32
|
+
[file_out.write(x) for x in (cipher_aes.nonce, tag, ciphertext)]
|
|
33
|
+
file_out.seek(0)
|
|
34
|
+
return b64encode(file_out.read())
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def salt_decrypt(message):
|
|
38
|
+
if message is None:
|
|
39
|
+
return None
|
|
40
|
+
raw_cipher_data = b64decode(message)
|
|
41
|
+
file_in = io.BytesIO(raw_cipher_data)
|
|
42
|
+
file_in.seek(0)
|
|
43
|
+
|
|
44
|
+
nonce, tag, ciphertext = [file_in.read(x) for x in (16, 16, -1)]
|
|
45
|
+
session_key = get_salt_session()
|
|
46
|
+
cipher_aes = AES.new(session_key, AES.MODE_EAX, nonce)
|
|
47
|
+
decrypted = cipher_aes.decrypt_and_verify(ciphertext, tag)
|
|
48
|
+
return decrypted
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class GTEncryptedText(models.TextField):
|
|
52
|
+
"""
|
|
53
|
+
Encrypt data using AES and secure django key use in models like:
|
|
54
|
+
in models.py
|
|
55
|
+
|
|
56
|
+
class MyModel(models.Model):
|
|
57
|
+
my_secret = GTEncryptedText(null=True, blank=True)
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def from_db_value(self, value, expression, connection):
|
|
62
|
+
if value is None:
|
|
63
|
+
return value
|
|
64
|
+
if isinstance(value, str):
|
|
65
|
+
value = value.encode()
|
|
66
|
+
return salt_decrypt(value)
|
|
67
|
+
|
|
68
|
+
def pre_save(self, model_instance, add):
|
|
69
|
+
field = getattr(model_instance, self.attname)
|
|
70
|
+
if field is None:
|
|
71
|
+
return None
|
|
72
|
+
dev = salt_encrypt(field)
|
|
73
|
+
if type(dev) == bytes:
|
|
74
|
+
dev = dev.decode()
|
|
75
|
+
return dev
|
|
76
|
+
|
|
77
|
+
def value_from_object(self, obj):
|
|
78
|
+
dev = super(GTEncryptedText, self).value_from_object(obj)
|
|
79
|
+
return dev.decode() if dev is not None else None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class GTEncryptedJSONField(models.JSONField):
|
|
83
|
+
"""
|
|
84
|
+
Encrypt data using AES and secure django key use in models can store JSON objects:
|
|
85
|
+
in models.py
|
|
86
|
+
|
|
87
|
+
class MyModel(models.Model):
|
|
88
|
+
my_secret = GTEncryptedJSONField(null=True, blank=True)
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def get_prep_value(self, value):
|
|
93
|
+
# encrypt the JSON before save to the database
|
|
94
|
+
if value is not None:
|
|
95
|
+
value = json.dumps(value)
|
|
96
|
+
encrypted_value = salt_encrypt(
|
|
97
|
+
super().get_prep_value(value).encode("utf-8")
|
|
98
|
+
)
|
|
99
|
+
return encrypted_value.decode("utf-8") # Store as text in DB
|
|
100
|
+
return value
|
|
101
|
+
|
|
102
|
+
def from_db_value(self, value, expression, connection):
|
|
103
|
+
# decrypt when loading from the database
|
|
104
|
+
if value is not None:
|
|
105
|
+
decrypted_value = salt_decrypt(value.encode("utf-8"))
|
|
106
|
+
return super().from_db_value(
|
|
107
|
+
decrypted_value.decode("utf-8"), expression, connection
|
|
108
|
+
)
|
|
109
|
+
return value
|
|
Binary file
|
|
@@ -7,7 +7,7 @@ msgid ""
|
|
|
7
7
|
msgstr ""
|
|
8
8
|
"Project-Id-Version: \n"
|
|
9
9
|
"Report-Msgid-Bugs-To: \n"
|
|
10
|
-
"POT-Creation-Date: 2025-
|
|
10
|
+
"POT-Creation-Date: 2025-12-18 21:41+0000\n"
|
|
11
11
|
"PO-Revision-Date: 2025-03-29 16:43-0600\n"
|
|
12
12
|
"Last-Translator: \n"
|
|
13
13
|
"Language-Team: \n"
|
|
@@ -18,6 +18,14 @@ msgstr ""
|
|
|
18
18
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
|
19
19
|
"X-Generator: Poedit 3.2.2\n"
|
|
20
20
|
|
|
21
|
+
#, fuzzy
|
|
22
|
+
#| msgid "Actions"
|
|
23
|
+
msgid "Action"
|
|
24
|
+
msgstr "Acciones"
|
|
25
|
+
|
|
26
|
+
msgid "History"
|
|
27
|
+
msgstr "Historial"
|
|
28
|
+
|
|
21
29
|
msgid "created"
|
|
22
30
|
msgstr "creado"
|
|
23
31
|
|
|
@@ -92,10 +100,6 @@ msgstr ""
|
|
|
92
100
|
"Se espera una lista de elementos, ej: [{name: 'nombre del archivo', "
|
|
93
101
|
"value:'representación en cadena base64'}]"
|
|
94
102
|
|
|
95
|
-
#, python-brace-format
|
|
96
|
-
msgid "Too many elements, max_file = {self.max_files}"
|
|
97
|
-
msgstr "Demasiados elementos, max_file = {self.max_files}"
|
|
98
|
-
|
|
99
103
|
msgid ""
|
|
100
104
|
"Invalid structure you need to provide {name: 'name of file', value:'base64 "
|
|
101
105
|
"string representation'}"
|
|
@@ -116,6 +120,54 @@ msgid "Invalid value not encoded as b64 o json parser error"
|
|
|
116
120
|
msgstr ""
|
|
117
121
|
"Valor inválido, debe ser un documento base64 o un error al parsear el json"
|
|
118
122
|
|
|
123
|
+
msgid "Contact"
|
|
124
|
+
msgstr "Contacto"
|
|
125
|
+
|
|
126
|
+
msgid "Date format"
|
|
127
|
+
msgstr "Formato de fecha"
|
|
128
|
+
|
|
129
|
+
msgid "Signature message"
|
|
130
|
+
msgstr "Mensaje de firma"
|
|
131
|
+
|
|
132
|
+
msgid "Font alignment"
|
|
133
|
+
msgstr "Alineación de fuente"
|
|
134
|
+
|
|
135
|
+
msgid "Font color"
|
|
136
|
+
msgstr "Color de fuente"
|
|
137
|
+
|
|
138
|
+
msgid "Font size"
|
|
139
|
+
msgstr "Tamaño de fuente"
|
|
140
|
+
|
|
141
|
+
msgid "Place"
|
|
142
|
+
msgstr "Lugar"
|
|
143
|
+
|
|
144
|
+
msgid "Reason"
|
|
145
|
+
msgstr "Razón"
|
|
146
|
+
|
|
147
|
+
msgid "Visible signature"
|
|
148
|
+
msgstr "Firma visible"
|
|
149
|
+
|
|
150
|
+
msgid "Signature image"
|
|
151
|
+
msgstr "Imagen de firma"
|
|
152
|
+
|
|
153
|
+
msgid "Image preview"
|
|
154
|
+
msgstr "Vista previa de imagen"
|
|
155
|
+
|
|
156
|
+
msgid "None"
|
|
157
|
+
msgstr ""
|
|
158
|
+
|
|
159
|
+
msgid "Right"
|
|
160
|
+
msgstr ""
|
|
161
|
+
|
|
162
|
+
msgid "Left"
|
|
163
|
+
msgstr ""
|
|
164
|
+
|
|
165
|
+
msgid "Top"
|
|
166
|
+
msgstr ""
|
|
167
|
+
|
|
168
|
+
msgid "Bottom"
|
|
169
|
+
msgstr ""
|
|
170
|
+
|
|
119
171
|
msgid "An unexpected error occurred during the signing process."
|
|
120
172
|
msgstr ""
|
|
121
173
|
"Un error inesperado ha ocurrido durante el proceso de firmado de documentos."
|
|
@@ -133,6 +185,61 @@ msgstr "El tiempo de espera del servicio de firma ha expirado."
|
|
|
133
185
|
msgid "An exception ocurred el request to the signing service."
|
|
134
186
|
msgstr "Un error ha ocurrido enviando la petición a los servicios de firmado."
|
|
135
187
|
|
|
188
|
+
msgid "Updated signature settings successfully."
|
|
189
|
+
msgstr "Configuración de firma actualizada correctamente."
|
|
190
|
+
|
|
191
|
+
#, fuzzy
|
|
192
|
+
#| msgid "Create"
|
|
193
|
+
msgid "Created"
|
|
194
|
+
msgstr "Crear"
|
|
195
|
+
|
|
196
|
+
#, fuzzy
|
|
197
|
+
#| msgid "Update"
|
|
198
|
+
msgid "Updated"
|
|
199
|
+
msgstr "Actualizar"
|
|
200
|
+
|
|
201
|
+
#, fuzzy
|
|
202
|
+
#| msgid "Delete"
|
|
203
|
+
msgid "Deleted"
|
|
204
|
+
msgstr "Eliminar"
|
|
205
|
+
|
|
206
|
+
#, fuzzy
|
|
207
|
+
#| msgid "Not found"
|
|
208
|
+
msgid "No user found"
|
|
209
|
+
msgstr "No se encontró"
|
|
210
|
+
|
|
211
|
+
msgid "Hard deleted"
|
|
212
|
+
msgstr "Eliminación permanente"
|
|
213
|
+
|
|
214
|
+
msgid "Restored"
|
|
215
|
+
msgstr "Restaurado"
|
|
216
|
+
|
|
217
|
+
#, fuzzy
|
|
218
|
+
#| msgid "Update"
|
|
219
|
+
msgid "updated"
|
|
220
|
+
msgstr "Actualizar"
|
|
221
|
+
|
|
222
|
+
msgid "deleted"
|
|
223
|
+
msgstr "eliminado"
|
|
224
|
+
|
|
225
|
+
msgid "hard deleted"
|
|
226
|
+
msgstr "eliminado permanentemente"
|
|
227
|
+
|
|
228
|
+
msgid "restored"
|
|
229
|
+
msgstr "restaurado"
|
|
230
|
+
|
|
231
|
+
#, python-format
|
|
232
|
+
msgid "An object of model %(model)s has been %(action)s"
|
|
233
|
+
msgstr "Un objeto del modelo %(model)s ha sido %(action)s"
|
|
234
|
+
|
|
235
|
+
#, python-format
|
|
236
|
+
msgid "%(msg)s. Fields: %(fields)s"
|
|
237
|
+
msgstr "%(msg)s. Campos: %(fields)s"
|
|
238
|
+
|
|
239
|
+
#, python-format
|
|
240
|
+
msgid "The record %(obj)s of model %(model)s has been %(action)s"
|
|
241
|
+
msgstr "El registro %(obj)s del modelo %(model)s ha sido %(action)s"
|
|
242
|
+
|
|
136
243
|
#, python-brace-format
|
|
137
244
|
msgid "Do you want to delete {obj}?"
|
|
138
245
|
msgstr "¿Desea eliminar {obj}?"
|
|
@@ -191,6 +298,30 @@ msgstr "Nombre de la url"
|
|
|
191
298
|
msgid "Permission"
|
|
192
299
|
msgstr "Permiso"
|
|
193
300
|
|
|
301
|
+
msgid "Content type"
|
|
302
|
+
msgstr ""
|
|
303
|
+
|
|
304
|
+
msgid "Object ID"
|
|
305
|
+
msgstr ""
|
|
306
|
+
|
|
307
|
+
msgid "Object repr"
|
|
308
|
+
msgstr ""
|
|
309
|
+
|
|
310
|
+
msgid "Value of str(instance) at deletion time"
|
|
311
|
+
msgstr ""
|
|
312
|
+
|
|
313
|
+
#, fuzzy
|
|
314
|
+
#| msgid "Delete"
|
|
315
|
+
msgid "Deleted by"
|
|
316
|
+
msgstr "Eliminar"
|
|
317
|
+
|
|
318
|
+
msgid "Trash"
|
|
319
|
+
msgstr ""
|
|
320
|
+
|
|
321
|
+
#, python-format
|
|
322
|
+
msgid "%(obj)s in trash"
|
|
323
|
+
msgstr "%(obj)s en papelera"
|
|
324
|
+
|
|
194
325
|
msgid "This is a html email, please use a visor with HTML support"
|
|
195
326
|
msgstr "Este es un correo HTML, por favor use un cliente con soporte HTML"
|
|
196
327
|
|
|
@@ -320,6 +451,9 @@ msgstr "Siguiente"
|
|
|
320
451
|
msgid "Create"
|
|
321
452
|
msgstr "Crear"
|
|
322
453
|
|
|
454
|
+
msgid "Signature settings"
|
|
455
|
+
msgstr "Configuración de firma"
|
|
456
|
+
|
|
323
457
|
msgid "Show all notifications"
|
|
324
458
|
msgstr "Ver todas las notificaciones"
|
|
325
459
|
|
|
@@ -508,6 +642,9 @@ msgid "You are now registered. Activation email sent."
|
|
|
508
642
|
msgstr ""
|
|
509
643
|
"Usted fue registrado adecuadamente. Un correo de activación ha sido enviado."
|
|
510
644
|
|
|
645
|
+
msgid "You updated correctly your password please login again."
|
|
646
|
+
msgstr ""
|
|
647
|
+
|
|
511
648
|
msgid "Select an User"
|
|
512
649
|
msgstr "Seleccione a un Usuario"
|
|
513
650
|
|
|
@@ -598,6 +735,9 @@ msgstr "Ejecutar Firmador Libre"
|
|
|
598
735
|
msgid "Run app"
|
|
599
736
|
msgstr "Ejecutar aplicación"
|
|
600
737
|
|
|
738
|
+
msgid "Expand"
|
|
739
|
+
msgstr "Expandir"
|
|
740
|
+
|
|
601
741
|
msgid ""
|
|
602
742
|
"Please go to the last page, in the signature space, try to place your "
|
|
603
743
|
"signature vertically centered."
|
|
@@ -614,57 +754,6 @@ msgstr "Por favor, espere mientras se firma el documento."
|
|
|
614
754
|
msgid "Start"
|
|
615
755
|
msgstr "Iniciar"
|
|
616
756
|
|
|
617
|
-
msgid "Unknown"
|
|
618
|
-
msgstr "Desconocido"
|
|
619
|
-
|
|
620
|
-
msgid "LEFT"
|
|
621
|
-
msgstr "IZQUIERDA"
|
|
622
|
-
|
|
623
|
-
msgid "CENTER"
|
|
624
|
-
msgstr "CENTRO"
|
|
625
|
-
|
|
626
|
-
msgid "RIGHT"
|
|
627
|
-
msgstr "DERECHA"
|
|
628
|
-
|
|
629
|
-
msgid "Signature settings"
|
|
630
|
-
msgstr "Configuración de firma"
|
|
631
|
-
|
|
632
|
-
msgid "Updated signature settings successfully."
|
|
633
|
-
msgstr "Configuración de firma actualizada correctamente."
|
|
634
|
-
|
|
635
|
-
msgid "Background color"
|
|
636
|
-
msgstr "Color de fondo"
|
|
637
|
-
|
|
638
|
-
msgid "Contact"
|
|
639
|
-
msgstr "Contacto"
|
|
640
|
-
|
|
641
|
-
msgid "Date format"
|
|
642
|
-
msgstr "Formato de fecha"
|
|
643
|
-
|
|
644
|
-
msgid "Signature message"
|
|
645
|
-
msgstr "Mensaje de firma"
|
|
646
|
-
|
|
647
|
-
msgid "Font"
|
|
648
|
-
msgstr "Fuente"
|
|
649
|
-
|
|
650
|
-
msgid "Font alignment"
|
|
651
|
-
msgstr "Alineación de fuente"
|
|
652
|
-
|
|
653
|
-
msgid "Font color"
|
|
654
|
-
msgstr "Color de fuente"
|
|
655
|
-
|
|
656
|
-
msgid "Font size"
|
|
657
|
-
msgstr "Tamaño de fuente"
|
|
658
|
-
|
|
659
|
-
msgid "Place"
|
|
660
|
-
msgstr "Lugar"
|
|
661
|
-
|
|
662
|
-
msgid "Reason"
|
|
663
|
-
msgstr "Razón"
|
|
664
|
-
|
|
665
|
-
msgid "Visible signature"
|
|
666
|
-
msgstr "Firma visible"
|
|
667
|
-
|
|
668
757
|
msgid "This registry of trash does not exist."
|
|
669
758
|
msgstr "Este registro de papelera no existe."
|
|
670
759
|
|
|
@@ -674,50 +763,10 @@ msgstr "El registro fue restaurado con éxito."
|
|
|
674
763
|
msgid "The registry could not be restored."
|
|
675
764
|
msgstr "No se pudo restaurar el registro."
|
|
676
765
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
msgstr "Vista previa de imagen"
|
|
682
|
-
|
|
683
|
-
msgid "Image signature"
|
|
684
|
-
msgstr "Imagen de firma"
|
|
685
|
-
msgid "Expand"
|
|
686
|
-
msgstr "Expandir"
|
|
687
|
-
|
|
688
|
-
# history
|
|
689
|
-
msgid "Restoration"
|
|
690
|
-
msgstr "Restaurado"
|
|
691
|
-
|
|
692
|
-
msgid "deleted"
|
|
693
|
-
msgstr "eliminado"
|
|
694
|
-
|
|
695
|
-
msgid "Hard deleted"
|
|
696
|
-
msgstr "Eliminación permanente"
|
|
697
|
-
|
|
698
|
-
msgid "History"
|
|
699
|
-
msgstr "Historial"
|
|
700
|
-
|
|
701
|
-
msgid "General"
|
|
702
|
-
msgstr "General"
|
|
703
|
-
|
|
704
|
-
msgid "restored"
|
|
705
|
-
msgstr "restaurado"
|
|
706
|
-
|
|
707
|
-
msgid "Restored"
|
|
708
|
-
msgstr "Restaurado"
|
|
709
|
-
|
|
710
|
-
msgid "hard deleted"
|
|
711
|
-
msgstr "eliminado permanentemente"
|
|
712
|
-
|
|
713
|
-
msgid "%(msg)s. Fields: %(fields)s"
|
|
714
|
-
msgstr "%(msg)s. Campos: %(fields)s"
|
|
715
|
-
|
|
716
|
-
msgid "The record %(obj)s of model %(model)s has been %(action)s"
|
|
717
|
-
msgstr "El registro %(obj)s del modelo %(model)s ha sido %(action)s"
|
|
718
|
-
|
|
719
|
-
msgid "An object of model %(model)s has been %(action)s"
|
|
720
|
-
msgstr "Un objeto del modelo %(model)s ha sido %(action)s"
|
|
766
|
+
#, fuzzy
|
|
767
|
+
#| msgid "Delete"
|
|
768
|
+
msgid "Deleted object"
|
|
769
|
+
msgstr "Eliminar"
|
|
721
770
|
|
|
722
|
-
msgid "
|
|
723
|
-
msgstr "
|
|
771
|
+
msgid "Unknown"
|
|
772
|
+
msgstr "Desconocido"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from django.utils.text import slugify
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("djgentelella")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_file_name(instance, name):
|
|
12
|
+
model_name = str(type(instance).__name__).lower()
|
|
13
|
+
|
|
14
|
+
return "%s-%s" % (
|
|
15
|
+
model_name,
|
|
16
|
+
name
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def upload_files_by_model_and_dates(instance, filename):
|
|
21
|
+
"""
|
|
22
|
+
Create a directory structure of the type model/date/file. Usage:
|
|
23
|
+
|
|
24
|
+
myfile = models.FileField(upload_to=upload_files_by_model_and_dates)
|
|
25
|
+
"""
|
|
26
|
+
date = int(datetime.datetime.now().strftime("%Y%m%d%H%M%S"))
|
|
27
|
+
path = Path(filename)
|
|
28
|
+
extension = path.suffix
|
|
29
|
+
|
|
30
|
+
if extension == ".zip":
|
|
31
|
+
name = path.stem
|
|
32
|
+
else:
|
|
33
|
+
name = get_file_name(instance, slugify(path.stem))
|
|
34
|
+
|
|
35
|
+
model_name = str(type(instance).__name__).lower()
|
|
36
|
+
return f"{model_name}/{date}/{name}{extension}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def upload_files_by_model_and_month(instance, filename):
|
|
40
|
+
"""
|
|
41
|
+
Create a directory structure of the type model/yearmonth/file. Usage:
|
|
42
|
+
|
|
43
|
+
myfile = models.FileField(upload_to=upload_files_by_model_and_dates)
|
|
44
|
+
"""
|
|
45
|
+
dates = datetime.datetime.now().strftime("%Y%m")
|
|
46
|
+
path = Path(filename)
|
|
47
|
+
extension = path.suffix
|
|
48
|
+
|
|
49
|
+
if extension == ".zip":
|
|
50
|
+
name = path.stem
|
|
51
|
+
else:
|
|
52
|
+
name = get_file_name(instance, slugify(path.stem))
|
|
53
|
+
|
|
54
|
+
model_name = str(type(instance).__name__).lower()
|
|
55
|
+
return f"{model_name}/{dates}/{name}{extension}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def delete_file_and_folder(file_field):
|
|
59
|
+
"""
|
|
60
|
+
Delete the file associated with file_field and, if the containing folder is empty,
|
|
61
|
+
delete it.
|
|
62
|
+
"""
|
|
63
|
+
if not file_field:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
# Obtener la ruta absoluta del archivo
|
|
67
|
+
file_path = file_field.path
|
|
68
|
+
if os.path.exists(file_path):
|
|
69
|
+
try:
|
|
70
|
+
os.remove(file_path)
|
|
71
|
+
logger.info(f"Deleted file: {file_path}")
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"Error deleting file: {file_path}", exc_info=e)
|
|
74
|
+
|
|
75
|
+
# Obtener la carpeta contenedora del archivo
|
|
76
|
+
directory = os.path.dirname(file_path)
|
|
77
|
+
|
|
78
|
+
# Intentar eliminar la carpeta, si está vacía.
|
|
79
|
+
if os.path.exists(directory):
|
|
80
|
+
# Listar el contenido de la carpeta
|
|
81
|
+
files = os.listdir(directory)
|
|
82
|
+
if not files:
|
|
83
|
+
try:
|
|
84
|
+
os.rmdir(directory)
|
|
85
|
+
logger.info(f"Folder created: {directory}")
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.error(f"Error deleting folder: {directory}", exc_info=e)
|
|
@@ -43,11 +43,36 @@ class AnyPermission(BasePermission):
|
|
|
43
43
|
|
|
44
44
|
class AllPermissionByAction(BasePermission):
|
|
45
45
|
def has_permission(self, request, view):
|
|
46
|
-
|
|
46
|
+
action = getattr(view, "action", None)
|
|
47
|
+
perms_map = getattr(view, "perms", {}) or {}
|
|
48
|
+
|
|
49
|
+
if action is None:
|
|
50
|
+
return False # Generate error >= 403 instead of 500
|
|
51
|
+
|
|
52
|
+
perms = perms_map.get(action)
|
|
53
|
+
if perms is None:
|
|
54
|
+
return False # unmapped action => 403
|
|
55
|
+
|
|
47
56
|
return all_permission(request.user, perms)
|
|
48
57
|
|
|
49
58
|
|
|
50
59
|
class AnyPermissionByAction(BasePermission):
|
|
51
60
|
def has_permission(self, request, view):
|
|
52
|
-
|
|
61
|
+
action = getattr(view, "action", None)
|
|
62
|
+
perms_map = getattr(view, "perms", {}) or {}
|
|
63
|
+
|
|
64
|
+
if action is None:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
perms = perms_map.get(action)
|
|
68
|
+
if perms is None:
|
|
69
|
+
return False
|
|
70
|
+
|
|
53
71
|
return any_permission(request.user, perms)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_actions_by_perms(user, actions_list):
|
|
75
|
+
actions = {}
|
|
76
|
+
for action, perms in actions_list.items():
|
|
77
|
+
actions[action] = all_permission(user, perms)
|
|
78
|
+
return actions
|
|
@@ -31,3 +31,17 @@ class GTDateTimeField(DateTimeField):
|
|
|
31
31
|
if not value and self.allow_empty_str:
|
|
32
32
|
return None
|
|
33
33
|
return super().to_internal_value(value)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DateFieldWithEmptyString(DateField):
|
|
37
|
+
def to_internal_value(self, value):
|
|
38
|
+
if not value:
|
|
39
|
+
return None
|
|
40
|
+
return super(DateFieldWithEmptyString, self).to_internal_value(value)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DateTimeFieldFieldWithEmptyString(DateTimeField):
|
|
44
|
+
def to_internal_value(self, value):
|
|
45
|
+
if not value:
|
|
46
|
+
return None
|
|
47
|
+
return super(DateTimeFieldFieldWithEmptyString, self).to_internal_value(value)
|