Slide 1

Slide 1 text

TO ACTIVE RECORD AND BEYOND

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

Evolution

Slide 4

Slide 4 text

–Charles Darwin “It is not the strongest of the species that survive, nor the most intelligent, but the one most responsive to change.”

Slide 5

Slide 5 text

Humans evolved

Slide 6

Slide 6 text

From Apes

Slide 7

Slide 7 text

To Covfefe

Slide 8

Slide 8 text

Computers Evolved

Slide 9

Slide 9 text

From Machines the size of a room

Slide 10

Slide 10 text

To ones that fit into a watch

Slide 11

Slide 11 text

Similarly

Slide 12

Slide 12 text

Web Applications have evolved

Slide 13

Slide 13 text

From static web-pages

Slide 14

Slide 14 text

To ‘Progressive Web Applications’

Slide 15

Slide 15 text

Rails is a catalyst of this evolution

Slide 16

Slide 16 text

Gave a new identity to Web Applications

Slide 17

Slide 17 text

Concepts like DRY and convention over configuration

Slide 18

Slide 18 text

Patterns like MVC and Active record

Slide 19

Slide 19 text

Libraries like ActiveSupport and ActionCable

Slide 20

Slide 20 text

Eased our productivity

Slide 21

Slide 21 text

We ♥ writing Rails applications

Slide 22

Slide 22 text

Business Logic + Data Flow = Web App

Slide 23

Slide 23 text

Business Logic + Data Flow = Web App

Slide 24

Slide 24 text

TL;DR ❖ Patterns used for data flow ❖ Problems in their implementations ❖ Alternatives

Slide 25

Slide 25 text

Data Flow in Rails

Slide 26

Slide 26 text

Active Record

Slide 27

Slide 27 text

– PoEAA by Martin Fowler “An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.”

Slide 28

Slide 28 text

Notice the responsibilities

Slide 29

Slide 29 text

– PoEAA by Martin Fowler “An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.”

Slide 30

Slide 30 text

– PoEAA by Martin Fowler “An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.”

Slide 31

Slide 31 text

– PoEAA by Martin Fowler “An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.”

Slide 32

Slide 32 text

Violation of Single Responsibility Principle

Slide 33

Slide 33 text

SRP is a tenet of good design

Slide 34

Slide 34 text

@president = President.find(44) @president.name # => Barack Obama

Slide 35

Slide 35 text

@president.name = "Donald Trump" @president.id = 45 @president.save

Slide 36

Slide 36 text

One class to read them all, One class to write them

Slide 37

Slide 37 text

Command Query Responsibility Segregation

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

CQRS enforces different object models for read and write

Slide 40

Slide 40 text

Fat Models

Slide 41

Slide 41 text

Seen your user.rb lately ?

Slide 42

Slide 42 text

