Packages
phoenix_kit
1.7.1
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/phoenix_kit/posts/post.ex
defmodule PhoenixKit.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_id` - 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_id: 1,
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
@primary_key {:id, UUIDv7, autogenerate: true}
@foreign_key_type UUIDv7
@type t :: %__MODULE__{
id: UUIDv7.t() | nil,
user_id: integer(),
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(),
comment_count: integer(),
view_count: integer(),
metadata: map(),
user: PhoenixKit.Users.Auth.User.t() | Ecto.Association.NotLoaded.t(),
media: [PhoenixKit.Posts.PostMedia.t()] | Ecto.Association.NotLoaded.t(),
likes: [PhoenixKit.Posts.PostLike.t()] | Ecto.Association.NotLoaded.t(),
comments: [PhoenixKit.Posts.PostComment.t()] | Ecto.Association.NotLoaded.t(),
mentions: [PhoenixKit.Posts.PostMention.t()] | Ecto.Association.NotLoaded.t(),
tags: [PhoenixKit.Posts.PostTag.t()] | Ecto.Association.NotLoaded.t(),
groups: [PhoenixKit.Posts.PostGroup.t()] | Ecto.Association.NotLoaded.t(),
inserted_at: NaiveDateTime.t() | nil,
updated_at: NaiveDateTime.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_usec
field :published_at, :utc_datetime_usec
field :repost_url, :string
field :slug, :string
field :like_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, type: :integer
has_many :media, PhoenixKit.Posts.PostMedia
has_many :likes, PhoenixKit.Posts.PostLike
has_many :comments, PhoenixKit.Posts.PostComment
has_many :mentions, PhoenixKit.Posts.PostMention
many_to_many :tags, PhoenixKit.Posts.PostTag,
join_through: PhoenixKit.Posts.PostTagAssignment,
join_keys: [post_id: :id, tag_id: :id]
many_to_many :groups, PhoenixKit.Posts.PostGroup,
join_through: PhoenixKit.Posts.PostGroupAssignment,
join_keys: [post_id: :id, group_id: :id]
timestamps(type: :naive_datetime)
end
@doc """
Changeset for creating or updating a post.
## Required Fields
- `user_id` - 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_id,
:title,
:sub_title,
:content,
:type,
:status,
:scheduled_at,
:published_at,
:repost_url,
:slug,
:metadata
])
|> validate_required([:user_id, :title, :content, :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_id)
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)
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) ->
# Validate scheduled_at is in the future
if DateTime.compare(datetime, DateTime.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