Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions apps/core/lib/prodigy/core/data/club.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright 2022-2025, Phillip Heller
#
# This file is part of Prodigy Reloaded.
#
# Prodigy Reloaded is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# Prodigy Reloaded is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License along with Prodigy Reloaded. If not,
# see <https://www.gnu.org/licenses/>.

defmodule Prodigy.Core.Data.Club do
use Ecto.Schema
import Ecto.Changeset

@moduledoc """
Schema for Prodigy Bulletin Board clubs
"""

schema "club" do
field(:handle, :string) # 3 character handle
field(:name, :string) # Full display name

has_many(:topics, Prodigy.Core.Data.Topic)

timestamps()
end

def changeset(club, attrs) do
club
|> cast(attrs, [:handle, :name])
|> validate_required([:handle, :name])
|> validate_length(:handle, is: 3)
|> unique_constraint(:handle)
end
end
78 changes: 78 additions & 0 deletions apps/core/lib/prodigy/core/data/post.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Copyright 2022-2025, Phillip Heller
#
# This file is part of Prodigy Reloaded.
#
# Prodigy Reloaded is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# Prodigy Reloaded is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License along with Prodigy Reloaded. If not,
# see <https://www.gnu.org/licenses/>.

defmodule Prodigy.Core.Data.Post do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query

@moduledoc """
Schema for Bulletin Board posts and replies
"""

schema "post" do
belongs_to(:topic, Prodigy.Core.Data.Topic)
field(:sent_date, :utc_datetime)
field(:in_reply_to, :integer) # NULL for top-level posts, post_id for replies
field(:to_id, :string, default: "") # Empty defaults to "All"
field(:from_id, :string) # user_id from posting context
field(:subject, :string)
field(:body, :binary) # Using binary to match message.contents pattern

# Virtual fields for computed values (not stored in DB)
field(:reply_count, :integer, virtual: true)
field(:last_reply_date, :utc_datetime, virtual: true)
field(:to_name, :string, virtual: true) # Derived from user_id
field(:from_name, :string, virtual: true) # Derived from user_id

timestamps()
end

def changeset(post, attrs) do
post
|> cast(attrs, [:topic_id, :sent_date, :in_reply_to, :to_id,
:from_id, :subject, :body])
|> validate_required([:topic_id, :sent_date, :from_id, :subject, :body])
|> foreign_key_constraint(:topic_id)
|> foreign_key_constraint(:in_reply_to)
end

@doc """
Query to load a post with reply count and last reply date.
This should be used when fetching posts for display.
"""
def with_reply_stats(query) do
from p in query,
left_join: r in __MODULE__,
on: r.in_reply_to == p.id,
group_by: p.id,
select: %{p |
reply_count: count(r.id),
last_reply_date: max(r.sent_date)
}
end

@doc """
Query to load a post with the from_name populated from the user table.
"""
def with_from_name(query) do
from p in query,
left_join: u in Prodigy.Core.Data.User,
on: u.id == p.from_id,
select: %{p |
from_name: fragment("COALESCE(?, ?)", u.first_name, p.from_id)
}
end
end
41 changes: 41 additions & 0 deletions apps/core/lib/prodigy/core/data/topic.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright 2022-2025, Phillip Heller
#
# This file is part of Prodigy Reloaded.
#
# Prodigy Reloaded is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# Prodigy Reloaded is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License along with Prodigy Reloaded. If not,
# see <https://www.gnu.org/licenses/>.

defmodule Prodigy.Core.Data.Topic do
use Ecto.Schema
import Ecto.Changeset

@moduledoc """
Schema for Bulletin Board topics within clubs
"""

# Using id type which will map to smallserial in the migration
schema "topic" do
belongs_to(:club, Prodigy.Core.Data.Club)
field(:title, :string)
field(:closed, :boolean, default: false) # Flag to close topic to new posts

has_many(:posts, Prodigy.Core.Data.Post)

timestamps()
end

def changeset(topic, attrs) do
topic
|> cast(attrs, [:club_id, :title, :closed])
|> validate_required([:club_id, :title])
|> foreign_key_constraint(:club_id)
end
end
51 changes: 51 additions & 0 deletions apps/core/lib/prodigy/core/data/user_club.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright 2022-2025, Phillip Heller
#
# This file is part of Prodigy Reloaded.
#
# Prodigy Reloaded is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# Prodigy Reloaded is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License along with Prodigy Reloaded. If not,
# see <https://www.gnu.org/licenses/>.

defmodule Prodigy.Core.Data.UserClub do
use Ecto.Schema
import Ecto.Changeset

@moduledoc """
Schema for tracking when a user last read posts in a bulletin board club.
Only created/updated when the user actually starts reading (via note cursor).
"""

@primary_key false
schema "user_club" do
field(:user_id, :string, primary_key: true)
field(:club_id, :integer, primary_key: true)
field(:last_read_date, :utc_datetime)

belongs_to(:user, Prodigy.Core.Data.User,
foreign_key: :user_id,
references: :id,
define_field: false)
belongs_to(:club, Prodigy.Core.Data.Club,
foreign_key: :club_id,
references: :id,
define_field: false)

timestamps()
end

def changeset(user_club, attrs) do
user_club
|> cast(attrs, [:user_id, :club_id, :last_read_date])
|> validate_required([:user_id, :club_id, :last_read_date])
|> foreign_key_constraint(:user_id)
|> foreign_key_constraint(:club_id)
|> unique_constraint([:user_id, :club_id], name: :user_club_pkey)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Copyright 2022-2025, Phillip Heller
#
# This file is part of Prodigy Reloaded.
#
# Prodigy Reloaded is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# Prodigy Reloaded is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License along with Prodigy Reloaded. If not,
# see <https://www.gnu.org/licenses/>.

defmodule Prodigy.Core.Data.Repo.Migrations.CreateBulletinBoards do
use Ecto.Migration

def change do
# Create clubs table
create table(:club) do
add :handle, :string, size: 3, null: false
add :name, :string, null: false

timestamps()
end

create unique_index(:club, [:handle])

# Create topics table with smallint id
create table(:topic, primary_key: false) do
add :id, :smallserial, primary_key: true
add :club_id, references(:club, on_delete: :restrict), null: false
add :title, :string, null: false
add :closed, :boolean, default: false, null: false

timestamps()
end

create index(:topic, [:club_id])

# Create posts table
create table(:post) do
add :topic_id, references(:topic, type: :smallint, on_delete: :restrict), null: false
add :sent_date, :utc_datetime, null: false
add :in_reply_to, references(:post, on_delete: :restrict), null: true
add :to_id, :string, default: ""
add :from_id, :string, null: false # References user.id but not enforced at DB level
add :subject, :string, null: false
add :body, :text, null: false

timestamps()
end

create index(:post, [:topic_id])
create index(:post, [:topic_id, :sent_date])
create index(:post, [:topic_id, :from_id, :sent_date])
create index(:post, [:in_reply_to])
create index(:post, [:to_id])
create index(:post, [:from_id])
create index(:post, [:sent_date])
create index(:post, [:in_reply_to, :sent_date])

create table(:user_club, primary_key: false) do
add :user_id, references(:user, type: :string, on_delete: :delete_all), primary_key: true, null: false
add :club_id, references(:club, on_delete: :delete_all), primary_key: true, null: false
add :last_read_date, :utc_datetime, null: false

timestamps()
end

create unique_index(:user_club, [:user_id, :club_id])
create index(:user_club, [:club_id])
create index(:user_club, [:last_read_date])


end
end
Loading
Loading