Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Save user metrics in db #47

Merged
merged 28 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a5f97b5
refactor: rewrite `SocialMetrics` class to functional style
daronenko Jul 18, 2024
26751ee
refactor: rename some methods
daronenko Jul 18, 2024
d152e98
refactor: convert array to set for optimization
daronenko Jul 18, 2024
c451324
fix(compose): add param for arm architecture
daronenko Jul 21, 2024
3b085a6
feat(save-metrics): generate model
daronenko Jul 21, 2024
de27b7e
feat(save-metrics): generate model
daronenko Jul 21, 2024
3e4b3f6
feat(save-metrics): generate model
daronenko Jul 21, 2024
4ab6e31
feat(save-metrics): generate model
daronenko Jul 21, 2024
5357c0f
feat(save-metrics): update model
daronenko Jul 21, 2024
bf36791
integrate db models into module
daronenko Jul 21, 2024
a200b2b
generate test data
daronenko Jul 21, 2024
023608a
feat(save-metrics): create controller for metrics
daronenko Jul 21, 2024
b507bdc
feat(save-metrics): add routes for metrics
daronenko Jul 21, 2024
edb94f0
feat: add to
daronenko Jul 21, 2024
2818664
disable AbcSize in rubocop config for metric model
daronenko Jul 21, 2024
971dd26
Merge branch 'dev' into feature/save-user-metrics-in-db
daronenko Jul 21, 2024
3f6ccaa
feat(save-metrics): update Post model
daronenko Jul 22, 2024
d31dafb
feat(save-metrics): update Friend model
daronenko Jul 22, 2024
f6be433
feat(save-metrics): update Relationship model
daronenko Jul 22, 2024
594f543
feat(save-metrics): update Metric model
daronenko Jul 22, 2024
3ac989d
feat(save-metrics): update User model
daronenko Jul 22, 2024
bcf12eb
remove Metrics controller
daronenko Jul 22, 2024
f22a17f
feat(save-metrics): update db schema
daronenko Jul 22, 2024
0bbccd2
update test data generation
daronenko Jul 22, 2024
72bfc2e
feat(save-metrics): update implementation of SocialMetrics module
daronenko Jul 22, 2024
9bba933
feat(save-metrics): disable AbcSize for MetricsCalculator in rubocop …
daronenko Jul 22, 2024
a34c3ed
Merge branch 'feature/save-user-metrics-in-db' of github.com:evrone/b…
daronenko Jul 22, 2024
136a6e6
feat(save-metrics): remove test for deleted controller
daronenko Jul 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,4 @@
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,rails

.DS_Store
.idea/
4 changes: 4 additions & 0 deletions app/helpers/metrics_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

module MetricsHelper
end
10 changes: 10 additions & 0 deletions app/models/friend.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

class Friend < ApplicationRecord
has_many :relationships
has_many :users, through: :relationships

validates :first_name, presence: true
validates :last_name, presence: true
validates :vk_uid, presence: true, uniqueness: true
end
5 changes: 5 additions & 0 deletions app/models/metric.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

class Metric < ApplicationRecord
belongs_to :user
end
7 changes: 7 additions & 0 deletions app/models/post.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class Post < ApplicationRecord
belongs_to :user

validates :vk_uid, presence: true, uniqueness: true
end
8 changes: 8 additions & 0 deletions app/models/relationship.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

class Relationship < ApplicationRecord
belongs_to :user
belongs_to :friend

validates :active, inclusion: { in: [true, false] }
end
56 changes: 48 additions & 8 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,54 @@ class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable

# include UserConcern

def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
# Если Instagram не предоставляет email, вы можете использовать другой уникальный идентификатор
user.email = auth.info.email || "#{auth.uid}@instagram.com"
user.password = Devise.friendly_token[0, 20]
user.name = auth.info.name # assuming the user model has a name
has_one :metric, dependent: :destroy

has_many :posts
has_many :relationships
has_many :friends, through: :relationships

has_many :active_relationships, -> { where active: true }, class_name: 'Relationship'
has_many :inactive_relationships, -> { where active: false }, class_name: 'Relationship'

has_many :active_friends, through: :active_relationships, class_name: 'Friend', source: :friend
has_many :inactive_friends, through: :inactive_relationships, class_name: 'Friend', source: :friend

after_create :create_metric

def create_metric
Metric.create(user: self)
end

