PhoenixKit.Modules.Posts (phoenix_kit v1.7.71)

Copy Markdown View Source

Context for managing posts, likes, tags, and groups.

Provides complete API for the social posts system including CRUD operations, counter cache management, tag assignment, and group organization. Comments are now handled by the standalone PhoenixKit.Modules.Comments module.

Features

  • Post Management: Create, update, delete, publish, schedule posts
  • Like System: Like/unlike posts, check like status
  • Comment System: Nested threaded comments with unlimited depth
  • Tag System: Hashtag categorization with auto-slugification
  • Group System: User collections for organizing posts
  • Media Attachments: Multiple images per post with ordering
  • Publishing: Draft/public/unlisted/scheduled status management
  • Analytics: View tracking (future feature)

Examples

# Create a post
{:ok, post} = Posts.create_post(user_uuid, %{
  title: "My First Post",
  content: "Hello world!",
  type: "post",
  status: "draft"
})

# Publish a post
{:ok, post} = Posts.publish_post(post)

# Like a post
{:ok, like} = Posts.like_post(post.uuid, user_uuid)

# Add a comment
{:ok, comment} = Posts.create_comment(post.uuid, user_uuid, %{
  content: "Great post!"
})

# Create a group
{:ok, group} = Posts.create_group(user_uuid, %{
  name: "Travel Photos",
  description: "My adventures"
})

Summary

Functions

Adds multiple posts to a group in a single transaction.

Adds tags to a post.

Creates a user group.

Creates a new post.

Decrements the comment counter for a post.

Decrements the dislike counter for a post.

Decrements the like counter for a post.

Deletes a group.

Deletes a post and all related data (cascades to media, likes, comments, etc.).

Detaches media from a post.

Detaches media from a post by PostMedia ID.

Disables the Posts module.

User dislikes a post.

Reverts a post to draft status.

Enables the Posts module.

Checks if the Posts module is enabled.

Finds or creates a tag by name.

Gets the current Posts module configuration and stats.

Gets the featured image for a post (PostMedia with position 1).

Gets a single group by ID with optional preloads.

Gets a single group by ID with optional preloads.

Gets a single post by ID with optional preloads.

Gets a single post by ID with optional preloads.

Gets a single post by slug.

Increments the comment counter for a post.

Increments the dislike counter for a post.

Increments the like counter for a post.

Increments the view counter for a post.

User likes a post.

Lists all groups ordered by name.

Lists popular tags by usage count.

Lists all dislikes for a post.

Lists all likes for a post.

Lists media for a post (ordered by position).

Lists mentioned users in a post.

Lists posts with optional filtering and pagination.

Lists public posts only.

Callback invoked by the Comments module when a comment is created on a post. Increments the post's denormalized comment_count.

Callback invoked by the Comments module when a comment is deleted from a post. Decrements the post's denormalized comment_count.

Parses hashtags from text.

Checks if a user has disliked a post.

Checks if a user has liked a post.

Processes scheduled posts that are ready to be published.

Publishes a post (makes it public).

Removes the featured image from a post.

Removes a mention from a post.

Removes a post from a group.

Removes a tag from a post.

Resolves post titles and admin paths for a list of resource IDs.

Schedules a post for future publishing.

Sets the featured image for a post (PostMedia with position 1).

User removes dislike from a post.

User unlikes a post.

Unschedules a post, reverting it to draft status.

Updates a group.

Updates an existing post.

Functions

add_mention_to_post(post_uuid, user_uuid, mention_type \\ "mention")

Adds a mention to a post.

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • user_uuid - User UUID (UUIDv7 string) to mention
  • mention_type - "contributor" or "mention" (default: "mention")

Examples

iex> add_mention_to_post("018e3c4a-...", "019145a1-...", "contributor")
{:ok, %PostMention{}}

add_post_to_group(post_uuid, group_uuid, opts \\ [])

Adds a post to a group.

Increments the group's post counter.

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • group_uuid - Group UUID (UUIDv7 string)
  • opts - Options
    • :position - Display position (default: 0)

Examples

iex> add_post_to_group("018e3c4a-...", "018e3c4a-...")
{:ok, %PostGroupAssignment{}}

add_posts_to_group(post_uuids, group_uuid, opts \\ [])

Adds multiple posts to a group in a single transaction.

