diff options
| -rw-r--r-- | ishtar_common/static/media/styles.css | 4 | ||||
| -rw-r--r-- | ishtar_common/templates/ishtar/import_step_by_step.html | 215 | ||||
| -rw-r--r-- | ishtar_common/utils.py | 6 | ||||
| -rw-r--r-- | ishtar_common/views.py | 88 | 
4 files changed, 285 insertions, 28 deletions
| diff --git a/ishtar_common/static/media/styles.css b/ishtar_common/static/media/styles.css index 9507674b7..6860ca93c 100644 --- a/ishtar_common/static/media/styles.css +++ b/ishtar_common/static/media/styles.css @@ -224,6 +224,10 @@ table.dataTable thead th, table.dataTable thead td {      overflow-x: auto;  } +.step-by-step-csv textarea { +    height: 50px; +} +  /* à adapter */  #message,  #message div{ diff --git a/ishtar_common/templates/ishtar/import_step_by_step.html b/ishtar_common/templates/ishtar/import_step_by_step.html index c827156c5..293ce3dce 100644 --- a/ishtar_common/templates/ishtar/import_step_by_step.html +++ b/ishtar_common/templates/ishtar/import_step_by_step.html @@ -2,28 +2,109 @@  {% load i18n inline_formset link_to_window %}  {% block content %} + +{% comment %} +<nav aria-label="Page navigation example"> +  <ul class="pagination justify-content-center"> +    <li class="page-item disabled"> +      <a class="page-link" href="#" tabindex="-1">Previous</a> +    </li> +    <li class="page-item"><a class="page-link" href="#">1</a></li> +    <li class="page-item"><a class="page-link" href="#">2</a></li> +    <li class="page-item"><a class="page-link" href="#">3</a></li> +    <li class="page-item"> +      <a class="page-link" href="#">Next</a> +    </li> +  </ul> +</nav> +{% endcomment %} + +  <h2>{% trans "Import step by step" %} – {{import.name}} – {% trans "line " %} {{line_number_displayed}}</h2>  {% if errors %}  <div class="alert alert-danger" role="alert">      <p>{% trans "The following error(s) has been encountered while parsing the source file:" %}</p> -    <ul>{% for error in errors %} -        <li>{{error}}</li> -    {% endfor %}</ul> +    <table class="table table-striped"> +        <tr> +            <th>{% trans "Column" %}</th> +            <th>{% trans "Error" %}</th> +        </tr> +        {% for line, column, error in errors %}<tr> +            <td> +                {% if column %} +                <a href="#col-{{column}}"> +                    <span class="badge badge-info"> +                        {% trans "Col."%} {{column}} +                    </span> +                </a> +                {% else %} +                – +                {% endif %} +            </td> +            <td>{{error}}</td> +        </tr>{% endfor %} +    </table>  </div>  {% if values %} -<table class="table table-striped"> -    {% for header, value in values %} -    <tr> -        <th>{{header}}</th> -        <td>{{value}}</td> -    </tr> -    {% endfor %} -</table> +<form action="." class='step-by-step-csv' method="post" name='step-by-step-csv'> +    {% csrf_token %} +    <table class="table table-striped"> +        <tr> +            <th>{% trans "Column number" %}</th> +            <th>{% trans "Name" %}</th> +            <th>{% trans "Raw value" %}</th> +            <th>{% trans "New value" %}</th> +        </tr> +        {% for idx, name, raw in values %}<tr id="col-{{idx}}"> +            <td>{{idx}}</td> +            <td>{{name|safe}}</td> +            <td>{{raw}}</td> +            <td><textarea name="col-{{idx}}">{{raw}}</textarea></td> +        </tr>{% endfor %} +    </table> + +    <div id="footer"> +        <div id="validation-bar" class="row text-center"> +            <div class="col-sm"> +                <button type="submit" name="valid" id="btn-change-csv" +                        value="change-csv" class="btn btn-success"> +                    {% trans "Update source file" %} +                </button> +            </div> +            <div class="col-sm"> +                <a href="{% url 'import_step_by_step' import.pk line_number_displayed %}"> +                    <button type="button" class="btn btn-secondary" id="btn-cancel"> +                        {% trans "Cancel" %} +                    </button> +                </a> +            </div> +            <div class="col-sm"> +                <a href="{% url 'current_imports' %}"> +                    <button type="button" class="btn btn-secondary" +                            id="btn-back"> +                        {% trans "Back to import list" %} +                    </button> +                </a> +            </div> +        </div> +        {% include 'ishtar/blocks/footer.html' %} +    </div> +</form>  {% endif %}  {% else %} +{% if not have_change %} +<div class="alert alert-success" role="alert"> +    <p>{% trans "No change for this item." %}</p> +</div> +{% else %} +<div class="alert alert-warning" role="alert"> +    <p>{% trans "Changes will be made for this item" %}</p> +</div> +{% endif %} +  {% if new_objects %}  <h3>{% trans "Objects to be created" %}</h3>  {% for path, cls, values in new_objects %} @@ -39,7 +120,7 @@          </tr>          {% for k, value in values.items %}<tr>              <th>{{k|safe}}</th> -            <td>{{value}}</td> +            <td>{{value|safe}}</td>          </tr>{% endfor %}      </table>    </div> @@ -49,6 +130,7 @@  {% endif %}  {% if updated_objects %} +  <h3>{% trans "Objects to be updated" %}</h3>  {% for path, obj, values, old_and_updated in updated_objects %} @@ -70,7 +152,6 @@              </tr>{% endfor %}          </table> -          <h5>{% trans "Updated values" %}</h5>          <table class="table table-striped">              <tr> @@ -93,29 +174,127 @@  <hr/>  {% endfor %} +{% endif %} + +{% if matched_objects %} +<h3>{% trans "Objects matched with no changes" %}</h3> +{% for path, obj, values in matched_objects %} + +<div class="card"> +    <div class="card-body"> +        <h5 class="card-title">{{path}} – {{obj}} {{obj|link_to_window}} ({{obj.get_verbose_name}})</h5> +    </div> +    <div class="card-body"> + +        <h5>{% trans "Matching values" %}</h5> +        <table class="table table-striped"> +            <tr> +                <th>{% trans "Key" %}</th> +                <th>{% trans "Value" %}</th> +            </tr> +            {% for k, value in values.items %}<tr> +            <th>{{k|safe}}</th> +            <td>{{value}}</td> +        </tr>{% endfor %} +        </table> +    </div> +</div> + +<hr/> +{% endfor %}  {% endif %} -<h3>{% trans "CSV values" %}</h3> -<div> + +<h3>{% trans "Source file" %}</h3> + +<form action="." class='step-by-step-csv' method="post" name='step-by-step-csv'> +    {% csrf_token %}      <table class="table table-striped">          <tr>              <th>{% trans "Column number" %}</th>              <th>{% trans "Name" %}</th> -            <th>{% trans "Raw CSV value" %}</th> +            <th>{% trans "Raw value" %}</th>              <th>{% trans "Interpreted value" %}</th> -            {% comment %}<th>{% trans "Merged value" %}</th>{% endcomment %} +            <th>{% trans "New value" %}</th>          </tr>          {% for idx, name, raw, interpreted in values %}<tr id="col-{{idx}}">              <td>{{idx}}</td>              <td>{{name|safe}}</td>              <td>{{raw}}</td>              <td>{{interpreted|safe}}</td> +            <td><textarea name="col-{{idx}}">{{raw}}</textarea></td>          </tr>{% endfor %}      </table> -</div> +    <div id="footer"> +        <div id="validation-bar" class="row text-center"> +            <div class="col-sm"> +                <button type="submit" name="valid" id="btn-change-csv" +                        value="change-csv" class="btn btn-success" +                        disabled="disabled"> +                    {% trans "Update source file" %} +                </button> +            </div> +            <div class="col-sm"> +                <button type="submit" name="valid" id="btn-import" +                        value="import" class="btn btn-success"> +                    {% trans "Import this line" %} +                </button> +            </div> +            <div class="col-sm"> +                <a href="{% url 'import_step_by_step' import.pk line_number_displayed %}"> +                    <button type="button" class="btn btn-secondary" +                        disabled="disabled" id="btn-cancel"> +                        {% trans "Cancel" %} +                    </button> +                </a> +            </div> +            <div class="col-sm"> +                <a href="{% url 'current_imports' %}"> +                    <button type="button" class="btn btn-secondary" +                            id="btn-back"> +                        {% trans "Back to import list" %} +                    </button> +                </a> +            </div> +        </div> +        {% include 'ishtar/blocks/footer.html' %} +    </div> + +</form> +<script type="text/javascript"> +    var modified = false; +    var no_modif_msg = "{% trans "No modification made to the source file" %}"; +    var modif_msg = "{% trans "Modification to the source file have been made. Save or cancel theses modification before import." %}"; + +    var no_modif_made = function(){ +        $("#btn-cancel").prop("title", no_modif_msg); +        $("#btn-change-csv").prop("title", no_modif_msg); +    }; +    var modif_made = function(){ +        if (modified) return; +        modified = true; +        $("#btn-cancel").prop("title", ''); +        $("#btn-cancel").prop("disabled", false); +        $("#btn-change-csv").prop("title", ''); +        $("#btn-change-csv").prop("disabled", false); +        $("#btn-import").prop("title", modif_msg); +        $("#btn-import").prop("disabled", true); +        $("#btn-back").prop("title", modif_msg); +        $("#btn-back").prop("disabled", true); +    }; +    $(function(){ +        $('textarea').bind('input propertychange', function() { +            modif_made(); +        }); +        no_modif_made(); +    }); +</script>  {% endif %} +  {% endblock %} +{% block footer %} +{% endblock %} diff --git a/ishtar_common/utils.py b/ishtar_common/utils.py index 5a03f7f5f..c25cdf605 100644 --- a/ishtar_common/utils.py +++ b/ishtar_common/utils.py @@ -28,6 +28,7 @@ from django.conf import settings  from django.contrib.gis.geos import GEOSGeometry  from django.contrib.sessions.backends.db import SessionStore  from django.core.cache import cache +from django.core.exceptions import FieldDoesNotExist  from django.core.urlresolvers import reverse  from django.utils.datastructures import MultiValueDict as BaseMultiValueDict  from django.utils.safestring import mark_safe @@ -406,7 +407,10 @@ def get_field_labels_from_path(model, path):      """      labels = []      for key in path: -        field = model._meta.get_field(key) +        try: +            field = model._meta.get_field(key) +        except: +            return labels          if hasattr(field, 'verbose_name'):              labels.append(field.verbose_name)          if field.one_to_many or field.one_to_one or field.many_to_many: diff --git a/ishtar_common/views.py b/ishtar_common/views.py index 1fa968a95..d6d16568a 100644 --- a/ishtar_common/views.py +++ b/ishtar_common/views.py @@ -29,6 +29,7 @@ from tempfile import NamedTemporaryFile  from tidylib import tidy_document as tidy  import unicodedata  from unidecode import unidecode +import unicodecsv  from weasyprint import HTML, CSS  from weasyprint.fonts import FontConfiguration @@ -1844,7 +1845,7 @@ class ImportStepByStepView(IshtarMixin, LoginRequiredMixin, TemplateView):      page_name = _(u"Import step by step")      current_url = 'import_step_by_step' -    def get(self, request, *args, **kwargs): +    def get_import(self):          try:              self.imprt_obj = models.Import.objects.get(                  pk=int(self.kwargs['pk']) @@ -1857,18 +1858,66 @@ class ImportStepByStepView(IshtarMixin, LoginRequiredMixin, TemplateView):              if self.imprt_obj.user != user:                  raise Http404          self.current_line_number = int(self.kwargs['line_number']) - 1 -        self.imprt = None + +    def update_csv(self, request): +        prefix = 'col-' +        submited_line = [(int(k[len(prefix):]), request.POST[k]) +                         for k in request.POST if k.startswith(prefix)] +        updated_line = [value for line, value in sorted(submited_line)] +        filename = self.imprt_obj.imported_file.path +        with open(filename, 'r') as f: +            reader = unicodecsv.reader( +                f, encoding=self.imprt_obj.encoding) +            lines = [] +            for idx, line in enumerate(reader): +                if idx == self.current_line_number: +                    line = updated_line +                line = [v.encode(self.imprt_obj.encoding, errors='replace') +                        for v in line] +                lines.append(line) +        with open(filename, 'w') as f: +            writer = csv.writer(f, **CSV_OPTIONS) +            writer.writerows(lines) + +    def import_line(self, request, *args, **kwargs):          try:              self.imprt, data = self.imprt_obj.importation( -                simulate=True,                  line_to_process=self.current_line_number,                  return_importer_and_data=True              )          except IOError as e:              self.errors = [e.message] -            return +            return super(ImportStepByStepView, self).get(request, *args, +                                                         **kwargs) + +    def post(self, request, *args, **kwargs): +        if not request.POST or request.POST.get('valid', None) not in ( +                'change-csv', 'import'): +            return self.get(request, *args, **kwargs) + +        self.get_import() +        if request.POST.get('valid') == 'change-csv': +            self.update_csv(request) +            return self.get(request, *args, **kwargs) +        self.import_line(request, *args, **kwargs) +        # todo redirect to next +        return self.get(request, *args, **kwargs) + +    def get(self, request, *args, **kwargs): +        self.get_import() +        self.imprt = None          self.errors, self.new_data = None, None -        if not data: +        try: +            self.imprt, data = self.imprt_obj.importation( +                simulate=True, +                line_to_process=self.current_line_number, +                return_importer_and_data=True +            ) +        except IOError as e: +            self.errors = [None, None, e.message] +            return super(ImportStepByStepView, self).get(request, *args, +                                                         **kwargs) +        if not data or not data[0]:              self.errors = self.imprt.errors          else:              self.new_data = data[:] @@ -1882,7 +1931,10 @@ class ImportStepByStepView(IshtarMixin, LoginRequiredMixin, TemplateView):          if self.errors:              if self.imprt.current_csv_line:                  headers = [f.label for f in self.imprt.get_formaters()] -                dct['values'] = zip(headers, self.imprt.current_csv_line) +                dct['values'] = zip( +                    range(1, len(headers) + 1), headers, +                    self.imprt.current_csv_line +                )              return dct          headers, self.path_to_column, interpreted_values = [], {}, []          for idx, formater in enumerate(self.imprt.get_formaters()): @@ -1934,7 +1986,12 @@ class ImportStepByStepView(IshtarMixin, LoginRequiredMixin, TemplateView):                      continue                  created_dict[k] = val              # check if it is not previously created -            key = (cls, tuple(sorted(created_dict.items()))) +            dct_key = [] +            for key, value in created_dict.items(): +                if isinstance(value, list): +                    value = tuple(sorted(value)) +                dct_key.append((key, value)) +            key = (cls, tuple(sorted(dct_key)))              if key in new_objects:                  # regroup it                  new_objects[key][0].append(label) @@ -1954,10 +2011,11 @@ class ImportStepByStepView(IshtarMixin, LoginRequiredMixin, TemplateView):          ]          updated_objects = [] +        main_changed = False          for path, obj, values, updated_values in self.imprt.updated_objects:              # transform path to explicit label -            label = self.transform_path_to_label(obj.__class__, path) +            label = self.transform_path_to_label(self.imprt.OBJECT_CLS, path)              # transform key into explicit label              values = self.transform_keys_to_label(path, obj.__class__, values) @@ -1979,6 +2037,7 @@ class ImportStepByStepView(IshtarMixin, LoginRequiredMixin, TemplateView):                      changed = current_value != updated_value                      current_value = self.get_value(current_value)                      updated_value = self.get_value(updated_value) +                main_changed |= changed                  old_and_updated[k] = [current_value, updated_value,                                        changed] @@ -1987,7 +2046,18 @@ class ImportStepByStepView(IshtarMixin, LoginRequiredMixin, TemplateView):                                                             old_and_updated)              updated_objects.append((label, obj, values, old_and_updated)) -        dct['updated_objects'] = updated_objects +        dct['have_change'] = main_changed +        dct['updated_objects'] = [] +        dct['matched_objects'] = [] +        for path, obj, values, old_and_updated in updated_objects: +            if old_and_updated: +                dct['updated_objects'].append( +                    (path, obj, values, old_and_updated) +                ) +            else: +                dct['matched_objects'].append( +                    (path, obj, values) +                )          dct['ambiguous_objects'] = self.imprt.ambiguous_objects          dct['not_find_objects'] = self.imprt.not_find_objects          return dct | 
