Packages

phoenix_kit

1.7.34
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 web settings.ex
Raw

lib/modules/publishing/web/settings.ex

defmodule PhoenixKit.Modules.Publishing.Web.Settings do
@moduledoc """
Admin configuration for site blogs.
"""
use PhoenixKitWeb, :live_view
use Gettext, backend: PhoenixKitWeb.Gettext
alias PhoenixKit.Modules.Languages
alias PhoenixKit.Modules.Publishing
alias PhoenixKit.Modules.Publishing.ListingCache
alias PhoenixKit.Modules.Publishing.PubSub, as: PublishingPubSub
alias PhoenixKit.Modules.Publishing.Renderer
alias PhoenixKit.Modules.Publishing.Storage
alias PhoenixKit.Settings
alias PhoenixKit.Utils.Routes
# New settings keys (write to these)
@file_cache_key "publishing_file_cache_enabled"
@memory_cache_key "publishing_memory_cache_enabled"
@render_cache_key "publishing_render_cache_enabled"
# Legacy settings keys (read from these as fallback)
@legacy_file_cache_key "blogging_file_cache_enabled"
@legacy_memory_cache_key "blogging_memory_cache_enabled"
@legacy_render_cache_key "blogging_render_cache_enabled"
def mount(_params, _session, socket) do
# Subscribe to group changes for live updates
if connected?(socket) do
PublishingPubSub.subscribe_to_groups()
end
blogs = Publishing.list_groups()
languages_enabled = Languages.enabled?()
# Add legacy status and primary language migration status to each blog
blogs_with_status =
blogs
|> add_legacy_status()
|> add_primary_language_status()
socket =
socket
|> assign(:project_title, Settings.get_project_title())
|> assign(:page_title, gettext("Manage Publishing"))
|> assign(
:current_path,
Routes.path("/admin/settings/publishing")
)
|> assign(:module_enabled, Publishing.enabled?())
|> assign(:blogs, blogs_with_status)
|> assign(:languages_enabled, languages_enabled)
|> assign(:global_primary_language, Storage.get_primary_language())
|> assign(:file_cache_enabled, get_cache_setting(@file_cache_key, @legacy_file_cache_key))
|> assign(
:memory_cache_enabled,
get_cache_setting(@memory_cache_key, @legacy_memory_cache_key)
)
|> assign(
:render_cache_enabled,
get_cache_setting(@render_cache_key, @legacy_render_cache_key)
)
|> assign(:cache_status, build_cache_status(blogs))
|> assign(:render_cache_stats, get_render_cache_stats())
|> assign(:render_cache_per_blog, build_render_cache_per_blog(blogs))
{:ok, socket}
end
def handle_params(_params, _uri, socket), do: {:noreply, socket}
def handle_event("remove_group", %{"slug" => slug}, socket) do
case Publishing.trash_group(slug) do
{:ok, trashed_name} ->
# The `Publishing.trash_group` call triggers `remove_group`, which handles
# the broadcast. This LiveView will catch the event and update its state.
{:noreply,
put_flash(
socket,
:info,
gettext("Blog moved to trash as: %{name}", name: trashed_name)
)}
{:error, :not_found} ->
# Blog directory doesn't exist, just remove from config
case Publishing.remove_group(slug) do
{:ok, _} ->
# The `Publishing.remove_group` call handles the broadcast. This
# LiveView will catch the event and update its state.
{:noreply, put_flash(socket, :info, gettext("Blog removed from configuration"))}
{:error, _reason} ->
{:noreply, put_flash(socket, :error, gettext("Failed to remove blog"))}
end
{:error, _reason} ->
{:noreply, put_flash(socket, :error, gettext("Failed to move blog to trash"))}
end
end
def handle_event("regenerate_cache", %{"slug" => slug}, socket) do
case ListingCache.regenerate(slug) do
:ok ->
{:noreply,
socket
|> assign(:cache_status, build_cache_status(socket.assigns.blogs))
|> put_flash(:info, gettext("Cache regenerated for %{blog}", blog: slug))}
{:error, _reason} ->
{:noreply, put_flash(socket, :error, gettext("Failed to regenerate cache"))}
end
end
def handle_event("invalidate_cache", %{"slug" => slug}, socket) do
ListingCache.invalidate(slug)
{:noreply,
socket
|> assign(:cache_status, build_cache_status(socket.assigns.blogs))
|> put_flash(:info, gettext("Cache cleared for %{blog}", blog: slug))}
end
def handle_event("migrate_storage", %{"slug" => slug}, socket) do
case Publishing.migrate_group(slug) do
{:ok, _new_path} ->
# Refresh blogs list with updated legacy status
blogs =
Publishing.list_groups()
|> add_legacy_status()
|> add_primary_language_status()
{:noreply,
socket
|> assign(:blogs, blogs)
|> assign(:cache_status, build_cache_status(blogs))
|> put_flash(:info, gettext("Storage migrated for %{blog}", blog: slug))}
{:error, :already_migrated} ->
{:noreply,
put_flash(socket, :info, gettext("Storage already migrated for %{blog}", blog: slug))}
{:error, :not_found} ->
{:noreply, put_flash(socket, :error, gettext("Blog not found: %{blog}", blog: slug))}
{:error, reason} ->
{:noreply,
put_flash(
socket,
:error,
gettext("Failed to migrate storage: %{reason}", reason: inspect(reason))
)}
end
end
def handle_event("migrate_primary_language", %{"slug" => slug}, socket) do
case Publishing.migrate_posts_to_current_primary_language(slug) do
{:ok, count} ->
# Refresh blogs list with updated status
blogs =
Publishing.list_groups()
|> add_legacy_status()
|> add_primary_language_status()
{:noreply,
socket
|> assign(:blogs, blogs)
|> assign(:cache_status, build_cache_status(blogs))
|> put_flash(
:info,
gettext("Updated %{count} posts to use primary language: %{lang}",
count: count,
lang: socket.assigns.global_primary_language
)
)}
{:error, reason} ->
{:noreply,
put_flash(
socket,
:error,
gettext("Failed to migrate primary language: %{reason}", reason: inspect(reason))
)}
end
end
def handle_event("regenerate_all_caches", _params, socket) do
results =
Enum.map(socket.assigns.blogs, fn blog ->
{blog["slug"], ListingCache.regenerate(blog["slug"])}
end)
success_count = Enum.count(results, fn {_, result} -> result == :ok end)
{:noreply,
socket
|> assign(:cache_status, build_cache_status(socket.assigns.blogs))
|> put_flash(:info, gettext("Regenerated %{count} caches", count: success_count))}
end
def handle_event("toggle_file_cache", _params, socket) do
new_value = !socket.assigns.file_cache_enabled
Settings.update_setting(@file_cache_key, to_string(new_value))
{:noreply,
socket
|> assign(:file_cache_enabled, new_value)
|> put_flash(:info, cache_toggle_message("File cache", new_value))}
end
def handle_event("toggle_memory_cache", _params, socket) do
new_value = !socket.assigns.memory_cache_enabled
Settings.update_setting(@memory_cache_key, to_string(new_value))
# If disabling memory cache, clear all :persistent_term entries
if !new_value do
Enum.each(socket.assigns.blogs, fn blog ->
try do
:persistent_term.erase(ListingCache.persistent_term_key(blog["slug"]))
rescue
ArgumentError -> :ok
end
end)
end
{:noreply,
socket
|> assign(:memory_cache_enabled, new_value)
|> assign(:cache_status, build_cache_status(socket.assigns.blogs))
|> put_flash(:info, cache_toggle_message("Memory cache", new_value))}
end
def handle_event("clear_render_cache", _params, socket) do
Renderer.clear_all_cache()
{:noreply,
socket
|> assign(:render_cache_stats, get_render_cache_stats())
|> put_flash(:info, gettext("Render cache cleared"))}
end
def handle_event("clear_blog_render_cache", %{"slug" => slug}, socket) do
case Renderer.clear_group_cache(slug) do
{:ok, count} ->
{:noreply,
socket
|> assign(:render_cache_stats, get_render_cache_stats())
|> put_flash(
:info,
gettext("Cleared %{count} cached posts for %{blog}", count: count, blog: slug)
)}
{:error, _} ->
{:noreply, put_flash(socket, :error, gettext("Failed to clear cache"))}
end
end
def handle_event("toggle_render_cache", _params, socket) do
new_value = !socket.assigns.render_cache_enabled
Settings.update_setting(@render_cache_key, to_string(new_value))
{:noreply,
socket
|> assign(:render_cache_enabled, new_value)
|> put_flash(:info, cache_toggle_message("Render cache", new_value))}
end
def handle_event("toggle_blog_render_cache", %{"slug" => slug}, socket) do
# Use Renderer helper to get the new key for writes
per_blog_key = Renderer.per_group_cache_key(slug)
current_value = Renderer.group_render_cache_enabled?(slug)
new_value = !current_value
Settings.update_setting(per_blog_key, to_string(new_value))
{:noreply,
socket
|> assign(:render_cache_per_blog, build_render_cache_per_blog(socket.assigns.blogs))
|> put_flash(:info, cache_toggle_message("Render cache for #{slug}", new_value))}
end
# ============================================================================
# PubSub Handlers - Live updates when groups change elsewhere
# ============================================================================
def handle_info({:group_created, _group}, socket) do
{:noreply, refresh_blogs(socket)}
end
def handle_info({:group_deleted, _slug}, socket) do
{:noreply, refresh_blogs(socket)}
end
def handle_info({:group_updated, _group}, socket) do
{:noreply, refresh_blogs(socket)}
end
defp refresh_blogs(socket) do
blogs =
Publishing.list_groups()
|> add_legacy_status()
|> add_primary_language_status()
socket
|> assign(:blogs, blogs)
|> assign(:cache_status, build_cache_status(blogs))
|> assign(:render_cache_per_blog, build_render_cache_per_blog(blogs))
end
# Helper for dual-key cache setting reads
defp get_cache_setting(new_key, legacy_key) do
case Settings.get_setting(new_key, nil) do
nil -> Settings.get_setting(legacy_key, "true") == "true"
value -> value == "true"
end
end
defp cache_toggle_message(cache_type, enabled) do
if enabled do
gettext("%{type} enabled", type: cache_type)
else
gettext("%{type} disabled", type: cache_type)
end
end
# Build cache status for all blogs
defp build_cache_status(blogs) do
Map.new(blogs, fn blog ->
slug = blog["slug"]
{slug, get_cache_info(slug)}
end)
end
defp get_cache_info(blog_slug) do
cache_path = ListingCache.cache_path(blog_slug)
case File.stat(cache_path) do
{:ok, stat} ->
# Read cache to get post count
post_count =
case File.read(cache_path) do
{:ok, content} ->
case Jason.decode(content) do
{:ok, %{"post_count" => count}} -> count
_ -> nil
end
_ ->
nil
end
# Check if in :persistent_term
in_memory =
case :persistent_term.get(ListingCache.persistent_term_key(blog_slug), :not_found) do
:not_found -> false
_ -> true
end
%{
exists: true,
file_size: stat.size,
modified_at: stat.mtime,
post_count: post_count,
in_memory: in_memory
}
{:error, :enoent} ->
%{exists: false, file_size: 0, modified_at: nil, post_count: nil, in_memory: false}
end
end
defp get_render_cache_stats do
PhoenixKit.Cache.stats(:publishing_posts)
rescue
_ -> %{hits: 0, misses: 0, puts: 0, invalidations: 0, hit_rate: 0.0}
end
defp build_render_cache_per_blog(blogs) do
Map.new(blogs, fn blog ->
slug = blog["slug"]
{slug, Renderer.group_render_cache_enabled?(slug)}
end)
end
# Add legacy storage status to each blog
defp add_legacy_status(blogs) do
Enum.map(blogs, fn blog ->
slug = blog["slug"]
Map.put(blog, "is_legacy", Publishing.legacy_group?(slug))
end)
end
# Add primary language migration status to each blog
defp add_primary_language_status(blogs) do
Enum.map(blogs, fn blog ->
slug = blog["slug"]
status = Publishing.get_primary_language_migration_status(slug)
needs_migration = status.needs_backfill > 0 or status.needs_migration > 0
blog
|> Map.put("primary_language_status", status)
|> Map.put("needs_primary_lang_migration", needs_migration)
end)
end
end