diff options
author | Étienne Loks <etienne.loks@iggdrasil.net> | 2023-01-10 13:08:48 +0100 |
---|---|---|
committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2023-01-12 09:50:56 +0100 |
commit | 061edbaac5a37b8e717391c3b7d804a96c041979 (patch) | |
tree | 79a9647f9e8563d44d08123dd4844a249001e63a | |
parent | ac839d9481b863c93a9c0f92011118bb581f7c2d (diff) | |
download | Ishtar-061edbaac5a37b8e717391c3b7d804a96c041979.tar.bz2 Ishtar-061edbaac5a37b8e717391c3b7d804a96c041979.zip |
Syndication - export external sources
29 files changed, 602 insertions, 151 deletions
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 @@ -193,6 +193,11 @@ urlpatterns = [ name="api-search-contextrecord" ), path( + "api/export/contextrecord/<slug:slug>/", + views_api.ExportContextRecordAPI.as_view(), + name="api-export-contextrecord" + ), + path( "api/get/contextrecord/<int:pk>/", 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 @@ -189,6 +189,11 @@ urlpatterns = [ name="api-search-file" ), path( + "api/export/file/<slug:slug>/", + views_api.ExportFileAPI.as_view(), + name="api-export-file" + ), + path( "api/get/file/<int:pk>/", 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 @@ -619,6 +619,11 @@ urlpatterns = [ name="api-search-find" ), path( + "api/export/find/<slug:slug>/", + views_api.ExportFindAPI.as_view(), + name="api-export-find" + ), + path( "api/get/find/<int:pk>/", 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 @@ -373,6 +373,16 @@ urlpatterns = [ name="api-search-archaeologicalsite" ), path( + "api/export/operation/<slug:slug>/", + views_api.ExportOperationAPI.as_view(), + name="api-export-operation" + ), + path( + "api/export/archaeologicalsite/<slug:slug>/", + views_api.ExportSiteAPI.as_view(), + name="api-export-archaeologicalsite" + ), + path( "api/get/operation/<int:pk>/", 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,15 +249,25 @@ urlpatterns = [ r"api/search/warehouse/$", views_api.SearchWarehouseAPI.as_view(), name="api-search-warehouse" ), - path( - "api/get/warehouse/<int:pk>/", 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/<slug:slug>/", + views_api.ExportWarehouseAPI.as_view(), + name="api-export-warehouse" + ), + path( + "api/export/container/<slug:slug>/", + views_api.ExportContainerAPI.as_view(), + name="api-export-container" + ), + path( + "api/get/warehouse/<int:pk>/", views_api.GetWarehouseAPI.as_view(), + name="api-get-warehouse" + ), + path( "api/get/container/<int:pk>/", 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 @@ </div> <div class="col"> <div class="btn-group btn-group-sm" role="group"> - <button id="export-{{name}}" type="button" - class="btn btn-secondary dropdown-toggle table-export" data-toggle="dropdown" - aria-haspopup="true" aria-expanded="false"> + <button id="export-{{name}}" type="button" + class="btn btn-secondary dropdown-toggle table-export" data-toggle="dropdown" + aria-haspopup="true" aria-expanded="false"> {% trans 'Export'%} - </button> + </button> <div class="dropdown-menu" aria-labelledby="export-{{name}}"> {% if source_full or extra_sources %} <a class="dropdown-item {{sname}}-csv" href='{{source}}csv' target='_blank' @@ -40,7 +40,7 @@ {% endif %} </div> <div class="input-group-append"> - <div class="selected-lines input-group-text" id="btnGroupAddon"> + <div class="selected-lines input-group-text"> <span class="sl-whole">{% trans "whole table" %}</span> <span class="sl-selected"><span class="sl-number"></span> {% trans "selected item(s)" %}</span> </div> @@ -70,13 +70,13 @@ <input type="hidden" id="hidden_{{name}}" name="{{name}}"/> - {% for source_id, source_label, source_url in external_sources %} + {% for source_id, source_label, source_url, columns, column_labels, export_urls in external_sources %} <div id="grid_{{name}}_{{source_id}}_meta_wrapper" class="sources sources-{{source_id}}"> <table id='grid_{{name}}_{{source_id}}' class="display" width="100%"> <thead> <tr> <th></th> - <th></th>{% for col in col_names %} + <th></th>{% for col in column_labels %} <th>{{col}}</th> {% endfor %}</tr> </thead> @@ -92,6 +92,7 @@ </button> </div> </div> + {% if export_urls %} <div class="col"> <div class="btn-group btn-group-sm" role="group"> <button id="export-{{name}}-" type="button" @@ -100,16 +101,18 @@ {% trans 'Export'%} </button> <div class="dropdown-menu" aria-labelledby="export-{{name}}"> - <a class="dropdown-item {{sname}}-csv" href="{{source}}csv" target="_blank" title="{% trans 'Export as CSV' %}">{% trans "CSV" %}</a> + {% for lbl_export, export_url in export_urls %} + <a class="dropdown-item {{sname}}-csv-external" href="{{export_url}}" target="_blank" title="{% trans 'Export as CSV - ' %}{{lbl_export}}">{{lbl_export}}</a>{% endfor %} </div> <div class="input-group-append"> - <div class="selected-lines input-group-text" id="btnGroupAddon"> + <div class="selected-lines input-group-text"> <span class="sl-whole">{% trans "whole table" %}</span> <span class="sl-selected"><span class="sl-number"></span> {% trans "selected item(s)" %}</span> </div> </div> </div> </div> + {% endif %} </div> diff --git a/ishtar_common/templates/blocks/DataTables-external-sources.html b/ishtar_common/templates/blocks/DataTables-external-sources.html index c317e0c6f..af8774b82 100644 --- a/ishtar_common/templates/blocks/DataTables-external-sources.html +++ b/ishtar_common/templates/blocks/DataTables-external-sources.html @@ -6,7 +6,7 @@ <input type="radio" name="_sources" autocomplete="off" checked> {% trans "Local" %} <span class="badge badge-light" id="source_badge_default">-</span> </label> - {% for source_id, source_label, source_url in external_sources %} + {% for source_id, source_label, source_url, columns, column_labels, export_urls in external_sources %} <label id="source_button_{{source_id}}" class="btn btn-secondary"> <input type="radio" name="_sources" autocomplete="off"> {{source_label}} <span class="badge badge-light" id="source_badge_{{source_id}}">-</span> diff --git a/ishtar_common/templates/blocks/DataTables-stats.html b/ishtar_common/templates/blocks/DataTables-stats.html index d443a7721..8c6d387ac 100644 --- a/ishtar_common/templates/blocks/DataTables-stats.html +++ b/ishtar_common/templates/blocks/DataTables-stats.html @@ -48,7 +48,7 @@ {% with stats_name=name %}{% with extra="default" %} {% include "blocks/DataTables-stats-detail.html" %} {% endwith %}{% endwith %} - {% if external_sources %}{% for source_id, source_label, source_url in external_sources %} + {% if external_sources %}{% for source_id, source_label, source_url, columns, column_labels, export_urls in external_sources %} {% with stats_name=name %}{% with extra=source_id %} {% include "blocks/DataTables-stats-detail.html" %} {% endwith %}{% endwith %} diff --git a/ishtar_common/templates/blocks/DataTables-tabs.html b/ishtar_common/templates/blocks/DataTables-tabs.html index 8b6d4b807..2576f4df5 100644 --- a/ishtar_common/templates/blocks/DataTables-tabs.html +++ b/ishtar_common/templates/blocks/DataTables-tabs.html @@ -42,7 +42,7 @@ id="tab-content-gallery-{{name}}" role="tabpanel" aria-labelledby="tab-gallery-{{name}}"> <div id="content-gallery-{{name}}" class="sources sources-default"></div> - {% if external_sources %}{% for source_id, source_label, source_url in external_sources %} + {% if external_sources %}{% for source_id, source_label, source_url, columns, column_labels, export_urls in external_sources %} <div id="content-gallery-{{name}}-{{source_id}}" class="sources sources-{{source_id}}"></div> {% endfor %}{% endif %} </div>{% endif %} @@ -50,7 +50,7 @@ id="tab-content-map-{{name}}" role="tabpanel" aria-labelledby="tab-map-{{name}}"> <div id="map-{{name}}-default" class="sources sources-default"></div> - {% if external_sources %}{% for source_id, source_label, source_url in external_sources %} + {% if external_sources %}{% for source_id, source_label, source_url, columns, column_labels, export_urls in external_sources %} <div id="map-{{name}}-{{source_id}}" class="sources sources-{{source_id}}"></div>{% endfor %}{% endif %} </div>{% endif %} {% if current_model.STATISTIC_MODALITIES %} diff --git a/ishtar_common/templates/blocks/DataTables.html b/ishtar_common/templates/blocks/DataTables.html index 8ef1a5403..19ffd948e 100644 --- a/ishtar_common/templates/blocks/DataTables.html +++ b/ishtar_common/templates/blocks/DataTables.html @@ -25,7 +25,7 @@ </div> </div> -{% for source_id, source_label, source_url in external_sources %} +{% for source_id, source_label, source_url, columns, column_labels, export_urls in external_sources %} <div class="modal fade table-modal-lg" tabindex="-1" role="dialog" aria-hidden="true" id="modal_grid_{{name}}_{{source_id}}"> <div class="modal-dialog full modal-lg"> @@ -62,7 +62,7 @@ $('#modal_grid_{{name}}').on('hide.bs.modal', function (e) { bs_hide_table("{{name}}"); }); -{% for source_id, source_label, source_url in external_sources %} +{% for source_id, source_label, source_url, columns, column_labels, export_urls in external_sources %} $('#modal_grid_{{name}}_{{source_id}}').on('show.bs.modal', function (e) { bs_expand_table("{{name}}_{{source_id}}"); }); @@ -103,7 +103,7 @@ var selItems_{{sname}} = new Array(); {% if gallery %} gallery_submit_search = function(image_page){ - {% if external_sources %}{% for source_id, source_label, source_url in external_sources %} + {% if external_sources %}{% for source_id, source_label, source_url, columns, column_labels, export_urls in external_sources %} _gallery_submit_search(image_page, query_vars, "{{name}}", "{{source_url}}", "-{{source_id}}"); {% endfor %}{% endif %} return _gallery_submit_search(image_page, query_vars, "{{name}}", "{{source}}"); @@ -114,7 +114,7 @@ map_submit_search = function(){ if (current_source == "default"){ return _map_submit_search(query_vars, "{{name}}", "{{source}}"); } - {% if external_sources %}{% for source_id, source_label, source_url in external_sources %} + {% if external_sources %}{% for source_id, source_label, source_url, columns, column_labels, export_urls in external_sources %} else if (current_source == "{{source_id}}"){ return _map_submit_search(query_vars, "{{name}}", "{{source_url}}", "{{source_id}}"); } @@ -123,17 +123,17 @@ map_submit_search = function(){ {% endif %} extra_list = [ - "default"{% for source_id, source_label, source_url in external_sources %}, + "default"{% for source_id, source_label, source_url, columns, column_labels, export_urls in external_sources %}, "{{source_id}}"{% endfor %} ]; sources = [ - "{{source}}"{% for source_id, source_label, source_url in external_sources %}, + "{{source}}"{% for source_id, source_label, source_url, columns, column_labels, export_urls in external_sources %}, "{{source_url}}"{% endfor %} ]; stats_submit_search = function(){ - {% if external_sources %}{% for source_id, source_label, source_url in external_sources %} + {% if external_sources %}{% for source_id, source_label, source_url, columns, column_labels, export_urls in external_sources %} _stats_submit_search(query_vars, "{{name}}", "{{source_url}}", "{{source_id}}"); {% endfor %}{% endif %} return _stats_submit_search(query_vars, "{{name}}", "{{source}}"); @@ -151,7 +151,7 @@ datatable_submit_search = function(not_submited){ datatable_{{sname}}.ajax.url(url); datatable_{{sname}}.draw(); -{% if external_sources %}{% for source_id, source_label, source_url in external_sources %} +{% if external_sources %}{% for source_id, source_label, source_url, columns, column_labels, export_urls in external_sources %} if (!not_submited){ url = "{{source_url}}?submited=1&" + data; } else { @@ -170,16 +170,23 @@ datatable_submit_search = function(not_submited){ return false; }; -update_submit_args = function(){ - var data = search_get_query_data(query_vars, "{{name}}"); - var csv_url = "{{source}}csv?submited=1&" + data; - $(".{{sname}}-csv").attr("href", csv_url); - var csv_full_url = "{{source_full}}csv?submited=1&" + data; - $(".{{sname}}-csv-full").attr("href", csv_full_url); +update_submit_args = function(source_cls){ + if (!source_cls) source_cls = "sources-default"; + let data = search_get_query_data(query_vars, "{{name}}"); + let extra = "?submited=1&" + data; + let csv_url = "{{source}}csv" + extra; + $("." + source_cls + " .{{sname}}-csv").attr("href", csv_url); + let csv_full_url = "{{source_full}}csv?submited=1&" + data; + $("." + source_cls + " .{{sname}}-csv-full").attr("href", csv_full_url); {% for slug, name, extra_source in extra_sources %} - $(".{{slug}}-csv-full").attr("href", '{{extra_source}}csv?submited=1&' + data);{% endfor %} + $("." + source_cls + " .{{slug}}-csv-full").attr("href", '{{extra_source}}csv?submited=1&' + data);{% endfor %} {% for template in current_model.label_templates %} - $(".{{template.slug}}-labels").attr("href", '{{template.get_baselink_for_labels}}?submited=1&' + data);{% endfor %} + $("." + source_cls + " .{{template.slug}}-labels").attr("href", '{{template.get_baselink_for_labels}}?submited=1&' + data);{% endfor %} + + $("." + source_cls + " .{{sname}}-csv-external").each(function(){ + let url = $(this).attr("href").split('?')[0] + extra; + $(this).attr("href", url); + }); if ($('.modal-progress').length > 0){ $('.modal-progress').modal('hide'); @@ -187,24 +194,24 @@ update_submit_args = function(){ return false; }; -update_select_args = function(dt){ +update_select_args = function(dt, source_cls){ let nb_row = dt.rows( { selected: true } ).count(); - $(".sl-number").html(nb_row); + $("." + source_cls + " .sl-number").html(nb_row); if (nb_row == 0){ - $(".selected-lines .sl-whole").show(); - $(".selected-lines .sl-selected").hide(); - update_submit_args(); + $("." + source_cls + " .selected-lines .sl-whole").show(); + $("." + source_cls + " .selected-lines .sl-selected").hide(); + update_submit_args(source_cls); return; } - $(".selected-lines .sl-selected").show(); - $(".selected-lines .sl-whole").hide(); + $("." + source_cls + " .selected-lines .sl-selected").show(); + $("." + source_cls + " .selected-lines .sl-whole").hide(); let extra_sources = [{% for slug, name, extra_source in extra_sources %} ["{{slug}}", "{{name}}", "{{extra_source}}"]{% if not forloop.last %},{% endif %} {% endfor %}]; let extra_tpl = [{% for template in current_model.label_templates %} ["{{template.slug}}", "{{template.get_baselink_for_labels}}"]{% if not forloop.last %},{% endif %} {% endfor %}]; - update_export_urls(dt, "{{sname}}", "{{source}}", "{{source_full}}", extra_sources, extra_tpl); + update_export_urls(dt, source_cls, "{{sname}}", "{{source}}", "{{source_full}}", extra_sources, extra_tpl); } var current_source = "default"; @@ -227,7 +234,7 @@ jQuery(document).ready(function(){ return false; } ); - {% for source_id, source_label, source_url in external_sources %} + {% for source_id, source_label, source_url, columns, column_labels, export_urls in external_sources %} $("#source_button_{{source_id}}").click( function(){ current_source = "{{source_id}}"; @@ -327,14 +334,14 @@ jQuery(document).ready(function(){ datatable_{{sname}}.on( 'select', function(e, dt, type, indexes){ - update_select_args(dt); + update_select_args(dt, 'sources-default'); dt_single_enable_disable_submit_button(e, dt, type, indexes); } ); datatable_{{sname}}.on( 'deselect', function(e, dt, type, indexes){ - update_select_args(dt); + update_select_args(dt, 'sources-default'); dt_single_enable_disable_submit_button(e, dt, type, indexes); } ); @@ -342,20 +349,20 @@ jQuery(document).ready(function(){ datatable_{{sname}}.on( 'select', function(e, dt, type, indexes){ - update_select_args(dt); + update_select_args(dt, 'sources-default'); dt_multi_enable_disable_submit_button(e, dt, type, indexes); } ); datatable_{{sname}}.on( 'deselect', function(e, dt, type, indexes){ - update_select_args(dt); + update_select_args(dt, 'sources-default'); dt_multi_enable_disable_submit_button(e, dt, type, indexes); } ); {% endif %} -{% if external_sources %}{% for source_id, source_label, source_url in external_sources %} +{% if external_sources %}{% for source_id, source_label, source_url, columns, column_labels, export_urls in external_sources %} var base_external_source = "{{source_url}}"; if (default_search_vector){ @@ -375,10 +382,13 @@ jQuery(document).ready(function(){ } }, "deferLoading": 0, + "select": { + "style": 'multi' + }, "dom": 'litp', "columns": [ { "data": "id", "visible": false }, - { "data": "link", "orderable": false },{% for col in extra_cols %} + { "data": "link", "orderable": false },{% for col in columns %} { "data": "{{col}}", "defaultContent": "-", "render": $.fn.dataTable.render.ellipsis( 70, true ) }{% if not forloop.last %},{% endif %}{% endfor %} ] @@ -386,6 +396,18 @@ jQuery(document).ready(function(){ $.extend(datatable_options_{{source_id}}, datatables_default); if (datatables_i18n) datatable_options_{{source_id}}['language'] = datatables_i18n; datatable_{{sname}}_{{source_id}} = jQuery("#grid_{{name}}_{{source_id}}").DataTable(datatable_options_{{source_id}}); + datatable_{{sname}}_{{source_id}}.on( + 'select', + function(e, dt, type, indexes){ + update_select_args(dt, 'sources-{{source_id}}'); + } + ); + datatable_{{sname}}_{{source_id}}.on( + 'deselect', + function(e, dt, type, indexes){ + update_select_args(dt, 'sources-{{source_id}}'); + } + ); {% endfor %}{% endif %} diff --git a/ishtar_common/urls.py b/ishtar_common/urls.py index 8a0886551..974ad061f 100644 --- a/ishtar_common/urls.py +++ b/ishtar_common/urls.py @@ -20,6 +20,7 @@ from django.conf import settings from django.conf.urls import include, url from django.conf.urls.static import static +from django.urls import path from django.views.generic import TemplateView from .menus import Menu @@ -313,11 +314,6 @@ urlpatterns += [ name="get-by-importer", ), url( - r"search-external/(?P<model>[a-z-]+)/(?P<external_source_id>\d+)/(?P<data_type>[a-z-]+)?", - views_item.get_distant_item, - name="search-external" - ), - url( r"new-person/(?:(?P<parent_name>[^/]+)/)?(?:(?P<limits>[^/]+)/)?$", views.new_person, name="new-person", @@ -601,6 +597,21 @@ urlpatterns += [ views.QANotAvailable.as_view(), name="qa-not-available", ), + path( + "external-search/<slug:model>/<int:external_source_id>/", + views_item.get_distant_item, + name="external-search" + ), + path( + "external-search/<slug:model>/<int:external_source_id>/<slug:data_type>", + views_item.get_distant_item, + name="external-search" + ), + path( + "external-export/<int:source_id>/<slug:model_name>/<slug:slug>/", + views_item.external_export, + name="external-export" + ), ] urlpatterns += get_urls_for_model(models.Document, views, own=True) diff --git a/ishtar_common/utils.py b/ishtar_common/utils.py index 1d3c4d143..3abc0ddd1 100644 --- a/ishtar_common/utils.py +++ b/ishtar_common/utils.py @@ -1026,6 +1026,54 @@ def get_field_labels_from_path(model, path): return labels +def get_columns_from_class(cls, table_cols_attr="TABLE_COLS", dict_col_labels=True): + """ + Get table columns and table label from a model + :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) + """ + col_labels = {} + slug = getattr(cls, "SLUG", None) + if hasattr(cls, "COL_LABELS"): + col_labels = cls.COL_LABELS + if slug in settings.COL_LABELS: + col_labels.update(settings.COL_LABELS[slug]) + tb_key = (slug, table_cols_attr) + if tb_key in settings.TABLE_COLS: + table_cols = settings.TABLE_COLS[tb_key] + else: + table_cols = getattr(cls, table_cols_attr) + if callable(table_cols): + table_cols = table_cols() + if dict_col_labels: + return table_cols, col_labels + table_cols_label = [] + for c in table_cols: + if c in col_labels: + table_cols_label.append(str(col_labels[c])) + else: + field_verbose_name, field_name = "", "" + field = cls + keys = c.split("__") + if "." in c: + keys = c.split(".") + for key in keys: + if hasattr(field, "remote_field") and field.remote_field: + field = field.remote_field.model + try: + field = field._meta.get_field(key) + field_verbose_name = field.verbose_name + except (fields.FieldDoesNotExist, AttributeError): + if hasattr(field, key + "_lbl"): + field_verbose_name = getattr(field, key + "_lbl") + else: + continue + table_cols_label.append(str(field_verbose_name)) + return table_cols, table_cols_label + + def create_default_areas(models=None, verbose=False): # can be used on migrations if models are provided if not models: diff --git a/ishtar_common/views.py b/ishtar_common/views.py index 3ae686f99..98bded3ec 100644 --- a/ishtar_common/views.py +++ b/ishtar_common/views.py @@ -650,17 +650,15 @@ def get_by_importer( if data_type == "json": res = "{}" return HttpResponse(res, content_type="text/plain") - imp = q.all()[0].get_importer_class() - cols, col_names = [], [] - for formater in imp.LINE_EXPORT_FORMAT: - if not formater: - cols.append("") - col_names.append("") - continue - cols.append(formater.export_field_name) - col_names.append(formater.label) - obj_name = imp.OBJECT_CLS.__name__.lower() - return get_item(imp.OBJECT_CLS, "get_" + obj_name, obj_name, own_table_cols=cols)( + importer = q.all()[0] + importer_class = importer.get_importer_class() + cols, col_names = importer.get_columns(importer_class=importer_class) + if data_type == "csv" or dct.get("type", "") == "csv": + obj_name = importer.name + else: + obj_name = importer_class.OBJECT_CLS.__name__.lower() + return get_item(importer_class.OBJECT_CLS, "get_" + obj_name, obj_name, + own_table_cols=cols)( request, data_type, full, force_own, col_names=col_names, **dct ) @@ -2691,55 +2689,6 @@ class GeoFormMixin(IshtarMixin, LoginRequiredMixin): return kwargs -class GeoEditView(GeoFormMixin, UpdateView): - page_name = _("Geo item modification") - - def get_form_kwargs(self): - kwargs = super(GeoEditView, self).get_form_kwargs() - try: - geo = models.GeoVectorData.objects.get(pk=self.kwargs.get("pk")) - assert check_permission(self.request, "geo/edit", geo.pk) - except (AssertionError, models.GeoVectorData.DoesNotExist): - raise Http404() - initial = {} - - for k in ( - list(self.form_class.base_fields.keys()) + - models.GeoVectorData.RELATED_MODELS - ): - value = getattr(geo, k) - if hasattr(value, "all"): - value = ",".join([str(v.pk) for v in value.all().order_by("pk")]) - if hasattr(value, "pk"): - value = value.pk - initial[k] = value - - kwargs["main_items_fields"] = {} - kwargs["too_many"] = {} - LIMIT = 10 - for k in models.GeoVectorData.RELATED_MODELS: - kwargs["main_items_fields"][k] = [] - values = [] - for idx, related_item in enumerate(getattr(geo, k).all()): - if idx >= LIMIT: - if k not in kwargs["too_many"]: - kwargs["too_many"][k] = [] - kwargs["too_many"][k].append(related_item.pk) - continue - pk = str(related_item.pk) - values.append(pk) - key = "{}_{}_main_item".format(k, pk) - kwargs["main_items_fields"][k].append( - (key, "{} - {}".format(_("Main geo item for"), related_item)) - ) - if related_item.main_geodata == geo: - initial[key] = True - initial[k] = ",".join(values) - kwargs["initial"] = initial - kwargs["user"] = self.request.user - return kwargs - - class GeoCreateView(GeoFormMixin, CreateView): page_name = _("Geo item creation") diff --git a/ishtar_common/views_item.py b/ishtar_common/views_item.py index ce70559f9..673ffb46a 100644 --- a/ishtar_common/views_item.py +++ b/ishtar_common/views_item.py @@ -1428,6 +1428,7 @@ def _get_data_from_query(items, query_table_cols, extra_request_keys, if hasattr(items.model, "locked"): values.append("locked") values.append("lock_user_id") + values = [v for v in values if v] # filter empty values return items.values_list(*values) @@ -2591,3 +2592,53 @@ def get_distant_item(request, model, external_source_id, data_type=None): if "link" in row: row["link"] = row["link"].replace(str(idx), source_id) return HttpResponse(json.dumps(dct), content_type="application/json") + + +def external_export(request, source_id, model_name, slug): + external_sources = request.session.get("EXTERNAL_SOURCES", {}) + url = None + for source in external_sources: + try: + src_id, __ = source.split("||") + src_id = int(src_id) + except ValueError: + continue + if src_id != source_id: + continue + for model in external_sources[source]: + if model_name == model.split("-")[-1]: + url = reverse("api-export-" + model_name, args=[slug]) + + if not url: + return HttpResponse('Unauthorized', status=401) + + try: + src = models_rest.ApiExternalSource.objects.get(pk=source_id) + except (models_rest.ApiExternalSource.DoesNotExist, ValueError): + return HttpResponse('Unauthorized', status=401) + + url = src.url + url + try: + response = requests.get( + url, + params=request.GET, + timeout=20, + headers={"Authorization": f"Token {src.key}"}, + ) + except requests.exceptions.Timeout: + return HttpResponse('Gateway Timeout', status=504) + if response.status_code != 200: + lbl = { + 401: "Unauthorized", + 404: "Page not found", + 500: "Server error", + } + lbl = lbl[response.status_code] \ + if response.status_code in lbl else "Unknown error" + return HttpResponse(lbl, status=response.status_code) + + response = HttpResponse(response.text, content_type="text/csv") + n = datetime.datetime.now() + filename = f"{model_name}-{n.strftime('%Y%m%d-%H%M%S')}.csv" + response["Content-Disposition"] = "attachment; filename=%s" % filename + return response diff --git a/ishtar_common/widgets.py b/ishtar_common/widgets.py index a6a621cbe..57dc1a730 100644 --- a/ishtar_common/widgets.py +++ b/ishtar_common/widgets.py @@ -44,7 +44,7 @@ from json import JSONEncoder from django.utils.translation import ugettext_lazy as _ from ishtar_common import models -from ishtar_common.utils import reverse_coordinates +from ishtar_common.utils import get_columns_from_class, reverse_coordinates logger = logging.getLogger(__name__) @@ -1204,20 +1204,10 @@ class DataTable(Select2Media, forms.RadioSelect): def get_cols(self, python=False): jq_col_names, extra_cols = [], [] - col_labels = {} - slug = getattr(self.associated_model, "SLUG", None) - if hasattr(self.associated_model, "COL_LABELS"): - col_labels = self.associated_model.COL_LABELS - if slug in settings.COL_LABELS: - col_labels.update(settings.COL_LABELS[slug]) - tb_key = (slug, self.table_cols) - if tb_key in settings.TABLE_COLS: - table_cols = settings.TABLE_COLS[tb_key] - else: - table_cols = getattr(self.associated_model, self.table_cols) - if callable(table_cols): - table_cols = table_cols() - + table_cols, col_labels = get_columns_from_class( + self.associated_model, + table_cols_attr=self.table_cols + ) for col_names in table_cols: field_verbose_names = [] field_verbose_name, field_name = "", "" diff --git a/ishtar_common/wizards.py b/ishtar_common/wizards.py index 8acf1a3ed..d8c789018 100644 --- a/ishtar_common/wizards.py +++ b/ishtar_common/wizards.py @@ -48,7 +48,7 @@ from django.template import loader from django.utils.translation import ugettext_lazy as _ from django.utils.safestring import mark_safe -from ishtar_common import models +from ishtar_common import models, models_rest from ishtar_common.forms import CustomForm, reverse_lazy from ishtar_common.utils import get_all_field_names, MultiValueDict, put_session_message @@ -134,8 +134,26 @@ class IshtarWizard(NamedUrlWizardView): src_id, lbl = source.split("||") except ValueError: continue - url = reverse("search-external", args=[base_model_name, src_id]) - context["external_sources"].append((src_id, lbl, url)) + q = models_rest.ApiExternalSource.objects.filter( + profiles=self.request.user.ishtaruser.current_profile, + pk=src_id + ) + if not q.count(): + continue + external_source = q.all()[0] + url = reverse("external-search", args=[base_model_name, src_id]) + export_urls = [] + for slug, label in external_source.get_exports(model_name): + export_urls.append( + (label, + reverse("external-export", + args=[src_id, base_model_name, slug])) + ) + context["external_sources"].append( + (src_id, lbl, url, external_source.get_columns(model_name), + external_source.get_column_labels(model_name), + export_urls) + ) form.fields["pk"].widget.external_sources = context["external_sources"] |