diff --git a/admin_interface/context_processor.py b/admin_interface/context_processor.py index ba772d4..cf832cb 100644 --- a/admin_interface/context_processor.py +++ b/admin_interface/context_processor.py @@ -10,8 +10,8 @@ def get_active_theme(request): obj = objs_manager.first() if obj: obj.set_active() - else: - obj = objs_manager.create() + # else: + # obj = objs_manager.create() elif objs_active_count == 1: obj = objs_active_ls[0] diff --git a/admin_interface/import_tema/__init__.py b/admin_interface/import_tema/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/admin_interface/import_tema/admin.py b/admin_interface/import_tema/admin.py new file mode 100644 index 0000000..1813918 --- /dev/null +++ b/admin_interface/import_tema/admin.py @@ -0,0 +1,156 @@ +import json +import os +import tempfile +import zipfile + +import django +from django import forms +from django.conf import settings +from django.contrib import admin, messages +from django.contrib.auth import get_permission_codename +from django.core.exceptions import PermissionDenied +from django.core.files.storage import default_storage +from django.http import HttpResponseRedirect +from django.template.response import TemplateResponse +from django.urls import path, reverse +from django.utils.translation import gettext_lazy as _ + +from .. import models +from .forms import ImportForm + + +class ImportMixin(admin.ModelAdmin): + """ + Import mixin. + + This is intended to be mixed with django.contrib.admin.ModelAdmin + https://docs.djangoproject.com/en/2.1/ref/contrib/admin/#modeladmin-objects + """ + + #: template for change_list view + change_list_template = 'admin/import_export/change_list_import.html' + #: template for import view + import_template_name = 'admin/import_export/import.html' + + def get_model_info(self): + app_label = self.model._meta.app_label + return (self.model._meta.app_label, self.model._meta.model_name) + + def has_import_permission(self, request): + """ + Returns whether a request has import permission. + """ + IMPORT_PERMISSION_CODE = getattr(settings, 'IMPORT_EXPORT_IMPORT_PERMISSION_CODE', None) + if IMPORT_PERMISSION_CODE is None: + return True + + opts = self.opts + codename = get_permission_codename(IMPORT_PERMISSION_CODE, opts) + return request.user.has_perm("%s.%s" % (opts.app_label, codename)) + + def get_urls(self): + urls = super().get_urls() + info = self.get_model_info() + my_urls = [ + path('import/', + self.admin_site.admin_view(self.import_action), + name='%s_%s_import' % info), + ] + return my_urls + urls + + def get_import_context_data(self, **kwargs): + return self.get_context_data(**kwargs) + + def get_context_data(self, **kwargs): + return {} + + def get_form_kwargs(self, form, *args, **kwargs): + """ + Prepare/returns kwargs for the import form. + + To distinguish between import and confirm import forms, + the following approach may be used: + + if isinstance(form, ImportForm): + # your code here for the import form kwargs + # e.g. update.kwargs({...}) + elif isinstance(form, ConfirmImportForm): + # your code here for the confirm import form kwargs + # e.g. update.kwargs({...}) + ... + """ + return kwargs + + def import_action(self, request, *args, **kwargs): + """ + Perform a dry_run of the import to make sure the import will not + result in errors. If there where no error, save the user + uploaded file to a local temp file that will be used by + 'process_import' for the actual import. + """ + if not self.has_import_permission(request): + raise PermissionDenied + + context = self.get_import_context_data() + + form_type = ImportForm + form_kwargs = self.get_form_kwargs(form_type, *args, **kwargs) + form = form_type(request.POST or None, + request.FILES or None, + **form_kwargs) + + if request.POST and form.is_valid(): + import_file_tema = form.cleaned_data['tema'] + if zipfile.is_zipfile(import_file_tema): + with zipfile.ZipFile(import_file_tema, 'r') as zip_ref: + with tempfile.TemporaryDirectory() as tempdir: + zip_ref.extractall(tempdir) + lst = os.listdir(tempdir) + allowed_extensions=[".gif", ".jpg", ".jpeg", ".png", ".svg"] + try: + tema_json = [s for s in os.listdir(f'{tempdir}/{lst[0]}') if '.json' in s][0] + logo = [s for s in os.listdir(f'{tempdir}/{lst[0]}/logo') if any(ele in s for ele in allowed_extensions)][0] + favicon = [s for s in os.listdir(f'{tempdir}/{lst[0]}/favicon') if any(ele in s for ele in allowed_extensions)][0] + with open(f'{tempdir}/{lst[0]}/{tema_json}', 'r') as temporary_file: + result = json.loads(temporary_file.read()) + with open(f'{tempdir}/{lst[0]}/logo/{logo}', 'rb') as temporary_file: + default_storage.save(f"admin-interface/logo/{temporary_file.name.split('/')[-1]}", temporary_file) + with open(f'{tempdir}/{lst[0]}/favicon/{favicon}', 'rb') as temporary_file: + default_storage.save(f"admin-interface/favicon/{temporary_file.name.split('/')[-1]}", temporary_file) + skip_result = False + except FileNotFoundError as e: + messages.error(request, 'Struttura del file .zip errata.') + skip_result = True + if not skip_result: + try: + new_theme = models.Theme( + **result[0]['fields'] + ) + new_theme.save() + + messages.success(request, _('Import finished')) + except: + messages.error(request, 'Struttura del file .json errata.') + else: + messages.error(request, 'È richiesto un file .zip') + + url = reverse('admin:%s_%s_changelist' % self.get_model_info(), + current_app=self.admin_site.name) + return HttpResponseRedirect(url) + else: + context.update(self.admin_site.each_context(request)) + + context['title'] = _("Import") + context['form'] = form + context['opts'] = self.model._meta + + request.current_app = self.admin_site.name + return TemplateResponse(request, [self.import_template_name], + context) + + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context['has_import_permission'] = self.has_import_permission(request) + return super().changelist_view(request, extra_context) + diff --git a/admin_interface/import_tema/forms.py b/admin_interface/import_tema/forms.py new file mode 100644 index 0000000..1d80c47 --- /dev/null +++ b/admin_interface/import_tema/forms.py @@ -0,0 +1,11 @@ +import os.path + +from django import forms +from django.contrib.admin.helpers import ActionForm +from django.utils.translation import gettext_lazy as _ + + +class ImportForm(forms.Form): + tema = forms.FileField( + label='Zip' + ) diff --git a/admin_interface/import_tema/locale/it/LC_MESSAGES/django.mo b/admin_interface/import_tema/locale/it/LC_MESSAGES/django.mo new file mode 100644 index 0000000..722f52c Binary files /dev/null and b/admin_interface/import_tema/locale/it/LC_MESSAGES/django.mo differ diff --git a/admin_interface/import_tema/locale/it/LC_MESSAGES/django.po b/admin_interface/import_tema/locale/it/LC_MESSAGES/django.po new file mode 100644 index 0000000..e60c55f --- /dev/null +++ b/admin_interface/import_tema/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,139 @@ +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Christian Galeffi , 2015. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-06-04 09:27+0200\n" +"PO-Revision-Date: 2015-08-30 20:32+0100\n" +"Last-Translator: Christian Galeffi \n" +"Language-Team: Italian \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.5.4\n" + +#: admin.py:194 +#, python-format +msgid "%s through import_export" +msgstr "" + +#: admin.py:200 +msgid "Import finished, with {} new and {} updated {}." +msgstr "" + +#: admin.py:298 +#, python-format +msgid "

