"""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 from django.contrib.postgres.fields import HStoreField 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"}') backoffice_title = models.TextField(null=True, default=None, verbose_name=_('Title for searching via BO')) 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(blank=True, null=True, default=None, 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')) slugs = HStoreField(null=True, blank=True, default=None, verbose_name=_('Slugs for current news obj'), help_text='{"en-GB":"some 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'))