Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use Django-import-export ImportForm and ConfirmImportForm outside admin

I want to use django-import-export's forms to implement the import feature for regular users, so it needs to be outside the admin section.

So far, all the implementations I have found are about
a) extending the feature inside the admin or
b) reimplementing the default views outside the admin.

But since the default forms work perfectly (specially the ConfirmImportForm that shows a diff between old records and new records attempting to be imported) I would like to subclass them as part of my project (outside admin) without reimplementing the whole view's logic, if such thing is even possible.

So far I tried (foolishly, I'm afraid) to subclass the import_export.admin.ImportMixin as two separated class views to import a model Period with resource PeriodResource. The methods import_action and process_import were reimplemented (basically copy and paste the same code and eliminating any code using self.site_admin) as View.get() and View.post():

# staging/views.py
from django.http import HttpResponseRedirect, HttpResponse
from django.urls import reverse
from django.views import generic
from django.template.response import TemplateResponse
from django.utils.translation import gettext_lazy as _
from django.utils.encoding import force_text

from import_export.forms import ConfirmImportForm
from import_export.signals import post_import
import import_export.admin

from .models import Period
from .resources import PeriodResource

class PeriodImportView(import_export.admin.ImportMixin, generic.View):
    """
    Subclassing of ImportMixin as a generic View implementing ImportForm
    """
    #: template for import view
    import_template_name = 'period/import.html'
    #: resource class
    resource_class = PeriodResource
    #: model to be imported
    model = Period

    def get_confirm_import_form(self):
        '''
        Get the form type used to display the results and confirm the upload.
        '''
        return ConfirmImportForm

    def get(self, request, *args, **kwargs):
        """
        Overriding the GET part of ImportMixin.import_action method to be used without site_admin
        """

        resource = self.get_import_resource_class()(**self.get_import_resource_kwargs(request, *args, **kwargs))

        context = self.get_import_context_data()

        import_formats = self.get_import_formats()
        form_type = self.get_import_form()
        form = form_type(import_formats,
                         request.POST or None,
                         request.FILES or None)

         # context.update(self.admin_site.each_context(request))

        context['title'] = _("Import")
        context['form'] = form
        context['opts'] = self.model._meta
        context['fields'] = [f.column_name for f in resource.get_user_visible_fields()]

        # request.current_app = self.admin_site.name
        return TemplateResponse(request, [self.import_template_name],
                                context)


    def post(self, request, *args, **kwargs):
        """
        Overriding the POST part of ImportMixin.import_action method to be used without site_admin
        """

        resource = self.get_import_resource_class()(**self.get_import_resource_kwargs(request, *args, **kwargs))

        context = self.get_import_context_data()

        import_formats = self.get_import_formats()
        form_type = self.get_import_form()
        form = form_type(import_formats,
                         request.POST or None,
                         request.FILES or None)

        if request.POST and form.is_valid():
            input_format = import_formats[
                int(form.cleaned_data['input_format'])
            ]()
            import_file = form.cleaned_data['import_file']
            # first always write the uploaded file to disk as it may be a
            # memory file or else based on settings upload handlers
            tmp_storage = self.write_to_tmp_storage(import_file, input_format)

            # then read the file, using the proper format-specific mode
            # warning, big files may exceed memory
            try:
                data = tmp_storage.read(input_format.get_read_mode())
                if not input_format.is_binary() and self.from_encoding:
                    data = force_text(data, self.from_encoding)
                dataset = input_format.create_dataset(data)
            except UnicodeDecodeError as ex1:
                return HttpResponse(_(u"<h1>Imported file has a wrong encoding: %s</h1>" % ex1))
            except Exception as ex2:
                return HttpResponse(_(u"<h1>%s encountered while trying to read file: %s</h1>" % (type(ex2).__name__, import_file.name)))
            result = resource.import_data(dataset, dry_run=True,
                                          raise_errors=False,
                                          file_name=import_file.name,
                                          user=request.user)

            context['result'] = result

            if not result.has_errors() and not result.has_validation_errors():
                context['confirm_form'] = self.get_confirm_import_form()(initial={
                    'import_file_name': tmp_storage.name,
                    'original_file_name': import_file.name,
                    'input_format': form.cleaned_data['input_format'],
                })

        # context.update(self.admin_site.each_context(request))

        context['title'] = _("Import")
        context['form'] = form
        context['opts'] = self.model._meta
        context['fields'] = [f.column_name for f in resource.get_user_visible_fields()]

        # request.current_app = self.admin_site.name
        return TemplateResponse(request, [self.import_template_name],
                                context)

class PeriodConfirmImportView(import_export.admin.ImportMixin, generic.View):
    """
    Subclassing of ImportMixin as a generic View implementing ConfirmImportForm
    """
    #: template for import view
    import_template_name = 'period/import.html'
    #: resource class
    resource_class = PeriodResource
    #: model to be imported
    model = Period

    def post(self, request, *args, **kwargs):
        """
        Perform the actual import action (after the user has confirmed the import)
        """
        # if not self.has_import_permission(request):
        #     raise PermissionDenied

        confirm_form = ConfirmImportForm(request.POST)
        if confirm_form.is_valid():
            import_formats = self.get_import_formats()
            input_format = import_formats[
                int(confirm_form.cleaned_data['input_format'])
            ]()
            tmp_storage = self.get_tmp_storage_class()(name=confirm_form.cleaned_data['import_file_name'])
            data = tmp_storage.read(input_format.get_read_mode())
            if not input_format.is_binary() and self.from_encoding:
                data = force_text(data, self.from_encoding)
            dataset = input_format.create_dataset(data)

            result = self.process_dataset(dataset, confirm_form, request, *args, **kwargs)

            tmp_storage.remove()

            self.generate_log_entries(result, request)
            self.add_success_message(result, request)
            post_import.send(sender=None, model=self.model)

            url = reverse('staging:index')
            return HttpResponseRedirect(url)

and then just show the forms in the template:

# staging/templates/period/import.html
{% if confirm_form %}
<form action="{% url 'staging:confirm_import_period' %}" method="POST">
  {% csrf_token %}
  {{ confirm_form.as_p }}
  <p>
    {% trans "Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'" %}
  </p>
  <div class="submit-row">
    <input type="submit" class="default" name="confirm" value="{% trans "Confirm import" %}">
  </div>
</form>
{% else %}
<form action="" method="post" enctype="multipart/form-data">
  {% csrf_token %}

  <p>
    {% trans "This importer will import the following fields: " %}
    <code>{{ fields|join:", " }}</code>
  </p>

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

        {{ field.label_tag }}

        {{ field }}

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

  <div class="submit-row">
    <input type="submit" class="default" value="{% trans "Submit" %}">
  </div>
</form>
{% endif %}

and my urls.py looks like this:

# staging/urls.py
from django.urls import path
from .views import PeriodIndexView, PeriodImportView, PeriodConfirmImportView

app_name = 'staging'

urlpatterns = [
    path('period/', PeriodIndexView.as_view(), name='index'),
    path('period/import/', PeriodImportView.as_view(), name='import_period'),
    path('period/confirm_import/', PeriodConfirmImportView.as_view(), name='confirm_import_period'),
]

So far it works as intended, but this approach is so tightly coupled to the internal implementation of ImportMixin that I am afraid it will not survive any version upgrade of django-import-export.

Is there any way to achieve that without reimplementing the whole import_action and process_import methods?

like image 907
Andrés Meza-Escallón Avatar asked Dec 28 '25 15:12

Andrés Meza-Escallón


1 Answers

After a lot of try and error I gave up on avoiding the re-implementation of methods import_action and process_import from import_export.admin.ImportMixin. Instead, I created my own mixins subclassing import_export.admin.ImportMixin and django.views.generic.View and removing all references to self.site_admin from methods import_action and process_import into equivalent methods get() and post().

# staging/views.py
    from django.http import HttpResponseRedirect, HttpResponse
    from django.urls import reverse
    from django.views import generic
    from django.template.response import TemplateResponse
    from django.utils.translation import gettext_lazy as _
    from django.utils.encoding import force_text

    from import_export.forms import ConfirmImportForm
    from import_export.signals import post_import
    import import_export.admin

    from .models import Period
    from .resources import PeriodResource

    class ImportView(import_export.admin.ImportMixin, generic.View):
        """
        Subclassing of ImportMixin as a generic View implementing ImportForm
        """
        #: template for import view
        import_template_name = 'import.html'
        #: resource class
        resource_class = None
        #: model to be imported
        model = None

        def get_confirm_import_form(self):
            '''
            Get the form type used to display the results and confirm the upload.
            '''
            return ConfirmImportForm

        def get(self, request, *args, **kwargs):
            """
            Overriding the GET part of ImportMixin.import_action method to be used without site_admin
            """
            return self.post(request, *args, **kwargs)


        def post(self, request, *args, **kwargs):
            """
            Overriding the POST part of ImportMixin.import_action method to be used without site_admin
            """

            resource = self.get_import_resource_class()(**self.get_import_resource_kwargs(request,
                                                                                          *args,
                                                                                          **kwargs))

            context = self.get_import_context_data()

            import_formats = self.get_import_formats()
            form = self.get_import_form()(import_formats, request.POST or None, request.FILES or None)

            if request.POST and form.is_valid():
                input_format = import_formats[
                    int(form.cleaned_data['input_format'])
                ]()
                import_file = form.cleaned_data['import_file']
                # first always write the uploaded file to disk as it may be a
                # memory file or else based on settings upload handlers
                tmp_storage = self.write_to_tmp_storage(import_file, input_format)

                # then read the file, using the proper format-specific mode
                # warning, big files may exceed memory
                try:
                    data = tmp_storage.read(input_format.get_read_mode())
                    if not input_format.is_binary() and self.from_encoding:
                        data = force_text(data, self.from_encoding)
                    dataset = input_format.create_dataset(data)
                except UnicodeDecodeError as ex1:
                    return HttpResponse(_(u"<h1>Imported file has a wrong encoding: %s</h1>" % ex1))
                except Exception as ex2:
                    return HttpResponse(_(u"<h1>%s encountered while trying to read file: %s</h1>" % (type(ex2).__name__, import_file.name)))
                result = resource.import_data(dataset, dry_run=True,
                                              raise_errors=False,
                                              file_name=import_file.name,
                                              user=request.user)

                context['result'] = result

                if not result.has_errors() and not result.has_validation_errors():
                    context['confirm_form'] = self.get_confirm_import_form()(initial={
                        'import_file_name': tmp_storage.name,
                        'original_file_name': import_file.name,
                        'input_format': form.cleaned_data['input_format'],
                    })

            # context.update(self.admin_site.each_context(request))

            context['title'] = _("Import " + self.get_model_info()[1])
            context['form'] = form
            context['opts'] = self.model._meta
            context['fields'] = [f.column_name for f in resource.get_user_visible_fields()]

            # request.current_app = self.admin_site.name
            return TemplateResponse(request, [self.import_template_name],
                                    context)

    class ConfirmImportView(import_export.admin.ImportMixin, generic.View):
        """
        Subclassing of ImportMixin as a generic View implementing ConfirmImportForm
        """
        #: template for import view
        import_template_name = 'import.html'
        #: resource class
        resource_class = None
        #: model to be imported
        model = None

        def get_confirm_import_form(self):
            '''
            Get the form type used to display the results and confirm the upload.
            '''
            return ConfirmImportForm

        def post(self, request, *args, **kwargs):
            """
            Perform the actual import action (after the user has confirmed the import)
            """

            confirm_form = self.get_confirm_import_form()(request.POST)
            if confirm_form.is_valid():
                import_formats = self.get_import_formats()
                input_format = import_formats[
                    int(confirm_form.cleaned_data['input_format'])
                ]()
                tmp_storage = self.get_tmp_storage_class()(name=confirm_form.cleaned_data['import_file_name'])
                data = tmp_storage.read(input_format.get_read_mode())
                if not input_format.is_binary() and self.from_encoding:
                    data = force_text(data, self.from_encoding)
                dataset = input_format.create_dataset(data)

                result = self.process_dataset(dataset, confirm_form, request, *args, **kwargs)

                tmp_storage.remove()

                self.generate_log_entries(result, request)
                self.add_success_message(result, request)
                post_import.send(sender=None, model=self.model)

                url = reverse('{}:{}_index'.format(self.get_model_info()[0], self.get_model_info()[1]))
                return HttpResponseRedirect(url)

So now from my custom mixins ImportView and ConfirmImportView y can subclass the specific classes to import my specific models just setting the model and resource_class attributes, which was sort of what I was looking for.

    class PeriodImportView(ImportView):
        """
        ImportView specific for model Period and resource PeriodResource
        """
        #: resource class
        resource_class = PeriodResource
        #: model to be imported
        model = Period

    class PeriodConfirmImportView(ConfirmImportView):
        """
        ConfirmImportView specific for model Period and resource PeriodResource
        """
        #: resource class
        resource_class = PeriodResource
        #: model to be imported
        model = Period
like image 176
Andrés Meza-Escallón Avatar answered Dec 30 '25 04:12

Andrés Meza-Escallón



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!