diff options
-rw-r--r-- | ishtar_common/admin.py | 111 | ||||
-rw-r--r-- | ishtar_common/models_imports.py | 8 | ||||
-rw-r--r-- | ishtar_common/serializers_utils.py | 47 | ||||
-rw-r--r-- | ishtar_common/templates/admin/gen_change_list.html | 5 | ||||
-rw-r--r-- | ishtar_common/templates/admin/json_change_list.html | 19 |
5 files changed, 176 insertions, 14 deletions
diff --git a/ishtar_common/admin.py b/ishtar_common/admin.py index 71dc2cbdc..e606a81e7 100644 --- a/ishtar_common/admin.py +++ b/ishtar_common/admin.py @@ -67,6 +67,9 @@ from ishtar_common.apps import admin_site from ishtar_common.utils import get_cache, create_slug from ishtar_common import forms as common_forms +from ishtar_common.serializers import restore_serialized, IMPORT_MODEL_LIST +from ishtar_common.serializers_utils import generic_get_results, \ + serialization_info from archaeological_files import forms as file_forms from archaeological_files_pdl import forms as file_pdl_forms from archaeological_operations import forms as operation_forms @@ -113,7 +116,7 @@ def change_value(attribute, value, description): return _change_value -def export_as_csv_action(description=_(u"Export selected as CSV file"), +def export_as_csv_action(description=_("Export selected as CSV file"), fields=None, exclude=None, header=True): """ This function returns an export csv action @@ -206,6 +209,44 @@ def export_as_geojson_action( return export_as_geojson +def serialize_action(dir_name, model_list): + def _serialize_action(modeladmin, request, queryset): + if model_list: + modellist = model_list[:] + else: + modellist = [modeladmin.model] + opts = modeladmin.model._meta + result = generic_get_results( + modellist, dir_name, + result_queryset={opts.object_name: queryset}) + basename = str(opts).replace('.', '_') + in_memory = BytesIO() + zip = zipfile.ZipFile(in_memory, "a") + for key in result.keys(): + __, model_name = key + zip.writestr(dir_name + os.sep + model_name + ".json", result[key]) + + # info + zip.writestr("info.json", json.dumps(serialization_info(), indent=2)) + + # fix for Linux zip files read in Windows + for file in zip.filelist: + file.create_system = 0 + zip.close() + response = HttpResponse(content_type='application/zip') + response['Content-Disposition'] = 'attachment; filename={}.zip'.format( + basename + ) + in_memory.seek(0) + response.write(in_memory.read()) + return response + return _serialize_action + + +SERIALIZE_DESC = _("Export selected as Ishtar (zipped JSON)") +serialize_type_action = serialize_action("types", None) +serialize_type_action.short_description = SERIALIZE_DESC + TokenAdmin.raw_id_fields = ('user',) admin_site.register(Token, TokenAdmin) @@ -693,6 +734,61 @@ class ImportGEOJSONActionAdmin(object): {'file_form': form, 'current_action': 'import_geojson'}) +class ImportJSONForm(forms.Form): + json_file = forms.FileField( + _("Zipped JSON file"), + help_text=_("Import from a zipped JSON file generated by Ishtar") + ) + + +class ImportJSONActionAdmin(admin.ModelAdmin): + change_list_template = "admin/json_change_list.html" + import_keys = ['slug', 'txt_idx'] + + def get_urls(self): + urls = super(ImportJSONActionAdmin, self).get_urls() + my_urls = [ + url(r'^import-from-json/$', self.import_json), + ] + return my_urls + urls + + def import_json(self, request): + form = None + + if 'apply' in request.POST: + form = ImportJSONForm(request.POST, request.FILES) + if form.is_valid(): + with tempfile.TemporaryDirectory() as tmpdirname: + filename = tmpdirname + os.sep + "export.zip" + with open(filename, "wb+") as zipped_file: + for chunk in request.FILES['json_file'].chunks(): + zipped_file.write(chunk) + result = None + result = restore_serialized(filename) + try: + result = restore_serialized(filename) + except ValueError as e: + self.message_user(request, str(e), + level=messages.ERROR) + if result: + for model, count in result: + self.message_user( + request, + str(_("{} {}(s) created/updated.")).format( + count, model)) + url = reverse( + 'admin:%s_%s_changelist' % ( + self.model._meta.app_label, self.model._meta.model_name) + ) + return HttpResponseRedirect(url) + if not form: + form = ImportJSONForm() + return render( + request, 'admin/import_from_file.html', + {'file_form': form, 'current_action': 'import_json'} + ) + + class AdminRelatedTownForm(forms.ModelForm): class Meta: model = models.Town.children.through @@ -744,12 +840,12 @@ class TownAdmin(ImportGEOJSONActionAdmin, ImportActionAdmin): admin_site.register(models.Town, TownAdmin) -class GeneralTypeAdmin(ImportActionAdmin): +class GeneralTypeAdmin(ImportActionAdmin, ImportJSONActionAdmin): list_display = ['label', 'txt_idx', 'available', 'comment'] search_fields = ('label', 'txt_idx', 'comment',) list_filter = ('available',) save_on_top = True - actions = [export_as_csv_action()] + actions = [export_as_csv_action(), serialize_type_action] prepopulated_fields = {"txt_idx": ("label",)} @csrf_protect_m @@ -995,9 +1091,14 @@ if settings.USE_LIBREOFFICE: importer_type_actions.append(generate_libreoffice_template) -class ImporterTypeAdmin(admin.ModelAdmin): +serialize_importer_action = serialize_action("common_imports", + IMPORT_MODEL_LIST) +serialize_importer_action.short_description = SERIALIZE_DESC + + +class ImporterTypeAdmin(ImportJSONActionAdmin): list_display = ('name', 'associated_models', 'available') - actions = importer_type_actions + actions = importer_type_actions + [serialize_importer_action] admin_site.register(models.ImporterType, ImporterTypeAdmin) diff --git a/ishtar_common/models_imports.py b/ishtar_common/models_imports.py index 7fdee8ce3..4fa1abb4a 100644 --- a/ishtar_common/models_imports.py +++ b/ishtar_common/models_imports.py @@ -103,11 +103,11 @@ class ImporterType(models.Model): associated_models = models.ForeignKey( ImporterModel, verbose_name=_("Associated model"), on_delete=models.SET_NULL, - related_name='+', blank=True, null=True) + related_name='importer_type_associated', blank=True, null=True) created_models = models.ManyToManyField( ImporterModel, verbose_name=_("Models that can accept new items"), blank=True, help_text=_("Leave blank for no restrictions"), - related_name='+') + related_name='importer_type_created') is_template = models.BooleanField(_("Can be exported"), default=False) unicity_keys = models.CharField(_("Unicity keys (separator \";\")"), blank=True, null=True, max_length=500) @@ -415,9 +415,11 @@ class ImporterColumn(models.Model): description = models.TextField(_("Description"), blank=True, null=True) regexp_pre_filter = models.ForeignKey( "Regexp", blank=True, null=True, on_delete=models.SET_NULL, + related_name="columns", ) value_format = models.ForeignKey( "ValueFormater", blank=True, null=True, on_delete=models.SET_NULL, + related_name="columns" ) required = models.BooleanField(_("Required"), default=False) export_field_name = models.CharField( @@ -552,7 +554,7 @@ class ImportTarget(models.Model): """ column = models.ForeignKey(ImporterColumn, related_name='targets') target = models.CharField("Target", max_length=500) - formater_type = models.ForeignKey("FormaterType") + formater_type = models.ForeignKey("FormaterType", related_name='targets') force_new = models.BooleanField(_("Force creation of new items"), default=False) concat = models.BooleanField(_("Concatenate with existing"), diff --git a/ishtar_common/serializers_utils.py b/ishtar_common/serializers_utils.py index 74ffbe176..c03a55e35 100644 --- a/ishtar_common/serializers_utils.py +++ b/ishtar_common/serializers_utils.py @@ -9,6 +9,7 @@ from zipfile import ZipFile from django.contrib.sites.models import Site from django.core.serializers import serialize +from django.db.models import Q from ishtar_common.version import get_version from . import models @@ -19,7 +20,10 @@ SERIALIZATION_VERSION = "1.0" def get_model_from_filename(filename): filename = filename.split(".")[0] # remove extension - module_name, model_name = filename.split("__") + splitted = filename.split("__") + if len(splitted) != 2: + return + module_name, model_name = splitted if module_name == "django": if model_name in ("Group", "Permission"): module = importlib.import_module("django.contrib.auth.models") @@ -107,6 +111,21 @@ def archive_serialization(result, archive_dir=None, archive=False, return archive_name +GENERIC_QUERYSET_FILTER = { + "Regexp": {"ImporterType": 'columns__importer_type__pk__in'}, + "ImporterModel": {"ImporterType": ['importer_type_associated__pk__in', + 'importer_type_created__pk__in']}, + "ValueFormater": {"ImporterType": 'columns__importer_type__pk__in'}, + "ImporterColumn": {"ImporterType": 'importer_type__pk__in'}, + "ImporterDefault": {"ImporterType": 'importer_type__pk__in'}, + "ImportTarget": {"ImporterType": 'column__importer_type__pk__in'}, + "FormaterType": {"ImporterType": 'targets__column__importer_type__pk__in'}, + "ImporterDefaultValues": { + "ImporterType": 'default_target__importer_type__pk__in'}, + "ImporterDuplicateField": {"ImporterType": 'column__importer_type__pk__in'}, +} + + def generic_get_results(model_list, dirname, no_geo=True, result_queryset=None, serialization_include=None): result = OrderedDict() @@ -114,11 +133,27 @@ def generic_get_results(model_list, dirname, no_geo=True, base_model_name = model.__name__ model_name = str(model.__module__).split(".")[0] + "__" + \ base_model_name - - if result_queryset and base_model_name in result_queryset: - base_q = result_queryset[base_model_name] - else: - base_q = model.objects + base_q = model.objects + if result_queryset: + if result_queryset and base_model_name in result_queryset: + base_q = result_queryset[base_model_name] + elif base_model_name in GENERIC_QUERYSET_FILTER: + alt_filter = GENERIC_QUERYSET_FILTER[base_model_name] + for k in alt_filter: + if k in result_queryset: + terms = alt_filter[k] + if not isinstance(terms, (list, tuple)): + terms = [terms] + ids = [r["pk"] + for r in result_queryset[k].values("pk").all()] + q = None + for term in terms: + if not q: + q = Q(**{term: ids}) + else: + q |= Q(**{term: ids}) + base_q = base_q.filter(q) + break q = base_q recursion = None if hasattr(model, "parent"): diff --git a/ishtar_common/templates/admin/gen_change_list.html b/ishtar_common/templates/admin/gen_change_list.html index 9cb566484..f14b62f27 100644 --- a/ishtar_common/templates/admin/gen_change_list.html +++ b/ishtar_common/templates/admin/gen_change_list.html @@ -10,6 +10,11 @@ </a> </li> <li> + <a href="import-from-json/" class="addlink"> + {% trans "Import from JSON" %} + </a> + </li> + <li> <a href="import-from-csv/" class="addlink"> {% trans "Import from CSV" %} </a> diff --git a/ishtar_common/templates/admin/json_change_list.html b/ishtar_common/templates/admin/json_change_list.html new file mode 100644 index 000000000..74a8cabe7 --- /dev/null +++ b/ishtar_common/templates/admin/json_change_list.html @@ -0,0 +1,19 @@ +{% extends "admin/change_list.html" %} +{% load i18n admin_urls static admin_list %} + + {% block object-tools-items %} + {% if has_add_permission %} + <li> + {% url cl.opts|admin_urlname:'add' as add_url %} + <a href="{% add_preserved_filters add_url is_popup to_field %}" class="addlink"> + {% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %} + </a> + </li> + <li> + <a href="import-from-json/" class="addlink"> + {% trans "Import from JSON" %} + </a> + </li> + {% endif %} + {% endblock %} + |