djgentelella 0.3.18__py3-none-any.whl → 0.3.20__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 (47) hide show
  1. djgentelella/__init__.py +1 -1
  2. djgentelella/blog/forms.py +3 -3
  3. djgentelella/fields/files.py +125 -0
  4. djgentelella/forms/decorators.py +3 -3
  5. djgentelella/forms/forms.py +153 -33
  6. djgentelella/forms/models.py +2 -2
  7. djgentelella/serializers/selects.py +32 -0
  8. djgentelella/settings.py +1 -1
  9. djgentelella/static/djgentelella.flags.vendors.min.css +1 -1
  10. djgentelella/static/djgentelella.readonly.vendors.min.js +1 -1
  11. djgentelella/static/djgentelella.vendors.header.min.js +1 -1
  12. djgentelella/static/djgentelella.vendors.min.js +3 -2
  13. djgentelella/static/gentelella/css/custom.css +42 -2
  14. djgentelella/static/gentelella/js/base/dateranges_gridslider.js +1 -2
  15. djgentelella/static/gentelella/js/base/fileupload.widget.js +175 -72
  16. djgentelella/static/gentelella/js/base.js +178 -75
  17. djgentelella/static/gentelella/js/datatables.js +20 -3
  18. djgentelella/static/gentelella/js/obj_api_management.js +150 -51
  19. djgentelella/static/gentelella/js/widgets.js +2 -1
  20. djgentelella/static/vendors/flags/1x1/cp.svg +2 -2
  21. djgentelella/static/vendors/flags/1x1/dg.svg +2 -2
  22. djgentelella/static/vendors/flags/1x1/es-ga.svg +2 -2
  23. djgentelella/static/vendors/flags/4x3/ac.svg +2 -2
  24. djgentelella/static/vendors/flags/4x3/ea.svg +2 -2
  25. djgentelella/static/vendors/flags/4x3/es-ct.svg +2 -2
  26. djgentelella/static/vendors/friconix/friconix.js +1 -1
  27. djgentelella/static/vendors/interact/interact.min.js +3 -2
  28. djgentelella/static/vendors/storymapjs/storymap.js +1 -1
  29. djgentelella/templates/forms/as_grid.html +39 -0
  30. djgentelella/templates/forms/as_horizontal.html +36 -0
  31. djgentelella/templates/forms/as_inline.html +38 -0
  32. djgentelella/templates/forms/as_plain.html +33 -0
  33. djgentelella/templates/gentelella/index.html +14 -14
  34. djgentelella/templates/gentelella/widgets/chunkedupload.html +9 -14
  35. djgentelella/templates/gentelella/widgets/file.html +4 -9
  36. djgentelella/tests/__init__.py +2 -1
  37. djgentelella/tests/fields/__init__.py +0 -0
  38. djgentelella/tests/fields/files.py +39 -0
  39. djgentelella/views/select2autocomplete.py +8 -2
  40. djgentelella/widgets/core.py +33 -0
  41. djgentelella/widgets/files.py +37 -8
  42. {djgentelella-0.3.18.dist-info → djgentelella-0.3.20.dist-info}/METADATA +30 -15
  43. {djgentelella-0.3.18.dist-info → djgentelella-0.3.20.dist-info}/RECORD +47 -39
  44. {djgentelella-0.3.18.dist-info → djgentelella-0.3.20.dist-info}/WHEEL +1 -1
  45. {djgentelella-0.3.18.dist-info → djgentelella-0.3.20.dist-info}/AUTHORS +0 -0
  46. {djgentelella-0.3.18.dist-info → djgentelella-0.3.20.dist-info}/LICENSE.txt +0 -0
  47. {djgentelella-0.3.18.dist-info → djgentelella-0.3.20.dist-info}/top_level.txt +0 -0
djgentelella/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.3.18'
1
+ __version__ = '0.3.20'
@@ -2,12 +2,12 @@ from django import forms
2
2
  from markitup.widgets import MarkItUpWidget
3
3
  from rest_framework.reverse import reverse_lazy
4
4
 
5
- from djgentelella.forms.forms import CustomForm
5
+ from djgentelella.forms.forms import GTForm
6
6
  from djgentelella.widgets import core as genwidgets