#belongs_to :custom_role has_and_belongs_to_many :projects, class_name: "Project", inverse_of: :project_sales has_and_belongs_to_many :pre_sales_projects, class_name: "Project", inverse_of: :project_pre_sales has_and_belongs_to_many :post_sales_projects, class_name: "Project", inverse_of: :project_post_sales belongs_to :default_for_client, class_name: "Client", inverse_of: :default_sales belongs_to :default_pre_sales_for_client, class_name: "Client", inverse_of: :default_pre_sales belongs_to :client, inverse_of: :users has_and_belongs_to_many :campaigns, inverse_of: :sales #has_many :site_visits, class_name: "SiteVisit", inverse_of: :sales #has_many :followups, class_name: "Followup", inverse_of: :sales has_many :staff, class_name: 'User', inverse_of: :manager belongs_to :manager, class_name: 'User', inverse_of: :staff has_many :call_availabilities has_many :booking_details,inverse_of: "sales" has_many :activities has_many :incentive_payment_infos, class_name: "IncentivePaymentInfo", inverse_of: :sales #has_many :feeds #has_many :calls, class_name: "Call", inverse_of: :sales has_many :search_criteria, class_name: "SearchCriterium" belongs_to :team has_many :primary_booking_details, class_name: "BookingDetail", :inverse_of => :primary_post_sales has_and_belongs_to_many :secondary_booking_details, class_name: "BookingDetail", :inverse_of => :secondary_post_sales # before_save :set_team_department_to_nil field :first_name, type: String field :last_name, type: String field :phone, type: String field :secondary_phone, type: String field :time_zone, type: String, default: "Mumbai" field :department, type: String field :role, type: String, default: :sales field :work_as_manager,type: Boolean,default: true field :calling_enabled,type: Boolean,default: false field :using_mobile_app,type: Boolean,default: false field :push_notification_mobile,type: Boolean,default: false field :temporary_reassignment, type: Boolean, default: false ## Database authenticatable field :email, type: String, default: "" field :encrypted_password, type: String, default: "" ## Recoverable field :reset_password_token, type: String field :reset_password_sent_at, type: Time ## Rememberable field :remember_created_at, type: DateTime ## Trackable field :sign_in_count, type: Integer, default: 0 field :current_sign_in_at, type: Time field :last_sign_in_at, type: Time field :current_sign_in_ip, type: String field :last_sign_in_ip, type: String ## Encryptable field :password_salt, type: String ## Confirmable field :confirmation_token field :confirmed_at field :confirmation_sent_at # field :unconfirmed_email # Only if using reconfirmable ## Lockable field :failed_attempts, type: Integer, default: 0 # Only if lock strategy is :failed_attempts field :unlock_token, type: String # Only if unlock strategy is :email or :both field :locked_at, type: Time ## Token authenticatable field :authentication_token, type: String field :is_active, type: Boolean,default: false # Used to lock_access of user field :roaster, type: String #a, na, brk, bsy, d field :gcm_id,type: String field :fcm_id,type: String field :daily_reports,type: Boolean, default: true field :phone_codes, type: Hash, default: {} field :owner_ids,type: Array,default: [] field :circle, type: String field :user_in_default_routing, type: Boolean,default: false # add user to client default routing. field :allow_to_manage_leads, type: Boolean, default: true # allow_to_manage_leads decides that ,whether user will be in any of the routing or not field :assign_leads, type: Boolean, default: true # assign_leads decides whether user will be assigned any new leads. He will still be able to manage his own leads provided allow_to_manage_leads is true field :relative_team_ids, type: Array , default: [] #team ids present in user's hierarchy field :oauth_accounts, type: Array, default: [] #[{provider: "facebook", access_token: "", refresh_token: ""}, {provider: "google", access_token: "", refresh_token: ""}] field :billable_user, type: Boolean, default: true # it will decide whether user is real/virtual(user whose record present in DB but dont have access to crm) # When a user gets deactivated. Value is set in before_save of user observer field :deactivated_at, type: DateTime, default: nil # Payload for workflow events specific to Sales(User). # This is defined just to include in other payloads like site_visit or followup payload. # Define payload as {id:"field", text:"Text on UI", roles:[roles]} self.payload = [ {id:"first_name", text:"First name", roles:@allowed_roles}, {id:"last_name", text:"Last name", roles:@allowed_roles}, {id:"phone", text:"Phone", roles:@allowed_roles}, {id:"department", text:"Department", roles:@allowed_roles}, {id:"email", text:"Email", roles:@allowed_roles} ] index(email: 1) index(phone: 1) validates :first_name,:last_name,:phone,:email,:role, presence: true validates_format_of :first_name, :last_name, with: /\A[a-zA-Z\d\s]*\Z/i validates :department, presence: true, :if => Proc.new{|u| ["sales", "pre_sales","post_sales", "manager"].include?(u.role) && u.allow_to_manage_leads == true} validates :client_id, presence: true, :if => Proc.new{|u| u.role != 'superadmin' && !RoleBasedAccessibility.roles.include?(u.role) } validates :phone, uniqueness: {scope: :client_id} validates :email, uniqueness: {case_sensitive: false} validate :department_role validate :oauth_account_count validate :is_only_sales_in_routing validate :is_virtual_user validate :custom_role_validations validate :is_billable_updated validate :is_fallback_user validate :is_in_workflow validate :is_in_routing # validates :phone,format: { with: /^([0])?\d{10}$/} accepts_nested_attributes_for :call_availabilities, update_only: true attr_accessible :email,:password,:remember_me,:first_name,:last_name,:phone,:secondary_phone,:time_zone, :department,:role,:manager_id,:current_password, :password_confirmation,:team_id, :gcm_id, :fcm_id, :using_mobile_app, :daily_reports,:push_notification_mobile,:user_in_default_routing,:project_ids,:pre_sales_project_ids,:campaign_ids, :call_availabilities_attributes,:allow_to_manage_leads,:billable_user,:assign_leads def role?(role) return (self.role.to_s == role.to_s) end def department?(department) return (self.department.to_s == department.to_s) end def available_search_criteria if ["sales","pre_sales"].include?(self.department) SearchCriterium.where(client_id: self.client_id).any_of({is_default:true },{available_for: self.department}) else SearchCriterium.where(client_id: self.client_id) end #TODO: get client.default_search_criteria. get search_criterias on which i am added as a user_id end def oauth_account(name) x = self.oauth_accounts.select{|x| x.with_indifferent_access["provider"] == name}.first if x.present? return x.with_indifferent_access else return nil end end def scheduled_events(scheduled_on, ends_on, act_id, new_act_type) cc = ClientConfiguration.where(client_id: client_id).only(:activity_calender_time).first events_hash = {'scheduled_events' => {}, 'scheduled_date' => '' } if cc.activity_calender_time['active'] followup_hours = cc.activity_calender_time['followup_hours'].to_i followup_minutes = cc.activity_calender_time['followup_minutes'].to_i events_hash = get_scheduled_events(events_hash, 'Followup', new_act_type, followup_hours, followup_minutes, scheduled_on, ends_on, act_id) events_hash = get_scheduled_events(events_hash, 'SiteVisit', new_act_type, followup_hours, followup_minutes, scheduled_on, ends_on, act_id) end events_hash end def get_from_and_to_times(existing_act_type, new_act_type, followup_hours, followup_minutes, scheduled_on, ends_on) if new_act_type == 'Followup' || existing_act_type == 'Followup' from_time = scheduled_on.advance(hours: -followup_hours, minutes: -followup_minutes+1 ) to_time = ends_on.advance(hours: followup_hours, minutes: (followup_minutes-1) ) else from_time = scheduled_on.advance(minutes: +1) to_time = ends_on.advance(minutes: -1) end return from_time, to_time end def get_scheduled_events(events_hash, existing_act_type, new_act_type, followup_hours, followup_minutes, scheduled_on, ends_on, act_id) events_hash['scheduled_date'] = scheduled_on.in_time_zone(client.time_zone).strftime('%d %b %Y') if %w(SiteVisit Followup).include?(existing_act_type) existing_activity_matcher = {client_id: client_id, sales_id: id, status: 'scheduled'} existing_activity_matcher[:_id] = { '$ne' => act_id } if act_id.present? from_time, to_time = get_from_and_to_times(existing_act_type, new_act_type, followup_hours, followup_minutes, scheduled_on, ends_on) if existing_act_type == 'SiteVisit' fields_to_select = [:scheduled_on, :ends_on, :lead_crm_id, :id, :lead_id, :project_id] existing_activity_matcher["$and"] = [{:scheduled_on => {'$lte' => to_time} }, {:ends_on => {'$gte' => from_time} }] elsif existing_act_type == 'Followup' fields_to_select = [:scheduled_on, :ends_on, :lead_crm_id, :id, :lead_id] existing_activity_matcher[:scheduled_on] = {'$gte' => from_time, '$lte' => to_time} end existing_activities = existing_act_type.constantize.where(existing_activity_matcher).only(fields_to_select) if existing_activities.count > 0 existing_activities.each do |existing_act| existing_from_time = existing_act[:scheduled_on] if existing_act[:_type] == 'SiteVisit' existing_to_time = existing_act[:ends_on] else existing_to_time = existing_act[:scheduled_on].advance(hours: followup_hours, minutes: followup_minutes ) end events_hash['scheduled_events'][existing_act.id] = { time: "#{existing_from_time.in_time_zone(client.time_zone).strftime('%l:%M %p')} - #{existing_to_time.in_time_zone(client.time_zone).strftime('%l:%M %p')}", lead_crm_id: existing_act[:lead_crm_id], lead_id: existing_act.lead_id, activity_type: existing_act_type } events_hash['scheduled_events'][existing_act.id][:project] = (existing_act_type == 'SiteVisit' ? existing_act.project.name : '-') end end end events_hash end def name "#{self.first_name} #{self.last_name}".titleize end def is_available_for_call? ctt = Time.now.in_time_zone(self.time_zone) wday = Date::DAYNAMES[Time.now.in_time_zone(self.time_zone).wday].downcase call_availability = self.call_availabilities.select{|x| x.day == wday and x.available == true}.first if(call_availability.present?) stt = Time.now.in_time_zone(self.time_zone).beginning_of_day + call_availability.start_hour.hours + call_availability.start_minute.minutes ett = Time.now.in_time_zone(self.time_zone).beginning_of_day + call_availability.end_hour.hours + call_availability.end_minute.minutes return (ctt >= stt and ctt <= ett) else return false end end # For devise confirmation # new function to return whether a password has been set def has_no_password? self.encrypted_password.blank? end # new function to provide access to protected method unless_confirmed def only_if_unconfirmed pending_any_confirmation {yield} end def password_required? # Password is required if it is being set, but not for new records if !persisted? false else !password.nil? || !password_confirmation.nil? end end def attempt_set_password(params) p = {} p[:password] = params[:password] p[:password_confirmation] = params[:password_confirmation] update_attributes(p) end def active_for_authentication? # used to lock unlock access #remember to call the super #then put our own check to determine "active" state using #our own "is_active" column super && self.is_active? rescue false end def deactivate!(target_sales_ids, is_temporary_assignment,new_manager_id = nil,current_user = nil) self.is_active = false if ["sales","pre_sales","post_sales"].exclude?(self.role) && ["sales","pre_sales"].include?(self.department) && self.valid? if new_manager_id.present? self.change_manager new_manager_id else self.remove_manager end # self.team_id = nil end if self.save audit_log({action: "deactivate_user", class_type: "User",current_user: current_user, user: self.id.to_s, client_id: self.client_id, reassigned_to: target_sales_ids, temporary_reassignment: is_temporary_assignment, leads_count: self.leads.count}) self.reassign_leads target_sales_ids, is_temporary_assignment return true else return false end end def todays_availability return call_availabilities.where(day:Time.now.strftime("%A").downcase).first end def remove_manager self.staff.update_all(manager_id: nil) end def change_manager(changed_manager_id) changed_manager = User.find changed_manager_id if changed_manager.role != "manager" changed_manager.role = "manager" changed_manager.save! end self.staff.update_all(manager_id: changed_manager.id) end def reassign_leads target_sales_ids, is_temporary_assignment=false target_sales_ids.reject!{|x| x.to_s == self.id.to_s} target_sales_ids = target_sales_ids.map(&:to_bson) errors = {} @client = self.client @client.campaigns.where(sale_ids:{"$in" => [self.id]}).each do |c| begin target_sales_ids.each{|id| c.sale_ids << id if !c.sale_ids.include? id} c.sale_ids.delete self.id if(!c.save) project_name = c.project.name rescue "" errors['campaigns'] ||= {} errors['campaigns'][c.name] = ["#{c.errors.full_messages}"] end rescue => e errors['campaigns'] ||= {} errors['campaigns'][c.name] = ["#{e.message}"] end end @client.rules.where(sale_ids:{"$in" => [self.id]}).each do |r| begin target_sales_ids.each{|id| r.sale_ids << id if !r.sale_ids.include? id} r.sale_ids.delete self.id if(!r.save) errors['rules'] ||= {} errors['rules'][r.id] = ["#{r.errors.full_messages}"] end rescue => e errors['rules'] ||= {} errors['rules'][r.id] = ["#{e.message}"] end end @client.projects.any_of({project_sale_ids:{"$in" => [self.id]}},{project_pre_sale_ids:{"$in"=>[self.id]}},{project_post_sale_ids:{"$in"=>[self.id]}}).each do |p| begin if p.project_sale_ids.present? && p.project_sale_ids.include?(self.id) target_sales_ids.each{|id| p.project_sale_ids << id if !p.project_sale_ids.include? id} p.project_sale_ids.delete self.id if(!p.save) errors['projects'] ||= {} errors['projects'][p.name] = ["#{p.errors.full_messages}"] end end rescue => e errors['projects'] ||= {} errors['projects'][p.name] = ["#{e.message}"] end begin if p.project_pre_sale_ids.present? && p.project_pre_sale_ids.include?(self.id) target_sales_ids.each{|id| p.project_pre_sale_ids << id if !p.project_pre_sale_ids.include? id} p.project_pre_sale_ids.delete self.id if(!p.save) errors['project_pre_sale_ids'] ||= {} errors['project_pre_sale_ids'][p.name] = ["#{p.errors.full_messages}"] end end rescue => e errors['project_pre_sale_ids'] ||= {} errors['project_pre_sale_ids'][p.name] = ["#{e.message}"] end begin if p.project_post_sale_ids.present? && p.project_post_sale_ids.include?(self.id) target_sales_ids.each{|id| p.project_post_sale_ids << id if !p.project_post_sale_ids.include? id} p.project_post_sale_ids.delete self.id if(!p.save) errors['projects'] ||= {} errors['projects'][p.name] = ["#{p.errors.full_messages}"] end end rescue => e errors['projects'] ||= {} errors['projects'][p.name] = ["#{e.message}"] end end if @client.default_sale_ids.include? self.id begin target_sales_ids.each{|id| @client.default_sales << @client.users.find(id) if [email protected]_sale_ids.include? id} @client.default_sales.delete self if([email protected]) errors['default_sale_ids'] ||= {} errors['default_sale_ids'][@client.name] = ["#{@client.errors.full_messages}"] end rescue => e errors['default_sale_ids'] ||= {} errors['default_sale_ids'][@client.name] = ["#{e.message}"] end end if @client.default_pre_sale_ids.include? self.id begin target_sales_ids.each{|id| @client.default_pre_sales << @client.users.find(id) if [email protected]_pre_sale_ids.include? id} @client.default_pre_sales.delete self if([email protected]) errors['default_pre_sale_ids'] ||= {} errors['default_pre_sale_ids'][@client.name] = ["#{@client.errors.full_messages}"] end rescue => e errors['default_pre_sale_ids'] ||= {} errors['default_pre_sale_ids'][@client.name] = ["#{e.message}"] end end @client.search_criteria.each do |s| begin if s.available_to_sale_ids.present? && s.available_to_sale_ids.include?(self.id.to_s) target_sales_ids.each{|id| s.available_to_sale_ids << id if !s.available_to_sale_ids.include? id.to_s} s.available_to_sale_ids.delete self.id.to_s if(!s.save) errors['search_criteria'] ||= {} errors['search_criteria'][s.name] = ["#{s.errors.full_messages}"] end end if s.primary_sales_person_ids.present? arr = s.primary_sales_person_ids.split(",") if arr.include? self.id.to_s target_sales_ids.each{|id| arr << id if !arr.include? id} arr.delete self.id.to_s s.primary_sales_person_ids = arr.join(",") if(!s.save) errors['search_criteria'] ||= {} errors['search_criteria'][s.name] = ["#{s.errors.full_messages}"] end end end if s.secondary_sales_person_ids.present? arr = s.secondary_sales_person_ids.split(",") if arr.include? self.id.to_s target_sales_ids.each{|id| arr << id if !arr.include? id} arr.delete self.id.to_s s.secondary_sales_person_ids = arr.join(",") if(!s.save) errors['search_criteria'] ||= {} errors['search_criteria'][s.name] = ["#{s.errors.full_messages}"] end end end rescue => e errors['search_criteria'] ||= {} errors['search_criteria'][s.name] = ["#{e.message}"] end end CallAvailability.in({fallback_user_ids: self.id.to_s}).each do |ca| begin ca.available = true ca.fallback_user_ids = nil if(!ca.save) errors['call_availabilities'] ||= {} errors['call_availabilities'][ca.day] = ["#{ca.errors.full_messages}"] end rescue => e errors['call_availabilities'] ||= {} errors['call_availabilities'][ca.day] = ["#{e.message}"] end end target_sales_ids = target_sales_ids.map(&:to_s) if is_temporary_assignment == "true" && self.department != "post_sales" TemporaryLeadTransferWorker.perform_async(self.id.to_s, target_sales_ids, "transfer", @client.id.to_s) else UserDeactivateWorker.perform_async(self.id.to_s, target_sales_ids, @client.id.to_s) end html = "" html += "Following errors were found while deactivating user

