Packages
phoenix_kit
1.7.77
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
Current section
Files
lib/modules/publishing/db_storage/mapper.ex
defmodule PhoenixKit.Modules.Publishing.DBStorage.Mapper do
@moduledoc """
Mapper: converts DB records to the map format expected by
Publishing's web layer (LiveViews, templates, controllers).
## Map Shape
The web layer expects maps with these keys:
- `:group` - group slug
- `:slug` - post slug identifier
- `:url_slug` - per-language URL slug
- `:date` - Date struct (timestamp mode)
- `:time` - Time struct (timestamp mode)
- `:mode` - :timestamp or :slug atom
- `:language` - current language code
- `:available_languages` - list of language codes
- `:language_statuses` - %{language => status}
- `:version` - current version number
- `:available_versions` - list of version numbers
- `:version_statuses` - %{version_number => status}
- `:version_dates` - %{version_number => date_string}
- `:content` - markdown/PHK body
- `:metadata` - map with :title, :description, :status, :slug, etc.
- `:primary_language` - primary language code
"""
alias PhoenixKit.Modules.Publishing.PublishingContent
alias PhoenixKit.Modules.Publishing.PublishingPost
alias PhoenixKit.Modules.Publishing.PublishingVersion
@doc """
Converts a full post read (post + version + content + all contents + all versions)
into the map format expected by the web layer.
"""
def to_post_map(
%PublishingPost{} = post,
%PublishingVersion{} = version,
%PublishingContent{} = content,
all_contents,
all_versions,
opts \\ []
) do
available_languages = Enum.map(all_contents, & &1.language) |> Enum.sort()
language_statuses =
Map.new(all_contents, fn c -> {c.language, c.status} end)
|> merge_published_statuses(Keyword.get(opts, :published_language_statuses, %{}))
available_versions = Enum.map(all_versions, & &1.version_number) |> Enum.sort()
version_statuses =
Map.new(all_versions, fn v -> {v.version_number, v.status} end)
version_dates =
Map.new(all_versions, fn v ->
{v.version_number, format_datetime(v.inserted_at)}
end)
group_slug = get_group_slug(post)
%{
uuid: post.uuid,
group: group_slug,
slug: post.slug,
url_slug: presence(content.url_slug) || post.slug,
date: post.post_date,
time: post.post_time,
mode: safe_mode_atom(post.mode),
language: content.language,
available_languages: available_languages,
language_statuses: language_statuses,
language_slugs: build_language_slugs(all_contents, post.slug),
language_previous_slugs: build_language_previous_slugs(all_contents),
version: version.version_number,
available_versions: available_versions,
version_statuses: version_statuses,
version_dates: version_dates,
content: content.content,
metadata: build_metadata(post, version, content),
primary_language: post.primary_language
}
end
@doc """
Converts a post to a listing-format map (no content body, just metadata).
Used for listing pages where full content isn't needed.
"""
def to_listing_map(%PublishingPost{} = post, version, all_contents, all_versions, opts \\ []) do
available_languages = Enum.map(all_contents, & &1.language) |> Enum.sort()
language_statuses =
Map.new(all_contents, fn c -> {c.language, c.status} end)
|> merge_published_statuses(Keyword.get(opts, :published_language_statuses, %{}))
available_versions = Enum.map(all_versions, & &1.version_number) |> Enum.sort()
version_statuses =
Map.new(all_versions, fn v -> {v.version_number, v.status} end)
version_dates =
Map.new(all_versions, fn v ->
{v.version_number, format_datetime(v.inserted_at)}
end)
primary_content =
Enum.find(all_contents, fn c -> c.language == post.primary_language end) ||
List.first(all_contents)
group_slug = get_group_slug(post)
current_version = if version, do: version.version_number, else: 1
%{
uuid: post.uuid,
group: group_slug,
slug: post.slug,
url_slug: presence(primary_content && primary_content.url_slug) || post.slug,
date: post.post_date,
time: post.post_time,
mode: safe_mode_atom(post.mode),
language: post.primary_language,
available_languages: available_languages,
language_statuses: language_statuses,
language_slugs: build_language_slugs(all_contents, post.slug),
language_previous_slugs: build_language_previous_slugs(all_contents),
version: current_version,
available_versions: available_versions,
version_statuses: version_statuses,
version_dates: version_dates,
content: primary_content && extract_excerpt(primary_content),
metadata: build_listing_metadata(post, primary_content),
primary_language: post.primary_language,
# Per-language data for listing pages (so language switching shows correct titles)
language_titles: Map.new(all_contents, fn c -> {c.language, c.title} end),
language_excerpts: Map.new(all_contents, fn c -> {c.language, extract_excerpt(c)} end)
}
end
# ===========================================================================
# Private Helpers
# ===========================================================================
defp get_group_slug(%PublishingPost{group: %{slug: slug}}), do: slug
defp get_group_slug(%PublishingPost{} = _post), do: nil
defp build_metadata(post, version, content) do
%{
title: content.title,
description: PublishingContent.get_description(content),
status: content.status,
slug: post.slug,
version: version.version_number,
allow_version_access: PublishingPost.allow_version_access?(post),
url_slug: content.url_slug,
previous_url_slugs: PublishingContent.get_previous_url_slugs(content),
published_at: format_datetime(post.published_at),
featured_image_uuid: PublishingContent.get_featured_image_uuid(content),
primary_language: post.primary_language
}
end
defp build_listing_metadata(post, nil) do
%{
title: nil,
description: nil,
status: post.status,
slug: post.slug,
published_at: format_datetime(post.published_at),
featured_image_uuid: nil,
primary_language: post.primary_language
}
end
defp build_listing_metadata(post, content) do
%{
title: content.title,
description: PublishingContent.get_description(content),
status: content.status,
slug: post.slug,
published_at: format_datetime(post.published_at),
featured_image_uuid: PublishingContent.get_featured_image_uuid(content),
primary_language: post.primary_language
}
end
defp build_language_slugs(all_contents, default_slug) do
Map.new(all_contents, fn c ->
{c.language, presence(c.url_slug) || default_slug}
end)
end
defp build_language_previous_slugs(all_contents) do
Map.new(all_contents, fn c ->
{c.language, PublishingContent.get_previous_url_slugs(c)}
end)
end
defp extract_excerpt(%PublishingContent{} = content) do
# Use custom excerpt from data, or description, or first paragraph
case PublishingContent.get_excerpt(content) do
excerpt when is_binary(excerpt) and excerpt != "" ->
excerpt
_ ->
case PublishingContent.get_description(content) do
desc when is_binary(desc) and desc != "" ->
desc
_ ->
extract_first_paragraph(content.content)
end
end
end
defp extract_first_paragraph(nil), do: nil
defp extract_first_paragraph(content) when is_binary(content) do
content
|> String.split(~r/\n\n+/)
|> Enum.reject(&String.starts_with?(&1, "#"))
|> List.first()
|> case do
nil -> ""
text -> text |> String.trim() |> String.slice(0, 300)
end
end
defp format_datetime(nil), do: nil
defp format_datetime(%DateTime{} = dt), do: DateTime.to_iso8601(dt)
defp format_datetime(other), do: to_string(other)
# Merges published version's language statuses into the latest version's statuses.
# For each language, if the published version has it as "published", override the
# latest version's status. This ensures the listing page shows "published" when
# a language is live on an older version even if the latest draft doesn't have it published.
defp merge_published_statuses(latest_statuses, published_statuses)
when map_size(published_statuses) == 0,
do: latest_statuses
defp merge_published_statuses(latest_statuses, published_statuses) do
Map.merge(latest_statuses, published_statuses, fn _lang, latest, published ->
if published == "published", do: "published", else: latest
end)
end
defp safe_mode_atom("timestamp"), do: :timestamp
defp safe_mode_atom("slug"), do: :slug
defp safe_mode_atom(_), do: :timestamp
# Returns nil for nil and empty string, otherwise the value
defp presence(nil), do: nil
defp presence(""), do: nil
defp presence(value), do: value
end