I am building a new app in Rails for an internal project that changes slightly each year based on the requirements of our clients. Any changes between years will occur within the models (add/remove columns, formatting, reports, etc). My plan is to build it to the requirements for this year and going forward each year I will create a new model and migration (e.g. Sample2019Record, Sample2020Record) that will encapsulate the requirements for that year. The app also needs to render previous year data and all the data is scoped based on the year meaning there is no need to render or query multiple years data. I would prefer not to create a new app each year since that is more apps that need to be maintained.
So my idea is to include the year into the URL (/2018/sample/new or /sample/new?year=2018) and parse the model based on the year ("Sample#{year}Record"). Can rails handle this safely and is there a Gem that can help assist with this approach?
Here is what I came up with, thanks for the advice.
routes.rb
get '/:year/samples', to: 'samples#index', as: :samples, defaults: { year: Time.current.year }
Routes will always default to the current year
application_controller.rb
before_action :check_year
def check_year
if params.has_key?(:year)
if "Sample#{params[:year]}Record".safe_constantize.nil?
redirect_to root_path(year: Time.current.year), notice: "Invalid Year"
end
else
redirect_to root_path(year: Time.current.year), notice: "Invalid Year"
end
end
def get_sample_record(year=Time.current.year)
"Sample#{year}Record".safe_constantize
end
Added a before_action to check the year parameter and added the get_sample_record method to safely constantize the record that can be called from any controller with an optional year like so:
sample_controller.rb
sample_2018_record = get_sample_record
sample_2018_record.count
#> 304
sample_2017_record = get_sample_record 2017
sample_2017_record.count
#> 575683
The result will be nil if an invalid year is passed so I will handle the check in the controller.
As #DaveNewton said, this seems like it should work fine so long as you keep data corresponding to different years' requirements in different tables. A few other observations:
Rails has a helper method constantize for parsing a model from a string:
klass_name = "Sample#{year}Record"
record = klass_name.constantize.new()
will make the variable record an instance of your class corresponding to the year variable. You may find it helpful to use a Factory pattern to encapsulate the process.
Also. be careful how you name and organise your files. You may find this thread helpful when working with the Rails infectors for classes with numbers in their names. A big part of working with Rails is allowing its magic to work for you rather than unwittingly trying to work against it.
As a general rather than Rails-specific piece of advice, I'd also give a considerable amount of thought to how you could define a common public interface for records that will persist across years. A codebase featuring things like
if record.instance_of? Sample2018Record
record.my_2018_method
elsif record.instance_of? Sample2019Record
record.my_method_only_relevant_to_2019
...
will become very difficult to reason about, especially for developers who join after a couple of years. Ruby has extremely powerful tools to help you duck type very effectively.
Related
I'm developing a Rails4 app and I have lots of date attributes in a model. For example;
first_payment_date
last_payment_date
first_application_date
last_application_date
first_result_validation_date
last_result_validation_date
etc.
I want to automate things a little bit and want my application act with these dates. For example, user's won't be able to do payment, after the last_payment_date, or they will not be able to make an application before first_application_date.
What is the best approach to plan this kind of thing? I heard about "state machines" and state_machine GEM but I'm not sure if it's the right thing for me.
Thanks.
If these dates are in the User model, you can create several helper methods inside it to return true of false based on your conditions:
def can_deliver_payment?
self.last_payment_date > Date.today
end
def can_make_application?
self.first_application_date > Date.today
end
# etc
So now when you have an instance of User, you can check these conditions on a more readable way.
I am trying to create a section in my app where a user can update certain site wide attributes. An example is a sales tax percent. Even though this amount is relatively constant, it does change every few years.
Currently I have created a Globals model with attributes I want to keep track of. For example, to access these attributes where needed, I could simply do something like the following snippet.
(1+ Globals.first.sales_tax) * #item.total
What is the best way to handle variables that do not change often, and are applied site wide? If I use this method is there a way to limit the model to one record? A final but more sobering question.......Am I even on the right track?
Ok, so I've dealt with this before, as a design pattern, it is not the ideal way to do things IMO, but it can sometimes be the only way, especially if you don't have direct disk write access, as you would if deployed on Heroku. Here is the solution.
class Global < ActiveRecord::Base
validate :only_one
private
def only_one
if Global.count >= 1
errors.add :base, 'There can only be one global setting/your message here'
end
end
end
If you DO have direct disk access, you can create a YAML config file that you can read/write/dump to when a user edits a config variable.
For example, you could have a yaml file in config/locales/globals.yml
When you wanted to edit it, you could write
filepath = "#{Rails.root}/config/locales/globals.yml"
globals = YAML.load(File.read("#{Rails.root}/config/locales/globals.yml"))
globals.merge!({ sales_tax: 0.07 })
File.write(filepath) do |f|
f.write YAML.dump(globals)
end
More on the ruby yaml documentation
You could also use JSON, XML, or whatever markup language you want
It seems to me like you are pretty close, but depending on the data structure you end up with, I would change it to
(1+ Globals.last.sales_tax) * #item.total
and then build some type of interface that either:
Allows a user to create a new Globals object (perhaps duplicating the existing one) - the use case here being that there is some archive of when these things changed, although you could argue that this should really be a warehousing function (I'm not sure of the scope of your project).
Allows a user to update the existing Globals object using something like paper_trail to track the changes (in which case you might want validations like those presented by #Brian Wheeler).
Alternatively, you could pivot the Global object and instead use something like a kind or type column to delineate different values so that you would have:
(1+ Globals.where(kind: 'Colorado Sales Tax').last) * #item.total
and still build interfaces similar to the ones described above.
You can create a create a class and dump all your constants in it.
For instance:
class Global
#sales_tax = 0.9
def sales_tax
#sales_tax
end
end
and access it like:
Global.sales_tax
Or, you can define global variables something on the lines of this post
I have some business logic in a controller that I want to test that involves setting two values to today's date and yesterday's date.
Initially I had a passing test that essentially looked like this;
controller:
def wibble
#start_time = Date.yesterday
end
test:
it 'blah blah'
get :wibble
assigns(:start_date).should eq(Date.yesterday)
end
But a new requirement has been added that means the date should be i18n'd, which means that the controller is returning back something different.
My Thoughts
I had thought about mocking the variables, because I could only care that they are set to something, but then the business logic of Today and Yesterday isn't being exercised.
I also considered forcing i18n on the test, but this seems way to brittle.
Can anyone suggest a good way to test this?
I have the model:
User -1---n- Transaction(amount,description, date)
User -1---n- TransactionImport -1---n- TransactonImportField(name,value)
(personal expense tracking app).
What I want to achieve is this:
User opens URL and pastes the CSV with the list of transactions.
User submits it.
System extracts data from CSV into TransactionImport (row) + TransactionImportField (cell).
User can choose which column means what (amount, description, date) from the imported data in TransactionImport(Field).
User click save and the system transfers TransactionImport into the Transaction.
What I can't seem to get right is the fact that step 3 creates multiple records of TransactionImport (and related TransactionImportField).
So doing POST /transaction_imports?csv=abcd is expected to produce one record if we would be RESTful. But the code is supposed to be something like this:
# TransactionImportsController
def create
result = TransactionImports.parse(params[:csv])
flash[:notice] = result.message
redirect_to transaction_imports_path
end
I am probably approaching the task from a wrong angle as I feel that implementation doesn't fit in tp the inherited_resources.
Could you please advise what would be the most conventional way of implementing this?
Thanks,
Dmytrii.
REST/HTTP has no expectation that doing POST will only create one record. That maybe the default rails behaviour, but you should not constrain your design because of that.
More and more I'm putting all of my code in models and helpers concerning MVC.
However, sometimes I'm not sure where to organize code. Should it go into the model or should it go into a helper. What are the benefits of each. Is one faster or are they the same. I've heard something about all models getting cached so it seems then like that would be a better place to put most of my code.
For example here is a scenario that works in a model or in helper:
def status
if self.purchased
"Purchased"
elsif self.confirmed
"Confirmed"
elsif self.reserved
"Reserved"
else
"Pending"
end
end
I don't need to save this status as in the database because there are boolean fields for purchased, and confirmed, and reserved. So why put this in a model or why put it into a helper?
So I'm not sure of the best practice or benefits gained on putting code into a model or into helper if it can be in both.
Your specific example is including a business rule in the sense that if the instance of the model is both purchased and confirmed then the proper status is "purchased" not "confirmed"
So in your example, I'd definitely put the method in the model since it is coding one of your applications business rules.
A different example:
def status_string
case status
when 0: "Purchased"
when 1: "Confirmed"
else
"Pending"
end
end
In this case, the status_string method could be reasonably defined either in a View Helper or Model--it has nothing to do with any business rules, it is changing the representation of the value. I'd put it in the model since I tend to only put html-related sw into the View Helpers. But depending on your internationalization scheme, a similar method might be better placed in the View Helper.
A good example of a View Helper is an application-wide method to transform date time values into the standard representation for your app. Eg
# application_helper.rb
def date_long_s(d)
d.strftime("%A, %b *%d, %Y *%I:%M %p")
end
This is really subjective and I agree, sometimes it is not clear if something belongs in a model or helper.
For example:
# using model
status ? status.nice_name : "Pending"
# using helper
nice_name(status)
The clear advantage here for the helper is that it can handle nil objects gracefully keeping views clean. The disadvantage is that the code is now in a different location away from the model
Performance wise you will not see any significant difference between using helpers and models. It is more likely that DB round trips to pull status objects will be a bottleneck.
I use constant hashes in this kind of situations.
Hash is defined in model file like this
STATUS = {
1 => "Pending",
2 => "Confirmed"
}
I also declare constants for each status like this.
ST_PENDING = 1
Declaring this is useful when writing queries. For example,
MyModel.all(:status=>ST_PENDING)
status field in database table is number.So when printing, I simply use this.
MyModel::STATUS[obj.status]