Imported file has a wrong encoding: %s

" +msgstr "" + +#: admin.py:300 +#, python-format +msgid "

%s encountered while trying to read file: %s

" +msgstr "" + +#: admin.py:331 templates/admin/import_export/change_list_import_item.html:5 +#: templates/admin/import_export/import.html:10 +msgid "Import" +msgstr "Importare" + +#: admin.py:496 templates/admin/import_export/change_list_export_item.html:5 +#: templates/admin/import_export/export.html:7 +msgid "Export" +msgstr "Esportare" + +#: admin.py:554 +msgid "You must select an export format." +msgstr "Devi selezionare un formato di esportazione." + +#: admin.py:567 +#, python-format +msgid "Export selected %(verbose_name_plural)s" +msgstr "Esporta selezionati %(verbose_name_plural)s" + +#: forms.py:10 +msgid "File to import" +msgstr "File da importare" + +#: forms.py:13 forms.py:41 forms.py:66 +msgid "Format" +msgstr "Formato" + +#: templates/admin/import_export/base.html:11 +msgid "Home" +msgstr "Home" + +#: templates/admin/import_export/export.html:31 +#: templates/admin/import_export/import.html:52 +msgid "Submit" +msgstr "Inviare" + +#: templates/admin/import_export/import.html:20 +msgid "" +"Below is a preview of data to be imported. If you are satisfied with the " +"results, click 'Confirm import'" +msgstr "" +"Questa è un'anteprima dei dati che saranno importati. Se il risultato è " +"soddisfacente, premi 'Conferma importazione'" + +#: templates/admin/import_export/import.html:23 +msgid "Confirm import" +msgstr "Conferma importazione" + +#: templates/admin/import_export/import.html:31 +msgid "This importer will import the following fields: " +msgstr "Verranno importati i seguenti campi:" + +#: templates/admin/import_export/import.html:61 +#: templates/admin/import_export/import.html:90 +msgid "Errors" +msgstr "Errori" + +#: templates/admin/import_export/import.html:72 +msgid "Line number" +msgstr "Numero linea" + +#: templates/admin/import_export/import.html:82 +msgid "Some rows failed to validate" +msgstr "" + +#: templates/admin/import_export/import.html:84 +msgid "" +"Please correct these errors in your data where possible, then reupload it " +"using the form above." +msgstr "" + +#: templates/admin/import_export/import.html:89 +msgid "Row" +msgstr "" + +#: templates/admin/import_export/import.html:116 +msgid "Non field specific" +msgstr "" + +#: templates/admin/import_export/import.html:137 +msgid "Preview" +msgstr "Anteprima" + +#: templates/admin/import_export/import.html:152 +msgid "New" +msgstr "Nuovo" + +#: templates/admin/import_export/import.html:154 +msgid "Skipped" +msgstr "Salta" + +#: templates/admin/import_export/import.html:156 +msgid "Delete" +msgstr "Cancella" + +#: templates/admin/import_export/import.html:158 +msgid "Update" +msgstr "Aggiorna" + +#~ msgid "Import finished" +#~ msgstr "Importazione terminata" diff --git a/admin_interface/import_tema/static/import_export/import.css b/admin_interface/import_tema/static/import_export/import.css new file mode 100644 index 0000000..bb20ba2 --- /dev/null +++ b/admin_interface/import_tema/static/import_export/import.css @@ -0,0 +1,81 @@ +.import-preview .errors { + position: relative; +} + +.validation-error-count { + display: inline-block; + background-color: #e40000; + border-radius: 6px; + color: white; + font-size: 0.9em; + position: relative; + font-weight: bold; + margin-top: -2px; + padding: 0.2em 0.4em; +} + +.validation-error-container { + position: absolute; + opacity: 0; + pointer-events: none; + background-color: #ffc1c1; + padding: 14px 15px 10px; + top: 25px; + margin: 0 0 20px 0; + width: 200px; + z-index: 2; +} + +table.import-preview tr.skip { + background-color: #d2d2d2; +} + +table.import-preview tr.new { + background-color: #bdd8b2; +} + +table.import-preview tr.delete { + background-color: #f9bebf; +} + +table.import-preview tr.update { + background-color: #fdfdcf; +} + +.import-preview td:hover .validation-error-count { + z-index: 3; +} +.import-preview td:hover .validation-error-container { + opacity: 1; + pointer-events: auto; +} + +.validation-error-list { + margin: 0; + padding: 0; +} + +.validation-error-list li { + list-style: none; + margin: 0; +} + +.validation-error-list > li > ul { + margin: 8px 0; + padding: 0; +} + +.validation-error-list > li > ul > li { + padding: 0; + margin: 0 0 10px; + line-height: 1.28em; +} + +.validation-error-field-label { + display: block; + border-bottom: 1px solid #e40000; + color: #e40000; + text-transform: uppercase; + font-weight: bold; + font-size: 0.85em; +} diff --git a/admin_interface/import_tema/templates/admin/import_export/base.html b/admin_interface/import_tema/templates/admin/import_export/base.html new file mode 100644 index 0000000..0aadf18 --- /dev/null +++ b/admin_interface/import_tema/templates/admin/import_export/base.html @@ -0,0 +1,17 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_modify %} +{% load admin_urls %} +{% load static %} + +{% block extrastyle %}{{ block.super }}{% endblock %} +{% block bodyclass %}{{ block.super }} {{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %} +{% if not is_popup %} +{% block breadcrumbs %} + +{% endblock %} +{% endif %} diff --git a/admin_interface/import_tema/templates/admin/import_export/change_list.html b/admin_interface/import_tema/templates/admin/import_export/change_list.html new file mode 100644 index 0000000..0f23dc7 --- /dev/null +++ b/admin_interface/import_tema/templates/admin/import_export/change_list.html @@ -0,0 +1,13 @@ +{% extends "admin/change_list.html" %} + +{# Original template renders object-tools only when has_add_permission is True. #} +{# This hack allows sub templates to add to object-tools #} +{% block object-tools %} +
    + {% block object-tools-items %} + {% if has_add_permission %} + {{ block.super }} + {% endif %} + {% endblock %} +
