Upgrade to Pro — share decks privately, control downloads, hide ads and more …

To Active Record and Beyond

To Active Record and Beyond

Active Record is the mother-of-all patterns when it comes to ORMs. But there are new kids(read ORMs) on the block, and they promise elegance, immutability and a separation of concerns. In this talk, we’ll shed light on these techniques and discuss a different approach to mapping data.

Shaunak Pagnis

August 12, 2017
Tweet

More Decks by Shaunak Pagnis

Other Decks in Programming

Transcript

  1. –Charles Darwin “It is not the strongest of the species

    that survive, nor the most intelligent, but the one most responsive to change.”
  2. TL;DR ❖ Patterns used for data flow ❖ Problems in

    their implementations ❖ Alternatives
  3. – 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.”
  4. – 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.”
  5. – 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.”
  6. – 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.”
  7. #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<br/><br/>\ <span> Sales Name:#{self.name} <br/><br/>Sales id:#{self.id}<br/> Alternate users were:#{target_sales_ids}<br/></span>\ <div><b><br></b></div>" errors.each do |key, val| if val.present? html+='<table border="1" style="width:400px; text-align: center"> <tr><td colspan = 2>'+key.titleize+'</td></tr> <tr> <th style="text-align: center; width:200px" >'+key.titleize+' Name/Id </th> <th style="text-align: center; width:200px" > Message </th> </tr>' val.each do |error_name, error_message| html+= '<tr> <td style="text-align: center; width:200px">'+ (error_name) +'</td> <td style="text-align: center; width:200px">'+ (error_message.join('')) +'</td> </tr>' end html+= '</table>' 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
  8. – Wikipedia “A facade is an object that provides a

    simplified interface to a larger body of code, such as a class library.”
  9. 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
  10. 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
  11. –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”
  12. class UserActivationDecorator def initialize user @user = user end def

    save @user.save && activate_user end private def activate_user # run activation logic end end
  13. But

  14. – PoEAA by Martin Fowler “Mediates between the domain and

    data mapping layers using a collection-like interface for accessing domain objects.”
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. module Database class UserRepo def klass end def new(attributes =

    {}) end def save(object) end def find_by_id(n) end end end
  21. 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
  22. – Wikipedia “A changeset is a set of changes which

    should be treated as an indivisible group”
  23. 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