djgentelella 0.4.30__py3-none-any.whl → 0.5.1__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 (66) hide show
  1. djgentelella/__init__.py +4 -1
  2. djgentelella/admin.py +41 -2
  3. djgentelella/firmador_digital/consumers/sign.py +24 -3
  4. djgentelella/firmador_digital/forms.py +140 -0
  5. djgentelella/firmador_digital/models.py +19 -24
  6. djgentelella/firmador_digital/utils.py +11 -0
  7. djgentelella/firmador_digital/views.py +31 -0
  8. djgentelella/firmador_digital/viewsets.py +15 -4
  9. djgentelella/forms/forms.py +10 -2
  10. djgentelella/groute.py +4 -1
  11. djgentelella/history/api.py +145 -0
  12. djgentelella/history/filterset.py +23 -0
  13. djgentelella/history/serializers.py +50 -0
  14. djgentelella/history/utils.py +70 -0
  15. djgentelella/locale/es/LC_MESSAGES/django.mo +0 -0
  16. djgentelella/locale/es/LC_MESSAGES/django.po +105 -0
  17. djgentelella/locale/es/LC_MESSAGES/djangojs.mo +0 -0
  18. djgentelella/locale/es/LC_MESSAGES/djangojs.po +46 -0
  19. djgentelella/migrations/0014_trash.py +31 -0
  20. djgentelella/migrations/0015_trash_deleted_by.py +21 -0
  21. djgentelella/migrations/0016_trash_created_at.py +20 -0
  22. djgentelella/migrations/0017_alter_chunkedupload_status.py +18 -0
  23. djgentelella/migrations/0018_alter_chunkedupload_status.py +18 -0
  24. djgentelella/models.py +97 -0
  25. djgentelella/models_manager.py +40 -0
  26. djgentelella/serializers/firmador_digital.py +9 -1
  27. djgentelella/serializers/selects.py +14 -0
  28. djgentelella/static/djgentelella.readonly.vendors.min.css +9 -5
  29. djgentelella/static/djgentelella.readonly.vendors.min.js +1 -1
  30. djgentelella/static/gentelella/css/custom.css +23 -2
  31. djgentelella/static/gentelella/css/modern.css +15 -0
  32. djgentelella/static/gentelella/css/modern_black_white.css +15 -0
  33. djgentelella/static/gentelella/css/pdfviewer.css +43 -1
  34. djgentelella/static/gentelella/images/default.png +0 -0
  35. djgentelella/static/gentelella/js/base/digital_signature.js +318 -79
  36. djgentelella/static/gentelella/js/base/form.common.js +9 -4
  37. djgentelella/static/gentelella/js/base/select2_wrap.js +15 -1
  38. djgentelella/static/gentelella/js/base/select2related.js +5 -0
  39. djgentelella/static/gentelella/js/base.js +347 -83
  40. djgentelella/static/gentelella/js/custom.js +6 -1
  41. djgentelella/static/gentelella/js/datatables.js +15 -0
  42. djgentelella/static/gentelella/js/digital_signature_update.js +29 -0
  43. djgentelella/static/gentelella/js/obj_api_management.js +403 -344
  44. djgentelella/static/gentelella/js/widgets.js +10 -1
  45. djgentelella/static/vendors/timeline/css/timeline.css +9 -5
  46. djgentelella/static/vendors/timeline/js/timeline.js +1 -1
  47. djgentelella/templates/forms/as_grid.html +2 -2
  48. djgentelella/templates/forms/as_inline.html +3 -5
  49. djgentelella/templates/gentelella/app/sidebar.html +16 -13
  50. djgentelella/templates/gentelella/digital_signature/update_signature_settings.html +45 -0
  51. djgentelella/templates/gentelella/registration/reset_done.html +24 -0
  52. djgentelella/templates/gentelella/widgets/digital_signature.html +47 -11
  53. djgentelella/trash/api.py +77 -0
  54. djgentelella/trash/filterset.py +20 -0
  55. djgentelella/trash/serializer.py +45 -0
  56. djgentelella/urls.py +13 -2
  57. djgentelella/views/select2autocomplete.py +40 -0
  58. djgentelella/widgets/core.py +6 -5
  59. djgentelella/widgets/digital_signature.py +5 -21
  60. djgentelella/widgets/selects.py +47 -0
  61. {djgentelella-0.4.30.dist-info → djgentelella-0.5.1.dist-info}/METADATA +2 -2
  62. {djgentelella-0.4.30.dist-info → djgentelella-0.5.1.dist-info}/RECORD +66 -48
  63. {djgentelella-0.4.30.dist-info → djgentelella-0.5.1.dist-info}/AUTHORS +0 -0
  64. {djgentelella-0.4.30.dist-info → djgentelella-0.5.1.dist-info}/LICENSE.txt +0 -0
  65. {djgentelella-0.4.30.dist-info → djgentelella-0.5.1.dist-info}/WHEEL +0 -0
  66. {djgentelella-0.4.30.dist-info → djgentelella-0.5.1.dist-info}/top_level.txt +0 -0
