Code duplication in Elixir and Ecto - ecto

I want to compose a module out of smaller modules.
This is a module I have right now:
defmodule Api.Product do
use Ecto.Schema
import Ecto.Changeset
import Api.Repo
import Ecto.Query
#derive {Poison.Encoder, only: [:name, :brand, :description, :image, :rating, :number_of_votes]}
schema "products" do
field :name, :string
field :brand, :string
field :description, :string
field :image, :string
field :rating, :integer
field :number_of_votes, :integer
field :not_vegan_count, :integer
end
def changeset(product, params \\ %{}) do
product
|> cast(params, [:name, :brand, :description, :image, :rating, :number_of_votes, :not_vegan_count])
|> validate_required([:name, :description, :brand])
|> unique_constraint(:brand, name: :unique_product)
end
def delete_all_from_products do
from(Api.Product) |> delete_all
end
def insert_product(conn, product) do
changeset = Api.Product.changeset(%Api.Product{}, product)
errors = changeset.errors
valid = changeset.valid?
case insert(changeset) do
{:ok, product} ->
{:success, product}
{:error, changeset} ->
{:error, changeset}
end
end
def get_product_by_name_and_brand(name, brand) do
Api.Product |> Ecto.Query.where(name: ^name) |> Ecto.Query.where(brand: ^brand) |> all
end
def get_products do
Api.Product |> all
end
end
But I want to have different things other than Product which all have most of the same fields as Product except for brand. Therefore is it best to create a module which has all fields except brand and then all the modules containing those fields have that module as a field?
Here is my module that all modules would contain:
defmodule Api.VeganThing do
use Ecto.Schema
import Ecto.Changeset
import Api.Repo
import Ecto.Query
#derive {Poison.Encoder, only: [:name, :description, :image, :rating, :number_of_votes]}
schema "vegan_things" do
field :name, :string
field :description, :string
field :image, :string
field :rating, :integer
field :number_of_votes, :integer
field :not_vegan_count, :integer
end
end
There will be no database table for vegan_things. But a few different modules which do have database tables will contain a vegan_thing.
Is this a good way to avoid the code duplication of rewriting every field in every module in Elixir?
Here is my current changeset:
defmodule Api.Repo.Migrations.CreateProducts do
use Ecto.Migration
def change do
create table(:products) do
add :name, :string
add :brand, :string
add :description, :string
add :image, :string
add :rating, :integer
add :number_of_votes, :integer
add :not_vegan_count, :integer
end
create unique_index(:products, [:name, :brand], name: :unique_product)
end
end
So I'm basing the uniqueness on a field which would be in vegan_thing and a field which is only in product. Can I do something like this?
defmodule Api.Repo.Migrations.CreateProducts do
use Ecto.Migration
def change do
create table(:products) do
add :name, :string
add :vegan_thing, :vegan_thing
end
create unique_index(:products, [:vegan_thing.name, :brand], name: :unique_product)
end
end
Or do I have to put the name field directly in product? instead of vegan_thing to be able to use it as a unique constraint?

Macros can be used for this situation:
defmodule Vegan do
defmacro vegan_schema name, fields do
quote do
schema unquote(name) do
unquote(fields)
field :name, :string
field :description, :string
field :image, :string
field :rating, :integer
field :number_of_votes, :integer
field :not_vegan_count, :integer
end
end
end
def changeset(struct_or_changeset, params) do
struct_or_changeset
|> Ecto.Changeset.cast(params, [:name, :description, :rating])
|> Ecto.Changeset.validate_required([:name, :description])
end
end
defmodule Product do
use Ecto.Schema
require Vegan
#derive {Poison.Encoder, only: [:name, :brand, :description, :image, :rating, :number_of_votes]}
Vegan.vegan_schema "products" do
field :brand, :string
end
def changeset(params) do
%Product{}
|> Vegan.changeset(params)
|> Ecto.Changeset.cast(params, [:brand])
|> Ecto.Changeset.validate_required([:brand])
end
end
For other functions dealing with Ecto.Changeset, then regular modules and functions should be fine for factoring out any duplicated code, as shown in the example above where Product.changeset/1 calls Vegan.changeset/2 to cast and validate the common fields.

