Packages

phoenix_kit

1.7.69
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 posts schemas post.ex
Raw

lib/modules/posts/schemas/post.ex

defmodule PhoenixKit.Modules.Posts.Post do
@moduledoc """
Schema for user posts with media attachments.
Represents a social post with type-specific layouts, privacy controls,
and scheduled publishing support.
## Post Types
- **post** - Standard full post with full content and media gallery
- **snippet** - Short form post with truncated display
- **repost** - Share of external content with source attribution
## Status Flow
- `draft` - Post is being edited (not visible to others)
- `public` - Post is published and visible to all
- `unlisted` - Post is accessible via direct link but not in feeds
- `scheduled` - Post will be auto-published at scheduled_at time
## Fields
- `user_uuid` - Owner of the post
- `title` - Post title (max length via settings)
- `sub_title` - Subtitle/tagline (max length via settings)
- `content` - Post content (max length via settings)
- `type` - post/snippet/repost (affects display layout)
- `status` - draft/public/unlisted/scheduled
- `scheduled_at` - When to auto-publish (nullable)
- `published_at` - When made public (nullable)
- `repost_url` - Source URL for reposts (nullable)
- `slug` - SEO-friendly URL slug
- `like_count` - Denormalized counter (updated via context)
- `comment_count` - Denormalized counter (updated via context)
- `view_count` - Page views counter (updated via context)
- `metadata` - Type-specific flexible data (JSONB)
## Examples
# Standard post
%Post{
id: "018e3c4a-9f6b-7890-abcd-ef1234567890",
user_uuid: "018e3c4a-1234-5678-abcd-ef1234567890",
title: "My First Post",
sub_title: "An introduction to my journey",
content: "This is the full content...",
type: "post",
status: "public",
slug: "my-first-post",
like_count: 42,
comment_count: 15,
view_count: 523,
published_at: ~U[2025-01-01 12:00:00Z]
}
# Scheduled post
%Post{
title: "Future Announcement",
content: "...",
type: "post",
status: "scheduled",
scheduled_at: ~U[2025-12-31 09:00:00Z],
published_at: nil
}
# Repost
%Post{
title: "Great Article",
content: "Check this out!",
type: "repost",
status: "public",
repost_url: "https://example.com/article",
metadata: %{"original_author" => "Jane Doe"}
}
"""
use Ecto.Schema
import Ecto.Changeset
alias PhoenixKit.Utils.Date, as: UtilsDate
@primary_key {:uuid, UUIDv7, autogenerate: true}
@foreign_key_type UUIDv7
@type t :: %__MODULE__{
uuid: UUIDv7.t() | nil,
user_uuid: UUIDv7.t() | nil,
title: String.t(),
sub_title: String.t() | nil,
content: String.t(),
type: String.t(),
status: String.t(),
scheduled_at: DateTime.t() | nil,
published_at: DateTime.t() | nil,
repost_url: String.t() | nil,
slug: String.t(),
like_count: integer(),
dislike_count: integer(),
comment_count: integer(),
view_count: integer(),
metadata: map(),
user: PhoenixKit.Users.Auth.User.t() | Ecto.Association.NotLoaded.t(),
media: [PhoenixKit.Modules.Posts.PostMedia.t()] | Ecto.Association.NotLoaded.t(),
likes: [PhoenixKit.Modules.Posts.PostLike.t()] | Ecto.Association.NotLoaded.t(),
dislikes: [PhoenixKit.Modules.Posts.PostDislike.t()] | Ecto.Association.NotLoaded.t(),
comments: [PhoenixKit.Modules.Posts.PostComment.t()] | Ecto.Association.NotLoaded.t(),
mentions: [PhoenixKit.Modules.Posts.PostMention.t()] | Ecto.Association.NotLoaded.t(),
tags: [PhoenixKit.Modules.Posts.PostTag.t()] | Ecto.Association.NotLoaded.t(),
groups: [PhoenixKit.Modules.Posts.PostGroup.t()] | Ecto.Association.NotLoaded.t(),
inserted_at: DateTime.t() | nil,
updated_at: DateTime.t() | nil
}
schema "phoenix_kit_posts" do
field :title, :string
field :sub_title, :string
field :content, :string
field :type, :string, default: "post"
field :status, :string, default: "draft"
field :scheduled_at, :utc_datetime
field :published_at, :utc_datetime
field :repost_url, :string
field :slug, :string
field :like_count, :integer, default: 0
field :dislike_count, :integer, default: 0
field :comment_count, :integer, default: 0
field :view_count, :integer, default: 0
field :metadata, :map, default: %{}
belongs_to :user, PhoenixKit.Users.Auth.User,
foreign_key: :user_uuid,
references: :uuid,
type: UUIDv7
has_many :media, PhoenixKit.Modules.Posts.PostMedia, foreign_key: :post_uuid
has_many :likes, PhoenixKit.Modules.Posts.PostLike, foreign_key: :post_uuid
has_many :dislikes, PhoenixKit.Modules.Posts.PostDislike, foreign_key: :post_uuid
has_many :comments, PhoenixKit.Modules.Posts.PostComment, foreign_key: :post_uuid
has_many :mentions, PhoenixKit.Modules.Posts.PostMention, foreign_key: :post_uuid
many_to_many :tags, PhoenixKit.Modules.Posts.PostTag,
join_through: PhoenixKit.Modules.Posts.PostTagAssignment,
join_keys: [post_uuid: :uuid, tag_uuid: :uuid]
many_to_many :groups, PhoenixKit.Modules.Posts.PostGroup,
join_through: PhoenixKit.Modules.Posts.PostGroupAssignment,
join_keys: [post_uuid: :uuid, group_uuid: :uuid]
timestamps(type: :utc_datetime)
end
@doc """
Changeset for creating or updating a post.
## Required Fields
- `user_uuid` - Owner of the post
- `title` - Post title
- `content` - Post content
- `type` - Must be: "post", "snippet", or "repost"
- `status` - Must be: "draft", "public", "unlisted", or "scheduled"
## Validation Rules
- Title and content lengths validated against settings
- Type must be valid (post/snippet/repost)
- Status must be valid (draft/public/unlisted/scheduled)
- Slug auto-generated from title if not provided
- Scheduled posts must have scheduled_at
- Reposts should have repost_url
"""
def changeset(post, attrs) do
post
|> cast(attrs, [
:user_uuid,
:title,
:sub_title,
:content,
:type,
:status,
:scheduled_at,
:published_at,
:repost_url,
:slug,
:metadata
])
|> validate_required([:user_uuid, :title, :type, :status])
|> validate_inclusion(:type, ["post", "snippet", "repost"])
|> validate_inclusion(:status, ["draft", "public", "unlisted", "scheduled"])
|> validate_length(:title, max: 255)
|> validate_length(:sub_title, max: 500)
|> validate_scheduled_at()
|> maybe_generate_slug()
|> foreign_key_constraint(:user_uuid)
end
@doc """
Check if post is published (public or unlisted).
"""
def published?(%__MODULE__{status: status}) when status in ["public", "unlisted"], do: true
def published?(_), do: false
@doc """
Check if post is scheduled for future publishing.
"""
def scheduled?(%__MODULE__{status: "scheduled"}), do: true
def scheduled?(_), do: false
@doc """
Check if post can receive comments (public or unlisted).
"""
def can_comment?(%__MODULE__{status: status}) when status in ["public", "unlisted"], do: true
def can_comment?(_), do: false
@doc """
Check if post is a draft.
"""
def draft?(%__MODULE__{status: "draft"}), do: true
def draft?(_), do: false
@doc """
Check if post is a repost type.
"""
def repost?(%__MODULE__{type: "repost"}), do: true
def repost?(_), do: false
# Private Functions
defp validate_scheduled_at(changeset) do
status = get_field(changeset, :status)
scheduled_at = get_field(changeset, :scheduled_at)
status_changed? = get_change(changeset, :status) != nil
scheduled_at_changed? = get_change(changeset, :scheduled_at) != nil
case {status, scheduled_at} do
{"scheduled", nil} ->
add_error(changeset, :scheduled_at, "must be set when status is scheduled")
{"scheduled", datetime} when not is_nil(datetime) ->
# Only validate scheduled_at is in the future if:
# 1. scheduled_at is being changed, OR
# 2. status is being changed TO "scheduled"
# This allows editing other fields without re-validating an existing schedule
if (scheduled_at_changed? or status_changed?) and
DateTime.compare(datetime, UtilsDate.utc_now()) == :lt do
add_error(changeset, :scheduled_at, "must be in the future")
else
changeset
end
_ ->
changeset
end
end
defp maybe_generate_slug(changeset) do
case get_change(changeset, :slug) do
nil ->
title = get_field(changeset, :title)
if title do
slug = slugify(title)
put_change(changeset, :slug, slug)
else
changeset
end
_slug ->
changeset
end
end
defp slugify(title) do
title
|> String.downcase()
|> String.replace(~r/[^\w\s-]/, "")
|> String.replace(~r/\s+/, "-")
|> String.trim("-")
end
end