\ Sales Name:#{self.name}

Sales id:#{self.id}
Alternate users were:#{target_sales_ids}
\

" errors.each do |key, val| if val.present? html+='' val.each do |error_name, error_message| html+= '' end html+= ' '+key.titleize+' '+key.titleize+' Name/Id Message '+ (error_name) +' '+ (error_message.join('')) +' ' end end if(errors.length > 0) target_sales = User.where(_id: {"$in" => target_sales_ids}).collect{|u| u.name} if(Rails.env.production?) AdminNotifier.notify_with_html(APP_CONFIG[:support_team],"Error in Deactivating user",html).deliver rescue "" end end end delegate :can?, :cannot?, :to => :ability def ability @ability ||= Ability.new(self) end def format_date(datetime) datetime.present? ? datetime.in_time_zone("Mumbai").strftime("%d-%b-%Y %I:%M:%S %p") : "N/A" end # before_create :create_call_availabilities def self.export(filter) scope = User.unscoped scope = scope.where("client_id" => filter["client_id"]) if(filter["start_date"].present? && filter["end_date"].present?) scope = scope.where("created_at" => {"$gte" => filter["start_date"],"$lt" => filter["end_date"]}) end if(filter["role"].present?) scope = scope.where("role" => {"$in" => filter["role"].split(",") }) end if(filter["is_active"].present?) if(filter["is_active"] == "true,false" || filter["is_active"] == "false,true") else if(filter["is_active"] == "true") scope = scope.where("is_active" => true) else scope = scope.where("is_active" => false) end end end if(filter["team_id"].present?) scope = scope.where("team_id" => {"$in" => filter["team_id"].split(",")}) end scope end def self.get_sales_display_names sales_ids sales = User.in(id: sales_ids) display_names = sales.collect do |user| text = "#{user.name} ( #{user.role.capitalize} )" text += "(#{user.team.name})" if user.team end display_names.to_sentence end # it returns mode score caluculated for user's department def get_user_score return unless self.team.present? score = 0 users_in_teams = self.team.get_team_user_ids(true) users_in_teams.delete(self.id.to_s) score = RedisWrapper.get_mode_score(self.client_id.to_s, users_in_teams) end def remove_from_routing rules = Rule.where(client_id: self.client_id) campaigns = Campaign.where(client_id: self.client_id) projects = Project.where(client_id: self.client_id) rules.each do |r| if(r.sale_ids.include? self.id) r.sale_ids.delete(self.id) r.save end end campaigns.each do |c| if(c.sale_ids.include? self.id) c.sale_ids.delete(self.id); c.save end end projects.each do |p| if(p.project_sale_ids.include? self.id) p.project_sale_ids.delete(self.id); p.save end if(p.project_pre_sale_ids.include? self.id) p.project_pre_sale_ids.delete(self.id); p.save end end client = self.client if(client.default_pre_sale_ids.include? self.id) client.default_pre_sale_ids.delete(self.id); client.save end if(client.default_sale_ids.include? self.id) client.default_sale_ids.delete(self.id); client.save end end def self.cache_fields [:_id, :first_name, :last_name, :team_id, :name, :department] end def ui_json(options={}) json = {} json[:id] = self.id, json[:email] = self.email json[:name] = self.name json[:first_name] = self.first_name json[:last_name] = self.last_name json[:phone] = self.phone json[:secondary_phone] = self.secondary_phone json[:time_zone] = self.time_zone json[:role] = self.role json[:team_id] = self.team_id json[:department] = self.department json[:work_as_manager] = self.work_as_manager json[:phone_codes] = self.phone_codes json[:roaster] = self.roaster json[:accessible_teams] = self.team.accessible_teams rescue [] json[:allow_to_manage_leads] = self.allow_to_manage_leads return json.to_json end private def create_call_availabilities %w[monday tuesday wednesday thursday friday saturday].each do |day| self.call_availabilities << CallAvailability.new(:day => day, :start_hour => 9, :end_hour => 19, :start_minute => 0, :end_minute => 0) end self.call_availabilities << CallAvailability.new(:day => "sunday", :start_hour => 9, :end_hour => 19, :start_minute => 0, :end_minute => 0, :available => false) end def set_team_department_to_nil if(!["manager", "sales", "pre_sales","post_sales"].include?(self.role)) self.department = nil self.team = nil end end def department_role if(["sales", "pre_sales"].include?(self.role)) self.errors.add(:department, "should be either Pre-sales or Sales") if self.role != self.department end end def custom_role_validations roles_array = RoleBasedAccessibility.roles + ["sales","pre_sales","post_sales","admin","manager"] deactivated = self.is_active_changed? && self.is_active == false # don't trigger these validation when user is deactivated as we set team = nil while deactivating if(!roles_array.include?(self.role) && !deactivated) if !self.billable_user if self.team.blank? self.errors.add(:team,"is required for non billable users") end if self.allow_to_manage_leads self.errors.add(:allow_to_manage_leads,"non billable user cant manage leads") end end if self.billable_user if self.allow_to_manage_leads if self.department.blank? self.errors.add(:department,"is required for billable users when allow_to_manage_leads is true") end if self.team.blank? self.errors.add(:team,"is required for billable users when allow_to_manage_leads is true") end end if !self.allow_to_manage_leads if (self.department.blank? ^ self.team.blank?) self.errors.add(:department_and_team,"Either both should be present or none") end end end end end def is_billable_updated if !self.new_record? && self.changes["billable_user"].present? && !self.changes["billable_user"][0].nil? self.errors.add(:billable_user,"billable attribute of user cant be changed") end end def self.subscribe_to_mailchimp(email, name) if Rails.env.production? begin url = "https://us11.api.mailchimp.com/2.0/lists/subscribe.json" data = { apikey: "fcd10f323112660a21bd2979b4fcacbc-us11", id: "6af27e2c0b", "email[email]" => email, "merge_vars[NAME]" => name, double_optin: false } uri = URI(url) res = Net::HTTP.post_form(uri, data) return res.body rescue StandardError => e Rails.logger.error "Could not subscribe_to_mailchimp : #{e.message}" end end end def self.disable_post_sales_users(client_id) User.where(client_id: client_id,department: "post_sales").each do |user| user.is_active = false user.save end end def oauth_account_count providers = self.oauth_accounts.collect{ |acc| acc.with_indifferent_access["provider"] }.uniq providers.each do |provider| accounts_count = self.oauth_accounts.select{|x| x.with_indifferent_access["provider"] == provider}.count if accounts_count > 1 self.errors.add(:oauth_accounts, "Only one oauth account allowed for #{provider}.") end end end def self.not_allowed_users_for_routing(client_id,uids) user_ids = [] user_ids = User.where(client_id: client_id).where(:id.in=> uids,assign_leads: false).distinct(:id) return user_ids.map(&:to_s) end def is_only_sales_in_routing client = self.client user_ids = client.eval("default_#{self.department.singularize}_ids") rescue [] if !user_ids.blank? invalid_condition = (user_ids.count == 1 && user_ids.include?(self.id) && self.assign_leads == false) if invalid_condition self.errors.add(:assign_leads, ": He/She is the only user in client default routing") end end end def is_virtual_user if self.billable_user == false && self.allow_to_manage_leads == true self.errors.add(:base, "User is not billable.So can not manage leads") end end def is_fallback_user if(self.assign_leads_changed? && !self.assign_leads) user_ids = self.client.users.distinct(:id) ca_count = CallAvailability.where(:user_id => {"$in" => user_ids}, :available => false, :fallback_user_ids => {"$elemMatch" => {"$eq" => self.id.to_s}}).count if(ca_count > 0) self.errors.add(:base,"User is a fallback user. So assign leads cannot be turned off") end end end def is_in_workflow if(self.assign_leads_changed? && !self.assign_leads) branch_ids = Branch.where(:recipe_id => {"$in" => self.client.recipe_ids},:branch_type => "action").map(&:id) action_count = Action.where(:branch_id => {"$in" => branch_ids}, :_type => "TaskAction", "data.sales_id" => self.id.to_s).count if action_count > 0 self.errors.add(:base,"User is in workflow. So assign leads cannot be turned off") end end end def is_in_routing if(assign_leads_changed? && !assign_leads) rules = Rule.where(client_id: self.client_id) campaigns = Campaign.where(client_id: self.client_id) projects = Project.where(client_id: self.client_id) is_in_rules = [] is_in_campaigns = [] is_in_projects = [] rules.each do |r| if(r.sale_ids.length == 1 && r.sale_ids.include?(self.id)) is_in_rules << r end end campaigns.each do |c| if(c.sale_ids.length == 1 && c.sale_ids.include?(self.id)) is_in_campaigns << c end