def total_likes
posts.sum do |post|
post.likes.size
end
end

def total_comments
posts.sum do |post|
post.comments.size
end
end

def set_active(value, friends)
relationships.where(friend: friends).update_all(active: value)
end

def update_metrics
MetricsCalculator.call(self)
end

def update_metrics_needed?
metrics_fields = %i[
average_likes target_likes average_comments target_comments
comments_likes_ratio target_comments_likes_ratio audience_score
average_engagement_score
]

metrics_have_nil = metrics_fields.map { |field| metric[field].nil? }.any?
post_metrics_have_nil = posts.map { |post| post.engagement_score.nil? }.any?

metrics_have_nil || post_metrics_have_nil
end
end
130 changes: 130 additions & 0 deletions app/services/metrics_calculator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# frozen_string_literal: true

# Contains logic for calculating user metrics
class MetricsCalculator
class << self
def call(user)
metric = user.metric

active_friends, inactive_friends = active_friends(user)
user.set_active(true, active_friends)
user.set_active(false, inactive_friends)

metric.average_likes = average_likes(user)
metric.target_likes = target_likes(user)

metric.average_comments = average_comments(user)
metric.target_comments = target_comments(user)

metric.comments_likes_ratio = comments_likes_ratio(user)
metric.target_comments_likes_ratio = target_comments_likes_ratio(user)

metric.average_engagement_score = average_engagement_score(user)
metric.audience_score = audience_score(user)

metric.save
end

private

# Computes set of active and inactive friends' vk uids
# based on activity (likes and comments) on posts
def active_friends(user)
vk_uids = user.posts.flat_map do |post|
post.likes + post.comments
end.uniq

active_users = user.friends.where(vk_uid: vk_uids).to_set
friends = user.friends.to_set

active_friends = active_users & friends
inactive_friends = friends - active_friends
[active_friends, inactive_friends]
end

# Computes average number of likes per post
def average_likes(user)
return 0 if user.posts.empty?

(user.total_likes.to_f / user.posts.size).round
end

# Computes target number of likes based on `target_likes_ratio`
# Target number - theoretically possible with the current
# number of followers
def target_likes(user, target_likes_ratio: 0.3)
(user.friends.size * target_likes_ratio).round
end

# Computes average number of comments per post
def average_comments(user)
return 0 if user.posts.empty?

(user.total_comments.to_f / user.posts.size).round
end

# Computes target number of comments based on `target_comments_ratio`
# Target number - theoretically possible with the current
# number of followers
def target_comments(user, target_comments_ratio: 0.1)
(user.friends.size * target_comments_ratio).round
end

# Computes ratio of total comments to total likes
# in the form "comments:likes"
def comments_likes_ratio(user)
common_divisor = user.total_comments.gcd(user.total_likes)
return '0:0' if common_divisor.zero?

"#{user.total_comments / common_divisor}:#{user.total_likes / common_divisor}"
end

# Computes target ratio of total comments to total likes
# in the form "comments:likes"
# Target number - theoretically possible with the current
# number of followers
def target_comments_likes_ratio(
user,
target_likes_ratio: 0.3,
target_comments_ratio: 0.1
)
target_comments = (user.total_comments * target_comments_ratio).round
target_likes = (user.total_likes * target_likes_ratio).round

common_divisor = target_comments.gcd(target_likes)
return '0:0' if common_divisor.zero?

"#{target_comments / common_divisor}:#{target_likes / common_divisor}"
end

# Calculates engagement score for a single post
# Engagement score - average activity shown by each user
def engagement_score(post, engagement_score_factor: 1000)
return 0 if post.user.friends.empty?

((post.likes.size + post.comments.size).to_f / post.user.friends.size * engagement_score_factor).round
end

# Computes average engagement score of posts
def average_engagement_score(user)
return 0 if user.posts.empty?

user.posts.each do |post|
post.engagement_score = engagement_score(post)
post.save
end

total_engagement_score = user.posts.sum(&:engagement_score)
(total_engagement_score / user.posts.size).round
end

# Computes audience score based on the ratio of active friends
# to total friends
# The score is scaled to a range of 0.0 to 10.0
def audience_score(user)
return 0.0 if user.friends.empty?

(user.active_relationships.count.to_f / user.friends.size * 10).round(1)
end
end
end
Loading
Loading