diff --git a/django/contatti_app/admin.py b/django/contatti_app/admin.py index 3e856c9..18c9c9b 100644 --- a/django/contatti_app/admin.py +++ b/django/contatti_app/admin.py @@ -12,8 +12,9 @@ from django.contrib import admin from django.core.cache import cache from django.core.paginator import Paginator from django.db.models import F - -from . import models +from django.template.response import TemplateResponse +from django.urls import path +from . import models, views # Modified version of a GIST I found in a SO thread # cfr. http://masnun.rocks/2017/03/20/django-admin-expensive-count-all-queries/ @@ -71,11 +72,43 @@ class CachingPaginator(Paginator): # def has_delete_permission(self, request, obj=None): # return False +class ImportaDaGoogleMixin(): + change_list_template = 'admin/contatti_app/import_google_contacts_button.html' + + def get_model_info(self): + app_label = self.model._meta.app_label + return (app_label, self.model._meta.model_name) + + def get_urls(self): + info = self.get_model_info() + urls = [path('googleimport/', + self.admin_site.admin_view(self.googleimport), + name='%s_%s_googleimport' % info), + path('googleimport/confirm', + self.admin_site.admin_view(self.googleimport_confirm), + name='%s_%s_googleimport_confirm' % info),] + urls += super().get_urls() + return urls + + def googleimport(self, request): + context = dict( self.admin_site.each_context(request), ) + if request.method == 'POST' and request.FILES.get('csv_file'): + app_label, model_name = self.get_model_info() + context.update({'app_label':app_label, 'model_name':model_name}) + context.update(views.googleimport_preview(request)) + return TemplateResponse(request, 'admin/contatti_app/import_google_contacts_preview.html', context) + else: + return TemplateResponse(request, "admin/contatti_app/import_google_contacts.html", context) + + def googleimport_confirm(self, request): + print('QUIIIIII') + pass + # --------------- FINE PREFISSO TEMPLATE --------------- @admin.register(models.ContattoAziendale) -class ContattoAziendaleAdmin(ImportExportModelAdmin, AutocompleteAdmin): +class ContattoAziendaleAdmin(HiddenModel, ImportExportModelAdmin, AutocompleteAdmin): # resource = resources.ContattoAziendaleResource # list_per_page = 15 # paginator = CachingPaginator @@ -84,8 +117,9 @@ class ContattoAziendaleAdmin(ImportExportModelAdmin, AutocompleteAdmin): list_display = ('persona','azienda','is_personale') pass + @admin.register(models.Recapito) -class RecapitoAdmin(ImportExportModelAdmin, PolymorphicParentModelAdmin): +class RecapitoAdmin(ImportaDaGoogleMixin, ImportExportModelAdmin, PolymorphicParentModelAdmin): # resource = resources.RecapitoResource # list_per_page = 15 # paginator = CachingPaginator @@ -201,7 +235,12 @@ class RecapitoInline(StackedPolymorphicInline, DrillDownAutocompleteAdmin): FaxInline, ) - +class AziendaInline(admin.TabularInline): + verbose_name_plural = 'Rapporti con aziende' + model = models.ContattoAziendale + extra = 0 + autocomplete_fields = ('azienda',) + @admin.register(models.PersonaFisica) class PersonaFisicaAdmin(PolymorphicInlineSupportMixin, PolymorphicChildModelAdmin, StackedInlineCollassati,AutocompleteAdmin): # resource = resources.PersonaFisicaResource @@ -211,7 +250,7 @@ class PersonaFisicaAdmin(PolymorphicInlineSupportMixin, PolymorphicChildModelAdm show_in_index = False get_model_perms = lambda self, req: {} search_fields = ('nome','cognome',) - inlines = (RecapitoInline,) + inlines = (RecapitoInline, AziendaInline) @admin.register(models.PersonaGiuridica) @@ -282,7 +321,6 @@ class SoggettoContattabileAdmin(PolymorphicParentModelAdmin): def get_queryset(self, request): qs=super().get_queryset(request).prefetch_related('polymorphic_ctype') return qs - pass @admin.register(models.Indirizzo) @@ -335,7 +373,7 @@ class PersonaInline(admin.TabularInline): extra = 0 @admin.register(models.Societa) -class SocietaAdmin(ImportExportModelAdmin, AutocompleteAdmin): +class SocietaAdmin(ImportExportModelAdmin, AutocompleteAdmin, StackedInlineCollassati): # resource = resources.SocietaResource # list_per_page = 15 # paginator = CachingPaginator diff --git a/django/contatti_app/static/admin/css/import_google_contacts.css b/django/contatti_app/static/admin/css/import_google_contacts.css new file mode 100644 index 0000000..d2b9d3a --- /dev/null +++ b/django/contatti_app/static/admin/css/import_google_contacts.css @@ -0,0 +1,13 @@ +.tabulator-row.tabulator-group.tabulator-group-level-0 { + display: flex; +} + +.contatto_group { + display: flex; + flex-grow: 1; + justify-content: flex-end; +} + +.contatto_group input[type="checkbox"] { + margin-left: 10px; +} \ No newline at end of file diff --git a/django/contatti_app/static/admin/css/vendor/tabulator b/django/contatti_app/static/admin/css/vendor/tabulator new file mode 120000 index 0000000..7d33a42 --- /dev/null +++ b/django/contatti_app/static/admin/css/vendor/tabulator @@ -0,0 +1 @@ +/home/guido/2_external_repo/tabulator/dist/css \ No newline at end of file diff --git a/django/contatti_app/static/admin/js/import_google_contacts.js b/django/contatti_app/static/admin/js/import_google_contacts.js new file mode 100644 index 0000000..897ecfa --- /dev/null +++ b/django/contatti_app/static/admin/js/import_google_contacts.js @@ -0,0 +1,92 @@ +/* eslint-env browser */ +var svg_new = ''; +var svg_exists = ' '; +var svg_ambig_contact = ' '; +var svg_email = ''; +var svg_phone = ''; +var svg_new_contact =' '; +var svg_exists_contact = ' '; +document.addEventListener("DOMContentLoaded", function () { + var table = new Tabulator("#import_preview", { + maxHeight: "80vh", + groupStartOpen: true, + groupToggleElement:"header", + columns: [ + { + "title": "Importare?", + formatter: "rowSelection", titleFormatter: "rowSelection", titleFormatterParams: { + rowRange: "active" //only toggle the values of the active filtered rows + }, hozAlign: "center", headerSort: false + }, + { + "title": "", + "field": "import_status", + formatter: function (cell, formatterParams, onRendered) { + switch (cell.getValue()) { + case true: + return svg_new; + case false: + return svg_exists; + case 'warning': + return svg_ambig_contact; + default: + return cell.getValue(); + } + } + }, + { + "title": "", + "field": "recapito", + "minWidth": 40, + "hozAlign": "center", + "headerWordWrap": false, + "resizable": true, + "sorter": "string", + "headerSort": true, + formatter: function (cell, formatterParams, onRendered) { + switch (cell.getValue()) { + case "email": + return svg_email; + case "telefono": + return svg_phone; + default: + return cell.getValue(); + } + } + }, { + "title": "Recapito", + "field": "valore", + "minWidth": 40, + "headerWordWrap": false, + "resizable": true, + "sorter": "string", + "headerSort": true + }, + // { + // "title": "Importare?", + // "field": "import", + // "minWidth": 40, + // "headerWordWrap": false, + // "resizable": true, + // "sorter": "boolean", + // formatter: "tickCross", + // "headerSort": true + // } + ], + groupBy: function (data) { + return data.nome + " " + data.cognome; + } + }); + window.table = table; + table.on("tableBuilt", function () { + table.setData(contatti); + table.setGroupHeader(function (value, count, data, group) { + //value - the value all members of this group share + //count - the number of rows in this group + //data - an array of all the row data objects in this group + //group - the group component for the group + let nc = data[0].nuovo_contatto? svg_new_contact : svg_exists_contact; + return nc + ' '+ value + '
' + ' ' + " (" + count + (count > 1 ? " recapiti)" : " recapito)") + '
'; //return the header contents + }); + }); +}); diff --git a/django/contatti_app/static/admin/js/vendor/tabulator b/django/contatti_app/static/admin/js/vendor/tabulator new file mode 120000 index 0000000..d95acf1 --- /dev/null +++ b/django/contatti_app/static/admin/js/vendor/tabulator @@ -0,0 +1 @@ +/home/guido/2_external_repo/tabulator/dist/js \ No newline at end of file diff --git a/django/contatti_app/templates/admin/contatti_app/import_google_contacts.html b/django/contatti_app/templates/admin/contatti_app/import_google_contacts.html new file mode 100644 index 0000000..601df9c --- /dev/null +++ b/django/contatti_app/templates/admin/contatti_app/import_google_contacts.html @@ -0,0 +1,8 @@ +{% extends "admin/base_site.html" %} +{% block content %} +
+ {% csrf_token %} + + +
+{% endblock %} \ No newline at end of file diff --git a/django/contatti_app/templates/admin/contatti_app/import_google_contacts_button.html b/django/contatti_app/templates/admin/contatti_app/import_google_contacts_button.html new file mode 100644 index 0000000..fb198ec --- /dev/null +++ b/django/contatti_app/templates/admin/contatti_app/import_google_contacts_button.html @@ -0,0 +1,7 @@ +{% extends "admin/change_list.html" %} +{% load admin_urls %} + +{% block object-tools-items %} +
  • Importa contatti da Google
  • + {{ block.super }} +{% endblock %} diff --git a/django/contatti_app/templates/admin/contatti_app/import_google_contacts_preview.html b/django/contatti_app/templates/admin/contatti_app/import_google_contacts_preview.html new file mode 100644 index 0000000..f0528d0 --- /dev/null +++ b/django/contatti_app/templates/admin/contatti_app/import_google_contacts_preview.html @@ -0,0 +1,27 @@ +{% extends "admin/base_site.html" %} +{% load admin_urls static %} + +{% block content %} +
    + {% csrf_token %} + +
    + +
    +{% endblock %} + +{% block extrastyle %} +{{ block.super }} + + +{% endblock %} + +{% block extrahead %} +{{ block.super }} + + + +{% endblock %} + diff --git a/django/contatti_app/views.py b/django/contatti_app/views.py index 107e53e..7212773 100644 --- a/django/contatti_app/views.py +++ b/django/contatti_app/views.py @@ -1,20 +1,212 @@ -from copy import deepcopy - -from django.shortcuts import render, redirect -from django.http import JsonResponse, HttpResponse -from django.utils.http import url_has_allowed_host_and_scheme -from django.contrib.auth.forms import AuthenticationForm -from django.contrib.auth import authenticate, login, logout -from rest_framework import viewsets -from rest_framework.authentication import SessionAuthentication, BasicAuthentication -from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated - -from django_auto_prefetching import AutoPrefetchViewSetMixin -from . import models -from . import serializers - # def index(request): # return HttpResponse("Hello, %s!" % (request.user.username if request.user.is_authenticated else 'World')) +import csv +import collections +import json +import re +from copy import deepcopy +from io import TextIOWrapper + +from contatti_app.models import Email, PersonaFisica, Telefono, Recapito +from dati_geo_app.models import CAP, Comune +from django_auto_prefetching import AutoPrefetchViewSetMixin +from rest_framework import viewsets +from rest_framework.authentication import (BasicAuthentication, + SessionAuthentication) +from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated + +from django.contrib import messages +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.forms import AuthenticationForm +from django.core.exceptions import ObjectDoesNotExist +from django.db.models import Q +from django.http import HttpResponse, JsonResponse +from django.shortcuts import redirect, render +from django.template.response import TemplateResponse +from django.utils.http import url_has_allowed_host_and_scheme + +from . import models, serializers + + +def iterable_but_not_str(obj): + return isinstance(obj, collections.abc.Iterable) and not isinstance(obj, (str, bytes)) + + +def googleimport_preview(request): + def normalizza_generico(*x): + while iterable_but_not_str(x): + if len(x) == 1: + x = x[0] + else: + return tuple(normalizza_generico(y) for y in x) + return x.strip().lower() + + normalizza_soggetto = normalizza_generico + normalizza_persona = normalizza_generico + normalizza_email = normalizza_generico + + def normalizza_telefono(x): + if iterable_but_not_str(x): + x = x[0] + x=re.sub(r'[^0-9+]', '', x.strip()) + if x.startswith('00'): + x='+'+x[2:] + if not x.startswith('+'): + x='+39'+x + return x + + csv_file = request.FILES['csv_file'] + csv_file_text = TextIOWrapper(csv_file, encoding='utf-8') + reader = csv.DictReader(csv_file_text) + + contacts = [] + + recapiti_db = Recapito.objects.select_related( + 'polymorphic_ctype', + 'soggetto__personafisica', + 'soggetto__personagiuridica' + ).filter( + polymorphic_ctype__model__in = {'email', 'pec', 'telefono', } + ) + + def new_contatto(): + return {'emails': set(), 'telefoni': set()} + + contatti_db = collections.defaultdict( + lambda: collections.defaultdict(new_contatto)) + soggetto_pk_by_recapito = collections.defaultdict(set) + for recapito_valore in recapiti_db: + soggetto = recapito_valore.soggetto + soggetto_pk = soggetto.pk + tipo_sogg = recapito_valore.soggetto.polymorphic_ctype.name + if tipo_sogg == 'persona fisica': + nome = soggetto.personafisica.nome + cognome = soggetto.personafisica.cognome + soggetto_descr = normalizza_persona(nome, cognome) + soggetto_descr_alt = normalizza_soggetto(f'{nome} {cognome}') + contatto = contatti_db[soggetto_descr] + contatti_db[soggetto_descr_alt] = contatto + elif tipo_sogg == 'persona giuridica': + soggetto_descr = normalizza_soggetto( + soggetto.personagiuridica.denominazione) + contatto = contatti_db[soggetto_descr] + else: + raise NotImplementedError + contatto_univoco = contatto[soggetto_pk] + if recapito_valore.polymorphic_ctype.model in {'email', 'pec', }: + recapito_descr = recapito_valore.email.indirizzo_email + contatto_univoco['emails'].add(recapito_descr) + elif recapito_valore.polymorphic_ctype.model in {'telefono', }: + recapito_descr = recapito_valore.telefono.numero + contatto_univoco['telefoni'].add(recapito_descr) + soggetto_pk_by_recapito[recapito_descr] = soggetto_pk + soggetto_pk_by_recapito = dict(soggetto_pk_by_recapito) + contatti_db = {k: dict(v) for k, v in contatti_db.items()} + + warning_altro_sogg_emails = set() + warning_altro_sogg_telefoni = set() + contacts = [] + for row in reader: + nome = row['Given Name'] + cognome = row['Family Name'] + contatto = normalizza_persona(nome, cognome) + contatto_alt = normalizza_soggetto(row['Name']) + soggetti_gia_presenti = {k:v for c in (contatto, contatto_alt) if c in contatti_db for k,v in contatti_db[c].items()} + emails = [row[f'E-mail {n} - Value'] for n in range(1, 4)] + emails = [normalizza_email(y) + for x in emails for y in x.split(':::') if y.strip()] + telefoni = [row[f'Phone {n} - Value'] for n in range(1, 4)] + telefoni = [normalizza_telefono(y) + for x in telefoni for y in x.split(':::') if y.strip()] + for recapiti,tipo_recapito in ( + (emails,'email',), + (telefoni,'telefono') + ): + for recapito_valore in recapiti: + contact = dict() + contacts.append(contact) + contact['recapito']=tipo_recapito + contact['nuovo_contatto'] = not soggetti_gia_presenti + contact['nome'], contact['cognome'] = nome, cognome + contact['full_name'] = contatto_alt + contact['valore']=recapito_valore + if recapito_valore in soggetto_pk_by_recapito: + pk_recapito = soggetto_pk_by_recapito[recapito_valore] + if pk_recapito in soggetti_gia_presenti: + contact['warning']=False + contact['import_status']=False + else: + contact['warning']=True + contact['import_status']='warning' + else: + contact['import_status']=True + + # TODO: considerare anche la casistica in cui esiste lo stesso contatto su diversi soggetti + # già direttamente nel file da importare. + # if warning_altro_sogg_emails: + # emails = Email.objects.all().select_related('soggetto') + # emails = {normalizza_email(x['indirizzo_email']): str( + # x.soggetto) for x in emails} + # for soggetto in contacts: + # for r in soggetto: + # if r['recapito'] == 'email' and r['valore'] in emails: + # r['warning_altro_soggetto'] = emails[r['valore']] + # if warning_altro_sogg_telefoni: + # telefoni = Telefono.objects.all().select_related('soggetto') + # telefoni = {normalizza_telefono(x['numero']): str( + # x.soggetto) for x in telefoni} + # for soggetto in contacts: + # for r in soggetto: + # if r['recapito'] == 'telefono' and r['valore'] in telefoni: + # r['warning_altro_soggetto'] = telefoni[r['valore']] + # contacts = [{**{k: v for k, v in soggetto.items() if k != 'recapiti'}, **recapito} + # for soggetto in contacts for recapito in soggetto['recapiti']] + context = {'contacts': json.dumps(contacts)} + return context + + +def import_google_contacts_confirm(request): + if request.method == 'POST': + selected_indices = [int(key.split('_')[1]) + for key in request.POST if key.startswith('import_')] + csv_file = request.session.get('csv_file') + + if csv_file and selected_indices: + reader = csv.DictReader(csv_file) + contacts = list(reader) + selected_contacts = [contacts[i] for i in selected_indices] + + for row in selected_contacts: + nome = row['Name'] + email = row['E-mail 1 - Value'] + telefono = row['Phone 1 - Value'] + + # Importa il soggetto solo se l'utente lo ha selezionato + if nome and row.get('import') == '1': + try: + soggetto = SoggettoContattabile.objects.get(nome=nome) + # Effettua l'aggiornamento del soggetto + soggetto.email = email + soggetto.telefono = telefono + soggetto.save() + messages.success(request, f'Updated contact: {nome}') + except SoggettoContattabile.DoesNotExist: + # Crea un nuovo soggetto + soggetto = SoggettoContattabile.objects.create( + nome=nome) + if email: + Email.objects.create( + soggetto=soggetto, indirizzo_email=email) + if telefono: + Telefono.objects.create( + soggetto=soggetto, numero=telefono) + messages.success(request, f'Imported contact: {nome}') + + # Reindirizza a una pagina dopo l'importazione + return redirect('admin') + + # Reindirizza se si verifica un errore o non sono stati selezionati contatti + return redirect('import_google_contacts') # --------------- FINE PREFISSO TEMPLATE ---------------