Slide 43

Slide 43 text

800 lines!

Slide 44

Slide 44 text

Quickest solution, use Concerns

Slide 45

Slide 45 text

Moving code to a concern doesn’t burn the fat away!

Slide 46

Slide 46 text

– Mark Twain “A clean desk is a sign of cluttered desk drawer”

Slide 47

Slide 47 text

Concerns is just taking the clutter and moving to a different place

Slide 48

Slide 48 text

Service Objects

Slide 49

Slide 49 text

Facade Pattern

Slide 50

Slide 50 text

– Wikipedia “A facade is an object that provides a simplified interface to a larger body of code, such as a class library.”

Slide 51

Slide 51 text

class UserAuthenticationService def initialize(user) @user = user end def authenticate(password) return false unless @user if BCrypt::Password.new(@user.password_digest) == password @user else false end end end

Slide 52

Slide 52 text

Conditional Callbacks

Slide 53

Slide 53 text

Not a good design choice

Slide 54

Slide 54 text

User Activation

Slide 55

Slide 55 text

class User include Mongoid::Document # some attributes after_create :activate_user attr_accessor :active def activate? not @active end def activate_user if activate? # run activation logic end end end

Slide 56

Slide 56 text

Decorators

Slide 57

Slide 57 text

–Design Patterns Gamma, Elrich, et al “Allows behaviour to be added to an individual object, either statically or dynamically, without affecting the behaviour of other objects from the same class”

