Skip to content

Commit

Permalink
feat: import export support (#67)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasvinclav authored May 12, 2023
1 parent 3d4cf3b commit 576d428
Show file tree
Hide file tree
Showing 25 changed files with 437 additions and 19 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Unfold is a new theme for Django Admin incorporating some most common practises
- [For global, row and detail action](#for-global-row-and-detail-action)
- [Action examples](#action-examples)
- [Filters](#filters)
- [Third party packages](#third-party-packages)
- [django-import-export](#django-import-export)
- [User Admin Form](#user-admin-form)
- [Adding Custom Styles and Scripts](#adding-custom-styles-and-scripts)
- [Project Level Tailwind Stylesheet](#project-level-tailwind-stylesheet)
Expand All @@ -52,6 +54,7 @@ INSTALLED_APPS = [
"unfold", # before django.contrib.admin
"unfold.contrib.filters", # optional, if special filters are needed
"unfold.contrib.forms", # optional, if special form elements are needed
"unfold.contrib.import_export", # optional, if django-import-export package is used
"django.contrib.admin", # required
]
```
Expand Down Expand Up @@ -485,6 +488,24 @@ class YourModelAdmin(ModelAdmin):
```


## Third party packages


### django-import-export

To get proper visual appearance for django-import-export, two things are needed

1. Add `unfold.contrib.import_export` to `INSTALLED_APPS` at the begging of the file. This action will override all templates coming from the plugin.
2. Change `import_form_class` and `export_form_class` in ModelAdmin which is inheriting from `ImportExportModelAdmin`. This chunk of code is responsible for adding proper styling to form elements.

```python
from unfold.admin import ModelAdmin
from unfold.contrib.import_export.forms import ExportForm, ImportForm

class ExampleAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
```

## User Admin Form

Expand Down
Empty file.
6 changes: 6 additions & 0 deletions src/unfold/contrib/import_export/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class ImportExportConfig(AppConfig):
name = "unfold.contrib.import_export"
label = "importexport"
16 changes: 16 additions & 0 deletions src/unfold/contrib/import_export/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from import_export.forms import ExportForm as BaseExportForm
from import_export.forms import ImportForm as BaseImportForm
from unfold.widgets import SELECT_CLASSES, UnfoldAdminFileFieldWidget


class ImportForm(BaseImportForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["import_file"].widget = UnfoldAdminFileFieldWidget()
self.fields["input_format"].widget.attrs["class"] = " ".join(SELECT_CLASSES)


class ExportForm(BaseExportForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["file_format"].widget.attrs["class"] = " ".join(SELECT_CLASSES)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% extends "admin/base_site.html" %}
{% load admin_modify admin_urls i18n static %}

{% block extrastyle %}
{{ block.super }}

<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />
{% endblock %}

{% block bodyclass %}
{{ block.super }} {{ opts.app_label }}-{{ opts.object_name.lower }} change-form
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% load admin_urls i18n %}

{% if has_export_permission %}
<li class="border-r flex-grow text-center last:border-0 dark:border-gray-700">
<a href="{% url opts|admin_urlname:'export' %}{{ cl.get_query_string }}" class="block px-4 py-2 text-gray-500 whitespace-nowrap hover:text-gray-700 dark:text-gray-400 hover:dark:text-gray-200">
{% trans "Export" %}
</a>
</li>
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{% extends "admin/import_export/change_list.html" %}

{% block actions-items %}
{% include "admin/import_export/change_list_import_item.html" %}
{% include "admin/import_export/change_list_export_item.html" %}
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% load admin_urls i18n %}

{% if has_import_permission %}
<li class="border-r flex-grow text-center last:border-0 dark:border-gray-700">
<a href="{% url opts|admin_urlname:"import" %}" class="block px-4 py-2 text-gray-500 whitespace-nowrap hover:text-gray-700 dark:text-gray-400 hover:dark:text-gray-200">
{% trans "Import" %}
</a>
</li>
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{% extends "admin/import_export/base.html" %}

{% load admin_urls i18n import_export_tags %}

{% block extrahead %}
{{ block.super }}
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>

{{ form.media }}
{% endblock %}

{% block breadcrumbs %}
<div class="px-4 lg:px-12">
<div class="container mb-6 mx-auto -my-3 lg:mb-12">
<ul class="flex">
{% url 'admin:index' as link %}
{% trans 'Home' as name %}
{% include 'unfold/helpers/breadcrumb_item.html' with link=link name=name %}

{% url 'admin:app_list' app_label=opts.app_label as link %}
{% include 'unfold/helpers/breadcrumb_item.html' with link=link name=opts.app_config.verbose_name %}

{% url opts|admin_urlname:'changelist' as link %}
{% include 'unfold/helpers/breadcrumb_item.html' with link=link name=opts.verbose_name_plural|capfirst %}

{% include 'unfold/helpers/breadcrumb_item.html' with link='' name=cl.opts.verbose_name_plural|capfirst %}

{% trans 'Export' as name %}
{% include 'unfold/helpers/breadcrumb_item.html' with link='' name=name %}
</ul>
</div>
</div>
{% endblock %}

{% block content %}
<form action="" method="POST">
{% csrf_token %}

<fieldset class="border border-gray-200 mb-8 rounded-md pt-3 px-3 shadow-sm dark:border-gray-800">
{% include "unfold/helpers/field.html" with field=form.file_format %}
</fieldset>

<button type="submit" class="bg-primary-600 border border-transparent font-medium px-3 py-2 rounded-md text-sm text-white">
{% translate 'Submit' %}
</button>
</form>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{% extends "admin/import_export/base.html" %}

{% load admin_urls i18n import_export_tags static %}

{% block extrastyle %}
{{ block.super }}

<link rel="stylesheet" type="text/css" href="{% static "import_export/import.css" %}" />
{% endblock %}

{% block extrahead %}
{{ block.super }}

<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>

{% if confirm_form %}
{{ confirm_form.media }}
{% else %}
{{ form.media }}
{% endif %}
{% endblock %}

{% block breadcrumbs %}
<div class="px-4 lg:px-12">
<div class="container mb-6 mx-auto -my-3 lg:mb-12">
<ul class="flex">
{% url 'admin:index' as link %}
{% trans 'Home' as name %}
{% include 'unfold/helpers/breadcrumb_item.html' with link=link name=name %}

{% url 'admin:app_list' app_label=opts.app_label as link %}
{% include 'unfold/helpers/breadcrumb_item.html' with link=link name=opts.app_config.verbose_name %}

{% url opts|admin_urlname:'changelist' as link %}
{% include 'unfold/helpers/breadcrumb_item.html' with link=link name=opts.verbose_name_plural|capfirst %}

{% include 'unfold/helpers/breadcrumb_item.html' with link='' name=cl.opts.verbose_name_plural|capfirst %}

{% trans 'Import' as name %}
{% include 'unfold/helpers/breadcrumb_item.html' with link='' name=name %}
</ul>
</div>
</div>
{% endblock %}

{% block content %}
{% if confirm_form %}
{% include "admin/import_export/import_confirm.html" %}
{% else %}
{% include "admin/import_export/import_form.html" %}
{% endif %}

{% if result %}
{% if result.has_errors %}
{% include "admin/import_export/import_errors.html" %}
{% elif result.has_validation_errors %}
{% include "admin/import_export/import_validation.html" %}
{% else %}
{% include "admin/import_export/import_preview.html" %}
{% endif %}
{% endif %}
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% load admin_urls i18n import_export_tags %}

{% block confirm_import_form %}
<form action="{% url opts|admin_urlname:"process_import" %}" method="post" class="border border-gray-200 mb-8 rounded-md shadow-sm dark:border-gray-800">
{% csrf_token %}

{{ confirm_form.as_p }}

<p class="font-medium p-4 text-sm text-gray-700 dark:text-gray-200">
{% trans "Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'" %}
</p>

<div class="border-t border-gray-200 flex flex-row px-4 py-3 dark:border-gray-800">
<button type="submit" class="bg-primary-600 border border-transparent font-medium ml-auto px-3 py-2 rounded-md text-sm text-white">
{% translate 'Confirm import' %}
</button>
</div>
</form>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{% load i18n %}

{% block errors %}
<hr class="dark:border-gray-800 my-8">

<ul>
{% for error in result.base_errors %}
<li>
{{ error.error }}

<div class="traceback">{{ error.traceback|linebreaks }}</div></li>
{% endfor %}

{% for line, errors in result.row_errors %}
{% for error in errors %}
<li x-data="{ open: false }">
<div class="bg-red-50 border border-red-200 cursor-pointer mb-4 rounded shadow-sm text-red-500 text-sm dark:bg-red-500/20 dark:border-red-500/20" x-on:click="open = ! open">
<div class="border-b border-red-200 flex flex-row font-medium py-3 px-4 dark:border-red-500/20">
{% trans "Line number" %}: {{ line }} - {{ error.error }}
</div>

<div class="px-4 py-3">
<code>
{{ error.row.values|join:", " }}
</code>
</div>
</div>

<div class="block border leading-relaxed rounded p-4 text-sm traceback dark:border-gray-800 dark:text-gray-400 " x-show="open">
{{ error.traceback|linebreaks }}
</div>
</li>
{% endfor %}
{% endfor %}
</ul>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{% load i18n %}

{% block import_form %}
<form action="" method="post" enctype="multipart/form-data">
{% csrf_token %}

<p class="bg-blue-50 mb-8 text-blue-500 px-3 py-3 rounded-md text-sm dark:bg-blue-500/20 dark:border-blue-500/10">
{% trans "This importer will import the following fields: " %}

{% if fields_list|length <= 1 %}
<span class="font-medium">
{{ 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 %}
</p>

<fieldset class="border border-gray-200 mb-8 rounded-md pt-3 px-3 shadow-sm dark:border-gray-800">
{% include "unfold/helpers/field.html" with field=form.import_file %}

{% include "unfold/helpers/field.html" with field=form.input_format %}
</fieldset>


<button type="submit" class="bg-primary-600 border border-transparent font-medium px-3 py-2 rounded-md text-sm text-white">
{% translate 'Submit' %}
</button>
</form>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{% load admin_urls i18n import_export_tags unfold %}

{% block preview %}
<table class="border-gray-200 border-spacing-none border-separate text-gray-700 w-full dark:text-gray-400 lg:border lg:rounded-md lg:shadow-sm lg:dark:border-gray-800">
<thead class="hidden lg:table-header-group">
<tr>
<th class="align-middle capitalize font-medium px-3 py-2 text-left text-gray-400 text-sm"></th>

{% for field in result.diff_headers %}
<th class="align-middle capitalize font-medium px-3 py-2 text-left text-gray-400 text-sm">
{{ field }}
</th>
{% endfor %}
</tr>
</thead>

<tbody>
{% for row in result.valid_rows %}
<tr class="{{ row.import_type }} {% cycle '' 'bg-gray-50 dark:bg-white/[.02]' %} block border mb-3 rounded-md shadow-sm lg:table-row lg:border-none lg:mb-0 lg:shadow-none dark:border-gray-800">
<td class="align-middle flex border-t border-gray-200 font-normal px-3 py-2 text-left text-sm before:block before:capitalize before:content-[attr(data-label)] before:mr-auto before:text-gray-500 dark:before:text-gray-400 lg:before:hidden lg:py-3 lg:table-cell dark:border-gray-800">
{% if row.import_type == 'new' %}
{% trans "New" %}
{% elif row.import_type == 'skip' %}
{% trans "Skipped" %}
{% elif row.import_type == 'delete' %}
{% trans "Delete" %}
{% elif row.import_type == 'update' %}
{% trans "Update" %}
{% endif %}
</td>

{% for field in row.diff %}
<td data-label="{{ result.diff_headers|index:forloop.counter0 }}" class="align-middle flex border-t border-gray-200 font-normal px-3 py-2 text-left text-sm before:block before:capitalize before:content-[attr(data-label)] before:mr-auto before:text-gray-500 dark:before:text-gray-400 lg:before:hidden lg:py-3 lg:table-cell dark:border-gray-800">
{{ field }}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
Loading

0 comments on commit 576d428

Please sign in to comment.