I’m currently working on a project management tool. The way the company works is with a project being either a group of jobs or a single job. So the first job could be X0001. To this you can then add sub-jobs as X0001A, X0001B and so on. The project X0001 is then a container holding the original job and information about all its sub-jobs.
It seemed to make sense to me to use the same database for both projects and jobs, and to have the Project class inherit from the Job class. This would let me use the same tools to analyse project figures at either the job or project level. So project.profit would give me the sum of the profit for the core job and all its sub-jobs. Whereas job.profit would give me the profit for just the one job.
There are probably better ways of doing this but this is the solution I came up with.
I first created the Job class as a standard Rails model. The required custom stuff was then put into the Project class. Here is core of the Project class definition:
class Project < Job #Used to check that a job number is of the correct pattern PROJECT_NUMBER_PATTERN = /^[A-Za-z]{1,2}[0-9]{4,}$/ # Replaced standard find method with one that will exclude sub-jobs from and find operation. def self.find(*args) options = modify_args_conditions_options(args.extract_options!) do_normal_find_things(args, options) end # Replace standard calculate method with one that will exclude results from sub-jobs. # For example if there are three jobs: 'J0001', 'J0002', and 'J0002A' # Job.count ---> 3 # Project.count ---> 2 # as 'J0002A' is a sub-job. def self.calculate(operation, column_name, options = {}) options = modify_args_conditions_options(options) do_normal_calculate_things(operation, column_name, options) end private def self.do_normal_find_things(args, options) validate_find_options(options) set_readonly_option!(options) case args.first when :first then find_initial(options) when :last then find_last(options) when :all then find_every(options) else find_from_ids(args, options) end end def self.do_normal_calculate_things(operation, column_name, options) validate_calculation_options(operation, options) column_name = options[:select] if options[:select] column_name = '*' if column_name == :all column = column_for column_name catch :invalid_query do if options[:group] return execute_grouped_calculation(operation, column_name, column, options) else return execute_simple_calculation(operation, column_name, column, options) end end 0 end def self.modify_args_conditions_options(options) projects_only_condition = "number REGEXP '#{PROJECT_NUMBER_PATTERN.source}'" if options[:conditions].kind_of? Array options[:conditions][0] << " AND #{projects_only_condition}" elsif options[:conditions].kind_of? String options[:conditions] << " AND #{projects_only_condition}" else options[:conditions] = projects_only_condition end return options end end
So what is going on here?
First I dug into the ActiveRecord find method and found that there was a place within its initial definition where I could put in a bit of code that would modify the options passed into the underlying methods. The modification was to add a clause to conditions so that only jobs with numbers that match the pattern prescribed for a Project, would be returned. A project number started with one or two letters and was then followed by numbers. A sub-job added trailing letters to the project number.
I first created a find class method for Project that exactly matched the standard ActiveRecord::Base.find code, and then added the additional code.
Once I'd done that I realised that I could separate out the bits of code I'd added (modify_args_conditions_options) from the unaltered original code (do_normal_find_things). This had two advantages. First it made maintenance easier as I knew which code was original and should be left alone, and which was custom code I'd added. It also meant that I could easily use my custom code elsewhere.....
This seemed to work fine at first for both find and paginate tasks (as will_paginate (mislav_will_paginate) relies on find - so modify how find works, and you also modify how will_paginate works). However, then I noticed that the number of pages was wrong - there were too many. When I investigated this problem, I realised I also needed to modify how the ActiveRecord::Calculations.count method behaved.
Drilling into ActiveRecord's count method, I realised that the place to modify the behaviour was to alter the way ActiveRecord::Calculations.calculate works. This would have the added benefit of also altering other calculation methods like average. It was simple for me to insert the modify_args_conditions_options method into calculate and then call the original methods to carry on the rest of the task.
Hey presto, I've got what I want. I can now modify Project methods as I need - adding new ones, leaving the Job defined ones and overloading Job methods with Project specific version as needed.
So my next job is to decide how to deal with common tasks. For example lets return to profit. Do I just define a new Project profit method, or redefine the Job profit method so it will handle figures coming from either a single job or a group of jobs. Of the two the latter will probably be the better.
To get this to work I probably need to redefine again how find and calculate work so that instead of returning the results for the primary job, they return the summarised figures for the primary job and all its sub-jobs. The key thing being that, now I know where I can hack into find and calculate to modify their behaviours, making this modification should be fairly straight forward.