7
7
  from . import models
8
8
 
9
9
 
10
- class EntryForm(CustomForm, forms.ModelForm):
10
+ class EntryForm(GTForm, forms.ModelForm):
11
11
  class Meta:
12
12
  model = models.Entry
13
13
  exclude = ('published_content', 'author')
@@ -22,7 +22,7 @@ class EntryForm(CustomForm, forms.ModelForm):
22
22
  }
23
23
 
24
24
 
25
- class CategoryForm(CustomForm, forms.ModelForm):
25
+ class CategoryForm(GTForm, forms.ModelForm):
26
26
  class Meta:
27
27
  model = models.Category
28
28
  fields = '__all__'
@@ -0,0 +1,125 @@
1
+ import base64
2
+ import json
3
+ from pathlib import Path
4
+
5
+ from django.core.files.base import ContentFile
6
+ from django.utils.text import slugify
7
+ from django.utils.translation import gettext_lazy as _
8
+ from rest_framework import serializers
9
+
10
+ from djgentelella.models import ChunkedUpload
11
+
12
+
13
+ class GTBase64FileField(serializers.FileField):
14
+ def __init__(self, *args, max_files=1, delete_if_empty=False,
15
+ allow_empty_file=False, **kwargs):
16
+ self.max_files = max_files
17
+ self.delete_if_empty = delete_if_empty
18
+ self.allow_empty_file = allow_empty_file
19
+ super().__init__(*args, **kwargs)
20
+
21
+ def to_internal_value(self, datalist):
22
+ result = []
23
+ if not isinstance(datalist, list):
24
+ raise serializers.ValidationError(
25
+ _("A list of elements is expected, ej: [{name: 'name of file', value:'base64 string representation'}]"))
26
+ if len(datalist) > self.max_files:
27
+ raise serializers.ValidationError(
28
+ _(f"Too many elements, max_file = {self.max_files}"))
29
+
30
+ for data in datalist:
31
+ required_fields = ["name", "value"]
32
+ for field in required_fields:
33
+ if field not in data:
34
+ raise serializers.ValidationError(
35
+ _("Invalid structure you need to provide {name: 'name of file', value:'base64 string representation'}"))
36
+ name = slugify(Path(data["name"]).stem)
37
+ suffix = Path(data["name"]).suffix
38
+ file_name = name + suffix
39
+ file_value = data["value"]
40
+
41
+ try:
42
+ # Decodificar el contenido en base64
43
+ decoded_value = base64.b64decode(file_value)
44
+ except base64.binascii.Error:
45
+ self.fail('invalid')
46
+ # raise serializers.ValidationError(_(
47
+ # "The 'value' is not a valid base64 string"))
48
+
49
+ file_content = ContentFile(decoded_value, name=file_name)
50
+ result.append(file_content)
51
+ if result:
52
+ if self.max_files == 1:
53
+ return result[0]
54
+
55
+ return result
56
+ # here check for field of the instance
57
+ if not self.delete_if_empty and self.root.instance:
58
+ if hasattr(self.root.instance, self.source):
59
+ return getattr(self.root.instance, self.source)
60
+ if not self.allow_empty_file:
61
+ self.fail('required')
62
+
63
+ def to_representation(self, value):
64
+ data = super().to_representation(value)
65
+ if data and value.name and value.storage.exists(value.name):
66
+ name = Path(value.name).name
67
+ return {'name': name, 'url': data}
68
+
69
+
70
+ class ChunkedFileField(serializers.FileField):
71
+
72
+ def parse_value(self, value):
73
+ """
74
+ Parses the given value and returns the parsed result.
75
+
76
+ Args:
77
+ value (str): The value to be parsed.
78
+
79
+ Returns:
80
+ The parsed result if the value is valid and contains the required attributes,
81
+ one of url, token, or actions otherwise returns None.
82
+ """
83
+ dev = None
84
+ try:
85
+ dev = json.loads(value)
86
+ if not ('url' in dev or 'token' in dev or 'actions' in dev):
87
+ dev = None
88
+ except Exception as e:
89
+ pass
90
+ return dev
91
+
92
+ def to_internal_value(self, data):
93
+ """
94
+ Converts the given data to internal value representation.
95
+
96
+ Args:
97
+ data (str): The data to be converted.
98
+
99
+ Returns:
100
+ The internal value representation of the data, or None if the data is invalid
101
+ or does not contain the required attributes.
102
+ """
103
+ token = self.parse_value(data)
104
+ dev = None
105
+ if token:
106
+ if 'actions' in token and token['actions'] == 'delete':
107
+ return False
108
+ if 'token' in token:
109
+ tmpupload = ChunkedUpload.objects.filter(
110
+ upload_id=token['token']).first()
111
+ if tmpupload:
112
+ dev = tmpupload.get_uploaded_file()
113
+ # tmpupload.delete()
114
+
115
+ else:
116
+ if self.root.instance:
117
+ if hasattr(self.root.instance, self.source):
118
+ return getattr(self.root.instance, self.source)
119
+ return dev
120
+
121
+ def to_representation(self, value):
122
+ data = super().to_representation(value)
123
+ if data and value.name and value.storage.exists(value.name):
124
+ name = Path(value.name).name
125
+ return {'name': value.name, 'url': data, 'display_name': name}
@@ -1,5 +1,5 @@
1
1
  from djgentelella.fields import tree