Related

Customising just a single property in a Resource

I have a simple Rails Model which has several columns in the database. I then have an ActiveAdmin Resource for this model and by default it renders me a nice index, show and form views with all of the columns and I'm able to do all the standard CRUD operations.
Is it possible to customise one of the columns without overwriting index, show and form functions and writing out all of the individual columns again?
For example if I want to change how a single column is rendered I know I can do this:
ActiveAdmin.register Product do
permit_params :name, :description, :price, :weight
index do
selectable_column
column :id
column :name
column :description
column :price do |f|
text_field "#{f.price} EUR"
end
end
show do
row :id
row :name
row :description
row :price do |f|
text_field "#{f.price} EUR"
end
row :weight
end
end
But I would instead like to do something like this:
ActiveAdmin.register Product do
permit_params :name, :description, :price, :weight
# param_view is (to my knowlege) an imaginary method
param_view :price do |p|
text_field "#{f.price} EUR"
end
end
Is this somehow possible? What about just hiding a single property?

How to add prefix to `join_through` table in `many_to_many` `assoc` at the Repo.*() level?

I'm trying query a many_to_many relationship with a prefix as follows:
Student
|> join(:left, [s], t in assoc(s, :teachers))
|> Repo.all(prefix: "my_prefix")
which results in a PostgreSQL query:
SELECT s0."id", s0."name", s0."inserted_at", s0."updated_at"
FROM "my_prefix"."students" AS s0
LEFT OUTER JOIN "teachers_students" AS t2 ON t2."student_id" = s0."id"
LEFT OUTER JOIN "my_prefix"."teachers" AS t1 ON t2."teacher_id" = t1."id"
I would expect the prefix to get added to the join_through table teacher_students, but it doesn't get added. Is this a bug in Ecto? Or is there a workaround for this?
Looks like I was able to fix the problem by changing the join_through value in the many_to_many definition from a string to a module name:
schema "students" do
# many_to_many :teachers, Teacher, join_through: "teachers_students", on_replace: :delete
many_to_many :teachers, Teacher, join_through: TeachersStudents, on_replace: :delete
...
end
defmodule MyApp.TeachersStudents do
use Ecto.Schema
alias MyApp.Teachers.Teacher
alias MyApp.Students.Student
schema "teachers_students" do
belongs_to :teacher, Teacher
belongs_to :student, Student
end
end
Ecto ignoring the prefix for the string value may be a bug.

make column of associated resource in index table non-linked

Okay, using ActiveAdmin (0.6.3) and am able to use the following code to get a caller's location attribute to appear in the table of callers. The name of each location appears as a link to the "show" action for the location. The "show" action and result is not useful for my application. I want to remove the link but keep the text. Help? Thanks :)
ActiveAdmin.register Caller do
index do
column 'Location', :location, :sortable => 'locations.name'
end
controller do
def scoped_collection
resource_class.includes(:location)
end
end
end
class Caller < ActiveRecord::Base
attr_accessible :active, :assignedname, :callingnumber, :description, :location_id, :lookupcount, :lastlookuptime
belongs_to :location
end
class Location < ActiveRecord::Base
attr_accessible :active, :description, :name, :callers_attributes
has_many :callers, dependent: :destroy
end
Try this.
index do
column 'Location', :sortable => 'locations.name' do |caller|
caller.location.name
end
end

ActiveAdmin won't save has many and belongs to many field

