From a060b26ee54f8f2e95ad812faff3ef0511accf3e Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Tue, 10 Jan 2023 13:08:48 +0100 Subject: Syndication - export external sources --- archaeological_context_records/urls.py | 5 ++ archaeological_context_records/views_api.py | 6 +- archaeological_files/urls.py | 5 ++ archaeological_files/views_api.py | 6 +- archaeological_finds/urls.py | 5 ++ archaeological_finds/views_api.py | 6 +- archaeological_operations/tests.py | 2 +- archaeological_operations/urls.py | 10 +++ archaeological_operations/views_api.py | 10 ++- archaeological_warehouse/urls.py | 18 ++++- archaeological_warehouse/views_api.py | 10 ++- ishtar_common/admin.py | 41 +++++++++- .../migrations/0222_auto_20230111_1857.py | 94 ++++++++++++++++++++++ ishtar_common/models_common.py | 16 +++- ishtar_common/models_imports.py | 18 +++++ ishtar_common/models_rest.py | 61 ++++++++++++++ ishtar_common/rest.py | 84 +++++++++++++++++-- ishtar_common/static/js/ishtar.js | 10 ++- .../templates/blocks/DataTables-content.html | 21 ++--- .../blocks/DataTables-external-sources.html | 2 +- .../templates/blocks/DataTables-stats.html | 2 +- .../templates/blocks/DataTables-tabs.html | 4 +- ishtar_common/templates/blocks/DataTables.html | 84 ++++++++++++------- ishtar_common/urls.py | 21 +++-- ishtar_common/utils.py | 48 +++++++++++ ishtar_common/views.py | 69 +++------------- ishtar_common/views_item.py | 51 ++++++++++++ ishtar_common/widgets.py | 20 ++--- ishtar_common/wizards.py | 24 +++++- 29 files changed, 602 insertions(+), 151 deletions(-) create mode 100644 ishtar_common/migrations/0222_auto_20230111_1857.py diff --git a/archaeological_context_records/urls.py b/archaeological_context_records/urls.py index fd267fc6a..446b3fbf9 100644 --- a/archaeological_context_records/urls.py +++ b/archaeological_context_records/urls.py @@ -192,6 +192,11 @@ urlpatterns = [ r"api/search/context-record/$", views_api.SearchContextRecordAPI.as_view(), name="api-search-contextrecord" ), + path( + "api/export/contextrecord//", + views_api.ExportContextRecordAPI.as_view(), + name="api-export-contextrecord" + ), path( "api/get/contextrecord//", views_api.GetContextRecordAPI.as_view(), name="api-get-contextrecord" diff --git a/archaeological_context_records/views_api.py b/archaeological_context_records/views_api.py index 8a5d8fbcd..db379a3b4 100644 --- a/archaeological_context_records/views_api.py +++ b/archaeological_context_records/views_api.py @@ -1,4 +1,4 @@ -from ishtar_common.rest import SearchAPIView, FacetAPIView, GetAPIView +from ishtar_common.rest import SearchAPIView, FacetAPIView, GetAPIView, ExportAPIView from archaeological_context_records import models, forms @@ -11,5 +11,9 @@ class SearchContextRecordAPI(SearchAPIView): model = models.ContextRecord +class ExportContextRecordAPI(ExportAPIView): + model = models.ContextRecord + + class GetContextRecordAPI(GetAPIView): model = models.ContextRecord diff --git a/archaeological_files/urls.py b/archaeological_files/urls.py index 0d1ae7632..0be30874b 100644 --- a/archaeological_files/urls.py +++ b/archaeological_files/urls.py @@ -188,6 +188,11 @@ urlpatterns = [ r"api/search/file/$", views_api.SearchFileAPI.as_view(), name="api-search-file" ), + path( + "api/export/file//", + views_api.ExportFileAPI.as_view(), + name="api-export-file" + ), path( "api/get/file//", views_api.GetFileAPI.as_view(), name="api-get-file" diff --git a/archaeological_files/views_api.py b/archaeological_files/views_api.py index b12634353..2bbfba521 100644 --- a/archaeological_files/views_api.py +++ b/archaeological_files/views_api.py @@ -1,4 +1,4 @@ -from ishtar_common.rest import SearchAPIView, FacetAPIView, GetAPIView +from ishtar_common.rest import SearchAPIView, FacetAPIView, GetAPIView, ExportAPIView from archaeological_files import models, forms @@ -11,5 +11,9 @@ class SearchFileAPI(SearchAPIView): model = models.File +class ExportFileAPI(ExportAPIView): + model = models.File + + class GetFileAPI(GetAPIView): model = models.File diff --git a/archaeological_finds/urls.py b/archaeological_finds/urls.py index 5e90afd55..fd98667d5 100644 --- a/archaeological_finds/urls.py +++ b/archaeological_finds/urls.py @@ -618,6 +618,11 @@ urlpatterns = [ r"api/search/find/$", views_api.SearchFindAPI.as_view(), name="api-search-find" ), + path( + "api/export/find//", + views_api.ExportFindAPI.as_view(), + name="api-export-find" + ), path( "api/get/find//", views_api.GetFindAPI.as_view(), name="api-get-find" diff --git a/archaeological_finds/views_api.py b/archaeological_finds/views_api.py index 64831de57..4302fbd89 100644 --- a/archaeological_finds/views_api.py +++ b/archaeological_finds/views_api.py @@ -2,7 +2,7 @@ from rest_framework import authentication, permissions from rest_framework.views import APIView from rest_framework.response import Response -from ishtar_common.rest import SearchAPIView, FacetAPIView, GetAPIView +from ishtar_common.rest import SearchAPIView, FacetAPIView, GetAPIView, ExportAPIView from ishtar_common.serializers import PublicSerializer from archaeological_finds import models, forms @@ -50,5 +50,9 @@ class SearchFindAPI(SearchAPIView): model = models.Find +class ExportFindAPI(ExportAPIView): + model = models.Find + + class GetFindAPI(GetAPIView): model = models.Find diff --git a/archaeological_operations/tests.py b/archaeological_operations/tests.py index b9c119826..04af3445a 100644 --- a/archaeological_operations/tests.py +++ b/archaeological_operations/tests.py @@ -4794,7 +4794,7 @@ class ApiTest(OperationInitTest, APITestCase): local_label="Néolithique moyen", ) - base_url = reverse("search-external", args=["operation", source.pk]) + base_url = reverse("external-search", args=["operation", source.pk]) params = self.__construct_search(source) url = base_url + params self._mock_search(mock_get, models.Operation, "/get-operation/" + params) diff --git a/archaeological_operations/urls.py b/archaeological_operations/urls.py index 6c6a2e21c..e4965d171 100644 --- a/archaeological_operations/urls.py +++ b/archaeological_operations/urls.py @@ -372,6 +372,16 @@ urlpatterns = [ r"api/search/archaeologicalsite/$", views_api.SearchSiteAPI.as_view(), name="api-search-archaeologicalsite" ), + path( + "api/export/operation//", + views_api.ExportOperationAPI.as_view(), + name="api-export-operation" + ), + path( + "api/export/archaeologicalsite//", + views_api.ExportSiteAPI.as_view(), + name="api-export-archaeologicalsite" + ), path( "api/get/operation//", views_api.GetOperationAPI.as_view(), name="api-get-operation" diff --git a/archaeological_operations/views_api.py b/archaeological_operations/views_api.py index b1d4cfb51..b6a17c837 100644 --- a/archaeological_operations/views_api.py +++ b/archaeological_operations/views_api.py @@ -1,4 +1,4 @@ -from ishtar_common.rest import SearchAPIView, FacetAPIView, GetAPIView +from ishtar_common.rest import SearchAPIView, FacetAPIView, GetAPIView, ExportAPIView from archaeological_operations import models, forms @@ -15,6 +15,14 @@ class SearchSiteAPI(SearchAPIView): model = models.ArchaeologicalSite +class ExportOperationAPI(ExportAPIView): + model = models.Operation + + +class ExportSiteAPI(ExportAPIView): + model = models.ArchaeologicalSite + + class GetOperationAPI(GetAPIView): model = models.Operation diff --git a/archaeological_warehouse/urls.py b/archaeological_warehouse/urls.py index 870008de4..0e032ff80 100644 --- a/archaeological_warehouse/urls.py +++ b/archaeological_warehouse/urls.py @@ -249,14 +249,24 @@ urlpatterns = [ r"api/search/warehouse/$", views_api.SearchWarehouseAPI.as_view(), name="api-search-warehouse" ), - path( - "api/get/warehouse//", views_api.GetWarehouseAPI.as_view(), - name="api-get-warehouse" - ), url( r"api/search/container/$", views_api.SearchContainerAPI.as_view(), name="api-search-container" ), + path( + "api/export/warehouse//", + views_api.ExportWarehouseAPI.as_view(), + name="api-export-warehouse" + ), + path( + "api/export/container//", + views_api.ExportContainerAPI.as_view(), + name="api-export-container" + ), + path( + "api/get/warehouse//", views_api.GetWarehouseAPI.as_view(), + name="api-get-warehouse" + ), path( "api/get/container//", views_api.GetContainerAPI.as_view(), name="api-get-container" diff --git a/archaeological_warehouse/views_api.py b/archaeological_warehouse/views_api.py index 468fe08c2..f26da20c2 100644 --- a/archaeological_warehouse/views_api.py +++ b/archaeological_warehouse/views_api.py @@ -1,4 +1,4 @@ -from ishtar_common.rest import SearchAPIView, FacetAPIView, GetAPIView +from ishtar_common.rest import SearchAPIView, FacetAPIView, GetAPIView, ExportAPIView from archaeological_warehouse import models, forms @@ -15,6 +15,14 @@ class SearchContainerAPI(SearchAPIView): model = models.Container +class ExportWarehouseAPI(ExportAPIView): + model = models.Warehouse + + +class ExportContainerAPI(ExportAPIView): + model = models.Container + + class GetWarehouseAPI(GetAPIView): model = models.Warehouse diff --git a/ishtar_common/admin.py b/ishtar_common/admin.py index 4da0a15ad..f0f795665 100644 --- a/ishtar_common/admin.py +++ b/ishtar_common/admin.py @@ -2257,13 +2257,25 @@ def get_api_choices(): class ApiSearchModelAdminForm(forms.ModelForm): class Meta: model = models_rest.ApiUser - exclude = [] + exclude = ["table_format"] content_type = forms.ModelChoiceField( label=_("Content type"), queryset=ContentType.objects, limit_choices_to=get_api_choices ) + def __init__(self, *args, **kwargs): + super(ApiSearchModelAdminForm, self).__init__(*args, **kwargs) + if self.instance.id: + model_name = self.instance.content_type.model.replace("_", "") + model_name = f"{self.instance.content_type.app_label}.models."\ + f"{model_name}" + q = models.ImporterType.objects.filter( + associated_models__klass__iexact=model_name + ) + # self.fields['table_format'].queryset = q + self.fields['export'].queryset = q + class ApiSearchModelAdmin(admin.ModelAdmin): form = ApiSearchModelAdminForm @@ -2300,7 +2312,8 @@ def update_types_from_source(modeladmin, request, queryset): source = queryset.all()[0] created, updated, deleted = 0, 0, 0 missing_models, missing_types = [], [] - for item_type in ("operation",): + config = {} + for item_type in ("operation", "contextrecord", "file", "find", "warehouse"): curl = source.url if not curl.endswith("/"): curl += "/" @@ -2335,6 +2348,17 @@ def update_types_from_source(modeladmin, request, queryset): str(_("Response of {} is not a valid JSON message.")).format(curl), ) continue + + if "config" in content: + for k in content["config"]: + if not content["config"][k]: + continue + if k not in config: + config[k] = "" + elif config[k]: + config[k] += "||" + config[k] += content["config"][k] + result = source.update_matches(content) if result.get("created", None): created += result['created'] @@ -2346,6 +2370,18 @@ def update_types_from_source(modeladmin, request, queryset): missing_models += result["search_model do not exist"] if result.get("type do not exist", None): missing_types += result["type do not exist"] + modified = False + for k in config: + if getattr(source, k) != config[k]: + modified = True + setattr(source, k, config[k]) + if modified: + source.save() + messages.add_message( + request, + messages.INFO, + str(_("Table/exports configuration updated")), + ) if created: messages.add_message( request, @@ -2485,6 +2521,7 @@ class ApiExternalSourceAdmin(admin.ModelAdmin): generate_match_document, update_association_from_match_document, ] + exclude = ("search_columns", "search_columns_label", "exports", "exports_label") autocomplete_fields = ["users"] list_display = ("name", "url", "key") diff --git a/ishtar_common/migrations/0222_auto_20230111_1857.py b/ishtar_common/migrations/0222_auto_20230111_1857.py new file mode 100644 index 000000000..e03a4ccf0 --- /dev/null +++ b/ishtar_common/migrations/0222_auto_20230111_1857.py @@ -0,0 +1,94 @@ +# Generated by Django 2.2.24 on 2023-01-11 18:57 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ishtar_common', '0221_auto_20220823_1935'), + ] + + operations = [ + migrations.AlterModelOptions( + name='spatialreferencesystem', + options={'ordering': ('order', 'label'), 'verbose_name': 'Geographic - Spatial reference system', 'verbose_name_plural': 'Geographic - Spatial reference systems'}, + ), + migrations.AddField( + model_name='apiexternalsource', + name='exports', + field=models.TextField(default='', verbose_name='Exports slug'), + ), + migrations.AddField( + model_name='apiexternalsource', + name='exports_label', + field=models.TextField(default='', verbose_name='Exports label'), + ), + migrations.AddField( + model_name='apiexternalsource', + name='search_columns', + field=models.TextField(default='', verbose_name='Search columns'), + ), + migrations.AddField( + model_name='apiexternalsource', + name='search_columns_label', + field=models.TextField(default='', verbose_name='Search columns label'), + ), + migrations.AddField( + model_name='apisearchmodel', + name='export', + field=models.ManyToManyField(blank=True, related_name='search_model_exports', to='ishtar_common.ImporterType', verbose_name='Export'), + ), + migrations.AddField( + model_name='apisearchmodel', + name='table_format', + field=models.ForeignKey(help_text='Not used. Set it when table columns will be set by importer.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='search_model_table_format', to='ishtar_common.ImporterType', verbose_name='Table formats'), + ), + migrations.AlterField( + model_name='apiexternalsource', + name='users', + field=models.ManyToManyField(blank=True, to='ishtar_common.IshtarUser', verbose_name='Users'), + ), + migrations.AlterField( + model_name='apisearchmodel', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ishtar_common.ApiUser', verbose_name='User'), + ), + migrations.AlterField( + model_name='apiuser', + name='user_ptr', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='apiuser', serialize=False, to=settings.AUTH_USER_MODEL, verbose_name='User'), + ), + migrations.AlterField( + model_name='import', + name='skip_lines', + field=models.IntegerField(default=1, help_text='Number of header lines in your file (can be 0 and should be 0 for geopackage or Shapefile).', verbose_name='Skip lines'), + ), + migrations.AlterField( + model_name='import', + name='state', + field=models.CharField(choices=[('C', 'Created'), ('AP', 'Analyse in progress'), ('A', 'Analysed'), ('HQ', 'Check modified in queue'), ('IQ', 'Import in queue'), ('HP', 'Check modified in progress'), ('IP', 'Import in progress'), ('PP', 'Post-processing in progress'), ('PI', 'Partially imported'), ('FE', 'Finished with errors'), ('F', 'Finished'), ('AC', 'Archived')], default='C', max_length=2, verbose_name='State'), + ), + migrations.AlterField( + model_name='importertype', + name='unicity_keys', + field=models.CharField(blank=True, help_text='Mandatory for update importer. Set to key that identify items without ambiguity. Warning: __ is not supported, only use level 1 key.', max_length=500, null=True, verbose_name='Unicity keys (separator ";")'), + ), + migrations.AlterField( + model_name='town', + name='geodata', + field=models.ManyToManyField(blank=True, related_name='related_items_ishtar_common_town', to='ishtar_common.GeoVectorData', verbose_name='Geodata'), + ), + migrations.AlterField( + model_name='town', + name='main_geodata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_related_items_ishtar_common_town', to='ishtar_common.GeoVectorData', verbose_name='Main geodata'), + ), + migrations.AlterField( + model_name='valueformater', + name='format_string', + field=models.CharField(help_text='A string used to format a value using the Python "format()" method. The site https://pyformat.info/ provide good examples of usage. Only one "{}" entry is managed. Instead you can use "{item}". The input is assumed to be a string.', max_length=100, verbose_name='Format string'), + ), + ] diff --git a/ishtar_common/models_common.py b/ishtar_common/models_common.py index 5e6e3af8f..151ab5650 100644 --- a/ishtar_common/models_common.py +++ b/ishtar_common/models_common.py @@ -37,7 +37,7 @@ from django.core.serializers import serialize from django.urls import reverse, NoReverseMatch from django.core.validators import validate_slug from django.db import connection, transaction, OperationalError, IntegrityError -from django.db.models import Q, Count, Max +from django.db.models import Q, Count, Max, fields from django.db.models.signals import post_save, post_delete, m2m_changed from django.template import loader from django.template.defaultfilters import slugify @@ -47,8 +47,9 @@ from ishtar_common.utils import ( ugettext_lazy as _, pgettext_lazy, get_image_path, + get_columns_from_class, human_date, - reverse_list_coordinates + reverse_list_coordinates, ) from simple_history.models import HistoricalRecords as BaseHistoricalRecords from simple_history.signals import ( @@ -4249,6 +4250,17 @@ class MainItem(ShortMenuItem, SerializeItem): def class_verbose_name(cls): return cls._meta.verbose_name + @classmethod + def get_columns(cls, table_cols_attr="TABLE_COLS", dict_col_labels=True): + """ + :param table_cols_attr: "TABLE_COLS" if not specified + :param dict_col_labels: (default: True) if set to False return list matching + with table_cols list + :return: (table_cols, table_col_labels) + """ + return get_columns_from_class(cls, table_cols_attr=table_cols_attr, + dict_col_labels=dict_col_labels) + def get_search_url(self): if self.SLUG: return reverse(self.SLUG + "_search") diff --git a/ishtar_common/models_imports.py b/ishtar_common/models_imports.py index 6358f070a..d4f8b5698 100644 --- a/ishtar_common/models_imports.py +++ b/ishtar_common/models_imports.py @@ -353,6 +353,24 @@ class ImporterType(models.Model): newclass = type(name, (Importer,), args) return newclass + def get_columns(self, importer_class=None): + """ + :param importer_class: importer class - if not provided get from self + :return: (columns: list, columns_names: list) - column attributes, column labels + """ + if not importer_class: + importer_class = self.get_importer_class() + cols, col_names = [], [] + for formater in importer_class.LINE_EXPORT_FORMAT: + if not formater: + cols.append("") + col_names.append("") + continue + cols.append(formater.export_field_name) + col_names.append(formater.label) + return cols, col_names + + def save(self, *args, **kwargs): if not self.slug: self.slug = create_slug(ImporterType, self.name) diff --git a/ishtar_common/models_rest.py b/ishtar_common/models_rest.py index e0a0ba97e..7a6c79a54 100644 --- a/ishtar_common/models_rest.py +++ b/ishtar_common/models_rest.py @@ -65,6 +65,14 @@ class ApiSearchModel(models.Model): null=True, help_text=_("Search query add to each request"), ) + table_format = models.ForeignKey( + "ishtar_common.ImporterType", on_delete=models.PROTECT, null=True, + verbose_name=_("Table formats"), related_name=_("search_model_table_format"), + help_text=_("Not used. Set it when table columns will be set by importer.") + ) + export = models.ManyToManyField( + "ishtar_common.ImporterType", blank=True, verbose_name=_("Export"), + related_name=_("search_model_exports")) class Meta: verbose_name = _("API - Remote access - Search model") @@ -96,6 +104,10 @@ class ApiExternalSource(models.Model): url = models.URLField(verbose_name=_("URL")) name = models.CharField(verbose_name=_("Name"), max_length=200) key = models.CharField(_("Key"), max_length=40) + search_columns = models.TextField(_("Search columns"), default="") + search_columns_label = models.TextField(_("Search columns label"), default="") + exports = models.TextField(_("Exports slug"), default="") + exports_label = models.TextField(_("Exports label"), default="") users = models.ManyToManyField("IshtarUser", blank=True, verbose_name=_("Users")) match_document = models.FileField( _("Match document"), @@ -117,6 +129,53 @@ class ApiExternalSource(models.Model): def __str__(self): return self.name + def get_columns(self, model_name): + """ + Column keys for table display + :return: (key1:str, key2:str, ...) - list of column key + """ + if not self.search_columns: + return [] + model_name += "-" + return [ + k[len(model_name):] + for k in self.search_columns.split("||") if k.startswith(model_name) + ] + + def get_column_labels(self, model_name): + """ + Column label for table display + :return: (label1:str, label2:str, ...) - list of column labels + """ + if not self.search_columns_label: + return [] + model_name += "-" + return [ + k[len(model_name):] + for k in self.search_columns_label.split("||") if k.startswith(model_name) + ] + + def get_exports(self, model_name): + """ + Get export list + :return: [(slug:slug, label:str)] - list of export slug and labels + """ + if not self.exports or not self.exports_label: + return [] + model_name += "-" + exports = [ + k[len(model_name):] + for k in self.exports.split("||") if k.startswith(model_name) + ] + exports_label = [ + k[len(model_name):] + for k in self.exports_label.split("||") if k.startswith(model_name) + ] + result = [] + for idx in range(min([len(exports), len(exports_label)])): + result.append((exports[idx], exports_label[idx])) + return result + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): super().save(force_insert, force_update, using, update_fields) @@ -137,6 +196,8 @@ class ApiExternalSource(models.Model): "type do not exist": [], } for search_model in content: + if search_model == "config": + continue app, model_name = search_model.split(".") try: ct = ContentType.objects.get(app_label=app, model=model_name) diff --git a/ishtar_common/rest.py b/ishtar_common/rest.py index f85061b69..e489e5f80 100644 --- a/ishtar_common/rest.py +++ b/ishtar_common/rest.py @@ -1,7 +1,10 @@ -import json +import datetime +import requests from django.conf import settings from django.db.models import Q +from django.http import HttpResponse +from django.shortcuts import reverse from django.utils.translation import activate, deactivate from rest_framework import authentication, permissions, generics @@ -10,8 +13,8 @@ from rest_framework.views import APIView from ishtar_common import models_rest from ishtar_common.models_common import GeneralType +from ishtar_common.models_imports import Importer from ishtar_common.views_item import get_item -from ishtar_common.serializers_utils import generic_get_results class IpModelPermission(permissions.BasePermission): @@ -39,15 +42,24 @@ class SearchAPIView(APIView): content_type__model=self.model._meta.model_name, ) - def get(self, request, format=None): + def get(self, request, slug=None, format=None, data_type="json"): + search_model = self.search_model_query(request).all()[0] + # get default importer + if slug: + q = search_model.export.filter(slug=slug) + if not q.count(): + return HttpResponse('Page not found', status=404) + table_cols, col_names = q.all()[0].get_columns() + else: + table_cols, col_names = self.model.get_columns(dict_col_labels=False) + _get_item = get_item( self.model, "get_" + self.model.SLUG, self.model.SLUG, - no_permission_check=True - # TODO: own_table_cols=get_table_cols_for_ope() - adapt columns + no_permission_check=True, + own_table_cols=table_cols ) - search_model = self.search_model_query(request).all()[0] if search_model.limit_query: query = search_model.limit_query if request.GET.get("search_vector", None): @@ -55,13 +67,30 @@ class SearchAPIView(APIView): request.GET._mutable = True request.GET["search_vector"] = query request.GET._mutable = False - data_type = "json" + + # source-1-689-source-1-1853 -> 689-1853 + if "selected_ids" in request.GET: + value = request.GET["selected_ids"] + values = [] + for idx, k in enumerate(value.split("-")): + if idx % 3 == 2: + values.append(k) + request.GET._mutable = True + request.GET["selected_ids"] = "-".join(values) + request.GET._mutable = False + if request.GET.get("data_type", None): data_type = request.GET.get("data_type") - response = _get_item(request, data_type=data_type) + response = _get_item(request, col_names=col_names, data_type=data_type, + type=data_type) return response +class ExportAPIView(SearchAPIView): + def get(self, request, slug=None, format=None, data_type="csv"): + return super().get(request, slug=slug, format=format, data_type=data_type) + + class FacetAPIView(APIView): authentication_classes = (authentication.TokenAuthentication,) permission_classes = (permissions.IsAuthenticated, IpModelPermission) @@ -78,6 +107,45 @@ class FacetAPIView(APIView): def get(self, request, format=None): values = {} base_queries = self._get_base_search_model_queries() + + # config + values["config"] = { + "search_columns": "", + "search_columns_label": "", + "exports": "", + "exports_label": "" + } + for model in self.models: + model_name = f"{model._meta.app_label}-{model._meta.model_name}-" + q = models_rest.ApiSearchModel.objects.filter( + user=request.user.apiuser, + content_type__app_label=model._meta.app_label, + content_type__model=model._meta.model_name + ) + if not q.count(): + continue + search_model = q.all()[0] + # cols, col_names = search_model.table_format.get_columns() + cols, col_names = model.get_columns(dict_col_labels=False) + cols, col_names = [c for c in cols if c], [c for c in col_names if c] + if cols and col_names: + if values["config"]["search_columns"]: + values["config"]["search_columns"] += "||" + values["config"]["search_columns_label"] += "||" + values["config"]["search_columns"] += "||".join([ + model_name + (col[0] if isinstance(col, list) else col) + for col in cols + ]) + values["config"]["search_columns_label"] += "||".join([ + model_name + col for col in col_names + ]) + for export in search_model.export.all(): + if values["config"]["exports"]: + values["config"]["exports"] += "||" + values["config"]["exports_label"] += "||" + values["config"]["exports"] += model_name + export.slug + values["config"]["exports_label"] += model_name + export.name + for idx, select_form in enumerate(self.select_forms): # only send types matching permissions model, q = base_queries[idx] diff --git a/ishtar_common/static/js/ishtar.js b/ishtar_common/static/js/ishtar.js index 3df8fe8f0..56f4e157d 100644 --- a/ishtar_common/static/js/ishtar.js +++ b/ishtar_common/static/js/ishtar.js @@ -1128,7 +1128,7 @@ var qa_action_register = function(url, slug) { }; -var update_export_urls = function(dt, sname, source, source_full, extra_sources, extra_tpl){ +var update_export_urls = function(dt, source_cls, sname, source, source_full, extra_sources, extra_tpl){ let rows = dt.rows( { selected: true } ).data(); let data = "selected_ids="; for (k in rows){ @@ -1136,11 +1136,17 @@ var update_export_urls = function(dt, sname, source, source_full, extra_sources, if (k > 0) data += "-"; data += rows[k]['id']; } - let csv_url = source + "csv?submited=1&" + data; + let extra = "?submited=1&" + data; + let csv_url = source + "csv" + extra; $("." + sname + "-csv").attr("href", csv_url); let csv_full_url = source_full + "csv?submited=1&" + data; $("." + sname + "-csv-full").attr("href", csv_full_url); + $("." + source_cls + " ." + sname + "-csv-external").each(function(){ + let url = $(this).attr("href").split('?')[0] + extra; + $(this).attr("href", url); + }); + for (k in extra_sources){ let src = extra_sources[k]; let slug = src[0]; diff --git a/ishtar_common/templates/blocks/DataTables-content.html b/ishtar_common/templates/blocks/DataTables-content.html index 029ebd84c..0665e7a4e 100644 --- a/ishtar_common/templates/blocks/DataTables-content.html +++ b/ishtar_common/templates/blocks/DataTables-content.html @@ -22,11 +22,11 @@
- +