+{% endblock %} diff --git a/admin_interface/import_tema/templates/admin/import_export/change_list_import.html b/admin_interface/import_tema/templates/admin/import_export/change_list_import.html new file mode 100644 index 0000000..9aa1860 --- /dev/null +++ b/admin_interface/import_tema/templates/admin/import_export/change_list_import.html @@ -0,0 +1,6 @@ +{% extends "admin/import_export/change_list.html" %} + +{% block object-tools-items %} + {% include "admin/import_export/change_list_import_item.html" %} + {{ block.super }} +{% endblock %} diff --git a/admin_interface/import_tema/templates/admin/import_export/import.html b/admin_interface/import_tema/templates/admin/import_export/import.html new file mode 100644 index 0000000..f791c79 --- /dev/null +++ b/admin_interface/import_tema/templates/admin/import_export/import.html @@ -0,0 +1,171 @@ +{% extends "admin/import_export/base.html" %} +{% load i18n %} +{% load admin_urls %} +{% load import_export_tags %} +{% load static %} + +{% block extrastyle %}{{ block.super }}{% endblock %} + +{% block breadcrumbs_last %} +{% trans "Import" %} +{% endblock %} + +{% block content %} + + {% if confirm_form %} +
+ {% csrf_token %} + {{ confirm_form.as_p }} +

