class ContentsEntryMixin(models.Model): """ Mixin for adding contents to a blog entry """ contents = PlaceholderField("blog_contents") # Adding the ContentItemRelation makes sure the admin can find all deleted objects too. contentitem_set = ContentItemRelation() class Meta: abstract = True def create_placeholder(self, slot="blog_contents", role='m', title=None): """ Create a placeholder on this blog entry. To fill the content items, use :func:`ContentItemModel.objects.create_for_placeholder() <fluent_contents.models.managers.ContentItemManager.create_for_placeholder>`. :rtype: :class:`~fluent_contents.models.Placeholder` """ return Placeholder.objects.create_for_object(self, slot, role=role, title=title)
class SharedContent(TranslatableModel): """ The parent hosting object for shared content """ translations = TranslatedFields( title=models.CharField(_("Title"), max_length=200)) slug = models.SlugField( _("Template code"), unique=True, help_text= _("This unique name can be used refer to this content in in templates." )) contents = PlaceholderField("shared_content", verbose_name=_("Contents")) # NOTE: settings such as "template_name", and which plugins are allowed can be added later. # Adding the reverse relation for ContentItem objects # causes the admin to list these objects when moving the shared content contentitem_set = ContentItemRelation() class Meta: verbose_name = _("Shared content") verbose_name_plural = _("Shared content") def __unicode__(self): return self.title
class ResultPage(TranslatableModel): image = models.ImageField(_('Header image'), blank=True, null=True) start_date = models.DateField(null=True, blank=True) end_date = models.DateField(null=True, blank=True) content = PlaceholderField('content', plugins=[ 'ProjectMapBlockPlugin', 'QuotesBlockPlugin', 'ActivitiesBlockPlugin', 'ShareResultsBlockPlugin', 'StatsBlockPlugin', 'SupporterTotalBlockPlugin', ]) translations = TranslatedFields(title=models.CharField(_('Title'), max_length=40), slug=models.SlugField(_('Slug'), max_length=40), description=models.CharField( _('Description'), max_length=45, blank=True, null=True)) class Meta: permissions = ( ('api_read_resultpage', 'Can view result pages through the API'), ('api_add_resultpage', 'Can add result pages through the API'), ('api_change_resultpage', 'Can change result pages through the API'), ('api_delete_resultpage', 'Can delete result pages through the API'), )
class AbstractResponsePage(models.Model): """ Response Pages are designed to be custom pages that allow the user to manage the content for pages such and 404 and 500. """ title = models.CharField( max_length=255, ) type = models.CharField( choices=RESPONSES, max_length=5, unique=True, ) is_active = models.BooleanField( default=False, ) content = PlaceholderField( 'response_page', plugins=appsettings.RESPONSE_PAGE_CONTENT_PLUGINS, ) class Meta: abstract = True def __str__(self): return self.get_type_display()
class AbstractUnpublishableSlideShow(models.Model): """ A reusable Slide Show. """ title = models.CharField(max_length=255, ) show_title = models.BooleanField( default=False, help_text=_('Should the title of the slide show be displayed?')) content = PlaceholderField( 'slide_show_content', plugins=appsettings.SLIDE_SHOW_CONTENT_PLUGINS, ) class Meta: abstract = True verbose_name = "Image gallery" verbose_name_plural = "Image galleries" def __str__(self): return self.title def preview(self, request): t = Template("""{% load icekit_tags thumbnail %} {% for item in obj.content.get_content_items %} <img src="{% thumbnail item.image.image 30x30 %}" alt=""> {% empty %} <cite>No items</cite> {% endfor %} """) c = Context({ 'obj': self, }) return t.render(c)
class SharedContent(CachedModelMixin, TranslatableModel): """ The parent hosting object for shared content """ translations = TranslatedFields( title = models.CharField(_("Title"), max_length=200) ) parent_site = models.ForeignKey(Site, on_delete=models.CASCADE, editable=False, default=get_current_site_id) slug = models.SlugField(_("Template code"), help_text=_("This unique name can be used refer to this content in in templates.")) is_cross_site = models.BooleanField(_("Share between all sites"), blank=True, default=False, help_text=_("This allows contents can be shared between multiple sites in this project.<br>\n" "Make sure that any URLs in the content work with all sites where the content is displayed.")) contents = PlaceholderField("shared_content", verbose_name=_("Contents")) # NOTE: settings such as "template_name", and which plugins are allowed can be added later. # Adding the reverse relation for ContentItem objects # causes the admin to list the related objects when deleting this model. contentitem_set = ContentItemRelation() objects = SharedContentManager() class Meta: verbose_name = _("Shared content") verbose_name_plural = _("Shared content") unique_together = ( ('parent_site', 'slug'), ) ordering = ('slug',) def __str__(self): return self.safe_translation_getter('title', self.slug) def __init__(self, *args, **kwargs): super(SharedContent, self).__init__(*args, **kwargs) self._was_cross_site = self.is_cross_site self._old_slug = self.slug def get_cache_keys(self): # When the shared content is saved, make sure all rendering output PTRs are cleared. # The 'slug' could have changed. Whether the Placeholder output is cleared, # depends on whether those objects are altered too. if self.is_cross_site or self._was_cross_site: sites = list(Site.objects.all().values_list('pk', flat=True)) else: sites = [self.parent_site_id] keys = [] for site_id in sites: for language_code, _ in settings.LANGUAGES: keys.append(get_shared_content_cache_key_ptr(site_id, self._old_slug, language_code)) return keys
class HomePage(TranslatableModel): content = PlaceholderField('content') translations = TranslatedFields() class Meta: permissions = ( ('api_read_homepage', 'Can view homepages through the API'), ('api_add_homepage', 'Can add homepages through the API'), ('api_change_homepage', 'Can change homepages through the API'), ('api_delete_homepage', 'Can delete homepages through the API'), )
class FaqQuestion(TagsMixin, FaqBaseModel): """ Category in the FAQ. """ # This is a separate model instead of using django-categories because: # - content needs to be placed on the category. # - the title and slug can be translated! # Be compatible with django-orderable table layout, # unfortunately, there isn't a good canonical version of it yet. order = models.PositiveIntegerField(db_index=True, blank=True, null=True) title = TranslatedField(any_language=True) translations = TranslatedFields( title=models.CharField(_("title"), max_length=200), slug=models.SlugField(_("slug")), ) contents = PlaceholderField("faq_answer", verbose_name=_("answer")) contentitem_set = ContentItemRelation( ) # this makes sure the admin can find all deleted objects too. # Organisation category = models.ForeignKey(FaqCategory, verbose_name=_("Category"), related_name='questions') objects = FaqQuestionManager() class Meta: verbose_name = _("FAQ Question") verbose_name_plural = _("FAQ Questions") ordering = ('order', 'creation_date') def __str__(self): # self.title is configured with any_language=True, so always returns a value. return self.title def get_relative_url(self): """ Return the link path from the archive page. """ # Return the link style, using the permalink style setting. return u'{0}{1}/'.format(self.category.get_relative_url(), self.slug) def similar_objects(self, num=None, **filters): """ Find similar objects using related tags. """ #TODO: filter appsettings.FLUENT_FAQ_FILTER_SITE_ID: # filters.setdefault('parent_site', self.parent_site_id) # FIXME: Using super() doesn't work, calling directly. return TagsMixin.similar_objects(self, num=num, **filters)
class NewsItem(AnonymizationMixin, PublishableModel): title = models.CharField(_("Title"), max_length=200) slug = models.SlugField(_("Slug")) # Contents main_image = ImageField( _("Main image"), help_text=_("Shows at the top of your post."), upload_to='blogs', blank=True, validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ]) language = models.CharField(_("language"), max_length=5, choices=lazy(get_languages, tuple)()) contents = PlaceholderField("blog_contents", plugins=[ 'TextPlugin', 'ImageTextPlugin', 'OEmbedPlugin', 'RawHtmlPlugin', 'PicturePlugin' ]) # This should not be necessary, but fixes deletion of some news items # See https://github.com/edoburu/django-fluent-contents/issues/19 contentitem_set = ContentItemRelation() allow_comments = models.BooleanField(_("Allow comments"), default=True) def __str__(self): return self.title def get_meta_description(self, **kwargs): request = kwargs.get('request') s = MLStripper() s.feed(mark_safe(render_placeholder(request, self.contents).html)) return truncatechars(s.get_data(), 250) class Meta(object): verbose_name = _("news item") verbose_name_plural = _("news items") permissions = ( ('api_read_newsitem', 'Can view news items through the API'), ('api_add_newsitem', 'Can add news items through the API'), ('api_change_newsitem', 'Can change news items through the API'), ('api_delete_newsitem', 'Can delete news items through the API'), )
class PlaceholderFieldTestPage(models.Model): """ A model with PlaceholderField, for testing, """ title = models.CharField(max_length=200) contents = PlaceholderField("field_slot1") placeholder_set = PlaceholderRelation() contentitem_set = ContentItemRelation() class Meta: verbose_name = "Test page" verbose_name_plural = "Test pages" def __str__(self): return self.title
class SharedContent(models.Model): """ The parent hosting object for shared content """ title = models.CharField(_("Title"), max_length=200) slug = models.SlugField(_("Template code"), unique=True, help_text=_("This unique name can be used refer to this content in in templates.")) contents = PlaceholderField("shared_content", verbose_name=_("Contents")) # NOTE: settings such as "template_name", and which plugins are allowed can be added later. class Meta: verbose_name = _("Shared content") verbose_name_plural = _("Shared content") def __unicode__(self): return self.title
class SharedContent(TranslatableModel): """ The parent hosting object for shared content """ translations = TranslatedFields( title=models.CharField(_("Title"), max_length=200)) parent_site = models.ForeignKey(Site, editable=False, default=_get_current_site) slug = models.SlugField( _("Template code"), help_text= _("This unique name can be used refer to this content in in templates." )) is_cross_site = models.BooleanField( _("Share between all sites"), blank=True, default=False, help_text= _("This allows contents can be shared between multiple sites in this project.<br>\n" "Make sure that any URLs in the content work with all sites where the content is displayed." )) contents = PlaceholderField("shared_content", verbose_name=_("Contents")) # NOTE: settings such as "template_name", and which plugins are allowed can be added later. # Adding the reverse relation for ContentItem objects # causes the admin to list these objects when moving the shared content contentitem_set = ContentItemRelation() objects = SharedContentManager() class Meta: verbose_name = _("Shared content") verbose_name_plural = _("Shared content") unique_together = (('parent_site', 'slug'), ) ordering = ('slug', ) def __str__(self): return self.title
class AbstractUnpublishableSlideShow(models.Model): """ A reusable Slide Show. """ title = models.CharField( max_length=255, ) show_title = models.BooleanField( default=False, help_text=_('Should the title of the slide show be displayed?') ) content = PlaceholderField( 'slide_show_content', plugins=appsettings.SLIDE_SHOW_CONTENT_PLUGINS, ) class Meta: abstract = True def __str__(self): return self.title
class FaqQuestion(FaqBaseModel): """ Category in the FAQ. """ # This is a separate model instead of using django-categories because: # - content needs to be placed on the category. # - the title and slug can be translated! # Be compatible with django-orderable table layout, # unfortunately, there isn't a good canonical version of it yet. order = models.PositiveIntegerField(db_index=True, blank=True, null=True) translations = TranslatedFields( title=models.CharField(_("title"), max_length=200), slug=models.SlugField(_("slug")), ) contents = PlaceholderField("faq_answer", verbose_name=_("answer")) contentitem_set = ContentItemRelation( ) # this makes sure the admin can find all deleted objects too. # Organisation category = models.ForeignKey(FaqCategory, verbose_name=_("Category"), related_name='questions') # Make association with tags optional. if TaggableManager is not None: tags = TaggableManager( blank=True, help_text=_("Tags are used to find related questions")) else: tags = None objects = FaqQuestionManager() class Meta: verbose_name = _("FAQ Question") verbose_name_plural = _("FAQ Questions") ordering = ('order', 'creation_date') def __unicode__(self): return self.title def get_relative_url(self): """ Return the link path from the archive page. """ # Return the link style, using the permalink style setting. return u'{0}{1}/'.format(self.category.get_relative_url(), self.slug) def similar_objects(self, num=None, **filters): tags = self.tags if not tags: return [] content_type = ContentType.objects.get_for_model(self.__class__) filters['content_type'] = content_type # can't filter, see # - https://github.com/alex/django-taggit/issues/32 # - http://django-taggit.readthedocs.org/en/latest/api.html#TaggableManager.similar_objects # # Otherwise this would be possible: # return tags.similar_objects(**filters) lookup_kwargs = tags._lookup_kwargs() lookup_keys = sorted(lookup_kwargs) qs = tags.through.objects.values(*lookup_kwargs.keys()) qs = qs.annotate(n=models.Count('pk')) qs = qs.exclude(**lookup_kwargs) subq = tags.all() qs = qs.filter(tag__in=list(subq)) qs = qs.order_by('-n') # from https://github.com/alex/django-taggit/issues/32#issuecomment-1002491 if filters is not None: qs = qs.filter(**filters) if num is not None: qs = qs[:num] # Normal taggit code continues # TODO: This all feels like a bit of a hack. items = {} if len(lookup_keys) == 1: # Can we do this without a second query by using a select_related() # somehow? f = tags.through._meta.get_field_by_name(lookup_keys[0])[0] objs = f.rel.to._default_manager.filter(**{ "%s__in" % f.rel.field_name: [r["content_object"] for r in qs] }) for obj in objs: items[(getattr(obj, f.rel.field_name), )] = obj else: preload = {} for result in qs: preload.setdefault(result['content_type'], set()) preload[result["content_type"]].add(result["object_id"]) for ct, obj_ids in preload.items(): ct = ContentType.objects.get_for_id(ct) for obj in ct.model_class()._default_manager.filter( pk__in=obj_ids): items[(ct.pk, obj.pk)] = obj results = [] for result in qs: obj = items[tuple(result[k] for k in lookup_keys)] obj.similar_tags = result["n"] results.append(obj) return results
class Author( AbstractCollectedContent, ICEkitContentsMixin, HeroMixin, ): """ An author model for use with article pages and assigning attribution. """ given_names = models.CharField(max_length=255, ) family_name = models.CharField( max_length=255, blank=True, ) slug = models.SlugField(max_length=255) url = models.CharField("URL", max_length=255, blank=True, help_text=_('The URL for the authors website.'), validators=[ RelativeURLValidator(), ]) oneliner = models.CharField( max_length=255, blank=True, help_text=_('An introduction about the author used on list pages.')) content = PlaceholderField('author_content') def __str__(self): return self.title @property def title(self): return " ".join((self.given_names, self.family_name)) @property def url_link_text(self): """ Return a cleaned-up version of the URL of an author's website, to use as a label for a link. TODO: make a template filter :return: String. """ url_link_text = re.sub('^https?://', '', self.url) return url_link_text.strip('/') def contributions(self): """ :return: List of all content that should show for this author. """ return [] @property def parent(self): try: return AuthorListing.objects.draft()[0] except IndexError: raise IndexError("You need to create a Author Listing Page") def get_absolute_url(self): parent_url = self.parent.get_absolute_url() return urljoin(parent_url, self.slug + "/") def get_layout_template_name(self): return "icekit_authors/detail.html" class Meta: ordering = ( 'family_name', 'given_names', )
class PublicationBase(EntityModel.materialized, ImagesFilesFluentMixin): """ RUS: Базовая модель публикаций. Определяет поля и их значения, компоненты представления, способы сортировки. """ ORDER_BY_NAME_ASC = 'publication__title' ORDER_BY_DATE = '-publication__created_at' ORDER_BY_CHRONOLOGICAL = "-publication__chronological" ORDER_BY_RECOMMENDATION = "-publication__pinned,-publication__created_at" ORDERING_MODES = ( (ORDER_BY_DATE, _('By date')), (ORDER_BY_CHRONOLOGICAL, _('By chronology')), (ORDER_BY_RECOMMENDATION, _('By recommendation')), (ORDER_BY_NAME_ASC, _('Alphabetical')), ) VIEW_COMPONENT_TILE = 'publication_tile' VIEW_COMPONENT_LIST = 'publication_list' VIEW_COMPONENTS = ( (VIEW_COMPONENT_TILE, _('Publication tile')), (VIEW_COMPONENT_LIST, _('Publication list')), ) LAYOUT_TERM_SLUG = get_layout_slug_by_model_name('publication') SHORT_SUBTITLE_MAX_WORDS_COUNT = 17 SHORT_SUBTITLE_MAX_CHARS_COUNT = 145 SHORT_SUBTITLE_TRUNCATE = '...' title = models.CharField( verbose_name=_("Title"), max_length=255, blank=False, null=False, ) subtitle = models.CharField(verbose_name=_("Subtitle"), max_length=255, blank=True, null=True, default='') lead = models.TextField(verbose_name=_("Lead"), blank=False, null=False) tags = models.CharField(verbose_name=_('tags'), help_text=_('Use semicolon as tag divider'), max_length=255, blank=True) statistic = models.IntegerField(verbose_name=_("Statistic"), blank=False, default='0') pinned = models.BooleanField(verbose_name=_("Is main publication"), default=False) content = PlaceholderField("content", verbose_name=_("Content")) contentitem_set = ContentItemRelation() unpublish_at = models.DateTimeField( db_index=True, blank=True, null=True, verbose_name=_('Unpublish at'), ) class Meta: """ RUS: Метаданные класса. """ abstract = True verbose_name = _("Publication") verbose_name_plural = _("Publications") class RESTMeta: """ RUS: Метакласс для определения параметров сериалайзера. """ include = { 'detail_url': ('rest_framework.serializers.CharField', { 'source': 'get_detail_url', 'read_only': True }), 'terms_ids': ('rest_framework.serializers.ListField', { 'child': serializers.IntegerField(), 'source': 'active_terms_ids' }), 'placeholder_id': ('rest_framework.serializers.IntegerField', { 'source': 'get_placeholder', 'read_only': True }), 'default_data_mart': ('edw.rest.serializers.entity.RelatedDataMartSerializer', { 'source': 'data_mart', 'read_only': True }), 'blocks_count': ('rest_framework.serializers.IntegerField', { 'read_only': True }), 'related_by_tags': ('edw.rest.serializers.entity.EntitySummarySerializer', { 'source': 'get_related_by_tags_publications', 'read_only': True, 'many': True }), 'gallery': ('edw.rest.serializers.related.entity_image.EntityImageSerializer', { 'read_only': True, 'many': True }), 'thumbnail': ('edw.rest.serializers.related.entity_image.EntityImageSerializer', { 'read_only': True, 'many': True }), 'attachments': ('edw.rest.serializers.related.entity_file.EntityFileSerializer', { 'read_only': True, 'many': True }), 'created_at': ('rest_framework.serializers.DateTimeField', { 'source': 'local_created_at', 'read_only': True }), 'short_subtitle': ('rest_framework.serializers.CharField', { 'source': 'get_short_subtitle', 'read_only': True }), } exclude = ['images', 'files', 'stored_request'] filters = { # Tags: см. пример в описании фильтра 'tags': ("edw_fluent.rest.filters.publication.TagFilter", { 'name': 'publication__tags' }) } def __str__(self): """ RUS: Переопределяет заголовок в строковом формате. """ return self.title @property def entity_name(self): """ RUS: Возвращает переопределенный заголовок. """ return self.title @classmethod def get_ordering_modes(cls, **kwargs): """ RUS: Возвращает отсортированные модели, являющиеся методами класса. """ full = cls.ORDERING_MODES context = kwargs.get("context", None) if context is None: return full ordering = context.get('ordering', None) if not ordering: return full ordering = ordering[0] created = '-publication__created_at' chrono = '-publication__chronological' mode_to_remove = None if ordering == created: mode_to_remove = chrono elif ordering == chrono: mode_to_remove = created return [m for m in full if m[0] != mode_to_remove] @classmethod def get_summary_annotation(cls, request): """ RUS: Возвращает аннотированные данные для сводного сериалайзера. """ return { 'publication__chronological': (Case( When(publication__pinned=True, then=ExpressionWrapper( F('created_at') + datetime.timedelta(days=1), output_field=models.DateTimeField())), default=F('created_at'), ), ), } @property def local_created_at(self): """ Преобразовывает дату/время создания объекта в формат даты/времени с учетом таймзоны заданой в настройках. В базе данных дата/время сохраняется в формате UTC и при сериализации в результате не будет указано смещение и для использования в шаблонах и внешних системах надо будет каким-то образом задавать смещение времени, для упрощения работы с сериализованными данными это преобразование нужно сделать на этапе сериализации. В конкретных моделях данных надо в сериалайзере использовать данный метод в качестве источника данных (src). Например: 2019-11-13T12:15:04.748250Z - сериализованные данные до преобразования 2019-11-13T15:15:04+03:00 - сериализованные данные после преобразования :return: дата/время в нужной таймзоне """ return datetime_to_local(self.created_at) def get_updated_at(self): """ RUS: Возвращает дату обновления публикации, преобразованную во время UTC. """ return naive_date_to_utc_date(self.updated_at) def clean(self, *args, **kwargs): """ RUS: Меняет шрифт текста в заголовке, подзаголовке, ЛИДе на шрифт Typograph. Ограничивает количество символов в заголовке до 90 знаков. """ self.title = Typograph.typograph_text(self.title, 'ru') if self.subtitle: self.subtitle = Typograph.typograph_text(self.subtitle, 'ru') if self.lead: self.lead = Typograph.typograph_text(self.lead, 'ru') max_length = getattr(settings, 'PUBLICATION_TITLE_MAX_LENGTH', 90) len_title = len(self.title) if len_title > max_length: raise ValidationError( _('The maximum number of characters {}, you have {}').format( max_length, len_title)) def get_placeholder(self): """ RUS: Возвращает id контента. """ return self.content.id @cached_property def breadcrumbs(self): """ RUS: Возвращает хлебные крошки, если есть витрина данных и страница к ней. """ data_mart = self.data_mart if data_mart: page = data_mart.get_detail_page() if page: return page.breadcrumb return None @cached_property def blocks_count(self): """ RUS: Получает количество текстовых блоков публикации. """ return self.get_blocks_count() def get_blocks_count(self): """ RUS: В соответствии с количеством и номером текстового блока формирует страницу. """ return BlockItem.objects.filter(placeholder=self.content).count() def get_short_subtitle(self): """ RUS: Подзаголовок берется из заполненного соответствующего поля, при его отсутствии берется из ЛИД. При превышении длины подзаголовок обрезается до нужного количества символов. """ value = self.subtitle if self.subtitle else self.lead return Truncator( Truncator(value).words(self.SHORT_SUBTITLE_MAX_WORDS_COUNT, truncate=self.SHORT_SUBTITLE_TRUNCATE, html=True)).chars( self.SHORT_SUBTITLE_MAX_CHARS_COUNT, truncate=self.SHORT_SUBTITLE_TRUNCATE, html=True) @cached_property def short_subtitle(self): """ RUS: Возвращает подзаголовок соответствующего размера. """ return self.get_short_subtitle() def get_summary_extra(self, context): """ ENG: Return extra data for summary serializer. RUS: Возвращает дополнительные данные для сводного сериалайзера. """ data_mart = context.get('data_mart', None) extra = { 'url': self.get_detail_url(data_mart), 'created_at': self.local_created_at, 'updated_at': self.updated_at, 'statistic': self.statistic, 'short_subtitle': self.short_subtitle, } return extra def get_detail_url(self, data_mart=None): """ RUS: Возвращает конечный url публикации. Получает адрес страницы публикации. """ if data_mart is None: data_mart = self.data_mart if data_mart: page = data_mart.get_detail_page() return reverse('publication_detail', args=[page.url.strip('/'), self.pk] if page is not None else [self.pk]) else: return reverse('publication_detail', args=[self.pk]) def get_tags(self): if self.tags and self.tags.strip(): tags = [tag.strip() for tag in self.tags.split(';')] tags = [tag for tag in tags if tag] return tags else: return [] @cached_property def text_blocks(self): """ RUS: Возвращает список текстовых блоков. """ return list(self.content.contentitems.instance_of(BlockItem)) def get_related_by_tags_publications(self, exclude_blockitem=True): """ Вернуть публикации, имеющие общие тэги с текущей публикацией """ exclude_ids = list( self.content.contentitems.instance_of(BlockItem).exclude( blockitem__subjects__isnull=True).values_list( 'blockitem__subjects__id', flat=True)) if exclude_blockitem else [] exclude_ids.append(self.id) if not hasattr(self, '_Publication__related_publications_cache'): tags = self.get_tags() if tags: related_by_tags_count = getattr(settings, 'RELATED_BY_TAGS_COUNT', 5) self.__related_publications_cache = EntityModel.objects \ .active() \ .exclude(pk__in=exclude_ids) \ .filter(reduce( or_, [Q(publication__tags__icontains=tag) for tag in tags] )) \ .order_by('-created_at')[:related_by_tags_count] else: self.__related_publications_cache = self.__class__.objects.none( ) return self.__related_publications_cache @classmethod def get_view_components(cls, **kwargs): """ RUS: Возвращает view components (компоненты представлений): плиточное представление, список. """ full = cls.VIEW_COMPONENTS if hasattr(cls, 'VIEW_COMPONENT_MAP'): reduced = tuple( [c for c in full if c[0] != cls.VIEW_COMPONENT_MAP]) context = kwargs.get("context", None) if context is None: return reduced term_ids = context.get('real_terms_ids', None) if not term_ids: return reduced return full def get_or_create_placeholder(self): try: placeholder = Placeholder.objects.get_by_slot(self, "content") except Placeholder.DoesNotExist: placeholder = Placeholder.objects.create_for_object( self, "content") return placeholder @classmethod def validate_term_model(cls): """ RUS: Валидация модели терминов. Добавляет термины класса объекта в дерево согласно структуре наследования. """ view_root = get_or_create_view_layouts_root() try: # publication root TermModel.objects.get(slug=cls.LAYOUT_TERM_SLUG, parent=view_root) except TermModel.DoesNotExist: publication_root = TermModel( slug=cls.LAYOUT_TERM_SLUG, parent=view_root, name=_('Publication'), semantic_rule=TermModel.XOR_RULE, system_flags=_publication_root_terms_system_flags_restriction) publication_root.save() super(PublicationBase, cls).validate_term_model() def need_terms_validation_after_save(self, origin, **kwargs): """ RUS: Проставляет автоматически термины, связанные с макетом представления публикации, после ее сохранения. """ do_validate_layout = kwargs["context"]["validate_view_layout"] = True return super(PublicationBase, self).need_terms_validation_after_save( origin, **kwargs) or do_validate_layout def validate_terms(self, origin, **kwargs): """ RUS: При выборе макета представления и его сохранения, проставляются соответствующие термины и выбирается автоматически соответствующий шаблон. При изменении макета, термины удаляются и заменяются новыми, соответствующими новому макету. """ context = kwargs["context"] force_validate_terms = context.get("force_validate_terms", False) if force_validate_terms or context.get("validate_view_layout", False): views_layouts = get_views_layouts() to_remove = [ v for k, v in views_layouts.items() if k != PublicationBase.LAYOUT_TERM_SLUG ] self.terms.remove(*to_remove) to_add = views_layouts.get(PublicationBase.LAYOUT_TERM_SLUG, None) if to_add is not None: self.terms.add(to_add) super(PublicationBase, self).validate_terms(origin, **kwargs)