Uses on_conflict: :nothing to skip duplicates, then increments the group's post_count by the actual number of newly inserted rows.

Parameters

  • post_uuids - List of Post UUIDs (UUIDv7 strings)
  • group_uuid - Group UUID (UUIDv7 string)
  • opts - Options
    • :position - Position in group (default: 0)

Examples

iex> add_posts_to_group(["018e3c4a-...", "018e3c4b-..."], "018e3c4a-...")
{:ok, 2}

add_tags_to_post(post, tag_names)

Adds tags to a post.

Creates tags if they don't exist, then assigns them to the post. Updates usage counters for tags.

Parameters

  • post - Post to tag
  • tag_names - List of tag names

Examples

iex> add_tags_to_post(post, ["elixir", "phoenix"])
{:ok, [%PostTag{}, %PostTag{}]}

attach_media(post_uuid, file_uuid, opts \\ [])

Attaches media to a post.

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • file_uuid - File UUID (UUIDv7 string, from PhoenixKit.Modules.Storage)
  • opts - Options
    • :position - Display position (default: 1)
    • :caption - Image caption

Examples

iex> attach_media("018e3c4a-...", "018e3c4a-...", position: 1)
{:ok, %PostMedia{}}

create_group(user_uuid, attrs)

Creates a user group.

Parameters

  • user_uuid - Owner UUID (UUIDv7 string)
  • attrs - Group attributes (name, description, etc.)

Examples

iex> create_group("019145a1-...", %{name: "Travel Photos"})
{:ok, %PostGroup{}}

create_post(user_uuid, attrs)

Creates a new post.

Parameters

  • user_uuid - Owner UUID (UUIDv7 string)
  • attrs - Post attributes (title, content, type, status, etc.)

Examples

iex> create_post("019145a1-...", %{title: "Test", content: "Content", type: "post"})
{:ok, %Post{}}

iex> create_post("019145a1-...", %{title: "", content: ""})
{:error, %Ecto.Changeset{}}

decrement_comment_count(post)

Decrements the comment counter for a post.

Examples

iex> decrement_comment_count(post)
{1, nil}

decrement_dislike_count(post)

Decrements the dislike counter for a post.

Examples

iex> decrement_dislike_count(post)
{1, nil}

decrement_like_count(post)

Decrements the like counter for a post.

Examples

iex> decrement_like_count(post)
{1, nil}

delete_group(group)

Deletes a group.

Parameters

  • group - Group to delete

Examples

iex> delete_group(group)
{:ok, %PostGroup{}}

delete_post(post)

Deletes a post and all related data (cascades to media, likes, comments, etc.).

Parameters

  • post - Post struct to delete

Examples

iex> delete_post(post)
{:ok, %Post{}}

iex> delete_post(post)
{:error, %Ecto.Changeset{}}

detach_media(post_uuid, file_uuid)

Detaches media from a post.

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • file_uuid - File UUID (UUIDv7 string)

Examples

iex> detach_media("018e3c4a-...", "018e3c4a-...")
{:ok, %PostMedia{}}

detach_media_by_uuid(media_uuid)

Detaches media from a post by PostMedia ID.

Parameters

  • media_uuid - PostMedia record UUID (UUIDv7 string)

Examples

iex> detach_media_by_uuid("018e3c4a-...")
{:ok, %PostMedia{}}

disable_system()

Disables the Posts module.

Examples

iex> disable_system()
{:ok, %Setting{}}

dislike_post(post_uuid, user_uuid)

User dislikes a post.

Creates a dislike record and increments the post's dislike counter. Returns error if user has already disliked the post.

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • user_uuid - User UUID (UUIDv7 string)

Examples

iex> dislike_post("018e3c4a-...", "019145a1-...")
{:ok, %PostDislike{}}

iex> dislike_post("018e3c4a-...", "019145a1-...")  # Already disliked
{:error, %Ecto.Changeset{}}

draft_post(post)

Reverts a post to draft status.

Examples

iex> draft_post(post)
{:ok, %Post{status: "draft"}}

enable_system()

Enables the Posts module.

Examples

iex> enable_system()
{:ok, %Setting{}}

enabled?()

Checks if the Posts module is enabled.

Examples

iex> enabled?()
true

find_or_create_tag(name)

