Error when user is not signed in : function nil.id/0 is undefined - phoenix-framework

I want to hide edit and delete buttons from users that didnt created topic.
I connected user_id with topics table and that works good.
When user make a new topic his id is in topic user_id.
But now when user is not logged in %{user: nil} i get an error:
function nil.id/0 is undefined
require_auth.ex
def call(conn , _params) do
if conn.assigns[:user] do
conn
else
conn
|> put_flash(:error, "You must be logged in.")
|> redirect(to: ZivotWeb.Router.Helpers.topic_path(conn, :index))
|> halt()
end
set_user.ex
def call(conn, _params) do
user_id = get_session(conn, :user_id)
cond do
user = user_id && Repo.get(User, user_id) ->
assign(conn, :user, user)
true ->
assign(conn, :user, nil)
end
end
I dont know if u need it but there is auth_controller.ex
def callback(%{assigns: %{ueberauth_auth: auth}} = conn ,params) do
user_params = %{token: auth.credentials.token, email: auth.info.email, provider: "github"}
changeset = Zivot.User.changeset(%User{}, user_params)
signin(conn,changeset)
end
def signout(conn,_params) do
conn
|> configure_session(drop: true)
|> put_flash(:info, "you successfully Signed Out!")
|> redirect(to: Routes.topic_path(conn, :index))
end
defp signin(conn, changeset) do
case insert_or_update_user(changeset) do
{:ok, user} ->
conn
|> put_flash(:info, "Welcome back!")
|> put_session(:user_id, user.id)
|> redirect(to: Routes.topic_path(conn, :index))
{:error, _reason} ->
conn
|> put_flash(:error, "Error signing in")
|> redirect(to: Routes.topic_path(conn, :index))
end
end
defp insert_or_update_user(changeset) do
case Repo.get_by(User, email: changeset.changes.email) do
nil ->
Repo.insert(changeset)
user -> {:ok, user}
end
end

Related

How do I create a single controller for two models?

I have two controllers for my MVC structure project. One is going to show a list of expenses and other lists of open sources but I have two different routes for that. How do I create a controller for just one view which will show both lists of expenses and open sources from just one controller?
What's the best way to solve this problem?
defmodule DashboardtaskWeb.OpensourceController do
use DashboardtaskWeb, :controller
alias Dashboardtask.Task
alias Dashboardtask.Task.Opensource
def index(conn, _params) do
opensources = Task.list_opensources()
render(conn, "index.html", opensources: opensources)
end
def new(conn, _params) do
changeset = Task.change_opensource(%Opensource{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"opensource" => opensource_params}) do
case Task.create_opensource(opensource_params) do
{:ok, opensource} ->
conn
|> put_flash(:info, "Opensource created successfully.")
|> redirect(to: Routes.opensource_path(conn, :show, opensource))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
def show(conn, %{"id" => id}) do
opensource = Task.get_opensource!(id)
render(conn, "show.html", opensource: opensource)
end
def edit(conn, %{"id" => id}) do
opensource = Task.get_opensource!(id)
changeset = Task.change_opensource(opensource)
render(conn, "edit.html", opensource: opensource, changeset: changeset)
end
def update(conn, %{"id" => id, "opensource" => opensource_params}) do
opensource = Task.get_opensource!(id)
case Task.update_opensource(opensource, opensource_params) do
{:ok, opensource} ->
conn
|> put_flash(:info, "Opensource updated successfully.")
|> redirect(to: Routes.opensource_path(conn, :show, opensource))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "edit.html", opensource: opensource, changeset: changeset)
end
end
def delete(conn, %{"id" => id}) do
opensource = Task.get_opensource!(id)
{:ok, _opensource} = Task.delete_opensource(opensource)
conn
|> put_flash(:info, "Opensource deleted successfully.")
|> redirect(to: Routes.opensource_path(conn, :index))
end
end
For expenses controller, it's the same. I was thinking to use polymorphic association for this. Do you think it's the best idea?

Phoenix: changeset does not take into account changes on foreign keys

I am trying to update values for a record in the console ies -S mix .
iex> video = Repo.one(from v in Video, limit: 1)
%Rumbl.Video{...}
if I change the title of the video, everything seems to be working correctly.
iex> changeset = Video.changeset(video, %{title: "some title"})
#Ecto.Changeset<action: nil, changes: %{title: "some title"},
errors: [], data: #Rumbl.Video<>, valid?: true>
But changing a foreign key seems to have no effect:
iex> changeset = Video.changeset(video, %{category_id: 3})
#Ecto.Changeset<action: nil, changes: %{},
errors: [], data: #Rumbl.Video<>, valid?: true>
What should I do to for the changes on the foreign key to be taken into accoung ?
Here is the model
defmodule Rumbl.Video do
use Rumbl.Web, :model
schema "videos" do
field :url, :string
field :title, :string
field :description, :string
belongs_to :user, Rumbl.User, foreign_key: :user_id
belongs_to :category, Rumbl.Category, foreign_key: :category_id
timestamps()
end
#required_fields ~w(url title description)
#optional_fields ~w(category_id)
#doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, #required_fields, #optional_fields)
|> validate_required([:url, :title, :description])
|> assoc_constraint(:category)
end
end
In Ecto 2.2, the fourth argument to cast is opts, not optional fields. It used to be optional fields earlier which was deprecated in v2.1 with a recommendation to use validate_required instead. This was apparently removed in v2.2.0 although I can't find it in the changelog. You should change your code to this for Ecto 2.2:
struct
|> cast(params, #required_fields ++ #optional_fields)
|> validate_required([:url, :title, :description])
or do this:
#required_fields ~w(url title description)a
#optional_fields ~w(category_id)a
and
|> cast(params, #required_fields ++ #optional_fields)
|> validate_required(#required_fields)

Zito.Register. __struct__/1 is undefined cannot expand struct

i get the following error whenever i run the mix phoenix.server command. Below is my user.ex model, register_controller.ex and my repo files. Seems to have a problem with my model but i cant really figure what it is.
user.ex
defmodule Zito.User do
use Zito.Web, :model
schema "users" do
field :email, :string
field :crypted_password, :string
timestamps()
end
#doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:email, :crypted_password])
|> validate_required([:email, :crypted_password])
|> validate_format(:email, ~r/#/)
|> validate_length(:crypted_password, min: 8)
|> validate_length(:crypted_password, max: 16)
end
end
(code)
register_controller.ex
defmodule Zito.RegisterController do
use Zito.Web, :controller
alias Zito.Register
def create(conn, %{"register" => register_params}) do
changeset = Register.changeset(%Register{}, register_params)
case Zito.Register.create(changeset, Zito.Repo) do
{:ok, changeset} ->
conn
|> put_flash(:info, "Your account was created")
|> redirect(to: "/")
{:error, changeset} ->
conn
|> put_flash(:info, "Unable to create account")
|> render("register.html", changeset: changeset)
end
end
end
and my repo file
defmodule Zito.Repo.Migrations.CreateUser do
use Ecto.Migration
def change do
create table(:users) do
add :email, :string
add :crypted_password, :string
timestamps()
end
create unique_index(:users, [:email])
end
end

Put constraint on PhoenixFramework changeset

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.

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