diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index e2d6d54fc12..cca9379c689 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -57,6 +57,8 @@ @import "./application/blocks/page-content/_page-content-header.css"; @import "./application/blocks/page-content/_page-content-members.css"; @import "./application/blocks/page-content/_page-content-prev-next.css"; +@import "./application/blocks/pair-work/_pair-work-info.css"; +@import "./application/blocks/pair-work/_pair-work-schedule-dates.css"; @import "./application/blocks/practice/_categories.css"; @import "./application/blocks/practice/_category-practices-item.css"; @import "./application/blocks/practice/_completion-massage.css"; diff --git a/app/assets/stylesheets/application/blocks/event/_event-main-actions.css b/app/assets/stylesheets/application/blocks/event/_event-main-actions.css index d18f20e3acd..f6582ae9e54 100644 --- a/app/assets/stylesheets/application/blocks/event/_event-main-actions.css +++ b/app/assets/stylesheets/application/blocks/event/_event-main-actions.css @@ -2,21 +2,23 @@ .event-main-actions { position: relative; z-index: 1; - padding-block: .75rem; margin-inline: 1rem; border-radius: 4px; } +.event-main-actions:first-child { + margin-top: 1rem; + margin-bottom: 0; +} + @media (min-width: 48em) { .event-main-actions { - padding-inline: 2rem; margin-bottom: 1.5rem; } } @media (max-width: 47.9375em) { .event-main-actions { - padding-inline: .75rem; margin-bottom: 1rem; } } @@ -45,6 +47,72 @@ color: var(--danger); } +.event-main-actions__header { + padding-block: .75rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.event-main-actions__header .a-button { + margin-block: -.5rem; +} + +@media (min-width: 48em) { + .event-main-actions__header { + padding-inline: 1rem; + } +} + +@media (max-width: 47.9375em) { + .event-main-actions__header { + padding-inline: .75rem; + } +} + +.is-participationed .event-main-actions__header { + border-bottom: 1px solid var(--success); + color: #4e732e; +} + +.is-unparticipationed.is-available .event-main-actions__header { + border-bottom: 1px solid var(--primary); + color: #28248c; +} + +.is-unparticipationed.is-capacity-over .event-main-actions__header { + border-bottom: 1px solid var(--warning); + color: #6f5819; +} + +.is-non-participationed .event-main-actions__header { + border-bottom: 1px solid var(--danger); + color: var(--danger); +} + +.event-main-actions__title { + font-size: .875rem; + line-height: 1.4; + font-weight: 700; + text-align: center; +} + +.event-main-actions__body { + padding-block: .75rem; +} + +@media (min-width: 48em) { + .event-main-actions__body { + padding-inline: 1rem; + } +} + +@media (max-width: 47.9375em) { + .event-main-actions__body { + padding-inline: .75rem; + } +} + .event-main-actions__description { font-size: .875rem; line-height: 1.4; diff --git a/app/assets/stylesheets/application/blocks/page/_page-body-header.css b/app/assets/stylesheets/application/blocks/page/_page-body-header.css new file mode 100644 index 00000000000..e7f2b870d23 --- /dev/null +++ b/app/assets/stylesheets/application/blocks/page/_page-body-header.css @@ -0,0 +1,13 @@ +.page-body-header__inner { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; +} + +.page-body-header__title { + font-size: 1rem; + line-height: 1.4; + font-weight: 700; + color: var(--main); +} diff --git a/app/assets/stylesheets/application/blocks/pair-work/_pair-work-info.css b/app/assets/stylesheets/application/blocks/pair-work/_pair-work-info.css new file mode 100644 index 00000000000..1b02a65406e --- /dev/null +++ b/app/assets/stylesheets/application/blocks/pair-work/_pair-work-info.css @@ -0,0 +1,48 @@ +.pair-work-info { + display: flex; + gap: 0.75rem; +} + +.pair-work-info__user-icon { + width: 3.5rem; + height: 3.5rem; +} + +.pair-work-info__end { + flex: 1; +} + +.pair-badge { + font-family: serif; + border: double 0.1875rem var(--stamp-color); + border-radius: 0.75rem; + width: 4rem; + height: 3.5rem; + padding: 0.125rem; + position: absolute; + z-index: 1; + transform: rotate(25deg); + font-size: 1.125rem; + line-height: 1.1; + display: flex; + align-items: center; + justify-content: center; + font-weight: 800; + color: var(--stamp-color); + flex-direction: column; + pointer-events: none; +} + +@media (min-width: 48em) { + .pair-badge { + right: 0; + top: -0.125rem; + } +} + +@media (max-width: 47.9375em) { + .pair-badge { + left: 80%; + top: 0; + } +} diff --git a/app/assets/stylesheets/application/blocks/pair-work/_pair-work-schedule-dates.css b/app/assets/stylesheets/application/blocks/pair-work/_pair-work-schedule-dates.css new file mode 100644 index 00000000000..b2b770778de --- /dev/null +++ b/app/assets/stylesheets/application/blocks/pair-work/_pair-work-schedule-dates.css @@ -0,0 +1,44 @@ +.pair-work-schedule-dates.is-solved .pair-work-schedule-dates__table { + display: none; +} + +.pair-work-schedule-dates.is-solved input:checked + .pair-work-schedule-dates__table { + display: block; +} + +.pair-work-schedule-dates__action-items { + display: flex; + justify-content: center; +} + +.pair-work-schedule-dates__action-item { + min-width: 20rem; +} + +.pair-work-schedule-dates__action-item-description { + margin-top: 0.25rem; + font-size: 0.75rem; + line-height: 1.4; + text-align: center; +} + +.pair-work-schedule-dates__title { + font-size: 1rem; + line-height: 1.4; + font-weight: 600; + margin-bottom: 1rem; +} + +.pair-work-schedule-dates__action + .pair-work-schedule-dates__table-container .pair-work-schedule-dates__table { + margin-top: 1rem; +} + +.pair-work-schedule-dates__cancel { + display: flex; + justify-content: center; + margin-bottom: 1rem; +} + +.pair-work-schedule-dates__cancel-action { + min-width: 20rem; +} diff --git a/app/assets/stylesheets/atoms/_a-card.css b/app/assets/stylesheets/atoms/_a-card.css index 5bb548f1b74..b4bc776b2d0 100644 --- a/app/assets/stylesheets/atoms/_a-card.css +++ b/app/assets/stylesheets/atoms/_a-card.css @@ -65,15 +65,6 @@ overflow-y: auto; } -.a-card:has(.a-table) { - border: none; -} - -.a-card:has(.a-table) .card-header { - border: solid 1px var(--border); - border-bottom: none; -} - .a-card.is-danger { border: solid 0.25rem var(--danger); } diff --git a/app/assets/stylesheets/atoms/_a-form-help.css b/app/assets/stylesheets/atoms/_a-form-help.css index 6ce6d52acfd..1b114490cfe 100644 --- a/app/assets/stylesheets/atoms/_a-form-help.css +++ b/app/assets/stylesheets/atoms/_a-form-help.css @@ -6,7 +6,7 @@ margin-top: 0.5rem; } label + .a-form-help:not(:first-child) { - margin-top: 0; + margin-top: -0.5rem; } .a-form-help + input, .a-form-help + textarea { margin-top: 0.5rem; diff --git a/app/assets/stylesheets/config/variables/_css-variables.css b/app/assets/stylesheets/config/variables/_css-variables.css index b651655649c..562d37a9e59 100644 --- a/app/assets/stylesheets/config/variables/_css-variables.css +++ b/app/assets/stylesheets/config/variables/_css-variables.css @@ -81,7 +81,7 @@ --input-focus-shadow: rgba(87, 82, 232, 0.4) 0 0 1px 2px; --header-height__md-up: 3.125rem; --header-height__sm-down: 2.75rem; - --global-nav-width: 5.5rem; + --global-nav-width: 5.75rem; --global-nav-width-sm: 13rem; --thread-header-author: 4.75rem; --side-nav-width: 17rem; diff --git a/app/assets/stylesheets/shared/blocks/_global-nav.css b/app/assets/stylesheets/shared/blocks/_global-nav.css index 52cbfff4d68..00b596bab11 100644 --- a/app/assets/stylesheets/shared/blocks/_global-nav.css +++ b/app/assets/stylesheets/shared/blocks/_global-nav.css @@ -51,8 +51,8 @@ input:checked + .global-nav .global-nav__background.a-overlay { @media (min-width: 48em) { .global-nav-links__link { flex-direction: column; - height: 4.125rem; - gap: 0.25rem; + height: 4.5rem; + gap: 0.375rem; padding-inline: 0; justify-content: center; } @@ -73,19 +73,15 @@ input:checked + .global-nav .global-nav__background.a-overlay { } @media (min-width: 48em) { .global-nav-links__link-icon { - font-size: 1.125rem; + font-size: 1.25rem; margin-right: 0; - margin-bottom: 0.25rem; + display: flex; + align-items: center; } } -.global-nav-links__link-icon .fa-rocket { - font-size: 1.15em; -} .global-nav-links__link-label { - font-size: 0.625rem; line-height: 1.2; - text-align: center; } @media (max-width: 47.9375em) { .global-nav-links__link-label { @@ -93,9 +89,10 @@ input:checked + .global-nav .global-nav__background.a-overlay { } } @media (min-width: 48em) { - .global-nav-links__link-label.is-sm { - transform: scale(0.875); - margin-top: -0.125rem; + .global-nav-links__link-label { + font-size: 0.625rem; + line-height: 1.4; + text-align: center; } } diff --git a/app/components/page_tabs_component.html.slim b/app/components/page_tabs_component.html.slim index 7f526a5291e..c7098a5833d 100644 --- a/app/components/page_tabs_component.html.slim +++ b/app/components/page_tabs_component.html.slim @@ -1,4 +1,4 @@ -.page-tabs +nav.page-tabs .container ul.page-tabs__items - tabs.each do |tab| diff --git a/app/components/sub_tab_component.html.slim b/app/components/sub_tab_component.html.slim index 07f8001c404..6276094d2f8 100644 --- a/app/components/sub_tab_component.html.slim +++ b/app/components/sub_tab_component.html.slim @@ -1,3 +1,9 @@ li.tab-nav__item = link_to link, class: "tab-nav__item-link #{active ? 'is-active' : ''}" do = name + - if count.present? + = " (#{count})" + - if badge&.positive? + .page-tabs__item-count.a-notification-count + .not-solved-count + = badge diff --git a/app/components/sub_tab_component.rb b/app/components/sub_tab_component.rb index b44a4a2cda8..105aa25e14d 100644 --- a/app/components/sub_tab_component.rb +++ b/app/components/sub_tab_component.rb @@ -1,13 +1,15 @@ # frozen_string_literal: true class SubTabComponent < ViewComponent::Base - def initialize(name:, link:, active: false) + def initialize(name:, link:, active: false, count: nil, badge: nil) @name = name @link = link @active = active + @count = count + @badge = badge end private - attr_reader :name, :link, :active + attr_reader :name, :link, :active, :count, :badge end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index a506ca6b1ad..81514916e95 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -7,6 +7,7 @@ def index if current_user display_dashboard display_events_on_dashboard + display_pair_works_on_dashboard display_welcome_message_for_adviser set_required_fields display_products_for_mentor @@ -57,6 +58,10 @@ def display_events_on_dashboard @upcoming_events_groups = UpcomingEvent.upcoming_events_groups end + def display_pair_works_on_dashboard + @upcoming_pair_works = PairWork.upcoming_pair_works(current_user) + end + def display_welcome_message_for_adviser @welcome_message_first_time = cookies[:confirmed_welcome_message] end diff --git a/app/controllers/pair_works/reservations_controller.rb b/app/controllers/pair_works/reservations_controller.rb new file mode 100644 index 00000000000..127a65164a9 --- /dev/null +++ b/app/controllers/pair_works/reservations_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class PairWorks::ReservationsController < ApplicationController + before_action :require_mentor_login, only: %i[create] + + def create + @pair_work = PairWork.find(params[:pair_work_id]) + if @pair_work.reserve(pair_work_reservation_params) + ActiveSupport::Notifications.instrument('pair_work.reserve', pair_work: @pair_work) + redirect_to Redirection.determin_url(self, @pair_work), notice: @pair_work.generate_notice_message(:reserve) + else + @comments = @pair_work.comments.order(:created_at) + render 'pair_works/show' + end + end + + def destroy; end + + private + + def pair_work_reservation_params + params.require(:pair_work).permit(:reserved_at).merge(buddy_id: current_user.id) + end +end diff --git a/app/controllers/pair_works_controller.rb b/app/controllers/pair_works_controller.rb new file mode 100644 index 00000000000..8b40fbd4e6f --- /dev/null +++ b/app/controllers/pair_works_controller.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +class PairWorksController < ApplicationController + before_action :set_my_pair_work, only: %i[edit update destroy] + + PAGER_NUMBER = 10 + + def index + @pair_works = PairWork.by_target(params[:target]) + .with_avatar + .includes(:practice, :comments, :user) + .order(:published_at) + .page(params[:page]) + .per(PAGER_NUMBER) + @pair_works_property = PairWork.generate_pair_works_property(params[:target]) + end + + def show + @pair_work = PairWork.find(params[:id]) + @comments = @pair_work.comments.order(:created_at) + end + + def edit; end + + def new + @pair_work = PairWork.new(channel: 'ペアワーク・モブワーク1') + end + + def create + @pair_work = PairWork.new(pair_work_params) + @pair_work.user = current_user + set_wip + if @pair_work.save + ActiveSupport::Notifications.instrument('pair_work.create', pair_work: @pair_work) + redirect_to Redirection.determin_url(self, @pair_work), notice: @pair_work.generate_notice_message(:create) + else + render :new + end + end + + def update + set_wip + if @pair_work.update(pair_work_params) + ActiveSupport::Notifications.instrument('pair_work.update', pair_work: @pair_work) + redirect_to Redirection.determin_url(self, @pair_work), notice: @pair_work.generate_notice_message(:update) + else + render :edit + end + end + + def destroy + @pair_work.destroy + redirect_to pair_works_url, notice: @pair_work.generate_notice_message(:destroy) + end + + private + + def pair_work_params + params.require(:pair_work).permit(:practice_id, :title, :description, :reserved_at, :buddy_id, :channel, + schedules_attributes: %i[id proposed_at _destroy]) + end + + def set_my_pair_work + @pair_work = current_user.admin? ? PairWork.find(params[:id]) : current_user.pair_works.find(params[:id]) + end + + def set_wip + @pair_work.wip = params[:commit] == 'WIP' + end +end diff --git a/app/decorators/pair_work_decorator.rb b/app/decorators/pair_work_decorator.rb new file mode 100644 index 00000000000..a1ea8eef288 --- /dev/null +++ b/app/decorators/pair_work_decorator.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module PairWorkDecorator + def important? + comments.blank? && !solved? + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 768588c202d..88513a6c301 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -15,4 +15,8 @@ def smart_search_available? def movie_available? Rails.env.local? || Switchlet.enabled?(:movie) end + + def pair_work_available? + Rails.env.local? || Switchlet.enabled?(:pair_work) + end end diff --git a/app/helpers/page_tabs/questions_and_pair_works_helper.rb b/app/helpers/page_tabs/questions_and_pair_works_helper.rb new file mode 100644 index 00000000000..eb75d17b371 --- /dev/null +++ b/app/helpers/page_tabs/questions_and_pair_works_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module PageTabs + module QuestionsAndPairWorksHelper + def questions_and_pair_works_page_tabs + tabs = [] + tabs << { name: 'Q&A', link: questions_path(target: 'not_solved'), badge: Question.unsolved_badge(current_user:, practice_id: params[:practice_id]) } + tabs << { name: 'ペアワーク', link: pair_works_path(target: 'not_solved'), badge: PairWork.unsolved_badge(current_user:) } + render PageTabsComponent.new(tabs:, active_tab: question_and_pair_work_active_tab) + end + + private + + def question_and_pair_work_active_tab + case request.path + when %r{\A/questions} + 'Q&A' + when %r{\A/pair_works} + 'ペアワーク' + end + end + end +end diff --git a/app/helpers/pair_works_helper.rb b/app/helpers/pair_works_helper.rb new file mode 100644 index 00000000000..783b07fab4e --- /dev/null +++ b/app/helpers/pair_works_helper.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module PairWorksHelper + def schedule_dates(date) + (0..6).map { |i| date.to_date + i.days } + end + + def sorted_wdays(date) + max_wday = 6 + sorted_wdays = [date.wday] + max_wday.times do + next sorted_wdays << 0 if sorted_wdays.last == max_wday + + sorted_wdays << sorted_wdays.last + 1 + end + sorted_wdays + end + + def schedule_check_disabled?(target_time, pair_work: nil) + expired = target_time < Time.current + if pair_work + expired || pair_work.user_id == current_user.id + else + expired + end + end + + def learning_time_frame_checked?(target_time, id) + !schedule_check_disabled?(target_time) && current_user.learning_time_frame_ids.include?(id) + end + + def schedule_target_time(day_count, hour_count, pair_work: nil) + if pair_work + pair_work.created_at.beginning_of_day + day_count.days + hour_count.hours + else + Time.current.beginning_of_day + day_count.days + hour_count.hours + end + end + + def schedule_check_box_id(target_time) + "schedule_ids_#{target_time.strftime('%Y%m%d%H%M')}" + end + + def meta_label_by_status(upcoming_pair_work) + today = upcoming_pair_work.reserved_at.to_date == Time.current.to_date + today ? 'a-meta__label is-important' : 'a-meta__label' + end +end diff --git a/app/helpers/sub_tabs/pair_works_helper.rb b/app/helpers/sub_tabs/pair_works_helper.rb new file mode 100644 index 00000000000..5f4ff2f98cd --- /dev/null +++ b/app/helpers/sub_tabs/pair_works_helper.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module SubTabs + module PairWorksHelper + def pair_works_sub_tabs + tabs = [] + tabs << { name: 'ペア募集中', link: pair_works_path(target: 'not_solved'), badge: PairWork.unsolved_badge(current_user:) } + tabs << { name: 'ペア確定', link: pair_works_path(target: 'solved') } + tabs << { name: '全て', link: pair_works_path } + render SubTabsComponent.new(tabs:, active_tab: pair_work_active_tab) + end + + private + + def pair_work_active_tab + case params[:target] + when 'not_solved' + 'ペア募集中' + when 'solved' + 'ペア確定' + else + '全て' + end + end + end +end diff --git a/app/helpers/page_tabs/questions_helper.rb b/app/helpers/sub_tabs/questions_helper.rb similarity index 84% rename from app/helpers/page_tabs/questions_helper.rb rename to app/helpers/sub_tabs/questions_helper.rb index d7f4f4c7340..47c2a6888c1 100644 --- a/app/helpers/page_tabs/questions_helper.rb +++ b/app/helpers/sub_tabs/questions_helper.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true -module PageTabs +module SubTabs module QuestionsHelper - def questions_page_tabs + def questions_sub_tabs unsolved_badge = Question.unsolved_badge(current_user:, practice_id: params[:practice_id]) tabs = [] tabs << { name: '未解決', link: questions_path(target: 'not_solved'), badge: unsolved_badge } tabs << { name: '解決済み', link: questions_path(target: 'solved') } tabs << { name: '全て', link: questions_path } - render PageTabsComponent.new(tabs:, active_tab: question_active_tab) + render SubTabsComponent.new(tabs:, active_tab: question_active_tab) end private diff --git a/app/mailers/activity_mailer.rb b/app/mailers/activity_mailer.rb index eac9d294aaf..4db68afc575 100644 --- a/app/mailers/activity_mailer.rb +++ b/app/mailers/activity_mailer.rb @@ -19,6 +19,7 @@ class ActivityMailer < ApplicationMailer # rubocop:todo Metrics/ClassLength @product = params[:product] if params&.key?(:product) @report = params[:report] if params&.key?(:report) @regular_event = params[:regular_event] if params&.key?(:regular_event) + @pair_work = params[:pair_work] if params&.key?(:pair_work) @message = params[:message] if params&.key?(:message) end @@ -468,4 +469,43 @@ def added_work(args = {}) message.perform_deliveries = @user.mail_notification? && !@user.retired? message end + + def came_pair_work(args = {}) + @receiver ||= args[:receiver] + @pair_work ||= args[:pair_work] + + @user = @receiver + @title = @pair_work.practice.present? ? "「#{@pair_work.practice.title}」についてのペアワーク依頼がありました。" : 'ペアワーク依頼がありました。' + @link_url = notification_redirector_url( + link: "/pair_works/#{@pair_work.id}", + kind: Notification.kinds[:came_pair_work] + ) + + subject = "[FBC] #{@pair_work.user.login_name}さんからペアワーク依頼「#{@pair_work.title}」が投稿されました。" + message = mail(to: @user.email, subject:) + + message.perform_deliveries = @user.mail_notification? && !@user.retired? + message + end + + def matching_pair_work(args = {}) + @receiver ||= args[:receiver] + @pair_work ||= args[:pair_work] + + @user = @receiver + @title = @pair_work.practice.present? ? "「#{@pair_work.practice.title}」についてのペアワークのペアが見つかりました。" : 'ペアワークのペアが見つかりました。' + matched_user = @pair_work.buddy + @user_name = @receiver == matched_user ? 'あなた' : "#{matched_user.login_name}さん" + + @link_url = notification_redirector_url( + link: "/pair_works/#{@pair_work.id}", + kind: Notification.kinds[:matching_pair_work] + ) + + subject = "[FBC] #{@pair_work.user.login_name}さんのペアワーク【 #{@pair_work.title} 】のペアが#{@user_name}に決定しました。" + message = mail(to: @user.email, subject:) + + message.perform_deliveries = @user.mail_notification? && !@user.retired? + message + end end diff --git a/app/models/cache.rb b/app/models/cache.rb index 1f3b96702e1..c32e39f03ef 100644 --- a/app/models/cache.rb +++ b/app/models/cache.rb @@ -63,6 +63,17 @@ def delete_not_solved_question_count Rails.cache.delete 'not_solved_question_count' end + def not_solved_pair_work_count + Rails.cache.fetch 'not_solved_pair_work_count' do + Rails.logger.info '[CACHE MISS] Executing DB query for not_solved_pair_work_count' + PairWork.not_solved.not_wip.count + end + end + + def delete_not_solved_pair_work_count + Rails.cache.delete 'not_solved_pair_work_count' + end + def mentioned_notification_count(user) Rails.cache.fetch "#{user.id}-mentioned_notification_count" do user.notifications.by_target(:mention).latest_of_each_link.size diff --git a/app/models/concerns/mentioner.rb b/app/models/concerns/mentioner.rb index 490703d2ead..a79430acc62 100644 --- a/app/models/concerns/mentioner.rb +++ b/app/models/concerns/mentioner.rb @@ -26,6 +26,9 @@ def where_mention "#{user.login_name}さんのQ&A「#{practice_title}」" when MicroReport "#{user.login_name}さんの分報" + when PairWork + practice_title = practice ? practice[:title] : 'プラクティス選択なし' + "#{user.login_name}さんのペアワーク「#{practice_title}」" end end @@ -77,7 +80,8 @@ def target_of_comment(commentable_class, commentable) Event: "特別イベント「#{commentable.title}」", RegularEvent: "定期イベント「#{commentable.title}」", Page: "Docs「#{commentable.title}」", - Announcement: "お知らせ「#{commentable.title}」" + Announcement: "お知らせ「#{commentable.title}」", + PairWork: "ペアワーク「#{commentable.title}」" }[:"#{commentable_class}"] end end diff --git a/app/models/concerns/watchable.rb b/app/models/concerns/watchable.rb index b7c8e44de94..e75443d6d78 100644 --- a/app/models/concerns/watchable.rb +++ b/app/models/concerns/watchable.rb @@ -33,12 +33,14 @@ def notification_title "Docs「#{self[:title]}」" when Announcement "お知らせ「#{self[:title]}」" + when PairWork + "ペアワーク「#{self[:title]}」" end end def body case self - when Question, Event, RegularEvent, Report, Announcement + when Question, Event, RegularEvent, Report, Announcement, PairWork self[:description] else self[:body] diff --git a/app/models/notification.rb b/app/models/notification.rb index d9e933e4a39..cffd62b2e0d 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -43,7 +43,9 @@ class Notification < ApplicationRecord create_article: 24, added_work: 25, came_inquiry: 26, - training_completed: 27 + training_completed: 27, + came_pair_work: 28, + matching_pair_work: 29 } scope :unreads, -> { where(read: false) } diff --git a/app/models/pair_work.rb b/app/models/pair_work.rb new file mode 100644 index 00000000000..15698facc06 --- /dev/null +++ b/app/models/pair_work.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +class PairWork < ApplicationRecord + include Searchable + include Commentable + include Reactionable + include Watchable + include WithAvatar + include Mentioner + + PairWorksProperty = Struct.new(:title, :empty_message) + + has_many :schedules, class_name: 'PairWorkSchedule', dependent: :destroy, inverse_of: :pair_work + belongs_to :user + belongs_to :practice, optional: true + belongs_to :buddy, class_name: 'User', optional: true + accepts_nested_attributes_for :schedules, allow_destroy: true + alias sender user + + after_save PairWorkCallbacks.new + before_destroy PairWorkCallbacks.new, prepend: true + after_destroy PairWorkCallbacks.new + + validates :title, presence: true, length: { maximum: 256 } + validates :description, presence: true + validates :schedules, presence: true + before_validation :set_published_at, if: :will_be_published? + validates :reserved_at, presence: { message: 'が選択されていません' }, on: :reserve + validates :buddy, presence: { message: 'が選択されていません' }, on: :reserve + validate :reserved_at_in_schedules, on: :reserve + + scope :solved, -> { where.not(reserved_at: nil) } + scope :not_solved, -> { where(reserved_at: nil) } + scope :wip, -> { where(wip: true) } + scope :not_wip, -> { where(wip: false) } + scope :not_held, -> { not_solved.or(where('reserved_at > ?', Time.current)) } + scope :by_target, lambda { |target| + case target + when 'solved' + solved + when 'not_solved' + not_solved.not_wip + else + all + end + } + scope :upcoming_pair_works, lambda { |user| + now = Time.current + within_day = now...(now + 3.days) + PairWork.where(user_id: user.id).or(PairWork.where(buddy_id: user.id)) + .solved + .where(reserved_at: within_day) + } + + mentionable_as :description + + def self.ransackable_attributes(_auth_object = nil) + %w[title description wip reserved_at published_at created_at updated_at buddy_id user_id practice_id] + end + + def self.ransackable_associations(_auth_object = nil) + %w[user buddy practice schedules comments reactions watches bookmarks] + end + + def self.generate_pair_works_property(target) + case target + when 'solved' + PairWorksProperty.new('ペア確定済みのペアワーク', 'ペア確定済みのペアワークはありません。') + when 'not_solved' + PairWorksProperty.new('募集中のペアワーク', '募集中のペアワークはありません。') + else + PairWorksProperty.new('全てのペアワーク', 'ペアワークはありません。') + end + end + + def self.unsolved_badge(current_user:) + return nil if !current_user.admin_or_mentor? + + ::Cache.not_solved_pair_work_count + end + + def generate_notice_message(action_name) + return 'ペアワークをWIPとして保存しました。' if wip? + + { + create: 'ペアワークを作成しました。', + update: 'ペアワークを更新しました。', + destroy: 'ペアワークを削除しました。', + reserve: 'ペアが確定しました。' + }[action_name] + end + + def solved? + !reserved_at.nil? + end + + def reserve(params) + assign_attributes(params) + save(context: :reserve) + end + + private + + def will_be_published? + !wip && published_at.nil? + end + + def set_published_at + self.published_at = Time.current + end + + def reserved_at_in_schedules + errors.add(:reserved_at, 'は提案されたスケジュールに含まれていません') unless schedules.map(&:proposed_at).include?(reserved_at) + end +end diff --git a/app/models/pair_work_callbacks.rb b/app/models/pair_work_callbacks.rb new file mode 100644 index 00000000000..65d0683eaaf --- /dev/null +++ b/app/models/pair_work_callbacks.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class PairWorkCallbacks + def after_save(pair_work) + if pair_work.saved_change_to_attribute?(:published_at, from: nil) + Rails.logger.info '[CACHE CLEARED#after_save] Cache destroyed for unsolved pair work count.' + Cache.delete_not_solved_pair_work_count + elsif pair_work.saved_change_to_attribute?(:reserved_at) || pair_work.saved_change_to_attribute?(:wip) + Rails.logger.info '[CACHE CLEARED#after_save] Cache destroyed for unsolved pair work count.' + Cache.delete_not_solved_pair_work_count + end + end + + def before_destroy(pair_work) + return if pair_work.wip? || pair_work.solved? + + Cache.delete_not_solved_pair_work_count + Rails.logger.info '[CACHE CLEARED#before_destroy] Cache destroyed for unsolved pair work count.' + end + + def after_destroy(pair_work) + delete_notification(pair_work) + end + + private + + def delete_notification(pair_work) + Notification.where(link: "/pair_works/#{pair_work.id}").destroy_all + end +end diff --git a/app/models/pair_work_matching_notifier.rb b/app/models/pair_work_matching_notifier.rb new file mode 100644 index 00000000000..597e22908e1 --- /dev/null +++ b/app/models/pair_work_matching_notifier.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class PairWorkMatchingNotifier + def call(_name, _started, _finished, _unique_id, payload) + pair_work = payload[:pair_work] + return if pair_work.wip? + return if !pair_work.saved_change_to_attribute?(:reserved_at, from: nil, to: pair_work.reserved_at) + + notify_watchers(pair_work) + notify_to_chat(pair_work) + end + + private + + def notify_watchers(pair_work) + receivers = User.where(id: pair_work.watches.select(:user_id)) + receivers.each do |receiver| + ActivityDelivery.with(pair_work:, receiver:).notify(:matching_pair_work) + end + end + + def notify_to_chat(pair_work) + ChatNotifier.message(<<~TEXT) + ペアワーク:「#{pair_work.title}」のマッチングペアが決定しました。 + <#{Rails.application.routes.url_helpers.pair_work_url(pair_work)}> + TEXT + end +end diff --git a/app/models/pair_work_notifier.rb b/app/models/pair_work_notifier.rb new file mode 100644 index 00000000000..2e17136af78 --- /dev/null +++ b/app/models/pair_work_notifier.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class PairWorkNotifier + def call(_name, _started, _finished, _unique_id, payload) + pair_work = payload[:pair_work] + return if pair_work.wip? + return unless pair_work.saved_change_to_attribute?(:published_at, from: nil) + + notify_mentors(pair_work) + notify_to_chat(pair_work) + end + + private + + def notify_mentors(pair_work) + User.mentor.each do |user| + ActivityDelivery.with(receiver: user, pair_work:).notify(:came_pair_work) if pair_work.user != user + end + end + + def notify_to_chat(pair_work) + ChatNotifier.message(<<~TEXT) + ペアワーク:「#{pair_work.title}」を#{pair_work.user.login_name}さんが作成しました。 + <#{Rails.application.routes.url_helpers.pair_work_url(pair_work)}> + TEXT + end +end diff --git a/app/models/pair_work_schedule.rb b/app/models/pair_work_schedule.rb new file mode 100644 index 00000000000..cabc1f36cf5 --- /dev/null +++ b/app/models/pair_work_schedule.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class PairWorkSchedule < ApplicationRecord + belongs_to :pair_work + validates :proposed_at, presence: true + validates :proposed_at, uniqueness: { scope: :pair_work_id } +end diff --git a/app/models/searcher/configuration.rb b/app/models/searcher/configuration.rb index 1a3876c3a7c..a85378499ca 100644 --- a/app/models/searcher/configuration.rb +++ b/app/models/searcher/configuration.rb @@ -74,6 +74,12 @@ module Configuration columns: %i[title description], includes: [:user], label: '定期イベント' + }, + pair_work: { + model: PairWork, + columns: %i[title description], + includes: [:user], + label: 'ペアワーク' } }.freeze diff --git a/app/models/unfinished_data_destroyer.rb b/app/models/unfinished_data_destroyer.rb index 38ae6d0240d..661aa74eeb8 100644 --- a/app/models/unfinished_data_destroyer.rb +++ b/app/models/unfinished_data_destroyer.rb @@ -5,6 +5,7 @@ def call(_name, _started, _finished, _id, payload) user = payload[:user] Product.where(user:).unchecked.destroy_all Report.where(user:).wip.destroy_all + PairWork.where(user:).not_held.destroy_all user.update(career_path: 0) end end diff --git a/app/models/user.rb b/app/models/user.rb index 6e10402b6f8..5abb27936d8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -132,6 +132,7 @@ class User < ApplicationRecord # rubocop:todo Metrics/ClassLength has_many :micro_reports, dependent: :destroy has_many :authored_micro_reports, class_name: 'MicroReport', foreign_key: 'comment_user_id', dependent: :destroy, inverse_of: :comment_user has_many :learning_time_frames_users, dependent: :destroy + has_many :pair_works, dependent: :destroy has_many :participate_events, through: :participations, diff --git a/app/models/watch_for_pair_work_creator.rb b/app/models/watch_for_pair_work_creator.rb new file mode 100644 index 00000000000..327ef13983b --- /dev/null +++ b/app/models/watch_for_pair_work_creator.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class WatchForPairWorkCreator + def call(_name, _started, _finished, _unique_id, payload) + pair_work = payload[:pair_work] + return if pair_work.wip? + return unless pair_work.saved_change_to_attribute?(:published_at, from: nil) + + watch_pair_work_records = watch_records(pair_work) + Watch.insert_all(watch_pair_work_records) # rubocop:disable Rails/SkipsModelValidations + end + + def watch_records(pair_work) + mentors = User.mentor.to_a + watching_users = mentors << pair_work.user + watching_users.uniq.map do |user| + { + watchable_type: 'PairWork', + watchable_id: pair_work.id, + created_at: Time.current, + updated_at: Time.current, + user_id: user.id + } + end + end +end diff --git a/app/notifiers/activity_notifier.rb b/app/notifiers/activity_notifier.rb index 121ea8c665c..9fe7ceb5e37 100644 --- a/app/notifiers/activity_notifier.rb +++ b/app/notifiers/activity_notifier.rb @@ -354,6 +354,39 @@ def chose_correct_answer(params = {}) ) end + def came_pair_work(params = {}) + params.merge!(@params) + pair_work = params[:pair_work] + receiver = params[:receiver] + + notification( + body: "#{pair_work.user.login_name}さんからペアワーク依頼「#{pair_work.title}」が投稿されました。", + kind: :came_pair_work, + receiver:, + sender: pair_work.user, + link: Rails.application.routes.url_helpers.polymorphic_path(pair_work), + read: false + ) + end + + def matching_pair_work(params = {}) + params.merge!(@params) + pair_work = params[:pair_work] + sender = pair_work.user + receiver = params[:receiver] + matched_user = pair_work.buddy + user_name = receiver == matched_user ? 'あなた' : "#{matched_user.login_name}さん" + + notification( + body: "#{sender.login_name}さんのペアワーク【 #{pair_work.title} 】のペアが#{user_name}に決定しました。", + kind: :matching_pair_work, + receiver:, + sender:, + link: Rails.application.routes.url_helpers.polymorphic_path(pair_work), + read: false + ) + end + def moved_up_event_waiting_user(params = {}) params.merge!(@params) event = params[:event] diff --git a/app/views/activity_mailer/came_pair_work.html.slim b/app/views/activity_mailer/came_pair_work.html.slim new file mode 100644 index 00000000000..81fc64f7add --- /dev/null +++ b/app/views/activity_mailer/came_pair_work.html.slim @@ -0,0 +1,9 @@ += render '/notification_mailer/notification_mailer_template', + title: @title, + link_url: @link_url, + link_text: 'ペアワークのページへ' do + p #{@pair_work.user.login_name}さんからペアワーク依頼がありました。確認してみよう!! + div(style='border-top: solid 1px #ccc; height: 0;') + h1(style='margin-top: 1em; border-left: solid 6px #4638a0; padding: 0 0 0 1rem; font-size: 1.5em; border-bottom: none; color: #444444;') + = @pair_work.title + = md2html(@pair_work.description) diff --git a/app/views/activity_mailer/matching_pair_work.html.slim b/app/views/activity_mailer/matching_pair_work.html.slim new file mode 100644 index 00000000000..f3593d1ae1e --- /dev/null +++ b/app/views/activity_mailer/matching_pair_work.html.slim @@ -0,0 +1,9 @@ += render '/notification_mailer/notification_mailer_template', + title: @title, + link_url: @link_url, + link_text: 'ペアワークのページへ' do + p [FBC] #{@pair_work.user.login_name}さんのペアワーク【 #{@pair_work.title} 】のペアが#{@user_name}に決定しました。 + div(style='border-top: solid 1px #ccc; height: 0;') + h1(style='margin-top: 1em; border-left: solid 6px #4638a0; padding: 0 0 0 1rem; font-size: 1.5em; border-bottom: none; color: #444444;') + = @pair_work.title + = md2html(@pair_work.description) diff --git a/app/views/admin/_admin_page_tabs.html.slim b/app/views/admin/_admin_page_tabs.html.slim index fcf0a950dbc..804ad367a20 100644 --- a/app/views/admin/_admin_page_tabs.html.slim +++ b/app/views/admin/_admin_page_tabs.html.slim @@ -1,4 +1,4 @@ -.page-tabs +nav.page-tabs .container ul.page-tabs__items li.page-tabs__item diff --git a/app/views/application/_global_nav.slim b/app/views/application/_global_nav.slim index 6fc70396ad9..a3ea4ed56ed 100644 --- a/app/views/application/_global_nav.slim +++ b/app/views/application/_global_nav.slim @@ -5,20 +5,22 @@ nav.global-nav li.global-nav-links__item = link_to root_path, class: "global-nav-links__link #{current_link(/^home/)}" do .global-nav-links__link-icon - i.fa-solid.fa-gauge + i.fa-solid.fa-home .global-nav-links__link-label.is-sm | ダッシュボード li.global-nav-links__item = link_to announcements_path, class: "global-nav-links__link #{current_link(/^announcements/)}" do .global-nav-links__link-icon i.fa-solid.fa-bullhorn - .global-nav-links__link-label お知らせ + .global-nav-links__link-label + | お知らせ li.global-nav-links__item - anchor = "category-#{@current_user_practice.category_active_or_unstarted_practice.id}" if @current_user_practice.category_active_or_unstarted_practice.present? = link_to course_practices_path(current_user.course, anchor:), class: "global-nav-links__link #{current_link(/^(courses-practices|practices)-/)}" do .global-nav-links__link-icon i.fa-solid.fa-book - .global-nav-links__link-label プラクティス + .global-nav-links__link-label + | プラクティス li.global-nav-links__item = link_to reports_path, class: "global-nav-links__link #{current_link(/^(reports|external_entries)/)}" do .global-nav-links__link-icon @@ -26,7 +28,12 @@ nav.global-nav - if admin_or_mentor_login? && Cache.unchecked_report_count.positive? .global-nav__item-count.a-notification-count.is-only-mentor = Cache.unchecked_report_count - .global-nav-links__link-label 日報・ブログ + .global-nav-links__link-label + | 日報 + br.is-hidden-sm-down + span.is-hidden-md-up + | ・ + | ブログ - if staff_login? li.global-nav-links__item - products_link = admin_or_mentor_login? ? products_unassigned_index_path : products_path @@ -36,15 +43,31 @@ nav.global-nav - if admin_or_mentor_login? && Cache.unassigned_product_count.positive? .global-nav__item-count.a-notification-count.is-only-mentor = Cache.unassigned_product_count - .global-nav-links__link-label 提出物 + .global-nav-links__link-label + | 提出物 li.global-nav-links__item - = link_to questions_path(target: 'not_solved'), class: "global-nav-links__link #{current_link(/^questions/)}" do + = link_to questions_path(target: 'not_solved'), class: "global-nav-links__link #{current_link(/^(questions|pair_works)/)}" do .global-nav-links__link-icon - i.fa-solid.fa-comments-question-check - - if Cache.not_solved_question_count.positive? + i.fa-solid.fa-comment-question + // TODO ペアワークを本番リリースしたらこの分岐は削除 + - if pair_work_available? + - if Cache.not_solved_question_count.positive? || Cache.not_solved_pair_work_count.positive? + .global-nav__item-count.a-notification-count + = Cache.not_solved_question_count + Cache.not_solved_pair_work_count + - elsif Cache.not_solved_question_count.positive? .global-nav__item-count.a-notification-count = Cache.not_solved_question_count - .global-nav-links__link-label Q&A + // TODO ペアワークを本番リリースしたらこの分岐は削除 + - if pair_work_available? + .global-nav-links__link-label + | Q&A + br.is-hidden-sm-down + span.is-hidden-md-up + | ・ + | ペアワーク + - else + .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 .global-nav-links__link-icon @@ -58,18 +81,20 @@ nav.global-nav = link_to portfolios_path, class: "global-nav-links__link #{current_link(/^works/)}" do .global-nav-links__link-icon i.fa-solid.fa-rocket - .global-nav-links__link-label.is-sm + .global-nav-links__link-label | ポートフォリオ li.global-nav-links__item = link_to users_path, class: "global-nav-links__link #{current_link(/^users-(index|show)/)}" do .global-nav-links__link-icon i.fa-solid.fa-users - .global-nav-links__link-label ユーザー + .global-nav-links__link-label + | ユーザー li.global-nav-links__item = link_to events_path, class: "global-nav-links__link #{current_link(/^events|^regular_events/)}" do .global-nav-links__link-icon i.fa-solid.fa-beer - .global-nav-links__link-label イベント + .global-nav-links__link-label + | イベント li.global-nav-links__item = link_to action_uncompleted_index_path(current_user.talk), class: "global-nav-links__link #{current_link(/talk/)}" do .global-nav-links__link-icon diff --git a/app/views/companies/_tabs.html.slim b/app/views/companies/_tabs.html.slim index f218dddefa9..11bc4f53b59 100644 --- a/app/views/companies/_tabs.html.slim +++ b/app/views/companies/_tabs.html.slim @@ -1,4 +1,4 @@ -.page-tabs +nav.page-tabs .container ul.page-tabs__items li.page-tabs__item diff --git a/app/views/events/_participation.html.slim b/app/views/events/_participation.html.slim index 7a49a90dc9a..cd9e83f24d4 100644 --- a/app/views/events/_participation.html.slim +++ b/app/views/events/_participation.html.slim @@ -1,40 +1,39 @@ - if current_user.trainee && event.job_hunting .event-main-actions.is-non-participationed - .event-main-actions__description - p - | 企業研修で参加されている方はこのイベントに参加できません。 - ul.event-main-actions__items - li.event-main-actions__item - .a-button.is-disabled.is-lg.is-block - | 参加申込 + .event-main-actions__body + .event-main-actions__description + p 企業研修で参加されている方はこのイベントに参加できません。 + ul.event-main-actions__items + li.event-main-actions__item + .a-button.is-disabled.is-lg.is-block + | 参加申込 - elsif current_user.participating?(event) .event-main-actions.is-participationed - .event-main-actions__description - p - | 参加登録しています。 - ul.event-main-actions__items - li.event-main-actions__item - = link_to event_participation_path(event_id: event), method: :delete, - data: { confirm: '特別イベントの参加をキャンセルします。よろしいですか?' }, class: 'event-main-actions__item-cancel' do - | 参加を取り消す + .event-main-actions__body + .event-main-actions__description + p 参加登録しています。 + ul.event-main-actions__items + li.event-main-actions__item + = link_to event_participation_path(event_id: event), method: :delete, + data: { confirm: '特別イベントの参加をキャンセルします。よろしいですか?' }, class: 'event-main-actions__item-cancel' do + | 参加を取り消す - elsif event.opening? .event-main-actions.is-unparticipationed(class="#{event.capacity > event.participants.count ? 'is-available' : 'is-capacity-over'}") - .event-main-actions__description - - if event.capacity > event.participants.count - // TODO helprtにしてわかりやすくしたい↑ - p - | あと#{event.capacity - event.participants.count}名が参加できます。 - - else - p - | 参加者が定員を超えているので補欠での登録になります。 - ul.event-main-actions__items - li.event-main-actions__item + .event-main-actions__body + .event-main-actions__description - if event.capacity > event.participants.count - // TODO helprtにしてわかりやすくしたい↑ - = link_to event_participations_path(event_id: event), method: :post, - data: { confirm: '特別イベント参加申込をします。よろしいですか?' }, class: 'a-button is-primary is-md is-block' do - | 参加申込 + // TODO helperにしてわかりやすくしたい↑ + p あと#{event.capacity - event.participants.count}名が参加できます。 - else - = link_to event_participations_path(event_id: event), method: :post, - data: { confirm: '補欠として特別イベント参加申込をします。よろしいですか?' }, class: 'a-button is-warning is-md is-block' do - | 補欠登録 + p 参加者が定員を超えているので補欠での登録になります。 + ul.event-main-actions__items + li.event-main-actions__item + - if event.capacity > event.participants.count + // TODO helperにしてわかりやすくしたい↑ + = link_to event_participations_path(event_id: event), method: :post, + data: { confirm: '特別イベント参加申込をします。よろしいですか?' }, class: 'a-button is-primary is-md is-block' do + | 参加申込 + - else + = link_to event_participations_path(event_id: event), method: :post, + data: { confirm: '補欠として特別イベント参加申込をします。よろしいですか?' }, class: 'a-button is-warning is-md is-block' do + | 補欠登録 diff --git a/app/views/home/_upcoming_pair_work.html.slim b/app/views/home/_upcoming_pair_work.html.slim new file mode 100644 index 00000000000..34f77dc44cc --- /dev/null +++ b/app/views/home/_upcoming_pair_work.html.slim @@ -0,0 +1,21 @@ +.card-list-item + .card-list-item__inner + .card-list-item__label + span ペア
ワーク + .card-list-item__rows + .card-list-item__row + header.card-list-item-title + h2.card-list-item-title__title + = link_to pair_work_path(upcoming_pair_work), class: 'card-list-item-title__link has-badge a-text-link' do + span.a-text-link__text + = upcoming_pair_work.title + .card-list-item__row + .card-list-item-meta + .card-list-item-meta__items + .card-list-item-meta__item + .a-meta + span class=meta_label_by_status(upcoming_pair_work) + | 開催日時 + span class=meta_label_by_status(upcoming_pair_work) + time datetime=upcoming_pair_work.reserved_at.iso8601 + = l upcoming_pair_work.reserved_at diff --git a/app/views/home/_upcoming_pair_works.html.slim b/app/views/home/_upcoming_pair_works.html.slim new file mode 100644 index 00000000000..681482d31ee --- /dev/null +++ b/app/views/home/_upcoming_pair_works.html.slim @@ -0,0 +1,7 @@ +.a-card + header.card-header.is-sm.has-no-border + h2.card-header__title + | ペアワークの予定があります! + hr.a-border-tint + .card-list.has-scroll + = render partial: 'upcoming_pair_work', collection: upcoming_pair_works diff --git a/app/views/home/index.html.slim b/app/views/home/index.html.slim index 0ac8a3c9679..fc163828271 100644 --- a/app/views/home/index.html.slim +++ b/app/views/home/index.html.slim @@ -25,6 +25,8 @@ .container.is-xl .row .col-xs-12.col-xl-6.col-xxl-6 + - if @upcoming_pair_works.present? + = render 'upcoming_pair_works', upcoming_pair_works: @upcoming_pair_works - if @upcoming_events_groups.present? = render 'upcoming_events_groups', upcoming_events_groups: @upcoming_events_groups - if @announcements.present? diff --git a/app/views/mentor/_mentor_page_tabs.html.slim b/app/views/mentor/_mentor_page_tabs.html.slim index 5c0cfb07a6c..10cd5c0a83b 100644 --- a/app/views/mentor/_mentor_page_tabs.html.slim +++ b/app/views/mentor/_mentor_page_tabs.html.slim @@ -1,4 +1,4 @@ -.page-tabs +nav.page-tabs .container ul.page-tabs__items li.page-tabs__item diff --git a/app/views/pages/_tabs.html.slim b/app/views/pages/_tabs.html.slim index f0d3c3ed859..86fbdb727ac 100644 --- a/app/views/pages/_tabs.html.slim +++ b/app/views/pages/_tabs.html.slim @@ -1,4 +1,4 @@ -.page-tabs +nav.page-tabs .container ul.page-tabs__items li.page-tabs__item diff --git a/app/views/pair_works/_body.html.slim b/app/views/pair_works/_body.html.slim new file mode 100644 index 00000000000..7cac768ccde --- /dev/null +++ b/app/views/pair_works/_body.html.slim @@ -0,0 +1,104 @@ +.a-card + .card-body + - if pair_work.solved? + - buddy = pair_work.buddy + .event-main-actions.is-participationed + header.event-main-actions__header + h2.event-main-actions__title + | ペアが確定しました + = link_to events_calendars_url(protocol: :webcal, format: :ics, user_id: current_user.id), class: 'a-button is-sm is-secondary' do + i.fa-regular.fa-plus + span カレンダーに登録 + + .event-main-actions__body + .pair-work-info + .pair-work-info__start + = render 'users/icon', user: buddy, link_class: 'pair-work-info__user-link', image_class: 'pair-work-info__user-icon' + .pair-work-info__end + .event-meta + .event-meta__items + .event-meta__item + dt.event-meta__item-label + | ペア + dd.event-meta__item-value + = link_to buddy.long_name, user_path(buddy) + .event-meta__item + dt.event-meta__item-label + | 日時 + dd.event-meta__item-value + time.pair-work-info__datetime datetime=pair_work.reserved_at.iso8601 + = l pair_work.reserved_at + .event-meta__item + dt.event-meta__item-label + | チャンネル + dd.event-meta__item-value + = pair_work.channel + + .card-body__description + .a-long-text.is-md.js-markdown-view + = pair_work.description + hr.a-border-tint + = render 'reactions/reactions', reactionable: pair_work + + hr.a-border-tint + footer.card-footer class=(current_user == pair_work.user || current_user.admin? ? '' : 'is-hidden') + .card-main-actions + ul.card-main-actions__items + li.card-main-actions__item + = link_to edit_pair_work_path(pair_work), class: 'card-main-actions__action a-button is-sm is-secondary is-block' do + i.fa-solid.fa-pen + | 内容修正 + li.card-main-actions__item.is-sub class=(current_user.admin? || pair_work.user == current_user ? '' : 'is-hidden') + = link_to '削除', pair_work_path(pair_work), + data: { confirm: '本当に削除しますか?' }, method: :delete, class: 'card-main-actions__muted-action' + +.a-card + header.card-header + h2.card-header__title + - if pair_work.user == current_user + | 希望日時 + - else + | ペアを希望する + hr.a-border-tint + .card-body + .card-body__description + .pair-work-schedule-dates class=(pair_work.solved? && pair_work.user != current_user ? 'is-solved' : '') + - if pair_work.solved? + .pair-work-schedule-dates__action + .pair-work-schedule-dates__action-items + - if pair_work.buddy == current_user + .pair-work-schedule-dates__action-item + label.a-button.is-md.is-secondary.is-block(for='show-schedule-dates') + | 日時の変更、ペアをキャンセル + .pair-work-schedule-dates__action-item-description + p あなたがペアとして確定しています。 + - elsif mentor_login? + .pair-work-schedule-dates__action-item + label.a-button.is-md.is-secondary.is-block(for='show-schedule-dates') + | ペア変更、自分がペアになる + .pair-work-schedule-dates__action-item-description + p 代わりに自分がペアになる場合はこちらから変更。 + - if mentor_login? + .pair-work-schedule-dates__table-container + input.a-toggle-checkbox#show-schedule-dates(type="checkbox") + .pair-work-schedule-dates__table + - if pair_work.buddy == current_user + h3.pair-work-schedule-dates__title + | 日時の変更、もしくはキャンセル + .pair-work-schedule-dates__cancel + .pair-work-schedule-dates__cancel-action + // TODO: ペア確定の取り消し機能実装後にリンク先を修正 + .a-button.is-md.is-danger.is-block + | ペア確定を取り消す + - else + h3.pair-work-schedule-dates__title + | ペアを希望する場合、以下のスケジュールから都合のいい日時を選択してください。 + = render 'pair_works/schedule', pair_work: + - elsif pair_work.user == current_user + .pair-work-schedule-dates__table-container + .pair-work-schedule-dates__table + = render 'pair_works/schedule', pair_work: + - else + .a-long-text.is-md + .message.danger + p 現在はテスト運用のため、メンターのみがペアを申し込めます。 diff --git a/app/views/pair_works/_form.html.slim b/app/views/pair_works/_form.html.slim new file mode 100644 index 00000000000..7466616fbff --- /dev/null +++ b/app/views/pair_works/_form.html.slim @@ -0,0 +1,101 @@ += render 'errors', object: pair_work += form_with model: pair_work, local: true, class: 'form', html: { name: 'pair_work' } do |f| + .form__items + .form-item + .form-item + .row + .col-lg-6.col-xs-12 + .form-item + = f.label :practice, class: 'a-form-label' + .select-practices + = f.select :practice_id, + practice_options_within_course, + { include_blank: 'プラクティス選択なし' }, + { class: 'js-choices-singles', id: 'js-choices-practice' } + + .form-item + .row.js-markdown-parent + .col-md-6.col-xs-12 + = f.label :title, class: 'a-form-label' + = f.text_field :title, class: 'a-text-input js-warning-form', placeholder: 'CSS の display: flex の使い方をきっちり理解したい' + .a-form-help + p + | 「タイトル」にはペアワークで解決したい問題を簡潔に明確に入力しましょう。 + br + | 具体的で詳細な内容は「内容」に入力してください。 + .form-item + .row.js-markdown-parent + .col-md-6.col-xs-12 + = f.label :description, class: 'a-form-label' + .form-textarea + .form-textarea__body + = f.text_area :description, class: 'a-text-input markdown-form__text-area js-markdown js-warning-form', + data: { 'preview': '.js-preview', 'input': '.file-input' } + .form-textarea__footer + .form-textarea__bottom-note + | 途中保存は「#{request.os == 'Mac OSX' ? 'command + s' : 'Ctrl + s'}」 マメに保存しよう。 + .col-md-6.col-xs-12 + .a-form-label + | プレビュー + .js-preview.a-long-text.is-md.practices-edit__input.markdown-form__preview + + - if pair_work.new_record? + .form-item + .row + .col-md-6.col-xs-12 + = f.label :schedules, class: 'a-form-label' + .a-form-help(class='!mb-2') + p + | ペアワークを実施する希望日時にチェックを入れてください。 + br + = link_to edit_current_user_path(anchor: 'activity-schedule') do + | ユーザー情報の「主な活動予定時間」 + | を登録しておくと、初期設定でその日時が選択されるようになります。 + .form-table.is-sm.is-sticky(class='!max-h-96') + table(class='!min-w-full') + thead + tr + th + - schedule_dates(Time.current).each do |day| + th = l day, format: :mdw_unique + tbody + - 24.times do |hour| + tr(class='hover:!bg-[var(--background-more-tint)]') + th + | #{hour}:00 + - sorted_wdays(Time.current).each_with_index do |wday, day| + ruby: + target_time = schedule_target_time(day, hour) + check_box_id = schedule_check_box_id(target_time) + learning_time_frame_id = wday * 24 + hour + 1 + td.form-table__check + = label_tag check_box_id do + span + = check_box_tag 'pair_work[schedules_attributes][][proposed_at]', + target_time.iso8601, + learning_time_frame_checked?(target_time, learning_time_frame_id), + id: check_box_id, disabled: schedule_check_disabled?(target_time) + .form-item + .row.js-markdown-parent + .col-md-6.col-xs-12 + = f.label :channel, 'ペアワークを行うチャンネル', class: 'a-form-label' + = f.text_field :channel, class: 'a-text-input js-warning-form' + .a-form-help(class='!mb-2') + p + | 特に理由がない場合は「ペアワーク・モブワーク1」を登録してください。 + br + | もし、同じ時間に「ペアワーク・モブワーク1」が別のペアワークに使われていて、 + | それに気づいた場合は別のチャンネルに変更してください。 + .form-actions + ul.form-actions__items + li.form-actions__item.is-main + = f.submit 'WIP', class: 'a-button is-lg is-secondary is-block', id: 'js-shortcut-wip' + li.form-actions__item.is-main + - if pair_work.new_record? + = f.submit '登録する', class: 'a-button is-lg is-primary is-block' + - elsif pair_work.wip? + = f.submit 'ペアワークを公開', class: 'a-button is-lg is-primary is-block' + - else + = f.submit '更新する', class: 'a-button is-lg is-primary is-block' + li.form-actions__item + = link_to 'キャンセル', pair_works_path(target: 'not_solved'), class: 'a-button is-sm is-secondary' diff --git a/app/views/pair_works/_header.html.slim b/app/views/pair_works/_header.html.slim new file mode 100644 index 00000000000..7e9f2e1c8bc --- /dev/null +++ b/app/views/pair_works/_header.html.slim @@ -0,0 +1,57 @@ +header.page-content-header + .page-content-header__start + .page-content-header__user + = render 'users/icon', user: pair_work.user, link_class: 'page-content-header__user-link', image_class: 'page-content-header__user-icon-image' + - if pair_work.solved? + .pair-badge + .pair-badge__label + | ペア + br + | 確定 + .page-content-header__end + .page-content-header__row + .page-content-header__before-title + - if pair_work.practice_id.present? + = link_to pair_work.practice.title, practice_path(pair_work.practice_id), class: 'a-category-link' + h1.page-content-header__title class=(pair_work.wip ? 'is-wip' : '') + - if pair_work.wip? + span.a-title-label.is-wip + | WIP + - elsif pair_work.solved? + span.a-title-label.is-solved.is-success.js-solved-status + | ペア確定 + - else + span.a-title-label.is-solved.is-danger.js-solved-status + | 募集中 + | #{pair_work.title} + + .page-content-header__row + .page-content-header-metas + .page-content-header-metas__start + - if pair_work.wip? + .page-content-header-metas__meta + .a-meta + span.a-meta__value + | ペアワーク作成中 + .page-content-header-metas__meta + = link_to pair_work.user.long_name, user_path(pair_work.user), class: 'a-user-name' + - if !pair_work.wip? + .page-content-header-metas__meta + .a-meta + span.a-meta__label + | 公開 + span.a-meta__value + time datetime=pair_work.published_at.iso8601 + = l pair_work.published_at + .page-content-header-metas__meta + .a-meta + span.a-meta__label + | 更新 + span.a-meta__value + time datetime=pair_work.updated_at.iso8601 + = l pair_work.updated_at + + .page-content-header__row + .page-content-header-actions + .page-content-header-actions__start + = render 'watches/watch_toggle', type: pair_work.class.to_s, id: pair_work.id, watch: pair_work.watch_by(current_user) diff --git a/app/views/pair_works/_pair_work.html.slim b/app/views/pair_works/_pair_work.html.slim new file mode 100644 index 00000000000..efcb857aa6c --- /dev/null +++ b/app/views/pair_works/_pair_work.html.slim @@ -0,0 +1,50 @@ +.card-list-item(class="#{pair_work.wip ? 'is-wip' : ''}") + .card-list-item__inner + .card-list-item__user + = render 'users/icon', user: pair_work.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 pair_work.wip? + .a-list-item-badge.is-wip + span WIP + h2.card-list-item-title__title(itemprop='name') + = link_to pair_work, itemprop: 'url', class: 'card-list-item-title__link a-text-link js-unconfirmed-link' do + = pair_work.title + - if pair_work.practice + .card-list-item__row + .card-list-item-meta + .card-list-item-meta__items + .card-list-item-meta__item + = link_to pair_work.practice.title, practice_path(pair_work.practice), class: 'a-meta is-practice' + .card-list-item__row + .card-list-item-meta + .card-list-item-meta__items + - if pair_work.wip? + .card-list-item-meta__item + .a-meta + | 作成中 + .card-list-item-meta__item + = link_to pair_work.user, class: 'a-user-name' do + = pair_work.user.long_name + .card-list-item__row + .card-list-item-meta + .card-list-item-meta__items + .card-list-item-meta__item + .a-meta + span.a-meta__label + | 投稿日時 + - if pair_work.wip? + span.a-meta__value + = l pair_work.created_at + - else + span.a-meta__value + = l pair_work.published_at + .card-list-item-meta__item + .a-meta(class="#{pair_work.important? ? 'is-important' : ''}") + | コメント(#{pair_work.comments.length}) + + - if pair_work.solved? + .stamp.is-circle.is-solved + .stamp__content.is-icon 確 + .stamp__content.is-icon 定 diff --git a/app/views/pair_works/_schedule.html.slim b/app/views/pair_works/_schedule.html.slim new file mode 100644 index 00000000000..68b18917ea9 --- /dev/null +++ b/app/views/pair_works/_schedule.html.slim @@ -0,0 +1,43 @@ +- proposed_at_list = pair_work.schedules.map(&:proposed_at) + +.a-table.is-sm(class='!text-center') + table(class='!min-w-full') + thead + tr + th + - schedule_dates(pair_work.created_at).each do |date| + th = l date, format: :mdw_unique + tbody + - 24.times do |hour| + tr(class='hover:!bg-[var(--background-more-tint)]') + th #{hour}:00 + - 7.times do |day| + - target_time = schedule_target_time(day, hour, pair_work:) + - if proposed_at_list.include?(target_time) + td(class='!p-0 !h-6 !relative') + = form_with model: pair_work, url: pair_work_reservations_path(pair_work), method: :post, local: true do |f| + = f.hidden_field :reserved_at, value: target_time + = f.button type: :submit, + disabled: schedule_check_disabled?(target_time, pair_work:), + class: ['!absolute', + '!top-0', + '!flex', + '!w-full', + '!h-full', + '!text-center', + '!justify-center', + '!items-center', + '!p-1', + '!border', + '!border-solid', + '!rounded', + '!text-[var(--main)]', + '!bg-[var(--input-selected-background)]', + '!border-[var(--main)]', + 'enabled:!cursor-pointer'].join(' '), + id: target_time.iso8601, + data: { confirm: "#{l target_time, format: :mdw_and_time} にペアワークを申し込みますか?" } do + i.fa-duotone.fa-solid.fa-check + - else + td(class='!p-0 !h-6') + span ✖️ diff --git a/app/views/pair_works/edit.html.slim b/app/views/pair_works/edit.html.slim new file mode 100644 index 00000000000..071f50b44d8 --- /dev/null +++ b/app/views/pair_works/edit.html.slim @@ -0,0 +1,21 @@ +- title 'ペアワーク編集' +- set_meta_tags description: 'ペアワーク編集ページです。' + += render 'questions_and_pair_works/questions_and_pair_works_header' +.page-main + header.page-main-header + .container + .page-main-header__inner + .page-main-header__start + h2.page-main-header__title + = title + .page-main-header__end + .page-header-actions + ul.page-header-actions__items + li.page-header-actions__item + = link_to pair_works_path(target: 'not_solved'), class: 'a-button is-md is-secondary is-block is-back' do + | ペアワーク一覧 + hr.a-border + .page-body + .container.is-xxl + = render 'form', pair_work: @pair_work diff --git a/app/views/pair_works/index.html.slim b/app/views/pair_works/index.html.slim new file mode 100644 index 00000000000..0aea86f7072 --- /dev/null +++ b/app/views/pair_works/index.html.slim @@ -0,0 +1,41 @@ +- title @pair_works_property.title +- set_meta_tags description: 'ペアワークの一覧です。' + += render 'questions_and_pair_works/questions_and_pair_works_header' +.page-main + header.page-main-header + .container + .page-main-header__inner + .page-main-header__start + h2.page-main-header__title + | ペアワーク + .page-main-header__end + .page-header-actions + ul.page-header-actions__items + li.page-header-actions__item + = link_to new_pair_work_path, class: 'a-button is-md is-secondary is-block' do + i.fa-regular.fa-plus + span ペアを募集する + .a-page-notice + .container + .a-page-notice__inner + p 現在はテスト運用のため、メンターのみがペアを申し込めます。 + + hr.a-border + = pair_works_sub_tabs + + .page-body + - if @pair_works.empty? + .o-empty-message + .o-empty-message__icon + i.fa-regular.fa-sad-tear + .o-empty-message__text + = @pair_works_property.empty_message + - else + .container.is-md + .page-content.pair_works + = paginate @pair_works + .card-list.a-card + .card-list__items + = render partial: 'pair_work', collection: @pair_works + = paginate @pair_works diff --git a/app/views/pair_works/new.html.slim b/app/views/pair_works/new.html.slim new file mode 100644 index 00000000000..68faecf01b5 --- /dev/null +++ b/app/views/pair_works/new.html.slim @@ -0,0 +1,22 @@ +- title 'ペア募集作成' +- set_meta_tags description: 'ペア募集作成ページです。' + += render 'questions_and_pair_works/questions_and_pair_works_header' +.page-main + header.page-main-header + .container + .page-main-header__inner + .page-main-header__start + h2.page-main-header__title + | ペア募集作成 + .page-main-header__end + .page-header-actions + ul.page-header-actions__items + li.page-header-actions__item + = link_to pair_works_path(target: 'not_solved'), class: 'a-button is-md is-secondary is-block is-back' do + | ペアワーク一覧 + hr.a-border + + .page-body + .container.is-xxl + = render 'form', pair_work: @pair_work diff --git a/app/views/pair_works/show.html.slim b/app/views/pair_works/show.html.slim new file mode 100644 index 00000000000..f2b4b9ae945 --- /dev/null +++ b/app/views/pair_works/show.html.slim @@ -0,0 +1,35 @@ +- title "ペアワーク: #{truncate(@pair_work.title, length: 35, omission: '...')}" +- set_meta_tags og: { title: "ペアワーク: #{@pair_work.title}" } +- if @pair_work.practice + - set_meta_tags description: "#{@pair_work.user.long_name}さんが投稿した、プラクティス「#{@pair_work.practice.title}」のペアワーク募集「#{@pair_work.title}」のページです。" +- else + - set_meta_tags description: "#{@pair_work.user.long_name}さんが投稿した、ペアワーク募集「#{@pair_work.title}」のページです。このペアワーク募集に関連するプラクティスはありません。" + += render 'questions_and_pair_works/questions_and_pair_works_header' +.page-main + header.page-main-header + .container + .page-main-header__inner + .page-main-header__start + h2.page-main-header__title + | ペアワーク + .page-main-header__end + .page-header-actions + ul.page-header-actions__items + li.page-header-actions__item + = link_to pair_works_path(target: 'not_solved'), class: 'a-button is-md is-secondary is-block is-back' do + | ペアワーク一覧 + li.page-header-actions__item + = link_to new_pair_work_path, class: 'a-button is-md is-secondary is-block' do + i.fa-regular.fa-plus + span ペアを募集する + hr.a-border + + .page-body + .page-body__inner + .container.is-md + .page-content.pair-work + = render 'errors', object: @pair_work + = render 'pair_works/header', pair_work: @pair_work + = render 'pair_works/body', pair_work: @pair_work + = render 'comments/comments', commentable: @pair_work, commentable_type: 'PairWork' diff --git a/app/views/products/_tabs.html.slim b/app/views/products/_tabs.html.slim index 89414700d14..b9e938b4f56 100644 --- a/app/views/products/_tabs.html.slim +++ b/app/views/products/_tabs.html.slim @@ -1,4 +1,4 @@ -.page-tabs +nav.page-tabs .container ul.page-tabs__items li.page-tabs__item diff --git a/app/views/questions/edit.html.slim b/app/views/questions/edit.html.slim index 47867afd08d..0a259bcf7c8 100644 --- a/app/views/questions/edit.html.slim +++ b/app/views/questions/edit.html.slim @@ -1,19 +1,27 @@ - title '質問編集' - set_meta_tags description: '質問編集ページです。' -header.page-header - .container - .page-header__inner - .page-header__start - h2.page-header__title - = title - .page-header__end - .page-header-actions - ul.page-header-actions__items - li.page-header-actions__item - = link_to questions_path(target: 'not_solved'), class: 'a-button is-md is-secondary is-block is-back' do - | Q&A一覧 += render 'questions_and_pair_works/questions_and_pair_works_header' hr.a-border -.page-body - .container.is-xxl - = render 'form', question: @question +.page-main + header.page-main-header + .container + .page-main-header__inner + .page-main-header__start + h2.page-main-header__title + = title + .page-main-header__end + .page-header-actions + ul.page-header-actions__items + li.page-header-actions__item + = link_to new_question_path, class: 'a-button is-md is-secondary is-block' do + i.fa-regular.fa-plus + span 質問する + li.page-header-actions__item + = link_to questions_path(target: 'not_solved'), class: 'a-button is-md is-secondary is-block is-back' do + | Q&A一覧 + hr.a-border + + .page-body + .container.is-xxl + = render 'form', question: @question diff --git a/app/views/questions/index.html.slim b/app/views/questions/index.html.slim index cd0ebf599c1..9f65a984ebc 100644 --- a/app/views/questions/index.html.slim +++ b/app/views/questions/index.html.slim @@ -4,37 +4,12 @@ - else - set_meta_tags description: 'Q&Aの一覧です。' -header.page-header - .container - .page-header__inner - .page-header__start - h2.page-header__title - | Q&A - .page-header__end - .page-header-actions - .page-header-actions__items - .page-header-actions__item - = link_to new_question_path, class: 'a-button is-md is-secondary is-block' do - i.fa-regular.fa-plus - span - | 質問する - -= questions_page_tabs - -- if @tag.present? - header.page-main-header - .container - .page-main-header__inner - .page-main-header__start - h1.page-main-header__title - | タグ「#{@tag.name}」のQ&A(#{@questions.total_count}) - - if admin_login? - .page-main-header__end - .page-main-header-actions - .page-main-header-actions__items - = react_component('Tags/TagEditButton', tagId: @tag.id, tagName: @tag.name) - hr.a-border += render 'questions_and_pair_works/questions_and_pair_works_header' .page-main + = render 'questions_and_pair_works/questions_and_pair_works_page_main_header' + hr.a-border + = questions_sub_tabs + nav.page-filter.form .container.is-md = form_with url: questions_path, method: 'get', local: true @@ -49,6 +24,20 @@ header.page-header hr.a-border .page-body .page-body__inner.has-side-nav + + - if @tag.present? + header.page-body-header + .container.is-md + .page-body-header__inner + .page-body-header__start + h1.page-body-header__title + | タグ「#{@tag.name}」のQ&A(#{@questions.total_count}) + - if admin_login? + .page-body-header__end + .page-body-header-actions + .page-body-header-actions__items + = react_component('Tags/TagEditButton', tagId: @tag.id, tagName: @tag.name) + .container.is-md .page-body__columns .page-body__column.is-main diff --git a/app/views/questions/new.html.slim b/app/views/questions/new.html.slim index 33a6378dbe4..7f47592526b 100644 --- a/app/views/questions/new.html.slim +++ b/app/views/questions/new.html.slim @@ -1,25 +1,17 @@ - title '質問作成' - set_meta_tags description: '質問作成ページです。' -header.page-header - .container - .page-header__inner - .page-header__start - h2.page-header__title - | 質問する - .page-header__end - .page-header-actions - ul.page-header-actions__items - li.page-header-actions__item - = link_to questions_path(target: 'not_solved'), class: 'a-button is-md is-secondary is-block is-back' do - | Q&A一覧 -hr.a-border -.a-page-notice.page-notice - .container - .a-page-notice__inner - p - = link_to '/pages/how_to_ask_questions', target: '_blank', class: 'page-notice__link', rel: 'noopener noreferrer' do - | このドキュメントを参考にして質問をしてみよう! -.page-body - .container.is-xxl - = render 'form', question: @question += render 'questions_and_pair_works/questions_and_pair_works_header' +.page-main + = render 'questions_and_pair_works/questions_and_pair_works_page_main_header' + hr.a-border + .a-page-notice.page-notice + .container + .a-page-notice__inner + p + = link_to '/pages/how_to_ask_questions', target: '_blank', class: 'page-notice__link', rel: 'noopener noreferrer' do + | このドキュメントを参考にして質問をしてみよう! + + .page-body + .container.is-xxl + = render 'form', question: @question diff --git a/app/views/questions/show.html.slim b/app/views/questions/show.html.slim index b2b0ebd1c14..fa38e83784a 100644 --- a/app/views/questions/show.html.slim +++ b/app/views/questions/show.html.slim @@ -5,61 +5,48 @@ - else - set_meta_tags description: "#{@question.user.long_name}さんが投稿した、質問「#{@question.title}」のページです。この質問に関連するプラクティスはありません。" -header.page-header - .container - .page-header__inner - .page-header__start - .page-header__title - | Q&A - .page-header__end - .page-header-actions - ul.page-header-actions__items - li.page-header-actions__item - = link_to new_question_path, class: 'a-button is-md is-secondary is-block' do - i.fa-regular.fa-plus - span - | 質問する - li.page-header-actions__item - = link_to questions_path(target: 'not_solved'), class: 'a-button is-md is-secondary is-block is-back' do - | Q&A一覧 -hr.a-border -.page-body - .page-body__inner.has-side-nav - .container.is-md - .question.page-content - = render 'question_header', question: @question - = render 'question_body', question: @question - .loading-content - - 3.times do - = render 'questions/comment_placeholder' - .answer-content.is-hidden - - if @question.ai_answer - = render 'ai_answer', question: @question - header.thread-comments__header - h2.thread-comments__title - | 回答・コメント - .answers-list - - @answers.each do |answer| - = render 'answer', question: @question, user: current_user, answer: answer - = render 'new_answer', question: @question, user: current_user - nav.a-side-nav - .a-side-nav__inner - header.a-side-nav__header - h2.a-side-nav__title - - if @question.practice - = link_to @question.practice, - class: 'a-side-nav__title-link' do - = @question.practice.title - - else - | 関連プラクティス無し - hr.a-border - .a-side-nav__body - = render 'nav_questions', questions: @practice_questions - hr.a-border - footer.card-footer - .card-footer__footer-link - = link_to questions_path(practice_id: @question.practice), class: 'card-footer__footer-text-link' do - | 全て見る += render 'questions_and_pair_works/questions_and_pair_works_header' +.page-main + = render 'questions_and_pair_works/questions_and_pair_works_page_main_header' + hr.a-border + + .page-body + .page-body__inner.has-side-nav + .container.is-md + .question.page-content + = render 'question_header', question: @question + = render 'question_body', question: @question + .loading-content + - 3.times do + = render 'questions/comment_placeholder' + .answer-content.is-hidden + - if @question.ai_answer + = render 'ai_answer', question: @question + header.thread-comments__header + h2.thread-comments__title + | 回答・コメント + .answers-list + - @answers.each do |answer| + = render 'answer', question: @question, user: current_user, answer: answer + = render 'new_answer', question: @question, user: current_user + nav.a-side-nav + .a-side-nav__inner + header.a-side-nav__header + h2.a-side-nav__title + - if @question.practice + = link_to @question.practice, + class: 'a-side-nav__title-link' do + = @question.practice.title + - else + | 関連プラクティス無し + hr.a-border + .a-side-nav__body + = render 'nav_questions', questions: @practice_questions + hr.a-border + footer.card-footer + .card-footer__footer-link + = link_to questions_path(practice_id: @question.practice), class: 'card-footer__footer-text-link' do + | 全て見る = render '/shared/modal', id: 'modal-delete-request', modal_title: '質問の削除申請' do .modal__description.is-md diff --git a/app/views/questions_and_pair_works/_questions_and_pair_works_header.html.slim b/app/views/questions_and_pair_works/_questions_and_pair_works_header.html.slim new file mode 100644 index 00000000000..7fd1bd5ee35 --- /dev/null +++ b/app/views/questions_and_pair_works/_questions_and_pair_works_header.html.slim @@ -0,0 +1,29 @@ +// TODO ペアワークを本番リリースしたらこの分岐は削除 +- if pair_work_available? + header.page-header + .container + .page-header__inner + .page-header__start + h2.page-header__title + | Q&A・ペアワーク + .page-header__end + = questions_and_pair_works_page_tabs +- else + header.page-header + .container + .page-header__inner + .page-header__start + h2.page-header__title + | Q&A + .page-header__end + .page-header-actions + ul.page-header-actions__items + - unless params[:action] == 'new' + li.page-header-actions__item + = link_to new_question_path, class: 'a-button is-md is-secondary is-block' do + i.fa-regular.fa-plus + span 質問する + - unless params[:action] == 'index' + li.page-header-actions__item + = link_to questions_path(target: 'not_solved'), class: 'a-button is-md is-secondary is-block is-back' do + | Q&A一覧 diff --git a/app/views/questions_and_pair_works/_questions_and_pair_works_page_main_header.html.slim b/app/views/questions_and_pair_works/_questions_and_pair_works_page_main_header.html.slim new file mode 100644 index 00000000000..7e2fb2bcc45 --- /dev/null +++ b/app/views/questions_and_pair_works/_questions_and_pair_works_page_main_header.html.slim @@ -0,0 +1,20 @@ +// TODO ペアワークを本番リリースしたらこの分岐は削除 +- if pair_work_available? + header.page-main-header + .container + .page-main-header__inner + .page-main-header__start + h2.page-main-header__title + | Q&A + .page-main-header__end + .page-header-actions + ul.page-header-actions__items + - unless params[:action] == 'new' + li.page-header-actions__item + = link_to new_question_path, class: 'a-button is-md is-secondary is-block' do + i.fa-regular.fa-plus + span 質問する + - unless params[:action] == 'index' + li.page-header-actions__item + = link_to questions_path(target: 'not_solved'), class: 'a-button is-md is-secondary is-block is-back' do + | Q&A一覧 diff --git a/app/views/regular_events/_participation.html.slim b/app/views/regular_events/_participation.html.slim index 05b61691df7..636a14672f4 100644 --- a/app/views/regular_events/_participation.html.slim +++ b/app/views/regular_events/_participation.html.slim @@ -1,17 +1,18 @@ - if current_user.participating?(regular_event) .event-main-actions.is-participationed - .event-main-actions__description - p - | 参加登録しています。 - ul.event-main-actions__items - li.event-main-actions__item - = link_to regular_event_participation_path(regular_event_id: regular_event), method: :delete, - data: { confirm: 'イベントの参加をキャンセルします。よろしいですか?' }, class: 'event-main-actions__item-cancel' do - | 参加を取り消す + .event-main-actions__body + .event-main-actions__description + p 参加登録しています。 + ul.event-main-actions__items + li.event-main-actions__item + = link_to regular_event_participation_path(regular_event_id: regular_event), method: :delete, + data: { confirm: 'イベントの参加をキャンセルします。よろしいですか?' }, class: 'event-main-actions__item-cancel' do + | 参加を取り消す - else .event-main-actions.is-unparticipationed.is-available - ul.event-main-actions__items - li.event-main-actions__item - = link_to regular_event_participations_path(regular_event_id: regular_event), method: :post, - data: { confirm: 'イベント参加申込をします。よろしいですか?' }, class: 'a-button is-primary is-md is-block' do - | 参加申込 + .event-main-actions__body + ul.event-main-actions__items + li.event-main-actions__item + = link_to regular_event_participations_path(regular_event_id: regular_event), method: :post, + data: { confirm: 'イベント参加申込をします。よろしいですか?' }, class: 'a-button is-primary is-md is-block' do + | 参加申込 diff --git a/app/views/users/form/_learning_time_frames.html.slim b/app/views/users/form/_learning_time_frames.html.slim index 4d2116e2e99..f7e69d69ab3 100644 --- a/app/views/users/form/_learning_time_frames.html.slim +++ b/app/views/users/form/_learning_time_frames.html.slim @@ -1,4 +1,4 @@ -.form-item +.form-item#activity-schedule = f.label :learning_time_frames, '主な活動予定時間', class: 'a-form-label' .a-form-help p diff --git a/config/initializers/active_support_notifications.rb b/config/initializers/active_support_notifications.rb index 98bff9b0154..2da6b3c9f83 100644 --- a/config/initializers/active_support_notifications.rb +++ b/config/initializers/active_support_notifications.rb @@ -68,4 +68,14 @@ times_channel_destroyer = TimesChannelDestroyer.new ActiveSupport::Notifications.subscribe('retirement.create', times_channel_destroyer) ActiveSupport::Notifications.subscribe('training_completion.create', times_channel_destroyer) + + watch_for_pair_work_creator = WatchForPairWorkCreator.new + ActiveSupport::Notifications.subscribe('pair_work.create', watch_for_pair_work_creator) + ActiveSupport::Notifications.subscribe('pair_work.update', watch_for_pair_work_creator) + + pair_work_notifier = PairWorkNotifier.new + ActiveSupport::Notifications.subscribe('pair_work.create', pair_work_notifier) + ActiveSupport::Notifications.subscribe('pair_work.update', pair_work_notifier) + + ActiveSupport::Notifications.subscribe('pair_work.reserve', PairWorkMatchingNotifier.new) end diff --git a/config/locales/ja.yml b/config/locales/ja.yml index a1a788f1d54..ba87ba169cb 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -10,6 +10,7 @@ ja: md: '%m月%d日' sm: '%m/%d' long: '%Y年%m月%d日(%a)' + mdw_unique: '%-m/%-d(%a)' time: formats: default: '%Y年%m月%d日(%a) %H:%M' @@ -21,6 +22,7 @@ ja: year_only: '%Y' mdw_unique: '%-m/%-d(%a)' long: '%Y年%m月%d日(%a)' + mdw_and_time: '%-m月%-d日(%a)%H:%M' dates: today: '今日' tomorrow: '明日' @@ -67,6 +69,7 @@ ja: coding_test: コーディングテスト movie: 動画 grant_course_application: 給付金対応コース受講申請 + pair_work: ペアワーク attributes: user: login_name: アカウント @@ -369,6 +372,13 @@ ja: output: 出力 doorkeeper/application: redirect_uri: Redirect URL + pair_work: + practice: プラクティス + title: タイトル + description: 内容 + schedules: スケジュール + reserved_at: 希望した日時 + buddy: ペア enums: emotion: negative: Negative diff --git a/config/routes.rb b/config/routes.rb index 0e5c0a80c54..0494034e998 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -117,6 +117,9 @@ end end resources :press_releases, only: %i(index) + resources :pair_works do + resource :reservations, only: %i(create destroy), controller: "pair_works/reservations" + end get "articles/tags/:tag", to: "articles#index", as: :tag, tag: /.+/ get 'sponsorships', to: 'articles/sponsorships#index' get "pages/tags/:tag", to: "pages#index", as: :pages_tag, tag: /.+/, format: "html" diff --git a/db/fixtures/pair_work_schedules.yml b/db/fixtures/pair_work_schedules.yml new file mode 100644 index 00000000000..85a3e9e00bc --- /dev/null +++ b/db/fixtures/pair_work_schedules.yml @@ -0,0 +1,11 @@ +pair_work_schedule1: + pair_work: pair_work1 + proposed_at: <%= Time.zone.parse('2025-02-07 23:00:00') %> + +pair_work_schedule2: + pair_work: pair_work2 + proposed_at: <%= Time.zone.parse('2025-03-06 23:00:00') %> + +pair_work_schedule3: + pair_work: pair_work3 + proposed_at: <%= Time.current.beginning_of_day + 23.hours %> diff --git a/db/fixtures/pair_works.yml b/db/fixtures/pair_works.yml new file mode 100644 index 00000000000..5a333aba734 --- /dev/null +++ b/db/fixtures/pair_works.yml @@ -0,0 +1,32 @@ +pair_work1: + title: ペア確定済みの動作確認 + description: ペア確定済みのペアワークです + reserved_at: <%= Time.zone.parse('2025-02-07 23:00:00') %> + user: kimura + practice: null + buddy: sotugyou + published_at: <%= Time.zone.parse('2025-02-01 00:00:00') %> + created_at: <%= Time.zone.parse('2025-02-01 00:00:00') %> + wip: false + +pair_work2: + title: 募集中の動作確認 + description: 募集中のペアワークです + reserved_at: null + user: kimura + practice: null + buddy: null + published_at: <%= Time.zone.parse('2025-02-28 00:00:00') %> + created_at: <%= Time.zone.parse('2025-02-28 00:00:00') %> + wip: false + +pair_work3: + title: 近日開催のペアワークの動作確認 + description: 近日開催のペア確定済みペアワークです + reserved_at: <%= Time.current.beginning_of_day + 23.hours %> + user: kimura + practice: null + buddy: sotugyou + published_at: <%= Time.current %> + created_at: <%= Time.current %> + wip: false diff --git a/db/migrate/20250203011220_create_pair_works.rb b/db/migrate/20250203011220_create_pair_works.rb new file mode 100644 index 00000000000..e6f9faeda9e --- /dev/null +++ b/db/migrate/20250203011220_create_pair_works.rb @@ -0,0 +1,16 @@ +class CreatePairWorks < ActiveRecord::Migration[8.1] + def change + create_table :pair_works do |t| + t.string :title, null: false + t.text :description + t.timestamp :reserved_at + t.references :user, null: false, foreign_key: true + t.references :practice, foreign_key: true + t.references :buddy, foreign_key: { to_table: :users } + t.timestamp :published_at + t.boolean :wip, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20250203025711_create_schedules.rb b/db/migrate/20250203025711_create_schedules.rb new file mode 100644 index 00000000000..ccb4a8ed9c9 --- /dev/null +++ b/db/migrate/20250203025711_create_schedules.rb @@ -0,0 +1,10 @@ +class CreateSchedules < ActiveRecord::Migration[8.1] + def change + create_table :schedules do |t| + t.references :pair_work, null: false, foreign_key: true + t.timestamp :proposed_at, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20250414013258_add_channel_to_pair_work.rb b/db/migrate/20250414013258_add_channel_to_pair_work.rb new file mode 100644 index 00000000000..a1a7cabc175 --- /dev/null +++ b/db/migrate/20250414013258_add_channel_to_pair_work.rb @@ -0,0 +1,5 @@ +class AddChannelToPairWork < ActiveRecord::Migration[8.1] + def change + add_column :pair_works, :channel, :string, default: 'ペアワーク・モブワーク1', null: false + end +end diff --git a/db/migrate/20250519012857_add_not_null_constraint_to_pair_works_description.rb b/db/migrate/20250519012857_add_not_null_constraint_to_pair_works_description.rb new file mode 100644 index 00000000000..899712cf3dc --- /dev/null +++ b/db/migrate/20250519012857_add_not_null_constraint_to_pair_works_description.rb @@ -0,0 +1,5 @@ +class AddNotNullConstraintToPairWorksDescription < ActiveRecord::Migration[8.1] + def change + change_column_null :pair_works, :description, false + end +end diff --git a/db/migrate/20250830033535_add_default_to_pair_works_wip.rb b/db/migrate/20250830033535_add_default_to_pair_works_wip.rb new file mode 100644 index 00000000000..4488aea7877 --- /dev/null +++ b/db/migrate/20250830033535_add_default_to_pair_works_wip.rb @@ -0,0 +1,5 @@ +class AddDefaultToPairWorksWip < ActiveRecord::Migration[8.1] + def change + change_column_default :pair_works, :wip, from: nil, to: false + end +end diff --git a/db/migrate/20250830033632_add_index_to_pair_works_published_at.rb b/db/migrate/20250830033632_add_index_to_pair_works_published_at.rb new file mode 100644 index 00000000000..09cefa3f427 --- /dev/null +++ b/db/migrate/20250830033632_add_index_to_pair_works_published_at.rb @@ -0,0 +1,5 @@ +class AddIndexToPairWorksPublishedAt < ActiveRecord::Migration[8.1] + def change + add_index :pair_works, :published_at, if_not_exists: true + end +end diff --git a/db/migrate/20251115001156_rename_schedules_to_pair_work_schedules.rb b/db/migrate/20251115001156_rename_schedules_to_pair_work_schedules.rb new file mode 100644 index 00000000000..aca72e30882 --- /dev/null +++ b/db/migrate/20251115001156_rename_schedules_to_pair_work_schedules.rb @@ -0,0 +1,5 @@ +class RenameSchedulesToPairWorkSchedules < ActiveRecord::Migration[8.1] + def change + rename_table :schedules, :pair_work_schedules + end +end diff --git a/db/migrate/20260111044958_add_unique_index_to_pair_work_schedules.rb b/db/migrate/20260111044958_add_unique_index_to_pair_work_schedules.rb new file mode 100644 index 00000000000..35f4be82319 --- /dev/null +++ b/db/migrate/20260111044958_add_unique_index_to_pair_work_schedules.rb @@ -0,0 +1,5 @@ +class AddUniqueIndexToPairWorkSchedules < ActiveRecord::Migration[8.1] + def change + add_index :pair_work_schedules, [:pair_work_id, :proposed_at], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 9c23be100f5..19a22ca37b1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -551,6 +551,33 @@ t.index ["user_id"], name: "index_pages_on_user_id" end + create_table "pair_work_schedules", force: :cascade do |t| + t.bigint "pair_work_id", null: false + t.datetime "proposed_at", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["pair_work_id", "proposed_at"], name: "index_pair_work_schedules_on_pair_work_id_and_proposed_at", unique: true + t.index ["pair_work_id"], name: "index_pair_work_schedules_on_pair_work_id" + end + + create_table "pair_works", force: :cascade do |t| + t.string "title", null: false + t.text "description", null: false + t.datetime "reserved_at" + t.bigint "user_id", null: false + t.bigint "practice_id" + t.bigint "buddy_id" + t.datetime "published_at" + t.boolean "wip", default: false, null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.string "channel", default: "ペアワーク・モブワーク1", null: false + t.index ["buddy_id"], name: "index_pair_works_on_buddy_id" + t.index ["practice_id"], name: "index_pair_works_on_practice_id" + t.index ["published_at"], name: "index_pair_works_on_published_at" + t.index ["user_id"], name: "index_pair_works_on_user_id" + end + create_table "participations", force: :cascade do |t| t.datetime "created_at", null: false t.boolean "enable", default: false, null: false @@ -1123,6 +1150,10 @@ add_foreign_key "organizers", "users" add_foreign_key "pages", "practices" add_foreign_key "pages", "users" + add_foreign_key "pair_work_schedules", "pair_works" + add_foreign_key "pair_works", "practices" + add_foreign_key "pair_works", "users" + add_foreign_key "pair_works", "users", column: "buddy_id" add_foreign_key "participations", "events" add_foreign_key "participations", "users" add_foreign_key "practices", "practices", column: "source_id" diff --git a/db/seeds.rb b/db/seeds.rb index d162743c707..e0ea89c47f4 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -74,6 +74,8 @@ skipped_practices grant_course_applications practices_movies + pair_works + pair_work_schedules ] ActiveRecord::FixtureSet.create_fixtures 'db/fixtures', tables diff --git a/test/decorators/pair_work_decorator_test.rb b/test/decorators/pair_work_decorator_test.rb new file mode 100644 index 00000000000..3300f1ed3f7 --- /dev/null +++ b/test/decorators/pair_work_decorator_test.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'test_helper' +require 'active_decorator_test_case' + +class PairWorkDecoratorTest < ActiveDecoratorTestCase + test '#important?' do + not_solved_pair_work = decorate(pair_works(:pair_work1)) + assert not_solved_pair_work.important? + + not_solved_pair_work.comments.create!( + user: users(:komagata), + description: 'コメント' + ) + assert_not not_solved_pair_work.important? + + solved_pair_work = decorate(pair_works(:pair_work2)) + assert_not solved_pair_work.important? + end +end diff --git a/test/deliveries/activity_delivery_test.rb b/test/deliveries/activity_delivery_test.rb index 888e4526a92..758ab42f59e 100644 --- a/test/deliveries/activity_delivery_test.rb +++ b/test/deliveries/activity_delivery_test.rb @@ -499,4 +499,52 @@ class ActivityDeliveryTest < ActiveSupport::TestCase ActivityDelivery.with(**params).notify(:added_work) end end + + test '.notify(:came_pair_work)' do + pair_work = pair_works(:pair_work1) + params = { + pair_work:, + receiver: users(:mentormentaro) + } + + assert_difference -> { AbstractNotifier::Testing::Driver.deliveries.count }, 1 do + ActivityDelivery.notify!(:came_pair_work, **params) + end + + assert_difference -> { AbstractNotifier::Testing::Driver.enqueued_deliveries.count }, 1 do + ActivityDelivery.notify(:came_pair_work, **params) + end + + assert_difference -> { AbstractNotifier::Testing::Driver.deliveries.count }, 1 do + ActivityDelivery.with(**params).notify!(:came_pair_work) + end + + assert_difference -> { AbstractNotifier::Testing::Driver.enqueued_deliveries.count }, 1 do + ActivityDelivery.with(**params).notify(:came_pair_work) + end + end + + test '.notify(:matching_pair_work)' do + pair_work = pair_works(:pair_work2) + params = { + pair_work:, + receiver: users(:mentormentaro) + } + + assert_difference -> { AbstractNotifier::Testing::Driver.deliveries.count }, 1 do + ActivityDelivery.notify!(:matching_pair_work, **params) + end + + assert_difference -> { AbstractNotifier::Testing::Driver.enqueued_deliveries.count }, 1 do + ActivityDelivery.notify(:matching_pair_work, **params) + end + + assert_difference -> { AbstractNotifier::Testing::Driver.deliveries.count }, 1 do + ActivityDelivery.with(**params).notify!(:matching_pair_work) + end + + assert_difference -> { AbstractNotifier::Testing::Driver.enqueued_deliveries.count }, 1 do + ActivityDelivery.with(**params).notify(:matching_pair_work) + end + end end diff --git a/test/fixtures/pair_work_schedules.yml b/test/fixtures/pair_work_schedules.yml new file mode 100644 index 00000000000..b852a84098c --- /dev/null +++ b/test/fixtures/pair_work_schedules.yml @@ -0,0 +1,15 @@ +pair_work_schedule1: + pair_work: pair_work1 + proposed_at: <%= Time.zone.parse('2025-01-01 00:00:00') %> + +pair_work_schedule2: + pair_work: pair_work1 + proposed_at: <%= Time.zone.parse('2025-01-07 23:00:00') %> + +pair_work_schedule3: + pair_work: pair_work2 + proposed_at: <%= Time.zone.parse('2025-01-02 01:00:00') %> + +pair_work_schedule4: + pair_work: pair_work3 + proposed_at: <%= Time.zone.parse('2025-01-02 01:00:00') %> diff --git a/test/fixtures/pair_works.yml b/test/fixtures/pair_works.yml new file mode 100644 index 00000000000..2920b597dfe --- /dev/null +++ b/test/fixtures/pair_works.yml @@ -0,0 +1,32 @@ +pair_work1: + title: 募集中のペアワークです(タイトル) + description: 募集中のペアワークです(詳細) + reserved_at: null + user: kimura + practice: practice1 + buddy: null + published_at: <%= Time.zone.parse('2025-01-01 00:00:00') %> + created_at: <%= Time.zone.parse('2025-01-01 00:00:00') %> + wip: false + +pair_work2: + title: ペア確定済みのペアワークです(タイトル) + description: ペア確定済みのペアワークです(詳細) + reserved_at: <%= Time.zone.parse('2025-01-02 01:00:00') %> + user: kimura + practice: practice1 + buddy: sotugyou + published_at: <%= Time.zone.parse('2025-01-01 00:00:00') %> + created_at: <%= Time.zone.parse('2025-01-01 00:00:00') %> + wip: false + +pair_work3: + title: wipのペアワークです(タイトル) + description: wipのペアワークです(詳細) + reserved_at: null + user: kimura + practice: null + buddy: null + published_at: null + created_at: <%= Time.zone.parse('2025-01-01 00:00:00') %> + wip: true diff --git a/test/helpers/pair_works_helper_test.rb b/test/helpers/pair_works_helper_test.rb new file mode 100644 index 00000000000..8579dfe36b0 --- /dev/null +++ b/test/helpers/pair_works_helper_test.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'test_helper' + +class PairWorksHelperTest < ActionView::TestCase + setup do + def current_user + users(:kimura) + end + end + + test 'schedule_dates' do + created_at = Time.zone.local(2025, 1, 1, 0, 0, 0) + dates = [ + Date.new(2025, 1, 1), + Date.new(2025, 1, 2), + Date.new(2025, 1, 3), + Date.new(2025, 1, 4), + Date.new(2025, 1, 5), + Date.new(2025, 1, 6), + Date.new(2025, 1, 7) + ] + assert_equal dates, schedule_dates(created_at) + + date = Date.new(2025, 1, 1) + assert_equal dates, schedule_dates(date) + end + + test 'sorted_wdays' do + wdays_if_wednesday = [3, 4, 5, 6, 0, 1, 2] + assert_equal wdays_if_wednesday, sorted_wdays(Date.new(2025, 1, 1)) + end + + test 'schedule_check_disabled?' do + past_date = Time.current.yesterday + future_date = Time.current.tomorrow + my_pair_work = pair_works(:pair_work1) + + assert schedule_check_disabled?(past_date) + assert_not schedule_check_disabled?(future_date) + assert schedule_check_disabled?(future_date, pair_work: my_pair_work) + end + + test 'learning_time_frame_checked?' do + future_date = Time.current.tomorrow + past_date = Time.current.yesterday + my_learning_time_frame_id = LearningTimeFramesUser.create!(user: current_user, learning_time_frame_id: 3).learning_time_frame_id + + assert learning_time_frame_checked?(future_date, my_learning_time_frame_id) + assert_not learning_time_frame_checked?(past_date, my_learning_time_frame_id) + end + + test 'schedule_target_time' do + elapsed_day_count = 1 + elapsed_time_count = 1 + base_time = Time.current.beginning_of_day + target_time = base_time + elapsed_day_count.days + elapsed_time_count.hours + + assert_equal target_time, schedule_target_time(elapsed_day_count, elapsed_time_count) + + pair_work = pair_works(:pair_work1) + base_time = pair_work.created_at + target_time = base_time + elapsed_day_count.days + elapsed_time_count.hours + + assert_equal target_time, schedule_target_time(elapsed_day_count, elapsed_time_count, pair_work:) + end + + test 'schedule_check_box_id' do + travel_to Time.zone.local(2025, 1, 1, 0, 0, 0) do + elapsed_day_count = 1 + elapsed_time_count = 1 + target_time = schedule_target_time(elapsed_day_count, elapsed_time_count) + + assert_equal 'schedule_ids_202501020100', schedule_check_box_id(target_time) + end + end + + test 'meta_label_by_status' do + pair_work = pair_works(:pair_work2) + normal_meta_label = 'a-meta__label' + important_meta_label = 'a-meta__label is-important' + + assert_equal normal_meta_label, meta_label_by_status(pair_work) + assert_not_equal important_meta_label, meta_label_by_status(pair_work) + + reserved_on = Time.zone.local(2025, 1, 2) + travel_to reserved_on do + assert_equal important_meta_label, meta_label_by_status(pair_work) + assert_not_equal normal_meta_label, meta_label_by_status(pair_work) + end + end +end diff --git a/test/mailers/activity_mailer_test.rb b/test/mailers/activity_mailer_test.rb index 82cb0ed9a39..bac2ff3b41d 100644 --- a/test/mailers/activity_mailer_test.rb +++ b/test/mailers/activity_mailer_test.rb @@ -1285,6 +1285,108 @@ class ActivityMailerTest < ActionMailer::TestCase assert_empty ActionMailer::Base.deliveries end + test 'came_pair_work' do + pair_work = pair_works(:pair_work1) + mentor = users(:komagata) + + ActivityMailer.came_pair_work( + receiver: mentor, + pair_work: + ).deliver_now + + assert_not ActionMailer::Base.deliveries.empty? + email = ActionMailer::Base.deliveries.last + query = CGI.escapeHTML({ kind: 28, link: "/pair_works/#{pair_work.id}" }.to_param) + assert_equal ['noreply@bootcamp.fjord.jp'], email.from + assert_equal ['komagata@fjord.jp'], email.to + assert_equal '[FBC] kimuraさんからペアワーク依頼「募集中のペアワークです(タイトル)」が投稿されました。', email.subject + assert_match(%r{ペアワークのページへ}, email.body.to_s) + end + + test 'came_pair_work with params' do + pair_work = pair_works(:pair_work1) + mentor = users(:komagata) + + mailer = ActivityMailer.with( + receiver: mentor, + pair_work: + ).came_pair_work + + perform_enqueued_jobs do + mailer.deliver_later + end + + assert_not ActionMailer::Base.deliveries.empty? + email = ActionMailer::Base.deliveries.last + query = CGI.escapeHTML({ kind: 28, link: "/pair_works/#{pair_work.id}" }.to_param) + assert_equal ['noreply@bootcamp.fjord.jp'], email.from + assert_equal ['komagata@fjord.jp'], email.to + assert_equal '[FBC] kimuraさんからペアワーク依頼「募集中のペアワークです(タイトル)」が投稿されました。', email.subject + assert_match(%r{ペアワークのページへ}, email.body.to_s) + end + + test 'matching_pair_work' do + pair_work = pair_works(:pair_work2) + mentor = users(:komagata) + + ActivityMailer.matching_pair_work( + receiver: mentor, + pair_work: + ).deliver_now + + assert_not ActionMailer::Base.deliveries.empty? + email = ActionMailer::Base.deliveries.last + query = CGI.escapeHTML({ kind: 29, link: "/pair_works/#{pair_work.id}" }.to_param) + assert_equal ['noreply@bootcamp.fjord.jp'], email.from + assert_equal ['komagata@fjord.jp'], email.to + assert_equal '[FBC] kimuraさんのペアワーク【 ペア確定済みのペアワークです(タイトル) 】のペアがsotugyouさんに決定しました。', email.subject + assert_match(%r{ペアワークのページへ}, email.body.to_s) + end + + test 'matching_pair_work with params' do + pair_work = pair_works(:pair_work2) + mentor = users(:komagata) + + mailer = ActivityMailer.with( + receiver: mentor, + pair_work: + ).matching_pair_work + + perform_enqueued_jobs do + mailer.deliver_later + end + + assert_not ActionMailer::Base.deliveries.empty? + email = ActionMailer::Base.deliveries.last + query = CGI.escapeHTML({ kind: 29, link: "/pair_works/#{pair_work.id}" }.to_param) + assert_equal ['noreply@bootcamp.fjord.jp'], email.from + assert_equal ['komagata@fjord.jp'], email.to + assert_equal '[FBC] kimuraさんのペアワーク【 ペア確定済みのペアワークです(タイトル) 】のペアがsotugyouさんに決定しました。', email.subject + assert_match(%r{ペアワークのページへ}, email.body.to_s) + end + + test 'matching_pair_work to mute email notification or retired user' do + pair_work = pair_works(:pair_work2) + receiver = users(:hatsuno) + Watch.create!(user: receiver, watchable: pair_work) + + receiver.update_columns(mail_notification: false, retired_on: nil) # rubocop:disable Rails/SkipsModelValidations + ActivityMailer.matching_pair_work(receiver:, pair_work: pair_work.reload).deliver_now + assert_empty ActionMailer::Base.deliveries + + receiver.update_columns(mail_notification: false, retired_on: Date.current) # rubocop:disable Rails/SkipsModelValidations + ActivityMailer.matching_pair_work(receiver:, pair_work: pair_work.reload).deliver_now + assert_empty ActionMailer::Base.deliveries + + receiver.update_columns(mail_notification: true, retired_on: Date.current) # rubocop:disable Rails/SkipsModelValidations + ActivityMailer.matching_pair_work(receiver:, pair_work: pair_work.reload).deliver_now + assert_empty ActionMailer::Base.deliveries + + receiver.update_columns(mail_notification: true, retired_on: nil) # rubocop:disable Rails/SkipsModelValidations + ActivityMailer.matching_pair_work(receiver:, pair_work: pair_work.reload).deliver_now + assert_not ActionMailer::Base.deliveries.empty? + end + private def mailer_url_options diff --git a/test/mailers/previews/activity_mailer_preview.rb b/test/mailers/previews/activity_mailer_preview.rb index c5767e6b661..f364cfc2789 100644 --- a/test/mailers/previews/activity_mailer_preview.rb +++ b/test/mailers/previews/activity_mailer_preview.rb @@ -186,4 +186,18 @@ def added_work ActivityMailer.with(work:, sender: user, receiver:).added_work end + + def came_pair_work + receiver = User.find(ActiveRecord::FixtureSet.identify(:mentormentaro)) + pair_work = PairWork.find(ActiveRecord::FixtureSet.identify(:pair_work1)) + + ActivityMailer.with(receiver:, pair_work:).came_pair_work + end + + def matching_pair_work + receiver = User.find(ActiveRecord::FixtureSet.identify(:mentormentaro)) + pair_work = PairWork.find(ActiveRecord::FixtureSet.identify(:pair_work2)) + + ActivityMailer.with(receiver:, pair_work:).matching_pair_work + end end diff --git a/test/models/pair_work_test.rb b/test/models/pair_work_test.rb new file mode 100644 index 00000000000..755836068a7 --- /dev/null +++ b/test/models/pair_work_test.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +require 'test_helper' + +class PairWorkTest < ActiveSupport::TestCase + test '.by_target' do + solved_pair_work = pair_works(:pair_work2) + not_solved_pair_work = pair_works(:pair_work1) + assert_includes PairWork.by_target('solved'), solved_pair_work + assert_not_includes PairWork.by_target('solved'), not_solved_pair_work + + assert_includes PairWork.by_target('not_solved'), not_solved_pair_work + assert_not_includes PairWork.by_target('not_solved'), solved_pair_work + + assert_includes PairWork.by_target(nil), solved_pair_work + assert_includes PairWork.by_target(nil), not_solved_pair_work + end + + test '.generate_pair_works_property' do + solved_pair_works_property = PairWork.generate_pair_works_property('solved') + assert_equal 'ペア確定済みのペアワーク', solved_pair_works_property.title + assert_equal 'ペア確定済みのペアワークはありません。', solved_pair_works_property.empty_message + + not_solved_pair_works_property = PairWork.generate_pair_works_property('not_solved') + assert_equal '募集中のペアワーク', not_solved_pair_works_property.title + assert_equal '募集中のペアワークはありません。', not_solved_pair_works_property.empty_message + + all_pair_works_property = PairWork.generate_pair_works_property(nil) + assert_equal '全てのペアワーク', all_pair_works_property.title + assert_equal 'ペアワークはありません。', all_pair_works_property.empty_message + end + + test '#generate_notice_message' do + wip_pair_work = pair_works(:pair_work3) + assert_equal 'ペアワークをWIPとして保存しました。', wip_pair_work.generate_notice_message(:create) + assert_equal 'ペアワークをWIPとして保存しました。', wip_pair_work.generate_notice_message(:update) + + published_pair_work = pair_works(:pair_work1) + assert_equal 'ペアワークを作成しました。', published_pair_work.generate_notice_message(:create) + assert_equal 'ペアワークを更新しました。', published_pair_work.generate_notice_message(:update) + end + + test '.unsolved_badge' do + assert_equal 1, PairWork.unsolved_badge(current_user: users(:komagata)) + + assert_nil PairWork.unsolved_badge(current_user: users(:hatsuno)) + end + + test '.upcoming_pair_works' do + user = users(:hajime) + + travel_to Time.zone.local(2025, 1, 15, 12, 0, 0) do + pair_work_template = { + user:, + title: 'ペアが確定していて、近日開催されるペアワーク', + description: 'ペアが確定していて、近日開催されるペアワーク', + buddy: users(:komagata), + channel: 'ペアワーク・モブワーク1', + wip: false + } + upcoming_pair_work_tomorrow = PairWork.create!(pair_work_template.merge( + reserved_at: Time.current.beginning_of_day + 1.day, + schedules_attributes: [{ proposed_at: Time.current.beginning_of_day + 1.day }] + )) + upcoming_pair_work_day_after_tomorrow = PairWork.create!(pair_work_template.merge( + reserved_at: Time.current.beginning_of_day + 2.days, + schedules_attributes: [{ proposed_at: Time.current.beginning_of_day + 2.days }] + )) + assert_includes PairWork.upcoming_pair_works(user), upcoming_pair_work_tomorrow + assert_includes PairWork.upcoming_pair_works(user), upcoming_pair_work_day_after_tomorrow + + held_today_pair_work = PairWork.create!(pair_work_template.merge( + reserved_at: Time.current.beginning_of_day, + schedules_attributes: [{ proposed_at: Time.current.beginning_of_day }] + )) + assert_not_includes PairWork.upcoming_pair_works(user), held_today_pair_work + + unrelated_pair_work = pair_works(:pair_work2) + assert_not_includes PairWork.upcoming_pair_works(user), unrelated_pair_work + end + end + + test '.not_held' do + travel_to Time.zone.local(2025, 1, 15, 12, 0, 0) do + not_held_pair_work = PairWork.create!({ + user: users(:kimura), + title: 'ペア確定したが、まだ実施されてないペアワーク', + description: 'ペア確定したが、まだ実施されてないペアワーク', + buddy: users(:komagata), + channel: 'ペアワーク・モブワーク1', + wip: false, + reserved_at: Time.current.beginning_of_day + 1.day, + schedules_attributes: [{ proposed_at: Time.current.beginning_of_day + 1.day }] + }) + not_solved_pair_work = pair_works(:pair_work1) + wip_pair_work = pair_works(:pair_work3) + + held_on_pair_work = pair_works(:pair_work2) + + assert_includes PairWork.not_held, not_held_pair_work + assert_includes PairWork.not_held, not_solved_pair_work + assert_includes PairWork.not_held, wip_pair_work + + assert_not_includes PairWork.not_held, held_on_pair_work + end + end + + test '#solved?' do + solved_pair_work = pair_works(:pair_work2) + assert solved_pair_work.solved? + + not_solved_pair_work = pair_works(:pair_work1) + assert_not not_solved_pair_work.solved? + end + + test '#reserve' do + valid_params = { + reserved_at: Time.zone.parse('2025-01-01 00:00:00'), + buddy_id: users(:komagata).id + } + pair_work = pair_works(:pair_work1) + + assert pair_work.reserve(valid_params) + end + + test '#reserve fails when buddy_id is nil' do + invalid_params = { + reserved_at: Time.zone.parse('2025-01-02 01:00:00'), + buddy_id: nil + } + pair_work = pair_works(:pair_work1) + + assert_not pair_work.reserve(invalid_params) + assert_includes pair_work.errors.full_messages, 'ペアが選択されていません' + end + + test '#reserve fails when buddy_id does not exist' do + invalid_params = { + reserved_at: Time.zone.parse('2025-01-02 01:00:00'), + buddy_id: 999_999 + } + pair_work = pair_works(:pair_work1) + + assert_not pair_work.reserve(invalid_params) + assert_includes pair_work.errors.full_messages, 'ペアが選択されていません' + end + + test '#reserve fails when reserved_at is nil' do + invalid_params = { + reserved_at: nil, + buddy_id: users(:komagata).id + } + pair_work = pair_works(:pair_work1) + + assert_not pair_work.reserve(invalid_params) + assert_includes pair_work.errors.full_messages, '希望した日時が選択されていません' + end + + test '#reserve fails when reserved_at is not in proposed schedules' do + unscheduled_reserved_at = Time.zone.parse('2025-01-04 00:00:00') + invalid_params_reserved_at = { + reserved_at: unscheduled_reserved_at, + buddy_id: users(:komagata).id + } + pair_work = pair_works(:pair_work1) + + assert_not pair_work.reserve(invalid_params_reserved_at) + assert_includes pair_work.errors.full_messages, '希望した日時は提案されたスケジュールに含まれていません' + end +end diff --git a/test/models/searcher/configuration_test.rb b/test/models/searcher/configuration_test.rb index 3a94fe3103f..e25b708ea19 100644 --- a/test/models/searcher/configuration_test.rb +++ b/test/models/searcher/configuration_test.rb @@ -19,6 +19,7 @@ class Searcher::ConfigurationTest < ActiveSupport::TestCase assert_includes configs.keys, :comment assert_includes configs.keys, :event assert_includes configs.keys, :regular_event + assert_includes configs.keys, :pair_work end test 'get returns specific configuration' do @@ -50,7 +51,7 @@ class Searcher::ConfigurationTest < ActiveSupport::TestCase test 'available_types returns all configuration keys' do types = Searcher::Configuration.available_types - expected_types = %i[practice user report product announcement page question answer correct_answer comment event regular_event] + expected_types = %i[practice user report product announcement page question answer correct_answer comment event regular_event pair_work] assert_equal expected_types.sort, types.sort end @@ -74,6 +75,7 @@ class Searcher::ConfigurationTest < ActiveSupport::TestCase assert_includes options, ['コメント', :comment] assert_includes options, ['イベント', :event] assert_includes options, ['定期イベント', :regular_event] + assert_includes options, ['ペアワーク', :pair_work] end test 'configurations have required keys' do diff --git a/test/models/searcher_test.rb b/test/models/searcher_test.rb index 397ca645578..ae822cd0ba5 100644 --- a/test/models/searcher_test.rb +++ b/test/models/searcher_test.rb @@ -411,7 +411,7 @@ def strip_html(text) end test 'available_types returns all configured types' do - expected_types = %i[practice user report product announcement page question answer correct_answer comment event regular_event] + expected_types = %i[practice user report product announcement page question answer correct_answer comment event regular_event pair_work] assert_equal expected_types.sort, Searcher::Configuration.available_types.sort end diff --git a/test/system/notification/pair_works_test.rb b/test/system/notification/pair_works_test.rb new file mode 100644 index 00000000000..8c70511524d --- /dev/null +++ b/test/system/notification/pair_works_test.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'notification_system_test_case' + +class Notification::PairWorksTest < NotificationSystemTestCase + setup do + @delivery_mode = AbstractNotifier.delivery_mode + AbstractNotifier.delivery_mode = :normal + end + + teardown do + AbstractNotifier.delivery_mode = @delivery_mode + end + + test 'mentor receive notification when pair_work is posted' do + travel_to Time.zone.local(2025, 3, 2, 0, 0, 0) do + visit_with_auth '/pair_works/new', 'kimura' + within 'form[name=pair_work]' do + fill_in 'pair_work[title]', with: 'テストのペアワーク募集' + fill_in 'pair_work[description]', with: 'テストのペアワーク募集です。' + within '.form-table' do + check 'schedule_ids_202503030000', allow_label_click: true + end + end + + click_button '登録する' + assert_text 'ペアワークを作成しました。' + + assert_user_has_notification(user: users(:mentormentaro), kind: Notification.kinds[:came_pair_work], text: 'kimuraさんからペアワーク依頼「テストのペアワーク募集」が投稿されました。') + end + end + + test 'watcher receive notification when pair_work is matched' do + travel_to Time.zone.local(2025, 3, 2, 0, 0, 0) do + visit_with_auth '/pair_works/new', 'kimura' + within 'form[name=pair_work]' do + fill_in 'pair_work[title]', with: 'テストのペアワーク募集' + fill_in 'pair_work[description]', with: 'テストのペアワーク募集です。' + within '.form-table' do + check 'schedule_ids_202503030000', allow_label_click: true + end + end + click_button '登録する' + assert_text 'ペアワークを作成しました。' + assert_text 'Watch中' + logout + + visit_with_auth pair_works_path(target: 'not_solved'), 'mentormentaro' + click_on 'テストのペアワーク募集' + assert_text 'Watch中' + within '.a-table' do + accept_alert do + find_button(id: '2025-03-03T00:00:00+09:00').click + end + end + assert_text 'ペアが確定しました。' + + assert_user_has_notification(user: users(:kimura), kind: Notification.kinds[:matching_pair_work], + text: 'kimuraさんのペアワーク【 テストのペアワーク募集 】のペアがmentormentaroさんに決定しました。') + assert_user_has_notification(user: users(:mentormentaro), kind: Notification.kinds[:matching_pair_work], + text: 'kimuraさんのペアワーク【 テストのペアワーク募集 】のペアがあなたに決定しました。') + end + end + + test 'notify when a WIP pair_work is published' do + travel_to Time.zone.local(2025, 3, 2, 0, 0, 0) do + visit_with_auth '/pair_works/new', 'kimura' + within 'form[name=pair_work]' do + fill_in 'pair_work[title]', with: 'WIPで保存時は通知が飛ばない' + fill_in 'pair_work[description]', with: 'WIPで保存時は通知が飛ばない' + within '.form-table' do + check 'schedule_ids_202503030000', allow_label_click: true + end + end + click_button 'WIP' + assert_text 'ペアワークをWIPとして保存しました。' + assert_user_has_no_notification(user: users(:mentormentaro), kind: Notification.kinds[:came_pair_work], + text: 'WIPで保存時は通知が飛ばない') + + within 'form[name=pair_work]' do + fill_in 'pair_work[title]', with: '公開された時に通知が飛ぶ' + fill_in 'pair_work[description]', with: '公開された時に通知が飛ぶ' + end + click_button 'ペアワークを公開' + assert_text 'ペアワークを更新しました。' + + assert_user_has_notification(user: users(:mentormentaro), kind: Notification.kinds[:came_pair_work], text: '公開された時に通知が飛ぶ') + end + end +end diff --git a/test/system/pair_works_test.rb b/test/system/pair_works_test.rb new file mode 100644 index 00000000000..6221c2b8361 --- /dev/null +++ b/test/system/pair_works_test.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require 'application_system_test_case' + +class PairWorksTest < ApplicationSystemTestCase + test 'show listing unsolved pair works' do + visit_with_auth pair_works_path(target: 'not_solved'), 'kimura' + assert_equal '募集中のペアワーク | FBC', title + end + + test 'show listing solved pair works' do + visit_with_auth pair_works_path(target: 'solved'), 'kimura' + assert_equal 'ペア確定済みのペアワーク | FBC', title + end + + test 'show listing all pair works' do + visit_with_auth pair_works_path, 'kimura' + assert_equal '全てのペアワーク | FBC', title + end + + test 'create a pair_work' do + travel_to Time.zone.local(2025, 3, 2, 0, 0, 0) do + visit_with_auth new_pair_work_path, 'kimura' + within 'form[name=pair_work]' do + within '.select-practices' do + find('.choices__inner').click + find('#choices--js-choices-practice-item-choice-12', text: 'sshdでパスワード認証を禁止にする').click + end + fill_in 'pair_work[title]', with: 'テストのペアワーク募集' + fill_in 'pair_work[description]', with: 'テストのペアワーク募集です。' + within '.form-table' do + check 'schedule_ids_202503030000', allow_label_click: true + end + click_button '登録する' + end + assert_text 'ペアワークを作成しました。' + assert_selector '.a-title-label.is-solved.is-danger', text: '募集中' + assert_text 'Watch中' + + visit_with_auth pair_works_path(target: 'not_solved'), 'mentormentaro' + click_on 'テストのペアワーク募集' + assert_text 'Watch中' + end + end + + test 'create a pair_work matching' do + travel_to Time.zone.local(2025, 1, 1, 0, 0, 0) do + pair_work = pair_works(:pair_work1) + visit_with_auth pair_work_path(pair_work), 'mentormentaro' + within '.a-table' do + accept_alert do + find_button(id: '2025-01-01T00:00:00+09:00').click + end + end + assert_selector '.a-title-label.is-solved.is-success', text: 'ペア確定' + within 'header.event-main-actions__header' do + assert_text 'ペアが確定しました' + assert_selector 'a', text: 'カレンダーに登録' + end + within '.event-main-actions__body' do + assert_selector "img[title*='mentormentaro']" + assert_selector 'a', text: 'mentormentaro (メンタ メンタロウ)' + assert_text '2025年01月01日(水) 00:00' + assert_text 'ペアワーク・モブワーク1' + end + end + end + + test 'cannot create a pair_work matching with a past date' do + travel_to Time.zone.local(2025, 1, 10, 0, 0, 0) do + pair_work = pair_works(:pair_work1) + visit_with_auth pair_work_path(pair_work), 'komagata' + assert_text '募集中' + within '.a-table' do + find_button(id: '2025-01-01T00:00:00+09:00', disabled: true) + end + end + end + + test 'update a pair_work' do + pair_work = pair_works(:pair_work1) + visit_with_auth pair_work_path(pair_work), 'kimura' + click_link '内容修正' + fill_in 'pair_work[title]', with: 'ペアワークのテスト(修正)' + fill_in 'pair_work[description]', with: 'ペアワークのテストです。(修正)' + within '.select-practices' do + find('.choices__inner').click + find('#choices--js-choices-practice-item-choice-12', text: 'sshdでパスワード認証を禁止にする').click + end + click_button '更新する' + + assert_text 'ペアワークを更新しました。' + assert_text 'ペアワークのテスト(修正)' + assert_text 'ペアワークのテストです。(修正)' + assert_selector 'a.a-category-link', text: 'sshdでパスワード認証を禁止にする' + end + + test 'delete a pair_work' do + pair_work = pair_works(:pair_work1) + visit_with_auth pair_work_path(pair_work), 'kimura' + accept_confirm do + click_link '削除' + end + + assert_text 'ペアワークを削除しました。' + assert_equal '全てのペアワーク | FBC', title + end + + test 'only authorized users can update and delete pair_works' do + pair_work = pair_works(:pair_work1) + pair_work_user = 'kimura' + admin_user = 'komagata' + user = 'hatsuno' + + visit_with_auth pair_work_path(pair_work), pair_work_user + assert_text '内容修正' + assert_text '削除' + + visit_with_auth pair_work_path(pair_work), admin_user + assert_text '内容修正' + assert_text '削除' + + visit_with_auth pair_work_path(pair_work), user + assert_no_text '内容修正' + assert_no_text '削除' + end +end