Finds or creates a tag by name.

Automatically generates slug from name. Returns existing tag if slug already exists.

Parameters

  • name - Tag name (e.g., "Web Development")

Examples

iex> find_or_create_tag("Web Development")
{:ok, %PostTag{name: "Web Development", slug: "web-development"}}

iex> find_or_create_tag("web development")  # Same slug
{:ok, %PostTag{name: "Web Development"}}  # Returns existing

get_config()

Gets the current Posts module configuration and stats.

Examples

iex> get_config()
%{enabled: true, total_posts: 42, published_posts: 30, ...}

get_group(id, opts \\ [])

Gets a single group by ID with optional preloads.

Returns nil if group not found.

Parameters

  • id - Group ID (UUIDv7)
  • opts - Options
    • :preload - List of associations to preload (e.g., [:user, :posts])

Examples

iex> get_group("018e3c4a-...")
%PostGroup{}

iex> get_group("018e3c4a-...", preload: [:user])
%PostGroup{user: %User{}}

iex> get_group("nonexistent")
nil

get_group!(id, opts \\ [])

Gets a single group by ID with optional preloads.

Raises Ecto.NoResultsError if group not found.

Parameters

  • id - Group ID (UUIDv7)
  • opts - Options
    • :preload - List of associations to preload (e.g., [:user, :posts])

Examples

iex> get_group!("018e3c4a-...")
%PostGroup{}

iex> get_group!("018e3c4a-...", preload: [:user])
%PostGroup{user: %User{}}

iex> get_group!("nonexistent")
** (Ecto.NoResultsError)

get_post(id, opts \\ [])

Gets a single post by ID with optional preloads.

Returns nil if post not found.

Parameters

  • id - Post ID (UUIDv7)
  • opts - Options
    • :preload - List of associations to preload

Examples

iex> get_post("018e3c4a-...")
%Post{}

iex> get_post("018e3c4a-...", preload: [:user, :media, :tags])
%Post{user: %User{}, media: [...], tags: [...]}

iex> get_post("nonexistent")
nil

get_post!(id, opts \\ [])

Gets a single post by ID with optional preloads.

Raises Ecto.NoResultsError if post not found.

Parameters

  • id - Post ID (UUIDv7)
  • opts - Options
    • :preload - List of associations to preload

Examples

iex> get_post!("018e3c4a-...")
%Post{}

iex> get_post!("018e3c4a-...", preload: [:user, :media, :tags])
%Post{user: %User{}, media: [...], tags: [...]}

iex> get_post!("nonexistent")
** (Ecto.NoResultsError)

get_post_by_slug(slug, opts \\ [])

Gets a single post by slug.

Parameters

  • slug - Post slug (e.g., "my-first-post")
  • opts - Options
    • :preload - List of associations to preload

Examples

iex> get_post_by_slug("my-first-post")
%Post{}

iex> get_post_by_slug("nonexistent")
nil

increment_comment_count(post)

Increments the comment counter for a post.

Examples

iex> increment_comment_count(post)
{1, nil}

increment_dislike_count(post)

Increments the dislike counter for a post.

Examples

iex> increment_dislike_count(post)
{1, nil}

increment_like_count(post)

Increments the like counter for a post.

Examples

iex> increment_like_count(post)
{1, nil}

increment_view_count(post)

Increments the view counter for a post.

Examples

iex> increment_view_count(post)
{1, nil}

like_post(post_uuid, user_uuid)

User likes a post.

Creates a like record and increments the post's like counter. Returns error if user already liked the post.

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • user_uuid - User UUID (UUIDv7 string)

Examples

iex> like_post("018e3c4a-...", "019145a1-...")
{:ok, %PostLike{}}

iex> like_post("018e3c4a-...", "019145a1-...")  # Already liked
{:error, %Ecto.Changeset{}}

list_groups(opts \\ [])

Lists all groups ordered by name.

Parameters

  • opts - Options
    • :preload - Associations to preload

Examples

iex> list_groups()
[%PostGroup{}, ...]

list_post_dislikes(post_uuid, opts \\ [])

Lists all dislikes for a post.

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • opts - Options
    • :preload - Associations to preload

Examples

iex> list_post_dislikes("018e3c4a-...")
[%PostDislike{}, ...]