djgentelella/__init__.py CHANGED
@@ -1 +1,4 @@
1
- __version__ = '0.4.30'
1
+ __version__ = '0.5.1'
2
+
3
+ if __name__ == '__main__':
4
+ print(__version__)
djgentelella/admin.py CHANGED
@@ -2,10 +2,12 @@ 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
+ from django.contrib.admin.models import LogEntry
9
+ from djgentelella.history.utils import ACTIONS
10
+ from django.utils.translation import gettext_lazy as _
9
11
 
10
12
  class MenuAdmin(admin.ModelAdmin):
11
13
  filter_horizontal = ['permission']
@@ -33,6 +35,41 @@ class ChunkedUploadAdmin(admin.ModelAdmin):
33
35
  class UserSignatureConfigAdmin(admin.ModelAdmin):
34
36
  list_display = ('id', 'user', 'config')
35
37
 
38
+ class TrashAdmin(admin.ModelAdmin):
39
+ list_display = ("id", "deleted_by", "content_type", "object_id", "object_repr", "created_at")
40
+ ordering = ("-created_at",)
41
+ search_fields = ("id",)
42
+
43
+
44
+ class ActionFlagFilter(admin.SimpleListFilter):
45
+ title = _("Action")
46
+ parameter_name = "action_flag"
47
+
48
+ def lookups(self, request, model_admin):
49
+ return [(k, str(v)) for k, v in ACTIONS.items()]
50
+
51
+ def queryset(self, request, queryset):
52
+ val = self.value()
53
+ if not val:
54
+ return queryset
55
+ try:
56
+ return queryset.filter(action_flag=int(val))
57
+ except ValueError:
58
+ return queryset.none()
59
+
60
+ class LogEntryAdmin(admin.ModelAdmin):
61
+ verbose_name = _("History")
62
+ verbose_name_plural = _("History")
63
+ list_display = ("id", "action_time", "user", "content_type", "object_id", "object_repr", "action_label", "change_message")
64
+ search_fields = ("content_type__app_label", "content_type__model", "object_id", "user__username")
65
+ list_filter = (
66
+ ActionFlagFilter,
67
+ )
68
+ def action_label(self, obj):
69
+ return ACTIONS.get(obj.action_flag, obj.get_action_flag_display() or obj.action_flag)
70
+ action_label.short_description = _("Action")
71
+
72
+
36
73
  admin.site.register(UserSignatureConfig, UserSignatureConfigAdmin)
37
74
  admin.site.register(ChunkedUpload, ChunkedUploadAdmin)
38
75
  admin.site.register(MenuItem, MenuAdmin)
@@ -40,3 +77,5 @@ admin.site.register(Help)
40
77
  admin.site.register(PermissionsCategoryManagement)
41
78
  admin.site.register(GentelellaSettings, GentelellaSettingsAdmin)
42
79
  admin.site.register(Notification, NotificationAdmin)
80
+ admin.site.register(Trash, TrashAdmin)
81
+ admin.site.register(LogEntry, LogEntryAdmin)
@@ -12,7 +12,7 @@ from djgentelella.firmador_digital.utils import RemoteSignerClient
12
12
  from djgentelella.serializers.firmador_digital import (
13
13
  WSRequest,
14
14
  InitialSignatureSerializer,
15
- CompleteSignatureSerializer,
15
+ CompleteSignatureSerializer, ValidateDocumentSerializer,
16
16
  )
17
17
 
18
18
  logger = logging.getLogger("djgentelella")
@@ -35,24 +35,30 @@ class SignConsumer(JsonWebsocketConsumer):
35
35
  return InitialSignatureSerializer(data=content)
36
36
  if serializer.validated_data["action"] == "complete_signature":
37
37
  return CompleteSignatureSerializer(data=content)
38
+ if serializer.validated_data["action"] == "validate_document":
39
+ return ValidateDocumentSerializer(data=content)
38
40
 
39
41
  def disconnect(self, close_code):
40
42
  super().disconnect(close_code)
43
+ logger.info(f"Disconnect {close_code}")
41
44
 
42
45
  def receive_json(self, content, **kwargs):
43
46
  """
44
47
  Called with decoded JSON content.
45
48
  """
49
+ socket_id = ""
46
50
  try:
47
51
  serializer = self.get_serializer(content)
48
52
 
49
53
  if serializer.is_valid():
50
-
54
+ socket_id = serializer.validated_data['socket_id']
51
55
  match serializer.validated_data["action"]:
52
56
  case "initial_signature":
53
57
  self.do_initial_signature(serializer)
54
58
  case "complete_signature":
55
59
  self.do_complete_signature(serializer)
60
+ case "validate_document":
61
+ self.do_validate_document(serializer)
56
62
  case _:
57
63
  self.do_default(serializer)
58
64
  else:
@@ -63,6 +69,7 @@ class SignConsumer(JsonWebsocketConsumer):
63
69
  "details": serializer.errors,
64
70
  "status": 400,
65
71
  "code": 11,
72
+ 'socket_id': serializer.data.get('socket_id')
66
73
  })
67
74
  logger.error("Invalid request.")
68
75
 
@@ -74,9 +81,20 @@ class SignConsumer(JsonWebsocketConsumer):
74
81
  "details": str(e),
75
82
  "status": 500,
76
83
  "code": 999,
84
+ "socket_id": socket_id
77
85
  })
78
86
  logger.error("An unexpected error occurred.", exc_info=e)
79
87
 
88
+ def do_validate_document(self, serializer):
89
+ signer = RemoteSignerClient(self.scope["user"])
90
+
91
+ response = signer.validate_document(
92
+ instance=serializer.validated_data["instance"],
93
+ )
94
+
95
+ response['socket_id'] = serializer.validated_data['socket_id']
96
+ self.send_json(response)
97
+
80
98
  def do_initial_signature(self, serializer):
81
99
  signer = RemoteSignerClient(self.scope["user"])
82
100
 
@@ -87,6 +105,8 @@ class SignConsumer(JsonWebsocketConsumer):
87
105
  docsettings=serializer.validated_data["docsettings"],
88
106
  )
89
107
 
108
+ response['socket_id'] = serializer.validated_data['socket_id']
109
+
90
110
  # remove signer image
91
111
  if "imageIcon" in response:
92
112
  del response["imageIcon"]
@@ -103,7 +123,8 @@ class SignConsumer(JsonWebsocketConsumer):
103
123
  signer = RemoteSignerClient(self.scope["user"])
104
124
  data = dict(serializer.validated_data)
105
125
  response = signer.complete_signature(data)
106
- self.send_json({"result": response})
126
+ self.send_json({"result": response,
127
+ 'socket_id': serializer.validated_data['socket_id']})
107
128
  except Exception as e:
108
129
  logger.error("Complete the signature fail", exc_info=e)
109
130
 
@@ -1,12 +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
 
12
+ from djgentelella.firmador_digital.models import UserSignatureConfig, \
13
+ get_signature_default, FORMATS_DATE, FONT_ALIGNMENT
7
14
  from djgentelella.firmador_digital.signvalue_utils import ValueDSParser
8
15
  from djgentelella.forms.forms import GTForm
9
16
  from djgentelella.widgets import core as genwidgets
17
+ from djgentelella.widgets.core import FileInput
10
18
 
11
19
  logger = logging.getLogger('djgentelella')
12
20
 
@@ -26,3 +34,135 @@ class RenderValueForm(GTForm, ValueDSParser):
26
34
  _("Invalid value not encoded as b64 o json parser error"),
27
35
  code="invalid")
28
36
  return jsondata
37
+
38
+
39
+ class SignatureConfigForm(GTForm, forms.ModelForm):
40
+ contact = forms.CharField(
41
+ required=False,
42
+ max_length=100,
43
+ widget=genwidgets.TextInput,
44
+ label=_("Contact")
45
+ )
46
+ dateFormat = forms.ChoiceField(
47
+ widget=genwidgets.Select,
48
+ required=True,
49
+ choices=FORMATS_DATE,
50
+ label=_("Date format")
51
+ )
52
+
53
+ defaultSignMessage = forms.CharField(
54
+ widget=genwidgets.Textarea,
55
+ required=False,
56
+ max_length=100,
57
+ label=_("Signature message")
58
+ )
59
+ fontAlignment = forms.ChoiceField(
60
+ choices=FONT_ALIGNMENT,
61
+ widget=genwidgets.Select,
62
+ required=True,
63
+ label=_("Font alignment")
64
+ )
65
+ fontColor = forms.CharField(
66
+ max_length=50,
67
+ widget=genwidgets.ColorInput,
68
+ required=True,
69
+ label=_("Font color"),
70
+ initial="#FFFFFF",
71
+ )
72
+ fontSize = forms.IntegerField(
73
+ min_value=5,
74
+ max_value=28,
75
+ initial=7,
76
+ widget=genwidgets.NumberInput,
77
+ label=_("Font size")
78
+ )
79
+ place = forms.CharField(
80
+ required=False,
81
+ max_length=100,
82
+ widget=genwidgets.TextInput,
83
+ label=_("Place")
84
+ )
85
+ reason = forms.CharField(
86
+ required=False,
87
+ max_length=100,
88
+ widget=genwidgets.TextInput,
89
+ label=_("Reason")
90
+ )
91
+ isVisibleSignature = forms.BooleanField(
92
+ required=False,
93
+ widget=genwidgets.YesNoInput,
94
+ label=_("Visible signature")
95
+ )
96
+ image = forms.ImageField(
97
+ required=False,
98
+ widget=FileInput,
99
+ label=_("Signature image")
100
+ )
101
+
102
+ default_render_type = "as_grid"
103
+
104
+ grid_representation = [
105
+ [["contact"]],
106
+ [["place"]],
107
+ [["reason"]],
108
+ [["image"], ["preview_image"]],
109
+ [["dateFormat"], ["isVisibleSignature"]],
110
+ [["fontSize"], ["fontColor"], ["fontAlignment"]],
111
+ [["defaultSignMessage"]],
112
+ ]
113
+
114
+ class Meta:
115
+ model = UserSignatureConfig
116
+ fields = []
117
+
118
+ def __init__(self, *args, **kwargs):
119
+ super().__init__(*args, **kwargs)
120
+
121
+ cfg = self.instance.config if getattr(self.instance, "config",
122
+ None) else get_signature_default()
123
+ for key, value in cfg.items():
124
+ if key in self.fields:
125
+ self.fields[key].initial = value
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
+
143
+ def save(self, commit=True):
144
+ data = get_signature_default()
145
+ prev_image = self.instance.config.get("image")
146
+
147
+ for key in data.keys():
148
+ if key in self.cleaned_data:
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
162
+
163
+ if isinstance(data[key], str) and not isinstance(val, str):
164
+ val = str(val)
165
+ data[key] = val
166
+
167
+ self.instance.config = data
168
+ return super().save(commit=commit)
@@ -1,31 +1,26 @@
1
1
  from django.contrib.auth import get_user_model
2
2
  from django.db import models
3
+ from django.utils.translation import gettext_lazy as _
4
+
5
+ FORMATS_DATE = [("dd/MM/yyyy hh:mm:ss a", "dd/MM/yyyy hh:mm:ss a"),
6
+ ("yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm:ss"),
7
+ ("MM/dd/yyyy hh:mm:ss a", "MM/dd/yyyy hh:mm:ss a"),
8
+ ("dd-MM-yyyy", "dd-MM-yyyy"), ]
9
+
10
+ FONT_ALIGNMENT = (('NONE', _('None')), ('RIGHT', _('Right')), ('LEFT', _('Left')),
11
+ ('TOP', _('Top')), ('BOTTOM', _('Bottom')),)
12
+
3
13
 
4
14
  def get_signature_default():
5
- return {
6
- "backgroundColor": "transparente",
7
- "cAdESLevel": "LTA",
8
- "contact": "",
9
- "country": "CR",
10
- "dateFormat": "dd/MM/yyyy hh\:mm\:ss a",
11
- "defaultSignMessage": "Esta es una representación gráfica únicamente,\nverifique la validez de la firma.",
12
- "font": "Nimbus Sans Regular",
13
- "fontAlignment": "RIGHT",
14
- "fontColor": "000000",
15
- "fontSize": "7",
16
- "image": "",
17
- "language": "es",
18
- "pAdESLevel": "LTA",
19
- "place": "",
20
- "portNumber": "3516",
21
- "reason": "",
22
- "signHeight": "33",
23
- "signWidth": "133",
24
- "signX": "40",
25
- "signY": "60",
26
- "xAdESLevel": "LTA",
27
- "isVisibleSignature": False,
28
- }
15
+ return {"backgroundColor": "transparent", "cAdESLevel": "LTA", "contact": "",
16
+ "country": "CR", "dateFormat": "dd/MM/yyyy hh:mm:ss a",
17
+ "defaultSignMessage": "Esta es una representación gráfica únicamente,\nverifique la validez de la firma.",
18
+ "font": "Nimbus Sans Regular", "fontAlignment": "None",
19
+ "fontColor": "000000",
20
+ "fontSize": "7", "image": "", "language": "es", "pAdESLevel": "LTA",
21
+ "place": "", "portNumber": "3516", "reason": "", "signHeight": "33",
22
+ "signWidth": "133", "signX": "40", "signY": "60", "xAdESLevel": "LTA",
23
+ "isVisibleSignature": False, "hideSignatureAdvice": False, }
29
24
 
30
25
 
31
26
  class UserSignatureConfig(models.Model):
@@ -47,6 +47,17 @@ class RemoteSignerClient:
47
47
  )
48
48
  return response.json()
49
49
 
50
+ def validate_document(self, instance):
51
+ b64doc = self.get_b64document(instance['value'])
52
+ files = {
53
+ "b64Document": b64doc,
54
+ "DocumentExtension": ".pdf",
55
+ }
56
+ response = requests.post(
57
+ settings.FIRMADOR_VALIDA_URL, json=files
58
+ )
59
+ return response.json()
60
+
50
61
  def _finalize_signature(self, data_to_sign, task):
51
62
  result = None
52
63
  error_msg = _("An unexpected error occurred during the signing process.")
@@ -0,0 +1,31 @@
1
+ from django.contrib import messages
2
+ from django.contrib.auth.decorators import login_required
3
+ from django.shortcuts import render, get_object_or_404, redirect
4
+ from django.utils.translation import gettext_lazy as _
5
+
6
+ from djgentelella.firmador_digital.forms import SignatureConfigForm
7
+ from djgentelella.firmador_digital.models import UserSignatureConfig
8
+
9
+ @login_required
10
+ def update_signature_settings(request):
11
+
12
+ config, is_created = UserSignatureConfig.objects.get_or_create(user=request.user)
13
+
14
+
15
+ if request.method == "POST":
16
+ form = SignatureConfigForm(request.POST, request.FILES, instance=config, render_type="as_grid")
17
+ if form.is_valid():
18
+ form.save()
19
+ messages.success(request, _("Updated signature settings successfully."))
20
+ return redirect("signature_config")
21
+ else:
22
+ form = SignatureConfigForm(instance=config, render_type="as_grid")
23
+
24
+
25
+ return render(
26
+ request,
27
+ "gentelella/digital_signature/update_signature_settings.html",
28
+ context={
29
+ "form": form
30
+ }
31
+ )
@@ -70,8 +70,19 @@ class DigitalSignatureRenderFileAPIView(APIView):
70
70
  return Response(file_data, content_type='application/pdf')
71
71
 
72
72
  def get_instance_file(self, instance, fieldname):
73
+ # check if instance and fieldname are not None
74
+ if not instance or not fieldname:
75
+ return None
76
+
73
77
  file_path = getattr(instance, fieldname)
