Put constraint on PhoenixFramework changeset - phoenix-framework

I have three models in Phoenix Framework, User, Post, Vote. Users can down vote the post only when they have more than 1 point. The user point is calculated by the up votes their posts got from other users.
This is what I defined in my Vote model:
schema "votes" do
field :type, :integer # -1 0 1
belongs_to :user, News.User
belongs_to :post, News.Post
timestamps()
end
Because the user points is not defined in Vote schema, so I can't use validate_change or add_error directly in model, unless I read other models' data to decide whether to add error to changeset, obviously it will be doing too many things in Vote model.
Where should I put the constraint? Controller or model?
Maybe I should place constraint on the database, make sure the user point never get below zero? I found something like trigger. But how will PostgreSQL trigger return their result to the changeset?
Updated (this one works, but I'm not sure if it's the best way)
I tried it in my controller:
def create(conn, %{"vote" => vote_params}, user) do
changeset = user
|> build_assoc(:votes)
|> Vote.changeset(vote_params)
changeset = if user.point < 1 do
Ecto.Changeset.add_error(changeset, :user_id, "You points is not enough.")
end
case Repo.insert(changeset) do
{:ok, vote} ->
conn
|> put_status(:created)
|> put_resp_header("location", vote_path(conn, :show, vote))
|> render("show.json", vote: vote)
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> render(WechatNews.ChangesetView, "error.json", changeset: changeset)
end
end
It's easy, but I have to repeat it in the update action too.

You could do something along the lines of
def changeset(struct, params) do
struct
|> cast([:points], params)
|> validate_required(:points)
end
def point_changeset(struct, params) do
struct
|> changeset(params)
|> check_points
end
defp check_points(changeset) do
if get_field(changeset, :points) < 1 do
add_error(changeset, :points, "too low")
else
changeset
end
end
This would give you a function point_changeset/2, which you can use only when you need to check that a user can do a certain action based on their points. It still calls the main changeset/2 function that has your validations that you want to always run.

Related

Create composable query for many_to_many association

I'm trying to create a composable ecto query for listing all Document for a specific Contributor. I want the api to look something like this:
Document
|> Document.for_contributor(contributor)
|> Repo.all()
But I'm at a loss as to where to begin. I've been doing composable queries before and in a has_many relation where a Contributor could have many Document I would do something like this:
def for_contributor(query, %Contributor{} = contributor) do
from(document in query, where: document.contributor_id == ^contributor.id)
end
But I'm not sure how I would go about doing something similar but with a many_to_many relation.
What would go in my for_contributor function?
defmodule MyApp.Document do
use Ecto.Schema
import Ecto.Changeset
alias MyApp.Contributor
schema "documents" do
many_to_many(:contributors, Contributor, join_through: "contributors_documents")
timestamps()
end
def for_contributor(query, %Contributor{} = contributor) do
# ???
end
end
My join table looks like this:
defmodule MyApp.Repo.Migrations.CreateContributorsDocuments do
use Ecto.Migration
def change do
create table(:contributors_documents, primary_key: false) do
add :contributor_id, references(:contributors)
add :document_id, references(:documents)
end
end
end
I made it more complicated in my head than it needed to be. I solved it with a simple join.
def for_contributor(query, %Contributor{} = contributor) do
from(
document in query,
join: c in assoc(document, :contributors),
where: c.id == ^contributor.id
)
end

struct__/1 is undefined, cannot expand struct error Phoenix 1.3

