Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User Stats #77

Merged
merged 24 commits into from
Apr 6, 2017
Merged

User Stats #77

merged 24 commits into from
Apr 6, 2017

Conversation

toyhammered
Copy link
Member

self.stats_data[slug] = 0
end

self.stats_data[slug] += 1

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


def increment_data(slug)
unless stats_data[slug]
self.stats_data[slug] = 0

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

end

def increment_data(slug)
unless stats_data[slug]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Favor modifier unless usage when having a single-line body. Another good alternative is the usage of control flow &&/||. (https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier)


# update data with + 1
def library_entry_update(library_entry)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra empty line detected at method body beginning.

end

# update data with + 1
def library_entry_update(library_entry)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put empty method definitions on a single line. (https://github.com/bbatsov/ruby-style-guide#no-single-line-methods)

@@ -228,4 +228,15 @@ def myanimelist_linked_account
type: 'LinkedAccount::MyAnimeList'
)
end

after_commit ->() { create_stat }, on: :create
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already know this name sucks

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's... actually not a bad name 🤔 That said, it might be better to just toss them in a block like this, depending on how much logic you'll have in there (and how many of these you need):

after_commit(on: :create) do
  Stat::AnimeGenreBreakdown.increment_genres(user, media.genres)
end

@@ -228,4 +228,15 @@ def myanimelist_linked_account
type: 'LinkedAccount::MyAnimeList'
)
end

after_commit ->() { create_stat }, on: :create
after_destroy ->() { delete_stat }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and this one


def library_entries
user.library_entries.where(media_type: 'Anime')
p record
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will remove

end
end

def self.increment_genres(user, genres)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of feel like that these methods can just be set to increment and decrement and then just used across all the other Stats (so they are all consistent, because we already know it is genres due to type)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thinking!

@@ -228,4 +228,15 @@ def myanimelist_linked_account
type: 'LinkedAccount::MyAnimeList'
)
end

after_commit ->() { create_stat }, on: :create
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's... actually not a bad name 🤔 That said, it might be better to just toss them in a block like this, depending on how much logic you'll have in there (and how many of these you need):

after_commit(on: :create) do
  Stat::AnimeGenreBreakdown.increment_genres(user, media.genres)
end

class Stat < ApplicationRecord
belongs_to :user, required: true
# expose for jsonapi
alias_attribute :kind, :type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is gonna need adjustment once #86 is merged

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, looked at what you did and it seems good.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need this with the STIResource

# expose for jsonapi
alias_attribute :kind, :type

validates_presence_of :type, :stats_data
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally I prefer validates :type, :stats_data, presence: true

save
else
update_attribute(:stats_data, stats_data)
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't save work for both of these cases?

Also we might want save! which'll throw an error if the save fails (otherwise this might break silently)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, now that I think about it I don't know why I actually split it.