+ {% trans "Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'" %} +

+
+ +
+
+ {% else %} +
+ {% csrf_token %} + +

+ {% trans "This importer will import the following fields: " %} + {{ fields|join:", " }} +

+ +
+ {% for field in form %} +
+ {{ field.errors }} + + {{ field.label_tag }} + + {{ field }} + + {% if field.field.help_text %} +

{{ field.field.help_text|safe }}

+ {% endif %} +
+ {% endfor %} +
+ +
+ +
+
+ {% endif %} + + {% if result %} + + {% if result.has_errors %} + +

{% trans "Errors" %}

+
    + {% for error in result.base_errors %} +
  • + {{ error.error }} +
    {{ error.traceback|linebreaks }}
    +
  • + {% endfor %} + {% for line, errors in result.row_errors %} + {% for error in errors %} +
  • + {% trans "Line number" %}: {{ line }} - {{ error.error }} +
    {{ error.row.values|join:", " }}
    +
    {{ error.traceback|linebreaks }}
    +
  • + {% endfor %} + {% endfor %} +
+ + {% elif result.has_validation_errors %} + +

{% trans "Some rows failed to validate" %}

+ +

{% trans "Please correct these errors in your data where possible, then reupload it using the form above." %}

+ + + + + + + {% for field in result.diff_headers %} + + {% endfor %} + + + + {% for row in result.invalid_rows %} + + + + {% for field in row.values %} + + {% endfor %} + + {% endfor %} + +
{% trans "Row" %}{% trans "Errors" %}{{ field }}
{{ row.number }} + {{ row.error_count }} +
+
    + {% for field_name, error_list in row.field_specific_errors.items %} +
  • + {{ field_name }} +
      + {% for error in error_list %} +
    • {{ error }}
    • + {% endfor %} +
    +
  • + {% endfor %} + {% if row.non_field_specific_errors %} +
  • + {% trans "Non field specific" %} +
      + {% for error in row.non_field_specific_errors %} +
    • {{ error }}
    • + {% endfor %} +
    +
  • + {% endif %} +
