5

So I have some data that gets pulled from another rails app in a controller lets call it ExampleController and I want to validate it as being there in my model before allowing the wizard to move to its next step and I can't quite figure out how I should be doing it (I know that getting this data directly from the controller into the model violates MVC I am looking for the best workaround to get my data from the controller) . The data must come from the controller as the methods for getting it are contained in ApplicationController however I could do this in the Awizard controller if this is easier. (Also I cannot use a gem)

Please offer some kind of suggestion to the problem and not an explanation of why this is not the correct way to do things I realise that already but cannot do it another way.


The Example Controller

should this instead render the data then check it isn't blank elsewhere?

class ExampleController < ApplicationController

  def valid_data?            
    data = #data could be nil or not
    if data.blank?
      return false
    else
      return true
  end

end

My Model - (models/awizard.rb)

How do I use the valid_data? method from the example controller? in my validation here.

class AWizard
include ActiveModel::Validations
include ActiveModel::Conversion
include ActiveModel::Dirty
include ActiveModel::Naming

#This class is used to manage the wizard steps using ActiveModel (not ActiveRecord)

attr_accessor :id
attr_writer :current_step  #used to write to current step
define_attribute_methods [:current_step] #used for marking change

validate :first_step_data, :if => lambda { |o| o.current_step == "step1" };

def first_step_data
  #What should i put here to check the valid_data? from the examplecontroller
end

def initialize(attributes = {})
   attributes.each do |name, value|
     send("#{name}=", value)
   end
end

def current_step
  @current_step || steps.first
end

def steps
  %w[step1 step2 step3] #make list of steps (partials)
end

def next_step
  current_step_will_change! #mark changed when moving stepped
  self.current_step = steps[steps.index(current_step)+1] unless last_step?
end

def previous_step
  current_step_will_change! #mark changed when moving stepped
  self.current_step = steps[steps.index(current_step)-1] unless first_step?
end

def first_step?
  current_step == steps.first
end

def last_step?
  current_step == steps.last
end

def all_valid?
  steps.all? do |step|
    self.current_step = step
    valid?
  end
end

def step(val)
  current_step_will_change!
  self.current_step = steps[val]
end

def persisted?
  self.id == 1
end

end

Or do I need to add this to this view?

(/views/awizard/_step1.html.erb)

<div class="field">
  <%= f.label 'Step1' %><br />
  #This is the step I want to validate
</div>

