From 28ca6328d1d3efbdf4845b88f1b2d484bb0bc120 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Tue, 29 Sep 2020 19:10:55 -0300 Subject: [PATCH 01/15] =?UTF-8?q?Deixa=20a=20tabela=20como=20um=20dado=20e?= =?UTF-8?q?xtra=20do=20model=20para=20conseguir=20ver=20as=20configura?= =?UTF-8?q?=C3=A7=C3=B5es=20din=C3=A2micas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/models.py b/core/models.py index d52a3799..bed546f3 100644 --- a/core/models.py +++ b/core/models.py @@ -419,6 +419,7 @@ def get_model(self, cache=True, data_table=None): "filtering": filtering, "ordering": ordering, "search": search, + "table": self, } DYNAMIC_MODEL_REGISTRY[cache_key] = Model return Model From 2ba9e6b930948c34a3ed93e5dcac02a0dc17de39 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Tue, 29 Sep 2020 19:11:23 -0300 Subject: [PATCH 02/15] =?UTF-8?q?Constr=C3=B3i=20model=20form=20din=C3=A2m?= =?UTF-8?q?ico=20baseado=20no=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/models.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/models.py b/core/models.py index bed546f3..6fa08ccc 100644 --- a/core/models.py +++ b/core/models.py @@ -93,7 +93,14 @@ def apply_filters(self, filtering): # TODO: filtering must be based on field's settings, not on models # settings. model_filtering = self.model.extra["filtering"] - processor = DynamicModelFilterProcessor(filtering, model_filtering) + + from django.forms import modelform_factory + FilterFormClass = modelform_factory(self.model, fields=model_filtering) + form = FilterFormClass(data=filtering) + if not form.is_valid(): + raise Exception(form.errors) # TODO create brasil.io custom exception + + processor = DynamicModelFilterProcessor(form.cleaned_data, model_filtering) return self.filter(**processor.filters) def apply_ordering(self, query): From 5459397c76392d6eac0470c51fdeed3b19066213 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Tue, 29 Sep 2020 19:11:56 -0300 Subject: [PATCH 03/15] =?UTF-8?q?Constr=C3=B3i=20campos=20com=20choices=20?= =?UTF-8?q?se=20field=20din=C3=A2micos=20possuir=20informa=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/models.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/core/models.py b/core/models.py index 6fa08ccc..1f7a1a65 100644 --- a/core/models.py +++ b/core/models.py @@ -95,14 +95,18 @@ def apply_filters(self, filtering): model_filtering = self.model.extra["filtering"] from django.forms import modelform_factory - FilterFormClass = modelform_factory(self.model, fields=model_filtering) + FilterFormClass = modelform_factory( + self.model, fields=model_filtering, formfield_callback=build_dynamic_filter_field + ) form = FilterFormClass(data=filtering) if not form.is_valid(): raise Exception(form.errors) # TODO create brasil.io custom exception - processor = DynamicModelFilterProcessor(form.cleaned_data, model_filtering) + cleaned_data = {k: v for k, v in form.cleaned_data.items() if v != ""} + processor = DynamicModelFilterProcessor(cleaned_data, model_filtering) return self.filter(**processor.filters) + def apply_ordering(self, query): qs = self # TODO: may use Model's meta "ordering" instead of extra["ordering"] @@ -160,6 +164,18 @@ def count(self): return self._count +def build_dynamic_filter_field(model_field): + from django import forms + table = model_field.model.extra["table"] + dynamic_field = [f for f in table.fields if f.name == model_field.name][0] + + if dynamic_field.has_choices: + choices = [("", "Todos")] + [(c, c) for c in dynamic_field.choices["data"]] + return forms.ChoiceField(required=False, choices=choices) + else: + return model_field.formfield(required=False) + + class Dataset(models.Model): author_name = models.CharField(max_length=255, null=False, blank=False) author_url = models.URLField(max_length=2000, null=True, blank=True) From 717969c0888730c578b310720268f34a3bee1d8e Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Thu, 1 Oct 2020 17:36:01 -0300 Subject: [PATCH 04/15] =?UTF-8?q?Move=20constru=C3=A7=C3=A3o=20de=20form?= =?UTF-8?q?=20din=C3=A2mico=20para=20a=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/forms.py | 15 +++++++++++++++ core/models.py | 25 +------------------------ core/views.py | 9 ++++++++- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/core/forms.py b/core/forms.py index 98b7c0a6..e26d8372 100644 --- a/core/forms.py +++ b/core/forms.py @@ -104,3 +104,18 @@ class ContactForm(forms.Form): class DatasetSearchForm(forms.Form): search = forms.CharField(label="Titulo ou Descrição") + + +def get_table_dynamic_form(table): + def config_dynamic_filter(model_field): + dynamic_field = [f for f in table.fields if f.name == model_field.name][0] + + if dynamic_field.has_choices: + choices = [("", "Todos")] + [(c, c) for c in dynamic_field.choices["data"]] + return forms.ChoiceField(required=False, choices=choices) + else: + return model_field.formfield(required=False) + + model = table.get_model() + fields = model.extra["filtering"] + return forms.modelform_factory(model, fields=fields, formfield_callback=config_dynamic_filter) diff --git a/core/models.py b/core/models.py index 6f7ebdd9..65020a5e 100644 --- a/core/models.py +++ b/core/models.py @@ -93,20 +93,9 @@ def apply_filters(self, filtering): # TODO: filtering must be based on field's settings, not on models # settings. model_filtering = self.model.extra["filtering"] - - from django.forms import modelform_factory - FilterFormClass = modelform_factory( - self.model, fields=model_filtering, formfield_callback=build_dynamic_filter_field - ) - form = FilterFormClass(data=filtering) - if not form.is_valid(): - raise Exception(form.errors) # TODO create brasil.io custom exception - - cleaned_data = {k: v for k, v in form.cleaned_data.items() if v != ""} - processor = DynamicModelFilterProcessor(cleaned_data, model_filtering) + processor = DynamicModelFilterProcessor(filtering, model_filtering) return self.filter(**processor.filters) - def apply_ordering(self, query): qs = self # TODO: may use Model's meta "ordering" instead of extra["ordering"] @@ -164,18 +153,6 @@ def count(self): return self._count -def build_dynamic_filter_field(model_field): - from django import forms - table = model_field.model.extra["table"] - dynamic_field = [f for f in table.fields if f.name == model_field.name][0] - - if dynamic_field.has_choices: - choices = [("", "Todos")] + [(c, c) for c in dynamic_field.choices["data"]] - return forms.ChoiceField(required=False, choices=choices) - else: - return model_field.formfield(required=False) - - class Dataset(models.Model): author_name = models.CharField(max_length=255, null=False, blank=False) author_url = models.URLField(max_length=2000, null=True, blank=True) diff --git a/core/views.py b/core/views.py index ac108187..d3120363 100644 --- a/core/views.py +++ b/core/views.py @@ -10,7 +10,7 @@ from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse -from core.forms import ContactForm, DatasetSearchForm +from core.forms import ContactForm, DatasetSearchForm, get_table_dynamic_form from core.middlewares import disable_non_logged_user_cache from core.models import Dataset, Table from core.templatetags.utils import obfuscate @@ -150,6 +150,13 @@ def dataset_detail(request, slug, tablename=""): TableModel = table.get_model() query, search_query, order_by = TableModel.objects.parse_querystring(querystring) + + DynamicForm = get_table_dynamic_form(table) + filter_form = DynamicForm(data=query) + if not filter_form.is_valid(): + raise Exception(str(filter_form.errors)) + + query = {k: v for k, v in filter_form.cleaned_data.items() if v != ""} all_data = TableModel.objects.composed_query(query, search_query, order_by) if download_csv: From d9ebffa5b8131bcd2c2792b7a523d8f26fc47311 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Thu, 1 Oct 2020 17:46:51 -0300 Subject: [PATCH 05/15] =?UTF-8?q?Refatora=20HTML=20de=20detalhe=20de=20dat?= =?UTF-8?q?aset=20para=20usar=20form=20din=C3=A2mico?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/templates/core/dataset-detail.html | 19 ++++--------------- core/views.py | 1 + 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/core/templates/core/dataset-detail.html b/core/templates/core/dataset-detail.html index 1fa1d733..8181bbba 100644 --- a/core/templates/core/dataset-detail.html +++ b/core/templates/core/dataset-detail.html @@ -82,23 +82,12 @@
Filtros
- {% for field in fields %} - {% if field.frontend_filter %}{% with value=query_dict|getplainattribute:field|default:'' %} + {% for field in filter_form %}
- - {% if field.has_choices %} - - {% else %} - - {% endif %} + + {{ field }}
- {% endwith %}{% endif %} - {% endfor %} + {% endfor %}
diff --git a/core/views.py b/core/views.py index d3120363..83ecdaaa 100644 --- a/core/views.py +++ b/core/views.py @@ -198,6 +198,7 @@ def dataset_detail(request, slug, tablename=""): "data": data, "dataset": dataset, "fields": fields, + "filter_form": filter_form, "max_export_rows": settings.CSV_EXPORT_MAX_ROWS, "query_dict": querystring, "querystring": querystring.urlencode(), From ab650c37f53f771eff12de2c840084546d24dea1 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Thu, 1 Oct 2020 17:47:11 -0300 Subject: [PATCH 06/15] =?UTF-8?q?Popula=20o=20label=20do=20campo=20com=20o?= =?UTF-8?q?=20atributo=20title=20do=20field=20din=C3=A2mico?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/forms.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/forms.py b/core/forms.py index e26d8372..8655f38d 100644 --- a/core/forms.py +++ b/core/forms.py @@ -109,12 +109,14 @@ class DatasetSearchForm(forms.Form): def get_table_dynamic_form(table): def config_dynamic_filter(model_field): dynamic_field = [f for f in table.fields if f.name == model_field.name][0] + kwargs = {"required": False, "label": dynamic_field.title} + field_factory = model_field.formfield if dynamic_field.has_choices: - choices = [("", "Todos")] + [(c, c) for c in dynamic_field.choices["data"]] - return forms.ChoiceField(required=False, choices=choices) - else: - return model_field.formfield(required=False) + kwargs["choices"] = [("", "Todos")] + [(c, c) for c in dynamic_field.choices["data"]] + field_factory = forms.ChoiceField + + return field_factory(**kwargs) model = table.get_model() fields = model.extra["filtering"] From 997fe44b25ee5bf72ae6873ff414ef78881d1c60 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Thu, 1 Oct 2020 18:00:08 -0300 Subject: [PATCH 07/15] =?UTF-8?q?Apresenta=20mensagem=20de=20erro=20no=20f?= =?UTF-8?q?ormul=C3=A1rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/templates/core/dataset-detail.html | 3 +++ core/views.py | 7 ++++--- static/css/base.css | 7 ++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/core/templates/core/dataset-detail.html b/core/templates/core/dataset-detail.html index 8181bbba..ac505027 100644 --- a/core/templates/core/dataset-detail.html +++ b/core/templates/core/dataset-detail.html @@ -86,6 +86,9 @@
Filtros
{{ field }} + {% if field.errors %} + {{ field.errors.as_text }} + {% endif %}
{% endfor %} diff --git a/core/views.py b/core/views.py index 83ecdaaa..90944374 100644 --- a/core/views.py +++ b/core/views.py @@ -153,10 +153,11 @@ def dataset_detail(request, slug, tablename=""): DynamicForm = get_table_dynamic_form(table) filter_form = DynamicForm(data=query) - if not filter_form.is_valid(): - raise Exception(str(filter_form.errors)) + if filter_form.is_valid(): + query = {k: v for k, v in filter_form.cleaned_data.items() if v != ""} + else: + query = {} - query = {k: v for k, v in filter_form.cleaned_data.items() if v != ""} all_data = TableModel.objects.composed_query(query, search_query, order_by) if download_csv: diff --git a/static/css/base.css b/static/css/base.css index 6816b675..56ee8bc8 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -191,4 +191,9 @@ a { .error-container p{ font-size: 1.4rem; -} \ No newline at end of file +} + +.filter-error { + color: #ee6e73; + font-size: 1rem; +} From 309d9af80cb07025b56de858bafb6799c9a2bc39 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Thu, 1 Oct 2020 18:16:34 -0300 Subject: [PATCH 08/15] =?UTF-8?q?Adiciona=20valida=C3=A7=C3=A3o=20de=20que?= =?UTF-8?q?rystring=20na=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/views.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/api/views.py b/api/views.py index 7ca19523..26acb51b 100644 --- a/api/views.py +++ b/api/views.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from api.serializers import DatasetDetailSerializer, DatasetSerializer, GenericSerializer +from core.forms import get_table_dynamic_form from core.models import Dataset, Table from core.templatetags.utils import obfuscate @@ -26,6 +27,11 @@ def retrieve(self, request, slug): return Response(serializer.data) +class InvalidFiltersException(Exception): + def __init__(self, errors_list): + self.errors_list = errors_list + + class DatasetDataListView(ListAPIView): pagination_class = paginators.LargeTablePageNumberPagination @@ -44,9 +50,16 @@ def get_queryset(self): del querystring[pagination_key] Model = self.get_model_class() - queryset = Model.objects.filter_by_querystring(querystring) + query, search_query, order_by = Model.objects.parse_querystring(querystring) - return queryset + DynamicForm = get_table_dynamic_form(self.get_table()) + filter_form = DynamicForm(data=query) + if filter_form.is_valid(): + query = {k: v for k, v in filter_form.cleaned_data.items() if v != ""} + else: + raise InvalidFiltersException(filter_form.errors) + + return Model.objects.composed_query(query, search_query, order_by) def get_serializer_class(self): table = self.get_table() @@ -72,6 +85,12 @@ def get_serializer(self, *args, **kwargs): return super().get_serializer(*args, **kwargs) + def handle_exception(self, exc): + if isinstance(exc, InvalidFiltersException): + return Response(exc.errors_list, status=400) + else: + return super().handle_exception(exc) + dataset_list = DatasetViewSet.as_view({"get": "list"}) dataset_detail = DatasetViewSet.as_view({"get": "retrieve"}, lookup_field="slug") From 31403920256d82d188720eda8b1b9876e397e719 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Fri, 2 Oct 2020 16:51:10 -0300 Subject: [PATCH 09/15] =?UTF-8?q?Remove=20campo=20que=20n=C3=A3o=20=C3=A9?= =?UTF-8?q?=20apresentado=20no=20html?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/views.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/views.py b/core/views.py index 90944374..67b2064e 100644 --- a/core/views.py +++ b/core/views.py @@ -146,7 +146,6 @@ def dataset_detail(request, slug, tablename=""): items_per_page = min(items_per_page, 1000) version = dataset.version_set.order_by("-order").first() - fields = table.fields TableModel = table.get_model() query, search_query, order_by = TableModel.objects.parse_querystring(querystring) @@ -180,7 +179,7 @@ def dataset_detail(request, slug, tablename=""): filename = "{}-{}.csv".format(slug, uuid.uuid4().hex) pseudo_buffer = Echo() writer = csv.writer(pseudo_buffer, dialect=csv.excel) - csv_rows = queryset_to_csv(all_data, fields) + csv_rows = queryset_to_csv(all_data, table.fields) response = StreamingHttpResponse( (writer.writerow(row) for row in csv_rows), content_type="text/csv;charset=UTF-8", ) @@ -198,7 +197,6 @@ def dataset_detail(request, slug, tablename=""): context = { "data": data, "dataset": dataset, - "fields": fields, "filter_form": filter_form, "max_export_rows": settings.CSV_EXPORT_MAX_ROWS, "query_dict": querystring, From 8bbcf07f50f98ae0d0c6b4b8e055d07e29741fb1 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Fri, 2 Oct 2020 17:18:23 -0300 Subject: [PATCH 10/15] =?UTF-8?q?Retorna=20status=20code=20400=20no=20caso?= =?UTF-8?q?=20de=20erro=20de=20formul=C3=A1rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/views.py b/core/views.py index 67b2064e..67caf3a8 100644 --- a/core/views.py +++ b/core/views.py @@ -207,7 +207,11 @@ def dataset_detail(request, slug, tablename=""): "total_count": all_data.count(), "version": version, } - return render(request, "core/dataset-detail.html", context) + + status = 200 + if filter_form.errors: + status = 400 + return render(request, "core/dataset-detail.html", context, status=status) def dataset_suggestion(request): From d3997a91cc6f6a040f183bb48a72223eb4f2f432 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Fri, 2 Oct 2020 17:22:39 -0300 Subject: [PATCH 11/15] =?UTF-8?q?Remove=20templatefilter=20que=20n=C3=A3o?= =?UTF-8?q?=20=C3=A9=20mais=20utilizada?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/templatetags/utils.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/core/templatetags/utils.py b/core/templatetags/utils.py index 51ed5e36..7bbaa0bc 100644 --- a/core/templatetags/utils.py +++ b/core/templatetags/utils.py @@ -25,11 +25,6 @@ def getattribute(obj, field): return _getattr(obj, field, should_obfuscate=True) -@register.filter(name="getplainattribute") -def getplainattribute(obj, field): - return _getattr(obj, field, should_obfuscate=False) - - @register.filter(name="render") def render(template_text, obj): template_text = "{% load utils %}" + template_text # inception From 3fe930ad0a9f6bbd7a821a1d26cd0e3502ba2651 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Fri, 2 Oct 2020 17:24:37 -0300 Subject: [PATCH 12/15] =?UTF-8?q?Remove=20m=C3=A9todo=20auxiliar=20de=20fi?= =?UTF-8?q?ltragem=20que=20s=C3=B3=20era=20usado=20na=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/models.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/models.py b/core/models.py index 65020a5e..41540fb4 100644 --- a/core/models.py +++ b/core/models.py @@ -113,10 +113,6 @@ def apply_ordering(self, query): return qs - def filter_by_querystring(self, querystring): - query, search_query, order_by = self.parse_querystring(querystring) - return self.composed_query(query, search_query, order_by) - def parse_querystring(self, querystring): query = querystring.copy() order_by = query.pop("order-by", [""]) From c14c1df402ecb52a5a3b76100a2eb17db720822a Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Fri, 2 Oct 2020 17:27:27 -0300 Subject: [PATCH 13/15] =?UTF-8?q?Remove=20m=C3=A9todo=20para=20parsear=20q?= =?UTF-8?q?uerystring=20do=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/views.py | 3 ++- core/filters.py | 9 +++++++++ core/models.py | 8 -------- core/views.py | 3 ++- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/api/views.py b/api/views.py index 26acb51b..f17d525a 100644 --- a/api/views.py +++ b/api/views.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from api.serializers import DatasetDetailSerializer, DatasetSerializer, GenericSerializer +from core.filters import parse_querystring from core.forms import get_table_dynamic_form from core.models import Dataset, Table from core.templatetags.utils import obfuscate @@ -50,7 +51,7 @@ def get_queryset(self): del querystring[pagination_key] Model = self.get_model_class() - query, search_query, order_by = Model.objects.parse_querystring(querystring) + query, search_query, order_by = parse_querystring(querystring) DynamicForm = get_table_dynamic_form(self.get_table()) filter_form = DynamicForm(data=query) diff --git a/core/filters.py b/core/filters.py index d76787a0..300af14b 100644 --- a/core/filters.py +++ b/core/filters.py @@ -8,6 +8,15 @@ def clean_value(key, value): return key, value +def parse_querystring(querystring): + query = querystring.copy() + order_by = query.pop("order-by", [""]) + order_by = [field.strip().lower() for field in order_by[0].split(",") if field.strip()] + search_query = query.pop("search", [""])[0] + query = {key: value for key, value in query.items() if value} + return query, search_query, order_by + + class DynamicModelFilterProcessor: def __init__(self, filtering: dict, allowed_filters: list): self.filtering = filtering diff --git a/core/models.py b/core/models.py index 41540fb4..4564f090 100644 --- a/core/models.py +++ b/core/models.py @@ -113,14 +113,6 @@ def apply_ordering(self, query): return qs - def parse_querystring(self, querystring): - query = querystring.copy() - order_by = query.pop("order-by", [""]) - order_by = [field.strip().lower() for field in order_by[0].split(",") if field.strip()] - search_query = query.pop("search", [""])[0] - query = {key: value for key, value in query.items() if value} - return query, search_query, order_by - def composed_query(self, filter_query=None, search_query=None, order_by=None): qs = self if search_query: diff --git a/core/views.py b/core/views.py index 67caf3a8..49b00c08 100644 --- a/core/views.py +++ b/core/views.py @@ -10,6 +10,7 @@ from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse +from core.filters import parse_querystring from core.forms import ContactForm, DatasetSearchForm, get_table_dynamic_form from core.middlewares import disable_non_logged_user_cache from core.models import Dataset, Table @@ -148,7 +149,7 @@ def dataset_detail(request, slug, tablename=""): version = dataset.version_set.order_by("-order").first() TableModel = table.get_model() - query, search_query, order_by = TableModel.objects.parse_querystring(querystring) + query, search_query, order_by = parse_querystring(querystring) DynamicForm = get_table_dynamic_form(table) filter_form = DynamicForm(data=query) From 92db67bc9674f1cf44d56bc8050497b03d046cf3 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Fri, 2 Oct 2020 18:04:25 -0300 Subject: [PATCH 14/15] =?UTF-8?q?Cria=20fun=C3=A7=C3=A3o=20auxiliar=20para?= =?UTF-8?q?=20facilitar=20a=20recupera=C3=A7=C3=A3o=20do=20campo=20da=20ta?= =?UTF-8?q?bela?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/forms.py | 2 +- core/models.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/forms.py b/core/forms.py index 8655f38d..f8233801 100644 --- a/core/forms.py +++ b/core/forms.py @@ -108,7 +108,7 @@ class DatasetSearchForm(forms.Form): def get_table_dynamic_form(table): def config_dynamic_filter(model_field): - dynamic_field = [f for f in table.fields if f.name == model_field.name][0] + dynamic_field = table.get_field(model_field.name) kwargs = {"required": False, "label": dynamic_field.title} field_factory = model_field.formfield diff --git a/core/models.py b/core/models.py index 4564f090..72a91d80 100644 --- a/core/models.py +++ b/core/models.py @@ -365,6 +365,9 @@ def get_dynamic_model_mixins(self): custom_mixins = [] if not self.dynamic_table_config else self.dynamic_table_config.get_model_mixins() return custom_mixins + mixins + def get_field(self, name): + return self.fields.get(name=name) + def get_model(self, cache=True, data_table=None): # TODO: the current dynamic model registry is handled by Brasil.IO's # code but it needs to be delegated to dynamic_models. From 5dcd56f5944edaaabc6ca99496f069a3b313a414 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Fri, 2 Oct 2020 18:04:40 -0300 Subject: [PATCH 15/15] =?UTF-8?q?Cria=20testes=20unit=C3=A1rios=20para=20f?= =?UTF-8?q?orm=20constru=C3=ADdo=20dinamicamente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/forms.py | 4 +-- core/tests/test_forms.py | 76 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 core/tests/test_forms.py diff --git a/core/forms.py b/core/forms.py index f8233801..58be27df 100644 --- a/core/forms.py +++ b/core/forms.py @@ -106,7 +106,7 @@ class DatasetSearchForm(forms.Form): search = forms.CharField(label="Titulo ou Descrição") -def get_table_dynamic_form(table): +def get_table_dynamic_form(table, cache=True): def config_dynamic_filter(model_field): dynamic_field = table.get_field(model_field.name) kwargs = {"required": False, "label": dynamic_field.title} @@ -118,6 +118,6 @@ def config_dynamic_filter(model_field): return field_factory(**kwargs) - model = table.get_model() + model = table.get_model(cache=cache) fields = model.extra["filtering"] return forms.modelform_factory(model, fields=fields, formfield_callback=config_dynamic_filter) diff --git a/core/tests/test_forms.py b/core/tests/test_forms.py new file mode 100644 index 00000000..4c88ea40 --- /dev/null +++ b/core/tests/test_forms.py @@ -0,0 +1,76 @@ +from django.forms import ChoiceField + +from core.forms import get_table_dynamic_form +from core.tests.utils import BaseTestCaseWithSampleDataset + + +class DynamicModelFormTests(BaseTestCaseWithSampleDataset): + DATASET_SLUG = "sample" + TABLE_NAME = "sample_table" + FIELDS_KWARGS = [ + {"name": "name", "options": {"max_length": 50}, "type": "text", "null": False}, + {"name": "uf", "options": {"max_length": 2}, "type": "text", "null": False}, + {"name": "city", "options": {"max_length": 50}, "type": "text", "null": False}, + ] + + def setUp(self): + self.table.filtering = [] + self.table.save() + + def get_model_field(self, name): + return [f for f in self.TableModel._meta.fields if f.name == name][0] + + def test_table_without_filters_gets_empty_form(self): + DynamicFormClasss = get_table_dynamic_form(self.table, cache=False) + form = DynamicFormClasss() + assert 0 == len(form.fields) + + def test_generate_form_based_in_table_filtering(self): + self.table.filtering = ["uf", "city"] + self.table.save() + + DynamicFormClasss = get_table_dynamic_form(self.table, cache=False) + form = DynamicFormClasss() + + assert "uf" in form.fields + assert "city" in form.fields + assert isinstance(form.fields["uf"], type(self.get_model_field("uf").formfield())) + assert isinstance(form.fields["city"], type(self.get_model_field("city").formfield())) + + def test_filter_form_does_not_invalidate_if_no_data(self): + self.table.filtering = ["uf", "city"] + self.table.save() + + DynamicFormClasss = get_table_dynamic_form(self.table, cache=False) + form = DynamicFormClasss(data={}) + + assert form.is_valid() + assert {"uf": "", "city": ""} == form.cleaned_data + + def test_validate_form_against_field_choices(self): + self.table.filtering = ["uf", "city"] + self.table.save() + uf_field = self.table.get_field("uf") + uf_field.has_choices = True + uf_field.choices = {"data": ["RJ", "SP", "MG"]} + uf_field.save() + city_field = self.table.get_field("city") + city_field.has_choices = True + city_field.choices = {"data": ["Rio de Janeiro", "São Pauyo", "Belo Horizonte"]} + city_field.save() + + # valid form + DynamicFormClasss = get_table_dynamic_form(self.table, cache=False) + form = DynamicFormClasss(data={"uf": "RJ", "city": "Rio de Janeiro"}) + assert isinstance(form.fields["uf"], ChoiceField) + assert isinstance(form.fields["city"], ChoiceField) + assert form.is_valid() + assert {"uf": "RJ", "city": "Rio de Janeiro"} == form.cleaned_data + + # invalid form + DynamicFormClasss = get_table_dynamic_form(self.table, cache=False) + form = DynamicFormClasss(data={"uf": "XXX", "city": "Rio de Janeiro"}) + assert not form.is_valid() + assert "uf" in form.errors + assert "city" not in form.errors + assert {"city": "Rio de Janeiro"} == form.cleaned_data