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

Analytic charts #75

Merged
merged 16 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 5 additions & 5 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,16 @@ gem 'bootsnap', require: false
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
gem 'active_storage_validations'
gem 'aws-sdk-s3', require: false
gem 'image_processing', '~> 1.2'

gem 'chartkick'
gem 'faker'
gem 'friendly_id', '~> 5.4.0'
gem 'pagy', '~> 6.0' # omit patch digit
gem 'rswag'

gem 'image_processing', '~> 1.2'
gem 'omniauth-github', '~> 2.0.0'
gem 'omniauth-google-oauth2'
gem 'omniauth-rails_csrf_protection'
gem 'pagy', '~> 6.0' # omit patch digit
gem 'rswag'
gem 'sidekiq'

group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
Expand Down
11 changes: 11 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
chartkick (5.0.4)
concurrent-ruby (1.2.2)
connection_pool (2.4.1)
crass (1.0.6)
cssbundling-rails (1.2.0)
railties (>= 6.0.0)
Expand Down Expand Up @@ -289,6 +291,8 @@ GEM
rainbow (3.1.1)
rake (13.0.6)
redis (4.8.1)
redis-client (0.18.0)
connection_pool
regexp_parser (2.8.1)
reline (0.3.6)
io-console (~> 0.5)
Expand Down Expand Up @@ -353,6 +357,11 @@ GEM
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
sidekiq (7.2.0)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.14.0)
snaky_hash (2.0.1)
hashie
version_gem (~> 1.1, >= 1.1.1)
Expand Down Expand Up @@ -406,6 +415,7 @@ DEPENDENCIES
bullet
cancancan
capybara
chartkick
cssbundling-rails (~> 1.2)
database_cleaner
debug
Expand Down Expand Up @@ -435,6 +445,7 @@ DEPENDENCIES
rswag
rubocop (>= 1.0, < 2.0)
selenium-webdriver
sidekiq
sprockets-rails
stimulus-rails
turbo-rails
Expand Down
1 change: 1 addition & 0 deletions Procfile.dev
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
web: unset PORT && bin/rails server
css: yarn build:css --watch
worker: bundle exec sidekiq
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,22 @@
<li><a href="https://github.com/igorkasyanchuk/active_storage_validations">Active Storage Validation</a></li>
<li><a href="https://github.com/flyerhzm/bullet">Bullet</a></li>
<li><a href="https://bulma.io/">Bulma</a></li>
<li><a href="https://github.com/cookpad/omniauth-rails_csrf_protection">OmniAuth Rails CSRF Protection</a></li>
<li><a href="https://github.com/omniauth/omniauth-github">OmniAuth GitHub</a></li>
<li><a href="https://github.com/zquestz/omniauth-google-oauth2">OmniAuth Google OAuth2</a></li>
<li><a href="https://github.com/CanCanCommunity/cancancan">Cancancan</a></li>
<li><a href="https://github.com/teamcapybara/capybara">Capybara</a></li>
<li><a href="https://github.com/ankane/chartkick">Chartkick</a></li>
<li><a href="https://rubygems.org/gems/devise/">Devise</a></li>
<li><a href="https://github.com/thoughtbot/factory_bot">Factory Bot</a></li>
<li><a href="https://github.com/faker-ruby/faker">Faker</a></li>
<li><a href="https://github.com/norman/friendly_id">FriendlyId</a></li>
<li><a href="https://jwt.io/">JWT</a></li>
<li><a href="https://github.com/cookpad/omniauth-rails_csrf_protection">OmniAuth Rails CSRF Protection</a></li>
<li><a href="https://github.com/omniauth/omniauth-github">OmniAuth GitHub</a></li>
<li><a href="https://github.com/zquestz/omniauth-google-oauth2">OmniAuth Google OAuth2</a></li>
<li><a href="https://github.com/ddnexus/pagy">Pagy</a></li>
<li><a href="https://github.com/rspec/rspec-rails">RSpec Rails</a></li>
<li><a href="https://github.com/rubocop/rubocop">Rubocop</a></li>
<li><a href="https://stimulus.hotwired.dev/">Stimulus</a></li>
<li><a href="https://github.com/sidekiq/sidekiq">Sidekiq</a></li>
<li><a href="https://github.com/hotwired/turbo-rails">Turbo Rails</a></li>
</ul>
</details>
Expand Down Expand Up @@ -140,15 +142,15 @@ Fill in the environment variables
Install this project with:
```sh
bundle install
rails db:create db:migrate
rails db:create db:migrate db:seed
```

### Usage

To run the project, execute the following command:

```sh
rails server
bin/dev
```
Open `http://localhost:3000/` on your browser.

Expand Down
11 changes: 11 additions & 0 deletions app/controllers/analytics_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class AnalyticsController < ApplicationController
def index
@start_date = params[:start_date] || (Date.today - 6.days).to_s
@end_date = params[:end_date] || Date.today.to_s
@counter_analytics = current_user.counter_analytics.order(created_at: :desc)
@data = []
@counter_analytics.each do |count|
@data << { name: count.action, data: { count.created_at.to_s => count.count } }
end
end
end
13 changes: 13 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ def current_user
current_member
end

def current_visitor
session[:visitor_id] ? Visitor.find(session[:visitor_id]) : create_current_visitor
end

def posts_per_page
@posts_per_page = 2
end
Expand Down Expand Up @@ -46,4 +50,13 @@ def update_allowed_parameters
u.permit(:name, :email, :password, :password_confirmation, :current_password, :avatar)
end
end

private

def create_current_visitor
visitor = Visitor.create!(user_agent: request.user_agent)
session[:visitor_id] = visitor.id

visitor
end
end
9 changes: 9 additions & 0 deletions app/controllers/comments_controller.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
class CommentsController < ApplicationController
include TrackEvent
before_action :authenticate_member!
before_action :set_post, only: %i[new create]
before_action :set_comment, only: %i[edit update destroy]
before_action :set_event, only: %i[create update destroy]
after_action :track_event, only: %i[create update destroy]

def new
@comment = @post.comments.new
Expand All @@ -10,6 +13,7 @@ def new
def create
@comment = @post.comments.new(comment_params)
@comment.author = current_user
session[:comment_size] = @comment.text.size
respond_to do |format|
if @comment.save!
format.html { redirect_to member_posts_path(@post.author), notice: 'Comment was successfully created' }
Expand Down Expand Up @@ -63,4 +67,9 @@ def set_comment
def comment_params
params.require(:comment).permit(:text)
end

def set_event
session[:action] = params['action']
session[:post_author] = Post.find(params[:post_id]).author_id
end
end
8 changes: 8 additions & 0 deletions app/controllers/concerns/track_event.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module TrackEvent
extend ActiveSupport::Concern

def track_event
# puts "************************** size #{session[:comment_size]}"
CreateCounterJob.perform_async(session[:action], session[:post_author])
end
end
1 change: 1 addition & 0 deletions app/controllers/members/omniauth_callbacks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def handle_auth(auth)
if @member.persisted?
flash[:notice] = I18n.t 'devise.omniauth_callbacks.success', kind: auth
sign_in_and_redirect @member, event: :authentication
current_visitor&.update!(member: @member)
else
session['devise.auth_data'] = request.env['omniauth.auth'].except('extra')
redirect_to new_member_registration_url, alert: @member.errors.full_messages.join("\n")
Expand Down
7 changes: 7 additions & 0 deletions app/controllers/members/registrations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Members::RegistrationsController < Devise::RegistrationsController
after_action :after_signup, only: %i[create]

def after_signup
current_visitor&.update!(member: current_user)
end
end
7 changes: 7 additions & 0 deletions app/controllers/members/sessions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Members::SessionsController < Devise::SessionsController
after_action :after_login, only: %i[create]

def after_login
current_visitor&.update!(member: current_user)
end
end
2 changes: 2 additions & 0 deletions app/helpers/analytics_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module AnalyticsHelper
end
6 changes: 4 additions & 2 deletions app/javascript/application.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"
import "@hotwired/turbo-rails";
import "controllers";
import "chartkick";
import "Chart.bundle";
52 changes: 52 additions & 0 deletions app/javascript/controllers/chart_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="chart"
export default class extends Controller {
static targets = [ "counterAnalytic", "counter" ]
static values = { startDate: String, endDate: String }

connect = () => {
this.counterChart = new Chartkick.ColumnChart(this.counterTarget, this.counterData);
}

counterAnalyticTargetConnected = () => {
if (this.counterChart) {
this.counterChart.updateData(this.counterData);
};
}

createDateRange = () => {
const result = {};
const currentDate = new Date(this.startDateValue);

while (currentDate <= new Date(this.endDateValue)) {
const formattedDate = currentDate.toISOString().split('T')[0];
result[formattedDate] = 0;
currentDate.setDate(currentDate.getDate() + 1);
}

return result;
}

loadCounterData = (data, target, value) => {
if (value > 0) {
if (!data[target.dataset.action]) {
data[target.dataset.action] = {
name: target.dataset.action,
data: this.createDateRange()
};
}
if (data[target.dataset.action] != null && data[target.dataset.action].data[target.dataset.created] != null) {
data[target.dataset.action].data[target.dataset.created] += value;
};
}
}

counterData = (success) => {
let data = {};
this.counterAnalyticTargets.forEach(target => {
this.loadCounterData(data, target, parseInt(target.dataset.count, 10));
});
success(Object.values(data));
}
}
7 changes: 7 additions & 0 deletions app/models/counter_analytic.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class CounterAnalytic < ApplicationRecord
belongs_to :member

