integreat_cms/cms/models/pois/poi.py
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
if TYPE_CHECKING:
from typing import Any
from django.db.models.base import ModelBase
from django.utils.translation import gettext_lazy as _
from linkcheck.models import Link
from ...utils.translation_utils import gettext_many_lazy as __
from ..abstract_content_model import AbstractContentModel
from ..media.media_file import MediaFile
from ..poi_categories.poi_category import POICategory
from ..pois.poi_translation import POITranslation
from ..users.organization import Organization
logger = logging.getLogger(__name__)
def get_default_opening_hours() -> list[dict[str, Any]]:
"""
Return the default opening hours
:return: The default opening hours
"""
return [
{"allDay": False, "closed": True, "appointmentOnly": False, "timeSlots": []}
for _ in range(7)
]
class POI(AbstractContentModel):
"""
Data model representing a point of interest (POI). It contains all relevant data about its exact position, including
coordinates.
"""
address = models.CharField(
max_length=250, verbose_name=_("street and house number")
)
postcode = models.CharField(max_length=10, verbose_name=_("postal code"))
city = models.CharField(max_length=250, verbose_name=_("city"))
country = models.CharField(max_length=250, verbose_name=_("country"))
latitude = models.FloatField(
null=True,
blank=True,
verbose_name=_("latitude"),
help_text=_("The latitude coordinate"),
validators=[MinValueValidator(-90.0), MaxValueValidator(90.0)],
)
longitude = models.FloatField(
null=True,
blank=True,
verbose_name=_("longitude"),
help_text=_("The longitude coordinate"),
validators=[MinValueValidator(-180.0), MaxValueValidator(180.0)],
)
location_on_map = models.BooleanField(
default=False,
verbose_name=_("Show this location on map"),
help_text=_("Tick if you want to show this location on map"),
)
icon = models.ForeignKey(
MediaFile,
verbose_name=_("icon"),
on_delete=models.SET_NULL,
blank=True,
null=True,
help_text=_("The best results are achieved with images in 16:9 aspect ratio."),
)
archived = models.BooleanField(
default=False,
verbose_name=_("archived"),
help_text=_("Whether or not the location is read-only and hidden in the API."),
)
website = models.URLField(max_length=250, blank=True, verbose_name=_("website"))
email = models.EmailField(
blank=True,
verbose_name=_("email address"),
)
phone_number = models.CharField(
max_length=250, blank=True, verbose_name=_("phone number")
)
category = models.ForeignKey(
POICategory,
on_delete=models.PROTECT,
related_name="pois",
verbose_name=_("category"),
)
temporarily_closed = models.BooleanField(
default=False,
verbose_name=_("temporarily closed"),
help_text=__(
_("Whether or not the location is temporarily closed."),
_("The opening hours remain and are only hidden."),
),
)
appointment_url = models.URLField(
max_length=500,
blank=True,
verbose_name=_("appointment link"),
help_text=_(
"Link to an external website where an appointment for this location can be made."
),
)
opening_hours = models.JSONField(
default=get_default_opening_hours,
verbose_name=_("opening hours"),
)
organization = models.ForeignKey(
Organization,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="pois",
verbose_name=_("organization"),
help_text=_("Specify which organization operates this location."),
)
barrier_free = models.BooleanField(
null=True,
default=None,
verbose_name=_("barrier free"),
help_text=_("Indicate if the location is barrier free."),
)
@property
def fallback_translations_enabled(self) -> bool:
"""
Whether translations should be returned in the default language if they do not exist
:return: Whether fallback translations are enabled
"""
return self.region.fallback_translations_enabled
@staticmethod
def get_translation_model() -> ModelBase:
"""
Returns the translation model of this content model
:return: The class of translations
"""
return POITranslation
def delete(self, *args: list, **kwargs: dict) -> bool:
r"""
Deletes the poi
:param \*args: The supplied arguments
:param \**kwargs: The supplied keyword arguments
"""
was_successful = False
if not self.is_used:
super().delete(*args, **kwargs)
was_successful = True
else:
logger.debug(
"Can't be deleted because this poi is used by an event or a contact"
)
return was_successful
def archive(self) -> bool:
"""
Archives the poi and removes all links of this poi from the linkchecker
"""
was_successful = False
if not self.is_used:
self.archived = True
self.save()
# Delete related link objects as they are no longer required
Link.objects.filter(poi_translation__poi=self).delete()
was_successful = True
else:
logger.debug(
"Can't be archived because this poi is used by an event or a contact"
)
return was_successful
def restore(self) -> None:
"""
Restores the poi and adds all links of this poi back
"""
self.archived = False
self.save()
# Restore related link objects
for translation in self.translations.distinct("poi__pk", "language__pk"):
# The post_save signal will create link objects from the content
translation.save(update_timestamp=False)
@property
def is_used(self) -> bool:
"""
:return: whether this poi is used by another model
"""
return self.events.exists() or self.contacts.exists()
class Meta:
#: The verbose name of the model
verbose_name = _("location")
#: The plural verbose name of the model
verbose_name_plural = _("locations")
#: The name that will be used by default for the relation from a related object back to this one
default_related_name = "pois"
#: The default permissions for this model
default_permissions = ("change", "delete", "view")
#: The fields which are used to sort the returned objects of a QuerySet
ordering = ["pk"]