djgentelella 0.4.10__py3-none-any.whl → 0.5.0__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 (55) hide show
  1. djgentelella/__init__.py +1 -1
  2. djgentelella/admin.py +7 -1
  3. djgentelella/firmador_digital/forms.py +46 -16
  4. djgentelella/firmador_digital/models.py +12 -15
  5. djgentelella/firmador_digital/views.py +1 -1
  6. djgentelella/locale/es/LC_MESSAGES/django.mo +0 -0
  7. djgentelella/locale/es/LC_MESSAGES/django.po +18 -0
  8. djgentelella/locale/es/LC_MESSAGES/djangojs.mo +0 -0
  9. djgentelella/migrations/0014_trash.py +31 -0
  10. djgentelella/migrations/0015_trash_deleted_by.py +21 -0
  11. djgentelella/migrations/0016_trash_created_at.py +20 -0
  12. djgentelella/models.py +96 -0
  13. djgentelella/models_manager.py +40 -0
  14. djgentelella/static/djgentelella.flags.vendors.min.css +1 -1
  15. djgentelella/static/gentelella/css/custom.css +19 -2
  16. djgentelella/static/gentelella/css/modern.css +15 -0
  17. djgentelella/static/gentelella/css/modern_black_white.css +15 -0
  18. djgentelella/static/gentelella/css/pdfviewer.css +43 -1
  19. djgentelella/static/gentelella/images/default.png +0 -0
  20. djgentelella/static/gentelella/js/base/digital_signature.js +87 -26
  21. djgentelella/static/gentelella/js/base.js +87 -26
  22. djgentelella/static/gentelella/js/digital_signature_update.js +29 -0
  23. djgentelella/static/vendors/bootstrap-datetimepicker/bootstrap-datetimepicker.min.css.map +75 -7
  24. djgentelella/static/vendors/flags/1x1/ac.svg +75 -7
  25. djgentelella/static/vendors/flags/1x1/cp.svg +75 -7
  26. djgentelella/static/vendors/flags/1x1/dg.svg +75 -7
  27. djgentelella/static/vendors/flags/1x1/ea.svg +75 -7
  28. djgentelella/static/vendors/flags/1x1/es-ct.svg +75 -7
  29. djgentelella/static/vendors/flags/1x1/es-ga.svg +75 -7
  30. djgentelella/static/vendors/flags/1x1/ic.svg +75 -7
  31. djgentelella/static/vendors/flags/1x1/ta.svg +75 -7
  32. djgentelella/static/vendors/flags/1x1/xx.svg +75 -7
  33. djgentelella/static/vendors/flags/4x3/ac.svg +75 -7
  34. djgentelella/static/vendors/flags/4x3/cp.svg +75 -7
  35. djgentelella/static/vendors/flags/4x3/dg.svg +75 -7
  36. djgentelella/static/vendors/flags/4x3/ea.svg +75 -7
  37. djgentelella/static/vendors/flags/4x3/es-ct.svg +75 -7
  38. djgentelella/static/vendors/flags/4x3/es-ga.svg +75 -7
  39. djgentelella/static/vendors/flags/4x3/ic.svg +75 -7
  40. djgentelella/static/vendors/flags/4x3/ta.svg +75 -7
  41. djgentelella/static/vendors/flags/4x3/xx.svg +75 -7
  42. djgentelella/templates/gentelella/app/sidebar.html +7 -4
  43. djgentelella/templates/gentelella/digital_signature/update_signature_settings.html +24 -36
  44. djgentelella/templates/gentelella/widgets/digital_signature.html +41 -7
  45. djgentelella/trash/api.py +58 -0
  46. djgentelella/trash/filterset.py +20 -0
  47. djgentelella/trash/serializer.py +45 -0
  48. djgentelella/urls.py +4 -0
  49. djgentelella/widgets/core.py +6 -5
  50. {djgentelella-0.4.10.dist-info → djgentelella-0.5.0.dist-info}/METADATA +2 -2
  51. {djgentelella-0.4.10.dist-info → djgentelella-0.5.0.dist-info}/RECORD +55 -46
  52. {djgentelella-0.4.10.dist-info → djgentelella-0.5.0.dist-info}/AUTHORS +0 -0
  53. {djgentelella-0.4.10.dist-info → djgentelella-0.5.0.dist-info}/LICENSE.txt +0 -0
  54. {djgentelella-0.4.10.dist-info → djgentelella-0.5.0.dist-info}/WHEEL +0 -0
  55. {djgentelella-0.4.10.dist-info → djgentelella-0.5.0.dist-info}/top_level.txt +0 -0
