Skip to content

Commit

Permalink
TinyMCE: Insert contact, but track using linkcheck, v2
Browse files Browse the repository at this point in the history
  • Loading branch information
charludo committed Dec 2, 2024
1 parent 36b8c05 commit af2ce21
Show file tree
Hide file tree
Showing 20 changed files with 437 additions and 457 deletions.
129 changes: 94 additions & 35 deletions integreat_cms/cms/models/contact/contact.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@
from typing import TYPE_CHECKING

from django.conf import settings
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector
from django.db import models
from django.db.models import Q
from django.utils import timezone
from django.utils.functional import cached_property, classproperty
from django.utils.translation import gettext_lazy as _
from linkcheck.models import Link

from ..abstract_base_model import AbstractBaseModel
from ..events.event_translation import EventTranslation
from ..pages.page_translation import PageTranslation
from ..pois.poi import POI
from ..pois.poi_translation import POITranslation
from ..regions.region import Region

if TYPE_CHECKING:
Expand Down Expand Up @@ -57,10 +62,6 @@ class Contact(AbstractBaseModel):
default=timezone.now, verbose_name=_("creation date")
)

_url_regex = re.compile(
r"^https:\/\/integreat\.app\/([^/?#]+)\/contact\/([0-9]+)\/"
)

@cached_property
def region(self) -> Region:
"""
Expand All @@ -78,30 +79,19 @@ def search(cls, region: Region, query: str) -> QuerySet:
:param query: The query string used for filtering the contacts
:return: A query for all matching objects
"""
searchable_fields = ("point_of_contact_for", "name", "email", "phone_number", "website")

q = models.Q()

for word in query.split():
# Every word has to appear in at least one field
OR = [
models.Q(**{f"{field}__icontains": word}) for field in searchable_fields
]
# We OR whether it appears in each of the field, and
# AND those expressions corresponding to each word
# because we are not interested in objects where one word is missing
q &= reduce(lambda a, b: a | b, OR)

# We could add annotations to determine how closely each result matches the query,
# e.g. by finding the length of the longest common substring between each field and the original query,
# taking the square of that value to obtain something representing the "contribution" of that field
# (so longer matches in only a few fields get a much higher value than many short matches all over)
# and then summing those together to obtain an overall score of how relevant that object is to the query,
# but that would require us find the longest common substring on the db level,
# and that feels a bit overkill for now (it will likely be confusing to re-discover and maintain,
# especially if we were to also employ fuzzy matching – which would be much preferred, if we can do it)

return cls.objects.filter(q, location__region=region)
vector = SearchVector(
"name",
"email",
"phone_number",
"website",
"point_of_contact_for",
)
query = SearchQuery(query)
return (
Contact.objects.filter(location__region=region, archived=False)
.annotate(rank=SearchRank(vector, query))
.order_by("-rank")
)

def __str__(self) -> str:
"""
Expand Down Expand Up @@ -145,6 +135,78 @@ def get_repr(self) -> str:
"""
return f"<Contact (id: {self.id}, point of contact for: {self.point_of_contact_for}, name: {self.name}, region: {self.region.slug})>"

@cached_property
def get_repr_short(self) -> str:
"""
Returns a short representation only contaiing the relevant data, no field names.
:return: The short representation of the contact
"""
point_of_contact_for = (
f"{self.point_of_contact_for}: " if self.point_of_contact_for else ""
)
name = f"{self.name} " if self.name else ""
details = [
detail for detail in [self.email, self.phone_number, self.website] if detail
]
details_repr = f"({', '.join(details)})" if details else ""

return f"{point_of_contact_for}{name}{details_repr}".strip()

@cached_property
def referring_page_translations(self) -> QuerySet[PageTranslation]:
"""
Returns a queryset containing all :class:`~integreat_cms.cms.models.pages.page_translation.PageTranslation` objects which reference this contact
:return: all PageTranslation objects referencing this contact
"""
from ...linklists import PageTranslationLinklist

return PageTranslation.objects.filter(
id__in=(
Link.objects.filter(
url__url=self.full_url,
content_type=PageTranslationLinklist.content_type(),
).values_list("object_id", flat=True)
),
)

