Updating unpermitted values in an Ecto changeset - phoenix-framework

I want to update some meta data that is not a permitted attribute in my schema's changeset:
def changeset(%Comment{} = comment, attrs) do
comment
|> cast(attrs, [:text])
|> validate_required([:text])
end
And then something like:
changeset = Comment.changeset(commet, %{under_moderation: true})
Repo.update(changeset)
Since under_moderation is not whitelisted, it gets ignored. What options do I have to force the update? If there are multiple options, is there a convention?

I would just create another changeset function that has the rights to set the value.
def admin_changeset(%Comment{} = comment, attrs) do
comment
|> cast(attrs, [:text, :under_moderation])
|> validate_required([:text])
end
Then simply use that to update the value. As you can see, I named it admin_changeset because it seems like this is a value that would be set by an admin. In your controller or context module, simply check the user role (if you have something like that) and then decide which changeset function you want to use.

Related

rails string substitution or similar solution in controller

I'm building a site with users in all 50 states. We need to display information for each user that is specific to their situation, e.g., the number of events they completed in that state. Each state's view (a partial) displays state-specific information and, therefore, relies upon state-specific calculations in a state-specific model. We'd like to do something similar to this:
##{user.state} = #{user.state.capitalize}.new(current_user)
in the users_controller instead of
#illinois = Illinois.new(current_user) if (#user.state == 'illinois')
.... [and the remaining 49 states]
#wisconsin = Wisconsin.new(current_user) if (#user.state == 'wisconsin')
to trigger the Illinois.rb model and, in turn, drive the view defined in the users_controller by
def user_state_view
#user = current_user
#events = Event.all
#illinois = Illinois.new(current_user) if (#user.state == 'illinois')
end
I'm struggling to find a better way to do this / refactor it. Thanks!
I would avoid dynamically defining instance variables if you can help it. It can be done with instance_variable_set but it's unnecessary. There's no reason you need to define the variable as #illinois instead of just #user_state or something like that. Here is one way to do it.
First make a static list of states:
def states
%{wisconsin arkansas new_york etc}
end
then make a dictionary which maps those states to their classes:
def state_classes
states.reduce({}) do |memo, state|
memo[state] = state.camelize.constantize
memo
end
end
# = { 'illinois' => Illinois, 'wisconsin' => Wisconsin, 'new_york' => NewYork, etc }
It's important that you hard-code a list of state identifiers somewhere, because it's not a good practice to pass arbitrary values to contantize.
Then instantiating the correct class is a breeze:
#user_state = state_classes[#user.state].new(current_user)
there are definitely other ways to do this (for example, it could be added on the model layer instead)

Why Changeset.change is skipping validation in Elixir?