+
+
{{ field }}
+ + {% else %} + +

{% trans "Preview" %}

+ + + + + + {% for field in result.diff_headers %} + + {% endfor %} + + + {% for row in result.valid_rows %} + + + {% for field in row.diff %} + + {% endfor %} + + {% endfor %} +
{{ field }}
+ {% if row.import_type == 'new' %} + {% trans "New" %} + {% elif row.import_type == 'skip' %} + {% trans "Skipped" %} + {% elif row.import_type == 'delete' %} + {% trans "Delete" %} + {% elif row.import_type == 'update' %} + {% trans "Update" %} + {% endif %} + {{ field }}
+ + {% endif %} + + {% endif %} +{% endblock %} diff --git a/admin_interface/migrations/0029_auto_20221025_1559.py b/admin_interface/migrations/0029_auto_20221025_1559.py new file mode 100644 index 0000000..2b0405e --- /dev/null +++ b/admin_interface/migrations/0029_auto_20221025_1559.py @@ -0,0 +1,32 @@ +# Generated by Django 4.0.3 on 2022-10-25 13:59 + +from django.conf import settings +import django.db.models.deletion +from django.db import connection, migrations, models, transaction + + +def forward(apps, schema_editor): + Theme = apps.get_model("admin_interface", "Theme") + if len(Theme.objects.all()) == 1 and Theme.objects.first().name == 'Django': + with transaction.atomic(): + cursor = connection.cursor() + cursor.execute("""truncate table "admin_interface_theme" restart identity;""") + +def reverse(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin_interface', '0028_alter_theme_demo'), + ] + + operations = [ + migrations.RunPython(forward, reverse), + migrations.AlterField( + model_name='usertheme', + name='user', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/admin_interface/models.py b/admin_interface/models.py index 090ef13..a485104 100644 --- a/admin_interface/models.py +++ b/admin_interface/models.py @@ -20,8 +20,8 @@ class UserTheme(models.Model): verbose_name = 'Users theme' verbose_name_plural = 'Users themes' - user = models.ForeignKey( - 'auth.User', on_delete=models.CASCADE, null=True, unique=True) + user = models.OneToOneField( + 'auth.User', on_delete=models.CASCADE, null=True) theme = models.ForeignKey('Theme', on_delete=models.CASCADE) @@ -65,8 +65,8 @@ class Theme(models.Model): obj = objs_manager.all().first() if obj: obj.set_active() - else: - obj = objs_manager.create() + # else: + # obj = objs_manager.create() elif objs_active_count == 1: obj = objs_active_ls[0]