gault-millau/apps/news/models.py
2019-11-27 12:49:33 +00:00

319 lines
12 KiB
Python

"""News app models."""
from django.contrib.contenttypes import fields as generic
from django.db import models
from django.db.models import Case, When
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from rest_framework.reverse import reverse
from rating.models import Rating, ViewCount
from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, HasTagsMixin,
ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin,
FavoritesMixin)
from utils.querysets import TranslationQuerysetMixin
from django.conf import settings
class Agenda(ProjectBaseMixin, TranslatedFieldsMixin):
"""News agenda model"""
event_datetime = models.DateTimeField(default=timezone.now, editable=False,
verbose_name=_('Event datetime'))
address = models.ForeignKey('location.Address', blank=True, null=True,
default=None, verbose_name=_('address'),
on_delete=models.SET_NULL)
content = TJSONField(blank=True, null=True, default=None,
verbose_name=_('content'),
help_text='{"en-GB":"some text"}')
class NewsBanner(ProjectBaseMixin, TranslatedFieldsMixin):
"""News banner model"""
title = TJSONField(blank=True, null=True, default=None,
verbose_name=_('title'),
help_text='{"en-GB":"some text"}')
image_url = models.URLField(verbose_name=_('Image URL path'),
blank=True, null=True, default=None)
content_url = models.URLField(verbose_name=_('Content URL path'),
blank=True, null=True, default=None)
class NewsType(models.Model):
"""NewsType model."""
name = models.CharField(_('name'), max_length=250)
tag_categories = models.ManyToManyField('tag.TagCategory',
related_name='news_types')
class Meta:
"""Meta class."""
verbose_name_plural = _('news types')
verbose_name = _('news type')
def __str__(self):
"""Overrided __str__ method."""
return self.name
class NewsQuerySet(TranslationQuerysetMixin):
"""QuerySet for model News"""
def sort_by_start(self):
"""Return qs sorted by start DESC"""
return self.order_by('-start')
def rating_value(self):
return self.annotate(rating=models.Count('ratings__ip', distinct=True))
def with_base_related(self):
"""Return qs with related objects."""
return self.select_related('news_type', 'country').prefetch_related('tags')
def with_extended_related(self):
"""Return qs with related objects."""
return self.select_related('created_by', 'agenda', 'banner')
def by_type(self, news_type):
"""Filter News by type"""
return self.filter(news_type__name=news_type)
def by_tags(self, tags):
return self.filter(tags__in=tags)
def by_country_code(self, code):
"""Filter collection by country code."""
return self.filter(country__code=code)
def recipe_news(self):
"""Returns news with tag 'cook' qs."""
return self.filter(tags__value=News.RECIPES_TAG_VALUE)
def international_news(self):
"""Returns only international news qs."""
return self.filter(tags__value=News.INTERNATIONAL_TAG_VALUE)
def published(self):
"""Return only published news"""
now = timezone.now()
return self.filter(models.Q(models.Q(end__gte=now) |
models.Q(end__isnull=True)),
state__in=self.model.PUBLISHED_STATES, start__lte=now)
# todo: filter by best score
# todo: filter by country?
def should_read(self, news, user):
return self.model.objects.exclude(pk=news.pk).published(). \
annotate_in_favorites(user). \
with_base_related().by_type(news.news_type).distinct().order_by('?')
def same_theme(self, news, user):
return self.model.objects.exclude(pk=news.pk).published(). \
annotate_in_favorites(user). \
with_base_related().by_type(news.news_type). \
by_tags(news.tags.all()).distinct().order_by('-start')
def annotate_in_favorites(self, user):
"""Annotate flag in_favorites"""
favorite_news_ids = []
if user.is_authenticated:
favorite_news_ids = user.favorite_news_ids
return self.annotate(
in_favorites=Case(
When(id__in=favorite_news_ids, then=True),
default=False,
output_field=models.BooleanField(default=False)
)
)
class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
FavoritesMixin):
"""News model."""
STR_FIELD_NAME = 'title'
# TEMPLATE CHOICES
NEWSPAPER = 0
MAIN_PDF_ERB = 1
MAIN = 2
TEMPLATE_CHOICES = (
(NEWSPAPER, 'newspaper'),
(MAIN_PDF_ERB, 'main.pdf.erb'),
(MAIN, 'main'),
)
# STATE CHOICES
WAITING = 0
HIDDEN = 1
PUBLISHED = 2
PUBLISHED_EXCLUSIVE = 3
PUBLISHED_STATES = [PUBLISHED, PUBLISHED_EXCLUSIVE]
STATE_CHOICES = (
(WAITING, _('Waiting')),
(HIDDEN, _('Hidden')),
(PUBLISHED, _('Published')),
(PUBLISHED_EXCLUSIVE, _('Published exclusive')),
)
INTERNATIONAL_TAG_VALUE = 'international'
RECIPES_TAG_VALUE = 'cook'
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
news_type = models.ForeignKey(NewsType, on_delete=models.PROTECT,
verbose_name=_('news type'))
title = TJSONField(blank=True, null=True, default=None,
verbose_name=_('title'),
help_text='{"en-GB":"some text"}')
subtitle = TJSONField(blank=True, null=True, default=None,
verbose_name=_('subtitle'),
help_text='{"en-GB":"some text"}')
description = TJSONField(blank=True, null=True, default=None,
verbose_name=_('description'),
help_text='{"en-GB":"some text"}')
start = models.DateTimeField(verbose_name=_('Start'))
end = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('End'))
slug = models.SlugField(unique=True, max_length=255,
verbose_name=_('News slug'))
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
verbose_name=_('State'))
is_highlighted = models.BooleanField(default=False,
verbose_name=_('Is highlighted'))
template = models.PositiveIntegerField(choices=TEMPLATE_CHOICES, default=NEWSPAPER)
address = models.ForeignKey('location.Address', blank=True, null=True,
default=None, verbose_name=_('address'),
on_delete=models.SET_NULL)
country = models.ForeignKey('location.Country', blank=True, null=True,
on_delete=models.SET_NULL,
verbose_name=_('country'))
tags = models.ManyToManyField('tag.Tag', related_name='news',
verbose_name=_('Tags'))
gallery = models.ManyToManyField('gallery.Image', through='news.NewsGallery')
views_count = models.OneToOneField('rating.ViewCount', blank=True, null=True, on_delete=models.SET_NULL)
ratings = generic.GenericRelation(Rating)
favorites = generic.GenericRelation(to='favorites.Favorites')
carousels = generic.GenericRelation(to='main.Carousel')
agenda = models.ForeignKey('news.Agenda', blank=True, null=True,
on_delete=models.SET_NULL,
verbose_name=_('agenda'))
banner = models.ForeignKey('news.NewsBanner', blank=True, null=True,
on_delete=models.SET_NULL,
verbose_name=_('banner'))
site = models.ForeignKey('main.SiteSettings', blank=True, null=True,
on_delete=models.SET_NULL, verbose_name=_('site settings'))
objects = NewsQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name = _('news')
verbose_name_plural = _('news')
def __str__(self):
return f'news: {self.slug}'
@property
def is_publish(self):
return self.state in self.PUBLISHED_STATES
@property
def is_international(self):
return self.INTERNATIONAL_TAG_VALUE in map(lambda tag: tag.value, self.tags.all())
@property
def web_url(self):
return reverse('web:news:rud', kwargs={'slug': self.slug})
def should_read(self, user):
return self.__class__.objects.should_read(self, user)[:3]
def same_theme(self, user):
return self.__class__.objects.same_theme(self, user)[:3]
@property
def main_image(self):
qs = self.news_gallery.main_image()
if qs.exists():
return qs.first().image
@property
def image_url(self):
return self.main_image.image.url if self.main_image else None
@property
def preview_image_url(self):
if self.main_image:
return self.main_image.get_image_url(thumbnail_key='news_preview')
@property
def view_counter(self):
count_value = 0
if self.views_count:
count_value = self.views_count.count
return count_value
# todo: remove in future
@property
def crop_gallery(self):
if hasattr(self, 'gallery'):
gallery = []
images = self.gallery.all()
model_name = self._meta.model_name.lower()
crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES
if p.startswith(model_name)]
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(
{f'{crop[len(f"{model_name}_"):]}_url': image.get_image_url(crop)})
gallery.append(d)
return gallery
@property
def crop_main_image(self):
if hasattr(self, 'main_image') and self.main_image:
image = self.main_image
model_name = self._meta.model_name.lower()
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(
{f'{crop[len(f"{model_name}_"):]}_url': image.get_image_url(crop)})
return image_property
class NewsGallery(IntermediateGalleryModelMixin):
news = models.ForeignKey(News, null=True,
related_name='news_gallery',
on_delete=models.CASCADE,
verbose_name=_('news'))
image = models.ForeignKey('gallery.Image', null=True,
related_name='news_gallery',
on_delete=models.CASCADE,
verbose_name=_('gallery'))
class Meta:
"""NewsGallery meta class."""
verbose_name = _('news gallery')
verbose_name_plural = _('news galleries')
unique_together = (('news', 'is_main'), ('news', 'image'))