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 CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = '0.5.5'
1
+ __version__ = '0.5.7'
2
2
 
3
3
  if __name__ == '__main__':
4
4
  print(__version__)
@@ -1,4 +1,4 @@
1
- from django.utils.translation import gettext as _
1
+ from django.utils.translation import gettext_lazy as _
2
2
 
3
3
 
4
4
  class http_status:
@@ -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
- prompt_msg = _(u'Do you want to delete {obj}?')
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
- option_list = BaseCommand.option_list + (
20
- make_option('--interactive',
21
- action='store_true',
22
- dest='interactive',
23
- default=False,
24
- help='Prompt confirmation before each deletion.'),
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
- qs = self.model.objects.all()
32
- qs = qs.filter(created_on__lt=(timezone.now() - EXPIRATION_DELTA))
41
+ exclude_ids = []
33
42
 
34
- for chunked_upload in qs:
35
- if interactive:
36
- prompt = prompt_msg.format(obj=chunked_upload) + u' (y/n): '
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
- count[chunked_upload.status] += 1
44
- # Deleting objects individually to call delete method explicitly
45
- chunked_upload.delete()
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
- print('%i complete uploads were deleted.' % count[COMPLETE])
48
- print('%i incomplete uploads were deleted.' % count[UPLOADING])
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, 'Subiendo'), (2, 'Completo')], default=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)
@@ -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())