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 CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = '0.5.4'
1
+ __version__ = '0.5.6'
2
2
 
3
3
  if __name__ == '__main__':
4
4
  print(__version__)
@@ -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
@@ -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-03-29 22:37+0000\n"
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
- msgid "Signature image"
678
- msgstr "Imagen de firma"
679
-
680
- msgid "Image preview"
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 "%(obj)s in trash"
723
- msgstr "%(obj)s en papelera"
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
- perms = view.perms[view.action]
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
- perms = view.perms[view.action]
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)
@@ -0,0 +1,8 @@
1
+ from rest_framework import serializers
2
+
3
+
4
+ class GTEncryptedTextField(serializers.CharField):
5
+ def to_representation(self, value):
6
+ if not value:
7
+ return ""
8
+ return str(value.decode())