iex> list_post_dislikes("018e3c4a-...", preload: [:user])
[%PostDislike{user: %User{}}, ...]

list_post_likes(post_uuid, opts \\ [])

Lists all likes for a post.

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • opts - Options
    • :preload - Associations to preload

Examples

iex> list_post_likes("018e3c4a-...")
[%PostLike{}, ...]

iex> list_post_likes("018e3c4a-...", preload: [:user])
[%PostLike{user: %User{}}, ...]

list_post_media(post_uuid, opts \\ [])

Lists media for a post (ordered by position).

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • opts - Options
    • :preload - Associations to preload

Examples

iex> list_post_media("018e3c4a-...")
[%PostMedia{}, ...]

list_post_mentions(post_uuid, opts \\ [])

Lists mentioned users in a post.

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • opts - Options
    • :preload - Associations to preload

Examples

iex> list_post_mentions("018e3c4a-...")
[%PostMention{}, ...]

list_posts(opts \\ [])

Lists posts with optional filtering and pagination.

Parameters

  • opts - Options
    • :user_uuid - Filter by user
    • :status - Filter by status (draft/public/unlisted/scheduled)
    • :type - Filter by type (post/snippet/repost)
    • :search - Search in title and content
    • :page - Page number (default: 1)
    • :per_page - Items per page (default: 20)
    • :preload - Associations to preload

Examples

iex> list_posts()
[%Post{}, ...]

iex> list_posts(status: "public", page: 1, per_page: 10)
[%Post{}, ...]

iex> list_posts(user_uuid: "018e3c4a-9f6b-7890-abcd-ef1234567890", type: "post")
[%Post{}, ...]

list_posts_by_group(group_uuid, opts \\ [])

Lists posts in a group.

Parameters

  • group_uuid - Group UUID (UUIDv7 string)
  • opts - Options
    • :preload - Associations to preload

Examples

iex> list_posts_by_group("018e3c4a-...")
[%Post{}, ...]

list_public_posts(opts \\ [])

Lists public posts only.

Parameters

Examples

iex> list_public_posts()
[%Post{}, ...]

list_user_groups(user_uuid, opts \\ [])

Lists user's groups.

Parameters

  • user_uuid - User UUID (UUIDv7 string)
  • opts - Options
    • :preload - Associations to preload

Examples

iex> list_user_groups("019145a1-...")
[%PostGroup{}, ...]

list_user_posts(user_uuid, opts \\ [])

Lists user's posts.

Parameters

  • user_uuid - User UUID (UUIDv7 string)
  • opts - See list_posts/1 for options

Examples

iex> list_user_posts("019145a1-...")
[%Post{}, ...]

on_comment_created(arg1, resource_uuid, comment)

Callback invoked by the Comments module when a comment is created on a post. Increments the post's denormalized comment_count.

on_comment_deleted(arg1, resource_uuid, comment)

Callback invoked by the Comments module when a comment is deleted from a post. Decrements the post's denormalized comment_count.

parse_hashtags(text)

Parses hashtags from text.

