Exemplo n.º 1
0
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)
Exemplo n.º 2
0
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
Exemplo n.º 3
0
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'),
        )
Exemplo n.º 4
0
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()
Exemplo n.º 5
0
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)
Exemplo n.º 6
0
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
Exemplo n.º 7
0
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'),
        )
Exemplo n.º 8
0
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)
Exemplo n.º 9
0
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'),
        )
Exemplo n.º 10
0
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
Exemplo n.º 11
0
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
Exemplo n.º 12
0
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
Exemplo n.º 13
0
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
Exemplo n.º 14
0
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
Exemplo n.º 15
0
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',
        )
Exemplo n.º 16
0
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)