mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
Rename YearlyDigests to Users::Digests
This commit is contained in:
parent
c2ba645642
commit
c25bb6f4d4
40 changed files with 587 additions and 572 deletions
|
|
@ -1,13 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Shared::YearlyDigestsController < ApplicationController
|
||||
helper YearlyDigestsHelper
|
||||
class Shared::DigestsController < ApplicationController
|
||||
helper Users::DigestsHelper
|
||||
|
||||
before_action :authenticate_user!, except: [:show]
|
||||
before_action :authenticate_active_user!, only: [:update]
|
||||
|
||||
def show
|
||||
@digest = YearlyDigest.find_by(sharing_uuid: params[:uuid])
|
||||
@digest = Users::Digest.find_by(sharing_uuid: params[:uuid])
|
||||
|
||||
unless @digest&.public_accessible?
|
||||
return redirect_to root_path,
|
||||
|
|
@ -19,18 +19,18 @@ class Shared::YearlyDigestsController < ApplicationController
|
|||
@distance_unit = @user.safe_settings.distance_unit || 'km'
|
||||
@is_public_view = true
|
||||
|
||||
render 'yearly_digests/public_year'
|
||||
render 'users/digests/public_year'
|
||||
end
|
||||
|
||||
def update
|
||||
@year = params[:year].to_i
|
||||
@digest = current_user.yearly_digests.yearly.find_by(year: @year)
|
||||
@digest = current_user.digests.yearly.find_by(year: @year)
|
||||
|
||||
return head :not_found unless @digest
|
||||
|
||||
if params[:enabled] == '1'
|
||||
@digest.enable_sharing!(expiration: params[:expiration] || '24h')
|
||||
sharing_url = shared_yearly_digest_url(@digest.sharing_uuid)
|
||||
sharing_url = shared_users_digest_url(@digest.sharing_uuid)
|
||||
|
||||
render json: {
|
||||
success: true,
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class YearlyDigestsController < ApplicationController
|
||||
helper YearlyDigestsHelper
|
||||
class Users::DigestsController < ApplicationController
|
||||
helper Users::DigestsHelper
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :authenticate_active_user!, only: [:create]
|
||||
before_action :set_digest, only: [:show]
|
||||
|
||||
def index
|
||||
@digests = current_user.yearly_digests.yearly.order(year: :desc)
|
||||
@digests = current_user.digests.yearly.order(year: :desc)
|
||||
@available_years = available_years_for_generation
|
||||
end
|
||||
|
||||
|
|
@ -20,26 +20,26 @@ class YearlyDigestsController < ApplicationController
|
|||
year = params[:year].to_i
|
||||
|
||||
if valid_year?(year)
|
||||
YearlyDigests::CalculatingJob.perform_later(current_user.id, year)
|
||||
redirect_to yearly_digests_path,
|
||||
Users::Digests::CalculatingJob.perform_later(current_user.id, year)
|
||||
redirect_to users_digests_path,
|
||||
notice: "Year-end digest for #{year} is being generated. Check back soon!",
|
||||
status: :see_other
|
||||
else
|
||||
redirect_to yearly_digests_path, alert: 'Invalid year selected', status: :see_other
|
||||
redirect_to users_digests_path, alert: 'Invalid year selected', status: :see_other
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_digest
|
||||
@digest = current_user.yearly_digests.yearly.find_by!(year: params[:year])
|
||||
@digest = current_user.digests.yearly.find_by!(year: params[:year])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to yearly_digests_path, alert: 'Digest not found'
|
||||
redirect_to users_digests_path, alert: 'Digest not found'
|
||||
end
|
||||
|
||||
def available_years_for_generation
|
||||
tracked_years = current_user.stats.select(:year).distinct.pluck(:year)
|
||||
existing_digests = current_user.yearly_digests.yearly.pluck(:year)
|
||||
existing_digests = current_user.digests.yearly.pluck(:year)
|
||||
|
||||
(tracked_years - existing_digests).sort.reverse
|
||||
end
|
||||
53
app/helpers/users/digests_helper.rb
Normal file
53
app/helpers/users/digests_helper.rb
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Users
|
||||
module DigestsHelper
|
||||
EARTH_CIRCUMFERENCE_KM = 40_075
|
||||
MOON_DISTANCE_KM = 384_400
|
||||
|
||||
def distance_with_unit(distance_meters, unit)
|
||||
value = Users::Digest.convert_distance(distance_meters, unit).round
|
||||
"#{number_with_delimiter(value)} #{unit}"
|
||||
end
|
||||
|
||||
def distance_comparison_text(distance_meters)
|
||||
distance_km = distance_meters.to_f / 1000
|
||||
|
||||
if distance_km >= MOON_DISTANCE_KM
|
||||
percentage = ((distance_km / MOON_DISTANCE_KM) * 100).round(1)
|
||||
"That's #{percentage}% of the distance to the Moon!"
|
||||
else
|
||||
percentage = ((distance_km / EARTH_CIRCUMFERENCE_KM) * 100).round(1)
|
||||
"That's #{percentage}% of Earth's circumference!"
|
||||
end
|
||||
end
|
||||
|
||||
def format_time_spent(minutes)
|
||||
return "#{minutes} minutes" if minutes < 60
|
||||
|
||||
hours = minutes / 60
|
||||
remaining_minutes = minutes % 60
|
||||
|
||||
if hours < 24
|
||||
"#{hours}h #{remaining_minutes}m"
|
||||
else
|
||||
days = hours / 24
|
||||
remaining_hours = hours % 24
|
||||
"#{days}d #{remaining_hours}h"
|
||||
end
|
||||
end
|
||||
|
||||
def yoy_change_class(change)
|
||||
return '' if change.nil?
|
||||
|
||||
change.negative? ? 'negative' : 'positive'
|
||||
end
|
||||
|
||||
def yoy_change_text(change)
|
||||
return '' if change.nil?
|
||||
|
||||
prefix = change.positive? ? '+' : ''
|
||||
"#{prefix}#{change}%"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module YearlyDigestsHelper
|
||||
EARTH_CIRCUMFERENCE_KM = 40_075
|
||||
MOON_DISTANCE_KM = 384_400
|
||||
|
||||
def distance_with_unit(distance_meters, unit)
|
||||
value = YearlyDigest.convert_distance(distance_meters, unit).round
|
||||
"#{number_with_delimiter(value)} #{unit}"
|
||||
end
|
||||
|
||||
def distance_comparison_text(distance_meters)
|
||||
distance_km = distance_meters.to_f / 1000
|
||||
|
||||
if distance_km >= MOON_DISTANCE_KM
|
||||
percentage = ((distance_km / MOON_DISTANCE_KM) * 100).round(1)
|
||||
"That's #{percentage}% of the distance to the Moon!"
|
||||
else
|
||||
percentage = ((distance_km / EARTH_CIRCUMFERENCE_KM) * 100).round(1)
|
||||
"That's #{percentage}% of Earth's circumference!"
|
||||
end
|
||||
end
|
||||
|
||||
def format_time_spent(minutes)
|
||||
return "#{minutes} minutes" if minutes < 60
|
||||
|
||||
hours = minutes / 60
|
||||
remaining_minutes = minutes % 60
|
||||
|
||||
if hours < 24
|
||||
"#{hours}h #{remaining_minutes}m"
|
||||
else
|
||||
days = hours / 24
|
||||
remaining_hours = hours % 24
|
||||
"#{days}d #{remaining_hours}h"
|
||||
end
|
||||
end
|
||||
|
||||
def yoy_change_class(change)
|
||||
return '' if change.nil?
|
||||
|
||||
change.negative? ? 'negative' : 'positive'
|
||||
end
|
||||
|
||||
def yoy_change_text(change)
|
||||
return '' if change.nil?
|
||||
|
||||
prefix = change.positive? ? '+' : ''
|
||||
"#{prefix}#{change}%"
|
||||
end
|
||||
end
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class YearlyDigests::CalculatingJob < ApplicationJob
|
||||
class Users::Digests::CalculatingJob < ApplicationJob
|
||||
queue_as :digests
|
||||
|
||||
def perform(user_id, year)
|
||||
YearlyDigests::CalculateYear.new(user_id, year).call
|
||||
Users::Digests::CalculateYear.new(user_id, year).call
|
||||
rescue StandardError => e
|
||||
create_digest_failed_notification(user_id, e)
|
||||
end
|
||||
|
|
@ -1,20 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class YearlyDigests::EmailSendingJob < ApplicationJob
|
||||
class Users::Digests::EmailSendingJob < ApplicationJob
|
||||
queue_as :mailers
|
||||
|
||||
def perform(user_id, year)
|
||||
user = User.find(user_id)
|
||||
digest = user.yearly_digests.yearly.find_by(year: year)
|
||||
digest = user.digests.yearly.find_by(year: year)
|
||||
|
||||
return unless should_send_email?(user, digest)
|
||||
|
||||
YearlyDigestsMailer.with(user: user, digest: digest).year_end_digest.deliver_later
|
||||
Users::DigestsMailer.with(user: user, digest: digest).year_end_digest.deliver_later
|
||||
|
||||
digest.update!(sent_at: Time.current)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
ExceptionReporter.call(
|
||||
'YearlyDigests::EmailSendingJob',
|
||||
'Users::Digests::EmailSendingJob',
|
||||
"User with ID #{user_id} not found. Skipping year-end digest email."
|
||||
)
|
||||
end
|
||||
|
|
@ -1,20 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class YearlyDigests::YearEndSchedulingJob < ApplicationJob
|
||||
class Users::Digests::YearEndSchedulingJob < ApplicationJob
|
||||
queue_as :digests
|
||||
|
||||
def perform
|
||||
year = Time.current.year - 1 # Previous year's digest
|
||||
|
||||
User.active_or_trial.find_each do |user|
|
||||
::User.active_or_trial.find_each do |user|
|
||||
# Skip if user has no data for the year
|
||||
next unless user.stats.where(year: year).exists?
|
||||
|
||||
# Schedule calculation first
|
||||
YearlyDigests::CalculatingJob.perform_later(user.id, year)
|
||||
Users::Digests::CalculatingJob.perform_later(user.id, year)
|
||||
|
||||
# Schedule email with delay to allow calculation to complete
|
||||
YearlyDigests::EmailSendingJob.set(wait: 30.minutes).perform_later(user.id, year)
|
||||
Users::Digests::EmailSendingJob.set(wait: 30.minutes).perform_later(user.id, year)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class YearlyDigestsMailer < ApplicationMailer
|
||||
helper YearlyDigestsHelper
|
||||
class Users::DigestsMailer < ApplicationMailer
|
||||
helper Users::DigestsHelper
|
||||
|
||||
def year_end_digest
|
||||
@user = params[:user]
|
||||
|
|
@ -20,7 +20,7 @@ class YearlyDigestsMailer < ApplicationMailer
|
|||
private
|
||||
|
||||
def generate_chart_attachment
|
||||
image_data = YearlyDigests::ChartImageGenerator.new(@digest, distance_unit: @distance_unit).call
|
||||
image_data = Users::Digests::ChartImageGenerator.new(@digest, distance_unit: @distance_unit).call
|
||||
filename = 'monthly_distance_chart.png'
|
||||
|
||||
attachments.inline[filename] = {
|
||||
|
|
@ -21,7 +21,7 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
|
|||
has_many :trips, dependent: :destroy
|
||||
has_many :tracks, dependent: :destroy
|
||||
has_many :raw_data_archives, class_name: 'Points::RawDataArchive', dependent: :destroy
|
||||
has_many :yearly_digests, dependent: :destroy
|
||||
has_many :digests, class_name: 'Users::Digest', dependent: :destroy
|
||||
|
||||
after_create :create_api_key
|
||||
after_commit :activate, on: :create, if: -> { DawarichSettings.self_hosted? }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class YearlyDigest < ApplicationRecord
|
||||
class Users::Digest < ApplicationRecord
|
||||
self.table_name = 'digests'
|
||||
|
||||
include DistanceConvertible
|
||||
133
app/services/users/digests/calculate_year.rb
Normal file
133
app/services/users/digests/calculate_year.rb
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Users
|
||||
module Digests
|
||||
class CalculateYear
|
||||
def initialize(user_id, year)
|
||||
@user = ::User.find(user_id)
|
||||
@year = year.to_i
|
||||
end
|
||||
|
||||
def call
|
||||
return nil if monthly_stats.empty?
|
||||
|
||||
digest = Users::Digest.find_or_initialize_by(user: user, year: year, period_type: :yearly)
|
||||
|
||||
digest.assign_attributes(
|
||||
distance: total_distance,
|
||||
toponyms: aggregate_toponyms,
|
||||
monthly_distances: build_monthly_distances,
|
||||
time_spent_by_location: calculate_time_spent,
|
||||
first_time_visits: calculate_first_time_visits,
|
||||
year_over_year: calculate_yoy_comparison,
|
||||
all_time_stats: calculate_all_time_stats
|
||||
)
|
||||
|
||||
digest.save!
|
||||
digest
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :year
|
||||
|
||||
def monthly_stats
|
||||
@monthly_stats ||= user.stats.where(year: year).order(:month)
|
||||
end
|
||||
|
||||
def total_distance
|
||||
monthly_stats.sum(:distance)
|
||||
end
|
||||
|
||||
def aggregate_toponyms
|
||||
countries = []
|
||||
cities = []
|
||||
|
||||
monthly_stats.each do |stat|
|
||||
toponyms = stat.toponyms
|
||||
next unless toponyms.is_a?(Array)
|
||||
|
||||
toponyms.each do |toponym|
|
||||
next unless toponym.is_a?(Hash)
|
||||
|
||||
countries << toponym['country'] if toponym['country'].present?
|
||||
|
||||
next unless toponym['cities'].is_a?(Array)
|
||||
|
||||
toponym['cities'].each do |city|
|
||||
cities << city['city'] if city.is_a?(Hash) && city['city'].present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
{
|
||||
'countries' => countries.uniq.compact.sort,
|
||||
'cities' => cities.uniq.compact.sort
|
||||
}
|
||||
end
|
||||
|
||||
def build_monthly_distances
|
||||
result = {}
|
||||
|
||||
monthly_stats.each do |stat|
|
||||
result[stat.month.to_s] = stat.distance
|
||||
end
|
||||
|
||||
# Fill in missing months with 0
|
||||
(1..12).each do |month|
|
||||
result[month.to_s] ||= 0
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def calculate_time_spent
|
||||
country_time = Hash.new(0)
|
||||
city_time = Hash.new(0)
|
||||
|
||||
monthly_stats.each do |stat|
|
||||
toponyms = stat.toponyms
|
||||
next unless toponyms.is_a?(Array)
|
||||
|
||||
toponyms.each do |toponym|
|
||||
next unless toponym.is_a?(Hash)
|
||||
|
||||
country = toponym['country']
|
||||
next unless toponym['cities'].is_a?(Array)
|
||||
|
||||
toponym['cities'].each do |city|
|
||||
next unless city.is_a?(Hash)
|
||||
|
||||
stayed_for = city['stayed_for'].to_i
|
||||
city_name = city['city']
|
||||
|
||||
country_time[country] += stayed_for if country.present?
|
||||
city_time[city_name] += stayed_for if city_name.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
{
|
||||
'countries' => country_time.sort_by { |_, v| -v }.first(10).map { |name, minutes| { 'name' => name, 'minutes' => minutes } },
|
||||
'cities' => city_time.sort_by { |_, v| -v }.first(10).map { |name, minutes| { 'name' => name, 'minutes' => minutes } }
|
||||
}
|
||||
end
|
||||
|
||||
def calculate_first_time_visits
|
||||
FirstTimeVisitsCalculator.new(user, year).call
|
||||
end
|
||||
|
||||
def calculate_yoy_comparison
|
||||
YearOverYearCalculator.new(user, year).call
|
||||
end
|
||||
|
||||
def calculate_all_time_stats
|
||||
{
|
||||
'total_countries' => user.countries_visited.count,
|
||||
'total_cities' => user.cities_visited.count,
|
||||
'total_distance' => user.stats.sum(:distance)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
42
app/services/users/digests/chart_image_generator.rb
Normal file
42
app/services/users/digests/chart_image_generator.rb
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Users
|
||||
module Digests
|
||||
class ChartImageGenerator
|
||||
def initialize(digest, distance_unit: 'km')
|
||||
@digest = digest
|
||||
@distance_unit = distance_unit
|
||||
end
|
||||
|
||||
def call
|
||||
html = render_chart_html
|
||||
generate_image(html)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :digest, :distance_unit
|
||||
|
||||
def render_chart_html
|
||||
ApplicationController.render(
|
||||
template: 'user/digests/chart',
|
||||
layout: false,
|
||||
assigns: {
|
||||
monthly_distances: digest.monthly_distances,
|
||||
distance_unit: distance_unit
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def generate_image(html)
|
||||
grover = Grover.new(
|
||||
html,
|
||||
format: 'png',
|
||||
viewport: { width: 600, height: 320 },
|
||||
wait_until: 'networkidle0'
|
||||
)
|
||||
grover.to_png
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
77
app/services/users/digests/first_time_visits_calculator.rb
Normal file
77
app/services/users/digests/first_time_visits_calculator.rb
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Users
|
||||
module Digests
|
||||
class FirstTimeVisitsCalculator
|
||||
def initialize(user, year)
|
||||
@user = user
|
||||
@year = year.to_i
|
||||
end
|
||||
|
||||
def call
|
||||
{
|
||||
'countries' => first_time_countries,
|
||||
'cities' => first_time_cities
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :year
|
||||
|
||||
def previous_years_stats
|
||||
@previous_years_stats ||= user.stats.where('year < ?', year)
|
||||
end
|
||||
|
||||
def current_year_stats
|
||||
@current_year_stats ||= user.stats.where(year: year)
|
||||
end
|
||||
|
||||
def previous_countries
|
||||
@previous_countries ||= extract_countries(previous_years_stats)
|
||||
end
|
||||
|
||||
def previous_cities
|
||||
@previous_cities ||= extract_cities(previous_years_stats)
|
||||
end
|
||||
|
||||
def current_countries
|
||||
@current_countries ||= extract_countries(current_year_stats)
|
||||
end
|
||||
|
||||
def current_cities
|
||||
@current_cities ||= extract_cities(current_year_stats)
|
||||
end
|
||||
|
||||
def first_time_countries
|
||||
(current_countries - previous_countries).sort
|
||||
end
|
||||
|
||||
def first_time_cities
|
||||
(current_cities - previous_cities).sort
|
||||
end
|
||||
|
||||
def extract_countries(stats)
|
||||
stats.flat_map do |stat|
|
||||
toponyms = stat.toponyms
|
||||
next [] unless toponyms.is_a?(Array)
|
||||
|
||||
toponyms.filter_map { |t| t['country'] if t.is_a?(Hash) }
|
||||
end.uniq.compact
|
||||
end
|
||||
|
||||
def extract_cities(stats)
|
||||
stats.flat_map do |stat|
|
||||
toponyms = stat.toponyms
|
||||
next [] unless toponyms.is_a?(Array)
|
||||
|
||||
toponyms.flat_map do |t|
|
||||
next [] unless t.is_a?(Hash) && t['cities'].is_a?(Array)
|
||||
|
||||
t['cities'].filter_map { |c| c['city'] if c.is_a?(Hash) }
|
||||
end
|
||||
end.uniq.compact
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
79
app/services/users/digests/year_over_year_calculator.rb
Normal file
79
app/services/users/digests/year_over_year_calculator.rb
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Users
|
||||
module Digests
|
||||
class YearOverYearCalculator
|
||||
def initialize(user, year)
|
||||
@user = user
|
||||
@year = year.to_i
|
||||
end
|
||||
|
||||
def call
|
||||
return {} unless previous_year_stats.exists?
|
||||
|
||||
{
|
||||
'previous_year' => year - 1,
|
||||
'distance_change_percent' => calculate_distance_change_percent,
|
||||
'countries_change' => calculate_countries_change,
|
||||
'cities_change' => calculate_cities_change
|
||||
}.compact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :year
|
||||
|
||||
def previous_year_stats
|
||||
@previous_year_stats ||= user.stats.where(year: year - 1)
|
||||
end
|
||||
|
||||
def current_year_stats
|
||||
@current_year_stats ||= user.stats.where(year: year)
|
||||
end
|
||||
|
||||
def calculate_distance_change_percent
|
||||
prev_distance = previous_year_stats.sum(:distance)
|
||||
return nil if prev_distance.zero?
|
||||
|
||||
curr_distance = current_year_stats.sum(:distance)
|
||||
((curr_distance - prev_distance).to_f / prev_distance * 100).round
|
||||
end
|
||||
|
||||
def calculate_countries_change
|
||||
prev_count = count_countries(previous_year_stats)
|
||||
curr_count = count_countries(current_year_stats)
|
||||
|
||||
curr_count - prev_count
|
||||
end
|
||||
|
||||
def calculate_cities_change
|
||||
prev_count = count_cities(previous_year_stats)
|
||||
curr_count = count_cities(current_year_stats)
|
||||
|
||||
curr_count - prev_count
|
||||
end
|
||||
|
||||
def count_countries(stats)
|
||||
stats.flat_map do |stat|
|
||||
toponyms = stat.toponyms
|
||||
next [] unless toponyms.is_a?(Array)
|
||||
|
||||
toponyms.filter_map { |t| t['country'] if t.is_a?(Hash) }
|
||||
end.uniq.compact.count
|
||||
end
|
||||
|
||||
def count_cities(stats)
|
||||
stats.flat_map do |stat|
|
||||
toponyms = stat.toponyms
|
||||
next [] unless toponyms.is_a?(Array)
|
||||
|
||||
toponyms.flat_map do |t|
|
||||
next [] unless t.is_a?(Hash) && t['cities'].is_a?(Array)
|
||||
|
||||
t['cities'].filter_map { |c| c['city'] if c.is_a?(Hash) }
|
||||
end
|
||||
end.uniq.compact.count
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module YearlyDigests
|
||||
class CalculateYear
|
||||
def initialize(user_id, year)
|
||||
@user = User.find(user_id)
|
||||
@year = year.to_i
|
||||
end
|
||||
|
||||
def call
|
||||
return nil if monthly_stats.empty?
|
||||
|
||||
digest = YearlyDigest.find_or_initialize_by(user: user, year: year, period_type: :yearly)
|
||||
|
||||
digest.assign_attributes(
|
||||
distance: total_distance,
|
||||
toponyms: aggregate_toponyms,
|
||||
monthly_distances: build_monthly_distances,
|
||||
time_spent_by_location: calculate_time_spent,
|
||||
first_time_visits: calculate_first_time_visits,
|
||||
year_over_year: calculate_yoy_comparison,
|
||||
all_time_stats: calculate_all_time_stats
|
||||
)
|
||||
|
||||
digest.save!
|
||||
digest
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :year
|
||||
|
||||
def monthly_stats
|
||||
@monthly_stats ||= user.stats.where(year: year).order(:month)
|
||||
end
|
||||
|
||||
def total_distance
|
||||
monthly_stats.sum(:distance)
|
||||
end
|
||||
|
||||
def aggregate_toponyms
|
||||
countries = []
|
||||
cities = []
|
||||
|
||||
monthly_stats.each do |stat|
|
||||
toponyms = stat.toponyms
|
||||
next unless toponyms.is_a?(Array)
|
||||
|
||||
toponyms.each do |toponym|
|
||||
next unless toponym.is_a?(Hash)
|
||||
|
||||
countries << toponym['country'] if toponym['country'].present?
|
||||
|
||||
next unless toponym['cities'].is_a?(Array)
|
||||
|
||||
toponym['cities'].each do |city|
|
||||
cities << city['city'] if city.is_a?(Hash) && city['city'].present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
{
|
||||
'countries' => countries.uniq.compact.sort,
|
||||
'cities' => cities.uniq.compact.sort
|
||||
}
|
||||
end
|
||||
|
||||
def build_monthly_distances
|
||||
result = {}
|
||||
|
||||
monthly_stats.each do |stat|
|
||||
result[stat.month.to_s] = stat.distance
|
||||
end
|
||||
|
||||
# Fill in missing months with 0
|
||||
(1..12).each do |month|
|
||||
result[month.to_s] ||= 0
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def calculate_time_spent
|
||||
country_time = Hash.new(0)
|
||||
city_time = Hash.new(0)
|
||||
|
||||
monthly_stats.each do |stat|
|
||||
toponyms = stat.toponyms
|
||||
next unless toponyms.is_a?(Array)
|
||||
|
||||
toponyms.each do |toponym|
|
||||
next unless toponym.is_a?(Hash)
|
||||
|
||||
country = toponym['country']
|
||||
next unless toponym['cities'].is_a?(Array)
|
||||
|
||||
toponym['cities'].each do |city|
|
||||
next unless city.is_a?(Hash)
|
||||
|
||||
stayed_for = city['stayed_for'].to_i
|
||||
city_name = city['city']
|
||||
|
||||
country_time[country] += stayed_for if country.present?
|
||||
city_time[city_name] += stayed_for if city_name.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
{
|
||||
'countries' => country_time.sort_by { |_, v| -v }.first(10).map { |name, minutes| { 'name' => name, 'minutes' => minutes } },
|
||||
'cities' => city_time.sort_by { |_, v| -v }.first(10).map { |name, minutes| { 'name' => name, 'minutes' => minutes } }
|
||||
}
|
||||
end
|
||||
|
||||
def calculate_first_time_visits
|
||||
FirstTimeVisitsCalculator.new(user, year).call
|
||||
end
|
||||
|
||||
def calculate_yoy_comparison
|
||||
YearOverYearCalculator.new(user, year).call
|
||||
end
|
||||
|
||||
def calculate_all_time_stats
|
||||
{
|
||||
'total_countries' => user.countries_visited.count,
|
||||
'total_cities' => user.cities_visited.count,
|
||||
'total_distance' => user.stats.sum(:distance)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module YearlyDigests
|
||||
class ChartImageGenerator
|
||||
def initialize(digest, distance_unit: 'km')
|
||||
@digest = digest
|
||||
@distance_unit = distance_unit
|
||||
end
|
||||
|
||||
def call
|
||||
html = render_chart_html
|
||||
generate_image(html)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :digest, :distance_unit
|
||||
|
||||
def render_chart_html
|
||||
ApplicationController.render(
|
||||
template: 'yearly_digests/chart',
|
||||
layout: false,
|
||||
assigns: {
|
||||
monthly_distances: digest.monthly_distances,
|
||||
distance_unit: distance_unit
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def generate_image(html)
|
||||
grover = Grover.new(
|
||||
html,
|
||||
format: 'png',
|
||||
viewport: { width: 600, height: 320 },
|
||||
wait_until: 'networkidle0'
|
||||
)
|
||||
grover.to_png
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module YearlyDigests
|
||||
class FirstTimeVisitsCalculator
|
||||
def initialize(user, year)
|
||||
@user = user
|
||||
@year = year.to_i
|
||||
end
|
||||
|
||||
def call
|
||||
{
|
||||
'countries' => first_time_countries,
|
||||
'cities' => first_time_cities
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :year
|
||||
|
||||
def previous_years_stats
|
||||
@previous_years_stats ||= user.stats.where('year < ?', year)
|
||||
end
|
||||
|
||||
def current_year_stats
|
||||
@current_year_stats ||= user.stats.where(year: year)
|
||||
end
|
||||
|
||||
def previous_countries
|
||||
@previous_countries ||= extract_countries(previous_years_stats)
|
||||
end
|
||||
|
||||
def previous_cities
|
||||
@previous_cities ||= extract_cities(previous_years_stats)
|
||||
end
|
||||
|
||||
def current_countries
|
||||
@current_countries ||= extract_countries(current_year_stats)
|
||||
end
|
||||
|
||||
def current_cities
|
||||
@current_cities ||= extract_cities(current_year_stats)
|
||||
end
|
||||
|
||||
def first_time_countries
|
||||
(current_countries - previous_countries).sort
|
||||
end
|
||||
|
||||
def first_time_cities
|
||||
(current_cities - previous_cities).sort
|
||||
end
|
||||
|
||||
def extract_countries(stats)
|
||||
stats.flat_map do |stat|
|
||||
toponyms = stat.toponyms
|
||||
next [] unless toponyms.is_a?(Array)
|
||||
|
||||
toponyms.filter_map { |t| t['country'] if t.is_a?(Hash) }
|
||||
end.uniq.compact
|
||||
end
|
||||
|
||||
def extract_cities(stats)
|
||||
stats.flat_map do |stat|
|
||||
toponyms = stat.toponyms
|
||||
next [] unless toponyms.is_a?(Array)
|
||||
|
||||
toponyms.flat_map do |t|
|
||||
next [] unless t.is_a?(Hash) && t['cities'].is_a?(Array)
|
||||
|
||||
t['cities'].filter_map { |c| c['city'] if c.is_a?(Hash) }
|
||||
end
|
||||
end.uniq.compact
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module YearlyDigests
|
||||
class YearOverYearCalculator
|
||||
def initialize(user, year)
|
||||
@user = user
|
||||
@year = year.to_i
|
||||
end
|
||||
|
||||
def call
|
||||
return {} unless previous_year_stats.exists?
|
||||
|
||||
{
|
||||
'previous_year' => year - 1,
|
||||
'distance_change_percent' => calculate_distance_change_percent,
|
||||
'countries_change' => calculate_countries_change,
|
||||
'cities_change' => calculate_cities_change
|
||||
}.compact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :year
|
||||
|
||||
def previous_year_stats
|
||||
@previous_year_stats ||= user.stats.where(year: year - 1)
|
||||
end
|
||||
|
||||
def current_year_stats
|
||||
@current_year_stats ||= user.stats.where(year: year)
|
||||
end
|
||||
|
||||
def calculate_distance_change_percent
|
||||
prev_distance = previous_year_stats.sum(:distance)
|
||||
return nil if prev_distance.zero?
|
||||
|
||||
curr_distance = current_year_stats.sum(:distance)
|
||||
((curr_distance - prev_distance).to_f / prev_distance * 100).round
|
||||
end
|
||||
|
||||
def calculate_countries_change
|
||||
prev_count = count_countries(previous_year_stats)
|
||||
curr_count = count_countries(current_year_stats)
|
||||
|
||||
curr_count - prev_count
|
||||
end
|
||||
|
||||
def calculate_cities_change
|
||||
prev_count = count_cities(previous_year_stats)
|
||||
curr_count = count_cities(current_year_stats)
|
||||
|
||||
curr_count - prev_count
|
||||
end
|
||||
|
||||
def count_countries(stats)
|
||||
stats.flat_map do |stat|
|
||||
toponyms = stat.toponyms
|
||||
next [] unless toponyms.is_a?(Array)
|
||||
|
||||
toponyms.filter_map { |t| t['country'] if t.is_a?(Hash) }
|
||||
end.uniq.compact.count
|
||||
end
|
||||
|
||||
def count_cities(stats)
|
||||
stats.flat_map do |stat|
|
||||
toponyms = stat.toponyms
|
||||
next [] unless toponyms.is_a?(Array)
|
||||
|
||||
toponyms.flat_map do |t|
|
||||
next [] unless t.is_a?(Hash) && t['cities'].is_a?(Array)
|
||||
|
||||
t['cities'].filter_map { |c| c['city'] if c.is_a?(Hash) }
|
||||
end
|
||||
end.uniq.compact.count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<div class="w-full my-5">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-3xl font-bold">Statistics</h1>
|
||||
<%= link_to yearly_digests_path, class: 'btn btn-outline btn-sm' do %>
|
||||
<%= link_to users_digests_path, class: 'btn btn-outline btn-sm' do %>
|
||||
<%= icon 'earth' %> Year-End Digests
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<% @available_years.each do |year| %>
|
||||
<li>
|
||||
<%= link_to year, yearly_digests_path(year: year),
|
||||
<%= link_to year, users_digests_path(year: year),
|
||||
data: { turbo_method: :post },
|
||||
class: 'text-base' %>
|
||||
</li>
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
<div class="card bg-base-200 shadow-xl hover:shadow-2xl transition-shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl justify-between">
|
||||
<%= link_to digest.year, yearly_digest_path(year: digest.year), class: 'hover:text-primary' %>
|
||||
<%= link_to digest.year, users_digest_path(year: digest.year), class: 'hover:text-primary' %>
|
||||
<% if digest.sharing_enabled? %>
|
||||
<span class="badge badge-success badge-sm">Shared</span>
|
||||
<% end %>
|
||||
|
|
@ -79,7 +79,7 @@
|
|||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<%= link_to yearly_digest_path(year: digest.year), class: 'btn btn-primary btn-sm' do %>
|
||||
<%= link_to users_digest_path(year: digest.year), class: 'btn btn-primary btn-sm' do %>
|
||||
View Details
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
<div class="w-full h-48 bg-base-200 rounded-lg p-4 relative">
|
||||
<%= column_chart(
|
||||
@digest.monthly_distances.sort.map { |month, distance_meters|
|
||||
[Date::ABBR_MONTHNAMES[month.to_i], YearlyDigest.convert_distance(distance_meters, @distance_unit).round]
|
||||
[Date::ABBR_MONTHNAMES[month.to_i], Users::Digest.convert_distance(distance_meters, @distance_unit).round]
|
||||
},
|
||||
height: '200px',
|
||||
suffix: " #{@distance_unit}",
|
||||
|
|
@ -106,7 +106,7 @@
|
|||
<div class="w-full h-64 bg-base-100 rounded-lg p-4">
|
||||
<%= column_chart(
|
||||
@digest.monthly_distances.sort.map { |month, distance_meters|
|
||||
[Date::ABBR_MONTHNAMES[month.to_i], YearlyDigest.convert_distance(distance_meters, @distance_unit).round]
|
||||
[Date::ABBR_MONTHNAMES[month.to_i], Users::Digest.convert_distance(distance_meters, @distance_unit).round]
|
||||
},
|
||||
height: '250px',
|
||||
suffix: " #{@distance_unit}",
|
||||
|
|
@ -206,7 +206,7 @@
|
|||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-wrap gap-4 justify-center">
|
||||
<%= link_to yearly_digests_path, class: 'btn btn-outline' do %>
|
||||
<%= link_to users_digests_path, class: 'btn btn-outline' do %>
|
||||
Back to All Digests
|
||||
<% end %>
|
||||
<button class="btn btn-outline" onclick="sharing_modal.showModal()">
|
||||
|
|
@ -227,7 +227,7 @@
|
|||
</h3>
|
||||
|
||||
<div data-controller="sharing-modal"
|
||||
data-sharing-modal-url-value="<%= sharing_yearly_digest_path(year: @digest.year) %>">
|
||||
data-sharing-modal-url-value="<%= sharing_users_digest_path(year: @digest.year) %>">
|
||||
|
||||
<!-- Enable/Disable Sharing Toggle -->
|
||||
<div class="form-control mb-4">
|
||||
|
|
@ -275,7 +275,7 @@
|
|||
readonly
|
||||
class="input input-bordered join-item flex-1"
|
||||
data-sharing-modal-target="sharingLink"
|
||||
value="<%= @digest.sharing_enabled? ? shared_yearly_digest_url(@digest.sharing_uuid) : '' %>" />
|
||||
value="<%= @digest.sharing_enabled? ? shared_users_digest_url(@digest.sharing_uuid) : '' %>" />
|
||||
<button type="button"
|
||||
class="btn btn-outline join-item"
|
||||
data-action="click->sharing-modal#copyLink">
|
||||
|
|
@ -98,13 +98,15 @@ Rails.application.routes.draw do
|
|||
as: :sharing_stats,
|
||||
constraints: { year: /\d{4}/, month: /\d{1,2}/ }
|
||||
|
||||
# Yearly digests routes
|
||||
resources :yearly_digests, only: %i[index create], param: :year
|
||||
get 'yearly_digests/:year', to: 'yearly_digests#show', as: :yearly_digest, constraints: { year: /\d{4}/ }
|
||||
get 'shared/year/:uuid', to: 'shared/yearly_digests#show', as: :shared_yearly_digest
|
||||
patch 'yearly_digests/:year/sharing',
|
||||
to: 'shared/yearly_digests#update',
|
||||
as: :sharing_yearly_digest,
|
||||
# User digests routes (yearly/monthly digest reports)
|
||||
scope module: 'users' do
|
||||
resources :digests, only: %i[index create], param: :year, as: :users_digests
|
||||
get 'digests/:year', to: 'digests#show', as: :users_digest, constraints: { year: /\d{4}/ }
|
||||
end
|
||||
get 'shared/digest/:uuid', to: 'shared/digests#show', as: :shared_users_digest
|
||||
patch 'digests/:year/sharing',
|
||||
to: 'shared/digests#update',
|
||||
as: :sharing_users_digest,
|
||||
constraints: { year: /\d{4}/ }
|
||||
|
||||
root to: 'home#index'
|
||||
|
|
|
|||
|
|
@ -52,5 +52,5 @@ nightly_family_invitations_cleanup_job:
|
|||
|
||||
year_end_digest_job:
|
||||
cron: "0 0 1 1 *" # January 1st at 00:00
|
||||
class: "YearlyDigests::YearEndSchedulingJob"
|
||||
class: "Users::Digests::YearEndSchedulingJob"
|
||||
queue: digests
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
namespace :points do
|
||||
namespace :raw_data do
|
||||
desc 'Restore raw_data from archive to database for a specific month'
|
||||
task :restore, [:user_id, :year, :month] => :environment do |_t, args|
|
||||
task :restore, %i[user_id year month] => :environment do |_t, args|
|
||||
validate_args!(args)
|
||||
|
||||
user_id = args[:user_id].to_i
|
||||
|
|
@ -27,7 +27,7 @@ namespace :points do
|
|||
end
|
||||
|
||||
desc 'Restore raw_data to memory/cache temporarily (for data migrations)'
|
||||
task :restore_temporary, [:user_id, :year, :month] => :environment do |_t, args|
|
||||
task :restore_temporary, %i[user_id year month] => :environment do |_t, args|
|
||||
validate_args!(args)
|
||||
|
||||
user_id = args[:user_id].to_i
|
||||
|
|
@ -69,9 +69,9 @@ namespace :points do
|
|||
puts ''
|
||||
|
||||
archives = Points::RawDataArchive.where(user_id: user_id)
|
||||
.select(:year, :month)
|
||||
.distinct
|
||||
.order(:year, :month)
|
||||
.select(:year, :month)
|
||||
.distinct
|
||||
.order(:year, :month)
|
||||
|
||||
puts "Found #{archives.count} months to restore"
|
||||
puts ''
|
||||
|
|
@ -113,9 +113,9 @@ namespace :points do
|
|||
|
||||
# Storage size via ActiveStorage
|
||||
total_blob_size = ActiveStorage::Blob
|
||||
.joins('INNER JOIN active_storage_attachments ON active_storage_attachments.blob_id = active_storage_blobs.id')
|
||||
.where("active_storage_attachments.record_type = 'Points::RawDataArchive'")
|
||||
.sum(:byte_size)
|
||||
.joins('INNER JOIN active_storage_attachments ON active_storage_attachments.blob_id = active_storage_blobs.id')
|
||||
.where("active_storage_attachments.record_type = 'Points::RawDataArchive'")
|
||||
.sum(:byte_size)
|
||||
|
||||
puts "Storage used: #{ActiveSupport::NumberHelper.number_to_human_size(total_blob_size)}"
|
||||
puts ''
|
||||
|
|
@ -130,10 +130,10 @@ namespace :points do
|
|||
puts '─────────────────────────────────────────────────'
|
||||
|
||||
Points::RawDataArchive.group(:user_id)
|
||||
.select('user_id, COUNT(*) as archive_count, SUM(point_count) as total_points')
|
||||
.order('archive_count DESC')
|
||||
.limit(10)
|
||||
.each_with_index do |stat, idx|
|
||||
.select('user_id, COUNT(*) as archive_count, SUM(point_count) as total_points')
|
||||
.order('archive_count DESC')
|
||||
.limit(10)
|
||||
.each_with_index do |stat, idx|
|
||||
user = User.find(stat.user_id)
|
||||
puts "#{idx + 1}. #{user.email.ljust(30)} #{stat.archive_count.to_s.rjust(3)} archives, #{stat.total_points.to_s.rjust(8)} points"
|
||||
end
|
||||
|
|
@ -142,7 +142,7 @@ namespace :points do
|
|||
end
|
||||
|
||||
desc 'Verify archive integrity (all unverified archives, or specific month with args)'
|
||||
task :verify, [:user_id, :year, :month] => :environment do |_t, args|
|
||||
task :verify, %i[user_id year month] => :environment do |_t, args|
|
||||
verifier = Points::RawData::Verifier.new
|
||||
|
||||
if args[:user_id] && args[:year] && args[:month]
|
||||
|
|
@ -177,7 +177,7 @@ namespace :points do
|
|||
end
|
||||
|
||||
desc 'Clear raw_data for verified archives (all verified, or specific month with args)'
|
||||
task :clear_verified, [:user_id, :year, :month] => :environment do |_t, args|
|
||||
task :clear_verified, %i[user_id year month] => :environment do |_t, args|
|
||||
clearer = Points::RawData::Clearer.new
|
||||
|
||||
if args[:user_id] && args[:year] && args[:month]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
namespace :webmanifest do
|
||||
desc "Generate site.webmanifest in public directory with correct asset paths"
|
||||
task :generate => :environment do
|
||||
desc 'Generate site.webmanifest in public directory with correct asset paths'
|
||||
task generate: :environment do
|
||||
require 'erb'
|
||||
|
||||
# Make sure assets are compiled first by loading the manifest
|
||||
|
|
@ -12,28 +14,28 @@ namespace :webmanifest do
|
|||
|
||||
# Generate the manifest content
|
||||
manifest_content = {
|
||||
"name": "Dawarich",
|
||||
"short_name": "Dawarich",
|
||||
"name": 'Dawarich',
|
||||
"short_name": 'Dawarich',
|
||||
"icons": [
|
||||
{
|
||||
"src": icon_192_path,
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
"sizes": '192x192',
|
||||
"type": 'image/png'
|
||||
},
|
||||
{
|
||||
"src": icon_512_path,
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
"sizes": '512x512',
|
||||
"type": 'image/png'
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
"theme_color": '#ffffff',
|
||||
"background_color": '#ffffff',
|
||||
"display": 'standalone'
|
||||
}.to_json
|
||||
|
||||
# Write to public/site.webmanifest
|
||||
File.write(Rails.root.join('public/site.webmanifest'), manifest_content)
|
||||
puts "Generated public/site.webmanifest with correct asset paths"
|
||||
puts 'Generated public/site.webmanifest with correct asset paths'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :yearly_digest do
|
||||
factory :users_digest, class: 'Users::Digest' do
|
||||
year { 2024 }
|
||||
period_type { :yearly }
|
||||
distance { 500_000 } # 500 km
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe YearlyDigests::CalculatingJob, type: :job do
|
||||
RSpec.describe Users::Digests::CalculatingJob, type: :job do
|
||||
describe '#perform' do
|
||||
let!(:user) { create(:user) }
|
||||
let(:year) { 2024 }
|
||||
|
|
@ -10,23 +10,23 @@ RSpec.describe YearlyDigests::CalculatingJob, type: :job do
|
|||
subject { described_class.perform_now(user.id, year) }
|
||||
|
||||
before do
|
||||
allow(YearlyDigests::CalculateYear).to receive(:new).and_call_original
|
||||
allow_any_instance_of(YearlyDigests::CalculateYear).to receive(:call)
|
||||
allow(Users::Digests::CalculateYear).to receive(:new).and_call_original
|
||||
allow_any_instance_of(Users::Digests::CalculateYear).to receive(:call)
|
||||
end
|
||||
|
||||
it 'calls YearlyDigests::CalculateYear service' do
|
||||
it 'calls Users::Digests::CalculateYear service' do
|
||||
subject
|
||||
|
||||
expect(YearlyDigests::CalculateYear).to have_received(:new).with(user.id, year)
|
||||
expect(Users::Digests::CalculateYear).to have_received(:new).with(user.id, year)
|
||||
end
|
||||
|
||||
it 'enqueues to the digests queue' do
|
||||
expect(described_class.new.queue_name).to eq('digests')
|
||||
end
|
||||
|
||||
context 'when YearlyDigests::CalculateYear raises an error' do
|
||||
context 'when Users::Digests::CalculateYear raises an error' do
|
||||
before do
|
||||
allow_any_instance_of(YearlyDigests::CalculateYear).to receive(:call).and_raise(StandardError.new('Test error'))
|
||||
allow_any_instance_of(Users::Digests::CalculateYear).to receive(:call).and_raise(StandardError.new('Test error'))
|
||||
end
|
||||
|
||||
it 'creates an error notification' do
|
||||
|
|
@ -38,7 +38,7 @@ RSpec.describe YearlyDigests::CalculatingJob, type: :job do
|
|||
|
||||
context 'when user does not exist' do
|
||||
before do
|
||||
allow_any_instance_of(YearlyDigests::CalculateYear).to receive(:call).and_raise(ActiveRecord::RecordNotFound)
|
||||
allow_any_instance_of(Users::Digests::CalculateYear).to receive(:call).and_raise(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'does not raise error' do
|
||||
|
|
@ -2,17 +2,17 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe YearlyDigests::EmailSendingJob, type: :job do
|
||||
RSpec.describe Users::Digests::EmailSendingJob, type: :job do
|
||||
describe '#perform' do
|
||||
let!(:user) { create(:user) }
|
||||
let(:year) { 2024 }
|
||||
let!(:digest) { create(:yearly_digest, user: user, year: year, period_type: :yearly) }
|
||||
let!(:digest) { create(:users_digest, user: user, year: year, period_type: :yearly) }
|
||||
|
||||
subject { described_class.perform_now(user.id, year) }
|
||||
|
||||
before do
|
||||
# Mock the mailer
|
||||
allow(YearlyDigestsMailer).to receive_message_chain(:with, :year_end_digest, :deliver_later)
|
||||
allow(Users::DigestsMailer).to receive_message_chain(:with, :year_end_digest, :deliver_later)
|
||||
end
|
||||
|
||||
it 'enqueues to the mailers queue' do
|
||||
|
|
@ -23,7 +23,7 @@ RSpec.describe YearlyDigests::EmailSendingJob, type: :job do
|
|||
it 'sends the email' do
|
||||
subject
|
||||
|
||||
expect(YearlyDigestsMailer).to have_received(:with).with(user: user, digest: digest)
|
||||
expect(Users::DigestsMailer).to have_received(:with).with(user: user, digest: digest)
|
||||
end
|
||||
|
||||
it 'updates the sent_at timestamp' do
|
||||
|
|
@ -39,7 +39,7 @@ RSpec.describe YearlyDigests::EmailSendingJob, type: :job do
|
|||
it 'does not send the email' do
|
||||
subject
|
||||
|
||||
expect(YearlyDigestsMailer).not_to have_received(:with)
|
||||
expect(Users::DigestsMailer).not_to have_received(:with)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ RSpec.describe YearlyDigests::EmailSendingJob, type: :job do
|
|||
it 'does not send the email' do
|
||||
subject
|
||||
|
||||
expect(YearlyDigestsMailer).not_to have_received(:with)
|
||||
expect(Users::DigestsMailer).not_to have_received(:with)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ RSpec.describe YearlyDigests::EmailSendingJob, type: :job do
|
|||
it 'does not send the email again' do
|
||||
subject
|
||||
|
||||
expect(YearlyDigestsMailer).not_to have_received(:with)
|
||||
expect(Users::DigestsMailer).not_to have_received(:with)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ RSpec.describe YearlyDigests::EmailSendingJob, type: :job do
|
|||
|
||||
it 'reports the exception' do
|
||||
expect(ExceptionReporter).to receive(:call).with(
|
||||
'YearlyDigests::EmailSendingJob',
|
||||
'Users::Digests::EmailSendingJob',
|
||||
anything
|
||||
)
|
||||
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe YearlyDigests::YearEndSchedulingJob, type: :job do
|
||||
RSpec.describe Users::Digests::YearEndSchedulingJob, type: :job do
|
||||
describe '#perform' do
|
||||
subject { described_class.perform_now }
|
||||
|
||||
|
|
@ -25,40 +25,40 @@ RSpec.describe YearlyDigests::YearEndSchedulingJob, type: :job do
|
|||
create(:stat, user: trial_user, year: previous_year, month: 1)
|
||||
create(:stat, user: inactive_user, year: previous_year, month: 1)
|
||||
|
||||
allow(YearlyDigests::CalculatingJob).to receive(:perform_later)
|
||||
allow(YearlyDigests::EmailSendingJob).to receive(:set).and_return(double(perform_later: nil))
|
||||
allow(Users::Digests::CalculatingJob).to receive(:perform_later)
|
||||
allow(Users::Digests::EmailSendingJob).to receive(:set).and_return(double(perform_later: nil))
|
||||
end
|
||||
|
||||
it 'schedules jobs for active users' do
|
||||
subject
|
||||
|
||||
expect(YearlyDigests::CalculatingJob).to have_received(:perform_later)
|
||||
expect(Users::Digests::CalculatingJob).to have_received(:perform_later)
|
||||
.with(active_user.id, previous_year)
|
||||
end
|
||||
|
||||
it 'schedules jobs for trial users' do
|
||||
subject
|
||||
|
||||
expect(YearlyDigests::CalculatingJob).to have_received(:perform_later)
|
||||
expect(Users::Digests::CalculatingJob).to have_received(:perform_later)
|
||||
.with(trial_user.id, previous_year)
|
||||
end
|
||||
|
||||
it 'does not schedule jobs for inactive users' do
|
||||
subject
|
||||
|
||||
expect(YearlyDigests::CalculatingJob).not_to have_received(:perform_later)
|
||||
expect(Users::Digests::CalculatingJob).not_to have_received(:perform_later)
|
||||
.with(inactive_user.id, anything)
|
||||
end
|
||||
|
||||
it 'schedules email sending job with delay' do
|
||||
email_job_double = double(perform_later: nil)
|
||||
allow(YearlyDigests::EmailSendingJob).to receive(:set)
|
||||
allow(Users::Digests::EmailSendingJob).to receive(:set)
|
||||
.with(wait: 30.minutes)
|
||||
.and_return(email_job_double)
|
||||
|
||||
subject
|
||||
|
||||
expect(YearlyDigests::EmailSendingJob).to have_received(:set)
|
||||
expect(Users::Digests::EmailSendingJob).to have_received(:set)
|
||||
.with(wait: 30.minutes).at_least(:twice)
|
||||
end
|
||||
end
|
||||
|
|
@ -70,21 +70,21 @@ RSpec.describe YearlyDigests::YearEndSchedulingJob, type: :job do
|
|||
before do
|
||||
create(:stat, user: user_with_stats, year: previous_year, month: 1)
|
||||
|
||||
allow(YearlyDigests::CalculatingJob).to receive(:perform_later)
|
||||
allow(YearlyDigests::EmailSendingJob).to receive(:set).and_return(double(perform_later: nil))
|
||||
allow(Users::Digests::CalculatingJob).to receive(:perform_later)
|
||||
allow(Users::Digests::EmailSendingJob).to receive(:set).and_return(double(perform_later: nil))
|
||||
end
|
||||
|
||||
it 'does not schedule jobs for user without stats' do
|
||||
subject
|
||||
|
||||
expect(YearlyDigests::CalculatingJob).not_to have_received(:perform_later)
|
||||
expect(Users::Digests::CalculatingJob).not_to have_received(:perform_later)
|
||||
.with(user_without_stats.id, anything)
|
||||
end
|
||||
|
||||
it 'schedules jobs for user with stats' do
|
||||
subject
|
||||
|
||||
expect(YearlyDigests::CalculatingJob).to have_received(:perform_later)
|
||||
expect(Users::Digests::CalculatingJob).to have_received(:perform_later)
|
||||
.with(user_with_stats.id, previous_year)
|
||||
end
|
||||
end
|
||||
|
|
@ -95,14 +95,14 @@ RSpec.describe YearlyDigests::YearEndSchedulingJob, type: :job do
|
|||
before do
|
||||
create(:stat, user: user_current_year_only, year: Time.current.year, month: 1)
|
||||
|
||||
allow(YearlyDigests::CalculatingJob).to receive(:perform_later)
|
||||
allow(YearlyDigests::EmailSendingJob).to receive(:set).and_return(double(perform_later: nil))
|
||||
allow(Users::Digests::CalculatingJob).to receive(:perform_later)
|
||||
allow(Users::Digests::EmailSendingJob).to receive(:set).and_return(double(perform_later: nil))
|
||||
end
|
||||
|
||||
it 'does not schedule jobs for that user' do
|
||||
subject
|
||||
|
||||
expect(YearlyDigests::CalculatingJob).not_to have_received(:perform_later)
|
||||
expect(Users::Digests::CalculatingJob).not_to have_received(:perform_later)
|
||||
.with(user_current_year_only.id, anything)
|
||||
end
|
||||
end
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe YearlyDigest, type: :model do
|
||||
RSpec.describe Users::Digest, type: :model do
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:user) }
|
||||
end
|
||||
|
|
@ -13,22 +13,22 @@ RSpec.describe YearlyDigest, type: :model do
|
|||
|
||||
describe 'uniqueness of year within scope' do
|
||||
let(:user) { create(:user) }
|
||||
let!(:existing_digest) { create(:yearly_digest, user: user, year: 2024, period_type: :yearly) }
|
||||
let!(:existing_digest) { create(:users_digest, user: user, year: 2024, period_type: :yearly) }
|
||||
|
||||
it 'does not allow duplicate yearly digest for same user and year' do
|
||||
duplicate = build(:yearly_digest, user: user, year: 2024, period_type: :yearly)
|
||||
duplicate = build(:users_digest, user: user, year: 2024, period_type: :yearly)
|
||||
expect(duplicate).not_to be_valid
|
||||
expect(duplicate.errors[:year]).to include('has already been taken')
|
||||
end
|
||||
|
||||
it 'allows same year for different period types' do
|
||||
monthly = build(:yearly_digest, user: user, year: 2024, period_type: :monthly)
|
||||
monthly = build(:users_digest, user: user, year: 2024, period_type: :monthly)
|
||||
expect(monthly).to be_valid
|
||||
end
|
||||
|
||||
it 'allows same year for different users' do
|
||||
other_user = create(:user)
|
||||
other_digest = build(:yearly_digest, user: other_user, year: 2024, period_type: :yearly)
|
||||
other_digest = build(:users_digest, user: other_user, year: 2024, period_type: :yearly)
|
||||
expect(other_digest).to be_valid
|
||||
end
|
||||
end
|
||||
|
|
@ -41,14 +41,14 @@ RSpec.describe YearlyDigest, type: :model do
|
|||
describe 'callbacks' do
|
||||
describe 'before_create :generate_sharing_uuid' do
|
||||
it 'generates a sharing_uuid if not present' do
|
||||
digest = build(:yearly_digest, sharing_uuid: nil)
|
||||
digest = build(:users_digest, sharing_uuid: nil)
|
||||
digest.save!
|
||||
expect(digest.sharing_uuid).to be_present
|
||||
end
|
||||
|
||||
it 'does not overwrite existing sharing_uuid' do
|
||||
existing_uuid = SecureRandom.uuid
|
||||
digest = build(:yearly_digest, sharing_uuid: existing_uuid)
|
||||
digest = build(:users_digest, sharing_uuid: existing_uuid)
|
||||
digest.save!
|
||||
expect(digest.sharing_uuid).to eq(existing_uuid)
|
||||
end
|
||||
|
|
@ -57,7 +57,7 @@ RSpec.describe YearlyDigest, type: :model do
|
|||
|
||||
describe 'helper methods' do
|
||||
let(:user) { create(:user) }
|
||||
let(:digest) { create(:yearly_digest, user: user) }
|
||||
let(:digest) { create(:users_digest, user: user) }
|
||||
|
||||
describe '#countries_count' do
|
||||
it 'returns count of countries from toponyms' do
|
||||
|
|
@ -133,7 +133,7 @@ RSpec.describe YearlyDigest, type: :model do
|
|||
end
|
||||
|
||||
context 'when no previous year data' do
|
||||
let(:digest) { create(:yearly_digest, :without_previous_year, user: user) }
|
||||
let(:digest) { create(:users_digest, :without_previous_year, user: user) }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(digest.yoy_distance_change).to be_nil
|
||||
|
|
@ -190,7 +190,7 @@ RSpec.describe YearlyDigest, type: :model do
|
|||
|
||||
describe 'sharing settings' do
|
||||
let(:user) { create(:user) }
|
||||
let(:digest) { create(:yearly_digest, user: user) }
|
||||
let(:digest) { create(:users_digest, user: user) }
|
||||
|
||||
describe '#sharing_enabled?' do
|
||||
context 'when sharing_settings is nil' do
|
||||
|
|
@ -408,7 +408,7 @@ RSpec.describe YearlyDigest, type: :model do
|
|||
|
||||
describe 'DistanceConvertible' do
|
||||
let(:user) { create(:user) }
|
||||
let(:digest) { create(:yearly_digest, user: user, distance: 10_000) } # 10 km
|
||||
let(:digest) { create(:users_digest, user: user, distance: 10_000) } # 10 km
|
||||
|
||||
describe '#distance_in_unit' do
|
||||
it 'converts distance to kilometers' do
|
||||
|
|
@ -2,15 +2,15 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Shared::YearlyDigests', type: :request do
|
||||
RSpec.describe 'Shared::Digests', type: :request do
|
||||
context 'public sharing' do
|
||||
let(:user) { create(:user) }
|
||||
let(:digest) { create(:yearly_digest, :with_sharing_enabled, user:, year: 2024) }
|
||||
let(:digest) { create(:users_digest, :with_sharing_enabled, user:, year: 2024) }
|
||||
|
||||
describe 'GET /shared/year/:uuid' do
|
||||
describe 'GET /shared/digest/:uuid' do
|
||||
context 'with valid sharing UUID' do
|
||||
it 'renders the public year view' do
|
||||
get shared_yearly_digest_url(digest.sharing_uuid)
|
||||
get shared_users_digest_url(digest.sharing_uuid)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include('Year in Review')
|
||||
|
|
@ -18,7 +18,7 @@ RSpec.describe 'Shared::YearlyDigests', type: :request do
|
|||
end
|
||||
|
||||
it 'includes required content in response' do
|
||||
get shared_yearly_digest_url(digest.sharing_uuid)
|
||||
get shared_users_digest_url(digest.sharing_uuid)
|
||||
|
||||
expect(response.body).to include('2024')
|
||||
expect(response.body).to include('Distance traveled')
|
||||
|
|
@ -28,7 +28,7 @@ RSpec.describe 'Shared::YearlyDigests', type: :request do
|
|||
|
||||
context 'with invalid sharing UUID' do
|
||||
it 'redirects to root with alert' do
|
||||
get shared_yearly_digest_url('invalid-uuid')
|
||||
get shared_users_digest_url('invalid-uuid')
|
||||
|
||||
expect(response).to redirect_to(root_path)
|
||||
expect(flash[:alert]).to eq('Shared digest not found or no longer available')
|
||||
|
|
@ -36,10 +36,10 @@ RSpec.describe 'Shared::YearlyDigests', type: :request do
|
|||
end
|
||||
|
||||
context 'with expired sharing' do
|
||||
let(:digest) { create(:yearly_digest, :with_sharing_expired, user:, year: 2024) }
|
||||
let(:digest) { create(:users_digest, :with_sharing_expired, user:, year: 2024) }
|
||||
|
||||
it 'redirects to root with alert' do
|
||||
get shared_yearly_digest_url(digest.sharing_uuid)
|
||||
get shared_users_digest_url(digest.sharing_uuid)
|
||||
|
||||
expect(response).to redirect_to(root_path)
|
||||
expect(flash[:alert]).to eq('Shared digest not found or no longer available')
|
||||
|
|
@ -47,10 +47,10 @@ RSpec.describe 'Shared::YearlyDigests', type: :request do
|
|||
end
|
||||
|
||||
context 'with disabled sharing' do
|
||||
let(:digest) { create(:yearly_digest, :with_sharing_disabled, user:, year: 2024) }
|
||||
let(:digest) { create(:users_digest, :with_sharing_disabled, user:, year: 2024) }
|
||||
|
||||
it 'redirects to root with alert' do
|
||||
get shared_yearly_digest_url(digest.sharing_uuid)
|
||||
get shared_users_digest_url(digest.sharing_uuid)
|
||||
|
||||
expect(response).to redirect_to(root_path)
|
||||
expect(flash[:alert]).to eq('Shared digest not found or no longer available')
|
||||
|
|
@ -58,15 +58,15 @@ RSpec.describe 'Shared::YearlyDigests', type: :request do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'PATCH /yearly_digests/:year/sharing' do
|
||||
describe 'PATCH /digests/:year/sharing' do
|
||||
context 'when user is signed in' do
|
||||
let!(:digest_to_share) { create(:yearly_digest, user:, year: 2024) }
|
||||
let!(:digest_to_share) { create(:users_digest, user:, year: 2024) }
|
||||
|
||||
before { sign_in user }
|
||||
|
||||
context 'enabling sharing' do
|
||||
it 'enables sharing and returns success' do
|
||||
patch sharing_yearly_digest_path(year: 2024),
|
||||
patch sharing_users_digest_path(year: 2024),
|
||||
params: { enabled: '1' },
|
||||
as: :json
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ RSpec.describe 'Shared::YearlyDigests', type: :request do
|
|||
end
|
||||
|
||||
it 'sets custom expiration when provided' do
|
||||
patch sharing_yearly_digest_path(year: 2024),
|
||||
patch sharing_users_digest_path(year: 2024),
|
||||
params: { enabled: '1', expiration: '12h' },
|
||||
as: :json
|
||||
|
||||
|
|
@ -94,10 +94,10 @@ RSpec.describe 'Shared::YearlyDigests', type: :request do
|
|||
end
|
||||
|
||||
context 'disabling sharing' do
|
||||
let!(:enabled_digest) { create(:yearly_digest, :with_sharing_enabled, user:, year: 2023) }
|
||||
let!(:enabled_digest) { create(:users_digest, :with_sharing_enabled, user:, year: 2023) }
|
||||
|
||||
it 'disables sharing and returns success' do
|
||||
patch sharing_yearly_digest_path(year: 2023),
|
||||
patch sharing_users_digest_path(year: 2023),
|
||||
params: { enabled: '0' },
|
||||
as: :json
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ RSpec.describe 'Shared::YearlyDigests', type: :request do
|
|||
|
||||
context 'when digest does not exist' do
|
||||
it 'returns not found' do
|
||||
patch sharing_yearly_digest_path(year: 2020),
|
||||
patch sharing_users_digest_path(year: 2020),
|
||||
params: { enabled: '1' },
|
||||
as: :json
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ RSpec.describe 'Shared::YearlyDigests', type: :request do
|
|||
|
||||
context 'when user is not signed in' do
|
||||
it 'returns unauthorized' do
|
||||
patch sharing_yearly_digest_path(year: 2024),
|
||||
patch sharing_users_digest_path(year: 2024),
|
||||
params: { enabled: '1' },
|
||||
as: :json
|
||||
|
||||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe '/yearly_digests', type: :request do
|
||||
RSpec.describe '/digests', type: :request do
|
||||
context 'when user is not signed in' do
|
||||
describe 'GET /index' do
|
||||
it 'redirects to the sign in page' do
|
||||
get yearly_digests_url
|
||||
get users_digests_url
|
||||
|
||||
expect(response.status).to eq(302)
|
||||
end
|
||||
|
|
@ -14,7 +14,7 @@ RSpec.describe '/yearly_digests', type: :request do
|
|||
|
||||
describe 'GET /show' do
|
||||
it 'redirects to the sign in page' do
|
||||
get yearly_digest_url(year: 2024)
|
||||
get users_digest_url(year: 2024)
|
||||
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
|
|
@ -22,7 +22,7 @@ RSpec.describe '/yearly_digests', type: :request do
|
|||
|
||||
describe 'POST /create' do
|
||||
it 'redirects to the sign in page' do
|
||||
post yearly_digests_url, params: { year: 2024 }
|
||||
post users_digests_url, params: { year: 2024 }
|
||||
|
||||
expect(response.status).to eq(302)
|
||||
end
|
||||
|
|
@ -36,46 +36,46 @@ RSpec.describe '/yearly_digests', type: :request do
|
|||
|
||||
describe 'GET /index' do
|
||||
it 'renders a successful response' do
|
||||
get yearly_digests_url
|
||||
get users_digests_url
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it 'displays existing digests' do
|
||||
digest = create(:yearly_digest, user:, year: 2024)
|
||||
digest = create(:users_digest, user:, year: 2024)
|
||||
|
||||
get yearly_digests_url
|
||||
get users_digests_url
|
||||
|
||||
expect(response.body).to include('2024')
|
||||
end
|
||||
|
||||
it 'shows empty state when no digests exist' do
|
||||
get yearly_digests_url
|
||||
get users_digests_url
|
||||
|
||||
expect(response.body).to include('No Year-End Digests Yet')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /show' do
|
||||
let!(:digest) { create(:yearly_digest, user:, year: 2024) }
|
||||
let!(:digest) { create(:users_digest, user:, year: 2024) }
|
||||
|
||||
it 'renders a successful response' do
|
||||
get yearly_digest_url(year: 2024)
|
||||
get users_digest_url(year: 2024)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it 'includes digest content' do
|
||||
get yearly_digest_url(year: 2024)
|
||||
get users_digest_url(year: 2024)
|
||||
|
||||
expect(response.body).to include('2024 Year in Review')
|
||||
expect(response.body).to include('Distance Traveled')
|
||||
end
|
||||
|
||||
it 'redirects when digest not found' do
|
||||
get yearly_digest_url(year: 2020)
|
||||
get users_digest_url(year: 2020)
|
||||
|
||||
expect(response).to redirect_to(yearly_digests_path)
|
||||
expect(response).to redirect_to(users_digests_path)
|
||||
expect(flash[:alert]).to eq('Digest not found')
|
||||
end
|
||||
end
|
||||
|
|
@ -86,39 +86,39 @@ RSpec.describe '/yearly_digests', type: :request do
|
|||
create(:stat, user:, year: 2024, month: 1)
|
||||
end
|
||||
|
||||
it 'enqueues YearlyDigests::CalculatingJob' do
|
||||
post yearly_digests_url, params: { year: 2024 }
|
||||
it 'enqueues Users::Digests::CalculatingJob' do
|
||||
post users_digests_url, params: { year: 2024 }
|
||||
|
||||
expect(YearlyDigests::CalculatingJob).to have_been_enqueued.with(user.id, 2024)
|
||||
expect(Users::Digests::CalculatingJob).to have_been_enqueued.with(user.id, 2024)
|
||||
end
|
||||
|
||||
it 'redirects with success notice' do
|
||||
post yearly_digests_url, params: { year: 2024 }
|
||||
post users_digests_url, params: { year: 2024 }
|
||||
|
||||
expect(response).to redirect_to(yearly_digests_path)
|
||||
expect(response).to redirect_to(users_digests_path)
|
||||
expect(flash[:notice]).to include('is being generated')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid year' do
|
||||
it 'redirects with alert for year with no stats' do
|
||||
post yearly_digests_url, params: { year: 2024 }
|
||||
post users_digests_url, params: { year: 2024 }
|
||||
|
||||
expect(response).to redirect_to(yearly_digests_path)
|
||||
expect(response).to redirect_to(users_digests_path)
|
||||
expect(flash[:alert]).to eq('Invalid year selected')
|
||||
end
|
||||
|
||||
it 'redirects with alert for year before 2000' do
|
||||
post yearly_digests_url, params: { year: 1999 }
|
||||
post users_digests_url, params: { year: 1999 }
|
||||
|
||||
expect(response).to redirect_to(yearly_digests_path)
|
||||
expect(response).to redirect_to(users_digests_path)
|
||||
expect(flash[:alert]).to eq('Invalid year selected')
|
||||
end
|
||||
|
||||
it 'redirects with alert for future year' do
|
||||
post yearly_digests_url, params: { year: Time.current.year + 1 }
|
||||
post users_digests_url, params: { year: Time.current.year + 1 }
|
||||
|
||||
expect(response).to redirect_to(yearly_digests_path)
|
||||
expect(response).to redirect_to(users_digests_path)
|
||||
expect(flash[:alert]).to eq('Invalid year selected')
|
||||
end
|
||||
end
|
||||
|
|
@ -130,7 +130,7 @@ RSpec.describe '/yearly_digests', type: :request do
|
|||
end
|
||||
|
||||
it 'returns an unauthorized response' do
|
||||
post yearly_digests_url, params: { year: 2024 }
|
||||
post users_digests_url, params: { year: 2024 }
|
||||
|
||||
expect(response).to redirect_to(root_path)
|
||||
expect(flash[:notice]).to eq('Your account is not active.')
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe YearlyDigests::CalculateYear do
|
||||
RSpec.describe Users::Digests::CalculateYear do
|
||||
describe '#call' do
|
||||
subject(:calculate_digest) { described_class.new(user.id, year).call }
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ RSpec.describe YearlyDigests::CalculateYear do
|
|||
end
|
||||
|
||||
it 'does not create a digest' do
|
||||
expect { calculate_digest }.not_to(change { YearlyDigest.count })
|
||||
expect { calculate_digest }.not_to(change { Users::Digest.count })
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -38,11 +38,11 @@ RSpec.describe YearlyDigests::CalculateYear do
|
|||
end
|
||||
|
||||
it 'creates a yearly digest' do
|
||||
expect { calculate_digest }.to change { YearlyDigest.count }.by(1)
|
||||
expect { calculate_digest }.to change { Users::Digest.count }.by(1)
|
||||
end
|
||||
|
||||
it 'returns the created digest' do
|
||||
expect(calculate_digest).to be_a(YearlyDigest)
|
||||
expect(calculate_digest).to be_a(Users::Digest)
|
||||
end
|
||||
|
||||
it 'sets the correct year' do
|
||||
|
|
@ -86,11 +86,11 @@ RSpec.describe YearlyDigests::CalculateYear do
|
|||
|
||||
context 'when digest already exists' do
|
||||
let!(:existing_digest) do
|
||||
create(:yearly_digest, user: user, year: 2024, period_type: :yearly, distance: 10_000)
|
||||
create(:users_digest, user: user, year: 2024, period_type: :yearly, distance: 10_000)
|
||||
end
|
||||
|
||||
it 'updates the existing digest' do
|
||||
expect { calculate_digest }.not_to(change { YearlyDigest.count })
|
||||
expect { calculate_digest }.not_to(change { Users::Digest.count })
|
||||
end
|
||||
|
||||
it 'updates the distance' do
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe YearlyDigests::FirstTimeVisitsCalculator do
|
||||
RSpec.describe Users::Digests::FirstTimeVisitsCalculator do
|
||||
describe '#call' do
|
||||
subject(:calculator) { described_class.new(user, year).call }
|
||||
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe YearlyDigests::YearOverYearCalculator do
|
||||
RSpec.describe Users::Digests::YearOverYearCalculator do
|
||||
describe '#call' do
|
||||
subject(:calculator) { described_class.new(user, year).call }
|
||||
|
||||
|
|
@ -25,12 +25,12 @@ RSpec.describe Users::SafeSettings do
|
|||
immich_api_key: nil,
|
||||
photoprism_url: nil,
|
||||
photoprism_api_key: nil,
|
||||
maps: { "distance_unit" => "km" },
|
||||
maps: { 'distance_unit' => 'km' },
|
||||
distance_unit: 'km',
|
||||
visits_suggestions_enabled: true,
|
||||
speed_color_scale: nil,
|
||||
fog_of_war_threshold: nil,
|
||||
enabled_map_layers: ['Routes', 'Heatmap'],
|
||||
enabled_map_layers: %w[Routes Heatmap],
|
||||
maps_maplibre_style: 'light'
|
||||
}
|
||||
)
|
||||
|
|
@ -56,7 +56,7 @@ RSpec.describe Users::SafeSettings do
|
|||
'photoprism_api_key' => 'photoprism-key',
|
||||
'maps' => { 'name' => 'custom', 'url' => 'https://custom.example.com' },
|
||||
'visits_suggestions_enabled' => false,
|
||||
'enabled_map_layers' => ['Points', 'Routes', 'Areas', 'Photos']
|
||||
'enabled_map_layers' => %w[Points Routes Areas Photos]
|
||||
}
|
||||
end
|
||||
let(:safe_settings) { described_class.new(settings) }
|
||||
|
|
@ -64,24 +64,25 @@ RSpec.describe Users::SafeSettings do
|
|||
it 'returns custom configuration' do
|
||||
expect(safe_settings.settings).to eq(
|
||||
{
|
||||
"fog_of_war_meters" => 100,
|
||||
"meters_between_routes" => 1000,
|
||||
"preferred_map_layer" => "Satellite",
|
||||
"speed_colored_routes" => true,
|
||||
"points_rendering_mode" => "simplified",
|
||||
"minutes_between_routes" => 60,
|
||||
"time_threshold_minutes" => 45,
|
||||
"merge_threshold_minutes" => 20,
|
||||
"live_map_enabled" => false,
|
||||
"route_opacity" => 80,
|
||||
"immich_url" => "https://immich.example.com",
|
||||
"immich_api_key" => "immich-key",
|
||||
"photoprism_url" => "https://photoprism.example.com",
|
||||
"photoprism_api_key" => "photoprism-key",
|
||||
"maps" => { "name" => "custom", "url" => "https://custom.example.com" },
|
||||
"visits_suggestions_enabled" => false,
|
||||
"enabled_map_layers" => ['Points', 'Routes', 'Areas', 'Photos'],
|
||||
"maps_maplibre_style" => "light"
|
||||
'fog_of_war_meters' => 100,
|
||||
'meters_between_routes' => 1000,
|
||||
'preferred_map_layer' => 'Satellite',
|
||||
'speed_colored_routes' => true,
|
||||
'points_rendering_mode' => 'simplified',
|
||||
'minutes_between_routes' => 60,
|
||||
'time_threshold_minutes' => 45,
|
||||
'merge_threshold_minutes' => 20,
|
||||
'live_map_enabled' => false,
|
||||
'route_opacity' => 80,
|
||||
'immich_url' => 'https://immich.example.com',
|
||||
'immich_api_key' => 'immich-key',
|
||||
'photoprism_url' => 'https://photoprism.example.com',
|
||||
'photoprism_api_key' => 'photoprism-key',
|
||||
'maps' => { 'name' => 'custom', 'url' => 'https://custom.example.com' },
|
||||
'visits_suggestions_enabled' => false,
|
||||
'enabled_map_layers' => %w[Points Routes Areas Photos],
|
||||
'maps_maplibre_style' => 'light',
|
||||
'digest_emails_enabled' => true
|
||||
}
|
||||
)
|
||||
end
|
||||
|
|
@ -91,24 +92,24 @@ RSpec.describe Users::SafeSettings do
|
|||
{
|
||||
fog_of_war_meters: 100,
|
||||
meters_between_routes: 1000,
|
||||
preferred_map_layer: "Satellite",
|
||||
preferred_map_layer: 'Satellite',
|
||||
speed_colored_routes: true,
|
||||
points_rendering_mode: "simplified",
|
||||
points_rendering_mode: 'simplified',
|
||||
minutes_between_routes: 60,
|
||||
time_threshold_minutes: 45,
|
||||
merge_threshold_minutes: 20,
|
||||
live_map_enabled: false,
|
||||
route_opacity: 80,
|
||||
immich_url: "https://immich.example.com",
|
||||
immich_api_key: "immich-key",
|
||||
photoprism_url: "https://photoprism.example.com",
|
||||
photoprism_api_key: "photoprism-key",
|
||||
maps: { "name" => "custom", "url" => "https://custom.example.com" },
|
||||
immich_url: 'https://immich.example.com',
|
||||
immich_api_key: 'immich-key',
|
||||
photoprism_url: 'https://photoprism.example.com',
|
||||
photoprism_api_key: 'photoprism-key',
|
||||
maps: { 'name' => 'custom', 'url' => 'https://custom.example.com' },
|
||||
distance_unit: nil,
|
||||
visits_suggestions_enabled: false,
|
||||
speed_color_scale: nil,
|
||||
fog_of_war_threshold: nil,
|
||||
enabled_map_layers: ['Points', 'Routes', 'Areas', 'Photos'],
|
||||
enabled_map_layers: %w[Points Routes Areas Photos],
|
||||
maps_maplibre_style: 'light'
|
||||
}
|
||||
)
|
||||
|
|
@ -137,9 +138,9 @@ RSpec.describe Users::SafeSettings do
|
|||
expect(safe_settings.immich_api_key).to be_nil
|
||||
expect(safe_settings.photoprism_url).to be_nil
|
||||
expect(safe_settings.photoprism_api_key).to be_nil
|
||||
expect(safe_settings.maps).to eq({ "distance_unit" => "km" })
|
||||
expect(safe_settings.maps).to eq({ 'distance_unit' => 'km' })
|
||||
expect(safe_settings.visits_suggestions_enabled?).to be true
|
||||
expect(safe_settings.enabled_map_layers).to eq(['Routes', 'Heatmap'])
|
||||
expect(safe_settings.enabled_map_layers).to eq(%w[Routes Heatmap])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue