diff options
| -rw-r--r-- | archaeological_operations/models.py | 20 | ||||
| -rw-r--r-- | archaeological_operations/templates/ishtar/sheet_operation.html | 83 | ||||
| -rw-r--r-- | ishtar_common/models.py | 3 | ||||
| -rw-r--r-- | ishtar_common/models_common.py | 49 | ||||
| -rw-r--r-- | ishtar_common/templates/ishtar/blocks/window_field_flex_multiple.html | 6 | ||||
| -rw-r--r-- | ishtar_common/templates/ishtar/blocks/window_nav.html | 6 | ||||
| -rw-r--r-- | ishtar_common/templates/ishtar/sheet.html | 8 | ||||
| -rw-r--r-- | ishtar_common/templatetags/ishtar_helpers.py | 14 | ||||
| -rw-r--r-- | ishtar_common/templatetags/window_field.py | 12 | ||||
| -rw-r--r-- | ishtar_common/utils.py | 13 | ||||
| -rw-r--r-- | ishtar_common/views_item.py | 83 | 
11 files changed, 256 insertions, 41 deletions
| diff --git a/archaeological_operations/models.py b/archaeological_operations/models.py index 050e50240..4747fbf9e 100644 --- a/archaeological_operations/models.py +++ b/archaeological_operations/models.py @@ -75,6 +75,7 @@ from ishtar_common.model_managers import UUIDModelManager  from ishtar_common.utils import (      cached_label_changed,      force_cached_label_changed, +    human_date,      mode,      m2m_historization_changed,      post_save_geo, @@ -732,6 +733,14 @@ def get_values_town_related(item, prefix, values, filtr=None):  class ClosedItem(object): +    def serialize_closing(self): +        value = self.closing() +        if not value: +            return "" +        value["date"] = human_date(value["date"]) +        value["user"] = str(value["user"]) +        return value +      def closing(self):          if self.is_active():              return @@ -1120,6 +1129,11 @@ class Operation(          "towns",          "periods",      ] +    SERIALIZE_PROPERTIES = MainItem.SERIALIZE_PROPERTIES + ["short_label"] +    SERIALIZE_DATES = ["start_date", "excavation_end_date"] +    SERIALIZE_CALL = {"closing": "serialize_closing", +                      "archaeological_sites_list": "archaeological_sites_list", +                      "documents_list": "documents_list"}      # fields definition      uuid = models.UUIDField(default=uuid.uuid4) @@ -1460,6 +1474,10 @@ class Operation(          )          return dct +    def archaeological_sites_list(self) -> list: +        return self.get_associated_main_item_list("archaeological_sites", +                                                  ArchaeologicalSite) +      @classmethod      def _get_department_code(cls, value):          if not settings.ISHTAR_DPTS: @@ -1499,7 +1517,7 @@ class Operation(          return [town.label_with_areas for town in self.towns.all()]      def towns_label(self): -        return " - ".join(self.towns_codes()) +        return " ; ".join(self.towns_codes())      def has_finds(self):          from archaeological_finds.models import BaseFind diff --git a/archaeological_operations/templates/ishtar/sheet_operation.html b/archaeological_operations/templates/ishtar/sheet_operation.html index babcc9ce0..811997515 100644 --- a/archaeological_operations/templates/ishtar/sheet_operation.html +++ b/archaeological_operations/templates/ishtar/sheet_operation.html @@ -21,9 +21,10 @@  {% with display_data=item.data %}  {% with display_relations=item|safe_or:"right_relations.count|left_relations.count" %} -{% with display_sites=item|safe_or:"archaeological_sites.count|grouped_parcels|administrative_act.count" %} +{% with display_sites=item|safe_or:"archaeological_sites.count|grouped_parcels|administrative_act.count|archaeological_sites_list" %}  {% with perm_documents=permission_view_own_document|or_:permission_view_document %} -{% with display_documents=perm_documents|and_:item.documents.count %} +{% with has_documents=item|safe_or:"documents.count|documents_list" %} +{% with display_documents=perm_documents|and_:has_documents %}  {% with perm_context_records=permission_view_own_contextrecord|or_:permission_view_contextrecord %}  {% with has_context_records=item|safe_or:"context_record.count" %}  {% with display_context_records=perm_context_records|and_:has_context_records %} @@ -31,7 +32,7 @@  {% with has_finds=item|safe_or:"has_finds" %}  {% with display_finds=perm_find|and_:has_finds %} -{% if output != "ODT" and output != "PDF"%} +{% if output != "ODT" and output != "PDF" %}  <ul class="nav nav-tabs" id="{{window_id}}-tabs" role="tablist">      <li class="nav-item">          <a class="nav-link active" id="{{window_id}}-general-tab" @@ -94,6 +95,7 @@          </a>      </li>      {% endif %} +    {% if not is_external %}      <li class="nav-item">          <a class="nav-link" id="{{window_id}}-statistics-tab"             data-toggle="tab" href="#{{window_id}}-statistics" role="tab" @@ -101,6 +103,7 @@              {% trans "Statistics" %}          </a>      </li> +    {% endif %}  </ul>  {% endif %} @@ -131,7 +134,7 @@                              {% if next %}                              {{ item|m2m_listing:'towns'|join:" ; "|default:''  }}                              {% else %} -                            {{ item.towns_codes|join:" ; "|default:''  }} +                            {{ item.cached_towns_label }}                              {% endif %}                          </p>                          <p class='window-refs' title="{% trans 'Name' %}">{{item.common_name|default:''}}</p> @@ -141,13 +144,19 @@              <div class="row">                  {% trans "Excavation dates (start/end)" as date_label %} +                {% if not is_external %}                  {% with start_date=item.start_date|date:"DATE_FORMAT"|default:"-" %}                  {% with end_date=item.excavation_end_date|date:"DATE_FORMAT"|default:"-" %}                  {% with dates=start_date|add:" / "|add:end_date %}                  {% field_flex_2 date_label dates %} -                {% endwith %} -                {% endwith %} -                {% endwith %} +                {% endwith %}{% endwith %}{% endwith %} +                {% else %} +                {% with start_date=item.start_date|default:"-" %} +                {% with end_date=item.excavation_end_date|default:"-" %} +                {% with dates=start_date|add:" / "|add:end_date %} +                {% field_flex_2 date_label dates %} +                {% endwith %}{% endwith %}{% endwith %} +                {% endif %}                  <dl class="col-12 col-md-6 col-lg-3 flex-wrap">                      <dt>                          {% trans "State" %} @@ -243,10 +252,12 @@              {% field_flex_full "Comment about scientific documentation" item.scientific_documentation_comment "<pre>" "</pre>" %}          </div> +        {% if not is_external %}          <h3>{% trans "Sheet"%}</h3>          <div class="row">              {% include "ishtar/blocks/sheet_creation_section.html" %}          </div> +        {% endif %}          {% if item.virtual_operation %}          <div class="alert alert-warning" role="alert"> @@ -282,7 +293,7 @@                  {% if next %}                  {% field_flex_full "Towns" item|m2m_listing:'towns'|join:" ; "  %}                  {% else %} -                {% field_flex_full "Towns" item.towns_codes|join:" ; "  %} +                {{ item.cached_towns_label }}                  {% endif %}                  {% field_flex "Address" item.address %}                  {% if not item.address %} @@ -301,9 +312,24 @@      {% if display_sites %}         <div class="tab-pane fade" id="{{window_id}}-sites"         role="tabpanel" aria-labelledby="{{window_id}}-sites-tab"> -        {% if item.archaeological_sites.count %}          {% trans "Archaeological sites" as archaeologicalsites_label %} +        {% if item.archaeological_sites.count %}          {% dynamic_table_document archaeologicalsites_label 'sites' 'operations' item.pk '' output %} +        {% elif item.archaeological_sites_list %} +        <h3>{{archaeologicalsites_label}}</h3> +        <table class='table table-striped datatables' +               id="{{window_id}}-sites-table"> +        {% for values in item.archaeological_sites_list %}{% if not forloop.counter0 %} +            <thead> +            {% for value in values %}<th class="text-center">{{value}}</th>{% endfor %} +            </thead> +            {% else %} +            <tr> +                {% for value in values %}<td>{{value}}</td>{% endfor %} +            </tr> +        {% endif %} +        {% endfor %} +        </table>          {% endif %}          {% if item.parcels.count %} @@ -322,7 +348,24 @@      <div class="tab-pane fade" id="{{window_id}}-documents"         role="tabpanel" aria-labelledby="{{window_id}}-documents-tab">          {% trans "Document from this operation" as operation_docs %} +        {% if item.documents.count %}          {% dynamic_table_document operation_docs 'documents' 'operations' item.pk '' output %} +        {% elif item.documents_list %} +        <h3>{{operation_docs}}</h3> +        <table class='table table-striped datatables' +               id="{{window_id}}-docs-table"> +            {% for values in item.documents_list %}{% if not forloop.counter0 %} +            <thead> +            {% for value in values %}<th class="text-center">{{value}}</th>{% endfor %} +            </thead> +            {% else %} +            <tr> +                {% for value in values %}<td>{{value}}</td>{% endfor %} +            </tr> +            {% endif %} +            {% endfor %} +        </table> +        {% endif %}      </div>      {% endif %} @@ -414,6 +457,7 @@          {% include "ishtar/blocks/sheet_json.html" %}      </div>      {% endif %} +    {% if not is_external %}      <div class="tab-pane fade" id="{{window_id}}-statistics"         role="tabpanel" aria-labelledby="{{window_id}}-statistics-tab">          <h3>{% trans "Statistics" %}</h3> @@ -538,8 +582,27 @@          </div>          {% endif %}      </div> +    {% endif %}  </div> -{% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} + +<script type="text/javascript"> +$(document).ready( function () { +    datatable_options = { +        "dom": 'ltip', +    }; +    $.extend(datatable_options, datatables_static_default); +    if (datatables_i18n) datatable_options['language'] = datatables_i18n; +    $('.datatables').each( +        function(){ +            var dt_id = "#" + $(this).attr('id'); +            if (! $.fn.DataTable.isDataTable(dt_id) ) { +                $(dt_id).DataTable(datatable_options); +            } +         }); +} ); +</script> + +{% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %}  {% endblock %}
\ No newline at end of file diff --git a/ishtar_common/models.py b/ishtar_common/models.py index 59248fa43..442e67429 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -3619,7 +3619,7 @@ class Document(      _TABLE_COLS = [          "title", -        "source_type", +        "source_type__label",          "cache_related_label",          "authors__cached_label",          "associated_url", @@ -3658,6 +3658,7 @@ class Document(      COL_LABELS = {          "authors__cached_label": _("Authors"),          "complete_identifier": _("Identifier"), +        "source_type__label": _("Type")      }      CACHED_LABELS = ["cache_related_label"] diff --git a/ishtar_common/models_common.py b/ishtar_common/models_common.py index 58eac91e2..e55a21e0c 100644 --- a/ishtar_common/models_common.py +++ b/ishtar_common/models_common.py @@ -9,6 +9,7 @@ import copy  from collections import OrderedDict  import datetime  import json +import locale  import logging  import os  import pyqrcode @@ -39,7 +40,8 @@ from django.db.models.signals import post_save, post_delete, m2m_changed  from django.template.defaultfilters import slugify  from django.utils.safestring import SafeText, mark_safe  from django.utils.translation import activate, deactivate -from ishtar_common.utils import ugettext_lazy as _, pgettext_lazy, get_image_path +from ishtar_common.utils import ugettext_lazy as _, pgettext_lazy, get_image_path, \ +    human_date  from simple_history.models import HistoricalRecords as BaseHistoricalRecords  from simple_history.signals import (      post_create_historical_record, @@ -2536,6 +2538,10 @@ class DocumentItem:          ),      } +    def documents_list(self) -> list: +        Document = apps.get_model("ishtar_common", "Document") +        return self.get_associated_main_item_list("documents", Document) +      def public_representation(self):          images = []          if getattr(self, "main_image", None): @@ -3247,6 +3253,9 @@ class MainItem(ShortMenuItem):      QUICK_ACTIONS = []      SERIALIZE_EXCLUDE = ["search_vector"] +    SERIALIZE_PROPERTIES = ["external_id", "multi_polygon_geojson", "point_2d_geojson"] +    SERIALIZE_CALL = {} +    SERIALIZE_DATES = []      def full_serialize(self) -> dict:          """ @@ -3268,20 +3277,56 @@ class MainItem(ShortMenuItem):              elif field.many_to_many:                  values = getattr(self, field.name)                  if values.count(): -                    values = [str(v) for v in values] +                    values = [str(v) for v in values.all()]                  else:                      values = []                  full_result[field.name] = values              else:                  serialize_fields.append(field.name) +          result = json.loads(serialize(              "json",              [self],              fields=serialize_fields          ))          full_result.update(result[0]["fields"]) +        for prop in self.SERIALIZE_PROPERTIES: +            if prop not in full_result: +                full_result[prop] = getattr(self, prop) +        if "point_2d_geojson" in full_result: +            full_result["point_2d"] = True +        if "multi_polygon_geojson" in full_result: +            full_result["multi_polygon"] = True +        for prop in self.SERIALIZE_DATES: +            dt = getattr(self, prop) or "" +            if dt: +                dt = human_date(dt) +            full_result[prop] = dt +        for k in self.SERIALIZE_CALL: +            full_result[k] = getattr(self, self.SERIALIZE_CALL[k])() +        full_result["SLUG"] = self.SLUG +        full_result["pk"] = f"external_{self.pk}" +        full_result["id"] = f"external_{self.id}"          return full_result +    def get_associated_main_item_list(self, attr, model) -> list: +        items = getattr(self, attr) +        if not items.count(): +            return [] +        lst = [] +        table_cols = model.TABLE_COLS +        if callable(table_cols): +            table_cols = table_cols() +        for colname in table_cols: +            if colname in model.COL_LABELS: +                lst.append(str(model.COL_LABELS[colname])) +            else: +                lst.append(model._meta.get_field(colname).verbose_name) +        lst = [lst] +        for values in items.values_list(*table_cols): +            lst.append(["-" if v is None else v for v in values]) +        return lst +      @classmethod      def class_verbose_name(cls):          return cls._meta.verbose_name diff --git a/ishtar_common/templates/ishtar/blocks/window_field_flex_multiple.html b/ishtar_common/templates/ishtar/blocks/window_field_flex_multiple.html index abf03ead1..1587963c5 100644 --- a/ishtar_common/templates/ishtar/blocks/window_field_flex_multiple.html +++ b/ishtar_common/templates/ishtar/blocks/window_field_flex_multiple.html @@ -1,8 +1,8 @@ -{% load i18n %}{% if data.count %} +{% load i18n %}{% if data.count or external %}  <dl class="col-12 {% if size == 2 %}col-lg-6{% else %}col-md-6 col-lg-3{% endif %} flex-wrap">      <dt>{% trans caption %}</dt> -    <dd>{% for d in data.all %} +    <dd>{% if not external %}{% for d in data.all %}          {% if forloop.counter0 %} ; {% endif %}{{ d }} -    {% endfor %}</dd> +    {% endfor %}{% else %}{{data}}{% endif %}</dd>  </dl>  {% endif %} diff --git a/ishtar_common/templates/ishtar/blocks/window_nav.html b/ishtar_common/templates/ishtar/blocks/window_nav.html index 1d8121faf..a1ec01dcb 100644 --- a/ishtar_common/templates/ishtar/blocks/window_nav.html +++ b/ishtar_common/templates/ishtar/blocks/window_nav.html @@ -1,4 +1,5 @@  {% load i18n ishtar_helpers %} +{% if not item.is_external %}  <div class="row toolbar">    {% if current_user.is_superuser %}    {% if previous or next %} @@ -112,4 +113,7 @@  $(document).ready(function(){      register_qa_on_sheet();  }); -</script>
\ No newline at end of file +</script> +{% else %} +<h3><i class="fa fa-globe" aria-hidden="true"></i> {{item.current_source}}</h3> +{% endif %}
\ No newline at end of file diff --git a/ishtar_common/templates/ishtar/sheet.html b/ishtar_common/templates/ishtar/sheet.html index b0f82941f..4596d8ec6 100644 --- a/ishtar_common/templates/ishtar/sheet.html +++ b/ishtar_common/templates/ishtar/sheet.html @@ -79,19 +79,19 @@          var last_window='{{window_id}}';          jQuery(document).ready(function(){ -            if (! get_next_table_id({{item.pk}})){ +            if (! get_next_table_id("{{item.pk}}")){                  jQuery('.next_page').hide();              } -            if (! get_previous_table_id({{item.pk}})){ +            if (! get_previous_table_id("{{item.pk}}")){                  jQuery('.previous_page').hide();              }              jQuery(".next_page").click(function() { -                load_window("{{current_window_url}}" + get_next_table_id({{item.pk}}) + "/", +                load_window("{{current_window_url}}" + get_next_table_id("{{item.pk}}") + "/",                              '', function(){hide_window("{{window_id}}");},                              true);              });              jQuery(".previous_page").click(function() { -                load_window("{{current_window_url}}" + get_previous_table_id({{item.pk}}) + "/", +                load_window("{{current_window_url}}" + get_previous_table_id("{{item.pk}}") + "/",                              '', function(){hide_window("{{window_id}}");},                              true);              }); diff --git a/ishtar_common/templatetags/ishtar_helpers.py b/ishtar_common/templatetags/ishtar_helpers.py index ade89bdc0..250b5719d 100644 --- a/ishtar_common/templatetags/ishtar_helpers.py +++ b/ishtar_common/templatetags/ishtar_helpers.py @@ -25,10 +25,16 @@ def safe_or(item, args):          result = True          current_item = item          for sub in arg.split("."): -            if not hasattr(current_item, sub) or not getattr(current_item, sub): -                result = False -                break -            current_item = getattr(current_item, sub) +            if isinstance(current_item, dict): +                if sub not in current_item: +                    result = False +                    break +                current_item = current_item[sub] +            else: +                if not hasattr(current_item, sub) or not getattr(current_item, sub): +                    result = False +                    break +                current_item = getattr(current_item, sub)          if result:              return True diff --git a/ishtar_common/templatetags/window_field.py b/ishtar_common/templatetags/window_field.py index b21f2b9e6..cffa2a54f 100644 --- a/ishtar_common/templatetags/window_field.py +++ b/ishtar_common/templatetags/window_field.py @@ -98,12 +98,20 @@ def field_multiple(caption, data, li=False, size=None):  @register.simple_tag  def field_multiple_obj(caption, item, attr, li=False, size=None): -    data = getattr(item, attr) if hasattr(item, attr) else "" +    data, external = "", False +    if isinstance(item, dict): +        if attr in item: +            data = " ; ".join(item[attr]) +            if data: +                external = True +    else: +        data = getattr(item, attr) if hasattr(item, attr) else ""      if not hasattr(item, '_step') or attr not in item.history_m2m \              or not item.history_m2m[attr]:          t = loader.get_template('ishtar/blocks/window_field_flex_multiple.html')          return t.render( -            {'caption': caption, 'data': data, 'li': li, "size": size} +            {'caption': caption, 'data': data, 'li': li, "size": size, +             "external": external}          )      field = getattr(item.instance.__class__, attr)      if hasattr(field, "remote_field"): diff --git a/ishtar_common/utils.py b/ishtar_common/utils.py index 3a349c04b..80d5af9d6 100644 --- a/ishtar_common/utils.py +++ b/ishtar_common/utils.py @@ -27,6 +27,7 @@ import hashlib  from importlib import import_module  import io  from jinja2 import Template +import locale  import os  import random  import re @@ -2111,6 +2112,18 @@ def get_image_path(instance, filename):      return instance._get_image_path(filename) +def human_date(value): +    language_code = settings.LANGUAGE_CODE.split("-") +    language_code = language_code[0] + "_" + language_code[1].upper() +    for language_suffix in (".utf8", ""): +        try: +            locale.setlocale(locale.LC_TIME, language_code + language_suffix) +            break +        except locale.Error: +            pass +    return value.strftime(settings.DATE_FORMAT) + +  class IshtarFileSystemStorage(FileSystemStorage):      def exists(self, name):          path_name = self.path(name) diff --git a/ishtar_common/views_item.py b/ishtar_common/views_item.py index 7ddfa179a..468312046 100644 --- a/ishtar_common/views_item.py +++ b/ishtar_common/views_item.py @@ -33,7 +33,7 @@ from django.db.models.functions import ExtractYear  from django.db.utils import ProgrammingError  from django import forms  from django.forms.models import model_to_dict -from django.http import HttpResponse +from django.http import HttpResponse, Http404  from django.shortcuts import render  from django.template import loader  from django.urls import reverse, NoReverseMatch @@ -283,6 +283,52 @@ def display_item(model, extra_dct=None, show_url=None):      return func +def show_source_item(request, source_id, model, name, base_dct, extra_dct): +    try: +        __, source_id, external_id = source_id.split("-") +        source_id, external_id = int(source_id), int(external_id) +    except ValueError: +        raise Http404() +    models_rest.ApiExternalSource.objects.get() +    # TODO: check permissions +    try: +        src = models_rest.ApiExternalSource.objects.get( +            pk=source_id) +    except models_rest.ApiExternalSource.DoesNotExist: +        return HttpResponse("{}", content_type="text/plain") +    url = src.url +    if not url.endswith("/"): +        url += "/" +    url += f"api/get/{model.SLUG}/{external_id}/" +    try: +        response = requests.get( +            url, +            timeout=20, +            headers={"Authorization": f"Token {src.key}"}, +        ) +    except requests.exceptions.Timeout: +        return HttpResponse("{}", content_type="text/plain") +    item = response.json() +    dct = deepcopy(base_dct) +    if not item: +        return HttpResponse("{}", content_type="text/plain") +    item["is_external"] = True +    item["current_source"] = src.name +    dct["item"], dct["item_name"] = item, name +    dct["is_external"] = True + +    if extra_dct: +        dct.update(extra_dct(request, dct)) + +    permissions = ["permission_view_document"] +    for p in permissions: +        dct[p] = True + +    tpl = loader.get_template(f"ishtar/sheet_{name}_window.html") +    content = tpl.render(dct, request) +    return HttpResponse(content, content_type="application/xhtml") + +  def show_item(model, name, extra_dct=None, model_for_perms=None):      def func(request, pk, **dct):          check_model = model @@ -298,10 +344,6 @@ def show_item(model, name, extra_dct=None, model_for_perms=None):              query_own = model.get_query_owns(request.user.ishtaruser)              if query_own:                  q = q.filter(query_own).distinct() -        try: -            item = q.get(pk=pk) -        except (ObjectDoesNotExist, ValueError): -            return HttpResponse("")          doc_type = "type" in dct and dct.pop("type")          url_name = (              "/".join(reverse("show-" + name, args=["0", ""]).split("/")[:-2]) + "/" @@ -315,12 +357,15 @@ def show_item(model, name, extra_dct=None, model_for_perms=None):          date = None          if "date" in dct:              date = dct.pop("date") -        dct["sheet_id"] = "%s-%d" % (name, item.pk) -        dct["window_id"] = "%s-%d-%s" % ( -            name, -            item.pk, -            datetime.datetime.now().strftime("%M%s"), -        ) +        dct["sheet_id"] = f"{name}-{pk}" +        dct["window_id"] = f"{name}-{pk}-{datetime.datetime.now().strftime('%M%s')}" +        if pk.startswith("source-"): +            return show_source_item( +                request, pk, model, name, dct, extra_dct) +        try: +            item = q.get(pk=pk) +        except (ObjectDoesNotExist, ValueError): +            return HttpResponse("")          # list current perms          if hasattr(request.user, "ishtaruser") and request.user.ishtaruser: @@ -2359,7 +2404,7 @@ def get_distant_item(      try:          src = models_rest.ApiExternalSource.objects.get(              pk=external_source_id) -    except models_rest.ApiExternalSource.DoesNotExist: +    except (models_rest.ApiExternalSource.DoesNotExist, ValueError):          return HttpResponse("{}", content_type="text/plain")      url = src.url      url += reverse(f"api-search-{model}") @@ -2377,6 +2422,18 @@ def get_distant_item(          )      except requests.exceptions.Timeout:          return HttpResponse("{}", content_type="text/plain") -    return HttpResponse(json.dumps(response.json()), content_type="application/json") +    dct = response.json() +    if "rows" in dct: +        for row in dct["rows"]: +            if "id" in row: +                try: +                    idx = int(row['id']) +                except ValueError: +                    continue +                source_id = f"source-{external_source_id}-{idx}" +                row["id"] = source_id +                if "link" in row: +                    row["link"] = row["link"].replace(str(idx), source_id) +    return HttpResponse(json.dumps(dct), content_type="application/json") | 