record.stats_data[genre.slug] -= 1
record.stats_data['total'] -= 1
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might be able to combine increment and decrement into an update_genres(genres, -1) method (where the second parameter is either -1 or +1 depending on whether you wanna decrement or increment. Just something to think about

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally had it with an update_genres and was thinking about going with the (+1 -1, we talked about it) but decided that splitting them up would be best for consistency across all the Stats.

@@ -0,0 +1 @@
class Stat::AnimeGenreBreakdownPolicy < StatPolicy; end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use nested module/class definitions instead of compact style.

end

def progress_to_time(le, progress)
return 0 if le.anime.episode_length.nil?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just assumed we are skipping if there is no episode_length?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable tbh

@@ -0,0 +1,13 @@
class StatResource < BaseResource
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be immutable, now that I think about it recalculate! is run with a rake task and both increment + decrement are just inside the LibraryEntry.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, yes, that's a good point!

class RegenerateStatService
class << self
def anime_genre_breakdown
User.where(id: LibraryEntry.select(:user_id).where(media_type: 'Anime'))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the .where(media-type: 'Anime') because no point running it if they only have manga library entries.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense


say_with_time 'Filling time_spent column' do
LibraryEntry.where(media_type: 'Anime').joins(:anime)
.update_in_batches('time_spent = anime.episode_length * progress')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens if there is no anime.episode_length because there is no default (or null constraint) so wouldn't it be nil * progress which will cause an error?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure it'll just replace it with 0 (since this is Postgres math, not Ruby math), that's what it did last night when I was pulling numbers for josh 🤔 But it's possible that order matters, I did progress * episode_length

If you wanna be explicit you could use coalesce(anime.episode_length, 0)

factory :stat do
association :user, factory: :user, strategy: :build
type 'Stat::AnimeGenreBreakdown'
stats_data { { total: 0 } }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am getting this error if stats_data is just a {}:

     Failure/Error: FactoryGirl::Linter.new([factory], :factory).lint!

     FactoryGirl::InvalidFactoryError:
       The following factories are invalid:

       * stat - Validation failed: Stats data can't be blank (ActiveRecord::RecordInvalid)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that if you do stats_data {} or is it with stats_data { {} }?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

first one is uhh... wrong? I have tried with the second one, and there is a default set already to {} at a database level.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First one is really more "ambiguous" than wrong, it could be either a block or a hash (and both are valid for FactoryGirl).

Also the problem is simple:

{}.blank? #=> true

You can probably drop the validation since the database provides a default

subject { described_class.new }

it { should belong_to(:user) }
it { should validate_presence_of(:type) }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add validation for stats_data also

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

after_commit(on: :create) do
sync_entry(:create) # mal exporter
# Stat STI
Stat::AnimeGenreBreakdown.increment(user, media.genres)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These probably shouldn't be in after_commit, that'll make them run in a separate transaction which could result in them being out-of-sync with this model. Should be in after_create and after_destroy hooks

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well the after_create is creating some janky timing errors with my tests now....

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really? That suggests your tests are wrong, since after_commit is just never run during tests (the commit never actually happens, instead it just runs a rollback after each test)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

heh, well I know what is wrong but my easy solution of just doing a before do subject.recalculate! fails it seems...

after_destroy do
sync_entry(:delete) # mal exporter
# Stat STI
Stat::AnimeGenreBreakdown.decrement(user, media.genres)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above

class Stat < ApplicationRecord
belongs_to :user, required: true
# expose for jsonapi
alias_attribute :kind, :type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need this with the STIResource


def self.decrement(user, library_entry)
record = user.stats.find_by(type: 'Stat::AnimeAmountWatched')
# TODO: do we want to raise or return?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just do nothing, raising an exception might break library updates

end

def progress_to_time(le, progress)
return 0 if le.anime.episode_length.nil?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable tbh

@@ -0,0 +1,13 @@
class StatResource < BaseResource
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, yes, that's a good point!

class RegenerateStatService
class << self
def anime_genre_breakdown
User.where(id: LibraryEntry.select(:user_id).where(media_type: 'Anime'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense


say_with_time 'Filling time_spent column' do
LibraryEntry.where(media_type: 'Anime').joins(:anime)
.update_in_batches('time_spent = anime.episode_length * progress')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure it'll just replace it with 0 (since this is Postgres math, not Ruby math), that's what it did last night when I was pulling numbers for josh 🤔 But it's possible that order matters, I did progress * episode_length

If you wanna be explicit you could use coalesce(anime.episode_length, 0)

factory :stat do
association :user, factory: :user, strategy: :build
type 'Stat::AnimeGenreBreakdown'
stats_data { { total: 0 } }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that if you do stats_data {} or is it with stats_data { {} }?

subject { described_class.new }

it { should belong_to(:user) }
it { should validate_presence_of(:type) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


after_create do
# Stat STI
case media_type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer if you used kind for this (returns a symbol :anime, :manga, etc.), since media_type is deprecated and slated for eventual removal

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, there are quite a few other places that I use media_type so should I change them to kind?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, anywhere you're using media_type from LibraryEntry, change to kind, so we can gradually wean off the polymorphic column


def default_stats
# needs to be a string and NOT a symbol
# due to how it is being saved.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure Rails will handle all the conversions itself?

Also it's probably better for default_stats to just be a DEFAULT_STATS constant that you self.stats_data = DEFAULT_STATS if stats_data.blank?, instead of a method which modifies the instance and returns the self.

When a method has a name like default_stats, you expect that it'll return the default stats. The fact that this method doesn't is a bit of a betrayal of expectations.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay! I actually wasn't entirely sure how to approach the default_stats because I felt like a method wasn't right. Thinking about it now, having a constant (frozen) is prob the best method.

save!
end

def default_stats
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above, re: the other default_stats method

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This, again.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops... I only fixed 1 of them...

add_column :library_entries, :time_spent, :integer

say_with_time 'Filling time_spent column' do
LibraryEntry.where(media_type: 'Anime').joins(:anime)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer if you used by_kind(:anime) for this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so instead of joins(:anime), use by_kind(:anime)? Can I have a tl;dr for what the difference is?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also for the media_type: 'Anime' should I change it to .where(kind: :anime)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nonono, replace where(media_type: 'Anime') with by_kind(:anime)

As the name implies, by_kind gets Library Entries by kind 😉

@@ -0,0 +1,10 @@
task user_stats: 'kitsu:user_stats_setup'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a task named user_stats_setup, cause I ain't seeing one 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh I failed at copying the stats.rake :D. it should be user_stats_setup (like 2 lines down)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uhhhh why are you copying stats.rake? That's literally just a thing to configure how we count lines of code with rake stats (try running it, it'll tell you the lines of code in Kitsu!)

It shouldn't be named user_stats_setup unless it's doing some from of configuration setup (which it isn't)

task user_stats: 'kitsu:user_stats_setup'

namespace :kitsu do
task :user_stats do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably want to use the environment dependency so that all of Rails will load for this (otherwise it probably won't be able to access any Rails models)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I get an English translation of this please?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR go look at kitsu.rake

@@ -0,0 +1,8 @@
namespace :kitsu do
task :user_stats => :environment do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the new Ruby 1.9 hash syntax. (https://github.com/bbatsov/ruby-style-guide#hash-literals)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assuming it just wants user_stats: :environment?

class Stat
class AnimeAmountWatched < Stat
DEFAULT_STATS = {
'total_anime' => 0,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use 2 spaces for indentation in a hash, relative to the start of the line where the left curly brace is.

@@ -0,0 +1,66 @@
class Stat
class AnimeAmountWatched < Stat
DEFAULT_STATS = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Freeze mutable objects assigned to constants.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tis ain't possible houndy :(.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't that possible? It looks possible to me :v

when :anime
Stat::AnimeGenreBreakdown.decrement(user, media.genres)
Stat::AnimeAmountWatched.decrement(user, self)
when :manga

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid when branches without a body.

when :anime
Stat::AnimeGenreBreakdown.increment(user, media.genres)
Stat::AnimeAmountWatched.increment(user, self)
when :manga

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid when branches without a body.

@@ -2,7 +2,7 @@ class UserResource < BaseResource
PRIVATE_FIELDS = %i[email password confirmed previous_email language time_zone
country share_to_global title_language_preference
sfw_filter rating_system].freeze

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -2,7 +2,7 @@ class UserResource < BaseResource
PRIVATE_FIELDS = %i[email password confirmed previous_email language time_zone
country share_to_global title_language_preference
sfw_filter rating_system].freeze

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

@NuckChorris NuckChorris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great progress, I like where this is going!

@@ -0,0 +1,66 @@
class Stat
class AnimeAmountWatched < Stat
DEFAULT_STATS = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't that possible? It looks possible to me :v

save!
end

def default_stats
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This, again.

db/schema.rb Outdated

ActiveRecord::Schema.define(version: 20170326222205) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might've gotten messed up in your merge, but I think the blank line is supposed to be after the ActiveRecord::Schema.define line 🤔

(not a big deal tho, it'll get fixed next time rails dumps the schema)

record = user.stats.find_or_initialize_by(
type: 'Stat::AnimeAmountWatched'
)
record.stats_data['all_time'] =

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Favor a normal if-statement over a modifier clause in a multiline statement. (https://github.com/bbatsov/ruby-style-guide#no-multiline-if-modifiers)

@NuckChorris NuckChorris merged commit 8117608 into the-future Apr 6, 2017
@wopian wopian mentioned this pull request Apr 18, 2017
20 tasks
@NuckChorris NuckChorris deleted the user-stats branch May 2, 2017 05:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants