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.
- djgentelella/__init__.py +1 -1
- djgentelella/admin.py +7 -1
- djgentelella/firmador_digital/forms.py +46 -16
- djgentelella/firmador_digital/models.py +12 -15
- djgentelella/firmador_digital/views.py +1 -1
- djgentelella/locale/es/LC_MESSAGES/django.mo +0 -0
- djgentelella/locale/es/LC_MESSAGES/django.po +18 -0
- djgentelella/locale/es/LC_MESSAGES/djangojs.mo +0 -0
- djgentelella/migrations/0014_trash.py +31 -0
- djgentelella/migrations/0015_trash_deleted_by.py +21 -0
- djgentelella/migrations/0016_trash_created_at.py +20 -0
- djgentelella/models.py +96 -0
- djgentelella/models_manager.py +40 -0
- djgentelella/static/djgentelella.flags.vendors.min.css +1 -1
- djgentelella/static/gentelella/css/custom.css +19 -2
- djgentelella/static/gentelella/css/modern.css +15 -0
- djgentelella/static/gentelella/css/modern_black_white.css +15 -0
- djgentelella/static/gentelella/css/pdfviewer.css +43 -1
- djgentelella/static/gentelella/images/default.png +0 -0
- djgentelella/static/gentelella/js/base/digital_signature.js +87 -26
- djgentelella/static/gentelella/js/base.js +87 -26
- djgentelella/static/gentelella/js/digital_signature_update.js +29 -0
- djgentelella/static/vendors/bootstrap-datetimepicker/bootstrap-datetimepicker.min.css.map +75 -7
- djgentelella/static/vendors/flags/1x1/ac.svg +75 -7
- djgentelella/static/vendors/flags/1x1/cp.svg +75 -7
- djgentelella/static/vendors/flags/1x1/dg.svg +75 -7
- djgentelella/static/vendors/flags/1x1/ea.svg +75 -7
- djgentelella/static/vendors/flags/1x1/es-ct.svg +75 -7
- djgentelella/static/vendors/flags/1x1/es-ga.svg +75 -7
- djgentelella/static/vendors/flags/1x1/ic.svg +75 -7
- djgentelella/static/vendors/flags/1x1/ta.svg +75 -7
- djgentelella/static/vendors/flags/1x1/xx.svg +75 -7
- djgentelella/static/vendors/flags/4x3/ac.svg +75 -7
- djgentelella/static/vendors/flags/4x3/cp.svg +75 -7
- djgentelella/static/vendors/flags/4x3/dg.svg +75 -7
- djgentelella/static/vendors/flags/4x3/ea.svg +75 -7
- djgentelella/static/vendors/flags/4x3/es-ct.svg +75 -7
- djgentelella/static/vendors/flags/4x3/es-ga.svg +75 -7
- djgentelella/static/vendors/flags/4x3/ic.svg +75 -7
- djgentelella/static/vendors/flags/4x3/ta.svg +75 -7
- djgentelella/static/vendors/flags/4x3/xx.svg +75 -7
- djgentelella/templates/gentelella/app/sidebar.html +7 -4
- djgentelella/templates/gentelella/digital_signature/update_signature_settings.html +24 -36
- djgentelella/templates/gentelella/widgets/digital_signature.html +41 -7
- djgentelella/trash/api.py +58 -0
- djgentelella/trash/filterset.py +20 -0
- djgentelella/trash/serializer.py +45 -0
- djgentelella/urls.py +4 -0
- djgentelella/widgets/core.py +6 -5
- {djgentelella-0.4.10.dist-info → djgentelella-0.5.0.dist-info}/METADATA +2 -2
- {djgentelella-0.4.10.dist-info → djgentelella-0.5.0.dist-info}/RECORD +55 -46
- {djgentelella-0.4.10.dist-info → djgentelella-0.5.0.dist-info}/AUTHORS +0 -0
- {djgentelella-0.4.10.dist-info → djgentelella-0.5.0.dist-info}/LICENSE.txt +0 -0
- {djgentelella-0.4.10.dist-info → djgentelella-0.5.0.dist-info}/WHEEL +0 -0
- {djgentelella-0.4.10.dist-info → djgentelella-0.5.0.dist-info}/top_level.txt +0 -0
djgentelella/__init__.py
CHANGED
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
|
|
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
|
-
|
|
108
|
+
[["image"], ["preview_image"]],
|
|
109
109
|
[["dateFormat"], ["isVisibleSignature"]],
|
|
110
|
-
[["fontSize"], ["fontColor"], ["
|
|
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",
|
|
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",
|
|
9
|
+
("dd-MM-yyyy", "dd-MM-yyyy"),
|
|
10
10
|
]
|
|
11
11
|
|
|
12
|
-
FONT_ALIGNMENT =
|
|
13
|
-
(
|
|
14
|
-
(
|
|
15
|
-
(
|
|
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": "
|
|
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": "
|
|
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."))
|
|
Binary file
|
|
@@ -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"
|
|
Binary file
|
|
@@ -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
|
+
|