2
- from djgentelella.forms.forms import CustomForm
2
+ from djgentelella.forms.forms import GTForm
3
3
  from djgentelella.widgets import core
4
4
  from djgentelella.widgets import trees
5
5
 
@@ -55,8 +55,8 @@ def decore_form_instance(form_instance, exclude=()):
55
55
  widget = get_field_widget(form_instance.fields[field])
56
56
  if widget:
57
57
  form_instance.fields[field].widget = widget
58
- for method in CustomForm.exposed_method:
58
+ for method in GTForm.exposed_method:
59
59
  setattr(form_instance, method,
60
- _form_instance(getattr(CustomForm, method), form_instance))
60
+ _form_instance(getattr(GTForm, method), form_instance))
61
61
  form_instance.is_customized = True
62
62
  return form_instance
@@ -1,5 +1,4 @@
1
1
  from django import forms
2
- # "'<tr%(html_class_attr)s><th>%(label)s</th><td>%(errors)s%(field)s%(help_text)s</td></tr>'"
3
2
  from django.forms import BaseFormSet, HiddenInput
4
3
  from django.forms import BaseModelFormSet
5
4
  from django.forms.formsets import DELETION_FIELD_NAME
@@ -8,46 +7,162 @@ from django.utils.safestring import mark_safe
8
7
 
9
8
  class GTForm(forms.Form):
10
9
  """
11
- Append the next render methods to forms
10
+ GTForm is the basis of form management, it does the work of django `forms.Form`, including enhancements and boostrap rendering, so it should be inherited from this form, rather than `forms.Form`.
11
+
12
+ Example of use:
13
+
14
+ .. code:: python
15
+
16
+ from djgentelella.forms.forms import GTForm
17
+ class MyForm(GTForm):
18
+ myfield = forms.TextField()
19
+
20
+ Using with forms.ModelForm
21
+
22
+ .. code:: python
23
+
24
+ from djgentelella.forms.forms import GTForm
25
+ class MyForm(GTForm, forms.ModelForm):
26
+ myfield = forms.TextField()
27
+ class Meta:
28
+ model = MyModel
29
+
30
+ Creating an instance and specify how render it.
31
+
32
+ .. code:: python
33
+
34
+ myform = myGTForm(render_type='as_inline', ... )
35
+
12
36
  """
13
37
  exposed_method = ('as_plain', 'as_inline', 'as_horizontal')
