My code:
defmodule Rumbl.Repo.Migrations.CreateUser do
use Ecto.Migration
def change do
create table(:users) do
add :name, :string
add :username, :string, null: false
add :password_hash, :string
timestamps()
end
create unique_index(:users, [:username])
end
end
defmodule Rumbl.Repo.Migrations.CreateVideo do
use Ecto.Migration
def change do
create table(:videos) do
add :url, :string
add :title, :string
add :slug, :string
add :description, :text
add :user_id, references(:users, on_delete: :nothing)
timestamps()
end
create index(:videos, [:user_id])
end
end
defmodule Rumbl.Repo.Migrations.CreateUserVideos do
use Ecto.Migration
def change do
create table(:user_videos) do
add :user_id, references(:users, on_delete: :nothing)
add :video_id, references(:videos, on_delete: :nothing)
timestamps()
end
create index(:user_videos, [:video_id])
create index(:user_videos, [:user_id])
create unique_index(:user_videos, [:user_id, :video_id])
end
end
### SCHEMAS ###
defmodule Rumbl.User do
use Rumbl.Web, :model
schema "users" do
field :name, :string
field :username, :string
field :password, :string, virtual: true
field :password_hash, :string
# has_many :videos, Rumbl.Video
many_to_many :videos, Rumbl.Video, join_through: Rumbl.UserVideos, on_replace: :delete
has_many :annotations, Rumbl.Annotation
timestamps()
end
def changeset(user, params \\ %{}) do
user
|> cast(params, [:name, :username], [])
|> validate_length(:username, min: 1, max: 20)
|> unique_constraint(:username)
end
def registration_changeset(user, params) do
user
|> changeset(params)
|> cast(params, [:password], [])
|> validate_required([:username, :password])
|> validate_length(:password, min: 12, max: 256)
|> _put_pass_hash()
end
defp _put_pass_hash(changeset) do
case changeset do
%Ecto.Changeset{valid?: true, changes: %{password: pass}} ->
change(changeset, Argon2.add_hash(pass))
_ ->
changeset
end
end
end
defmodule Rumbl.Video do
use Rumbl.Web, :model
# The key to be used to extract the id we want to be used by the url helpers
@derive {Phoenix.Param, key: :slug}
schema "videos" do
field :url, :string
field :title, :string
field :description, :string
field :slug, :string
field :soft_deleted, :boolean, default: false
belongs_to :user, Rumbl.User
many_to_many :users, Rumbl.User, join_through: Rumbl.UserVideos
belongs_to :category, Rumbl.Category
has_many :annotations, Rumbl.Annotation
timestamps()
end
@required_fields [:url, :title, :description]
@optional_fields [:category_id, :soft_deleted]
@doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
# @IMPORTANT must come before `update_change/3`, otherwise the slug will not
# be fixed when we run Rumbl.ReleaseTasks.fix_video_records/0
|> slugify_title()
|> update_change(:url, &UtilsFor.Text.Trim.to_single_whitespace/1)
|> update_change(:title, &UtilsFor.Text.Trim.to_single_whitespace/1)
|> update_change(:description, &String.trim/1)
# @TODO change constraint to the youtube video id
|> unique_constraint(:slug)
|> assoc_constraint(:category)
end
defp slugify_title(changeset) do
if title = get_change(changeset, :title) do
put_change(changeset, :slug, UtilsFor.Text.Slug.slugify(title, "-"))
else
changeset
end
end
end
### ADD VIDEO ###
defmodule Rumbl.Videos.Add.Video do
def add_for(%Rumbl.User{} = user, %{} = params) do
changeset = %Rumbl.Video{} |> Rumbl.Video.changeset(params)
case Rumbl.Repo.insert(changeset) do
{:ok, %Rumbl.Video{} = video} ->
video
# @TODO Use instead Ecto Associations?
|> Rumbl.Videos.AssociateWithUser.Video.associate(user)
{:ok, video}
{:error, changeset} ->
{:error, changeset, video}
end
end
end
### ASSOCIATE VIDEO ###
defmodule Rumbl.Videos.AssociateWithUser.Video do
def associate(%Rumbl.Video{} = video, %Rumbl.User{} = user) do
Rumbl.UserVideos.new_changeset(%{user_id: user.id, video_id: video.id})
|> Rumbl.Repo.insert()
end
end
Then I use it from the controller:
def create(conn, %{"video" => video_params}, user) do
case Rumbl.Videos.Add.Video.add_for(user, video_params) do
{:ok, %Rumbl.Video{} = video} ->
Logger.info("Created new video with slug:#{video.slug} for user @#{user.username}:#{user.id}")
conn
|> redirect(to: watch_path(conn, :show, video))
{:error, changeset, video} ->
render(conn, "new.html", changeset: changeset)
end
end
So, as you can see by the code the association for the video to the user is done by invoking Rumbl.Videos.AssociateWithUser.Video.associate(video, user)
from Rumbl.Videos.Add.Video.add_for(user, params)
.
Hope that the code works better then 1000 words