Packages

phoenix_kit

1.7.74
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules publishing publishing.ex
Raw

lib/modules/publishing/publishing.ex

defmodule PhoenixKit.Modules.Publishing do
@moduledoc """
Publishing module for managing content groups and their posts.
Database-backed CMS for creating timestamped or slug-based posts
with multi-language support and versioning.
This module acts as a facade, delegating to focused submodules:
- `Publishing.Groups` — Group CRUD
- `Publishing.Posts` — Post CRUD, reading, and listing
- `Publishing.Versions` — Version create, publish, delete
- `Publishing.TranslationManager` — Language/translation management
- `Publishing.StaleFixer` — Stale value detection and repair
"""
use PhoenixKit.Module
require Logger
alias PhoenixKit.Dashboard.Tab
alias PhoenixKit.Modules.Languages
alias PhoenixKit.Modules.Publishing.DBStorage
alias PhoenixKit.Modules.Publishing.LanguageHelpers
alias PhoenixKit.Modules.Publishing.SlugHelpers
# ============================================================================
# Language Utility Delegates
# ============================================================================
defdelegate get_language_info(language_code), to: LanguageHelpers
defdelegate enabled_language_codes(), to: LanguageHelpers
defdelegate get_primary_language(), to: LanguageHelpers
defdelegate language_enabled?(language_code, enabled_languages), to: LanguageHelpers
defdelegate get_display_code(language_code, enabled_languages), to: LanguageHelpers
defdelegate order_languages_for_display(available_languages, enabled_languages),
to: LanguageHelpers
defdelegate order_languages_for_display(available_languages, enabled_languages, primary),
to: LanguageHelpers
# ============================================================================
# Slug Utility Delegates
# ============================================================================
defdelegate validate_slug(slug), to: SlugHelpers
defdelegate slug_exists?(group_slug, post_slug), to: SlugHelpers
defdelegate generate_unique_slug(group_slug, title), to: SlugHelpers
defdelegate generate_unique_slug(group_slug, title, preferred_slug), to: SlugHelpers
defdelegate generate_unique_slug(group_slug, title, preferred_slug, opts), to: SlugHelpers
defdelegate validate_url_slug(group_slug, url_slug, language, exclude), to: SlugHelpers
defdelegate clear_url_slug_from_post(group_slug, post_slug, url_slug), to: DBStorage
# ============================================================================
# Cache Delegates
# ============================================================================
alias PhoenixKit.Modules.Publishing.ListingCache
defdelegate regenerate_cache(group_slug), to: ListingCache, as: :regenerate
defdelegate invalidate_cache(group_slug), to: ListingCache, as: :invalidate
defdelegate cache_exists?(group_slug), to: ListingCache, as: :exists?
defdelegate find_cached_post(group_slug, post_slug), to: ListingCache, as: :find_post
defdelegate find_cached_post_by_path(group_slug, date, time),
to: ListingCache,
as: :find_post_by_path
# ============================================================================
# Group Delegates
# ============================================================================
alias PhoenixKit.Modules.Publishing.Groups
defdelegate list_groups(), to: Groups
defdelegate list_groups(status), to: Groups
defdelegate get_group(slug), to: Groups
defdelegate add_group(name, opts \\ []), to: Groups
defdelegate remove_group(slug), to: Groups
defdelegate remove_group(slug, opts), to: Groups
defdelegate update_group(slug, params), to: Groups
defdelegate trash_group(slug), to: Groups
defdelegate group_name(slug), to: Groups
defdelegate get_group_mode(group_slug), to: Groups
defdelegate preset_types(), to: Groups
defdelegate valid_types(), to: Groups
defdelegate restore_group(slug), to: Groups
defdelegate list_trashed_groups(), to: Groups
# ============================================================================
# Post Delegates
# ============================================================================
alias PhoenixKit.Modules.Publishing.Posts
defdelegate list_posts(group_slug, preferred_language \\ nil), to: Posts
defdelegate list_posts_by_status(group_slug, status), to: Posts
defdelegate list_raw_posts(group_slug, status \\ nil), to: Posts
defdelegate count_primary_language_status(posts, primary_language), to: Posts
defdelegate create_post(group_slug, opts \\ %{}), to: Posts
defdelegate read_post(group_slug, identifier, language \\ nil, version \\ nil), to: Posts
defdelegate read_post_by_uuid(post_uuid, language \\ nil, version \\ nil), to: Posts
defdelegate update_post(group_slug, post, params, opts \\ %{}), to: Posts
defdelegate change_post_status(group_slug, post_uuid, new_status, opts \\ []), to: Posts
defdelegate trash_post(group_slug, post_uuid), to: Posts
defdelegate restore_post(group_slug, post_uuid), to: Posts
defdelegate count_posts_on_date(group_slug, date), to: Posts
defdelegate list_times_on_date(group_slug, date), to: Posts
defdelegate read_post_by_datetime(group_slug, date, time), to: DBStorage
defdelegate find_by_url_slug(group_slug, language, url_slug), to: Posts
defdelegate find_by_previous_url_slug(group_slug, language, url_slug), to: Posts
defdelegate extract_slug_version_and_language(group_slug, identifier), to: Posts
@doc "Always returns false — auto-versioning is disabled."
def should_create_new_version?(_post, _params, _editing_language), do: false
@doc "Returns true when the given post is a DB-backed post (has a UUID)."
@spec db_post?(map()) :: boolean()
defdelegate db_post?(post), to: Posts
# ============================================================================
# Version Delegates
# ============================================================================
alias PhoenixKit.Modules.Publishing.Versions
defdelegate list_versions(group_slug, post_slug), to: Versions
defdelegate get_published_version(group_slug, post_slug), to: Versions
defdelegate get_version_status(group_slug, post_slug, version_number, language), to: Versions
defdelegate get_version_metadata(group_slug, post_slug, version_number, language), to: Versions
defdelegate create_new_version(group_slug, source_post, params \\ %{}, opts \\ %{}),
to: Versions
defdelegate publish_version(group_slug, post_uuid, version, opts \\ []), to: Versions
defdelegate create_version_from(
group_slug,
post_uuid,
source_version,
params \\ %{},
opts \\ %{}
),
to: Versions
defdelegate delete_version(group_slug, post_uuid, version), to: Versions
@doc false
defdelegate broadcast_version_created(group_slug, broadcast_id, new_version), to: Versions
# ============================================================================
# Translation Delegates
# ============================================================================
alias PhoenixKit.Modules.Publishing.TranslationManager
defdelegate get_post_primary_language(group_slug, post_slug, version \\ nil),
to: TranslationManager
defdelegate check_primary_language_status(group_slug, post_slug), to: TranslationManager
defdelegate update_post_primary_language(group_slug, post_uuid, new_primary_language),
to: TranslationManager
defdelegate update_posts_primary_language(group_slug), to: TranslationManager
defdelegate count_posts_needing_language_update(group_slug), to: TranslationManager
defdelegate add_language_to_post(group_slug, post_uuid, language_code, version \\ nil),
to: TranslationManager
@doc false
defdelegate add_language_to_db(group_slug, post_uuid, language_code, version_number),
to: TranslationManager
defdelegate delete_language(group_slug, post_uuid, language_code, version \\ nil),
to: TranslationManager
defdelegate clear_translation(group_slug, post_uuid, language_code), to: TranslationManager
defdelegate set_translation_status(group_slug, post_identifier, version, language, status),
to: TranslationManager
defdelegate translate_post_to_all_languages(group_slug, post_uuid, opts \\ []),
to: TranslationManager
# ============================================================================
# Stale Value Correction Delegates
# ============================================================================
alias PhoenixKit.Modules.Publishing.StaleFixer
defdelegate fix_stale_group(group), to: StaleFixer
defdelegate fix_stale_post(post), to: StaleFixer
defdelegate fix_stale_version(version), to: StaleFixer
defdelegate fix_stale_content(content), to: StaleFixer
defdelegate fix_all_stale_values(), to: StaleFixer
defdelegate reconcile_post_status(post), to: StaleFixer
# ============================================================================
# Module Behaviour Callbacks
# ============================================================================
@publishing_enabled_key "publishing_enabled"
@impl PhoenixKit.Module
@spec enabled?() :: boolean()
def enabled? do
settings_call(:get_boolean_setting, [@publishing_enabled_key, false])
end
@impl PhoenixKit.Module
@spec enable_system() :: {:ok, any()} | {:error, any()}
def enable_system do
settings_call(:update_boolean_setting, [@publishing_enabled_key, true])
end
@impl PhoenixKit.Module
@spec disable_system() :: {:ok, any()} | {:error, any()}
def disable_system do
settings_call(:update_boolean_setting, [@publishing_enabled_key, false])
end
@impl PhoenixKit.Module
def module_key, do: "publishing"
@impl PhoenixKit.Module
def module_name, do: "Publishing"
@impl PhoenixKit.Module
def get_config do
%{
enabled: enabled?(),
groups_count: length(list_groups())
}
end
@impl PhoenixKit.Module
def permission_metadata do
%{
key: "publishing",
label: "Publishing",
icon: "hero-document-duplicate",
description: "Database-backed CMS pages and multi-language content"
}
end
@impl PhoenixKit.Module
def admin_tabs do
[
Tab.new!(
id: :admin_publishing,
label: "Publishing",
icon: "hero-document-text",
path: "publishing",
priority: 600,
level: :admin,
permission: "publishing",
match: :prefix,
group: :admin_modules,
subtab_display: :when_active,
highlight_with_subtabs: false,
dynamic_children: &__MODULE__.publishing_children/1
)
]
end
@doc "Dynamic children function for Publishing sidebar tabs."
def publishing_children(_scope) do
groups = load_publishing_groups_for_tabs()
groups
|> Enum.with_index()
|> Enum.map(fn {group, idx} ->
slug = group["slug"] || ""
name = group["name"] || slug
hash = :erlang.phash2(slug) |> Integer.to_string(16) |> String.downcase()
sanitized = slug |> String.replace(~r/[^a-zA-Z0-9_]/, "_") |> String.slice(0, 50)
%Tab{
id: :"admin_publishing_#{sanitized}_#{hash}",
label: name,
icon: "hero-document-text",
path: "publishing/#{slug}",
priority: 601 + idx,
level: :admin,
permission: "publishing",
match: :prefix,
parent: :admin_publishing
}
end)
rescue
e ->
Logger.warning("[Publishing] dashboard_tabs failed: #{inspect(e)}")
[]
end
defp load_publishing_groups_for_tabs do
alias PhoenixKit.Settings
publishing_enabled = Settings.get_boolean_setting("publishing_enabled", false)
if publishing_enabled do
alias PhoenixKit.Modules.Publishing.DBStorage
DBStorage.list_groups()
|> Enum.map(fn g -> %{"name" => g.name, "slug" => g.slug} end)
else
[]
end
rescue
e ->
Logger.warning("[Publishing] load_publishing_groups_for_tabs failed: #{inspect(e)}")
[]
end
@impl PhoenixKit.Module
def settings_tabs do
[
Tab.new!(
id: :admin_settings_publishing,
label: "Publishing",
icon: "hero-document-text",
path: "publishing",
priority: 921,
level: :admin,
parent: :admin_settings,
permission: "publishing"
)
]
end
@impl PhoenixKit.Module
def children, do: [PhoenixKit.Modules.Publishing.Presence]
@impl PhoenixKit.Module
def route_module, do: PhoenixKitWeb.Routes.PublishingRoutes
# ============================================================================
# Shared Helpers (used across submodules)
# ============================================================================
alias PhoenixKit.Modules.Publishing.Shared
@slug_regex ~r/^[a-z0-9]+(?:-[a-z0-9]+)*$/
@doc false
def slugify(name) when is_binary(name) do
name
|> String.downcase()
|> String.replace(~r/[^a-z0-9]+/u, "-")
|> String.trim("-")
end
@doc """
Returns true when the slug matches the allowed lowercase letters, numbers, and hyphen pattern,
and is not a reserved language code.
Group slugs cannot be language codes (like 'en', 'es', 'fr') to prevent routing ambiguity.
"""
@spec valid_slug?(String.t()) :: boolean()
def valid_slug?(slug) when is_binary(slug) do
slug != "" and Regex.match?(@slug_regex, slug) and not reserved_language_code?(slug)
end
def valid_slug?(_), do: false
defp reserved_language_code?(slug) do
language_codes =
try do
Languages.get_language_codes()
rescue
e ->
Logger.debug(
"[Publishing] reserved_language_code? check failed, assuming no reserved codes: #{inspect(e)}"
)
[]
end
slug in language_codes
end
@doc false
defdelegate fetch_option(opts, key), to: Shared
@doc false
defdelegate audit_metadata(scope, action), to: Shared
# ============================================================================
# Settings Helpers (private)
# ============================================================================
defp settings_module do
case PhoenixKit.Config.get(:publishing_settings_module) do
:not_found -> PhoenixKit.Settings
{:ok, module} -> module
end
end
defp settings_call(fun, args) do
module = settings_module()
case fun do
:get_json_setting_cached ->
if function_exported?(module, :get_json_setting_cached, length(args)) do
apply(module, :get_json_setting_cached, args)
else
apply(module, :get_json_setting, args)
end
_ ->
apply(module, fun, args)
end
end
end