Slide 58

Slide 58 text

Traditionally used in view objects

Slide 59

Slide 59 text

Can fit our use case

Slide 60

Slide 60 text

class UserActivationDecorator def initialize user @user = user end def save @user.save && activate_user end private def activate_user # run activation logic end end

Slide 61

Slide 61 text

class UsersController < ApplicationController def activate @user = UserActivationDecorator.new( User.new(user_params) ) if @user.save redirect_to users_path else render "activate" end end end

Slide 62

Slide 62 text

We know Patterns maybe flawed

Slide 63

Slide 63 text

We know Libraries maybe flawed

Slide 64

Slide 64 text

But

Slide 65

Slide 65 text

— Eddard ‘Ned’ Stark “Nothing someone says before the word ‘but’ really counts”

Slide 66

Slide 66 text

Why cure if we can prevent?

Slide 67

Slide 67 text

Responsive to Change

Slide 68

Slide 68 text

Repositories

Slide 69

Slide 69 text

– PoEAA by Martin Fowler “Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.”

Slide 70

Slide 70 text

Interface between domain and data layers

Slide 71

Slide 71 text

Data layer Agnostic

Slide 72

Slide 72 text

Faster Tests!

Slide 73

Slide 73 text

if Rails.env.test? Repository.register :users, Memory::UserRepo Repository.register :articles, Memory::ArticleRepo end