broadcasts_to lambda { |counter_analytic|
[counter_analytic.member_id, 'counter_analytics']
}, inserts_by: :prepend, partial: 'analytics/counter_analytics'
end
1 change: 1 addition & 0 deletions app/models/member.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class Member < ApplicationRecord
has_many :comments, foreign_key: 'author_id', dependent: :destroy
has_many :likes, foreign_key: 'author_id', dependent: :destroy
has_many :posts, foreign_key: 'author_id', dependent: :destroy
has_many :counter_analytics
has_one_attached :avatar do |attachable|
attachable.variant :thumb, resize_to_limit: [110, 110]
end
Expand Down
3 changes: 3 additions & 0 deletions app/models/visitor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Visitor < ApplicationRecord
belongs_to :member, optional: true
end
9 changes: 9 additions & 0 deletions app/sidekiq/create_counter_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class CreateCounterJob
include Sidekiq::Job

def perform(action, member)
event = CounterAnalytic.where(action:, member_id: member).first_or_create!
event.count += 1
event.save!
end
end
6 changes: 6 additions & 0 deletions app/views/analytics/_broadcast.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<%= turbo_stream_from current_member.id, "counter_analytics" %>
<div id="counter_analytics">
<% counter_analytics.each do |counter_analytic| %>
<%= render "counter_analytics", counter_analytic: counter_analytic %>
<% end %>
</div>
13 changes: 13 additions & 0 deletions app/views/analytics/_counter_analytics.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<%= tag.div id: dom_id(counter_analytic),
data: {
chart_target: "counterAnalytic",
action: counter_analytic.action.titleize,
created: counter_analytic.created_at.strftime("%Y-%m-%d"),
count: counter_analytic.count
} do %>
<p>
Action: <%= counter_analytic.action %>
Count: <%= counter_analytic.count %>
created: <%= counter_analytic.created_at %>
</p>
<% end %>
12 changes: 12 additions & 0 deletions app/views/analytics/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<h1>Analytics</h1>
<div
data-controller="chart"
data-chart-start-date-value=<%= @start_date %>
data-chart-end-date-value=<%= @end_date %>
>
<%= render "broadcast", counter_analytics: @counter_analytics %>
<p class="text-xl font-semibold">Counter</p>
<div data-chart-target="counter"></div>
</div>

<%= link_to "Back", root_path %>
3 changes: 3 additions & 0 deletions app/views/shared/_navbar.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
}
%>
</div>
<div class="navbar-item">
<%= link_to 'Analytics', analytics_path %>
</div>
<% end %>
</div>
<div class='navbar-end'>
Expand Down
2 changes: 2 additions & 0 deletions config/importmap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"
pin "chartkick", to: "chartkick.js"
pin "Chart.bundle", to: "Chart.bundle.js"
6 changes: 4 additions & 2 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
Rails.application.routes.draw do
get 'analytics/index', as: 'analytics'
mount Rswag::Ui::Engine => '/api-docs'
mount Rswag::Api::Engine => '/api-docs'
devise_for :members, controllers: { omniauth_callbacks: 'members/omniauth_callbacks' }

devise_for :members, controllers: { omniauth_callbacks: 'members/omniauth_callbacks',
sessions: "members/sessions",
registrations: "members/registrations" }
resources :members, only: %i[index show] do
resources :posts do
resources :comments
Expand Down
10 changes: 10 additions & 0 deletions db/migrate/20231129010134_create_visitors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class CreateVisitors < ActiveRecord::Migration[7.0]
def change
create_table :visitors do |t|
t.string :user_agent
t.references :member, foreign_key: { on_delete: :cascade }

t.timestamps
end
end
end
11 changes: 11 additions & 0 deletions db/migrate/20231129120527_create_counter_analytics.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class CreateCounterAnalytics < ActiveRecord::Migration[7.0]
def change
create_table :counter_analytics do |t|
t.string :action
t.integer :count, default: 0
t.references :member, null: false, foreign_key: { on_delete: :cascade }

t.timestamps
end
end
end
Loading
Loading