38
+ default_render_type = None
39
+ template_name_plain = 'forms/as_plain.html'
40
+ template_name_inline = 'forms/as_inline.html'
41
+ template_name_horizontal = 'forms/as_horizontal.html'
42
+ template_name_grid = 'forms/as_grid.html'
43
+ grid_representation = None
44
+
45
+ def __init__(self, *args, **kwargs):
46
+ render_type = self.default_render_type if self.default_render_type is not None else 'as_horizontal'
47
+ if 'render_type' in kwargs and hasattr(self, kwargs['render_type']):
48
+ render_type = kwargs.pop('render_type')
49
+
50
+ super().__init__(*args, **kwargs)
51
+
52
+ match render_type:
53
+ case 'as_table':
54
+ self.renderer.form_template_name = self.template_name_table
55
+ case 'as_ul':
56
+ self.renderer.form_template_name = self.template_name_ul
57
+ case 'as_p':
58
+ self.renderer.form_template_name = self.template_name_p
59
+ case 'as_div':
60
+ self.renderer.form_template_name = self.template_name_div
61
+ case 'as_horizontal':
62
+ self.renderer.form_template_name = self.template_name_horizontal
63
+ case 'as_inline':
64
+ self.renderer.form_template_name = self.template_name_inline
65
+ case 'as_plain':
66
+ self.renderer.form_template_name = self.template_name_plain
67
+ case 'as_grid':
68
+ self.renderer.form_template_name = self.template_name_grid
69
+ case _:
70
+ self.renderer.form_template_name = self.template_name_horizontal
71
+
72
+ @property
73
+ def grid(self):
74
+ """
75
+ Example of return structure:
76
+
77
+ .. code:: python
78
+
79
+ [
80
+ [ [forms.Field],[], [] ],
81
+ [ [], [] ],
82
+ ]
83
+
84
+ """
85
+ fields = []
86
+ dic_fields = {}
87
+ for name, bf in self._bound_items():
88
+ if not bf.is_hidden:
89
+ fields.append(bf)
90
+ dic_fields[name] = bf
91
+
92
+ if self.grid_representation is not None:
93
+ grid = [] # self.grid_representation
94
+ for row in self.grid_representation:
95
+ col_list = []
96
+ for col in row:
97
+ row_list = []
98
+ for field in col:
99
+ if field in dic_fields and dic_fields[field]:
100
+ row_list.append(dic_fields[field])
101
+ else:
102
+ if hasattr(self, field):
103
+ row_list.append(getattr(self, field)())
104
+ col_list.append(row_list)
105
+ grid.append(col_list)
106
+ return grid
107
+ return [[fields]]
14
108
 
15
109
  def as_plain(self):
16
- "Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
17
-
18
- return self._html_output(
19
- normal_row='<div class="as_plain"><div %(html_class_attr)s ' +
20
- '>%(label)s%(errors)s%(field)s%(help_text)s</div></div>',
21
- error_row='%s',
22
- row_ender=' ',
23
- help_text_html='<br /><span class="helptext">%s</span>',
24
- errors_on_separate_row=False)
110
+ "Returns this form rendered as HTML using as_plain bootstrap approach."
111
+ if hasattr(self, '_html_output'):
112
+ return self._html_output(
113
+ normal_row='<div class="as_plain"><div %(html_class_attr)s ' +
114
+ '>%(label)s%(errors)s%(field)s%(help_text)s</div></div>',
115
+ error_row='%s',
116
+ row_ender=' ',
117
+ help_text_html='<br /><span class="helptext">%s</span>',
118
+ errors_on_separate_row=False)
119
+ return self.render(self.template_name_plain)
25
120
 
26
121
  def as_inline(self):
27
- "Return this form rendered as HTML <tr>s -- excluding the <table></table>."
28
- return self._html_output(
29
- normal_row='<div class="mb-4"><span class="">%(label)s</span>' +
30
- ' %(errors)s%(field)s%(help_text)s</div>',
31
- error_row='%s',
32
- row_ender='</div>',
33
- help_text_html=' <span class="helptext">%s</span>',
34
- errors_on_separate_row=False,
35
- )
122
+ "Return this form rendered as HTML using as_inline bootstrap approach."
123
+ if hasattr(self, '_html_output'):
124
+ return self._html_output(
125
+ normal_row='<div class="mb-4"><span class="">%(label)s</span>' +
126
+ ' %(errors)s%(field)s%(help_text)s</div>',
127
+ error_row='%s',
128
+ row_ender='</div>',
129
+ help_text_html=' <span class="helptext">%s</span>',
130
+ errors_on_separate_row=False,
131
+ )
132
+ return self.render(self.template_name_inline)
36
133
 
37
134
  def as_horizontal(self):