Jesse Whitham
  • 824
  • 9
  • 32
  • 3
    I feel like you could simplify this question quite a bit. But you should never (can't) call a controller method from a model. Why is that method in the controller anyhow? I think you'll benefit from showing actual code. From what I can tell from what you've shown here, I'd just move that controller method into the model. You might also check out https://github.com/schneems/wicked – 99miles Jan 14 '13 at 05:16
  • Yeah i did try simplify this before but the answers i got back didn't really get what i was trying to do. I can't show the actual code as its for work. I could try move that method into the model but not sure it would still work with getting the data from the other rails app will give this a go. Can't use a gem for the wizard but will take a look at the code see if i can pick up any tips. Thanks for your suggestions. (Yea i did wonder about the controller model thing) – Jesse Whitham Jan 14 '13 at 06:11
  • So I have tried moving my method from the controller to the method but the problem is it requires various stuff from application_controller.rb I am really looking for a way to get this data in my model even if it has to be given to a route i.e example/data.json and taken from there? – Jesse Whitham Jan 14 '13 at 21:58
  • +1 for @99miles's comment about calling a controller method from within a model. If that validation defines the model, then that code should be in the model. It seems like perhaps you're fighting the framework and that might be a source of your frustration. What is it that you're trying to accomplish? – Tass Jan 15 '13 at 15:30
  • I have to have the logic to request the data in the controller is basically my problem I realize getting the data from the controller in the model violates MVC but I can't move it, so I am looking for the most elegant way to check that data in the model from the controller basically. – Jesse Whitham Jan 15 '13 at 20:01
  • If your model is trying to validate itself using code from the controller, and that code relies upon lots of other controller code, then it seems you have major architectural problems. It sounds like you have way too much logic in the controllers in general that should be moved to models, not just this one method. – Ben Lee Jan 15 '13 at 21:39
  • The extra controller logic comes around security concerns, this is a very large multi tier app, there is not a large amount of code in the controller, but there is some in the application controller that is needed unless I somehow moved this out into a specific model to be required in the other models, I am looking for the best way to do what I wan't to do I realise its not the standard way to do it. – Jesse Whitham Jan 15 '13 at 21:53
  • Where is the model being called from? – Matt Glover Jan 16 '13 at 05:27
  • Another controller, i removed the code as it seemed to be of no real use for the question. – Jesse Whitham Jan 16 '13 at 06:11

4 Answers4

2

I maybe have misunderstood the question since my answer is simple. However here's a solution that doesn't resort to metaprogramming, but to the fact that Wizard (the class not objects it creates ) is a singleton/constant.

class ExampleController < ApplicationController

  def valid_data?            
    data = #data could be nil or not
    result = data.blank?
    Awizard.valid_data= result
    result
  end

end

class Wizard
  cattr_accessor :valid_data


  def valid_data?
    self.class.valid_data
  end
end

If course ExampleController#valid_data must have been called before you play around with a Wizard passing step_one.

UPDATE:Reasoning about the global state problem

(raised by @Valery Kvon)

The argument is that Wizard is global to the application and that @wizard instances will be dependant on a global state and are therefore badly encapsulated. But Data, coming from another site, is gloabl in the scope of your app. So there's no mismatch with Wizard beeing the one holding the data. On the contrary it can be considered as a feature.

One example. Wizards magic is only efficient at full moon. Application SkyReport sends data :

:full_moon => true

It affects all wizards in stage 1 if they need to go on step2 of their power. Therefore relying on the global state of Wizard.valid_data? is exactly what we want...

However if each wizard has a personal message coming from Gandalf's application, then we'll want to inforce the invocation of Gandalf's data but then the solution is even simpler :

# in example_controller.rb
before_filter :set_wizard_data, :only => [:create, :update]
....
def set_wizard_data
  @wizard = Wizard.find params[:id]
  @wizard.valid_data= valid_data
end

But this again implies that Gandalf.app knows (something of) the @wizard and from how the problem is presented, data coming from the other site is pretty agnostic !

The issue here is that we don't know enough about the app, its requirements and underlying logic to decide what's good or not...

charlysisto
  • 3,700
  • 17
  • 30
  • Agree with the last sentence :) But anyway, how do we know that data, coming from another site is global in the scope of app? And the second: EVEN if it is global, using global variables to store session specific data - is extremely bad idea. If you have to work with global conditions, limit this work on a controller (request) level and dont touch the model. And the third: in your case with the full moon... How do you think is it normal if the moon has just became at 'full' state, but the application (Wizard class) still think its not? – Valery Kvon Jan 17 '13 at 13:36
  • The premises here are that the only way to access the sate of ExtApp is going through the controller request, so the 'truth' of it's state comes from the last request (remember instances can't ask for the extapp data). So when Wizard stores it, the moon is full & will keep being full (although ExtApp may change just after) & all it's instances know about. Did the moon shrink meanwhile & wizard is passing step1 ? We don't care because @wizard can't ask, we need to go through the controller request... That makes the usage of Wizard consistent I believe. – charlysisto Jan 17 '13 at 15:36
  • Thanks @charlysisto this is exactly what I was thinking sorry but I could not add the code about the data for security requirements but problem is solved. – Jesse Whitham Jan 17 '13 at 21:02
  • speculating about the unknown was an interesting challenge. Also @ValeryKvon raised issues I hadn't thought of and forced me into thinking through the problem. Very stimulating – charlysisto Jan 17 '13 at 22:46
1

The only way to share controller level data with model is through external accessor. Using metaprogramming you can trick the way to pass it to a model instance.

controller