Slide 74

Slide 74 text

if Rails.env.test? Repository.register :users, DataBase::UserRepo Repository.register :articles, DataBase::ArticleRepo end

Slide 75

Slide 75 text

module Memory class UserRepo def initialize @records = {} @id = 1 end def klass UserStruct end def new(attributes = {}) klass.new(attributes) end def save(object) object.id = @id @records[@id] = object @id += 1 UserDomainObject.new(object) end def find_by_id(n) UserDomainObject.new(@records[n.to_i]) end end end

Slide 76

Slide 76 text

module Memory class UserRepo def initialize @records = {} @id = 1 end def klass UserStruct end def new(attributes = {}) klass.new(attributes) end def save(object) object.id = @id @records[@id] = object @id += 1 UserDomainObject.new(object) end def find_by_id(n) UserDomainObject.new(@records[n.to_i]) end end end

Slide 77

Slide 77 text

module Memory class UserRepo def initialize end def klass end def new(attributes = {}) end def save(object) end def find_by_id(n) end end end

Slide 78

Slide 78 text

module Database class UserRepo def klass User end def new(attributes = {}) klass.new(attributes) end def save(object) object.save UserDomainObject.new(object) end def find_by_id(n) UserDomainObject.new(klass.get(n)) end end end

Slide 79