djgentelella/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = '0.4.10'
1
+ __version__ = '0.5.0'
2
2
 
3
3
  if __name__ == '__main__':
4
4
  print(__version__)
djgentelella/admin.py CHANGED
@@ -2,7 +2,7 @@ from django.contrib import admin
2
2
 
3
3
  from djgentelella.firmador_digital.models import UserSignatureConfig
4
4
  from djgentelella.models import MenuItem, Help, GentelellaSettings, Notification, \
5
- ChunkedUpload
5
+ ChunkedUpload, Trash
6
6
  from djgentelella.models import PermissionsCategoryManagement
7
7
  from djgentelella.utils import clean_cache
8
8
 
@@ -33,6 +33,11 @@ class ChunkedUploadAdmin(admin.ModelAdmin):
33
33
  class UserSignatureConfigAdmin(admin.ModelAdmin):
34
34
  list_display = ('id', 'user', 'config')
35
35
 
36
+ class TrashAdmin(admin.ModelAdmin):
37
+ list_display = ("id", "deleted_by", "content_type", "object_id", "object_repr", "created_at")
38
+ ordering = ("-created_at",)
39
+ search_fields = ("id",)
40
+
36
41
  admin.site.register(UserSignatureConfig, UserSignatureConfigAdmin)
37
42
  admin.site.register(ChunkedUpload, ChunkedUploadAdmin)
38
43
  admin.site.register(MenuItem, MenuAdmin)
@@ -40,3 +45,4 @@ admin.site.register(Help)
40
45
  admin.site.register(PermissionsCategoryManagement)
41
46
  admin.site.register(GentelellaSettings, GentelellaSettingsAdmin)
42
47
  admin.site.register(Notification, NotificationAdmin)
48
+ admin.site.register(Trash, TrashAdmin)
@@ -1,14 +1,20 @@
1
+ import base64
1
2
  import logging
3
+ from io import BytesIO
2
4
 
3
5
  from django import forms
4
6
  from django.core.exceptions import ValidationError
7
+ from django.core.files.uploadedfile import UploadedFile
8
+ from django.templatetags.static import static
9
+ from django.utils.safestring import mark_safe
5
10
  from django.utils.translation import gettext_lazy as _
6
11
 
7
12
  from djgentelella.firmador_digital.models import UserSignatureConfig, \
8
- get_signature_default, FORMATS_DATE, FONT_ALIGNMENT, FONT_CHOICES
13
+ get_signature_default, FORMATS_DATE, FONT_ALIGNMENT
9
14
  from djgentelella.firmador_digital.signvalue_utils import ValueDSParser
10
15
  from djgentelella.forms.forms import GTForm
11
16
  from djgentelella.widgets import core as genwidgets
17
+ from djgentelella.widgets.core import FileInput
12
18
 
13
19
  logger = logging.getLogger('djgentelella')
14
20
 
@@ -31,12 +37,6 @@ class RenderValueForm(GTForm, ValueDSParser):
31
37
 
32
38
 
33
39
  class SignatureConfigForm(GTForm, forms.ModelForm):
34
- backgroundColor = forms.CharField(
35
- max_length=50,
36
- widget=genwidgets.ColorInput,
37
- required=True,
38
- label=_("Background color")
39
- )
40
40
  contact = forms.CharField(
41
41
  required=False,
42
42
  max_length=100,
@@ -56,12 +56,6 @@ class SignatureConfigForm(GTForm, forms.ModelForm):
56
56
  max_length=100,
57
57
  label=_("Signature message")
58
58
  )
59
- # font = forms.ChoiceField(
60
- # required=True,
61
- # widget=genwidgets.Select,
62
- # label=_("Font"),
63
- # choices=FONT_CHOICES
64
- # )
65
59
  fontAlignment = forms.ChoiceField(
66
60
  choices=FONT_ALIGNMENT,
67
61
  widget=genwidgets.Select,
@@ -73,6 +67,7 @@ class SignatureConfigForm(GTForm, forms.ModelForm):
73
67
  widget=genwidgets.ColorInput,
74
68
  required=True,
75
69
  label=_("Font color"),
70
+ initial="#FFFFFF",
76
71
  )
77
72
  fontSize = forms.IntegerField(
78
73
  min_value=5,
@@ -98,6 +93,11 @@ class SignatureConfigForm(GTForm, forms.ModelForm):
98
93
  widget=genwidgets.YesNoInput,
99
94
  label=_("Visible signature")
100
95
  )
96
+ image = forms.ImageField(
97
+ required=False,
98
+ widget=FileInput,
99
+ label=_("Signature image")
100
+ )
101
101
 