def valid_data?            
  data = #data could be nil or not
  result = data.blank? ? false : true
  instance_eval <<-EOV
    def AWizard.new(*args)
      super(*args).tap {|aw| aw.external_valid = #{result}}
    end
  EOV
  result
end

model

class AWizard
  attr_accessor :external_valid

  def initialize(attributes = {})
    attributes.each do |name, value|
      send("#{name}=", value)
    end
  end

  validate :first_step_data, :if => lambda { |o| o.current_step == "step1" };

  def first_step_data
    # :external_valid would be true or false according to a valid_data?. Nil would be if valid_data? has not been called
    if external_valid == false
      errors.add ...
    end
  end
end
Valery Kvon
  • 4,438
  • 1
  • 20
  • 15
  • Hi thanks for this answer there was a lot of good points in this although as @charlysisto has a better solution I beleive. – Jesse Whitham Jan 16 '13 at 22:39
  • Wrong. Because when you polute the class itself It will share validation data between all requests to your application. In my solution you implement ad hoc singleton method and validation data is unique for the current request only. – Valery Kvon Jan 17 '13 at 00:07
  • I'm sorry but you are going to have to go into that a little more, could you give me an example of the problems that would occur from using the suggested answer? By the way each model i.e `Awizard` would be **unique** for the various wizards in the app i.e `class TestWizard < Awizard` as the steps for the wizards need to be loaded into this. Or are you talking about the controller? as this will also be **unique** to the wizard. – Jesse Whitham Jan 17 '13 at 00:27
  • 1
    Wizard class is a common, application-wide constant. You assign value (validation data) to a class level attribute (variable actually), since that all another calls for this class and all its instances will see these data. Now Imagine you send a request and get valid data as "false", your Wizard valid data is "false"... Someone other is doing request at the same time reading validation data from his object, which could be true, but since you modified it, it is false now. His validation fails. – Valery Kvon Jan 17 '13 at 01:11
  • +1 for additional info @charlysisto seemed to say wizard the class was a singleton/constant. Will do some testing and see if what you say is true and happens and if so then your answer would seem the better of the two. – Jesse Whitham Jan 17 '13 at 02:31
  • Try it, especially in production mode (when classes are being cached :) Anyway the only guarantee that the _instance_ data will remain within the _instance_ objects' space is the _instance_ variable, not the class variable. – Valery Kvon Jan 17 '13 at 04:39
  • @ValeryKvon see my updated post, it was a little too long to fit in a comment. – charlysisto Jan 17 '13 at 12:50
0

You can pass the data from the controller as a parameter to the validation method in the model.

In your models/awizard.rb

def valid_for_step_one?(some_external_data)
  #validation logic here
end

So that in your controller, you can can call:

model.valid_for_step_one?(data_from_controller)

It would also be good if you can give a description of the data you are getting from the controller. How is it related to the model awizard?

Because another option is to set the external data as an attribute of the model. Then you can use it in your validation functions from there.

Heinrich Lee Yu
  • 1,472
  • 15
  • 31
0

So I tried to edit and add to the @charlysisto question as this was closest to the answer but it did not work so here is the solution I used, as suggested the answer was to send the data from the controller to the model (Although the answers left out using the view to call the controller method) here is my solution

Model - models/awizard.rb

class Awizard
  include ActiveModel::Validations

  cattr_accessor :valid_data

  validate :data_validation :if => lambda { |o| o.current_step == "step1" }

  def data_validation
    if self.valid_data == false || self.valid_data.blank?
      errors.add(:valid_data, "not found")
    end
  end

  #Other wizard stuff

end

View - awizard/_step1.html.erb

<div class="field">
  <% f.label "valid data? %>
  <% @_controller.valid_data %> #Call controller method to send data to model
</div>

Controller

class AwizardController < ApplicationController

  def valid_data
    data = #data from elsewhere
    if !data.blank?
      Awizard.valid_data = true
    else
      Awizard.valid_data = false
  end

end
Jesse Whitham
  • 824
  • 9
  • 32