38
- "Return this form rendered as HTML <tr>s -- excluding the <table></table>."
39
- return self._html_output(
40
- normal_row='<div class="form-group row"><span class="col-sm-3">' +
41
- '%(label)s</span> <div class="col-sm-9 " ' +
42
- '>%(errors)s%(field)s%(help_text)s</div></div>',
43
- error_row='%s',
44
- row_ender='</div>',
45
- help_text_html=' <span class="helptext">%s</span>',
46
- errors_on_separate_row=False,
47
- )
48
-
49
-
50
- CustomForm = GTForm
135
+ "Return this form rendered as HTML using as_horizontal bootstrap approach."
136
+ if hasattr(self, '_html_output'):
137
+ return self._html_output(
138
+ normal_row='<div class="form-group row"><span class="col-sm-3">' +
139
+ '%(label)s</span> <div class="col-sm-9 " ' +
140
+ '>%(errors)s%(field)s%(help_text)s</div></div>',
141
+ error_row='%s',
142
+ row_ender='</div>',
143
+ help_text_html=' <span class="helptext">%s</span>',
144
+ errors_on_separate_row=False,
145
+ )
146
+ return self.render(self.template_name_horizontal)
147
+
148
+ def as_grid(self):
149
+ """
150
+ Allow you to arrange the form fields in rows and cols,
151
+ When you use this render needs to fill `grid_representation` attribute in your form
152
+ Return this form rendered as HTML using grid bootstrap approach.,
153
+
154
+ .. code:: python
155
+
156
+ grid_representation=[
157
+ [ ['key'],[], [] ],
158
+ [ [], [] ],
159
+ ]
160
+
161
+ """
162
+ return self.render(self.template_name_grid)
163
+
164
+ def closediv(self):
165
+ return "</div>"
51
166
 
52
167
 
53
168
  class BaseFormset:
@@ -65,6 +180,11 @@ class BaseFormset:
65
180
 
66
181
 
67
182
  class GTFormSet(BaseFormSet, BaseFormset):
183
+ """
184
+ This class Allow to manage FormSet using GTForm and Widgets,
185
+ provide an implementation to integrate with django formset system.
186
+ """
187
+
68
188
  ordering_widget = HiddenInput
69
189
 
70
190
  def add_fields(self, form, index):
@@ -2,10 +2,10 @@ from django import forms
2
2
 
3
3
  from djgentelella.models import Help
4
4
  from djgentelella.widgets import core as genwidgets
5
- from .forms import CustomForm
5
+ from .forms import GTForm
6
6
 
7
7
 
8
- class HelpForm(CustomForm, forms.ModelForm):
8
+ class HelpForm(GTForm, forms.ModelForm):
9
9
  class Meta:
10
10
  model = Help
11
11
  fields = ['help_title', 'help_text']
@@ -0,0 +1,32 @@
1
+ from rest_framework import serializers
2
+
3
+
4
+ class GTS2SerializerBase(serializers.Serializer):
5
+ id_field = 'pk'
6
+ display_fields = '__str__'
7
+ default_selected = True
8
+ default_disable = False
9
+
10
+ def get_id(self, obj):
11
+ return getattr(obj, self.id_field)
12
+
13
+ def get_text(self, obj):
14
+ if isinstance(self.display_fields, str):
15
+ if self.display_fields == '__str__':
16
+ return str(obj)
17
+ return getattr(obj, self.display_fields)
18
+ if isinstance(self.display_fields, (list, tuple)):
19
+ lwords = [getattr(obj, key) for key in self.display_fields]
20
+ return " ".join(lwords)
21
+ return repr(obj)
22
+
23
+ def get_selected(self, obj):
24
+ return self.default_selected
25
+
26
+ def get_disabled(self, obj):
27
+ return self.default_disable
28
+
29
+ id = serializers.SerializerMethodField()
30
+ text = serializers.SerializerMethodField()
31
+ disabled = serializers.SerializerMethodField()
32
+ selected = serializers.SerializerMethodField()
djgentelella/settings.py CHANGED
@@ -44,7 +44,7 @@ except Exception as e:
44
44
  from django.contrib.auth.models import User
45
45
 
46
46
  #####################################################
47
- # Chuncked Upload
47
+ # Chunked Upload
48
48
  ############################################
49
49
 
50
50
  # How long after creation the upload will expire