Slide 79 text

module Database class UserRepo def klass User end def new(attributes = {}) klass.new(attributes) end def save(object) object.save UserDomainObject.new(object) end def find_by_id(n) UserDomainObject.new(klass.get(n)) end end end

Slide 80

Slide 80 text

module Database class UserRepo def klass end def new(attributes = {}) end def save(object) end def find_by_id(n) end end end

Slide 81

Slide 81 text

Repository.for(:users).new.find_by_ id(1) # => UserDomainObject

Slide 82

Slide 82 text

Repository methods return a domain object

Slide 83

Slide 83 text

The repository is a collection of the domain objects

Slide 84

Slide 84 text

And hides the communication with database layer

Slide 85

Slide 85 text

Domain Object can be a simple PORO

Slide 86

Slide 86 text

class UserDomainObject attr_reader :id, :first_name, :last_name def initialize user @id = user.id @first_name = user.first_name @last_name = user.last_name end def name "#{@first_name} + #{@last_name}" end end

Slide 87

Slide 87 text

def UsersController < ApplicationController def show @user = Repository.for(:users).new.find_by_id(params[ :id]) end end

Slide 88

Slide 88 text

Similarly for writes

Slide 89

Slide 89 text

def UsersController < ApplicationController def create user_repo = Repository.for(:users).new(user_params) if @user = user_repo.save # redirect else # render form end end end

Slide 90

Slide 90 text

Changesets

Slide 91

Slide 91 text

– Wikipedia “A changeset is a set of changes which should be treated as an indivisible group”

Slide 92

Slide 92 text

Changesets are indivisible

Slide 93

Slide 93 text

Changesets are immutable

Slide 94

Slide 94 text

Changesets maintain integrity of data to be inserted

Slide 95

Slide 95 text

changeset = User.changeset(%User{}, %{age: 42, email: "[email protected]"}) {:error, changeset} = Repo.insert(changeset) changeset.action #=> :insert

Slide 96

Slide 96 text

TL;DR ❖ Active Record is good if domain logic is simple ❖ Responsive to change ❖ Repositories and Changesets are a new perspective to handle data flow

Slide 97

Slide 97 text

Thank You Twitter - @_kanuahs Github - @shaunakpp