I'm trying to create a contact form in a phoenix 1.3 app. I used mix phx.gen.html to create the relevant files. However, I'm getting a compilation error when trying to start the server:
== Compilation error on file lib/iotc/web/controllers/email_controller.ex ==
** (CompileError) lib/iotc/web/controllers/email_controller.ex:7: Email.__struct__/1 is undefined, cannot expand struct Email
(stdlib) lists.erl:1354: :lists.mapfoldl/3
lib/iotc/web/controllers/email_controller.ex:6: (module)
(stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
Looking at some other posts with a similar issue, it could be related to the alias, but I have alias Itoc.Contact in the controller, and I don't think alias Iotc.Contact.Email would be right here.
email_controller.ex
defmodule Iotc.Web.EmailController do
use Iotc.Web, :controller
alias Iotc.Contact
def index(conn, _params) do
changeset = Email.changeset(%Email{})
emails = Contact.list_emails()
render(conn, "index.html", emails: emails, changeset: changeset)
end
...
email.ex
defmodule Iotc.Contact.Email do
use Ecto.Schema
import Ecto.Changeset
alias Iotc.Contact.Email
schema "contact_emails" do
field :email, :string
field :event, :string
field :message, :string
field :name, :string
timestamps()
end
#doc false
def changeset(%Email{} = email, attrs) do
email
|> cast(attrs, [:name, :email, :message, :event])
|> validate_required([:name, :email, :message, :event])
end
end
With respect to this
OK makes sense. I've updated it to the controller to : changeset =
Contact.Email.changeset(%Contact.Email{}) But I now get: warning:
function Iotc.Contact.Email.changeset/1 is undefined or private. Did
you mean one of: * changeset/2
You only have one function changeset/2 defined in the Email module.
But you're doing Contact.Email.changeset(%Contact.Email{}) passing only one argument. Do ``Contact.Email.changeset(%Contact.Email{})` and it should work.
The /2 part of the signature tells you the arity of the function, namely how many arguments takes.

Validating Ecto "many-to-many" relationships

I'm trying to determine the right way to validate a many-to-many relationship in Ecto 2. I have a Conversation model that needs to have many members, and Users can be part of many conversations, so I've established the models like so:
# User Model
defmodule MyApp.User do
...
schema "users" do
....
many_to_many :conversations, Conversation, join_through: "conversations_users"
...
end
...
end
# Conversation Model
defmodule MyApp.Conversation do
...
schema "conversations" do
has_many :messages, Message
many_to_many :members, User, join_through: "conversations_users"
timestamps()
end
def changeset(struct, _params) do
struct
|> validate_member_count
end
defp validate_member_count(changeset) do
members = Repo.all(assoc(changeset, :members))
valid? = length(members) == 2
if valid? do
add_error(changeset, :members, "foo")
else
changeset
end
end
end
However, I just can't get this to work. I've written a simple test to verify that the validations run correctly, but I keep getting the following error:
# Test
test "fails to validate a conversation with less than two members" do
changeset = Conversation.changeset(%Conversation{}, %{})
{message, []} = changeset.errors[:members]
assert message === "must have at least two members"
end
** (FunctionClauseError) no function clause matching in Ecto.Changeset.add_error/4
I'm having a hard time understanding what I'm doing wrong. It seems like it can't find the function, but I've checked the documentation and it seems like Ecto.Changeset.add_error/4 is definitely right, and the arguments to it seem correct as well.
My best guess is that I need to do something in the validation before calling my custom validator, but I just don't know what I should do.
There are 2 mistakes:
You're passing a MyApp.Conversation to validate_member_count, not an Ecto.Changeset. You can convert an Ecto Schema defining Struct into an Ecto.Changeset using Ecto.Changeset.change/1:
def changeset(struct, _params) do
struct
|> change
|> validate_member_count
end
Ecto.assoc/2 accepts an Ecto Schema Struct, not an Ecto.Changeset. You can access the underlying struct from an Ecto.Changeset using .data:
members = Repo.all(assoc(changeset.data, :members))
Final code:
def changeset(struct, _params) do
struct
|> change
|> validate_member_count
end
defp validate_member_count(changeset) do
members = Repo.all(assoc(changeset.data, :members))
valid? = length(members) == 2
if valid? do
add_error(changeset, :members, "foo")
else
changeset
end
end

An elegant way to validate and process a complex input using changesets

I'm trying to create a small Phoenix application, and having troubles finding the best way to process user input that came to the model from a controller.
I have 2 models and 1 controller:
defmodule MyApp.Post do
use MyApp.Web, :model
schema "posts" do
field :title, :string
field :text, :string
field :comments_count, :integer
has_many :comments, MyApp.Comment
timestamps()
end
end
defmodule MyApp.Comment do
use MyApp.Web, :model
schema "comments" do
field :text, :string
field :parent_path, :string # I want to store comments in a tree, using "Materialized Path" method
belongs_to :post, MyApp.Post
timestamps()
end
defmodule Ops do
# I keep all operations that are related to comments in this module
alias MyApp.{Repo, Comment}
def create_by_user(params) do
# params came straight from the controller. Expected fields are:
# 1. text - text of the comment, required
# 2. post_id - id of the post, required
# 3. parent_id - id of the parent comment, optional
# This function must:
# 1. Validate presence of the text (this is simple)
# 2. Check that post with given "post_id" exists
# 3. If "parent_id" is given:
# 3.1. Check that parent comment exists and belongs to the same post
# 3.2. Based on fields of parent comment, calculate the "parent_path" value of the new comment
# 4. If input is valid, insert a new comment to database and update post's "comments_count" field
end
end
end
defmodule MyApp.CommentController do
use MyApp.Web, :controller
alias MyApp.{Post, Comment}
def create(conn, params) do
case Comment.Ops.create_by_user(params) do
{:ok, comment} -> conn |> put_status(200) |> json("not implemented yet")
{:error, changeset} -> conn |> put_status(422) |> json("not implemented yet")
# Also, in case of error, it would be nice to look into changeset.errors and if post wasn't found, return 404
end
end
end
What is the most elegant implementation of Comment.Ops.create_by_user function?

Inserting an model into a many-to-many relationship with an existing model in ecto 2

I'm trying out the Ecto 2 rc.
My models are:
schema "containers" do
field :name, :string
many_to_many :items, Test.Item, join_through: Test.ContainerItem, on_delete: :delete_all
timestamps
end
schema "items" do
field :content, :string
many_to_many :containers, Test.Container, join_through: Test.ContainerItem, on_delete: :delete_all
timestamps
end
schema "containers_items" do
belongs_to :container, Test.Container
belongs_to :item, Test.Item
timestamps
end
And my controller code is:
def add_item(conn, %{"item" => item_params, "container_id" => container_id}) do
item = Item.changeset(%Item{}, item_params)
IO.inspect(item) #TODO remove
container = Container |> Repo.get(container_id) |> Repo.preload([:items])
IO.inspect(container) #TODO remove
changeset = container
|> Ecto.Changeset.change
|> Ecto.Changeset.put_assoc(:items, [item])
IO.inspect(changeset) #TODO remove
if changeset.valid? do
Repo.update(changeset)
conn
|> put_flash(:info, "Item added.")
|> redirect(to: container_path(conn, :show, container))
else
render(conn, "show.html", container: container, changeset: changeset)
end
end
Now this works fine if I'm adding a single item to a container. However if an item exists on a container, then trying to add another item gives me:
(RuntimeError) you are attempting to change relation :items of
Test.Container, but there is missing data.
I can't help but feel I'm going about this the wrong way, some advice would be appreciated.
Ok, so I just figured this out.
My problem was is not turning the items into Changesets so that ecto can track the changes that it needs to make.
The only edits I needed to make are to the controller.
It should look like this instead:
def add_item(conn, %{"item" => item_params, "container_id" => container_id}) do
item = Item.changeset(%Item{}, item_params)
IO.inspect(item) #TODO remove
container = Container |> Repo.get(container_id) |> Repo.preload([:items])
IO.inspect(container) #TODO remove
item_changesets = Enum.map([item | container.items], &Ecto.Changeset.change/1)
changeset = container
|> Ecto.Changeset.change
|> Ecto.Changeset.put_assoc(:items, item_changesets)
IO.inspect(changeset) #TODO remove
if changeset.valid? do
Repo.update(changeset)
conn
|> put_flash(:info, "Item added.")
|> redirect(to: container_path(conn, :show, container))
else
render(conn, "show.html", container: container, changeset: changeset)
end
end

Resources