Skip to content

Commit

Permalink
Merge b73130f into a78396b
Browse files Browse the repository at this point in the history
  • Loading branch information
cocorocho committed Jan 9, 2024
2 parents a78396b + b73130f commit dc13c1f
Show file tree
Hide file tree
Showing 12 changed files with 410 additions and 56 deletions.
1 change: 1 addition & 0 deletions docs/changelog.rst
Expand Up @@ -8,6 +8,7 @@ Changelog
4.0.0-beta.3 (unreleased)
-------------------------

- Added feature: selectable fields for admin export view (#1538)
- Fix issue where declared Resource fields not defined in ``fields`` are still imported (#1702)
- Added customizable ``MediaStorage`` (#1708)
- Relocated admin integration section from advanced_usage.rst into new file (#1713)
Expand Down
7 changes: 4 additions & 3 deletions import_export/admin.py
Expand Up @@ -18,7 +18,7 @@
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_POST

from .forms import ConfirmImportForm, ExportForm, ImportForm
from .forms import ConfirmImportForm, ImportForm, SelectableFieldsExportForm
from .mixins import BaseExportMixin, BaseImportMixin
from .results import RowResult
from .signals import post_export, post_import
Expand Down Expand Up @@ -596,8 +596,9 @@ class ExportMixin(BaseExportMixin, ImportExportMixinBase):
export_template_name = "admin/import_export/export.html"
#: export data encoding
to_encoding = None
#: form class to use for the initial import step
export_form_class = ExportForm
#: form class to use for the initial export step
#: Use `ExportForm` if you would like to disable selectable fields feature
export_form_class = SelectableFieldsExportForm

def get_urls(self):
urls = super().get_urls()
Expand Down
156 changes: 156 additions & 0 deletions import_export/forms.py
@@ -1,9 +1,13 @@
import os.path
from copy import deepcopy
from typing import Iterable

from django import forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _

from .resources import ModelResource


class ImportExportFormBase(forms.Form):
resource = forms.ChoiceField(
Expand Down Expand Up @@ -91,3 +95,155 @@ class ExportForm(ImportExportFormBase):
export_items = forms.MultipleChoiceField(
widget=forms.MultipleHiddenInput(), required=False
)


class SelectableFieldsExportForm(ExportForm):
def __init__(self, formats, resources, *args, **kwargs):
super().__init__(formats, resources, *args, **kwargs)
self._init_selectable_fields(resources)

def _init_selectable_fields(self, resources: Iterable[ModelResource]) -> None:
"""
Create `BooleanField(s)` for resource fields
"""
self.resources = resources
self.is_selectable_fields_form = True
self.resource_fields = {resource.__name__: list() for resource in resources}

for resource in resources:
self._create_boolean_fields(resource)

def _create_boolean_fields(self, resource: ModelResource) -> None:
# Initiate resource to get ordered export fields
fields = resource().get_export_order()

boolean_fields = [] # will be used for ordering the fields

for field in fields:
field_name = self.create_boolean_field_name(resource, field)
boolean_field = forms.BooleanField(
label=field.replace("_", " ").title(),
initial=True,
required=False,
)
# These attributes will be used for rendering in template
boolean_field.is_selectable_field = True
boolean_field.resource_name = resource.__name__

self.fields[field_name] = boolean_field
boolean_fields.append(field_name)

self.order_fields(boolean_fields) # Order fields by boolean fields

self.resource_fields[resource.__name__] = boolean_fields

@staticmethod
def create_boolean_field_name(resource: ModelResource, field_name: str) -> str:
"""
Create field name by combining `resource_name` + `field_name` to prevent
confliction between resource fields with same name
Example:
BookResource + name -> bookresource_name
BookResourceWithNames + name -> bookresourcewithnames_name
"""
return resource.__name__.lower() + "_" + field_name

def full_clean(self) -> None:
return super().full_clean()

def clean(self):
selected_resource = self.get_selected_resource()

if selected_resource:
# Remove fields for not selected resources
self._remove_unselected_resource_fields(selected_resource)
# Normalize resource field names
self._normalize_resource_fields(selected_resource)
# Validate at least one field is selected for selected resource
self._validate_any_field_selected(selected_resource)

return self.cleaned_data

def _remove_unselected_resource_fields(
self, selected_resource: ModelResource
) -> None:
"""
Remove boolean fields except the fields for selected resource
"""
_cleaned_data = deepcopy(self.cleaned_data)

for resource_name, fields in self.resource_fields.items():
if selected_resource.__name__ == resource_name:
# Skip selected resource
continue

for field in fields:
del _cleaned_data[field]

self.cleaned_data = _cleaned_data

def get_selected_resource(self):
"""
Get selected resource
"""
if not getattr(self, "cleaned_data", None):
raise forms.ValidationError(
_("Form is not validated, call `is_valid` first")
)

# Return selected resource by index
resource_index = 0
if "resource" in self.cleaned_data:
try:
resource_index = int(self.cleaned_data["resource"])
except ValueError:
pass
return self.resources[resource_index]

def _normalize_resource_fields(self, selected_resource: ModelResource) -> dict:
"""
Field names are combination of resource_name + field_name,
normalize field names by removing resource name
"""
selected_resource_name = selected_resource.__name__.lower() + "_"
_cleaned_data = {}
self._selected_resource_fields = []

for k, v in self.cleaned_data.items():
if selected_resource_name in k:
field_name = k.replace(selected_resource_name, "")
_cleaned_data[field_name] = v
if v is True:
# Add to _selected_resource_fields to determine what
# fields were selected for export
self._selected_resource_fields.append(field_name)
continue
_cleaned_data[k] = v

self.cleaned_data = _cleaned_data

def get_selected_resource_export_fields(self):
selected_resource = self.get_selected_resource()
# Initialize resource to use `get_export_order` method
resource_fields = selected_resource().get_export_order()
return [
field
for field, value in self.cleaned_data.items()
if field in resource_fields and value is True
]

def _validate_any_field_selected(self, resource) -> None:
"""
Validate if any field for resource was selected in form data
"""
resource_fields = [field for field in resource().get_export_order()]

if not any([v for k, v in self.cleaned_data.items() if k in resource_fields]):
raise forms.ValidationError(
_("""Select at least 1 field for "%(resource_name)s" to export"""),
code="invalid",
params={
"resource_name": resource.get_display_name(),
},
)
17 changes: 14 additions & 3 deletions import_export/mixins.py
Expand Up @@ -7,7 +7,7 @@
from django.views.generic.edit import FormView

from .formats import base_formats
from .forms import ExportForm
from .forms import SelectableFieldsExportForm
from .resources import modelresource_factory
from .signals import post_export

Expand Down Expand Up @@ -102,12 +102,23 @@ def choose_export_resource_class(self, form):
def get_export_resource_kwargs(self, request, **kwargs):
return self.get_resource_kwargs(request, **kwargs)

def get_export_resource_fields_from_from(self, form):
if isinstance(form, SelectableFieldsExportForm):
export_fields = form.get_selected_resource_export_fields()
if export_fields:
return export_fields

return

def get_data_for_export(self, request, queryset, **kwargs):
export_form = kwargs.get("export_form")
export_class = self.choose_export_resource_class(export_form)
export_resource_kwargs = self.get_export_resource_kwargs(request, **kwargs)
export_fields = self.get_export_resource_fields_from_from(export_form)
cls = export_class(**export_resource_kwargs)
export_data = cls.export(queryset=queryset, **kwargs)
export_data = cls.export(
queryset=queryset, export_fields=export_fields, **kwargs
)
return export_data

def get_export_filename(self, file_format):
Expand All @@ -121,7 +132,7 @@ def get_export_filename(self, file_format):


class ExportViewMixin(BaseExportMixin):
form_class = ExportForm
form_class = SelectableFieldsExportForm

def get_export_data(self, file_format, queryset, *args, **kwargs):
"""
Expand Down
26 changes: 19 additions & 7 deletions import_export/resources.py
Expand Up @@ -1013,13 +1013,24 @@ def export_field(self, field, instance):
def get_export_fields(self):
return [self.fields[f] for f in self.get_export_order()]

def export_resource(self, instance):
return [
self.export_field(field, instance) for field in self.get_export_fields()
]
def export_resource(self, instance, fields=None):
export_fields = self.get_export_fields()

if isinstance(fields, list) and fields:
return [
self.export_field(field, instance)
for field in export_fields
if field.column_name in fields
]

return [self.export_field(field, instance) for field in export_fields]

def get_export_headers(self):
def get_export_headers(self, fields=None):
headers = [force_str(field.column_name) for field in self.get_export_fields()]

if isinstance(fields, list) and fields:
return [f for f in headers if f in fields]

return headers

def get_user_visible_headers(self):
Expand Down Expand Up @@ -1061,11 +1072,12 @@ def export(self, queryset=None, **kwargs):
if queryset is None:
queryset = self.get_queryset()
queryset = self.filter_export(queryset, **kwargs)
headers = self.get_export_headers()
export_fields = kwargs.get("export_fields", None)
headers = self.get_export_headers(fields=export_fields)
dataset = tablib.Dataset(headers=headers)

for obj in self.iter_queryset(queryset):
dataset.append(self.export_resource(obj))
dataset.append(self.export_resource(obj, fields=export_fields))

self.after_export(queryset, dataset, **kwargs)

Expand Down
38 changes: 22 additions & 16 deletions import_export/templates/admin/import_export/export.html
Expand Up @@ -30,28 +30,34 @@

<fieldset class="module aligned">
{% for field in form.visible_fields %}
<div class="form-row">
{{ field.errors }}

{{ field.label_tag }}

{% if field.field.widget.attrs.readonly %}
{{ field.field.value }}
{{ field.as_hidden }}
{% else %}
{{ field }}
{% endif %}

{% if field.field.help_text %}
<p class="help">{{ field.field.help_text|safe }}</p>
{% endif %}
</div>
{% if not field.field.is_selectable_field %}
<div class="form-row">
{{ field.errors }}

{{ field.label_tag }}

{% if field.field.widget.attrs.readonly %}
{{ field.field.value }}
{{ field.as_hidden }}
{% else %}
{{ field }}
{% endif %}

{% if field.field.help_text %}
<p class="help">{{ field.field.help_text|safe }}</p>
{% endif %}
</div>
{% endif %}
{% endfor %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
</fieldset>

<div>
{{ form.non_field_errors }}
</div>

<div class="submit-row">
<input type="submit" class="default" value="{% translate "Submit" %}">
</div>
Expand Down
Expand Up @@ -7,15 +7,19 @@
{% translate "This importer will import the following fields: " %}
{% endif %}

{% if fields_list|length <= 1 %}
<code>{{ fields_list.0.1|join:", " }}</code>
{% if form.is_selectable_fields_form %}
{% include "admin/import_export/selectable_resource_export_fields.html" %}
{% else %}
<dl>
{% for resource, fields in fields_list %}
<dt>{{ resource }}</dt>
<dd><code>{{ fields|join:", " }}</code></dd>
{% endfor %}
</dl>
{% if fields_list|length <= 1 %}
<code>{{ fields_list.0.1|join:", " }}</code>
{% else %}
<dl>
{% for resource, fields in fields_list %}
<dt>{{ resource }}</dt>
<dd><code>{{ fields|join:", " }}</code></dd>
{% endfor %}
</dl>
{% endif %}
{% endif %}
</p>
{% endblock %}
@@ -0,0 +1,19 @@
{% regroup form by field.resource_name as selectable_fields_list %}

{% for resource in selectable_fields_list %}
<div class="form-row">
{% if resource.grouper %}
<fieldset>
<legend>{{ resource.grouper }}</legend>
{% for field in resource.list %}
<div class="selectable-field-row">
{{ field.errors }}

{{ field.label_tag }}
{{ field }}
</div>
{% endfor %}
</fieldset>
{% endif %}
</div>
{% endfor %}

0 comments on commit dc13c1f

Please sign in to comment.