From 81e9fa33bd2f586bc99b24e8dc0951faed0f134d Mon Sep 17 00:00:00 2001 From: MizukiTemma Date: Fri, 3 Jan 2025 17:18:30 +0100 Subject: [PATCH] Suggest users potential contacts Co-authored-by: JoeyStk <72705147+JoeyStk@users.noreply.github.com> --- .../contact_from_email_and_phone.html | 61 ++++++++ .../contact_from_email_and_phone_row.html | 35 +++++ .../cms/templates/contacts/contact_list.html | 34 +++-- integreat_cms/cms/urls/protected.py | 5 + integreat_cms/cms/utils/link_utils.py | 19 +++ integreat_cms/cms/views/contacts/__init__.py | 1 + .../cms/views/contacts/contact_form_view.py | 3 + .../contacts/contact_from_existing_data.py | 134 ++++++++++++++++++ integreat_cms/locale/de/LC_MESSAGES/django.po | 60 ++++++-- 9 files changed, 328 insertions(+), 24 deletions(-) create mode 100644 integreat_cms/cms/templates/contacts/contact_from_email_and_phone.html create mode 100644 integreat_cms/cms/templates/contacts/contact_from_email_and_phone_row.html create mode 100644 integreat_cms/cms/views/contacts/contact_from_existing_data.py diff --git a/integreat_cms/cms/templates/contacts/contact_from_email_and_phone.html b/integreat_cms/cms/templates/contacts/contact_from_email_and_phone.html new file mode 100644 index 0000000000..c043d59ff2 --- /dev/null +++ b/integreat_cms/cms/templates/contacts/contact_from_email_and_phone.html @@ -0,0 +1,61 @@ +{% extends "_base.html" %} +{% load i18n %} +{% load static %} +{% block content %} +
+

+ {% translate "Potential contact data" %} +

+
+
+ + {% translate "Here is the list of E-mail addresses and phone numbers that are embedded in contents. We recommend to replace them with a contact card so they can be centrally managed and updated in future." %} + +
+
+
+

+ {% translate "Pages" %} +

+ {% if links_per_page %} + {{ links_per_page|length }}{% translate " pages have potential contacts." %} + {% endif %} +
+ {% for content, links, contacts in links_per_page %} + {% include "contacts/contact_from_email_and_phone_row.html" with collapsed=True %} + {% empty %} + {% translate "No E-mail address and phone number detected" %} + {% endfor %} +
+
+
+

+ {% translate "Events" %} +

+ {% if links_per_event %} + {{ links_per_event|length }}{% translate " events have potential contacts." %} + {% endif %} +
+ {% for content, links, contacts in links_per_event %} + {% include "contacts/contact_from_email_and_phone_row.html" with collapsed=True %} + {% empty %} + {% translate "No E-mail address and phone number detected" %} + {% endfor %} +
+
+
+

+ {% translate "Locations" %} +

+ {% if links_per_poi %} + {{ links_per_poi|length }}{% translate " events have potential contacts." %} + {% endif %} +
+ {% for content, links, contacts in links_per_poi %} + {% include "contacts/contact_from_email_and_phone_row.html" with collapsed=True %} + {% empty %} + {% translate "No E-mail address and phone number detected" %} + {% endfor %} +
+ {% include "pagination.html" with chunk=contacts %} +{% endblock content %} diff --git a/integreat_cms/cms/templates/contacts/contact_from_email_and_phone_row.html b/integreat_cms/cms/templates/contacts/contact_from_email_and_phone_row.html new file mode 100644 index 0000000000..cfd1e86513 --- /dev/null +++ b/integreat_cms/cms/templates/contacts/contact_from_email_and_phone_row.html @@ -0,0 +1,35 @@ +{% extends "../_collapsible_box.html" %} +{% load i18n %} +{% load content_filters %} +{% load page_filters %} +{% load tree_filters %} +{% load static %} +{% load rules %} +{% block collapsible_box_icon %} +{% endblock collapsible_box_icon %} +{% block collapsible_box_title %} + {{ content.best_translation.title }} + +{% endblock collapsible_box_title %} +{% block collapsible_box_content %} +
+
+ {% for link in links %} +
+ {{ link }} + {% translate "Convert to contact" %} +
+ {% endfor %} +
+
+ {% for contact in contacts %} +
+ {{ contact }} +
+ {% empty %} + {% trans "There is no contact suggested." %} + {% endfor %} +
+
+{% endblock collapsible_box_content %} diff --git a/integreat_cms/cms/templates/contacts/contact_list.html b/integreat_cms/cms/templates/contacts/contact_list.html index 3b1cc2bb74..a345754866 100644 --- a/integreat_cms/cms/templates/contacts/contact_list.html +++ b/integreat_cms/cms/templates/contacts/contact_list.html @@ -11,21 +11,27 @@