This is a simple function used to insert or update some data.
If user data is already in db i simply update it, otherwise i insert a new row with data. Everything works fine but i have a problem with validation.
Changeset definition:
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:name, :surname, :user_id])
|> validate_required([:name, :surname, :user_id])
|> unique_constraint(:user_id)
end
validate_required is currently working only during insert and not during update.
def add_or_change(user_id, new_data) do
data_from_db = data_by_user_id (user_id)
case data_from_db do
nil ->
Data.changeset(%Data{}, new_data)
|> Repo.insert()
_ ->
Changeset.change(data_from_db, new_data)
|> Repo.update()
end
end
If i try to insert "" as :name value, i get an error (can't be blank) as expected. However if i'm updating an existing row with "" as :name value, changeset does not pass through validation and my db is updated improperly. How to force validation also on change, before Repo.update()??
According to the doc: Ecto.Changeset/2 is meant for internal data changes, so it bypasses validations:
The function is meant for working with data internal to the application. Because of that neither validation nor casting is performed. This means change/2 expects the keys in the changes map or keyword to be atoms.
You should use Ecto.Changeset.cast/4 to apply the validations, and then update if it is valid.
Don't use this:
Changeset.change(data_from_db, new_data)
Just run the same function you were already using:
Data.changeset(data_from_db, new_data)
By the way, you can actually simplify this function a lot:
def add_or_change(user_id, new_data) do
(data_by_user_id(user_id) || %Data{})
|> Data.changeset(new_data)
|> Repo.insert_or_update()
end

How to use polymorphism to remove a switch statement which compares strings?

I am new to Ruby, so let me describe the context of my problem first:
I have a json as input which has the following key / value pair:
{
"service": "update"
}
The value has many different values for example: insert,delete etc.
Next there is a method x which handles the different requests:
def x(input)
case input[:service]
services = GenericService.new
when "update"
result = services.service(UpdateService.new,input)
when "insert"
result = services.service(InsertService.new,input)
when "delete"
result = services.service(DeleteService.new,input)
....
....
else
raise "Unknown service"
end
puts JSON.pretty_generate(result)
end
What is bothering me is that I still need to use a switch statement to check the String values (reminds me of 'instance of' ugh..). Is there a cleaner way (not need to use a switch)?
Finally I tried to search for an answer to my question and did not succeed, if however I missed it feel free to comment the related question.
Update: I was thinking to maybe cast the string to the related class name as follows: How do I create a class instance from a string name in ruby? and then call result = services.services(x.constantize.new,input) , then the class names ofcourse needs to match the input of the json.
You can try something like:
def x(input)
service_class_name = "#{input[:service].capitalize}Service"
service_class = Kernel.const_get(service_class_name)
service_class.new(input).process
end
In addition you might want to check if this is a valid Service class name at all.
I don't understand why you want to pass the service to GenericService this seems strange. let the service do it's job.
If you're trying to instatiate a class by it's name you're actually speaking about Reflection rather than Polymorphism.
In Ruby you can achieve this in this way:
byName = Object.const_get('YourClassName')
or if you are in a Rails app
byName= 'YourClassName'.constantize
Hope this helps
Just first thoughts, but you can do:
eval(services.service("#{input[:service].capitalize}Service.new, #{input})") if valid_service? input[:service]
def valid_service?
w%(delete update insert).include? input[:service]
end
As folks will no doubt shout, eval needs to be used with alot of care

Create an object if one is not found

How do I create an object if one is not found? This is the query I was running:
#event_object = #event_entry.event_objects.find_all_by_plantype('dog')
and I was trying this:
#event_object = EventObject.new unless #event_entry.event_objects.find_all_by_plantype('dog')
but that does not seem to work. I know I'm missing something very simple like normal :( Thanks for any help!!! :)
find_all style methods return an array of matching records. That is an empty array if no matching records are found. And an empty is truthy. Which means:
arr = []
if arr
puts 'arr is considered turthy!' # this line will execute
end
Also, the dynamic finder methods (like find_by_whatever) are officially depreacted So you shouldn't be using them.
You probably want something more like:
#event_object = #event_entry.event_objects.where(plantype: 'dog').first || EventObject.new
But you can also configure the event object better, since you obviously want it to belong to #event_entry.
#event_object = #event_entry.event_objects.where(plantype: 'dog').first
#event_object ||= #event_entry.event_objects.build(plantype: dog)
In this last example, we try to find an existing object by getting an array of matching records and asking for the first item. If there are no items, #event_object will be nil.
Then we use the ||= operator that says "assign the value on the right if this is currently set to a falsy value". And nil is falsy. So if it's nil we can build the object form the association it should belong to. And we can preset it's attributes while we are at it.
Why not use built in query methods like find_or_create_by or find_or_initialize_by
#event_object = #event_entry.event_objects.find_or_create_by(plantype:'dog')
This will find an #event_entry.event_object with plantype = 'dog' if one does not exist it will then create one instead.
find_or_initialize_by is probably more what you want as it will leave #event_object in an unsaved state with just the association and plantype set
#event_object = #event_entry.event_objects.find_or_initialize_by(plantype:'dog')
This assumes you are looking for a single event_object as it will return the first one it finds with plantype = 'dog'. If more than 1 event_object can have the plantype ='dog' within the #event_entry scope then this might not be the best solution but it seems to fit with your description.

Can I add params values to a hash?

I have a User model. I also have a form_for(#user...) form. This form spans 3 partials. In order for every partial to remember values I use the following command inside my create action in my UsersController:
session[:user_params].deep_merge!(params[:user]) if params[:user]
This way every partial adds params[:user] to session[:user_params]. I also have other form values stored inside the params hash which are not part of the User model. Is there a command which would allow me to add all single params values (not just the :user hash) to the session[:user_params] hash without adding every single value one by one like this:
session[:num_children] = params[:num_children] if params[:num_children]
...etc...
Try:
params.each {|key,value| session.deep_merge!(key=>value)}

Resources