A set of models, an admin and utilities for frequently used patterns. https://docs.franco.net.eu.org/django-futils/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
django-futils/django_futils/abstract_models.py

397 lines
13 KiB

#
# abstract_models.py
#
# Copyright (C) 2020-2021 Franco Masotti (franco \D\o\T masotti {-A-T-} tutanota \D\o\T com)
#
# This file is part of django-futils.
#
# django-futils is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# django-futils is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with django-futils. If not, see <http://www.gnu.org/licenses/>.
#
from django.conf import settings
from django.contrib.gis.db import models as gis_models
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django_countries.fields import CountryField
from phone_field import PhoneField
from simple_history.models import HistoricalRecords
from vies.models import VATINField
import django_futils.constants as const
from .utils import (get_address_data, personattachment_directory_path,
save_primary, set_primary_next_element)
class AbstractRecordTimestamps(models.Model):
added = models.DateTimeField(_('added'), auto_now_add=True)
updated = models.DateTimeField(_('updated'), auto_now=True)
history = HistoricalRecords(inherit=True)
class Meta:
abstract = True
class AbstractBasicElement(models.Model):
code = models.CharField(_('code'),
max_length=const.CODE_LENGTH,
unique=True)
description = models.TextField(_('description'), blank=True, null=True)
history = HistoricalRecords(inherit=True)
class Meta:
abstract = True
########################
# AbstractType classes #
########################
class AbstractType(AbstractBasicElement):
type = models.CharField(_('type'),
max_length=const.NAME_LENGTH,
db_index=True)
def __str__(self):
return self.type
class Meta:
abstract = True
class AbstractAddressType(AbstractType):
class Meta:
abstract = True
verbose_name = _('address type')
verbose_name_plural = _('address types')
class AbstractEmailType(AbstractType):
class Meta:
abstract = True
verbose_name = _('email type')
verbose_name_plural = _('email types')
class AbstractTelephoneType(AbstractType):
class Meta:
abstract = True
verbose_name = _('telephone type')
verbose_name_plural = _('telephone types')
class AbstractAttachmentType(AbstractType):
class Meta:
abstract = True
verbose_name = _('attachment type')
verbose_name_plural = _('attachment types')
###########################
# AbstractElement classes #
###########################
class AbstractElement(AbstractBasicElement):
name = models.CharField(_('name'),
max_length=const.NAME_LENGTH,
db_index=True)
def __str__(self):
return self.name
class Meta:
abstract = True
class AbstractMunicipality(AbstractElement):
r"""This is the equivalent of:
'comune' in 'IT'
"""
country = CountryField(_('country'), default='IT')
class Meta:
abstract = True
verbose_name = _('municipality')
verbose_name_plural = _('municipalities')
##################
# Normal classes #
##################
class AbstractCommonAttachment(AbstractRecordTimestamps):
name = models.CharField(_('name'), max_length=const.NAME_LENGTH)
notes = models.TextField(_('notes'), blank=True, null=True)
class Meta:
abstract = True
def __str__(self):
return self.name
class AbstractHasPrimaryQuerySet(models.QuerySet):
# This operation is slow for bulk deletes
def delete(self, *args, **kwargs):
for obj in self:
if not obj._can_be_empty and obj.is_primary:
raise ValidationError(_('cannot delete a primary required object'))
else:
r = obj
obj.delete()
s = set_primary_next_element(r)
if s is not None:
s.save()
super(AbstractHasPrimaryQuerySet, self).delete(*args, **kwargs)
class AbstractHasPrimary(AbstractRecordTimestamps):
objects = AbstractHasPrimaryQuerySet.as_manager()
is_primary = models.BooleanField(_('is primary'), default=False)
class Meta:
abstract = True
class AbstractTelephoneCommon(AbstractHasPrimary):
number = PhoneField(_('number'), unique=True, db_index=True)
has_whatsapp = models.BooleanField(_('has whatsapp'), default=False)
has_telegram = models.BooleanField(_('has telegram'), default=False)
is_primary = models.BooleanField(_('primary'), default=False)
@property
def _can_be_empty(self):
return False
def __str__(self):
return str(self.number)
class Meta:
abstract = True
class AbstractPersonTelephone(AbstractTelephoneCommon):
class Meta:
abstract = True
verbose_name = _('person telephone')
verbose_name_plural = _('person telephones')
def save(self, *args, **kwargs):
save_primary(self=self, field_name='person', field_value=self.person)
super().save(*args, **kwargs)
def clean(self):
if self.pk and self.is_primary:
if type(self).objects.get(pk=self.pk).person != self.person:
raise ValidationError(_('cannot assign a primary telephone to a different person once it is set'))
class AbstractCompanyTelephone(AbstractTelephoneCommon):
class Meta:
abstract = True
verbose_name = _('company telephone')
verbose_name_plural = _('company telephones')
def save(self, *args, **kwargs):
save_primary(self=self, field_name='company', field_value=self.company)
super().save(*args, **kwargs)
def clean(self):
if self.pk and self.is_primary:
if type(self).objects.get(pk=self.pk).company != self.company:
raise ValidationError(_('cannot assign a primary telephone to a different company once it is set'))
class AbstractEmailCommon(AbstractHasPrimary):
email = models.EmailField(_('email'),
max_length=const.EMAIL_MAX_LENGTH,
unique=True,
db_index=True)
@property
def _can_be_empty(self):
return True
class Meta:
abstract = True
def __str__(self):
return str(self.email)
class AbstractPersonEmail(AbstractEmailCommon):
class Meta:
abstract = True
verbose_name = _('person email')
verbose_name_plural = _('person emails')
def save(self, *args, **kwargs):
save_primary(self=self, field_name='person', field_value=self.person)
super().save(*args, **kwargs)
def clean(self):
if self.pk and self.is_primary:
if type(self).objects.get(pk=self.pk).person != self.person:
raise ValidationError(_('cannot assign a primary email to a different person once it is set'))
class AbstractCompanyEmail(AbstractEmailCommon):
class Meta:
abstract = True
verbose_name = _('company email')
verbose_name_plural = _('company emails')
def save(self, *args, **kwargs):
save_primary(self=self, field_name='company', field_value=self.company)
super().save(*args, **kwargs)
def clean(self):
if self.pk and self.is_primary:
if type(self).objects.get(pk=self.pk).person != self.person:
raise ValidationError(_('cannot assign a primary email to a different company once it is set'))
class AbstractAddressCommon(AbstractHasPrimary):
map = gis_models.PointField(_('map'), blank=True, null=True)
street_number = models.CharField(_('street number'),
max_length=const.GENERIC_CHAR_FIELD_LENGTH,
db_index=True)
street = models.CharField(_('street'), max_length=const.GENERIC_CHAR_FIELD_LENGTH, db_index=True)
city = models.CharField(_('city'), max_length=const.GENERIC_CHAR_FIELD_LENGTH, db_index=True)
postal_code = models.CharField(_('postal code'),
max_length=const.GENERIC_CHAR_FIELD_LENGTH,
blank=True)
auto_fill = models.BooleanField(_('auto fill'), default=False)
@property
def _can_be_empty(self):
return False
class Meta:
abstract = True
def save(self, *args, **kwargs):
self.map, self.postal_code = get_address_data(
self.municipality.country.code, self.city, self.street_number,
self.street, self.postal_code, self.map, self.auto_fill)
super().save(*args, **kwargs)
def __str__(self):
return self.street + ', ' + self.street_number + ', ' + self.city
class AbstractPersonAddress(AbstractAddressCommon):
class Meta:
abstract = True
verbose_name = _('person address')
verbose_name_plural = _('person addresses')
def save(self, *args, **kwargs):
save_primary(self=self, field_name='person', field_value=self.person)
super().save(*args, **kwargs)
def clean(self):
if self.pk and self.is_primary:
if type(self).objects.get(pk=self.pk).person != self.person:
raise ValidationError(_('cannot assign a primary address to a different person once it is set'))
class AbstractCompanyAddress(AbstractAddressCommon):
class Meta:
abstract = True
verbose_name = _('company address')
verbose_name_plural = _('company addresses')
def save(self, *args, **kwargs):
save_primary(self=self, field_name='company', field_value=self.company)
super().save(*args, **kwargs)
def clean(self):
if self.pk and self.is_primary:
if type(self).objects.get(pk=self.pk).company != self.company:
raise ValidationError(_('cannot assign a primary address to a different company once it is set'))
class AbstractCompany(AbstractRecordTimestamps):
name = models.CharField(_('name'), max_length=const.GENERIC_CHAR_FIELD_LENGTH)
vat = VATINField(_('VAT'), unique=True)
is_primary = models.BooleanField(_('is primary'), default=False)
class Meta:
abstract = True
verbose_name = _('company')
verbose_name_plural = _('companies')
def save(self, *args, **kwargs):
save_primary(self=self, field_name='person', field_value=self.person)
super().save(*args, **kwargs)
def __str__(self):
return self.name
class AbstractPerson(AbstractRecordTimestamps):
first_name = models.CharField(_('first name'),
max_length=const.GENERIC_CHAR_FIELD_LENGTH,
db_index=True)
last_name = models.CharField(_('last name'), max_length=const.GENERIC_CHAR_FIELD_LENGTH, db_index=True)
date_of_birth = models.DateField(_('date of birth'), default=timezone.now)
city_of_birth = models.CharField(_('city of birth'), max_length=const.GENERIC_CHAR_FIELD_LENGTH)
fiscal_code = models.CharField(_('fiscal code'),
max_length=const.GENERIC_CHAR_FIELD_LENGTH,
unique=True,
db_index=True)
def __str__(self):
return self.first_name + ' ' + self.last_name + ' ' + '[' + self.fiscal_code + ']'
class Meta:
abstract = True
verbose_name = _('person')
verbose_name_plural = _('people')
class AbstractPersonAttachment(AbstractCommonAttachment):
file = models.FileField(_('file'),
upload_to=personattachment_directory_path,
null=True)
class Meta:
abstract = True
verbose_name = _('person attachment')
verbose_name_plural = _('person attachments')
class AbstractGeocoderCache(AbstractRecordTimestamps):
r"""As required by the terms of use, Nominatim results must be cached.
See
https://operations.osmfoundation.org/policies/nominatim/
"""
street_number = models.CharField(_('street number'),
max_length=const.GENERIC_CHAR_FIELD_LENGTH,
db_index=True)
street = models.CharField(_('street'), max_length=const.GENERIC_CHAR_FIELD_LENGTH, db_index=True)
city = models.CharField(_('city'), max_length=const.GENERIC_CHAR_FIELD_LENGTH, db_index=True)
country_code = models.CharField(_('contry code'), max_length=const.GENERIC_CHAR_FIELD_LENGTH, db_index=True)
postal_code = models.CharField(_('postal code'),
max_length=const.GENERIC_CHAR_FIELD_LENGTH,
blank=True, null=True)
map = gis_models.PointField(_('map'), blank=True, null=True)
cache_hits = models.IntegerField(_('cache hits'), default=0)
class Meta:
abstract = True
verbose_name = _('Nominatim cache')
verbose_name_plural = _('Nominatim caches')