{% translate "Contacts" %} {% endif %}

- {% if is_archive %} - - {% translate "Back to contacts" %} +
+ + {% trans "Analysis of content for existing contacts" %} - {% else %} - - - - {% translate "Archived contacts" %} - ({{ archived_count }}) - - - {% endif %} + {% if is_archive %} + + {% translate "Back to contacts" %} + + {% else %} + + + + {% translate "Archived contacts" %} + ({{ archived_count }}) + + + {% endif %} +
{% if perms.cms.change_contact and not is_archive %} diff --git a/integreat_cms/cms/urls/protected.py b/integreat_cms/cms/urls/protected.py index ad74ba5f3c..f7456a5fe8 100644 --- a/integreat_cms/cms/urls/protected.py +++ b/integreat_cms/cms/urls/protected.py @@ -1519,6 +1519,11 @@ ] ), ), + path( + "potential_targets", + contacts.PotentialContactSourcesView.as_view(), + name="potential_targets", + ), ] ), ), diff --git a/integreat_cms/cms/utils/link_utils.py b/integreat_cms/cms/utils/link_utils.py index 42207878c3..c57d619cbd 100644 --- a/integreat_cms/cms/utils/link_utils.py +++ b/integreat_cms/cms/utils/link_utils.py @@ -1,8 +1,12 @@ from __future__ import annotations import re +from typing import TYPE_CHECKING from urllib.parse import ParseResult, unquote, urlparse +if TYPE_CHECKING: + from linkcheck.models import Url + def fix_domain_encoding(url: re.Match[str]) -> str: """ @@ -24,3 +28,18 @@ def fix_content_link_encoding(content: str) -> str: :return: The fixed content """ return re.sub(r"(?<=[\"'])(https?://.+?)(?=[\"'])", fix_domain_encoding, content) + + +def clean_url(url: Url) -> str: + """ + Remove the prefix tel: and mailto: + + :param url: The URL object + :return: URL string without prefix + """ + + if url.type == "phone": + return url.url.lstrip("tel:") + if url.type == "mailto": + return url.url[7:] + return url.url diff --git a/integreat_cms/cms/views/contacts/__init__.py b/integreat_cms/cms/views/contacts/__init__.py index eaa76fff83..ada0b7b16c 100644 --- a/integreat_cms/cms/views/contacts/__init__.py +++ b/integreat_cms/cms/views/contacts/__init__.py @@ -11,4 +11,5 @@ ) from .contact_form_ajax_view import ContactFormAjaxView from .contact_form_view import ContactFormView +from .contact_from_existing_data import PotentialContactSourcesView from .contact_list_view import ContactListView diff --git a/integreat_cms/cms/views/contacts/contact_form_view.py b/integreat_cms/cms/views/contacts/contact_form_view.py index 87cea1bf43..a62c346340 100644 --- a/integreat_cms/cms/views/contacts/contact_form_view.py +++ b/integreat_cms/cms/views/contacts/contact_form_view.py @@ -39,6 +39,7 @@ def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: :param \**kwargs: The supplied keyword arguments :return: The rendered template response """ + link = request.GET.get("link", None) region = request.region contact_instance = Contact.objects.filter( id=kwargs.get("contact_id"), location__region=region @@ -107,6 +108,8 @@ def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: else None ) + contact_form.fields["phone_number"].initial = link if link else None + return render( request, self.template_name, diff --git a/integreat_cms/cms/views/contacts/contact_from_existing_data.py b/integreat_cms/cms/views/contacts/contact_from_existing_data.py new file mode 100644 index 0000000000..6957fdd684 --- /dev/null +++ b/integreat_cms/cms/views/contacts/contact_from_existing_data.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from django.contrib.contenttypes.models import ContentType +from django.db.models import Q +from django.shortcuts import render +from django.views.generic.base import TemplateView + +from integreat_cms.cms.utils.link_utils import clean_url + +from ...models import Contact, Event, Page, POI +from ...utils.linkcheck_utils import get_region_links + +if TYPE_CHECKING: + from typing import Any + + from django.http import HttpRequest, HttpResponse + from django.template.response import TemplateResponse + + +class PotentialContactSourcesView(TemplateView): + # pylint: disable=too-many-locals + """ + View for the contact suggestion + """ + + template_name = "contacts/contact_from_email_and_phone.html" + extra_context = { + "current_menu_item": "contacts", + } + + def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> TemplateResponse: + r""" + Render the contact suggestion + + :param request: Object representing the user call + :param \*args: The supplied arguments + :param \**kwargs: The supplied keyword arguments + :return: The rendered template response + """ + + region_links = get_region_links(request.region) + email_or_phone_links = region_links.filter( + Q(url__url__startswith="mailto") | Q(url__url__startswith="tel") + ) + + page_translation_content_type_id = ContentType.objects.get( + model="pagetranslation" + ).id + event_translation_content_type_id = ContentType.objects.get( + model="eventtranslation" + ).id + poi_translation_content_type_id = ContentType.objects.get( + model="poitranslation" + ).id + + links_in_pages = email_or_phone_links.filter( + content_type=page_translation_content_type_id + ) + links_in_events = email_or_phone_links.filter( + content_type=event_translation_content_type_id + ) + links_in_pois = email_or_phone_links.filter( + content_type=poi_translation_content_type_id + ) + + page_ids = ( + links_in_pages.order_by() + .values_list("page_translation__page", flat=True) + .distinct() + ) + links_per_page = [] + for page_id in page_ids: + page = Page.objects.filter(id=page_id).first() + links = [ + clean_url(link.url) + for link in links_in_pages.filter(page_translation__page=page_id) + .order_by() + .distinct("url__url") + ] + contacts = Contact.objects.filter( + Q(email__in=links) | Q(phone_number__in=links) + ) + links_per_page += [(page, links, contacts)] + + event_ids = ( + links_in_events.order_by() + .values_list("event_translation__event", flat=True) + .distinct() + ) + links_per_event = [] + for event_id in event_ids: + event = Event.objects.filter(id=event_id).first() + links = [ + clean_url(link.url) + for link in links_in_events.filter(event_translation__event=event_id) + .order_by() + .distinct("url__url") + ] + contacts = Contact.objects.filter( + Q(email__in=links) | Q(phone_number__in=links) + ) + links_per_event += [(event, links, contacts)] + + poi_ids = ( + links_in_pois.order_by() + .values_list("poi_translation__poi", flat=True) + .distinct() + ) + links_per_poi = [] + for poi_id in poi_ids: + poi = POI.objects.filter(id=poi_id).first() + links = [ + clean_url(link.url) + for link in links_in_pois.filter(poi_translation__poi=poi_id) + .order_by() + .distinct("url__url") + ] + contacts = Contact.objects.filter( + Q(email__in=links) | Q(phone_number__in=links) + ) + links_per_poi += [(poi, links, contacts)] + + return render( + request, + self.template_name, + { + **self.get_context_data(**kwargs), + "links_per_page": links_per_page, + "links_per_event": links_per_event, + "links_per_poi": links_per_poi, + }, + ) diff --git a/integreat_cms/locale/de/LC_MESSAGES/django.po b/integreat_cms/locale/de/LC_MESSAGES/django.po index 7b8658860d..c8a0f950ca 100644 --- a/integreat_cms/locale/de/LC_MESSAGES/django.po +++ b/integreat_cms/locale/de/LC_MESSAGES/django.po @@ -3089,6 +3089,7 @@ msgstr "Impressums-Feedback" #: cms/models/feedback/map_feedback.py cms/models/regions/region.py #: cms/templates/_base.html cms/templates/contacts/contact_form.html +#: cms/templates/contacts/contact_from_email_and_phone.html #: cms/templates/organizations/organization_form.html #: cms/templates/pois/poi_list.html msgid "Locations" @@ -4229,12 +4230,14 @@ msgstr "verbrauchtes Budget" #: cms/models/regions/region.py cms/templates/_base.html #: cms/templates/contacts/contact_form.html +#: cms/templates/contacts/contact_from_email_and_phone.html #: cms/templates/organizations/organization_form.html msgid "Pages" msgstr "Seiten" #: cms/models/regions/region.py cms/templates/_base.html #: cms/templates/contacts/contact_form.html +#: cms/templates/contacts/contact_from_email_and_phone.html #: cms/templates/events/event_list.html msgid "Events" msgstr "Veranstaltungen" @@ -5407,10 +5410,48 @@ msgstr "Kontakt wiederherstellen" msgid "Delete contact" msgstr "Kontakt löschen" +#: cms/templates/contacts/contact_from_email_and_phone.html +msgid "Potential contact data" +msgstr "Mögliche Kontaktdaten" + +#: cms/templates/contacts/contact_from_email_and_phone.html +msgid "" +"Here is the list of E-mail addresses and phone numbers that are embedded in " +"contents. We recommend to replace them with a contact card so they can be " +"centrally managed and updated in future." +msgstr "" +"Hier ist die Liste der E-Mail-Adressen und Telefonnummern, die in Inhalte " +"eingebettet sind. Wir empfehlen, diese durch eine Kontaktkarte zu ersetzen, " +"damit sie künftig zentral verwaltet und aktualisiert werden können." + +#: cms/templates/contacts/contact_from_email_and_phone.html +msgid " pages have potential contacts." +msgstr " Seiten haben mögliche Kontakte." + +#: cms/templates/contacts/contact_from_email_and_phone.html +msgid "No E-mail address and phone number detected" +msgstr "Keine E-Mail-Ad­res­se und Te­le­fon­num­mer gefunden." + +#: cms/templates/contacts/contact_from_email_and_phone.html +msgid " events have potential contacts." +msgstr " Veranstaltungen haben mögliche Kontakte." + +#: cms/templates/contacts/contact_from_email_and_phone_row.html +msgid "Convert to contact" +msgstr "Zu Kontakt konvertieren" + +#: cms/templates/contacts/contact_from_email_and_phone_row.html +msgid "There is no contact suggested." +msgstr "Es wird kein Kontakt vorgeschlagen." + #: cms/templates/contacts/contact_list.html msgid "Archived Contacts" msgstr "Archivierte Kontakte" +#: cms/templates/contacts/contact_list.html +msgid "Analysis of content for existing contacts" +msgstr "Inhaltsanalyse für bestehende Kontaktdaten" + #: cms/templates/contacts/contact_list.html msgid "Back to contacts" msgstr "Zurück zu Kontakte" @@ -11325,6 +11366,9 @@ msgstr "" #~ msgid "Event does not come from an external calendar" #~ msgstr "Veranstaltung kommt nicht aus einem externen Kalender" +#~ msgid "There is no page yet." +#~ msgstr "Es gibt keinen Seiteninhalt." + #~ msgid "Invalid links" #~ msgstr "Ungültige Links" @@ -11458,17 +11502,16 @@ msgstr "" #~ "Wir laden im Hintergrund gerade Ihre veralteten und fehlenden " #~ "Übersetzungen. Bitte haben Sie noch ein wenig Geduld." -#, python-format #~ msgid "" -#~ "Your pages currently have " -#~ "%(number_of_missing_or_outdated_translations)s pages that have an " +#~ "Your pages currently have " +#~ "%(number_of_missing_or_outdated_translations)s pages that have an " #~ "outdated or no translation. In order for the users to benefit from your " #~ "content you should translate them or have them translated." #~ msgstr "" -#~ "Ihre Inhalten haben aktuell " -#~ "%(number_of_missing_or_outdated_translations)s Seiten mit " -#~ "fehlender oder veralteter Übersetzung. Damit die Nutzer:innen von Ihren " -#~ "Inhalten profitieren können, sollten Sie diese übersetzen (lassen)." +#~ "Ihre Inhalten haben aktuell " +#~ "%(number_of_missing_or_outdated_translations)s Seiten mit fehlender " +#~ "oder veralteter Übersetzung. Damit die Nutzer:innen von Ihren Inhalten " +#~ "profitieren können, sollten Sie diese übersetzen (lassen)." #~ msgid "View location" #~ msgstr "Ort ansehen" @@ -11863,9 +11906,6 @@ msgstr "" #~ msgid "Status message" #~ msgstr "Status-Nachricht" -#~ msgid "There is no page yet." -#~ msgstr "Es gibt keinen Seiteninhalt." - #~ msgid "Current machine translation" #~ msgstr "Die Übersetzung ist aktuell und wurde maschinell erzeugt"