102
102
  default_render_type = "as_grid"
103
103
 
@@ -105,10 +105,9 @@ class SignatureConfigForm(GTForm, forms.ModelForm):
105
105
  [["contact"]],
106
106
  [["place"]],
107
107
  [["reason"]],
108
- # [["dateFormat"], ["font"]],
108
+ [["image"], ["preview_image"]],
109
109
  [["dateFormat"], ["isVisibleSignature"]],
110
- [["fontSize"], ["fontColor"], ["backgroundColor"], ["fontAlignment"]],
111
- # [["isVisibleSignature"]],
110
+ [["fontSize"], ["fontColor"], ["fontAlignment"]],
112
111
  [["defaultSignMessage"]],
113
112
  ]
114
113
 
@@ -125,14 +124,45 @@ class SignatureConfigForm(GTForm, forms.ModelForm):
125
124
  if key in self.fields:
126
125
  self.fields[key].initial = value
127
126
 
127
+ def preview_image(self):
128
+ label = _("Image preview")
129
+ src = static("gentelella/images/default.png")
130
+
131
+ if self.instance and self.instance.config:
132
+ image_b64 = self.instance.config.get("image")
133
+ if image_b64 and image_b64.startswith("data:image"):
134
+ src = image_b64
135
+
136
+ return mark_safe(f"""
137
+ <label for="image-preview"><strong>{label}:</strong></label><br>
138
+ <div class="d-flex justify-content-center">
139
+ <img id="image-preview" alt="{label}" src="{src}" style="max-height:100px; max-width:300px; display:block;">
140
+ </div>
141
+ """)
142
+
128
143
  def save(self, commit=True):
129
144
  data = get_signature_default()
145
+ prev_image = self.instance.config.get("image")
146
+
130
147
  for key in data.keys():
131
148
  if key in self.cleaned_data:
132
149
  val = self.cleaned_data[key]
150
+ if key == "image":
151
+ if isinstance(val, UploadedFile):
152
+ # el usuario subió un nuevo archivo
153
+ buffered = BytesIO()
154
+ for chunk in val.chunks():
155
+ buffered.write(chunk)
156
+ mime = val.content_type
157
+ b64 = base64.b64encode(buffered.getvalue()).decode()
158
+ val = f"data:{mime};base64,{b64}"
159
+ else:
160
+ # no se subió archivo nuevo → conservamos imagen anterior
161
+ val = prev_image
133
162
 
134
163
  if isinstance(data[key], str) and not isinstance(val, str):
135
164
  val = str(val)
136
165
  data[key] = val
166
+
137
167
  self.instance.config = data
138
168
  return super().save(commit=commit)
@@ -4,34 +4,30 @@ from django.db import models
4
4
 
5
5
  FORMATS_DATE = [
6
6
  ("dd/MM/yyyy hh:mm:ss a", "dd/MM/yyyy hh:mm:ss a"),
7
- ("yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm:ss"),
7
+ ("yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm:ss"),
8
8
  ("MM/dd/yyyy hh:mm:ss a", "MM/dd/yyyy hh:mm:ss a"),
9
- ("dd-MM-yyyy", "dd-MM-yyyy"),
9
+ ("dd-MM-yyyy", "dd-MM-yyyy"),
10
10
  ]
11
11
 
12
- FONT_ALIGNMENT = [
13
- ("LEFT", _("LEFT")),
14
- ("CENTER", _("CENTER")),
15
- ("RIGHT", _("RIGHT")),
16
- ]
12
+ FONT_ALIGNMENT = (
13
+ ('NONE', _('None')),
14
+ ('RIGHT', _('Right')),
15
+ ('LEFT', _('Left')),
16
+ ('TOP', _('Top')),
17
+ ('BOTTOM', _('Bottom')),
18
+ )
17
19
 
18
- FONT_CHOICES = [
19
- ("Nimbus Sans Regular", "Nimbus Sans Regular"),
20
- ("Nimbus Sans Bold", "Nimbus Sans Bold"),
21
- ("Nimbus Sans Italic", "Nimbus Sans Italic"),
22
- ("Nimbus Sans Bold Italic", "Nimbus Sans Bold Italic"),
23
- ]
24
20
 
25
21
  def get_signature_default():