@cached_property
def referring_poi_translations(self) -> QuerySet[POITranslation]:
"""
Returns a queryset containing all :class:`~integreat_cms.cms.models.pois.poi_translation.POITranslation` objects which reference this contact
:return: all POITranslation objects referencing this contact
"""
from ...linklists import POITranslationLinklist

return POITranslation.objects.filter(
id__in=(
Link.objects.filter(
url__url=self.full_url,
content_type=POITranslationLinklist.content_type(),
).values_list("object_id", flat=True)
),
)

@cached_property
def referring_event_translations(self) -> QuerySet[EventTranslation]:
"""
Returns a queryset containing all :class:`~integreat_cms.cms.models.events.event_translation.EventTranslation` objects which reference this contact
:return: all EventTranslation objects referencing this contact
"""
from ...linklists import EventTranslationLinklist

return EventTranslation.objects.filter(
id__in=(
Link.objects.filter(
url__url=self.full_url,
content_type=EventTranslationLinklist.content_type(),
).values_list("object_id", flat=True)
),
)

def archive(self) -> None:
"""
Archives the contact
Expand All @@ -168,10 +230,6 @@ def copy(self) -> None:
self.point_of_contact_for = self.point_of_contact_for + " " + _("(Copy)")
self.save()

@classproperty
def url_regex(cls) -> re.Pattern:
return cls._url_regex

@cached_property
def url_prefix(self) -> str:
"""
Expand Down Expand Up @@ -213,8 +271,8 @@ def base_link(self) -> str:
:return: the base link of the content
"""
if not self.id:
return settings.WEBAPP_URL + "/"
return settings.WEBAPP_URL + self.url_prefix
return settings.BASE_URL + "/"
return settings.BASE_URL + self.url_prefix

def get_absolute_url(self) -> str:
"""
Expand All @@ -241,7 +299,8 @@ def full_url(self) -> str:
:return: The full url
"""
return settings.WEBAPP_URL + self.get_absolute_url()
# f"{settings.WEBAPP_URL}/{self.location.region.slug}/contact/{self.id}/"
return settings.BASE_URL + self.get_absolute_url()

class Meta:
verbose_name = _("contact")
Expand Down
3 changes: 1 addition & 2 deletions integreat_cms/cms/templates/_tinymce_config.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,9 @@
data-contact-icon-text='{% translate "Contact Person" %}'
data-contact-icon-src="{% get_base_url %}{% static 'svg/contact.svg' %}"
data-contact-icon-alt="{% translate "Contact Person" %}"
data-contact-ajax-url="{% url 'search_content_ajax' region_slug=request.region.slug language_slug=language.slug %}"
data-contact-ajax-url="{% url 'search_contact_ajax' region_slug=request.region.slug %}"
data-contact-menu-text='{% translate "Contact..." %}'
data-contact-no-results-text='{% translate "no results" %}'
data-contact-url-regex="{{ contact_url_regex }}"
data-speech-icon-text='{% translate "Spoken Languages" %}'
data-speech-icon-src="{% get_base_url %}{% static 'svg/speech.svg' %}"
data-speech-icon-alt="{% translate "Spoken Languages" %}"
Expand Down
76 changes: 36 additions & 40 deletions integreat_cms/cms/templates/contacts/contact_card.html
Original file line number Diff line number Diff line change
@@ -1,43 +1,31 @@
{% load settings_tags %}
{% load static %}
{% spaceless %}
<div contenteditable="false"
{% if not contact %} data-invalid="true" {% endif %}
style="
display: inline-block;
box-sizing: border-box;
min-width: 50%;
padding: 0.1em 1em;
border-radius: 0.3em;
background: rgba(127, 127, 127, 0.15);
box-shadow: 0 .1em .1em rgba(0,0,0,0.4);
cursor: default !important;
color: initial;
text-decoration: initial;
background-image: linear-gradient(to right, rgba(255,255,255,0.9) 0 100%), url({% get_static_prefix %}/svg/contact.svg) !important;
background-blend-mode: difference;
background-position: calc(100% + 2em) calc(100% + 1em);
background-size: 7em;
background-repeat: no-repeat;
">
<a href={{ contact.full_url }}
style="
opacity: 0;
position: absolute;
font-size: 0;
">Contact</a>
<div contenteditable="false"
data-contact-id="{{ contact.pk }}"
data-contact-url="{{ contact.full_url }}"
class="contact-card">
<a href={{ contact.full_url }} class="marker-link">Contact</a>
{% if contact %}
<h4>{% if contact.point_of_contact_for and contact.point_of_contact_for.strip %}
<h4>
{% if contact.point_of_contact_for and contact.point_of_contact_for.strip %}
{{ contact.point_of_contact_for.strip }}:
{% endif %}{% if contact.name and contact.name.strip %}<span class="notranslate" dir="ltr" translate="no">{{ contact.name.strip }}
{% endif %}
{% if contact.name and contact.name.strip %}
<span class="notranslate" dir="ltr" translate="no">{{ contact.name.strip }}
</span>
{% endif %}
</h4>
{% if contact.email and contact.email.strip %}
<p>
<img src="{% get_static_prefix %}/svg/email.svg"
alt="Email"
style="width: 15px; height: 15px;"
/>
<picture>
<source srcset="{% get_base_url %}{% static "svg/email.svg" %}" type="image/svg+xml">
<img src="{% get_static_prefix %}/svg/email.svg"
alt="Email: "
width="15"
height="15" />
</picture>
&nbsp;
<a href="mailto:{{ contact.email.strip }}">
<span class="notranslate" dir="ltr" translate="no">
{{ contact.email.strip }}
Expand All @@ -47,10 +35,14 @@ <h4>{% if contact.point_of_contact_for and contact.point_of_contact_for.strip %}
{% endif %}
{% if contact.phone_number and contact.phone_number.strip %}
<p>
<img src="{% get_static_prefix %}/svg/call.svg"
alt="Phone Number"
style="width: 15px; height: 15px;"
/>
<picture>
<source srcset="{% get_base_url %}{% static "svg/call.svg" %}" type="image/svg+xml">
<img src="{% get_static_prefix %}/svg/call.svg"
alt="Phone Number: "
width="15"
height="15" />
</picture>
&nbsp;
<a href="tel:{{ contact.phone_number.strip }}">
<span class="notranslate" dir="ltr" translate="no">
{{ contact.phone_number.strip }}
Expand All @@ -60,10 +52,14 @@ <h4>{% if contact.point_of_contact_for and contact.point_of_contact_for.strip %}
{% endif %}
{% if contact.website and contact.website.strip %}
<p>
<img src="{% get_static_prefix %}/svg/www.svg"
alt="Email"
style="width: 15px; height: 15px;"
/>
<picture>
<source srcset="{% get_base_url %}{% static "svg/www.svg" %}" type="image/svg+xml">
<img src="{% get_static_prefix %}/svg/www.svg"
alt="Website: "
width="15"
height="15" />
</picture>
&nbsp;
<a href="{{ contact.website.strip }}">
<span class="notranslate" dir="ltr" translate="no">
{{ contact.website.strip }}
Expand All @@ -72,5 +68,5 @@ <h4>{% if contact.point_of_contact_for and contact.point_of_contact_for.strip %}
</p>
{% endif %}
{% endif %}
</div>
</div>
{% endspaceless %}
25 changes: 16 additions & 9 deletions integreat_cms/cms/urls/protected.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,7 @@
POITranslationForm,
RegionForm,
)
from ..models import (
Event,
Language,
OfferTemplate,
Page,
POI,
POICategory,
Role,
)
from ..models import Event, Language, OfferTemplate, Page, POI, POICategory, Role
from ..views import (
analytics,
bulk_action_views,
Expand Down Expand Up @@ -759,6 +751,11 @@
utils.search_content_ajax,
name="search_content_ajax",
),
path(
"search/contact/",
utils.search_contact_ajax,
name="search_contact_ajax",
),
path(
"dismiss-tutorial/<slug:slug>/",
settings.DismissTutorial.as_view(),
Expand Down Expand Up @@ -1446,6 +1443,16 @@
"<int:contact_id>/",
include(
[
path(
"",
utils.get_contact,
name="get_contact",
),
path(
"raw/",
utils.get_contact_raw,
name="get_contact_raw",
),
path(
"edit/",
contacts.ContactFormView.as_view(),
Expand Down
Loading

0 comments on commit af2ce21

Please sign in to comment.