djgentelella 0.5.5__py3-none-any.whl → 0.5.7__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/chunked_upload/constants.py +1 -1
- djgentelella/chunked_upload/utils.py +47 -0
- djgentelella/fields/secure.py +109 -0
- djgentelella/management/commands/delete_expired_uploads.py +42 -32
- djgentelella/migrations/0018_alter_chunkedupload_status.py +1 -1
- djgentelella/models_utils.py +87 -0
- djgentelella/permission_management/__init__.py +7 -0
- djgentelella/serializers/__init__.py +14 -0
- djgentelella/serializers/secure.py +8 -0
- djgentelella/static/djgentelella.vendors.header.min.js +1 -1
- djgentelella/static/gentelella/js/permissionmanagement.js +174 -183
- djgentelella/static/vendors/friconix/friconix.js +1 -1
- djgentelella/tasks.py +39 -0
- djgentelella/templates/gentelella/permission_management/permissionmanagement_list.html +19 -10
- {djgentelella-0.5.5.dist-info → djgentelella-0.5.7.dist-info}/METADATA +64 -7
- {djgentelella-0.5.5.dist-info → djgentelella-0.5.7.dist-info}/RECORD +21 -16
- {djgentelella-0.5.5.dist-info → djgentelella-0.5.7.dist-info}/WHEEL +1 -1
- {djgentelella-0.5.5.dist-info → djgentelella-0.5.7.dist-info}/AUTHORS +0 -0
- {djgentelella-0.5.5.dist-info → djgentelella-0.5.7.dist-info}/LICENSE.txt +0 -0
- {djgentelella-0.5.5.dist-info → djgentelella-0.5.7.dist-info}/top_level.txt +0 -0
djgentelella/__init__.py
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for chunked upload management.
|
|
3
|
+
"""
|
|
4
|
+
from django.utils import timezone
|
|
5
|
+
|
|
6
|
+
from djgentelella.chunked_upload.constants import UPLOADING, COMPLETE
|
|
7
|
+
from djgentelella.models import ChunkedUpload
|
|
8
|
+
from djgentelella.settings import EXPIRATION_DELTA
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_expired_uploads():
|
|
12
|
+
"""
|
|
13
|
+
Get queryset of chunked uploads that have expired.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
QuerySet: ChunkedUpload objects that have expired.
|
|
17
|
+
"""
|
|
18
|
+
return ChunkedUpload.objects.filter(
|
|
19
|
+
created_on__lt=(timezone.now() - EXPIRATION_DELTA)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def delete_expired_uploads(exclude_ids=None):
|
|
24
|
+
"""
|
|
25
|
+
Delete chunked uploads that have already expired.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
exclude_ids: Optional list of upload IDs to exclude from deletion.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
dict: A dictionary with 'complete' and 'uploading' keys containing
|
|
32
|
+
the count of deleted uploads for each status.
|
|
33
|
+
"""
|
|
34
|
+
count = {UPLOADING: 0, COMPLETE: 0}
|
|
35
|
+
qs = get_expired_uploads()
|
|
36
|
+
|
|
37
|
+
if exclude_ids:
|
|
38
|
+
qs = qs.exclude(id__in=exclude_ids)
|
|
39
|
+
|
|
40
|
+
for chunked_upload in qs:
|
|
41
|
+
count[chunked_upload.status] += 1
|
|
42
|
+
chunked_upload.delete()
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
'complete': count[COMPLETE],
|
|
46
|
+
'uploading': count[UPLOADING],
|
|
47
|
+
}
|
|
@@ -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
|
|
@@ -1,48 +1,58 @@
|
|
|
1
|
-
from optparse import make_option
|
|
2
|
-
|
|
3
|
-
from chunked_upload.constants import UPLOADING, COMPLETE
|
|
4
|
-
from chunked_upload.models import ChunkedUpload
|
|
5
|
-
from chunked_upload.settings import EXPIRATION_DELTA
|
|
6
1
|
from django.core.management.base import BaseCommand
|
|
7
|
-
from django.utils import timezone
|
|
8
|
-
from django.utils.translation import ugettext as _
|
|
9
2
|
|
|
10
|
-
|
|
3
|
+
from djgentelella.chunked_upload.constants import UPLOADING, COMPLETE
|
|
4
|
+
from djgentelella.chunked_upload.utils import get_expired_uploads, delete_expired_uploads
|
|
11
5
|
|
|
12
6
|
|
|
13
7
|
class Command(BaseCommand):
|
|
14
|
-
# Has to be a ChunkedUpload subclass
|
|
15
|
-
model = ChunkedUpload
|
|
16
|
-
|
|
17
8
|
help = 'Deletes chunked uploads that have already expired.'
|
|
18
9
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
10
|
+
def add_arguments(self, parser):
|
|
11
|
+
parser.add_argument(
|
|
12
|
+
'--interactive',
|
|
13
|
+
action='store_true',
|
|
14
|
+
dest='interactive',
|
|
15
|
+
default=False,
|
|
16
|
+
help='Prompt confirmation before each deletion.',
|
|
17
|
+
)
|
|
26
18
|
|
|
27
19
|
def handle(self, *args, **options):
|
|
28
20
|
interactive = options.get('interactive')
|
|
29
21
|
|
|
22
|
+
if interactive:
|
|
23
|
+
result = self._handle_interactive()
|
|
24
|
+
else:
|
|
25
|
+
result = delete_expired_uploads()
|
|
26
|
+
|
|
27
|
+
self.stdout.write(
|
|
28
|
+
self.style.SUCCESS(
|
|
29
|
+
f"{result['complete']} complete uploads were deleted."
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
self.stdout.write(
|
|
33
|
+
self.style.SUCCESS(
|
|
34
|
+
f"{result['uploading']} incomplete uploads were deleted."
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def _handle_interactive(self):
|
|
39
|
+
"""Handle interactive deletion with user confirmation."""
|
|
30
40
|
count = {UPLOADING: 0, COMPLETE: 0}
|
|
31
|
-
|
|
32
|
-
qs = qs.filter(created_on__lt=(timezone.now() - EXPIRATION_DELTA))
|
|
41
|
+
exclude_ids = []
|
|
33
42
|
|
|
34
|
-
for chunked_upload in
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
for chunked_upload in get_expired_uploads():
|
|
44
|
+
prompt = f'Do you want to delete {chunked_upload}? (y/n): '
|
|
45
|
+
answer = input(prompt).lower()
|
|
46
|
+
while answer not in ('y', 'n'):
|
|
37
47
|
answer = input(prompt).lower()
|
|
38
|
-
while answer not in ('y', 'n'):
|
|
39
|
-
answer = input(prompt).lower()
|
|
40
|
-
if answer == 'n':
|
|
41
|
-
continue
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
if answer == 'n':
|
|
50
|
+
exclude_ids.append(chunked_upload.id)
|
|
51
|
+
else:
|
|
52
|
+
count[chunked_upload.status] += 1
|
|
53
|
+
chunked_upload.delete()
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
return {
|
|
56
|
+
'complete': count[COMPLETE],
|
|
57
|
+
'uploading': count[UPLOADING],
|
|
58
|
+
}
|
|
@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
|
|
|
13
13
|
migrations.AlterField(
|
|
14
14
|
model_name='chunkedupload',
|
|
15
15
|
name='status',
|
|
16
|
-
field=models.PositiveSmallIntegerField(choices=[(1, '
|
|
16
|
+
field=models.PositiveSmallIntegerField(choices=[(1, 'Uploading'), (2, 'Complete')], default=1),
|
|
17
17
|
),
|
|
18
18
|
]
|
|
@@ -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)
|
|
@@ -69,3 +69,10 @@ class AnyPermissionByAction(BasePermission):
|
|
|
69
69
|
return False
|
|
70
70
|
|
|
71
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)
|