26
22
  return {
27
- "backgroundColor": "transparente",
23
+ "backgroundColor": "transparent",
28
24
  "cAdESLevel": "LTA",
29
25
  "contact": "",
30
26
  "country": "CR",
31
27
  "dateFormat": "dd/MM/yyyy hh\:mm\:ss a",
32
28
  "defaultSignMessage": "Esta es una representación gráfica únicamente,\nverifique la validez de la firma.",
33
29
  "font": "Nimbus Sans Regular",
34
- "fontAlignment": "RIGHT",
30
+ "fontAlignment": "None",
35
31
  "fontColor": "000000",
36
32
  "fontSize": "7",
37
33
  "image": "",
@@ -46,6 +42,7 @@ def get_signature_default():
46
42
  "signY": "60",
47
43
  "xAdESLevel": "LTA",
48
44
  "isVisibleSignature": False,
45
+ "hideSignatureAdvice": False,
49
46
  }
50
47
 
51
48
 
@@ -13,7 +13,7 @@ def update_signature_settings(request):
13
13
 
14
14
 
15
15
  if request.method == "POST":
16
- form = SignatureConfigForm(request.POST, instance=config, render_type="as_grid")
16
+ form = SignatureConfigForm(request.POST, request.FILES, instance=config, render_type="as_grid")
17
17
  if form.is_valid():
18
18
  form.save()
19
19
  messages.success(request, _("Updated signature settings successfully."))
@@ -665,3 +665,21 @@ msgstr "Razón"
665
665
  msgid "Visible signature"
666
666
  msgstr "Firma visible"
667
667
 
668
+ msgid "This registry of trash does not exist."
669
+ msgstr "Este registro de papelera no existe."
670
+
671
+ msgid "The registry was successfully restored."
672
+ msgstr "El registro fue restaurado con éxito."
673
+
674
+ msgid "The registry could not be restored."
675
+ msgstr "No se pudo restaurar el registro."
676
+ msgid "Signature image"
677
+ msgstr "Imagen de firma"
678
+
679
+ msgid "Image preview"
680
+ msgstr "Vista previa de imagen"
681
+
682
+ msgid "Image signature"
683
+ msgstr "Imagen de firma"
684
+ msgid "Expand"
685
+ msgstr "Expandir"
@@ -0,0 +1,31 @@
1
+ # Generated by Django 5.1.6 on 2025-08-03 17:13
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('contenttypes', '0002_remove_content_type_name'),
11
+ ('djgentelella', '0013_usersignatureconfig'),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.CreateModel(
16
+ name='Trash',
17
+ fields=[
18
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19
+ ('object_id', models.PositiveIntegerField(verbose_name='Object ID')),
20
+ ('object_repr', models.CharField(help_text='Value of str(instance) at deletion time', max_length=200, verbose_name='Object repr')),
21
+ ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype', verbose_name='Content type')),
22
+ ],
23
+ options={
24
+ 'verbose_name': 'Trash',
25
+ 'verbose_name_plural': 'Trash',
26
+ 'ordering': ('id',),
27
+ 'indexes': [models.Index(fields=['content_type', 'object_id'], name='djgentelell_content_9fa474_idx')],
28
+ 'unique_together': {('content_type', 'object_id')},
29
+ },
30
+ ),
31
+ ]
@@ -0,0 +1,21 @@
1
+ # Generated by Django 5.1.6 on 2025-08-03 17:29
2
+
3
+ import django.db.models.deletion
4
+ from django.conf import settings
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ('djgentelella', '0014_trash'),
12
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.AddField(
17
+ model_name='trash',
18
+ name='deleted_by',
19
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Deleted by'),
20
+ ),
21
+ ]
@@ -0,0 +1,20 @@
1
+ # Generated by Django 5.1.6 on 2025-08-03 17:54
2
+
3
+ import django.utils.timezone
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('djgentelella', '0015_trash_deleted_by'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name='trash',
16
+ name='created_at',
17
+ field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
18
+ preserve_default=False,
19
+ ),
20
+ ]
djgentelella/models.py CHANGED
@@ -5,6 +5,10 @@ from django.utils.translation import gettext_lazy as _
5
5
  from tree_queries.models import TreeNode
6
6
 
7
7
  from djgentelella.chunked_upload.models import AbstractChunkedUpload
8
+ from django.contrib.contenttypes.models import ContentType
9
+ from django.contrib.contenttypes.fields import GenericForeignKey
10
+ from .models_manager import ObjectManager, AllObjectsManager, \
11
+ DeletedObjectsManager
8
12
 
9
13
 
10
14
  class GentelellaSettings(models.Model):
@@ -123,3 +127,95 @@ class ChunkedUpload(AbstractChunkedUpload):
123
127
  null=DEFAULT_MODEL_USER_FIELD_NULL,