Extracts all hashtags (#word) from text and returns list of tag names.

Parameters

  • text - Text to parse

Examples

iex> parse_hashtags("Check out #elixir and #phoenix!")
["elixir", "phoenix"]

iex> parse_hashtags("No tags here")
[]

post_disliked_by?(post_uuid, user_uuid)

Checks if a user has disliked a post.

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • user_uuid - User UUID (UUIDv7 string)

Examples

iex> post_disliked_by?("018e3c4a-...", "019145a1-...")
true

iex> post_disliked_by?("018e3c4a-...", "019145a2-...")
false

post_liked_by?(post_uuid, user_uuid)

Checks if a user has liked a post.

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • user_uuid - User UUID (UUIDv7 string)

Examples

iex> post_liked_by?("018e3c4a-...", "019145a1-...")
true

iex> post_liked_by?("018e3c4a-...", "019145a2-...")
false

process_scheduled_posts()

Processes scheduled posts that are ready to be published.

Finds all posts with status "scheduled" where scheduled_at <= now, and publishes them. Returns list of published posts.

Should be called periodically (e.g., via Oban job every minute).

Examples

iex> process_scheduled_posts()
{:ok, 2}

publish_post(post)

Publishes a post (makes it public).

Sets status to "public" and published_at to current time.

Examples

iex> publish_post(post)
{:ok, %Post{status: "public"}}

remove_mention_from_post(post_uuid, user_uuid)

Removes a mention from a post.

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • user_uuid - User UUID (UUIDv7 string)

Examples

iex> remove_mention_from_post("018e3c4a-...", "019145a1-...")
{:ok, %PostMention{}}

remove_post_from_group(post_uuid, group_uuid)

Removes a post from a group.

Decrements the group's post counter.

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • group_uuid - Group UUID (UUIDv7 string)

Examples

iex> remove_post_from_group("018e3c4a-...", "018e3c4a-...")
{:ok, %PostGroupAssignment{}}

remove_tag_from_post(post_uuid, tag_uuid)

Removes a tag from a post.

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • tag_uuid - Tag UUID (UUIDv7 string)

Examples

iex> remove_tag_from_post("018e3c4a-...", "018e3c4a-...")
{:ok, %PostTagAssignment{}}

reorder_groups(user_uuid, group_uuid_positions)

Reorders user's groups.

Updates position field for multiple groups.

Parameters

  • user_uuid - User UUID (UUIDv7 string)
  • group_uuid_positions - Map of group_uuid => position

Examples

iex> reorder_groups("019145a1-...", %{"group1" => 0, "group2" => 1})
:ok

reorder_media(post_uuid, file_uuid_positions)

Reorders media in a post.

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • file_uuid_positions - Map of file_uuid => position

Examples

iex> reorder_media("018e3c4a-...", %{"file1" => 1, "file2" => 2})
:ok

resolve_comment_resources(resource_uuids)

Resolves post titles and admin paths for a list of resource IDs.

Called by the Comments module to display resource context in the admin UI. Returns a map of resource_uuid => %{title: ..., path: ...}.

schedule_post(post, scheduled_at, attrs \\ %{}, opts \\ [])

Schedules a post for future publishing.

Updates the post status to "scheduled" and creates an entry in the scheduled jobs table for execution by the cron worker.

Parameters

  • post - Post to schedule
  • scheduled_at - DateTime to publish at (must be in future)
  • attrs - Additional attributes to update (title, content, etc.)
  • opts - Options
    • :created_by_uuid - UUID of user scheduling the post

Examples

iex> schedule_post(post, ~U[2025-12-31 09:00:00Z])
{:ok, %Post{status: "scheduled"}}

iex> schedule_post(post, ~U[2025-12-31 09:00:00Z], %{title: "New Title"})
{:ok, %Post{status: "scheduled", title: "New Title"}}

undislike_post(post_uuid, user_uuid)

User removes dislike from a post.

Deletes the dislike record and decrements the post's dislike counter. Returns error if dislike doesn't exist.

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • user_uuid - User UUID (UUIDv7 string)

Examples

iex> undislike_post("018e3c4a-...", "019145a1-...")
{:ok, %PostDislike{}}

iex> undislike_post("018e3c4a-...", "019145a1-...")  # Not disliked
{:error, :not_found}

unlike_post(post_uuid, user_uuid)

User unlikes a post.

Deletes the like record and decrements the post's like counter. Returns error if like doesn't exist.

Parameters

  • post_uuid - Post UUID (UUIDv7 string)
  • user_uuid - User UUID (UUIDv7 string)

Examples

iex> unlike_post("018e3c4a-...", "019145a1-...")
{:ok, %PostLike{}}

iex> unlike_post("018e3c4a-...", "019145a1-...")  # Not liked
{:error, :not_found}

unschedule_post(post)

Unschedules a post, reverting it to draft status.

Cancels any pending scheduled jobs for this post.

Parameters

  • post - Post to unschedule

Examples

iex> unschedule_post(post)
{:ok, %Post{status: "draft"}}

update_group(group, attrs)

Updates a group.

Parameters

  • group - Group to update
  • attrs - Attributes to update

Examples

iex> update_group(group, %{name: "New Name"})
{:ok, %PostGroup{}}

update_post(post, attrs)

Updates an existing post.

Parameters

  • post - Post struct to update
  • attrs - Attributes to update

Examples

iex> update_post(post, %{title: "Updated Title"})
{:ok, %Post{}}

iex> update_post(post, %{title: ""})
{:error, %Ecto.Changeset{}}