class BlogConfig(TranslatableModel, AppHookConfig):
    """
    Adds translatable, per-app-instance fields.
    """

    translations = TranslatedFields(
        app_title=models.CharField(_("application title"), max_length=234),
        object_name=models.CharField(_("object name"), max_length=234, default=get_setting("DEFAULT_OBJECT_NAME")),
    )

    class Meta:
        verbose_name = _("blog config")
        verbose_name_plural = _("blog configs")

    def get_app_title(self):
        return getattr(self, "app_title", _("untitled"))

    @property
    def schemaorg_type(self):
        """Compatibility shim to fetch data from legacy gplus_type field."""
        return self.gplus_type class BlogConfigForm(AppDataForm):
    """
    Settings that can be changed per-apphook.

    Their default value is the same as the corresponding Django settings,
    but it can be customized per each apphook and changed at runtime.
    """

    app_title = get_setting("AUTO_APP_TITLE")
    """
    Free text title that can be used as title in templates
    """
    object_name = get_setting("DEFAULT_OBJECT_NAME")
    """
    Free text label for Blog items in django CMS Wizard
    """
    #: Post published by default (default: :ref:`DEFAULT_PUBLISHED <DEFAULT_PUBLISHED>`)
    default_published = forms.BooleanField(
        label=_("Post published by default"), required=False, initial=get_setting("DEFAULT_PUBLISHED")
    )
    #: Default size of full images
    default_image_full = forms.ModelChoiceField(
        label=_("Default size of full images"),
        queryset=ThumbnailOption.objects.all(),
        required=False,
        help_text=_("If left empty the image size will have to be set for every newly created post."),
    )
    #: Default size of thumbnail images
    default_image_thumbnail = forms.ModelChoiceField(
        label=_("Default size of thumbnail images"),
        queryset=ThumbnailOption.objects.all(),
        required=False,
        help_text=_("If left empty the thumbnail image size will have to be set for every newly created post."),
    )
    #: Structure of permalinks (get_absolute_url); see :ref:`AVAILABLE_PERMALINK_STYLES <AVAILABLE_PERMALINK_STYLES>`
    url_patterns = forms.ChoiceField(
        label=_("Permalink structure"),
        required=False,
        initial=get_setting("AVAILABLE_PERMALINK_STYLES")[0][0],
        choices=get_setting("AVAILABLE_PERMALINK_STYLES"),
    )
    #: Use placeholder and plugins for article body (default: :ref:`USE_PLACEHOLDER <USE_PLACEHOLDER>`)
    use_placeholder = forms.BooleanField(
        label=_("Use placeholder and plugins for article body"), required=False, initial=get_setting("USE_PLACEHOLDER")
    )
    #: Use abstract field (default: :ref:`USE_ABSTRACT <USE_ABSTRACT>`)
    use_abstract = forms.BooleanField(
        label=_("Use abstract field"), required=False, initial=get_setting("USE_ABSTRACT")
    )
    #: Enable related posts (default: :ref:`USE_RELATED <USE_RELATED>`)
    use_related = forms.BooleanField(
        label=_("Enable related posts"), required=False, initial=get_setting("USE_RELATED")
    )
    #: Set author by default (default: :ref:`AUTHOR_DEFAULT <AUTHOR_DEFAULT>`)
    set_author = forms.BooleanField(
        label=_("Set author"),
        required=False,
        help_text=_("Set author by default"),
        initial=get_setting("AUTHOR_DEFAULT"),
    )
    #: When paginating list views, how many articles per page? (default: :ref:`PAGINATION <PAGINATION>`)
    paginate_by = forms.IntegerField(
        label=_("Paginate size"),
        required=False,
        initial=get_setting("PAGINATION"),
        help_text=_("When paginating list views, how many articles per page?"),
    )
    #: Alternative directory to load the blog templates from (default: "")
    template_prefix = forms.CharField(
        label=_("Template prefix"),
        required=False,
        initial="",
        help_text=_("Alternative directory to load the blog templates from"),
    )
    #: Menu structure (default: ``MENU_TYPE_COMPLETE``, see :ref:`MENU_TYPES <MENU_TYPES>`)
    menu_structure = forms.ChoiceField(
        label=_("Menu structure"),
        required=True,
        choices=get_setting("MENU_TYPES"),
        initial=MENU_TYPE_COMPLETE,
        help_text=_("Structure of the django CMS menu"),
    )
    #: Show empty categories in menu (default: :ref:`MENU_EMPTY_CATEGORIES <MENU_EMPTY_CATEGORIES>`)
    menu_empty_categories = forms.BooleanField(
        label=_("Show empty categories in menu"),
        initial=get_setting("MENU_EMPTY_CATEGORIES"),
        required=False,
        help_text=_("Show categories with no post attached in the menu"),
    )
    #: Sitemap changefreq (default: :ref:`SITEMAP_CHANGEFREQ_DEFAULT <SITEMAP_CHANGEFREQ_DEFAULT>`,
    #: see: :ref:`SITEMAP_CHANGEFREQ <SITEMAP_CHANGEFREQ>`)
    sitemap_changefreq = forms.ChoiceField(
        label=_("Sitemap changefreq"),
        required=True,
        choices=get_setting("SITEMAP_CHANGEFREQ"),
        initial=get_setting("SITEMAP_CHANGEFREQ_DEFAULT"),
        help_text=_("Changefreq attribute for sitemap items"),
    )
    #: Sitemap priority (default: :ref:`SITEMAP_PRIORITY_DEFAULT <SITEMAP_PRIORITY_DEFAULT>`)
    sitemap_priority = forms.CharField(
        label=_("Sitemap priority"),
        required=True,
        initial=get_setting("SITEMAP_PRIORITY_DEFAULT"),
        help_text=_("Priority attribute for sitemap items"),
    )
    #: Object type (default: :ref:`TYPE <TYPE>`, see :ref:`TYPES <TYPES>`)
    object_type = forms.ChoiceField(
        label=_("Object type"), required=False, choices=get_setting("TYPES"), initial=get_setting("TYPE")
    )
    #: Facebook type (default: :ref:`FB_TYPE <FB_TYPE>`, see :ref:`FB_TYPES <FB_TYPES>`)
    og_type = forms.ChoiceField(
        label=_("Facebook type"), required=False, choices=get_setting("FB_TYPES"), initial=get_setting("FB_TYPE")
    )
    #: Facebook application ID (default: :ref:`FB_PROFILE_ID <FB_PROFILE_ID>`)
    og_app_id = forms.CharField(
        max_length=200, label=_("Facebook application ID"), required=False, initial=get_setting("FB_PROFILE_ID")
    )
    #: Facebook profile ID (default: :ref:`FB_PROFILE_ID <FB_PROFILE_ID>`)
    og_profile_id = forms.CharField(
        max_length=200, label=_("Facebook profile ID"), required=False, initial=get_setting("FB_PROFILE_ID")
    )
    #: Facebook page URL (default: :ref:`FB_PUBLISHER <FB_PUBLISHER>`)
    og_publisher = forms.CharField(
        max_length=200, label=_("Facebook page URL"), required=False, initial=get_setting("FB_PUBLISHER")
    )
    #: Facebook author URL (default: :ref:`FB_AUTHOR_URL <FB_AUTHOR_URL>`)
    og_author_url = forms.CharField(
        max_length=200, label=_("Facebook author URL"), required=False, initial=get_setting("FB_AUTHOR_URL")
    )
    #: Facebook author (default: :ref:`FB_AUTHOR <FB_AUTHOR>`)
    og_author = forms.CharField(
        max_length=200, label=_("Facebook author"), required=False, initial=get_setting("FB_AUTHOR")
    )
    #: Twitter type field (default: :ref:`TWITTER_TYPE <TWITTER_TYPE>`)
    twitter_type = forms.ChoiceField(
        label=_("Twitter type"),
        required=False,
        choices=get_setting("TWITTER_TYPES"),
        initial=get_setting("TWITTER_TYPE"),
    )
    #: Twitter site handle (default: :ref:`TWITTER_SITE <TWITTER_SITE>`)
    twitter_site = forms.CharField(
        max_length=200, label=_("Twitter site handle"), required=False, initial=get_setting("TWITTER_SITE")
    )
    #: Twitter author handle (default: :ref:`TWITTER_AUTHOR <TWITTER_AUTHOR>`)
    twitter_author = forms.CharField(
        max_length=200, label=_("Twitter author handle"), required=False, initial=get_setting("TWITTER_AUTHOR")
    )
    #: Schema.org object type (default: :ref:`SCHEMAORG_TYPE <SCHEMAORG_TYPE>`)
    gplus_type = forms.ChoiceField(
        label=_("Schema.org type"),
        required=False,
        choices=get_setting("SCHEMAORG_TYPES"),
        initial=get_setting("SCHEMAORG_TYPE"),
    )
    #: Schema.org author name abstract field (default: :ref:`SCHEMAORG_AUTHOR <SCHEMAORG_AUTHOR>`)
    gplus_author = forms.CharField(
        max_length=200, label=_("Schema.org author name"), required=False, initial=get_setting("SCHEMAORG_AUTHOR")
    )
    #: Send notifications on post update. Require channels integration
    send_knock_create = forms.BooleanField(
        label=_("Send notifications on post publish"),
        required=False,
        initial=False,
        help_text=_("Emits a desktop notification -if enabled- when publishing a new post"),
    )
    #: Send notifications on post update. Require channels integration
    send_knock_update = forms.BooleanField(
        label=_("Send notifications on post update"),
        required=False,
        initial=False,
        help_text=_("Emits a desktop notification -if enabled- when editing a published post"),
    ) class CategoryAdminForm(ConfigFormBase, TranslatableModelForm):
    def __init__(self, *args, **kwargs):
        self.base_fields["meta_description"].validators = [MaxLengthValidator(get_setting("META_DESCRIPTION_LENGTH"))]
        original_attrs = self.base_fields["meta_description"].widget.attrs
        if "cols" in original_attrs:
            del original_attrs["cols"]
        if "rows" in original_attrs:
            del original_attrs["rows"]
        original_attrs["maxlength"] = get_setting("META_DESCRIPTION_LENGTH")
        self.base_fields["meta_description"].widget = forms.TextInput(original_attrs)
        super().__init__(*args, **kwargs)

        if "parent" in self.fields:
            qs = self.fields["parent"].queryset
            if self.instance.pk:
                qs = qs.exclude(pk__in=[self.instance.pk] + [child.pk for child in self.instance.descendants()])
            config = None
            if getattr(self.instance, "app_config_id", None):
                qs = qs.namespace(self.instance.app_config.namespace)
            elif "app_config" in self.initial:
                config = BlogConfig.objects.get(pk=self.initial["app_config"])
            elif self.data.get("app_config", None):
                config = BlogConfig.objects.get(pk=self.data["app_config"])
            if config:
                qs = qs.namespace(config.namespace)
            self.fields["parent"].queryset = qs

    class Meta:
        model = BlogCategory
        fields = "__all__" class PostAdminForm(PostAdminFormBase):
    def __init__(self, *args, **kwargs):
        self.base_fields["meta_description"].validators = [MaxLengthValidator(get_setting("META_DESCRIPTION_LENGTH"))]
        original_attrs = self.base_fields["meta_description"].widget.attrs
        if "cols" in original_attrs:
            del original_attrs["cols"]
        if "rows" in original_attrs:
            del original_attrs["rows"]
        original_attrs["maxlength"] = get_setting("META_DESCRIPTION_LENGTH")
        self.base_fields["meta_description"].widget = forms.TextInput(original_attrs)
        self.base_fields["meta_title"].validators = [MaxLengthValidator(get_setting("META_TITLE_LENGTH"))]
        super().__init__(*args, **kwargs)
        if "categories" in self.fields:
            if self.app_config and self.app_config.url_patterns == PERMALINK_TYPE_CATEGORY:
                self.fields["categories"].required = True
            self.fields["categories"].queryset = self.available_categories

        if "app_config" in self.fields:
            # Don't allow app_configs to be added here. The correct way to add an
            # apphook-config is to create an apphook on a cms Page.
            self.fields["app_config"].widget.can_add_related = False

        if self.app_config:
            if not self.initial.get("main_image_full", ""):
                self.initial["main_image_full"] = self.app_config.app_data["config"].get("default_image_full")
            if not self.initial.get("main_image_thumbnail", ""):
                self.initial["main_image_thumbnail"] = self.app_config.app_data["config"].get(
                    "default_image_thumbnail"
                ) class BlogCategory(BlogMetaMixin, TranslatableModel):
    """
    Blog category
    """

    parent = models.ForeignKey(
        "self", verbose_name=_("parent"), null=True, blank=True, related_name="children", on_delete=models.CASCADE
    )
    date_created = models.DateTimeField(_("created at"), auto_now_add=True)
    date_modified = models.DateTimeField(_("modified at"), auto_now=True)
    app_config = AppHookConfigField(BlogConfig, null=True, verbose_name=_("app. config"))

    translations = TranslatedFields(
        name=models.CharField(_("name"), max_length=752),
        slug=models.SlugField(_("slug"), max_length=752, blank=True, db_index=True),
        meta_description=models.TextField(verbose_name=_("category meta description"), blank=True, default=""),
        meta={"unique_together": (("language_code", "slug"),)},
    )

    objects = AppHookConfigTranslatableManager()

    _metadata = {
        "title": "get_title",
        "description": "get_description",
        "og_description": "get_description",
        "twitter_description": "get_description",
        "schemaorg_description": "get_description",
        "schemaorg_type": "get_meta_attribute",
        "locale": "get_locale",
        "object_type": "get_meta_attribute",
        "og_type": "get_meta_attribute",
        "og_app_id": "get_meta_attribute",
        "og_profile_id": "get_meta_attribute",
        "og_publisher": "get_meta_attribute",
        "og_author_url": "get_meta_attribute",
        "og_author": "get_meta_attribute",
        "twitter_type": "get_meta_attribute",
        "twitter_site": "get_meta_attribute",
        "twitter_author": "get_meta_attribute",
        "url": "get_absolute_url",
    }

    class Meta:
        verbose_name = _("blog category")
        verbose_name_plural = _("blog categories")

    def descendants(self):
        children = []
        if self.children.exists():
            children.extend(self.children.all())
            for child in self.children.all():
                children.extend(child.descendants())
        return children

    @cached_property
    def linked_posts(self):
        return self.blog_posts.namespace(self.app_config.namespace)

    @cached_property
    def count(self):
        return self.linked_posts.published().count()

    @cached_property
    def count_all_sites(self):
        return self.linked_posts.published(current_site=False).count()

    def get_absolute_url(self, lang=None):
        lang = _get_language(self, lang)
        if self.has_translation(lang):
            slug = self.safe_translation_getter("slug", language_code=lang)
            return reverse(
                "%s:posts-category" % self.app_config.namespace,
                kwargs={"category": slug},
                current_app=self.app_config.namespace,
            )
        # in case category doesn't exist in this language, gracefully fallback
        # to posts-latest
        return reverse("%s:posts-latest" % self.app_config.namespace, current_app=self.app_config.namespace)

    def __str__(self):
        default = gettext("BlogCategory (no translation)")
        return self.safe_translation_getter("name", any_language=True, default=default)

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        for lang in self.get_available_languages():
            self.set_current_language(lang)
            if not self.slug and self.name:
                self.slug = slugify(force_str(self.name))
        self.save_translations()

    def get_title(self):
        title = self.safe_translation_getter("name", any_language=True)
        return title.strip()

    def get_description(self):
        description = self.safe_translation_getter("meta_description", any_language=True)
        return escape(strip_tags(description)).strip() class Post(KnockerModel, BlogMetaMixin, TranslatableModel):
    """
    Blog post
    """

    author = models.ForeignKey(
        dj_settings.AUTH_USER_MODEL,
        verbose_name=_("author"),
        null=True,
        blank=True,
        related_name="djangocms_blog_post_author",
        on_delete=models.PROTECT,
    )

    date_created = models.DateTimeField(_("created"), auto_now_add=True)
    date_modified = models.DateTimeField(_("last modified"), auto_now=True)
    date_published = models.DateTimeField(_("published since"), null=True, blank=True)
    date_published_end = models.DateTimeField(_("published until"), null=True, blank=True)
    date_featured = models.DateTimeField(_("featured date"), null=True, blank=True)
    publish = models.BooleanField(_("publish"), default=False)
    categories = models.ManyToManyField(
        "djangocms_blog.BlogCategory", verbose_name=_("category"), related_name="blog_posts", blank=True
    )
    main_image = FilerImageField(
        verbose_name=_("main image"),
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        related_name="djangocms_blog_post_image",
    )
    main_image_thumbnail = models.ForeignKey(
        thumbnail_model,
        verbose_name=_("main image thumbnail"),
        related_name="djangocms_blog_post_thumbnail",
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )
    main_image_full = models.ForeignKey(
        thumbnail_model,
        verbose_name=_("main image full"),
        related_name="djangocms_blog_post_full",
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )
    enable_comments = models.BooleanField(
        verbose_name=_("enable comments on post"), default=get_setting("ENABLE_COMMENTS")
    )
    sites = models.ManyToManyField(
        "sites.Site",
        verbose_name=_("Site(s)"),
        blank=True,
        help_text=_(
            "Select sites in which to show the post. "
            "If none is set it will be "
            "visible in all the configured sites."
        ),
    )
    app_config = AppHookConfigField(BlogConfig, null=True, verbose_name=_("app. config"))

    translations = TranslatedFields(
        title=models.CharField(_("title"), max_length=752),
        slug=models.SlugField(_("slug"), max_length=752, blank=True, db_index=True, allow_unicode=True),
        subtitle=models.CharField(verbose_name=_("subtitle"), max_length=767, blank=True, default=""),
        abstract=HTMLField(_("abstract"), blank=True, default="", configuration="BLOG_ABSTRACT_CKEDITOR"),
        meta_description=models.TextField(verbose_name=_("post meta description"), blank=True, default=""),
        meta_keywords=models.TextField(verbose_name=_("post meta keywords"), blank=True, default=""),
        meta_title=models.CharField(
            verbose_name=_("post meta title"),
            help_text=_("used in title tag and social sharing"),
            max_length=2000,
            blank=True,
            default="",
        ),
        post_text=HTMLField(_("text"), default="", blank=True, configuration="BLOG_POST_TEXT_CKEDITOR"),
        meta={"unique_together": (("language_code", "slug"),)},
    )
    media = PlaceholderField("media", related_name="media")
    content = PlaceholderField("post_content", related_name="post_content")
    liveblog = PlaceholderField("live_blog", related_name="live_blog")
    enable_liveblog = models.BooleanField(verbose_name=_("enable liveblog on post"), default=False)

    objects = GenericDateTaggedManager()
    tags = TaggableManager(
        blank=True,
        related_name="djangocms_blog_tags",
        help_text=_("Type a tag and hit tab, or start typing and select from autocomplete list."),
    )

    related = SortedManyToManyField("self", verbose_name=_("Related Posts"), blank=True, symmetrical=False)

    _metadata = {
        "title": "get_title",
        "description": "get_description",
        "keywords": "get_keywords",
        "og_description": "get_description",
        "twitter_description": "get_description",
        "schemaorg_description": "get_description",
        "locale": "get_locale",
        "image": "get_image_full_url",
        "image_width": "get_image_width",
        "image_height": "get_image_height",
        "object_type": "get_meta_attribute",
        "og_type": "get_meta_attribute",
        "og_app_id": "get_meta_attribute",
        "og_profile_id": "get_meta_attribute",
        "og_publisher": "get_meta_attribute",
        "og_author_url": "get_meta_attribute",
        "og_author": "get_meta_attribute",
        "twitter_type": "get_meta_attribute",
        "twitter_site": "get_meta_attribute",
        "twitter_author": "get_meta_attribute",
        "schemaorg_type": "get_meta_attribute",
        "published_time": "date_published",
        "modified_time": "date_modified",
        "expiration_time": "date_published_end",
        "tag": "get_tags",
        "url": "get_absolute_url",
    }

    class Meta:
        verbose_name = _("blog article")
        verbose_name_plural = _("blog articles")
        ordering = ("-date_published", "-date_created")
        get_latest_by = "date_published"

    def __str__(self):
        default = gettext("Post (no translation)")
        return self.safe_translation_getter("title", any_language=True, default=default)

    @property
    def guid(self, language=None):
        if not language:
            language = self.get_current_language()
        base_string = "-{0}-{2}-{1}-".format(
            language,
            self.app_config.namespace,
            self.safe_translation_getter("slug", language_code=language, any_language=True),
        )
        return hashlib.sha256(force_bytes(base_string)).hexdigest()

    @property
    def date(self):
        if self.date_featured:
            return self.date_featured
        return self.date_published

    def save(self, *args, **kwargs):
        """
        Handle some auto configuration during save
        """
        if self.publish and self.date_published is None:
            self.date_published = timezone.now()
        if not self.slug and self.title:
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)

    def save_translation(self, translation, *args, **kwargs):
        """
        Handle some auto configuration during save
        """
        if not translation.slug and translation.title:
            translation.slug = slugify(translation.title)
        super().save_translation(translation, *args, **kwargs)

    def get_absolute_url(self, lang=None):
        lang = _get_language(self, lang)
        with switch_language(self, lang):
            category = self.categories.first()
            kwargs = {}
            if self.date_published:
                current_date = self.date_published
            else:
                current_date = self.date_created
            urlconf = get_setting("PERMALINK_URLS")[self.app_config.url_patterns]
            if "<int:year>" in urlconf:
                kwargs["year"] = current_date.year
            if "<int:month>" in urlconf:
                kwargs["month"] = "%02d" % current_date.month
            if "<int:day>" in urlconf:
                kwargs["day"] = "%02d" % current_date.day
            if "<str:slug>" in urlconf or "<slug:slug>" in urlconf:
                kwargs["slug"] = self.safe_translation_getter("slug", language_code=lang, any_language=True)  # NOQA
            if "<slug:category>" in urlconf or "<str:category>" in urlconf:
                kwargs["category"] = category.safe_translation_getter(
                    "slug", language_code=lang, any_language=True
                )  # NOQA
            return reverse(
                "%s:post-detail" % self.app_config.namespace, kwargs=kwargs, current_app=self.app_config.namespace
            )

    def get_title(self):
        title = self.safe_translation_getter("meta_title", any_language=True)
        if not title:
            title = self.safe_translation_getter("title", any_language=True)
        return title.strip()

    def get_keywords(self):
        """
        Returns the list of keywords (as python list)
        :return: list
        """
        return self.safe_translation_getter("meta_keywords", default="").strip().split(",")

    def get_description(self):
        description = self.safe_translation_getter("meta_description", any_language=True)
        if not description:
            description = self.safe_translation_getter("abstract", any_language=True)
        return escape(strip_tags(description)).strip()

    def get_image_full_url(self):
        if self.main_image:
            return self.build_absolute_uri(self.main_image.url)
        return ""

    def get_image_width(self):
        if self.main_image:
            return self.main_image.width

    def get_image_height(self):
        if self.main_image:
            return self.main_image.height

    def get_tags(self):
        """
        Returns the list of object tags as comma separated list
        """
        taglist = [tag.name for tag in self.tags.all()]
        return ",".join(taglist)

    def get_author(self):
        """
        Return the author (user) objects
        """
        return self.author

    def _set_default_author(self, current_user):
        if not self.author_id and self.app_config.set_author:
            if get_setting("AUTHOR_DEFAULT") is True:
                user = current_user
            else:
                user = get_user_model().objects.get(username=get_setting("AUTHOR_DEFAULT"))
            self.author = user

    def thumbnail_options(self):
        if self.main_image_thumbnail_id:
            return self.main_image_thumbnail.as_dict
        else:
            return get_setting("IMAGE_THUMBNAIL_SIZE")

    def full_image_options(self):
        if self.main_image_full_id:
            return self.main_image_full.as_dict
        else:
            return get_setting("IMAGE_FULL_SIZE")

    @property
    def is_published(self):
        """
        Checks wether the blog post is *really* published by checking publishing dates too
        """
        return (
            self.publish
            and (self.date_published and self.date_published <= timezone.now())
            and (self.date_published_end is None or self.date_published_end > timezone.now())
        )

    def should_knock(self, signal_type, created=False):
        """
        Returns whether to emit knocks according to the post state
        """
        new = self.app_config.send_knock_create and self.is_published and self.date_published == self.date_modified
        updated = self.app_config.send_knock_update and self.is_published
        return (new or updated) and signal_type in ("post_save", "post_delete")

    def get_cache_key(self, language, prefix):
        return "djangocms-blog:{2}:{0}:{1}".format(language, self.guid, prefix)

    @property
    def liveblog_group(self):
        return "liveblog-{apphook}-{lang}-{post}".format(
            lang=self.get_current_language(),
            apphook=self.app_config.namespace,
            post=self.safe_translation_getter("slug", any_language=True),
        ) MENU_TYPE_NONE = "none" PERMALINK_TYPE_CATEGORY = "category" PERMALINK_TYPE_FULL_DATE = "full_date" def get_setting(name):
    """Get setting value from django settings with fallback to globals defaults."""
    from django.conf import settings

    return getattr(settings, "BLOG_%s" % name, params["BLOG_%s" % name]) class BaseTest(BaseTestCase):
    """
    Base class with utility function
    """

    category_1 = None
    thumb_1 = None
    thumb_2 = None

    _pages_data = (
        {
            "en": {"title": "page one", "template": "blog.html", "publish": True},
            "fr": {"title": "page un", "publish": True},
            "it": {"title": "pagina uno", "publish": True},
        },
        {
            "en": {
                "title": "page two",
                "template": "blog.html",
                "publish": True,
                "apphook": "BlogApp",
                "apphook_namespace": "sample_app",
            },
            "fr": {"title": "page deux", "publish": True},
            "it": {"title": "pagina due", "publish": True},
        },
        {
            "en": {
                "title": "page three",
                "template": "blog.html",
                "publish": True,
                "apphook": "BlogApp",
                "apphook_namespace": "sample_app2",
            },
            "fr": {"title": "page trois", "publish": True},
            "it": {"title": "pagina tre", "publish": True},
        },
    )

    _post_data = (
        {
            "en": {
                "title": "First post",
                "abstract": "<p>first line</p>",
                "description": "This is the description",
                "keywords": "keyword1, keyword2",
                "text": "Post text",
                "app_config": "sample_app",
                "publish": True,
            },
            "it": {
                "title": "Primo post",
                "abstract": "<p>prima riga</p>",
                "description": "Questa è la descrizione",
                "keywords": "keyword1, keyword2",
                "text": "Testo del post",
            },
        },
        {
            "en": {
                "title": "Second post",
                "abstract": "<p>second post first line</p>",
                "description": "Second post description",
                "keywords": "keyword3, keyword4",
                "text": "Second post text",
                "app_config": "sample_app",
                "publish": False,
            },
            "it": {
                "title": "Secondo post",
                "abstract": "<p>prima riga del secondo post</p>",
                "description": "Descrizione del secondo post",
                "keywords": "keyword3, keyword4",
                "text": "Testo del secondo post",
                "app_config": "sample_app",
            },
        },
        {
            "en": {
                "title": "Third post",
                "abstract": "<p>third post first line</p>",
                "description": "third post description",
                "keywords": "keyword5, keyword6",
                "text": "Third post text",
                "app_config": "sample_app",
                "publish": False,
            },
            "it": {
                "title": "Terzo post",
                "abstract": "<p>prima riga del terzo post</p>",
                "description": "Descrizione del terzo post",
                "keywords": "keyword5, keyword6",
                "text": "Testo del terzo post",
            },
        },
        {
            "en": {
                "title": "Different appconfig",
                "abstract": "<p>Different appconfig first line</p>",
                "description": "Different appconfig description",
                "keywords": "keyword5, keyword6",
                "text": "Different appconfig text",
                "app_config": "sample_app2",
                "publish": True,
            },
            "it": {
                "title": "Altro appconfig",
                "abstract": "<p>prima riga del Altro appconfig</p>",
                "description": "Descrizione Altro appconfig",
                "keywords": "keyword5, keyword6",
                "text": "Testo del Altro appconfig",
            },
        },
    )

    _categories_data = (
        {"en": {"name": "Very loud", "app_config": "sample_app"}, "it": {"name": "Fortissimo"}},
        {"en": {"name": "Very very silent", "app_config": "sample_app"}, "it": {"name": "Pianississimo"}},
        {"en": {"name": "Almost", "app_config": "sample_app"}, "it": {"name": "Mezzo"}},
        {"en": {"name": "Drums", "app_config": "sample_app2"}, "it": {"name": "Tamburi"}},
        {"en": {"name": "Guitars", "app_config": "sample_app2"}, "it": {"name": "Chitarre"}},
        {
            "en": {"name": "Loud", "parent_id": _get_cat_pk("en", "Almost"), "app_config": "sample_app"},
            "it": {"name": "Forte", "parent_id": _get_cat_pk("it", "Mezzo")},
        },
        {"en": {"name": "Silent", "parent_id": _get_cat_pk("en", "Almost"), "app_config": "sample_app"}},
    )

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.thumb_1, __ = ThumbnailOption.objects.get_or_create(
            name="base", width=100, height=100, crop=True, upscale=False
        )
        cls.thumb_2, __ = ThumbnailOption.objects.get_or_create(
            name="main", width=200, height=200, crop=False, upscale=False
        )
        cls.app_config_1, __ = BlogConfig.objects.get_or_create(namespace="sample_app")
        cls.app_config_2, __ = BlogConfig.objects.get_or_create(namespace="sample_app2")
        cls.app_config_1.app_title = "app1"
        cls.app_config_1.object_name = "Blog"
        cls.app_config_1.app_data.config.paginate_by = 1
        cls.app_config_1.app_data.config.send_knock_create = True
        cls.app_config_1.app_data.config.send_knock_update = True
        cls.app_config_1.save()
        cls.app_config_2.app_title = "app2"
        cls.app_config_2.object_name = "Article"
        cls.app_config_2.app_data.config.paginate_by = 2
        cls.app_config_2.app_data.config.send_knock_create = True
        cls.app_config_2.app_data.config.send_knock_update = True
        cls.app_config_2.save()
        cls.app_configs = {
            "sample_app": cls.app_config_1,
            "sample_app2": cls.app_config_2,
        }
        cls.category_1 = BlogCategory.objects.create(name="category 1", app_config=cls.app_config_1)
        cls.category_1.set_current_language("it", initialize=True)
        cls.category_1.name = "categoria 1"
        cls.category_1.save()
        cls.site_2, __ = Site.objects.get_or_create(domain="http://example2.com", name="example 2")
        cls.site_3, __ = Site.objects.get_or_create(domain="http://example3.com", name="example 3")
        cache.clear()

    @classmethod
    def tearDownClass(cls):
        cache.clear()
        super().tearDownClass()

    def tearDown(self):
        cache.clear()
        super().tearDown()

    def get_nodes(self, menu_pool, request):
        try:
            nodes = menu_pool.get_renderer(request).get_nodes()
        except AttributeError:
            nodes = menu_pool.get_nodes(request)
        return nodes

    def _get_category(self, data, category=None, lang="en"):
        data = deepcopy(data)
        for k, v in data.items():
            if callable(v):
                data[k] = v()
        if not category:
            with smart_override(lang):
                data["app_config"] = self.app_configs[data["app_config"]]
                category = BlogCategory.objects.create(**data)
        else:
            category.set_current_language(lang, initialize=True)
            for attr, val in data.items():
                setattr(category, attr, val)
            category.save()
        return category

    def _get_post(self, data, post=None, lang="en", sites=None):
        if not post:
            post_data = {
                "author": self.user,
                "title": data["title"],
                "abstract": data["abstract"],
                "meta_description": data["description"],
                "meta_keywords": data["keywords"],
                "app_config": self.app_configs[data["app_config"]],
            }
            post = Post.objects.create(**post_data)
        else:
            post.create_translation(
                lang,
                title=data["title"],
                abstract=data["abstract"],
                meta_description=data["description"],
                meta_keywords=data["keywords"],
            )
        post.categories.add(self.category_1)
        if sites:
            for site in sites:
                post.sites.add(site)
        return post

    def get_posts(self, sites=None):
        posts = []
        cache.clear()
        if Post.objects.all().exists():
            return list(Post.objects.all())
        for post in self._post_data:
            post1 = self._get_post(post["en"], sites=sites)
            post1 = self._get_post(post["it"], post=post1, lang="it")
            post1.publish = post["en"]["publish"]
            post1.main_image = self.create_filer_image_object()
            post1.save()
            posts.append(post1)
        return posts

    def get_post_index(self):
        from haystack import connections
        from haystack.constants import DEFAULT_ALIAS

        search_conn = connections[DEFAULT_ALIAS]
        unified_index = search_conn.get_unified_index()
        index = unified_index.get_index(Post)
        return index

    def _reset_menus(self):
        cache.clear()
        BlogCategoryMenu._config = {}
        BlogNavModifier._config = {}

    def _reload_menus(self):
        menu_pool.clear(all=True)
        menu_pool.discover_menus()
        # All cms menu modifiers should be removed from menu_pool.modifiers
        # so that they do not interfere with our menu nodes
        menu_pool.modifiers = [m for m in menu_pool.modifiers if m.__module__.startswith("djangocms_blog")]
        self._reset_menus()

    def read_json(self, path, raw=False):
        """
        Read a json file from the given path

        :param path: JSON file path (relative to the current test file
        :type path: str
        :param raw: return the file content without loading as a json object
        :type raw: bool
        :return json data (either raw or loaded)
        :type: (dict|str)
        """
        full_path = os.path.join(os.path.dirname(__file__), path)
        with open(full_path) as src:
            if raw:
                return src.read()
            else:
                return json.load(src) class CustomPostAdmin(PostAdmin):
    _fieldsets = [
        (None, {"fields": ["title", "subtitle", "slug", "publish", "categories"]}),
        (
            _("Info"),
            {
                "fields": [
                    ["tags"],
                    ["date_published", "date_published_end", "date_featured"],
                    "app_config",
                    "enable_comments",
                ],
                "classes": ("collapse",),
            },
        ),
        (
            _("Images"),
            {"fields": [["main_image", "main_image_thumbnail", "main_image_full"]], "classes": ("collapse",)},
        ),
        (_("SEO"), {"fields": [["meta_description", "meta_title", "meta_keywords"]], "classes": ("collapse",)}),
    ]
    _fieldset_extra_fields_position = {
        "abstract": [0, 1],
        "post_text": [0, 1],
        "sites": [1, 1, 0],
        "author": [1, 1],
        "enable_liveblog": None,
        "related": None,
    }