74
- file_data = None
75
- with open(file_path.path, 'rb') as f:
76
- file_data = f.read()
77
- return file_data
78
+ if not file_path:
79
+ return None
80
+
81
+ # check if not file
82
+ try:
83
+ with open(file_path.path, 'rb') as f:
84
+ file_data = f.read()
85
+
86
+ return file_data
87
+ except (FileNotFoundError, OSError):
88
+ return None
@@ -69,6 +69,11 @@ class GTForm(forms.Form):
69
69
  case _:
70
70
  self.renderer.form_template_name = self.template_name_horizontal
71
71
 
72
+ def get_error_for_grid(self, name):
73
+ if name in self.errors:
74
+ return self.errors[name]
75
+ return None
76
+
72
77
  @property
73
78
  def grid(self):
74
79
  """
@@ -97,10 +102,13 @@ class GTForm(forms.Form):
97
102
  row_list = []
98
103
  for field in col:
99
104
  if field in dic_fields and dic_fields[field]:
100
- row_list.append(dic_fields[field])
105
+ row_list.append(
106
+ (dic_fields[field], self.get_error_for_grid(field))
107
+ )
101
108
  else:
102
109
  if hasattr(self, field):
103
- row_list.append(getattr(self, field)())
110
+ row_list.append((getattr(self, field)(),
111
+ self.get_error_for_grid(field)))
104
112
  col_list.append(row_list)
105
113
  grid.append(col_list)
106
114
  return grid
djgentelella/groute.py CHANGED
@@ -18,7 +18,10 @@ def register_lookups(prefix='', basename=None):
18
18
  def decore(klass):
19
19
  if basename in ['userbase', 'groupbase']:
20
20
  from djgentelella import settings
21
- if settings.REGISTER_DEFAULT_USER_API:
21
+ if klass.__module__ == 'djgentelella.gtselects':
22
+ if settings.REGISTER_DEFAULT_USER_API:
23
+ routes.register(prefix, klass, basename=basename)
24
+ else:
22
25
  routes.register(prefix, klass, basename=basename)
23
26
  else:
24
27
  routes.register(prefix, klass, basename=basename)
@@ -0,0 +1,145 @@
1
+ from djgentelella.objectmanagement import AuthAllPermBaseObjectManagement
2
+ from django.utils.translation import gettext_lazy as _
3
+ from djgentelella.history.utils import add_log
4
+ from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
5
+ from djgentelella.history.serializers import HistoryDataTableSerializer
6
+ from djgentelella.history.filterset import HistoryFilterSet
7
+ from rest_framework.authentication import SessionAuthentication
8
+ from rest_framework.filters import SearchFilter, OrderingFilter
9
+ from rest_framework.pagination import LimitOffsetPagination
10
+ from django_filters.rest_framework import DjangoFilterBackend
11
+ from rest_framework.response import Response
12
+ from django.conf import settings
13
+ from django.db.models import Q
14
+ from django.contrib.contenttypes.models import ContentType
15
+
16
+
17
+
18
+ class BaseViewSetWithLogs(AuthAllPermBaseObjectManagement):
19
+
20
+ def perform_create(self, serializer):
21
+ super().perform_create(serializer)
22
+ new_instance = serializer.instance
23
+
24
+ add_log(
25
+ self.request.user,
26
+ new_instance,
27
+ ADDITION,
28
+ new_instance._meta.verbose_name.title().lower(),
29
+ [],
30
+ change_message=_("Created"),
31
+ )
32
+
33
+ def perform_update(self, serializer):
34
+ # get the instance before the update
35
+ instance = self.get_object()
36
+ old_values = {
37
+ field: getattr(instance, field)
38
+ for field in serializer.validated_data.keys()
39
+ }
40
+
41
+ super().perform_update(serializer)
42
+
43
+ new_instance = serializer.instance
44
+ # get changed fields
45
+ changed_fields = []
46
+
47
+ for field in serializer.validated_data.keys():
48
+ old_value = old_values.get(field)
49
+ new_value = getattr(new_instance, field)
50
+ if old_value != new_value:
51
+ changed_fields.append(field)
52
+
53
+ add_log(
54
+ self.request.user,
55
+ new_instance,
56
+ CHANGE,
57
+ new_instance._meta.verbose_name.title().lower(),
58
+ changed_data=changed_fields,
59
+ change_message=_("Updated"),
60
+ )
61
+
62
+ def perform_destroy(self, instance):
63
+ instance = self.get_object()
64
+ if instance._meta.verbose_name.title() in self.models_log:
65
+ add_log(
66
+ self.request.user,
67
+ instance,
68
+ DELETION,
69
+ instance._meta.verbose_name.title().lower(),
70
+ change_message=_("Deleted"),
71
+ )
72
+
73
+ super().perform_destroy(instance)
74
+
75
+
76
+ class HistoryViewSet(AuthAllPermBaseObjectManagement):
77
+ authentication_classes = [SessionAuthentication]
78
+ serializer_class = HistoryDataTableSerializer
79
+ queryset = LogEntry.objects.all()
80
+ pagination_class = LimitOffsetPagination
81
+ filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter)
82
+ search_fields = ["object_repr"]
83
+ filterset_class = HistoryFilterSet
84
+ ordering_fields = ["-action_time"]
85
+ ordering = ("-action_time",)
86
+ perms = {"list": ["admin.view_logentry"]}
87
+
88
+ def get_queryset(self):
89
+ queryset = self.queryset
90
+
91
+ # check allowed models in settings
92
+ allowed = getattr(settings, "GT_HISTORY_ALLOWED_MODELS", None)
93
+
94
+ if allowed:
95
+ allowed_ctypes = self.contenttypes_from_settings(allowed)
96
+
97
+ if not allowed_ctypes.exists():
98
+ return queryset.none()
99
+
100
+ queryset = queryset.filter(content_type__in=allowed_ctypes).distinct()
101
+
102
+
103
+ # check contenttype param in form
104
+ ctypes_param = self.request.GET.get("contenttype")
105
+ print(allowed)
106
+ if ctypes_param and ctypes_param in allowed:
107
+
108
+ ctypes_qs = self.contenttypes_from_settings([ctypes_param])
109
+ print(ctypes_qs)
110
+
111
+
112
+ if not ctypes_qs.exists():
113
+ return queryset.none()
114
+
115
+ queryset = queryset.filter(content_type__in=ctypes_qs).distinct()
116
+
117
+ # default values
118
+ return queryset
119
+
120
+ def contenttypes_from_settings(self, entries):
121
+ q = Q()
122
+ for item in entries:
123
+ if isinstance(item, str) and "." in item:
124
+ app_label, model_name = item.split(".", 1)
125
+ app_label = app_label.strip()
126
+ model_key = model_name.strip().lower()
127
+ q |= Q(app_label=app_label, model=model_key)
128
+
129
+ if not q:
130
+ return ContentType.objects.none()
131
+ return ContentType.objects.filter(q)
132
+
133
+
134
+ def list(self, request, *args, **kwargs):
135
+ queryset = self.filter_queryset(self.get_queryset())
136
+ page = self.paginate_queryset(queryset)
137
+ data = page if page is not None else queryset
138
+
139
+ response = {
140
+ "data": data,
141
+ "recordsTotal": LogEntry.objects.count(),
142
+ "recordsFiltered": queryset.count(),
143
+ "draw": self.request.GET.get("draw", 1),
144
+ }
145
+ return Response(self.get_serializer(response).data)
@@ -0,0 +1,23 @@
1
+ from django.contrib.admin.models import LogEntry
2
+ from django_filters.rest_framework import FilterSet, ChoiceFilter
3
+ from django_filters import DateTimeFromToRangeFilter
4
+ from djgentelella.fields.drfdatetime import DateTimeRangeTextWidget
5
+ from djgentelella.history.utils import ACTIONS
6
+
7
+ class HistoryFilterSet(FilterSet):
8
+ action_time = DateTimeFromToRangeFilter(
9
+ widget=DateTimeRangeTextWidget(attrs={"placeholder": "DD/MM/YYYY/"})
10
+ )
11
+
12
+ action_flag = ChoiceFilter(
13
+ field_name="action_flag",
14
+ choices=[(k, str(v)) for k, v in ACTIONS.items()], # added 4 y 5
15
+ )
16
+
17
+ class Meta:
18
+ model = LogEntry
19
+ fields = {
20
+ "object_repr": ["icontains"],
21
+ "change_message": ["icontains"],
22
+ "user": ["exact"],
23
+ }