571 lines
18 KiB
Python
571 lines
18 KiB
Python
"""Utils app models."""
|
||
import logging
|
||
from os.path import exists
|
||
|
||
from django.conf import settings
|
||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||
from django.contrib.contenttypes.models import ContentType
|
||
from django.contrib.gis.db import models
|
||
from django.contrib.postgres.aggregates import ArrayAgg
|
||
from django.contrib.postgres.fields import JSONField
|
||
from django.contrib.postgres.fields.jsonb import KeyTextTransform
|
||
from django.core.validators import FileExtensionValidator
|
||
from django.shortcuts import get_object_or_404
|
||
from django.utils import timezone
|
||
from django.utils.functional import cached_property
|
||
from django.utils.html import mark_safe
|
||
from django.utils.translation import ugettext_lazy as _, get_language
|
||
from easy_thumbnails.fields import ThumbnailerImageField
|
||
from sorl.thumbnail import get_thumbnail
|
||
from sorl.thumbnail.fields import ImageField as SORLImageField
|
||
|
||
from configuration.models import TranslationSettings
|
||
from utils.methods import image_path, svg_image_path, file_path
|
||
from utils.validators import svg_image_validator
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class ProjectBaseMixin(models.Model):
|
||
"""Base mixin model."""
|
||
|
||
created = models.DateTimeField(default=timezone.now, editable=False,
|
||
verbose_name=_('Date created'))
|
||
modified = models.DateTimeField(auto_now=True,
|
||
verbose_name=_('Date updated'))
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
|
||
abstract = True
|
||
|
||
|
||
class TJSONField(JSONField):
|
||
"""Overrided JsonField."""
|
||
|
||
|
||
def to_locale(language):
|
||
"""Turn a language name (en-us) into a locale name (en_US)."""
|
||
if language:
|
||
language, _, country = language.lower().partition('-')
|
||
if not country:
|
||
return language
|
||
# A language with > 2 characters after the dash only has its first
|
||
# character after the dash capitalized; e.g. sr-latn becomes sr-Latn.
|
||
# A language with 2 characters after the dash has both characters
|
||
# capitalized; e.g. en-us becomes en-US.
|
||
country, _, tail = country.partition('-')
|
||
country = country.title() if len(country) > 2 else country.upper()
|
||
if tail:
|
||
country += '-' + tail
|
||
return language + '-' + country
|
||
|
||
|
||
def get_current_locale():
|
||
"""Get current language."""
|
||
return to_locale(get_language())
|
||
|
||
|
||
def get_default_locale():
|
||
return TranslationSettings.get_solo().default_language or \
|
||
settings.FALLBACK_LOCALE
|
||
|
||
|
||
def translate_field(self, field_name, toggle_field_name=None):
|
||
def translate(self):
|
||
field = getattr(self, field_name)
|
||
toggler = getattr(self, toggle_field_name, None)
|
||
if isinstance(field, dict):
|
||
if toggler:
|
||
field = {locale: v for locale, v in field.items() if toggler.get(locale) in [True, 'True', 'true']}
|
||
value = field.get(to_locale(get_language()))
|
||
# fallback
|
||
if value is None:
|
||
value = field.get(get_default_locale())
|
||
if value is None:
|
||
try:
|
||
value = next(iter(field.values()))
|
||
except StopIteration:
|
||
# field values are absent
|
||
return None
|
||
return value
|
||
return None
|
||
|
||
return translate
|
||
|
||
|
||
# todo: refactor this
|
||
class IndexJSON:
|
||
|
||
def __getattr__(self, item):
|
||
return None
|
||
|
||
|
||
def index_field(self, field_name):
|
||
def index(self):
|
||
field = getattr(self, field_name)
|
||
obj = IndexJSON()
|
||
if isinstance(field, dict):
|
||
for key, value in field.items():
|
||
setattr(obj, key, value)
|
||
return obj
|
||
|
||
return index
|
||
|
||
|
||
class TranslatedFieldsMixin:
|
||
"""Translated Fields mixin"""
|
||
|
||
STR_FIELD_NAME = ''
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
"""Overrided __init__ method."""
|
||
super(TranslatedFieldsMixin, self).__init__(*args, **kwargs)
|
||
cls = self.__class__
|
||
for field in cls._meta.fields:
|
||
field_name = field.name
|
||
if isinstance(field, TJSONField):
|
||
setattr(cls, f'{field.name}_translated',
|
||
property(translate_field(self, field_name, f'locale_to_{field_name}_is_active')))
|
||
setattr(cls, f'{field_name}_indexing',
|
||
property(index_field(self, field_name)))
|
||
|
||
def __str__(self):
|
||
"""Overrided __str__ method."""
|
||
value = None
|
||
if self.STR_FIELD_NAME:
|
||
field = getattr(self, getattr(self, 'STR_FIELD_NAME'))
|
||
if isinstance(field, dict):
|
||
value = field.get(get_current_locale())
|
||
return value if value else super(TranslatedFieldsMixin, self).__str__()
|
||
|
||
|
||
class OAuthProjectMixin:
|
||
"""OAuth2 mixin for project GM"""
|
||
|
||
def get_source(self):
|
||
"""Method to get of platform"""
|
||
return NotImplementedError()
|
||
|
||
|
||
class BaseAttributes(ProjectBaseMixin):
|
||
created_by = models.ForeignKey(
|
||
'account.User', on_delete=models.SET_NULL, verbose_name=_('created by'),
|
||
null=True, related_name='%(class)s_records_created'
|
||
)
|
||
modified_by = models.ForeignKey(
|
||
'account.User', on_delete=models.SET_NULL, verbose_name=_('modified by'),
|
||
null=True, related_name='%(class)s_records_modified'
|
||
)
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
|
||
abstract = True
|
||
|
||
|
||
class FileMixin(models.Model):
|
||
"""File model."""
|
||
|
||
file = models.FileField(upload_to=file_path,
|
||
blank=True, null=True, default=None,
|
||
verbose_name=_('File'),
|
||
validators=[FileExtensionValidator(
|
||
allowed_extensions=('jpg', 'jpeg', 'png', 'doc', 'docx', 'pdf')
|
||
)])
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
|
||
abstract = True
|
||
|
||
def get_file_url(self):
|
||
"""Get file url."""
|
||
return self.file.url if self.file else None
|
||
|
||
def get_full_file_url(self, request):
|
||
"""Get full file url"""
|
||
if self.file and exists(self.file.path):
|
||
return request.build_absolute_uri(self.file.url)
|
||
else:
|
||
return None
|
||
|
||
|
||
class ImageMixin(models.Model):
|
||
"""Avatar model."""
|
||
|
||
THUMBNAIL_KEY = 'default'
|
||
|
||
image = ThumbnailerImageField(upload_to=image_path,
|
||
blank=True, null=True, default=None,
|
||
verbose_name=_('Image'))
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
|
||
abstract = True
|
||
|
||
def get_image(self, key=None):
|
||
"""Get thumbnailed image file."""
|
||
return self.image[key or self.THUMBNAIL_KEY] if self.image else None
|
||
|
||
def get_image_url(self, key=None):
|
||
"""Get image thumbnail url."""
|
||
return self.get_image(key).url if self.image else None
|
||
|
||
def image_tag(self):
|
||
"""Admin preview tag."""
|
||
if self.image:
|
||
return mark_safe('<img src="%s" />' % self.get_image_url())
|
||
else:
|
||
return None
|
||
|
||
def get_full_image_url(self, request, thumbnail_key=None):
|
||
"""Get full image url"""
|
||
if self.image and exists(self.image.path):
|
||
return request.build_absolute_uri(self.get_image(thumbnail_key).url)
|
||
else:
|
||
return None
|
||
|
||
image_tag.short_description = _('Image')
|
||
image_tag.allow_tags = True
|
||
|
||
|
||
class SORLImageMixin(models.Model):
|
||
"""Abstract model for SORL ImageField"""
|
||
|
||
image = SORLImageField(upload_to=image_path,
|
||
blank=True, null=True, default=None,
|
||
verbose_name=_('Image'))
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
abstract = True
|
||
|
||
def get_image(self, thumbnail_key: str):
|
||
"""Get thumbnail image file."""
|
||
if thumbnail_key in settings.SORL_THUMBNAIL_ALIASES:
|
||
return get_thumbnail(
|
||
file_=self.image,
|
||
**settings.SORL_THUMBNAIL_ALIASES[thumbnail_key])
|
||
|
||
def get_image_url(self, thumbnail_key: str):
|
||
"""Get image thumbnail url."""
|
||
crop_image = self.get_image(thumbnail_key)
|
||
if hasattr(crop_image, 'url'):
|
||
return crop_image.url
|
||
|
||
def image_tag(self):
|
||
"""Admin preview tag."""
|
||
if self.image:
|
||
return mark_safe(f'<img src="{self.image.url}" style="max-height: 25%; max-width: 25%" />')
|
||
else:
|
||
return None
|
||
|
||
def get_cropped_image(self, geometry: str, quality: int, cropbox: str) -> dict:
|
||
cropped_image = get_thumbnail(self.image,
|
||
geometry_string=geometry,
|
||
# crop=crop,
|
||
# upscale=False,
|
||
cropbox=cropbox,
|
||
quality=quality)
|
||
return {
|
||
'geometry_string': geometry,
|
||
'crop_url': cropped_image.url,
|
||
'quality': quality,
|
||
# 'crop': crop,
|
||
'cropbox': cropbox,
|
||
}
|
||
|
||
image_tag.short_description = _('Image')
|
||
image_tag.allow_tags = True
|
||
|
||
|
||
class SVGImageMixin(models.Model):
|
||
"""SVG image model."""
|
||
|
||
svg_image = models.FileField(upload_to=svg_image_path,
|
||
blank=True, null=True, default=None,
|
||
validators=[svg_image_validator, ],
|
||
verbose_name=_('SVG image'))
|
||
|
||
@property
|
||
def svg_image_indexing(self):
|
||
return self.svg_image.url if self.svg_image else None
|
||
|
||
class Meta:
|
||
abstract = True
|
||
|
||
|
||
class URLImageMixin(models.Model):
|
||
"""URl for image and special methods."""
|
||
image_url = models.URLField(verbose_name=_('Image URL path'),
|
||
blank=True, null=True, default=None)
|
||
|
||
class Meta:
|
||
abstract = True
|
||
|
||
def image_tag(self):
|
||
if self.image_url:
|
||
return mark_safe(
|
||
f'<a href="{self.image_url}"><img src="{self.image_url}" height="50"/>')
|
||
else:
|
||
return None
|
||
|
||
image_tag.short_description = _('Image')
|
||
image_tag.allow_tags = True
|
||
|
||
|
||
class PlatformMixin(models.Model):
|
||
"""Platforms"""
|
||
|
||
MOBILE = 0
|
||
WEB = 1
|
||
ALL = 2
|
||
|
||
SOURCES = (
|
||
(MOBILE, _('Mobile')),
|
||
(WEB, _('Web')),
|
||
(ALL, _('All'))
|
||
)
|
||
source = models.PositiveSmallIntegerField(choices=SOURCES, default=MOBILE,
|
||
verbose_name=_('Source'))
|
||
|
||
class Meta:
|
||
"""Meta class"""
|
||
abstract = True
|
||
|
||
|
||
class LocaleManagerMixin(models.Manager):
|
||
"""Manager for locale"""
|
||
|
||
def annotate_localized_fields(self, locale):
|
||
"""Return queryset by locale"""
|
||
prefix = 'trans'
|
||
|
||
# Prepare fields to localization (only JSONField can be localized)
|
||
fields = [field.name for field in self.model._meta.fields if isinstance(field, JSONField)]
|
||
|
||
# Check filters to check if field has localization
|
||
filters = {f'{field}__has_key': locale for field in fields}
|
||
# Filter QuerySet by prepared filters
|
||
queryset = self.filter(**filters)
|
||
|
||
# Prepare field for annotator
|
||
localized_fields = {f'{field}_{prefix}': KeyTextTransform(f'{locale}', field) for field in
|
||
fields}
|
||
|
||
# Annotate them
|
||
for _ in fields:
|
||
queryset = queryset.annotate(**localized_fields)
|
||
return queryset
|
||
|
||
|
||
class GMTokenGenerator(PasswordResetTokenGenerator):
|
||
CHANGE_EMAIL = 0
|
||
RESET_PASSWORD = 1
|
||
CHANGE_PASSWORD = 2
|
||
CONFIRM_EMAIL = 3
|
||
|
||
TOKEN_CHOICES = (
|
||
CHANGE_EMAIL,
|
||
RESET_PASSWORD,
|
||
CHANGE_PASSWORD,
|
||
CONFIRM_EMAIL
|
||
)
|
||
|
||
def __init__(self, purpose: int):
|
||
if purpose in self.TOKEN_CHOICES:
|
||
self.purpose = purpose
|
||
|
||
def get_fields(self, user, timestamp):
|
||
"""
|
||
Get user fields for hash value.
|
||
"""
|
||
fields = [str(timestamp), str(user.is_active), str(user.pk)]
|
||
if self.purpose == self.CHANGE_EMAIL or \
|
||
self.purpose == self.CONFIRM_EMAIL:
|
||
fields.extend([str(user.email_confirmed), str(user.email)])
|
||
elif self.purpose == self.RESET_PASSWORD or \
|
||
self.purpose == self.CHANGE_PASSWORD:
|
||
fields.append(str(user.password))
|
||
return fields
|
||
|
||
def _make_hash_value(self, user, timestamp):
|
||
"""
|
||
Hash the user's primary key and some user state that's sure to change
|
||
after a password reset to produce a token that invalidated when it's
|
||
used.
|
||
"""
|
||
return self.get_fields(user, timestamp)
|
||
|
||
|
||
class GalleryMixin:
|
||
"""Mixin for models that has gallery."""
|
||
|
||
@property
|
||
def crop_gallery(self):
|
||
if hasattr(self, 'gallery') and hasattr(self, '_meta'):
|
||
gallery = []
|
||
images = self.gallery.all()
|
||
crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES
|
||
if p.startswith(self._meta.model_name.lower())]
|
||
for image in images:
|
||
d = {
|
||
'id': image.id,
|
||
'title': image.title,
|
||
'original_url': image.image.url,
|
||
'orientation_display': image.get_orientation_display(),
|
||
'auto_crop_images': {},
|
||
}
|
||
for crop in crop_parameters:
|
||
d['auto_crop_images'].update({crop: image.get_image_url(crop)})
|
||
gallery.append(d)
|
||
return gallery
|
||
|
||
@property
|
||
def crop_main_image(self):
|
||
if hasattr(self, 'main_image') and hasattr(self, '_meta'):
|
||
if self.main_image:
|
||
image = self.main_image
|
||
image_property = {
|
||
'id': image.id,
|
||
'title': image.title,
|
||
'original_url': image.image.url,
|
||
'orientation_display': image.get_orientation_display(),
|
||
'auto_crop_images': {},
|
||
}
|
||
crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES
|
||
if p.startswith(self._meta.model_name.lower())]
|
||
for crop in crop_parameters:
|
||
image_property['auto_crop_images'].update(
|
||
{crop: image.get_image_url(crop)}
|
||
)
|
||
return image_property
|
||
|
||
|
||
class IntermediateGalleryModelQuerySet(models.QuerySet):
|
||
"""Extended QuerySet."""
|
||
|
||
def main_image(self):
|
||
"""Return objects with flag is_main is True"""
|
||
return self.filter(is_main=True)
|
||
|
||
|
||
class IntermediateGalleryModelMixin(models.Model):
|
||
"""Mixin for intermediate gallery model."""
|
||
|
||
is_main = models.BooleanField(default=False,
|
||
verbose_name=_('Is the main image'))
|
||
|
||
objects = IntermediateGalleryModelQuerySet.as_manager()
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
abstract = True
|
||
|
||
def __str__(self):
|
||
"""Overridden str method."""
|
||
if hasattr(self, 'image'):
|
||
return self.image.title if self.image.title else self.id
|
||
|
||
|
||
class HasTagsMixin(models.Model):
|
||
"""Mixin for filtering tags"""
|
||
|
||
@property
|
||
def visible_tags(self):
|
||
return self.tags.filter(category__public=True).prefetch_related(
|
||
'category',
|
||
'translation',
|
||
'category__translation',
|
||
).exclude(category__value_type='bool')
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
abstract = True
|
||
|
||
|
||
class FavoritesMixin:
|
||
"""Append favorites_for_user property."""
|
||
|
||
@property
|
||
def favorites_for_users(self):
|
||
return self.favorites.aggregate(arr=ArrayAgg('user_id')).get('arr')
|
||
|
||
|
||
timezone.datetime.now().date().isoformat()
|
||
|
||
|
||
class TypeDefaultImageMixin:
|
||
"""Model mixin for default image."""
|
||
|
||
@property
|
||
def default_image_url(self):
|
||
"""Return image url."""
|
||
if hasattr(self, 'default_image') and self.default_image:
|
||
return self.default_image.image.url
|
||
|
||
@property
|
||
def preview_image_url(self):
|
||
if hasattr(self, 'default_image') and self.default_image:
|
||
return self.default_image.get_image_url(thumbnail_key='type_preview')
|
||
|
||
|
||
def default_menu_bool_array():
|
||
return [False] * 7
|
||
|
||
|
||
class PhoneModelMixin:
|
||
"""Mixin for PhoneNumberField."""
|
||
|
||
@cached_property
|
||
def country_calling_code(self):
|
||
"""Return phone code from PhonеNumberField."""
|
||
if hasattr(self, 'phone') and self.phone:
|
||
return f'+{self.phone.country_code}'
|
||
|
||
@cached_property
|
||
def national_calling_number(self):
|
||
"""Return phone national number from from PhonеNumberField."""
|
||
if hasattr(self, 'phone') and (self.phone and hasattr(self.phone, 'national_number')):
|
||
return self.phone.national_number
|
||
|
||
|
||
class AwardsModelMixin:
|
||
def remove_award(self, award_id: int):
|
||
from main.models import Award
|
||
award = get_object_or_404(Award, pk=award_id)
|
||
|
||
if hasattr(self, 'awards'):
|
||
self.awards.remove(award)
|
||
|
||
|
||
class CarouselMixin:
|
||
@property
|
||
def must_of_the_week(self) -> bool:
|
||
"""Detects whether current item in carousel"""
|
||
from main.models import Carousel
|
||
|
||
if hasattr(self, 'pk') and (hasattr(self, 'country') or hasattr(self, 'country_id')):
|
||
kwargs = {
|
||
'content_type': ContentType.objects.get_for_model(self),
|
||
'object_id': self.pk,
|
||
'country': getattr(self, 'country', getattr(self, 'country_id', None)),
|
||
}
|
||
|
||
return Carousel.objects.filter(**kwargs).exists()
|
||
|
||
return False
|
||
|
||
|
||
class UpdateByMixin(models.Model):
|
||
"""Modify by mixin"""
|
||
last_update_by_manager = models.DateTimeField(null=True)
|
||
|
||
last_update_by_gm = models.DateTimeField(null=True)
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
abstract = True
|