"""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') 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')) 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 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'))