diff options
author | Étienne Loks <etienne.loks@iggdrasil.net> | 2023-09-21 19:29:44 +0200 |
---|---|---|
committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2024-04-16 16:38:32 +0200 |
commit | ee4619364d1751b1c877b3047906439a24aacd36 (patch) | |
tree | 5e8e2b72d0ea36d8e40ec67df0da81ccb14b1b0e | |
parent | 27eac8dc6cf0e96f78edfe4e09845e454fb6d510 (diff) | |
download | Ishtar-ee4619364d1751b1c877b3047906439a24aacd36.tar.bz2 Ishtar-ee4619364d1751b1c877b3047906439a24aacd36.zip |
✨ Imports: built-in CSV viewer
-rw-r--r-- | changelog/en/changelog_2022-06-15.md | 2 | ||||
-rw-r--r-- | changelog/fr/changelog_2023-01-25.md | 2 | ||||
-rw-r--r-- | ishtar_common/models_imports.py | 22 | ||||
-rw-r--r-- | ishtar_common/static/js/ishtar.js | 4 | ||||
-rw-r--r-- | ishtar_common/templates/ishtar/blocks/import_table_buttons_view.html | 18 | ||||
-rw-r--r-- | ishtar_common/templates/ishtar/blocks/view_import_csv.html | 32 | ||||
-rw-r--r-- | ishtar_common/templates/ishtar/blocks/window_nav.html | 2 | ||||
-rw-r--r-- | ishtar_common/templates/ishtar/import_table.html | 146 | ||||
-rw-r--r-- | ishtar_common/urls.py | 5 | ||||
-rw-r--r-- | ishtar_common/views.py | 60 | ||||
-rw-r--r-- | scss/custom.scss | 54 |
11 files changed, 282 insertions, 65 deletions
diff --git a/changelog/en/changelog_2022-06-15.md b/changelog/en/changelog_2022-06-15.md index e178ff7b0..cf881bdaf 100644 --- a/changelog/en/changelog_2022-06-15.md +++ b/changelog/en/changelog_2022-06-15.md @@ -2,8 +2,10 @@ v4.0.XX - 2099-12-31 -------------------- ### Features/improvements ### +- pre-import forms - imports form: reorganisation of field order - import table : + - built-in CSV viewer - automatic progress refresh - reorganization of fields - improved presentation diff --git a/changelog/fr/changelog_2023-01-25.md b/changelog/fr/changelog_2023-01-25.md index 2f23ed826..b5265c4d0 100644 --- a/changelog/fr/changelog_2023-01-25.md +++ b/changelog/fr/changelog_2023-01-25.md @@ -2,8 +2,10 @@ v4.0.XX - 2099-12-31 -------------------- ### Fonctionnalités/améliorations ### +- ajout de formulaire pré-imports - formulaire d'imports: réorganisation de l'ordre des champs - table des imports : + - visualisateur CSV intégré - raffrachissement automatique de l'avancement - réorganisation des champs - amélioration de la présentation diff --git a/ishtar_common/models_imports.py b/ishtar_common/models_imports.py index ca0a38832..fce25cedd 100644 --- a/ishtar_common/models_imports.py +++ b/ishtar_common/models_imports.py @@ -1397,6 +1397,24 @@ class BaseImport(models.Model): class Meta: abstract = True + @classmethod + def query_can_access(cls, user): + """ + Filter the query to check access permissions + :param user: User instance + :return: import query + """ + q = cls.objects + if user.is_superuser: + return q + ishtar_user = models.IshtarUser.objects.get(pk=user.pk) + q = q.filter(user=ishtar_user) + return q + + @property + def group_prefix(self): + return "" + @property def has_pre_import_form(self) -> bool: raise NotImplemented() @@ -1452,6 +1470,10 @@ class ImportGroup(BaseImport): return f"{self.name} ({self.importer_type.name})" @property + def group_prefix(self): + return "group-" + + @property def has_pre_import_form(self) -> bool: return False diff --git a/ishtar_common/static/js/ishtar.js b/ishtar_common/static/js/ishtar.js index 1656cb427..9797c7d4a 100644 --- a/ishtar_common/static/js/ishtar.js +++ b/ishtar_common/static/js/ishtar.js @@ -955,7 +955,7 @@ function toggle_window_menu(){ return false; } -var register_qa_on_sheet = function(){ +var register_qa = function(){ $(".btn-qa").click(function(){ var target = $(this).attr('data-target'); dt_qa_open(target); @@ -1081,6 +1081,8 @@ var dt_qa_open = function (url, modal_id){ } ); $('#' + modal_id).modal("show"); + let table_scroll_height = $(window).height() - 180; + $(".table-scroll table").height(table_scroll_height + "px"); }, error: function() { close_wait(); diff --git a/ishtar_common/templates/ishtar/blocks/import_table_buttons_view.html b/ishtar_common/templates/ishtar/blocks/import_table_buttons_view.html new file mode 100644 index 000000000..061dfe0f4 --- /dev/null +++ b/ishtar_common/templates/ishtar/blocks/import_table_buttons_view.html @@ -0,0 +1,18 @@ +{% load i18n %} +<div class="btn-group btn-group-sm" role="group"> + <span class="btn btn-outline-secondary no-hover"> + <i class="{{logo}}" aria-hidden="true"></i> + {{ file_label }} + </span> + {% if file_type %} + <a class="btn btn-qa btn-secondary" + href='#' + data-target="{% url 'import_display_csv' file_type current_import.group_prefix current_import.pk %}" + onclick="" + title="{% trans 'View' %}"> + <i class="fa fa-eye" aria-hidden="true"></i> + </a>{% endif %} + <a class="btn btn-secondary" href='{{file.url}}' title="{% trans 'Download' %}"> + <i class="fa fa-download" aria-hidden="true"></i> + </a> +</div> diff --git a/ishtar_common/templates/ishtar/blocks/view_import_csv.html b/ishtar_common/templates/ishtar/blocks/view_import_csv.html new file mode 100644 index 000000000..f1b089ce2 --- /dev/null +++ b/ishtar_common/templates/ishtar/blocks/view_import_csv.html @@ -0,0 +1,32 @@ +{% load i18n %} + +<div class="modal-dialog full modal-lg"> + <div class="modal-content"> + <div class="modal-header"> + <h2>{{ title }} | + <i class="{{icon}}" aria-hidden="true"></i> + {{target}} + </h2> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="table-scroll"> + <table class="table table-striped table-bordered"> + <thead> + <tr> + <th> </th> + {% for head in header %}<th>{{head}}</th>{% endfor %} + </tr> + </thead> + <tbody> + {% for line in content %} + <tr> + <td>{{ forloop.counter }}</td> + {% for cell in line %}<td>{{cell}}</td>{% endfor %} + </tr>{% endfor %} + </tbody> + </table> + </div> + </div> +</div> diff --git a/ishtar_common/templates/ishtar/blocks/window_nav.html b/ishtar_common/templates/ishtar/blocks/window_nav.html index 41572c5f2..a8c344bae 100644 --- a/ishtar_common/templates/ishtar/blocks/window_nav.html +++ b/ishtar_common/templates/ishtar/blocks/window_nav.html @@ -117,7 +117,7 @@ {% endif %} <script type="text/javascript"> $(document).ready(function(){ - register_qa_on_sheet(); + register_qa(); }); </script> {% else %} diff --git a/ishtar_common/templates/ishtar/import_table.html b/ishtar_common/templates/ishtar/import_table.html index 3be92d42d..57e28abc3 100644 --- a/ishtar_common/templates/ishtar/import_table.html +++ b/ishtar_common/templates/ishtar/import_table.html @@ -1,7 +1,14 @@ {% load i18n l10n inline_formset %} +{% trans "Source" as source_label %} +{% trans "Media" as media_label %} +{% trans "Result" as result_label %} +{% trans "Error" as error_label %} +{% trans "Match" as match_label %} {% localize off %}<script type="text/javascript"> {% comment %} + /* TODO : à effacer ? */ + var html = $("#message_list").html(); {% if MESSAGES and AJAX %}{% for message, message_type in MESSAGES %} html += '<div class="alert alert-{{message_type}} alert-dismissible fade show"'; @@ -14,6 +21,7 @@ html += ' </div>'; {% endfor %}{% endif %} $("#message_list").html(html); + {% endcomment %} $("#import-list").find('select').prop('disabled', true); @@ -30,6 +38,7 @@ if (import_table_update_import_ids.length) need_refresh = true; $(document).ready(function(){ + register_qa(); if (need_refresh) setInterval(function(){ import_table_update_import_list(import_table_update_import_ids) }, 3 * 1000); @@ -51,6 +60,7 @@ <th>{% trans "Diagnostic files" %}</th>{% endif %} </tr> {% for import in object_list %} + {% with current_import=import %} <tr id="import-{{import.import_id}}" class='import-row{% if import.has_error or not import.pre_import_form_is_valid %}-error{% endif %}{% if import.pk in refreshed_pks %} bg-info{% endif %}'> <td><ul class="simple"> @@ -74,38 +84,57 @@ {% endfor%} </select> </td> - <td><ul class="simple"> - {% if import.imported_file %}<li> - <i class="fa fa-fw fa-file-text-o" aria-hidden="true"></i> <a href='{{import.imported_file.url}}'>{% trans "Source" %}</a> + <td><ul class="simple table-import-files"> + {% if import.imported_file %}<li class="p-1"> + {% with file_label=source_label logo='fa fa-fw fa-file-text-o' file_type='source' file=import.imported_file %} + {% include "ishtar/blocks/import_table_buttons_view.html" %} + {% endwith %} </li> - {% if import.get_imported_images %}<li> - <i class="fa fa-fw fa-file-image-o" aria-hidden="true"></i> <a href="{{ import.get_imported_images.url }}">{% trans "Media" %}</a> + {% if import.get_imported_images %}<li class="p-1"> + {% with file_label=media_label logo='fa fa-fw fa-file-image-o' file_type='' file=import.get_imported_images %} + {% include "ishtar/blocks/import_table_buttons_view.html" %} + {% endwith %} </li>{% endif %} {% elif import.archive_file %}<li> <i class="fa fa-fw fa-file-archive-o" aria-hidden="true"></i> <a href='{{import.archive_file.url}}'>{% trans "Archive" context "name" %}</a> </li>{% endif %} </ul></td>{% if not ARCHIVE_PAGE %} - <td><ul class="simple"> - {% if import.has_pre_import_form %}<li> - {% if not import.pre_import_form_is_valid %} - <i class="text-danger fa fa-fw fa-exclamation-triangle" aria-hidden="true"></i> - {% else %} - <i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i> - {% endif %} - <a href='{% url "import_pre_import_form" import.pk %}'>{% trans "Pre-import values" %}</a> + <td><ul class="simple table-import-match-files"> + {% if import.has_pre_import_form %}<li class="p-1"> + <div class="btn-group btn-group-sm" role="group"> + <a class="btn btn-secondary" href='{% url "import_pre_import_form" import.pk %}'> + {% if not import.pre_import_form_is_valid %} + <i class="text-danger fa fa-fw fa-exclamation-triangle" aria-hidden="true"></i> + {% else %} + <i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i> + {% endif %} + {% trans "Pre-import values" %} + </a> + </div> </li>{% endif %} - {% if import.need_matching %}<li> - <i class="fa fa-fw fa-arrows-h" aria-hidden="true"></i> <a href='{% url "import_link_unmatched" import.pk %}'>{% trans "Make match" %}</a> + {% if import.need_matching %}<li class="p-1"> + <div class="btn-group btn-group-sm" role="group"> + <a class="btn btn-secondary" href='{% url "import_link_unmatched" import.pk %}'> + <i class="fa fa-fw fa-arrows-h" aria-hidden="true"></i> {% trans "Make match" %} + </a> + </div> </li>{% endif %} </ul></td> - <td style="white-space: nowrap;"><ul class="simple">{% if import.error_file %}<li> - <i class="text-danger fa fa-fw fa-exclamation-triangle" aria-hidden="true"></i> <a href='{{import.error_file.url}}'>{% trans "Error" %}</a> + <td style="white-space: nowrap;"><ul class="simple table-import-diag"> + {% if import.error_file %}<li class="p-1"> + {% with file_label=error_label logo='text-danger fa fa-fw fa-exclamation-triangle' file_type='error' file=import.error_file %} + {% include "ishtar/blocks/import_table_buttons_view.html" %} + {% endwith %} </li>{% endif %} - {% if import.result_file %}<li> - <i class="fa fa-fw fa-th" aria-hidden="true"></i> <a href='{{import.result_file.url}}'>{% trans "Result" %}</a> + {% if import.result_file %}<li class="p-1"> + {% with file_label=result_label logo='fa fa-fw fa-th' file_type='result' file=import.result_file %} + {% include "ishtar/blocks/import_table_buttons_view.html" %} + {% endwith %} </li>{% endif %} - {% if import.match_file %}<li> - <i class="fa fa-fw fa-arrows-h" aria-hidden="true"></i> <a href='{{import.match_file.url}}'>{% trans "Match" %}</a> + {% if import.match_file %}<li class="p-1"> + {% with file_label=match_label logo='fa fa-fw fa-arrows-h' file_type='match' file=import.match_file %} + {% include "ishtar/blocks/import_table_buttons_view.html" %} + {% endwith %} </li>{% endif %}</ul> </td>{% endif %} </tr> @@ -122,46 +151,66 @@ </td> </tr> {% endif %} + {% endwith %} {% if not import.importer_type.type_label and not ARCHIVE_PAGE %} {# group #} {% for sub in import.import_list %} + {% with current_import=sub %} <tr id="import-{{sub.import_id}}"> <td></td> <td>{{sub.importer_type}}</td> <td id="status-{{sub.import_id}}">{{sub.status}}</td> <td></td> - <td><ul class="simple"> - {% if sub.imported_file %}<li> - <i class="fa fa-fw fa-file-text-o" aria-hidden="true"></i> <a href='{{sub.imported_file.url}}'>{% trans "Source" %}</a> + <td><ul class="simple table-import-files"> + {% if sub.imported_file %}<li class="p-1"> + {% with file_label=source_label logo='fa fa-fw fa-file-text-o' file_type='source' file=sub.imported_file %} + {% include "ishtar/blocks/import_table_buttons_view.html" %} + {% endwith %} </li>{% endif %} - {% if sub.get_imported_images %}<li> - <i class="fa fa-fw fa-file-image-o" aria-hidden="true"></i> <a href="{{ sub.get_imported_images.url }}">{% trans "Media" %}</a> + {% if sub.get_imported_images %}<li class="p-1"> + {% with file_label=media_label logo='fa fa-fw fa-file-image-o' file_type='' file=sub.get_imported_images %} + {% include "ishtar/blocks/import_table_buttons_view.html" %} + {% endwith %} </li>{% endif %} </ul></td>{% if not ARCHIVE_PAGE %} - <td><ul class="simple"> - {% if sub.has_pre_import_form %}<li> - {% if not sub.pre_import_form_is_valid %} - <i class="text-danger fa fa-fw fa-exclamation-triangle" aria-hidden="true"></i> - {% else %} - <i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i> - {% endif %} - <a href='{% url "import_pre_import_form" sub.pk %}'>{% trans "Pre-import values" %}</a> - </li>{% endif %} - {% if sub.need_matching %}<li> - <i class="fa fa-fw fa-arrows-h" aria-hidden="true"></i> <a href='{% url "import_link_unmatched" sub.pk %}'>{% trans "Make match" %}</a> - </li>{% endif %} + <td><ul class="simple table-import-match-files"> + {% if sub.has_pre_import_form %}<li class="p-1"> + <div class="btn-group btn-group-sm" role="group"> + <a class="btn btn-secondary" href='{% url "import_pre_import_form" sub.pk %}'> + {% if not sub.pre_import_form_is_valid %} + <i class="text-danger fa fa-fw fa-exclamation-triangle" aria-hidden="true"></i> + {% else %} + <i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i> + {% endif %} + {% trans "Pre-import values" %} + </a> + </div> + </li>{% endif %} + {% if sub.need_matching %}<li class="p-1"> + <div class="btn-group btn-group-sm" role="group"> + <a class="btn btn-secondary" href='{% url "import_link_unmatched" sub.pk %}'> + <i class="fa fa-fw fa-arrows-h" aria-hidden="true"></i> {% trans "Make match" %} + </a> + </div> + </li>{% endif %} </ul></td> - <td><ul class="simple"> - {% if sub.error_file %}<li> - <i class="text-danger fa fa-fw fa-exclamation-triangle" aria-hidden="true"></i> <a href='{{sub.error_file.url}}'>{% trans "Error" %}</a> - </li>{% endif %} - {% if sub.result_file %}<li> - <i class="fa fa-fw fa-th" aria-hidden="true"></i> <a href='{{sub.result_file.url}}'>{% trans "Result" %}</a> + <td style="white-space: nowrap;"><ul class="simple table-import-diag"> + {% if sub.error_file %}<li class="p-1"> + {% with file_label=error_label logo='text-danger fa fa-fw fa-exclamation-triangle' file_type='error' file=sub.error_file %} + {% include "ishtar/blocks/import_table_buttons_view.html" %} + {% endwith %} </li>{% endif %} - {% if sub.match_file %}<li> - <i class="fa fa-fw fa-arrows-h" aria-hidden="true"></i> <a href='{{sub.match_file.url}}'>{% trans "Match" %}</a> + {% if sub.result_file %}<li class="p-1"> + {% with file_label=result_label logo='fa fa-fw fa-th' file_type='result' file=sub.result_file %} + {% include "ishtar/blocks/import_table_buttons_view.html" %} + {% endwith %} </li>{% endif %} - </ul> - </td>{% endif %} + {% if sub.match_file %}<li class="p-1"> + {% with file_label=match_label logo='fa fa-fw fa-arrows-h' file_type='match' file=sub.match_file %} + {% include "ishtar/blocks/import_table_buttons_view.html" %} + {% endwith %} + </li>{% endif %}</ul> + </td> + {% endif %} </tr> <tr></tr>{# only for even and odd style #} <tr id="progress-display-{{sub.id}}"{% if sub.state != 'IP' and sub.state != 'PP' %} style="display:none"{% endif %}> @@ -174,6 +223,7 @@ </div> </td> </tr> + {% endwith %} {% endfor %} {% endif %} {% endfor %} diff --git a/ishtar_common/urls.py b/ishtar_common/urls.py index ebcb132b8..2dbc13434 100644 --- a/ishtar_common/urls.py +++ b/ishtar_common/urls.py @@ -264,6 +264,11 @@ urlpatterns = [ name="import_link_unmatched", ), url( + r"^import-csv-view/(?P<target>source|result|match|error)/(?P<group>group\-)?(?P<pk>[0-9]+)/$", + views.ImportCSVView.as_view(), + name="import_display_csv", + ), + url( r"^import-step-by-step/all/(?P<pk>[0-9]+)/(?P<line_number>[0-9]+)/$", views.ImportStepByStepView.as_view(), name="import_step_by_step_all", diff --git a/ishtar_common/views.py b/ishtar_common/views.py index 4a76207f6..ca9df098c 100644 --- a/ishtar_common/views.py +++ b/ishtar_common/views.py @@ -1553,13 +1553,12 @@ class ImportListView(IshtarMixin, LoginRequiredMixin, ListView): return query.exclude(state="AC") def get_queryset(self): - q1 = self._queryset_filter(self.model.objects) - q2 = self._queryset_filter(models.ImportGroup.objects) - if not self.request.user.is_superuser: - user = models.IshtarUser.objects.get(pk=self.request.user.pk) - q1 = q1.filter(user=user) - q2 = q2.filter(user=user) + user = self.request.user + if not user.pk: + raise Http404() + q1 = self._queryset_filter(self.model.query_can_access(user)) q1 = q1.filter(group__isnull=True).order_by("-creation_date", "-pk") + q2 = self._queryset_filter(models.ImportGroup.query_can_access(user)) q2 = q2.order_by("-creation_date", "-pk") return list(reversed(sorted(list(q1) + list(q2), key=lambda x: x.creation_date))) @@ -2143,6 +2142,55 @@ class ImportGroupDeleteView(IshtarMixin, LoginRequiredMixin, DeleteView): return reverse("current_imports") +class ImportCSVView(IshtarMixin, LoginRequiredMixin, TemplateView): + template_name = "ishtar/blocks/view_import_csv.html" + ATTRIBUTES = { + "source": "imported_file", + "error": "error_file", + "result": "result_file", + "match": "match_file" + } + TITLES = { + "source": ("fa fa-file-text-o", _("Source")), + "error": ("text-danger fa fa-exclamation-triangle", _("Error")), + "result": ("fa fa-th", _("Result")) , + "match": ("fa fa-arrows-h", _("Match")), + } + + def get(self, request, *args, **kwargs): + user = self.request.user + if not user.pk: + raise Http404() + model = models.ImportGroup if kwargs.get("group", None) else models.Import + q = model.query_can_access(self.request.user).filter(pk=kwargs.get("pk", -1)) + if not q.count(): + raise Http404() + self.import_item = q.all()[0] + attribute = self.ATTRIBUTES[kwargs["target"]] + self.csv_file = getattr(self.import_item, attribute) + if not self.csv_file: + raise Http404() + return super().get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + data = super().get_context_data(**kwargs) + encoding = "utf-8" + if self.kwargs["target"] == "source" and self.import_item.encoding: + encoding = self.import_item.encoding + data["icon"], data["target"] = self.TITLES[self.kwargs["target"]] + data["title"] = str(self.import_item) + data["content"] = [] + with open(self.csv_file.path, "r", encoding=encoding) as f: + reader = csv.reader(f) + for idx, line in enumerate(reader): + if not idx: + data["header"] = line + continue + data["content"].append(line) + data["window_id"] = "csv-view-" + (self.kwargs.get("group", "") or "") + str(self.import_item.pk) + return data + + class PersonCreate(LoginRequiredMixin, CreateView): model = models.Person form_class = forms.BasePersonForm diff --git a/scss/custom.scss b/scss/custom.scss index 934d67937..c5ee6d060 100644 --- a/scss/custom.scss +++ b/scss/custom.scss @@ -123,6 +123,16 @@ input[type="file"].form-control{ border-collapse: separate; } +.btn-outline-secondary.no-hover:hover { + color: inherit; + background-color: inherit; + border-color: inherit; +} + +.btn.no-hover { + color: #212529; +} + .has-previous-value .form-control, .has-previous-value .btn, .has-previous-value .input-group-text{ @@ -289,6 +299,41 @@ pre { background-color: white; } +.import-row-error, +.table-striped tbody tr:nth-of-type(2n+1).import-row-error { + background-color: lighten(red, 40%); +} + + +#import-container { + overflow: scroll; +} + +#import-list li { + white-space: nowrap; +} + +ul.table-import-files > li span.btn{ + width: 6rem; +} + +ul.table-import-diag > li span.btn{ + width: 10rem; +} + +.table-scroll { + padding: 1em; + + table { + display:block; + overflow-x : scroll; + overflow-y : scroll; + } + thead { + position: sticky; + top: -1px; + } +} .tab-content{ padding-top: 0; @@ -672,15 +717,6 @@ div#validation-bar{ color: darken(red, 20%); } -.import-row-error, -.table-striped tbody tr:nth-of-type(2n+1).import-row-error { - background-color: lighten(red, 40%); -} - -#import-list li { -white-space: nowrap; -} - /* context menu */ #shortcut-menu { width: 700px; |