124
128
  blank=DEFAULT_MODEL_USER_FIELD_BLANK
125
129
  )
130
+
131
+
132
+ # Trash
133
+ class Trash(models.Model):
134
+ """
135
+ Trash generic. Each row represents an instance deleted.
136
+ """
137
+ content_type = models.ForeignKey(ContentType, on_delete=models.PROTECT,
138
+ verbose_name=_("Content type"))
139
+ object_id = models.PositiveIntegerField(verbose_name=_("Object ID"))
140
+ content_object = GenericForeignKey("content_type", "object_id")
141
+ object_repr = models.CharField(
142
+ _("Object repr"), max_length=200,
143
+ help_text=_("Value of str(instance) at deletion time"),
144
+ )
145
+ deleted_by = models.ForeignKey(
146
+ User,
147
+ on_delete=models.SET_NULL,
148
+ null=True,
149
+ blank=True,
150
+ verbose_name=_("Deleted by")
151
+ )
152
+ created_at = models.DateTimeField(auto_now_add=True)
153
+
154
+ class Meta:
155
+ ordering = ("id",)
156
+ unique_together = ("content_type", "object_id")
157
+ indexes = [
158
+ models.Index(fields=["content_type", "object_id"]),
159
+ ]
160
+ verbose_name = _("Trash")
161
+ verbose_name_plural = _("Trash")
162
+
163
+ def __str__(self):
164
+ return _("Registration in trash: %(obj)s") % {"obj": self.object_repr}
165
+
166
+ def restore(self, user=None):
167
+ obj = self.content_object
168
+
169
+ # if `is_deleted` is in the model, unmark it
170
+ if hasattr(obj, "restore"):
171
+ obj.restore()
172
+
173
+ self.delete() # delete the instance of trash
174
+
175
+
176
+ def hard_delete(self):
177
+ """
178
+ Permanent deletion of the original object and then the trash entry.
179
+ """
180
+ obj = self.content_object
181
+
182
+ if obj is None:
183
+ return
184
+
185
+ obj.delete(hard=True)
186
+
187
+ super().delete()
188
+
189
+
190
+ class DeletedWithTrash(models.Model):
191
+ is_deleted = models.BooleanField(default=False, db_index=True)
192
+
193
+ objects = ObjectManager()
194
+ objects_with_deleted = AllObjectsManager()
195
+ objects_deleted_only = DeletedObjectsManager()
196
+
197
+ class Meta:
198
+ abstract = True
199
+
200
+ def delete(self, using=None, keep_parents=False, *, hard=False, user=None):
201
+ if hard:
202
+ # Permanent deletion
203
+ return super().delete(using=using, keep_parents=keep_parents)
204
+
205
+ self.is_deleted = True
206
+ self.save(update_fields=["is_deleted"])
207
+
208
+ # create trash instance
209
+ Trash.objects.get_or_create(
210
+ content_type=ContentType.objects.get_for_model(self.__class__),
211
+ object_id=self.pk,
212
+ defaults={
213
+ "object_repr": str(self)[:200],
214
+ "deleted_by": user,
215
+ },
216
+ )
217
+
218
+ # Restore an object
219
+ def restore(self):
220
+ self.is_deleted = False
221
+ self.save(update_fields=["is_deleted"])
@@ -0,0 +1,40 @@
1
+ from django.db import models
2
+
3
+
4
+ class ObjectQuerySet(models.QuerySet):
5
+ # Non-deleted records
6
+ def alive(self):
7
+ return self.filter(is_deleted=False)
8
+
9
+ # Only deleted records
10
+ def dead(self):
11
+ return self.filter(is_deleted=True)
12
+
13
+ # Overridden delete (soft delete)
14
+ def delete(self):
15
+ return super().update(is_deleted=True)
16
+
17
+ # Permanent deletion
18
+ def hard_delete(self):
19
+ return super().delete()
20
+
21
+ # Bulk restore
22
+ def restore(self):
23
+ return self.update(is_deleted=False)
24
+
25
+
26
+ class ObjectManager(models.Manager):
27
+ # Only alive objects by default
28
+ def get_queryset(self):
29
+ return ObjectQuerySet(self.model, using=self._db).alive()
30
+
31
+ class AllObjectsManager(models.Manager):
32
+ # Explicit access to all objects
33
+ def get_queryset(self):
34
+ return ObjectQuerySet(self.model, using=self._db)
35
+
36
+ class DeletedObjectsManager(models.Manager):
37
+ # Only deleted objects
38
+ def get_queryset(self):
39
+ return ObjectQuerySet(self.model, using=self._db).dead()
40
+