import re
import parler
            import knocker  # noqa
from contextlib import contextmanager
from copy import deepcopy
from datetime import timedelta
from unittest import SkipTest
from cms.api import add_plugin
from cms.utils.copy_plugins import copy_plugins_to
from cms.utils.plugins import downcast_plugins
from django.contrib import admin
from django.contrib.auth.models import AnonymousUser
from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.sites.models import Site
from django.http import QueryDict
from django.test import override_settings
from django.urls import reverse
from django.utils.encoding import force_str
from django.utils.html import strip_tags
from django.utils.http import urlquote
from django.utils.timezone import now
from django.utils.translation import get_language, override
from filer.models import ThumbnailOption
from menus.menu_pool import menu_pool
from parler.tests.utils import override_parler_settings
from parler.utils.conf import add_default_language_settings
from parler.utils.context import smart_override
from taggit.models import Tag
from djangocms_blog.cms_appconfig import BlogConfig, BlogConfigForm
from djangocms_blog.forms import CategoryAdminForm, PostAdminForm
from djangocms_blog.models import BlogCategory, Post
from djangocms_blog.settings import MENU_TYPE_NONE, PERMALINK_TYPE_CATEGORY, PERMALINK_TYPE_FULL_DATE, get_setting
from .base import BaseTest
from .test_utils.admin import CustomPostAdmin
    from knocker.signals import pause_knocks

        self.app_config_1.app_data.config.url_patterns = PERMALINK_TYPE_CATEGORY
        self.app_config_1.save()

        response = post_admin.change_view(request, str(post.pk))
        self.assertEqual(
            response.context_data["adminform"].form.fields["categories"].required,
            self.app_config_1.url_patterns == PERMALINK_TYPE_CATEGORY,
        )

        self.app_config_1.app_data.config.url_patterns = PERMALINK_TYPE_FULL_DATE
        self.app_config_1.save()

    def test_admin_post_views(self):
        self.get_pages()

        post_admin = admin.site._registry[Post]
        request = self.get_page_request("/", self.user, r"/en/blog/", edit=False)

        post = self._get_post(self._post_data[0]["en"])
        post = self._get_post(self._post_data[0]["it"], post, "it")

        # Add view only contains the apphook selection widget
        response = post_admin.add_view(request)
        self.assertNotContains(response, '<input id="id_slug" maxlength="752" name="slug" type="text"')
        self.assertContains(response, '<option value="%s">Blog / sample_app</option>' % self.app_config_1.pk)

        # Changeview is 'normal'
        response = post_admin.change_view(request, str(post.pk))
        response.render()
        try:
            self.assertRegex(force_str(response.content), r'name="slug"[^>]*value="first-post"')
        except AssertionError:
            self.assertRegex(force_str(response.content), r'value="first-post"[^>]*name="slug"')
        try:
            self.assertRegex(force_str(response.content), r'id="id_meta_description"[^>]*maxlength="320"')
        except AssertionError:
            self.assertRegex(force_str(response.content), r'maxlength="320"[^>]*id="id_meta_description"')
        try:
            self.assertRegex(
                force_str(response.content),
                r'selected[^>]*value="%s">Blog / sample_app</option>' % self.app_config_1.pk,
            )
        except AssertionError:
            self.assertRegex(
                force_str(response.content),
                r'value="%s"[^>]*selected[^>]*>Blog / sample_app</option>' % self.app_config_1.pk,
            )

        # Test for publish view
        post.publish = False
        post.save()
        response = post_admin.publish_post(request, str(post.pk))
        # Redirects to current post
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response["Location"], post.get_absolute_url())
        post = self.reload_model(post)
        # post is publshed
        self.assertTrue(post.publish)

        # Non-existing post is redirected to posts list
        response = post_admin.publish_post(request, "1000000")
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response["Location"], reverse("djangocms_blog:posts-latest"))

        # unless a referer is set
        request.META["HTTP_REFERER"] = "/"
        response = post_admin.publish_post(request, "1000000")
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response["Location"], "/")

    def test_admin_changelist_view(self):
        posts = self.get_posts()
        post_admin = admin.site._registry[Post]
        request = self.get_page_request("/", self.user, r"/en/blog/", edit=False)

        # Normal changelist, all existing posts
        response = post_admin.changelist_view(request)
        self.assertEqual(response.context_data["cl"].queryset.count(), len(posts))
        self.assertTrue(posts[0] in response.context_data["cl"].queryset.all())

        # Site 2 is added to first post, but no changelist filter, no changes
        posts[0].sites.add(self.site_2)
        response = post_admin.changelist_view(request)
        self.assertTrue(posts[0] in response.context_data["cl"].queryset.all())

        # Filtering on site, first post not shown
        request = self.get_page_request("/", self.user, r"/en/blog/?sites=1", edit=False)
        response = post_admin.changelist_view(request)
        self.assertEqual(response.context_data["cl"].queryset.count(), len(posts) - 1)
        self.assertTrue(posts[0] not in response.context_data["cl"].queryset.all())

        # Removing site filtering, first post appears again
        request = self.get_page_request("/", self.user, r"/en/blog/?", edit=False)
        response = post_admin.changelist_view(request)
        self.assertEqual(response.context_data["cl"].queryset.count(), len(posts))
        self.assertTrue(posts[0] in response.context_data["cl"].queryset.all())

        # Filtering on the apphook config and site
        request = self.get_page_request(
            "/", self.user, r"/en/blog/?app_config__id__exact=%s&sites=1" % self.app_config_1.pk, edit=False
        )
        response = post_admin.changelist_view(request)
        # First and last post in the list are now in the queryset
        self.assertEqual(response.context_data["cl"].queryset.count(), len(posts) - 2)
        self.assertTrue(posts[0] not in response.context_data["cl"].queryset.all())
        self.assertTrue(posts[-1] not in response.context_data["cl"].queryset.all())

        # Publishing one post, two published in total
        posts[1].publish = True
        posts[1].save()
        published = Post.objects.published(current_site=False)
        request = self.get_page_request("/", self.user, r"/en/blog/?publish__exact=1", edit=False)
        response = post_admin.changelist_view(request)
        # The admin queryset and the model queryset are the same
        self.assertEqual(response.context_data["cl"].queryset.count(), published.count())
        # Published post is in the changelist queryset
        self.assertTrue(posts[1] in response.context_data["cl"].queryset.all())

    def test_admin_blogconfig_views(self):

        post_admin = admin.site._registry[BlogConfig]