I have 2 models. Category and Post. They are connected using a has_many_and_belongs_to_many relationship. I checked in the rails console and the relationship works.
I created checkboxes in activeadmin to set the post categories using this form field:
f.input :categories, as: :check_boxes, collection: Category.all
The problem is when I try to save it because every other field data (title, body, meta infos etc.) is saved, but the category stays the same even if I unchecked it, or checked another too.
I am using strong parameters like this:
post_params = params.require(:post).permit(:title,:body,:meta_keywords,:meta_description,:excerpt,:image,:categories)
Please give me some suggestions to make active admin save the categories too!
Best Wishes,
Matt
Try this in AA:
controller do
def permitted_params
params.permit post: [:title, :body, :meta_keywords, :meta_description, :excerpt, :image, category_ids: []]
end
end
Put something like this in /app/admin/post.rb:
ActiveAdmin.register Post do
permit_params :title, :body, :meta_keywords, :meta_description, :excerpt, :image, category_ids: [:id]
end
If you are using accepts_nested_attributes_for then it would look like this:
ActiveAdmin.register Post do
permit_params :title, :body, :meta_keywords, :meta_description, :excerpt, :image, categories_attributes: [:id]
end
I've tested, this might works for you and others as well
# This is to show you the form field section
form do |f|
f.inputs "Basic Information" do
f.input :categories, :multiple => true, as: :check_boxes, :collection => Category.all
end
f.actions
end
# This is the place to write the controller and you don't need to add any path in routes.rb
controller do
def update
post = Post.find(params[:id])
post.categories.delete_all
categories = params[:post][:category_ids]
categories.shift
categories.each do |category_id|
post.categories << Category.find(category_id.to_i)
end
redirect_to resource_path(post)
end
end
Remember to permit the attributes if you're using strong parameters as well (see zarazan answer above :D)
References taken from http://rails.hasbrains.org/questions/369

rails_admin searchable association

I am using rails_admin together with globalize3 and cannot get searchable associations to work. Here are the models (Person has_one/belongs_to Name has_many/belongs_to NameTranslation):
class Person < ActiveRecord::Base
has_one :name, inverse_of: :person
end
class Name < ActiveRecord::Base
belongs_to :person, inverse_of: :name
translates :first_name, :last_name
has_many :name_translations, inverse_of: :name, dependent: :destroy
end
class NameTranslation < ActiveRecord::Base
belongs_to :name, inverse_of: :name_translations
end
The NameTranslation model is coming from globalize3, it contains the same attributes as name (first_name and last_name) plus locale and name_id,.
In config/initializers/rails_admin.rb I have
config.model Person do
list do
field :name do
searchable name_translations: :last_name
end
end
end
Then, in the GUI, when I add a filter on name, I get:
SQLite3::SQLException: no such column: name_translations.last_name: SELECT "people".* FROM "people" WHERE (((name_translations.last_name LIKE '%freud%'))) ORDER BY people.id desc LIMIT 20 OFFSET 0
Obviously, rails_admin is looking for a column named name_translations.last_name in people instead of joining/including names and name_translations - why?
What I need rails_admin to do is this, working in irb:
>> Person.joins( name: :name_translations ).where('name_translations.last_name like "test"')
which generates the following SQL:
SELECT "people".* FROM "people" INNER JOIN "names" ON "names"."person_id" = "people"."id" INNER JOIN "name_translations" ON "name_translations"."name_id" = "names"."id" WHERE (name_translations.last_name like "test")
Can this be done in rails_admin? Thanks for your help...
From this thread, I followed Nick Roosevelt's suggestion and it worked for my case
class Room < ActiveRecord:Base
has_many :time_slots
end
class TimeSlot < ActiveRecord::Base
belongs_to :room
rails_admin do
list do
field :day do
searchable true
end
# field :room do
# searchable room: :name
# end
field :room do
searchable [{Room => :name}]
queryable true
end
end
end
end
I tried searchable room: :name and it was not working, but searchable [{Room => :name}] seem to make it work.
I had a similar problem with a has one relationship.
The way I solved it was to set a default_scope on the model and join it with the associated table (it is was the only way I could get rails admin to join these two tables).
I also had to set queryable true on the associated field.
Imagine that you had to search only inside the name association, then here's how it would work:
class Person < ActiveRecord::Base
has_one :name, inverse_of: :person
default_scope { eager_load(:name) }
end
config.model Person do
list do
field :name do
queryable true
searchable [:column1, :column2, ..]
end
end
end
However, you need to search through the has many association and I don't know whether that approach would still work, but here's a guess:
class Person < ActiveRecord::Base
has_one :name, inverse_of: :person
has_many :name_translations, through: :name
default_scope { eager_load(:name_translations) }
end
config.model Person do
list do
field :name_translations do
queryable true
searchable :last_name
end
end
end

Resources