diff --git a/Gemfile.lock b/Gemfile.lock index c4bc295ba4f..de18dcdb5aa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -415,7 +415,7 @@ GEM nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.17) + rack (2.2.18) rack-cors (2.0.2) rack (>= 2.0.0) rack-dev-mark (0.8.0) diff --git a/app/controllers/api/answers_controller.rb b/app/controllers/api/answers_controller.rb index e61262a8b9b..284c9e7dbfd 100644 --- a/app/controllers/api/answers_controller.rb +++ b/app/controllers/api/answers_controller.rb @@ -30,7 +30,6 @@ def create @answer.user = current_user if @answer.save ActiveSupport::Notifications.instrument('answer.create', answer: @answer) - Newspaper.publish(:answer_save, { answer: @answer }) render partial: 'questions/answer', locals: { question:, answer: @answer, user: current_user }, status: :created else head :bad_request @@ -39,7 +38,6 @@ def create def update if @answer.update(answer_params) - Newspaper.publish(:answer_save, { answer: @answer }) head :ok else head :bad_request @@ -47,8 +45,13 @@ def update end def destroy + if @answer.is_a?(CorrectAnswer) + Newspaper.publish(:answer_destroy, { + answer: @answer, + action: "#{self.class.name}##{action_name}" + }) + end @answer.destroy - Newspaper.publish(:answer_destroy, { answer: @answer }) end private diff --git a/app/controllers/api/correct_answers_controller.rb b/app/controllers/api/correct_answers_controller.rb index ff2ff909a90..41a29413c02 100644 --- a/app/controllers/api/correct_answers_controller.rb +++ b/app/controllers/api/correct_answers_controller.rb @@ -7,8 +7,11 @@ def create @answer = @question.answers.find(params[:answer_id]) @answer.type = 'CorrectAnswer' if @answer.save - Newspaper.publish(:answer_save, { answer: @answer }) - Newspaper.publish(:correct_answer_save, { answer: @answer }) + Newspaper.publish(:answer_save, { + answer: @answer, + action: "#{self.class.name}##{action_name}" + }) + ActiveSupport::Notifications.instrument('correct_answer.save', answer: @answer) head :ok else head :bad_request @@ -18,7 +21,10 @@ def create def update answer = @question.answers.find(params[:answer_id]) answer.update!(type: '') - Newspaper.publish(:answer_save, { answer: @answer }) + Newspaper.publish(:answer_save, { + answer:, + action: "#{self.class.name}##{action_name}" + }) end private diff --git a/app/controllers/api/events_controller.rb b/app/controllers/api/events_controller.rb deleted file mode 100644 index 3dbf684b646..00000000000 --- a/app/controllers/api/events_controller.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -class API::EventsController < API::BaseController - before_action :require_active_user_login - - def index - @events = Event.with_avatar - .includes(:comments, :users) - .order(start_at: :desc) - .page(params[:page]) - end -end diff --git a/app/controllers/comeback_controller.rb b/app/controllers/comeback_controller.rb index abf51bd5b5d..f06543cf4c3 100644 --- a/app/controllers/comeback_controller.rb +++ b/app/controllers/comeback_controller.rb @@ -12,7 +12,7 @@ def create if @user if @user&.hibernated? @user.comeback! - Newspaper.publish(:comeback_update, { user: @user }) + ActiveSupport::Notifications.instrument('comeback.update', user: @user) @user.create_comebacked_comment redirect_to root_url, notice: '休会から復帰しました。' else diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 73ad532e530..ecf57e97b3e 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -1,9 +1,12 @@ # frozen_string_literal: true class EventsController < ApplicationController + PAGER_NUMBER = 20 + before_action :set_event, only: %i[edit update destroy] def index + @events = Event.with_avatar.includes(:comments, :users).order(start_at: :desc).page(params[:page]).per(PAGER_NUMBER) @upcoming_events_groups = UpcomingEvent.upcoming_events_groups end diff --git a/app/controllers/graduation_controller.rb b/app/controllers/graduation_controller.rb index f2fa004c90d..8916b283644 100644 --- a/app/controllers/graduation_controller.rb +++ b/app/controllers/graduation_controller.rb @@ -9,7 +9,7 @@ class GraduationController < ApplicationController def update if @user.update(graduated_on: Date.current) Subscription.new.destroy(@user.subscription_id) if @user.subscription_id - Newspaper.publish(:graduation_update, { user: @user }) + ActiveSupport::Notifications.instrument('graduation.update', user: @user) redirect_to @redirect_url, notice: 'ユーザー情報を更新しました。' else redirect_to @redirect_url, alert: 'ユーザー情報の更新に失敗しました' diff --git a/app/controllers/reports/unchecked_controller.rb b/app/controllers/reports/unchecked_controller.rb new file mode 100644 index 00000000000..f7dc0158f5f --- /dev/null +++ b/app/controllers/reports/unchecked_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Reports::UncheckedController < ApplicationController + PAGER_NUMBER = 25 + before_action :require_admin_or_mentor! + + def index + @reports = Report.list.page(params[:page]).per(PAGER_NUMBER) + @reports = @reports.unchecked.not_wip + render 'reports/index' + end + + private + + def require_admin_or_mentor! + redirect_to reports_path unless admin_or_mentor_login? + end +end diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index 8d5e56856bd..ad79dd68e1e 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -16,7 +16,6 @@ class ReportsController < ApplicationController # rubocop:todo Metrics/ClassLeng def index @reports = Report.list.page(params[:page]).per(PAGER_NUMBER) @reports = @reports.joins(:practices).where(practices: { id: params[:practice_id] }) if params[:practice_id].present? - @reports = @reports.unchecked.not_wip if params[:unchecked].present? && admin_or_mentor_login? end def show diff --git a/app/helpers/meta_tags_helper.rb b/app/helpers/meta_tags_helper.rb index 2852fa7ef62..827095e2538 100644 --- a/app/helpers/meta_tags_helper.rb +++ b/app/helpers/meta_tags_helper.rb @@ -7,7 +7,7 @@ def default_meta_tags site: 'FBC', reverse: true, charset: 'utf-8', - description: '月額29,800円、全機能が使えるお試し期間付き。FBCは現場の即戦力になるためのスキルとプログラミングの楽しさを伝える、現役ソフトウェアエンジニアが考える理想のプログラミングスクールの実現に励んでいます。', + description: '月額32,780円(税込)、全機能が使えるお試し期間付き。FBCは現場の即戦力になるためのスキルとプログラミングの楽しさを伝える、現役ソフトウェアエンジニアが考える理想のプログラミングスクールの実現に励んでいます。', viewport: 'width=device-width, initial-scale=1.0', og: { title: :title, @@ -31,7 +31,7 @@ def default_meta_tags def welcome_meta_tags default_meta_tags.deep_merge({ title:, - description: '月額29,800円、全機能が使えるお試し期間付き。FBCは現場の即戦力になるためのスキルとプログラミングの楽しさを伝える、現役ソフトウェアエンジニアが考える理想のプログラミングスクールの実現に励んでいます。', + description: '月額32,780円(税込)、全機能が使えるお試し期間付き。FBCは現場の即戦力になるためのスキルとプログラミングの楽しさを伝える、現役ソフトウェアエンジニアが考える理想のプログラミングスクールの実現に励んでいます。', og: { title: title || 'FJORD BOOT CAMP(フィヨルドブートキャンプ)', description: :description diff --git a/app/javascript/initializeAnswer.js b/app/javascript/initializeAnswer.js index 1fd15e76fc8..eff9d9908a2 100644 --- a/app/javascript/initializeAnswer.js +++ b/app/javascript/initializeAnswer.js @@ -115,7 +115,6 @@ export default function initializeAnswer(answer) { if (window.confirm('本当に宜しいですか?')) { deleteAnswer(answerId) if (answerBadgeElement.classList.contains('correct-answer')) { - cancelBestAnswer(answerId, questionId) const otherCancelBestAnswerButtons = document.querySelectorAll( '.make-best-answer-button' ) diff --git a/app/javascript/markdown-initializer.js b/app/javascript/markdown-initializer.js index e5582ed58ad..f7be260a16c 100644 --- a/app/javascript/markdown-initializer.js +++ b/app/javascript/markdown-initializer.js @@ -12,7 +12,6 @@ import MarkDownItContainerMessage from 'markdown-it-container-message' import MarkDownItContainerDetails from 'markdown-it-container-details' import MarkDownItLinkAttributes from 'markdown-it-link-attributes' import MarkDownItContainerSpeak from 'markdown-it-container-speak' -import MarkdownItPurifier from 'markdown-it-purifier' import ReplaceLinkToCard from 'replace-link-to-card' import MarkDownItContainerFigure from 'markdown-it-container-figure' import MarkdownItVimeo from 'markdown-it-vimeo' @@ -62,7 +61,6 @@ export default class { md.use(MarkDownItContainerFigure) md.use(MarkdownItVimeo) md.use(MarkdownItYoutube) - md.use(MarkdownItPurifier) return md.render(text) } } diff --git a/app/javascript/stylesheets/lp/blocks/lp/_lp-price.sass b/app/javascript/stylesheets/lp/blocks/lp/_lp-price.sass index 67840e861cd..60e08c5c225 100644 --- a/app/javascript/stylesheets/lp/blocks/lp/_lp-price.sass +++ b/app/javascript/stylesheets/lp/blocks/lp/_lp-price.sass @@ -9,20 +9,22 @@ font-size: .875rem +media-breakpoint-down(sm) font-size: .5625rem + &.is-total + gap: .25em &.is-sm justify-content: flex-start +media-breakpoint-up(lg) font-size: .5625em .lp-price__tax - font-size: 1rem + font-size: 1.5rem +media-breakpoint-only(md) font-size: .5em .lp-price__tax - font-size: .875rem + font-size: 1rem +media-breakpoint-down(sm) font-size: .375em .lp-price__tax - font-size: .75rem + font-size: .875rem .lp-price__label +text-block(1.25em 1, 700) @@ -40,6 +42,7 @@ display: flex align-self: baseline gap: .5em + flex-wrap: wrap .lp-price__amount +text-block(6.25em 1, 900) @@ -51,14 +54,28 @@ align-self: baseline .lp-price__details - +text-block(1.25em 1, 800) + +text-block(2.25em 1, 800) display: flex align-self: baseline gap: .125em .lp-price__separator font-weight: 100 + align-self: baseline + +.lp-price__per-person + align-self: baseline + +.lp-price__tax + font-size: 1.25em + align-self: baseline + +.lp-price__total-value + display: flex + align-self: baseline + gap: .25em + font-size: 1.5em + font-weight: 600 .lp-price-note - +media-breakpoint-down(sm) - font-size: .75rem + line-height: 1.4 diff --git a/app/javascript/stylesheets/lp/layouts/_l-cards.sass b/app/javascript/stylesheets/lp/layouts/_l-cards.sass index eb45bdd6248..accf9d8b1a2 100644 --- a/app/javascript/stylesheets/lp/layouts/_l-cards.sass +++ b/app/javascript/stylesheets/lp/layouts/_l-cards.sass @@ -35,6 +35,6 @@ .l-cards__item-inner flex: 0 0 40rem - max-width: 100% + max-width: calc(100vw - 2rem) .a-card margin-inline: 0 diff --git a/app/javascript/textarea-initializer.js b/app/javascript/textarea-initializer.js index 72d787523bc..d42348cba10 100644 --- a/app/javascript/textarea-initializer.js +++ b/app/javascript/textarea-initializer.js @@ -17,7 +17,6 @@ import CSRF from 'csrf' import TextareaMarkdownLinkify from 'textarea-markdown-linkify' import ReplaceLinkToCard from 'replace-link-to-card' import MarkDownItContainerFigure from 'markdown-it-container-figure' -import MarkdownItPurifier from 'markdown-it-purifier' import MarkdownItVimeo from 'markdown-it-vimeo' import MarkdownItYoutube from 'markdown-it-youtube' @@ -90,8 +89,7 @@ export default class { MarkDownItContainerSpeak, MarkDownItContainerFigure, MarkdownItVimeo, - MarkdownItYoutube, - MarkdownItPurifier + MarkdownItYoutube ], markdownOptions: MarkdownOption }) diff --git a/app/models/announcement.rb b/app/models/announcement.rb index b4a2d8fe320..7c5cb41b154 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -7,6 +7,7 @@ class Announcement < ApplicationRecord include Reactionable include WithAvatar include Watchable + include Bookmarkable enum target: { all: 0, diff --git a/app/models/answer_cache_destroyer.rb b/app/models/answer_cache_destroyer.rb index c374dc9650d..b5ede21f2a0 100644 --- a/app/models/answer_cache_destroyer.rb +++ b/app/models/answer_cache_destroyer.rb @@ -3,6 +3,8 @@ class AnswerCacheDestroyer def call(payload) _answer = payload[:answer] + action = payload[:action] Cache.delete_not_solved_question_count + Rails.logger.info "[AnswerCacheDestroyer] #{action} Cache destroyed for unsolved question count." end end diff --git a/app/models/cache.rb b/app/models/cache.rb index 020f1a9df80..1f3b96702e1 100644 --- a/app/models/cache.rb +++ b/app/models/cache.rb @@ -54,7 +54,8 @@ def delete_self_assigned_no_replied_product_count(user_id) def not_solved_question_count Rails.cache.fetch 'not_solved_question_count' do - Question.not_solved.count + Rails.logger.info '[CACHE MISS] Executing DB query for not_solved_question_count' + Question.not_solved.not_wip.count end end diff --git a/app/models/comeback_notifier.rb b/app/models/comeback_notifier.rb index 1112ba9da18..6beefbf2f9b 100644 --- a/app/models/comeback_notifier.rb +++ b/app/models/comeback_notifier.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ComebackNotifier - def call(payload) + def call(_name, _started, _finished, _unique_id, payload) user = payload[:user] User.admins_and_mentors.each do |admin_or_mentor| ActivityDelivery.with(sender: user, receiver: admin_or_mentor).notify(:comebacked) diff --git a/app/models/correct_answer_notifier.rb b/app/models/correct_answer_notifier.rb index 27fd20be16a..e421b3fc5c5 100644 --- a/app/models/correct_answer_notifier.rb +++ b/app/models/correct_answer_notifier.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class CorrectAnswerNotifier - def call(payload) + def call(_name, _started, _finished, _unique_id, payload) answer = payload[:answer] notify_correct_answer(answer) notify_to_chat(answer) diff --git a/app/models/graduation_notifier.rb b/app/models/graduation_notifier.rb index 6ee13c4eada..91ff553a82e 100644 --- a/app/models/graduation_notifier.rb +++ b/app/models/graduation_notifier.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class GraduationNotifier - def call(payload) + def call(_name, _started, _finished, _unique_id, payload) user = payload[:user] User.mentor.each do |mentor| ActivityDelivery.with(sender: user, receiver: mentor).notify(:graduated) diff --git a/app/models/grass.rb b/app/models/grass.rb index 676877b3aa7..237820f43ae 100644 --- a/app/models/grass.rb +++ b/app/models/grass.rb @@ -1,45 +1,7 @@ # frozen_string_literal: true class Grass - # rubocop:disable Metrics/MethodLength def self.times(user, end_date) - start_date = end_date.prev_year.sunday - sql = <<~SQL - WITH series AS ( - SELECT - date - FROM - generate_series(:start_date::DATE, :end_date, '1 day') AS series(date) - ), - summary AS ( - SELECT - reported_on AS date, - EXTRACT(epoch FROM SUM(finished_at - started_at)) / 60 / 60 AS total_hour - FROM - learning_times JOIN reports ON learning_times.report_id = reports.id - WHERE - reports.user_id = :user_id - GROUP BY - reported_on - ORDER BY - reported_on - ) - SELECT - series.date AS date, - CASE - WHEN summary.total_hour > 6 THEN 4 - WHEN 6 >= summary.total_hour AND summary.total_hour > 4 THEN 3 - WHEN 4 >= summary.total_hour AND summary.total_hour > 2 THEN 2 - WHEN 2 >= summary.total_hour AND summary.total_hour > 0 THEN 1 - ELSE 0 - END AS velocity - FROM - series - LEFT JOIN - summary ON series.date = summary.date - SQL - - LearningTime.find_by_sql([sql, { start_date:, end_date:, user_id: user.id }]) + GrassLearningTimeQuery.call(user, end_date) end - # rubocop:enable Metrics/MethodLength end diff --git a/app/models/product.rb b/app/models/product.rb index ce7665dfcdf..8512b24c529 100644 --- a/app/models/product.rb +++ b/app/models/product.rb @@ -40,7 +40,10 @@ class Product < ApplicationRecord # rubocop:todo Metrics/ClassLength scope :unchecked, -> { where.not(id: Check.where(checkable_type: 'Product').pluck(:checkable_id)) } scope :unassigned, -> { where(checker_id: nil) } scope :self_assigned_product, ->(user_id) { where(checker_id: user_id) } - scope :self_assigned_and_replied_products, ->(user_id) { self_assigned_product(user_id).where.not(id: self_assigned_no_replied_product_ids(user_id)) } + scope :self_assigned_and_replied_products, lambda { |user_id| + self_assigned_product(user_id) + .where.not(id: ProductSelfAssignedNoRepliedQuery.new(user_id:).call.select(:id).reorder(nil)) + } scope :wip, -> { where(wip: true) } scope :not_wip, -> { where(wip: false) } @@ -64,41 +67,8 @@ def self.add_latest_commented_at end end - # rubocop:disable Metrics/MethodLength - def self.self_assigned_no_replied_product_ids(user_id) - sql = <<~SQL - WITH last_comments AS ( - SELECT * - FROM comments AS parent - WHERE commentable_type = 'Product' AND id = ( - SELECT id - FROM comments AS child - WHERE parent.commentable_id = child.commentable_id - AND commentable_type = 'Product' - ORDER BY created_at DESC LIMIT 1 - ) - ), - self_assigned_products AS ( - SELECT products.* - FROM products - WHERE checker_id = ? - AND wip = false - ) - SELECT self_assigned_products.id - FROM self_assigned_products - LEFT JOIN last_comments ON self_assigned_products.id = last_comments.commentable_id - WHERE last_comments.id IS NULL - OR self_assigned_products.checker_id != last_comments.user_id - ORDER BY self_assigned_products.created_at DESC - SQL - Product.find_by_sql([sql, user_id]).map(&:id) - end - # rubocop:enable Metrics/MethodLength - def self.self_assigned_no_replied_products(user_id) - no_replied_product_ids = self_assigned_no_replied_product_ids(user_id) - Product.where(id: no_replied_product_ids) - .order(published_at: :asc, id: :asc) + ProductSelfAssignedNoRepliedQuery.new(user_id:).call end def self.require_assignment_products diff --git a/app/models/question.rb b/app/models/question.rb index c3cbd2d5a70..27a82e1277c 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -82,7 +82,7 @@ def unsolved_badge(current_user:, practice_id: nil) if practice_id.present? Question.not_solved.not_wip.where(practice_id:).size else - Question.not_solved.not_wip.size + ::Cache.not_solved_question_count end end end diff --git a/app/models/question_auto_closer.rb b/app/models/question_auto_closer.rb index c933d609230..dc74195bf66 100644 --- a/app/models/question_auto_closer.rb +++ b/app/models/question_auto_closer.rb @@ -98,8 +98,12 @@ def select_as_best_answer(close_answer) end def publish_events(correct_answer) - Newspaper.publish(:answer_save, { answer: correct_answer }) - Newspaper.publish(:correct_answer_save, { answer: correct_answer }) + method_name = __method__ + Newspaper.publish(:answer_save, { + answer: correct_answer, + action: "#{name}.#{method_name}" + }) + ActiveSupport::Notifications.instrument('correct_answer.save', answer: correct_answer) end end end diff --git a/app/queries/grass_learning_time_query.rb b/app/queries/grass_learning_time_query.rb new file mode 100644 index 00000000000..8e105dd3a9c --- /dev/null +++ b/app/queries/grass_learning_time_query.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +class GrassLearningTimeQuery < Patterns::Query + queries LearningTime + + def initialize(user, end_date, relation = LearningTime.all) + super(relation) + @user = user + @end_date = end_date + @start_date = end_date.prev_year.sunday + end + + private + + def query + relation + .from("(#{compiled_sql}) AS grass_data") + .select(:date, :velocity) + end + + def compiled_sql + ActiveRecord::Base.send(:sanitize_sql_array, [sql_template, sql_params]) + end + + def sql_template + <<~SQL + WITH series AS ( + SELECT date FROM generate_series(:start_date::DATE, :end_date::DATE, '1 day') AS series(date) + ), summary AS ( + SELECT + reported_on AS date, + EXTRACT(epoch FROM SUM(finished_at - started_at)) / 60 / 60 AS total_hour + FROM learning_times + JOIN reports ON learning_times.report_id = reports.id + WHERE reports.user_id = :user_id + AND reports.reported_on BETWEEN :start_date AND :end_date + GROUP BY reported_on + ) + SELECT + series.date, + CASE + WHEN summary.total_hour > 6 THEN 4 + WHEN summary.total_hour > 4 THEN 3 + WHEN summary.total_hour > 2 THEN 2 + WHEN summary.total_hour > 0 THEN 1 + ELSE 0 + END AS velocity + FROM series + LEFT JOIN summary ON series.date = summary.date + ORDER BY series.date + SQL + end + + def sql_params + { + start_date: @start_date, + end_date: @end_date, + user_id: @user.id + } + end +end diff --git a/app/queries/product_self_assigned_no_replied_query.rb b/app/queries/product_self_assigned_no_replied_query.rb new file mode 100644 index 00000000000..7c545ad89de --- /dev/null +++ b/app/queries/product_self_assigned_no_replied_query.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class ProductSelfAssignedNoRepliedQuery < Patterns::Query + queries Product + + private + + def initialize(relation = Product.all, user_id:) + super(relation) + @user_id = user_id + end + + def query + no_replied_product_ids = self_assigned_no_replied_product_ids + relation + .where(id: no_replied_product_ids) + .order(published_at: :asc, id: :asc) + end + + # rubocop:disable Metrics/MethodLength + def self_assigned_no_replied_product_ids + sql = <<~SQL + WITH last_comments AS ( + SELECT * + FROM comments AS parent + WHERE commentable_type = 'Product' AND id = ( + SELECT id + FROM comments AS child + WHERE parent.commentable_id = child.commentable_id + AND commentable_type = 'Product' + ORDER BY created_at DESC LIMIT 1 + ) + ), + self_assigned_products AS ( + SELECT products.* + FROM products + WHERE checker_id = ? + AND wip = false + ) + SELECT self_assigned_products.id + FROM self_assigned_products + LEFT JOIN last_comments ON self_assigned_products.id = last_comments.commentable_id + WHERE last_comments.id IS NULL + OR self_assigned_products.checker_id != last_comments.user_id + ORDER BY self_assigned_products.created_at DESC + SQL + Product.find_by_sql([sql, @user_id]).map(&:id) + end + # rubocop:enable Metrics/MethodLength +end diff --git a/app/views/admin/campaigns/index.html.slim b/app/views/admin/campaigns/index.html.slim index eded29f902d..4a1f46d3507 100644 --- a/app/views/admin/campaigns/index.html.slim +++ b/app/views/admin/campaigns/index.html.slim @@ -6,14 +6,6 @@ header.page-header .page-header__start h2.page-header__title | 管理ページ - .page-header__end - .page-header-actions - .page-header-actions__items - .page-header-actions__item - = link_to new_admin_campaign_path, class: 'a-button is-md is-secondary is-block' do - i.fa-regular.fa-plus - span - | お試し延長作成 = render 'admin/admin_page_tabs' @@ -24,6 +16,14 @@ main.page-main .page-main-header__start h1.page-main-header__title | お試し延長一覧 + .page-main-header__end + .page-main-header-actions + .page-main-header-actions__items + .page-main-header-actions__item + = link_to new_admin_campaign_path, class: 'a-button is-md is-secondary is-block' do + i.fa-regular.fa-plus + span + | お試し延長作成 hr.a-border .page-body .container.is-lg diff --git a/app/views/admin/campaigns/new.html.slim b/app/views/admin/campaigns/new.html.slim index eec3b19ac99..241e82630ed 100644 --- a/app/views/admin/campaigns/new.html.slim +++ b/app/views/admin/campaigns/new.html.slim @@ -17,9 +17,9 @@ main.page-main h1.page-main-header__title | お試し延長作成 .page-main-header__end - .page-header-actions - .page-header-actions__items - .page-header-actions__item + .page-main-header-actions + .page-main-header-actions__items + .page-main-header-actions__item = link_to admin_campaigns_path, class: 'a-button is-md is-secondary is-block is-back' do | お試し延長一覧 hr.a-border diff --git a/app/views/admin/faqs/_form.html.slim b/app/views/admin/faqs/_form.html.slim index f3216b14fc8..e62e41e20b2 100644 --- a/app/views/admin/faqs/_form.html.slim +++ b/app/views/admin/faqs/_form.html.slim @@ -1,5 +1,15 @@ = form_with model: [:admin, faq], local: true, html: { name: 'faq' } do |f| + = render 'errors', object: faq .form__items + .form-item + label.a-form-label + | カテゴリー + .checkboxes + .checkboxes__items + - @faq_categories.each do |faq_category| + .checkboxes__item.is-radio + = f.radio_button :faq_category_id, faq_category.id, class: 'a-toggle-checkbox' + = f.label :faq_category_id, faq_category.name, value: faq_category.id .form-item = f.label :question, class: 'a-form-label' = f.text_area :question, class: 'a-text-input is-xs' @@ -14,16 +24,6 @@ .a-form-label | プレビュー .js-preview.a-long-text.is-md.markdown-form__preview - .form-item - = render 'errors', object: faq - label.a-form-label - | カテゴリー - .checkboxes - .checkboxes__items - - @faq_categories.each do |faq_category| - .checkboxes__item.is-radio - = f.radio_button :faq_category_id, faq_category.id, class: 'a-toggle-checkbox' - = f.label :faq_category_id, faq_category.name, value: faq_category.id .form-actions ul.form-actions__items li.form-actions__item.is-main diff --git a/app/views/announcements/show.html.slim b/app/views/announcements/show.html.slim index 278f029a555..66e139f8186 100644 --- a/app/views/announcements/show.html.slim +++ b/app/views/announcements/show.html.slim @@ -55,6 +55,8 @@ hr.a-border .page-content-header-actions .page-content-header-actions__start = render 'watches/watch_toggle', type: @announcement.class.to_s, id: @announcement.id, watch: @announcement.watch_by(current_user) + .page-content-header-actions__action + = react_component('BookmarkButton', bookmarkableId: @announcement.id, bookmarkableType: 'Announcement') .page-content-header-actions__end .page-content-header-actions__action = link_to new_announcement_path(id: @announcement), class: 'a-button is-sm is-secondary is-block', id: 'copy' do diff --git a/app/views/api/events/_event.json.jbuilder b/app/views/api/events/_event.json.jbuilder deleted file mode 100644 index dd308c2814b..00000000000 --- a/app/views/api/events/_event.json.jbuilder +++ /dev/null @@ -1,8 +0,0 @@ -json.(event, :id, :title, :capacity, :wip, :start_at) -json.participants_count event.participants.count -json.waitlist_count event.waitlist.count -json.start_at_localized l(event.start_at) -json.comments_count event.comments.size -json.url event_url(event) -json.user event.user, partial: "api/users/user", as: :user -json.ended event.ended? diff --git a/app/views/api/events/index.json.jbuilder b/app/views/api/events/index.json.jbuilder deleted file mode 100644 index 5c4d3c80953..00000000000 --- a/app/views/api/events/index.json.jbuilder +++ /dev/null @@ -1,2 +0,0 @@ -json.events @events, partial: "api/events/event", as: :event -json.total_pages @events.total_pages diff --git a/app/views/application/_global_nav.slim b/app/views/application/_global_nav.slim index 2fc3daf3988..c3ad4c750ed 100644 --- a/app/views/application/_global_nav.slim +++ b/app/views/application/_global_nav.slim @@ -35,9 +35,9 @@ nav.global-nav = link_to questions_path(target: 'not_solved'), class: "global-nav-links__link #{current_link(/^questions/)}" do .global-nav-links__link-icon i.fa-solid.fa-comments-question-check - - if Question.not_solved.not_wip.count.positive? + - if Cache.not_solved_question_count.positive? .global-nav__item-count.a-notification-count - = Question.not_solved.not_wip.count + = Cache.not_solved_question_count .global-nav-links__link-label Q&A li.global-nav-links__item = link_to '/pages', class: "global-nav-links__link #{(@page&.slug != 'help' && @page&.slug != 'help-adviser') && current_link(/^pages/)}" do diff --git a/app/views/events/_events.html.slim b/app/views/events/_events.html.slim index e69de29bb2d..3327ef9a11c 100644 --- a/app/views/events/_events.html.slim +++ b/app/views/events/_events.html.slim @@ -0,0 +1,43 @@ += paginate @events +ul.card-list.a-card + - @events.each do |event| + li.card-list-item + .card-list-item__inner + .card-list-item__user + = render 'users/icon', user: event.user, link_class: 'card-list-item__user-link', image_class: 'card-list-item__user-icon' + .card-list-item__rows + .card-list-item__row + .card-list-item-title + - if event.wip + .a-list-item-badge.is-wip + span + | WIP + - elsif event.ended? + .a-list-item-badge.is-ended + span + | 終了 + h2.card-list-item-title__title(itemprop='name') + = link_to event, itemprop: 'url', class: 'card-list-item-title__link a-text-link' do + = event.title + .card-list-item__row + = link_to user_path(event.user), class: 'a-user-name' do + = event.user.long_name + .card-list-item__row + .card-list-item-meta + .card-list-item-meta__items + .card-list-item-meta__item + time.a-meta(datetime=event.start_at) + span.a-meta__label 開催日時 + span.a-meta__value = l event.start_at + .card-list-item-meta__item + .a-meta + | 参加者(#{event.participants.count}名 / #{event.capacity}名) + - if event.waitlist.count.positive? + .card-list-item-meta__item + .a-meta + | 補欠者(#{event.waitlist.count}名) + - if event.comments.size.positive? + .card-list-item-meta__item + .a-meta + | コメント(#{event.comments.size}) += paginate @events diff --git a/app/views/events/index.html.slim b/app/views/events/index.html.slim index d9fc08c5ba6..c2e068b79f7 100644 --- a/app/views/events/index.html.slim +++ b/app/views/events/index.html.slim @@ -23,5 +23,12 @@ .page-body .page-body__inner.has-side-nav .container.is-md - = react_component 'Events' + - if @events.empty? + .o-empty-message + .o-empty-message__icon + i.fa-regular.fa-sad-tear + p.o-empty-message__text + | 登録されている特別イベントはありません。 + - else + = render 'events/events' = render 'events/upcoming_events_groups', upcoming_events_groups: @upcoming_events_groups diff --git a/app/views/home/_unchecked_report_alert.html.slim b/app/views/home/_unchecked_report_alert.html.slim index edef920300b..3fce7507492 100644 --- a/app/views/home/_unchecked_report_alert.html.slim +++ b/app/views/home/_unchecked_report_alert.html.slim @@ -7,7 +7,7 @@ .unchecked-report-alert__icon i.fa-regular.fa-triangle-exclamation .unchecked-report-alert__message - = link_to '/reports/unchecked', class: 'unchecked-report-alert__message-link' do + = link_to reports_unchecked_index_path, class: 'unchecked-report-alert__message-link' do | 未チェックの日報が strong.unchecked-report-alert__count span.unchecked-report-alert__count-number diff --git a/app/views/reports/index.html.slim b/app/views/reports/index.html.slim index 39e8acdb943..956309b9bc0 100644 --- a/app/views/reports/index.html.slim +++ b/app/views/reports/index.html.slim @@ -20,10 +20,10 @@ main.page-main nav.pill-nav ul.pill-nav__items li.pill-nav__item - = link_to '全て', reports_path, class: "pill-nav__item-link#{' is-active' unless params[:unchecked]}" + = link_to '全て', reports_path, class: "pill-nav__item-link#{' is-active' if controller_path == 'reports'}" li.pill-nav__item - = link_to '未チェック', reports_path(unchecked: true), class: "pill-nav__item-link#{' is-active' if params[:unchecked]}" - - if params[:unchecked].blank? + = link_to '未チェック', reports_unchecked_index_path, class: "pill-nav__item-link#{' is-active' if controller_path == 'reports/unchecked'}" + - if controller_path != 'reports/unchecked' nav.page-filter.form.pb-0 .container.is-md = form_with url: reports_path, local: true, method: :get do @@ -44,4 +44,6 @@ main.page-main .card-list.a-card .card-list__items = render partial: 'reports/report', collection: @reports, as: :report, locals: { user_icon_display: true, actions_display: true } + - if mentor_login? && controller_path == 'reports/unchecked' + = render partial: 'unconfirmed_links_open', locals: { label: '未チェックの日報を一括で開く' } = paginate @reports diff --git a/app/views/shared/_check_actions.html.slim b/app/views/shared/_check_actions.html.slim index 927998953a6..34e41540f71 100644 --- a/app/views/shared/_check_actions.html.slim +++ b/app/views/shared/_check_actions.html.slim @@ -22,8 +22,9 @@ = checkable.checker.login_name li.card-main-actions__item class=(checkable.checks.any? ? 'is-sub' : '') - - if check - = form_with url: polymorphic_path([checkable, check]), method: :delete, local: true do |f| + - checked_checkable = checkable.checks.first + - if checked_checkable + = form_with url: polymorphic_path([checkable, checked_checkable]), method: :delete, local: true do |f| - if checkable_type == 'Product' = f.submit "#{checkable_label}の合格を取り消す", class: 'card-main-actions__muted-action' - else diff --git a/app/views/welcome/_pricing.html.slim b/app/views/welcome/_pricing.html.slim index 6e54607e379..621f33f5cb4 100644 --- a/app/views/welcome/_pricing.html.slim +++ b/app/views/welcome/_pricing.html.slim @@ -14,25 +14,16 @@ section.lp-content.is-lp-bg-2.is-top-title#pricing | 月額 span.lp-price__value span.lp-price__amount - | 29,800 + | 32,780 span.lp-price__currency | 円 - span.lp-price__details - span.lp-price__tax - | (税込) - .new-price-info.text-center.a-notice-block.is-danger.mt-4 - p.text-base - | 2023年10月1日より、価格改定を行います。 - br - | 価格改定後は月額 税抜29,800円(税込32,780円) - br - = link_to 'https://bootcamp.fjord.jp/articles/177', class: 'a-link is-primary', target: '_blank', rel: 'noopener' do - | 価格改定の詳細はこちら - + span.lp-price__details + span.lp-price__tax + | (税込) p.lp-price-note.text-center.mt-4 = link_to training_path do | 法人でのご利用の場合 - | 、月額 99,800円(税込) + | 、月額 109,780円(税込) .lp-content-stack__item section.lp-content-section diff --git a/app/views/welcome/certified_reskill_courses/rails_developer_course/_course_fees.html.slim b/app/views/welcome/certified_reskill_courses/rails_developer_course/_course_fees.html.slim index 7bc408a934c..42ca2c6582d 100644 --- a/app/views/welcome/certified_reskill_courses/rails_developer_course/_course_fees.html.slim +++ b/app/views/welcome/certified_reskill_courses/rails_developer_course/_course_fees.html.slim @@ -9,17 +9,6 @@ section.lp-content.is-lp-bg-2.is-top-title .lp-content-stack .lp-content-stack__item .l-cards.is-stacked - .l-cards__item.is-wide - .l-cards__item-inner - .new-price-info.text-center.a-notice-block.is-danger - p.text-base - | 2023年10月1日より、価格改定を行います。 - br - | その前のお申し込みをおすすめします! - br - = link_to 'https://bootcamp.fjord.jp/articles/177', class: 'a-link is-primary', target: '_blank', rel: 'noopener' do - | 価格改定の詳細はこちら - .l-cards__item.is-wide .l-cards__item-inner .a-card @@ -31,10 +20,10 @@ section.lp-content.is-lp-bg-2.is-top-title .lp-left-image-section__end header.lp-left-image-section__header h3.lp-card-title - | 受講料 + 教材費 + | 受講料 h4.lp-price.is-sm span.lp-price__value - span.lp-price__amount 746,218 + span.lp-price__amount 786,720 span.lp-price__currency 円 span.lp-price__details span.lp-price__tax (税込) @@ -44,11 +33,8 @@ section.lp-content.is-lp-bg-2.is-top-title | Railsエンジニア(Reスキル認定講座対応)コースの場合、 = link_to practices_path do | 通常のRailsエンジニアコース - | とは違い、受講料(715,200円)と教材費(31,018円)を + | とは違い、受講料(税込 786,720円)を | サブスクではなく、一括でお支払いいただく必要があります。 - p - | Railsエンジニア(Reスキル認定講座対応)コースの場合、 - | フィヨルドブートキャンプで教材を揃え、ご自宅に発送します。 .l-cards__item.is-wide .l-cards__item-inner .a-card.is-danger @@ -62,14 +48,14 @@ section.lp-content.is-lp-bg-2.is-top-title | 給付金最大80% h4.lp-price.is-sm span.lp-price__value - span.lp-price__amount 596,974 + span.lp-price__amount 629,376 span.lp-price__currency 円 .lp-card-description .a-short-text p | 給付金最大80%を受給した場合、 - | 受講料 + 教材費746,218円の80%である - | 596,974円が給付されます。 + | 受講料(税込)の80%である + | 629,376円が給付されます。 .lp-left-image-section .lp-left-image-section__inner .lp-left-image-section__start.is-hidden-sm-down @@ -80,14 +66,14 @@ section.lp-content.is-lp-bg-2.is-top-title | 最大給付金適用後の自己負担額 h4.lp-price.is-sm span.lp-price__value - span.lp-price__amount 149,244 + span.lp-price__amount 157,344 span.lp-price__currency 円 .lp-card-description .a-short-text p | 給付金最大80%を受給した場合、 | 実質自己負担額は - | 149,244円になります。 + | 157,344円になります。 .lp-content-stack__item .lp-content__description .l-inner-container.is-sm diff --git a/app/views/welcome/certified_reskill_courses/rails_developer_course/_course_overview.html.slim b/app/views/welcome/certified_reskill_courses/rails_developer_course/_course_overview.html.slim index 79bb9f6896f..b3b1ae3417e 100644 --- a/app/views/welcome/certified_reskill_courses/rails_developer_course/_course_overview.html.slim +++ b/app/views/welcome/certified_reskill_courses/rails_developer_course/_course_overview.html.slim @@ -73,9 +73,9 @@ section.lp-content.is-lp-bg-1.is-top-title | 受講料 td p - | 715,200円(税込) + | 786,720円(税込) br - | ※授業料に必須教材費は含まれていません。 + | ※受講料に必須教材費は含まれていません。 tr th | 必須教材費 @@ -84,6 +84,8 @@ section.lp-content.is-lp-bg-1.is-top-title | 31,018円(税込) br | ※カリキュラムで指定している必須書籍の合計金額。 + | セールの状況や電子書籍 or 紙の書籍の選択などで + | 金額が変わります。 tr th | 受講料の支払方法 diff --git a/app/views/welcome/index.html.slim b/app/views/welcome/index.html.slim index 4643dfcccee..568ce7945e1 100644 --- a/app/views/welcome/index.html.slim +++ b/app/views/welcome/index.html.slim @@ -1,5 +1,5 @@ - set_meta_tags(site: 'プログラミングスクール FJORD BOOT CAMP(フィヨルドブートキャンプ)', - description: '月額29,800円、全機能が使えるお試し期間付き。FBCは現場の即戦力になるためのスキルとプログラミングの楽しさを伝える、現役ソフトウェアエンジニアが考える理想のプログラミングスクールの実現に励んでいます。') + description: '月額32,780円(税込)、全機能が使えるお試し期間付き。FBCは現場の即戦力になるためのスキルとプログラミングの楽しさを伝える、現役ソフトウェアエンジニアが考える理想のプログラミングスクールの実現に励んでいます。') - content_for :extra_body_classes, 'welcome welcome-home' - content_for :head_last do = javascript_include_tag 'https://sdk.form.run/js/v2/formrun.js' diff --git a/app/views/welcome/law.html.slim b/app/views/welcome/law.html.slim index d18019a0f87..157e1ea0171 100644 --- a/app/views/welcome/law.html.slim +++ b/app/views/welcome/law.html.slim @@ -1,6 +1,6 @@ - title '特定商取引法に基づく表記' - set_meta_tags(site: 'FJORD BOOT CAMP(フィヨルドブートキャンプ)', - description: '「特定商取引に関する法律」第11条(通信販売についての広告)に基づきこのページに明示いたします。オンラインプログラミングスクールのフィヨルドブートキャンプは月額29,800円、全機能が使えるお試し期間付き。') + description: '「特定商取引に関する法律」第11条(通信販売についての広告)に基づきこのページに明示いたします。オンラインプログラミングスクールのフィヨルドブートキャンプは月額32,780円(税込)、全機能が使えるお試し期間付き。') header.lp-page-header .l-container diff --git a/app/views/welcome/pricing.html.slim b/app/views/welcome/pricing.html.slim index 198e702c363..bffbd821d26 100644 --- a/app/views/welcome/pricing.html.slim +++ b/app/views/welcome/pricing.html.slim @@ -3,7 +3,7 @@ ruby: title '料金' trial_period = Campaign.current_trial_period - set_meta_tags(site: 'FJORD BOOT CAMP(フィヨルドブートキャンプ)', - description: 'フィヨルドブートキャンプの利用料は月額29,800円です。クレジットカードでのお支払いになります。初期費用、入会金は一切ありません。') + description: 'フィヨルドブートキャンプの利用料は月額32,780円(税込)です。クレジットカードでのお支払いになります。初期費用、入会金は一切ありません。') article.lp header.lp-content.is-lp-bg-main.is-hero @@ -45,24 +45,16 @@ article.lp | 月額 span.lp-price__value span.lp-price__amount - | 29,800 + | 32,780 span.lp-price__currency | 円 span.lp-price__details span.lp-price__tax | (税込) - .new-price-info.text-center.a-notice-block.is-danger.mt-4 - p.text-base - | 2023年10月1日より、価格改定を行います。 - br - | 価格改定後は月額 税抜29,800円(税込32,780円) - br - = link_to 'https://bootcamp.fjord.jp/articles/177', class: 'a-link is-primary', target: '_blank', rel: 'noopener' do - | 価格改定の詳細はこちら p.lp-price-note.text-center.mt-4 = link_to training_path do | 法人でのご利用の場合 - | 、月額 99,800円(税込) + | 、月額 109,780円(税込) .lp-content__end .lp-content__description.mb-12 .l-inner-container.is-md diff --git a/app/views/welcome/training.html.slim b/app/views/welcome/training.html.slim index fe7cb3d192c..4eca8567328 100644 --- a/app/views/welcome/training.html.slim +++ b/app/views/welcome/training.html.slim @@ -300,7 +300,7 @@ article.lp | 研修の成果も向上します。 section.lp-content.is-lp-bg-1.is-top-title.corporate-training-fee - .l-container.is-md + .l-container.is-lg .lp-content__inner .lp-content__start header.lp-content__header @@ -309,32 +309,22 @@ article.lp .lp-content__end .lp-content-stack .lp-content-stack__item + p.lp-price.mb-2 + span.lp-price__total-value + | 1名につき月額 p.lp-price - span.lp-price__label - | 月額 span.lp-price__value span.lp-price__amount - | 99,800 + | 109,780 span.lp-price__currency | 円 - span.lp-price__details - span.lp-price__tax - | (税込) - span.lp-price__separator - | / - span.lp-price__per-person - | 人 - .new-price-info.text-center.a-notice-block.is-danger.mt-4 - p.text-base - | 2023年10月1日より、価格改定を行います。 - br - | 価格改定後は月額 税抜99,800円(税込109,780円) - br - = link_to 'https://bootcamp.fjord.jp/articles/177', class: 'a-link is-primary', target: '_blank', rel: 'noopener' do - | 価格改定の詳細はこちら - + span.lp-price__details + span.lp-price__tax + | (税込) p.lp-price-note.text-center.mt-4 - | 一般利用の場合は、月額 29,800円(税込) + = link_to pricing_path do + | 一般利用の場合 + | 、月額 32,780円(税込) .lp-content-stack__item h3.lp-content-sub-title.text-center | 一般利用と法人利用の違い diff --git a/config/initializers/active_support_notifications.rb b/config/initializers/active_support_notifications.rb index 78fd7f5052e..84d2951c216 100644 --- a/config/initializers/active_support_notifications.rb +++ b/config/initializers/active_support_notifications.rb @@ -4,6 +4,7 @@ ActiveSupport::Notifications.subscribe('answer.create', AnswererWatcher.new) ActiveSupport::Notifications.subscribe('answer.create', AnswerNotifier.new) ActiveSupport::Notifications.subscribe('answer.create', NotifierToWatchingUser.new) + ActiveSupport::Notifications.subscribe('correct_answer.save', CorrectAnswerNotifier.new) ActiveSupport::Notifications.subscribe('event.create', EventOrganizerWatcher.new) ActiveSupport::Notifications.subscribe('regular_event.create', RegularEventOrganizerWatcher.new) sad_streak_updater = SadStreakUpdater.new @@ -30,7 +31,9 @@ ActiveSupport::Notifications.subscribe('product.update', ProductUpdateNotifierForWatcher.new) ActiveSupport::Notifications.subscribe('product.update', ProductUpdateNotifierForChecker.new) ActiveSupport::Notifications.subscribe('came.comment', CommentNotifier.new) - + ActiveSupport::Notifications.subscribe('graduation.update', GraduationNotifier.new) + ActiveSupport::Notifications.subscribe('comeback.update', ComebackNotifier.new) + learning_status_updater = LearningStatusUpdater.new ActiveSupport::Notifications.subscribe('product.save', learning_status_updater) ActiveSupport::Notifications.subscribe('check.create', learning_status_updater) diff --git a/config/initializers/newspaper.rb b/config/initializers/newspaper.rb index 6f1afea679f..6dd95f1fb73 100644 --- a/config/initializers/newspaper.rb +++ b/config/initializers/newspaper.rb @@ -4,11 +4,6 @@ answer_cache_destroyer = AnswerCacheDestroyer.new Newspaper.subscribe(:answer_save, answer_cache_destroyer) Newspaper.subscribe(:answer_destroy, answer_cache_destroyer) - Newspaper.subscribe(:correct_answer_save, CorrectAnswerNotifier.new) - - Newspaper.subscribe(:graduation_update, GraduationNotifier.new) - - Newspaper.subscribe(:comeback_update, ComebackNotifier.new) unfinished_data_destroyer = UnfinishedDataDestroyer.new Newspaper.subscribe(:retirement_create, unfinished_data_destroyer) diff --git a/config/routes/api.rb b/config/routes/api.rb index 508f43eed5a..70ddc5b37b2 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -72,7 +72,6 @@ end resources :products, only: %i(index show) resources :bookmarks, only: %i(index create destroy) - resources :events, only: %i(index) resources :report_templates, only: %i(create update) resources :markdown_tasks, only: %i(create) namespace :talks do diff --git a/config/routes/reports.rb b/config/routes/reports.rb index a92fad36217..e9ad5259b7a 100644 --- a/config/routes/reports.rb +++ b/config/routes/reports.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true Rails.application.routes.draw do + namespace :reports do + resources :unchecked, only: %i[index] + end resources :reports do resources :checks, only: [:create, :destroy] end diff --git a/db/schema.rb b/db/schema.rb index b68008280c3..4b4814deabb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2025_08_20_212112) do +ActiveRecord::Schema.define(version: 2025_09_05_025850) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" diff --git a/test/integration/api/events_test.rb b/test/integration/api/events_test.rb deleted file mode 100644 index a9ed814fa97..00000000000 --- a/test/integration/api/events_test.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require 'test_helper' - -class API::EventsTest < ActionDispatch::IntegrationTest - test 'GET /api/events.json' do - get api_events_path(format: :json) - assert_response :unauthorized - - token = create_token('kimura', 'testtest') - get api_events_path(format: :json), - headers: { 'Authorization' => "Bearer #{token}" } - assert_response :ok - end -end diff --git a/test/queries/grass_learning_time_query_test.rb b/test/queries/grass_learning_time_query_test.rb new file mode 100644 index 00000000000..b58b1577ea0 --- /dev/null +++ b/test/queries/grass_learning_time_query_test.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require 'test_helper' + +class GrassLearningTimeQueryTest < ActiveSupport::TestCase + setup do + @user = users(:sotugyou) + end + + test 'calculates velocity based on total learning time for the same day' do + report = Report.create!( + user: @user, + reported_on: Date.new(2025, 1, 3), + title: '同日の複数記録', + description: '午前と午後に勉強', + emotion: :happy + ) + + LearningTime.create!( + report:, + started_at: Time.zone.local(2025, 1, 3, 9), + finished_at: Time.zone.local(2025, 1, 3, 11) + ) + + LearningTime.create!( + report:, + started_at: Time.zone.local(2025, 1, 3, 13), + finished_at: Time.zone.local(2025, 1, 3, 16) + ) + + end_date = Date.new(2025, 1, 3) + results = GrassLearningTimeQuery.new(@user, end_date, LearningTime.all).call + result_by_date = results.index_by { |r| r.date.to_date } + + assert_equal 3, result_by_date[Date.new(2025, 1, 3)].velocity + end + + test 'returns velocity 0 for dates with no learning data' do + # 2025-01-04 に何も記録しない + end_date = Date.new(2025, 1, 4) + results = GrassLearningTimeQuery.new(@user, end_date, LearningTime.all).call + result_by_date = results.index_by { |r| r.date.to_date } + + assert_equal 0, result_by_date[Date.new(2025, 1, 4)].velocity + end + + test 'returns series from the Sunday of end_date.prev_year to end_date with correct length' do + end_date = Date.new(2025, 1, 3) + results = GrassLearningTimeQuery.new(@user, end_date, LearningTime.all).call + dates = results.map { |r| r.date.to_date } + + expected_start_date = end_date.prev_year.sunday + assert_equal expected_start_date, dates.first + expected_length = (end_date - expected_start_date).to_i + 1 + assert_equal expected_length, dates.size + end + + test 'velocity is 1 when total_hour is exactly 2' do + report = Report.create!( + user: @user, + reported_on: Date.new(2025, 1, 5), + title: '2時間テスト', + description: 'テスト用ダミー内容', + emotion: :happy + ) + LearningTime.create!( + report:, + started_at: Time.zone.local(2025, 1, 5, 10), + finished_at: Time.zone.local(2025, 1, 5, 12) + ) + + results = GrassLearningTimeQuery.new(@user, Date.new(2025, 1, 5)).call + result_by_date = results.index_by { |r| r.date.to_date } + + assert_equal 1, result_by_date[Date.new(2025, 1, 5)].velocity + end + + test 'velocity is 2 when total_hour is exactly 4' do + report = Report.create!( + user: @user, + reported_on: Date.new(2025, 1, 6), + title: '4時間テスト', + description: 'テスト用ダミー内容', + emotion: :happy + ) + LearningTime.create!( + report:, + started_at: Time.zone.local(2025, 1, 6, 9), + finished_at: Time.zone.local(2025, 1, 6, 13) + ) + + results = GrassLearningTimeQuery.new(@user, Date.new(2025, 1, 6)).call + result_by_date = results.index_by { |r| r.date.to_date } + + assert_equal 2, result_by_date[Date.new(2025, 1, 6)].velocity + end + + test 'velocity is 3 when total_hour is exactly 6' do + report = Report.create!( + user: @user, + reported_on: Date.new(2025, 1, 7), + title: '6時間テスト', + description: 'テスト用ダミー内容', + emotion: :happy + ) + LearningTime.create!( + report:, + started_at: Time.zone.local(2025, 1, 7, 8), + finished_at: Time.zone.local(2025, 1, 7, 14) + ) + + results = GrassLearningTimeQuery.new(@user, Date.new(2025, 1, 7)).call + result_by_date = results.index_by { |r| r.date.to_date } + + assert_equal 3, result_by_date[Date.new(2025, 1, 7)].velocity + end +end diff --git a/test/queries/product_self_assigned_no_replied_query_test.rb b/test/queries/product_self_assigned_no_replied_query_test.rb new file mode 100644 index 00000000000..85fedf7bf56 --- /dev/null +++ b/test/queries/product_self_assigned_no_replied_query_test.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ProductSelfAssignedNoRepliedQueryTest < ActiveSupport::TestCase + test 'should return product self assigned no replied' do + mentor = users(:mentormentaro) + no_replied_product = Product.create!( + body: 'test', + user: users(:kimura), + practice: practices(:practice5), + checker_id: mentor.id, + published_at: Time.current, + wip: false + ) + + result = ProductSelfAssignedNoRepliedQuery.new(user_id: mentor.id).call + + assert_includes result, no_replied_product + end + + test 'should not include products where user has already replied' do + mentor = users(:mentormentaro) + + product = Product.create!( + body: 'test', + user: users(:kimura), + practice: practices(:practice5), + checker_id: mentor.id, + published_at: Time.current, + wip: false + ) + + Comment.create!( + commentable: product, + user: mentor, + description: '返信コメント' + ) + + result = ProductSelfAssignedNoRepliedQuery.new(user_id: mentor.id).call + + assert_not_includes result, product + end + + test 'should include products where last comment is not by checker' do + mentor = users(:mentormentaro) + user = users(:kimura) + + product = Product.create!( + body: 'test', + user:, + practice: practices(:practice5), + checker_id: mentor.id, + published_at: Time.current, + wip: false + ) + + Comment.create!( + commentable: product, + user:, + description: '生徒からの返信コメント' + ) + + result = ProductSelfAssignedNoRepliedQuery.new(user_id: mentor.id).call + + assert_includes result, product + end + + test 'should be ordered by published_at asc' do + mentor = users(:mentormentaro) + user = users(:hajime) + + time = Time.current + + Product.create!( + body: 'test', + user:, + practice: practices(:practice1), + checker_id: mentor.id, + published_at: time, + wip: false + ) + + Product.create!( + body: 'test', + user:, + practice: practices(:practice2), + checker_id: mentor.id, + published_at: time, + wip: false + ) + + Product.create!( + body: 'test', + user:, + practice: practices(:practice3), + checker_id: mentor.id, + published_at: time, + wip: false + ) + + result = ProductSelfAssignedNoRepliedQuery.new(user_id: mentor.id).call + + assert_equal(result, result.sort_by { |p| [p.published_at, p.id] }) + end +end diff --git a/test/system/admin/campaigns_test.rb b/test/system/admin/campaigns_test.rb index c2b93dc8282..07c617eb47a 100644 --- a/test/system/admin/campaigns_test.rb +++ b/test/system/admin/campaigns_test.rb @@ -85,7 +85,7 @@ class CampaignsTest < ApplicationSystemTestCase # example_end_at = (TODAY + 3.days).strftime('%-m月%-d日10時10分9秒') # example_pay_at = (TODAY + 3.days).strftime('%-m月%-d日10時10分10秒') - # assert_text "3日間のお試し期間\n月額29,800円は決して安い金額ではありません。" + # assert_text "3日間のお試し期間\n月額32,780円(税込)は決して安い金額ではありません。" # assert_text 'フィヨルドブートキャンプを使うべきかを判断するために3日間のお試し期間を用意' # assert_text 'その3日間、がっつりフィヨルドブートキャンプを見たり使ったりして判断してください。' diff --git a/test/system/articles_test.rb b/test/system/articles_test.rb index e88284c6852..75d5e3063d1 100644 --- a/test/system/articles_test.rb +++ b/test/system/articles_test.rb @@ -265,7 +265,7 @@ class ArticlesTest < ApplicationSystemTestCase end assert_text '記事を作成しました' - meta_description = '月額29,800円、全機能が使えるお試し期間付き。FBCは現場の即戦力になるためのスキルとプログラミングの楽しさを伝える、現役ソフトウェアエンジニアが考える理想のプログラミングスクールの実現に励んでいます。' + meta_description = '月額32,780円(税込)、全機能が使えるお試し期間付き。FBCは現場の即戦力になるためのスキルとプログラミングの楽しさを伝える、現役ソフトウェアエンジニアが考える理想のプログラミングスクールの実現に励んでいます。' assert_selector "meta[name='description'][content='#{meta_description}']", visible: false assert_selector "meta[property='og:description'][content='#{meta_description}']", visible: false assert_selector "meta[name='twitter:description'][content='#{meta_description}']", visible: false diff --git a/test/system/bookmarks_test.rb b/test/system/bookmarks_test.rb index 62e1cebd1b9..f2e3be15317 100644 --- a/test/system/bookmarks_test.rb +++ b/test/system/bookmarks_test.rb @@ -6,6 +6,7 @@ class BookmarksTest < ApplicationSystemTestCase setup do @report = reports(:report1) @question = questions(:question1) + @announcement = announcements(:announcement1) end test 'show my bookmark report' do @@ -78,4 +79,28 @@ class BookmarksTest < ApplicationSystemTestCase visit '/current_user/bookmarks' assert_no_text @question.title end + + test 'bookmark announcement' do + visit_with_auth "/announcements/#{@announcement.id}", 'hatsuno' + find('#bookmark-button').click + assert_selector '#bookmark-button.is-active' + assert_no_selector '#bookmark-button.is-inactive' + + visit '/current_user/bookmarks' + assert_text @announcement.title + end + + test 'unbookmark announcement' do + user = users(:kimura) + user.bookmarks.create!(bookmarkable: @announcement) + + visit_with_auth "/announcements/#{@announcement.id}", user.login_name + assert_selector '#bookmark-button.is-active' + find('#bookmark-button').click + assert_selector '#bookmark-button.is-inactive' + assert_no_selector '#bookmark-button.is-active' + + visit '/current_user/bookmarks' + assert_no_text @announcement.title + end end diff --git a/test/system/events_test.rb b/test/system/events_test.rb index e8717f69bb9..8b4af09931d 100644 --- a/test/system/events_test.rb +++ b/test/system/events_test.rb @@ -249,7 +249,9 @@ class EventsTest < ApplicationSystemTestCase assert_text '参加登録しました' visit_with_auth events_path, 'hatsuno' - click_link '先着順のイベント' + within 'ul.card-list' do + click_link '先着順のイベント' + end accept_confirm do click_link '参加申込' end @@ -279,7 +281,9 @@ class EventsTest < ApplicationSystemTestCase assert_text '参加登録しました' visit_with_auth events_path, 'hatsuno' - click_link '補欠者のいるイベント' + within 'ul.card-list' do + click_link '補欠者のいるイベント' + end accept_confirm do click_link '補欠登録' end @@ -309,7 +313,9 @@ class EventsTest < ApplicationSystemTestCase assert_text '参加登録しました' visit_with_auth events_path, 'hatsuno' - click_link '補欠者が繰り上がるイベント' + within 'ul.card-list' do + click_link '補欠者が繰り上がるイベント' + end accept_confirm do click_link '補欠登録' end @@ -320,7 +326,9 @@ class EventsTest < ApplicationSystemTestCase end visit_with_auth events_path, 'kimura' - click_link '補欠者が繰り上がるイベント' + within 'ul.card-list' do + click_link '補欠者が繰り上がるイベント' + end accept_confirm do click_link '参加を取り消す' end @@ -443,7 +451,7 @@ class EventsTest < ApplicationSystemTestCase assert_text 'イベントを作成しました' within 'form[name=announcement]' do assert has_field? 'announcement[title]', with: /#{event[:title]}/ - assert has_field? 'announcement[description]', with: /#{event[:desription]}/ + assert has_field? 'announcement[description]', with: /#{event[:description]}/ end end @@ -473,7 +481,7 @@ class EventsTest < ApplicationSystemTestCase assert_text 'イベントを更新しました' within 'form[name=announcement]' do assert has_field? 'announcement[title]', with: /#{event[:title]}/ - assert has_field? 'announcement[description]', with: /#{event[:desription]}/ + assert has_field? 'announcement[description]', with: /#{event[:description]}/ end end diff --git a/test/system/markdown_test.rb b/test/system/markdown_test.rb index 1b220e71ac4..5fea5b0a5ae 100644 --- a/test/system/markdown_test.rb +++ b/test/system/markdown_test.rb @@ -16,6 +16,8 @@ class MarkdownTest < ApplicationSystemTestCase end test 'javascript link is sanitized' do + skip 'markdown-it-purifierの問題が解決したら戻す' + visit_with_auth new_page_path, 'komagata' fill_in 'page[title]', with: 'リンク除去' fill_in 'page[body]', with: 'リンク' diff --git a/test/system/notification/reports_test.rb b/test/system/notification/reports_test.rb index e0394b4bc88..ef65e73ce26 100644 --- a/test/system/notification/reports_test.rb +++ b/test/system/notification/reports_test.rb @@ -123,7 +123,7 @@ class Notification::ReportsTest < ApplicationSystemTestCase assert_no_text 'kimuraさんがはじめての日報を書きました!' end - test '複数の日報が投稿されているときは通知が飛ばない' do + test 'no notification if report already posted' do # 他のテストの通知に影響を受けないよう、テスト実行前に通知を削除する visit_with_auth '/notifications', 'muryou' click_link '全て既読にする' @@ -180,7 +180,7 @@ def assert_notify_only_at_first_published_of_report( end end - test '研修生が日報を作成し提出した時、企業のアドバイザーに通知する' do + test 'notify company advisor only when report is initially posted' do kensyu_login_name = 'kensyu' advisor_login_name = 'senpai' title = '研修生が日報を作成し提出した時' @@ -197,7 +197,7 @@ def assert_notify_only_at_first_published_of_report( ) end - test '初めて提出した時だけ、フォローされているユーザーに通知する' do + test 'notify follower only when report is initially posted' do following = Following.first followed_user_login_name = User.find(following.followed_id).login_name follower_user_login_name = User.find(following.follower_id).login_name @@ -215,7 +215,7 @@ def assert_notify_only_at_first_published_of_report( ) end - test '初めて提出した時だけ、メンション通知する' do + test 'notify mention target only when report is initially posted' do mention_target_login_name = 'kimura' author_login_name = 'machida' title = '初めて提出したら、' @@ -229,7 +229,7 @@ def assert_notify_only_at_first_published_of_report( ) end - test '初日報は初めて公開した時だけ通知する' do + test 'notify user only when first report is initially posted' do check_notification_login_name = 'machida' author_login_name = 'nippounashi' title = '初めての日報を提出したら' diff --git a/test/system/reports_practice_filter_test.rb b/test/system/reports_practice_filter_test.rb index fe3c1c5dc0c..70c4e3092f8 100644 --- a/test/system/reports_practice_filter_test.rb +++ b/test/system/reports_practice_filter_test.rb @@ -34,9 +34,4 @@ class ReportsPracticeFilterTest < ApplicationSystemTestCase assert_text 'レポート1', wait: 5 assert_no_text 'レポート2', wait: 5 end - - test 'practice filter is hidden when unchecked parameter is present' do - visit reports_path(unchecked: true) - assert_no_selector 'select#js-choices-single-select', wait: 5 - end end