diff --git a/.rubocop.yml b/.rubocop.yml index 8b2d084092d..b6c222ec208 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,10 +8,6 @@ inherit_gem: - config/rubocop.yml - config/rails.yml -Metrics/BlockLength: - Exclude: - - test/**/* - Metrics/ClassLength: Exclude: - test/**/* @@ -20,14 +16,16 @@ Metrics/ClassLength: - app/controllers/users_controller.rb - app/mailers/activity_mailer.rb - app/models/event.rb - - app/models/notification.rb - - app/models/notification_facade.rb - app/models/practice.rb - app/models/user.rb - app/models/product.rb - app/models/report.rb - app/notifiers/activity_notifier.rb +Metrics/ModuleLength: + Exclude: + - app/decorators/user_decorator.rb + AllCops: Exclude: - '**/templates/**/*' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 77c71421814..ba87dd8713a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -103,9 +103,3 @@ Style/HashSyntax: - 'test/system/sign_up_test.rb' - 'test/system/user/products_test.rb' - 'test/test_helper.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantFreeze: - Exclude: - - 'app/models/link_checker/extractor.rb' diff --git a/app/controllers/api/products/self_assigned_controller.rb b/app/controllers/api/products/self_assigned_controller.rb index da9dc3c7a82..72f2d178149 100644 --- a/app/controllers/api/products/self_assigned_controller.rb +++ b/app/controllers/api/products/self_assigned_controller.rb @@ -7,13 +7,15 @@ def index @target = 'self_assigned_all' unless target_allowlist.include?(@target) @products = case @target when 'self_assigned_all' - Product.self_assigned_product(current_user.id) + Product.unhibernated_user_products + .self_assigned_product(current_user.id) .unchecked .list .order_for_self_assigned_list .page(params[:page]) when 'self_assigned_no_replied' - Product.self_assigned_no_replied_products(current_user.id) + Product.unhibernated_user_products + .self_assigned_no_replied_products(current_user.id) .unchecked .list .page(params[:page]) diff --git a/app/decorators/user_decorator.rb b/app/decorators/user_decorator.rb index 1dbe776f5c5..8ffcea64805 100644 --- a/app/decorators/user_decorator.rb +++ b/app/decorators/user_decorator.rb @@ -1,59 +1,13 @@ # frozen_string_literal: true module UserDecorator + include Role + include Retire + def twitter_url "https://twitter.com/#{twitter_account}" end - def roles - role_list = [ - { role: 'retired', value: retired? }, - { role: 'hibernationed', value: hibernated? }, - { role: 'admin', value: admin? }, - { role: 'mentor', value: mentor? }, - { role: 'adviser', value: adviser? }, - { role: 'graduate', value: graduated? }, - { role: 'trainee', value: trainee? } - ] - roles = role_list.find_all { |v| v[:value] } - .map { |v| v[:role] } - roles << :student if roles.empty? - - roles - end - - def primary_role - roles.first - end - - def staff_roles - staff_roles = [ - { role: '管理者', value: admin? }, - { role: 'メンター', value: mentor? }, - { role: 'アドバイザー', value: adviser? } - ] - staff_roles.find_all { |v| v[:value] } - .map { |v| v[:role] } - .join('、') - end - - def roles_to_s - return '' if roles.empty? - - roles = [ - { role: '退会ユーザー', value: retired? }, - { role: '休会ユーザー', value: hibernated? }, - { role: '管理者', value: admin? }, - { role: 'メンター', value: mentor? }, - { role: 'アドバイザー', value: adviser? }, - { role: '卒業生', value: graduated? }, - { role: '研修生', value: trainee? } - ] - roles.find_all { |v| v[:value] } - .map { |v| v[:role] } - .join('、') - end - def icon_title ["#{login_name} (#{name})", staff_roles].reject(&:blank?) .join(': ') @@ -120,4 +74,8 @@ def address country_name end end + + def hibernation_days + ActiveSupport::Duration.build(Time.zone.now - hibernated_at).in_days.floor if hibernated_at? + end end diff --git a/app/decorators/user_decorator/retire.rb b/app/decorators/user_decorator/retire.rb new file mode 100644 index 00000000000..55d4b404242 --- /dev/null +++ b/app/decorators/user_decorator/retire.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module UserDecorator + module Retire + def retire_countdown + ActiveSupport::Duration.build(scheduled_retire_at - Time.current) if hibernated_at? + end + + def retire_deadline + countdown = + if retire_countdown.in_hours < 1 + "#{retire_countdown.in_minutes.floor}分" + elsif retire_countdown.in_hours < 24 + "#{retire_countdown.in_hours.floor}時間" + else + "#{retire_countdown.in_days.floor}日" + end + + "#{l scheduled_retire_at} (自動退会まであと#{countdown})" + end + + def countdown_danger_tag + retire_countdown.in_days <= 7 ? 'is-danger' : '' + end + end +end diff --git a/app/decorators/user_decorator/role.rb b/app/decorators/user_decorator/role.rb new file mode 100644 index 00000000000..b27b1d44ea6 --- /dev/null +++ b/app/decorators/user_decorator/role.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module UserDecorator + module Role + def roles + role_list = [ + { role: 'retired', value: retired? }, + { role: 'hibernationed', value: hibernated? }, + { role: 'admin', value: admin? }, + { role: 'mentor', value: mentor? }, + { role: 'adviser', value: adviser? }, + { role: 'graduate', value: graduated? }, + { role: 'trainee', value: trainee? } + ] + roles = role_list.find_all { |v| v[:value] } + .map { |v| v[:role] } + roles << :student if roles.empty? + + roles + end + + def primary_role + roles.first + end + + def staff_roles + staff_roles = [ + { role: '管理者', value: admin? }, + { role: 'メンター', value: mentor? }, + { role: 'アドバイザー', value: adviser? } + ] + staff_roles.find_all { |v| v[:value] } + .map { |v| v[:role] } + .join('、') + end + + def roles_to_s + return '' if roles.empty? + + roles = [ + { role: '退会ユーザー', value: retired? }, + { role: '休会ユーザー', value: hibernated? }, + { role: '管理者', value: admin? }, + { role: 'メンター', value: mentor? }, + { role: 'アドバイザー', value: adviser? }, + { role: '卒業生', value: graduated? }, + { role: '研修生', value: trainee? } + ] + roles.find_all { |v| v[:value] } + .map { |v| v[:role] } + .join('、') + end + end +end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index ab0f8f4da3b..74586db4dcd 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -16,8 +16,8 @@ def searchable_url(searchable) document = searchable.commentable_type.constantize.find(searchable.commentable_id) "#{polymorphic_url(document)}#comment_#{searchable.id}" elsif searchable.instance_of?(Answer) || searchable.instance_of?(CorrectAnswer) - document = searchable.question - polymorphic_url(document).to_s + document = Question.find(searchable.question.id) + "#{polymorphic_url(document)}#answer_#{searchable.id}" else polymorphic_url(searchable) end diff --git a/app/javascript/components/Searchable.jsx b/app/javascript/components/Searchable.jsx new file mode 100644 index 00000000000..3be60d4584f --- /dev/null +++ b/app/javascript/components/Searchable.jsx @@ -0,0 +1,173 @@ +import React from 'react' +import dayjs from 'dayjs' +import ja from 'dayjs/locale/ja' +dayjs.locale(ja) + +export default function Searchable({ searchable, word }) { + const roleClass = `is-${searchable.primary_role}` + const searchableClass = searchable.wip + ? `is-wip is-${searchable.model_name}` + : `is-${searchable.model_name}` + + const userUrl = `/users/${searchable.user_id}` + const documentAuthorUserUrl = `/users/${searchable.document_author_id}` + const talkUrl = `/talks/${searchable.talk_id}` + + const updatedAt = dayjs(searchable.updated_at).format( + 'YYYY年MM月DD日(dd) HH:mm' + ) + const canDisplayTalk = searchable.model_name === 'user' && searchable.talk_id + const currentUser = window.currentUser + + const labelContent = + searchable.model_name === 'regular_event' ? ( + + 定期 +
+ イベント +
+ ) : searchable.model_name === 'event' ? ( + + 特別 +
+ イベント +
+ ) : searchable.model_name === 'practice' ? ( + + プラク +
+ ティス +
+ ) : ( + + {searchable.model_name_with_i18n} + + ) + + const badgeContent = searchable.wip ? ( +
+ WIP +
+ ) : searchable.is_comment_or_answer ? ( +
+ コメント +
+ ) : searchable.is_user ? ( +
+ ユーザー +
+ ) : ( + '' + ) + + const summary = () => { + const wordsPattern = word + .trim() + .replaceAll(/[.*+?^=!:${}()|[\]/\\]/g, '\\$&') + .replaceAll(/\s+/g, '|') + const pattern = new RegExp(wordsPattern, 'gi') + if (word) { + return searchable.summary.replaceAll( + pattern, + `$&` + ) + } else { + return searchable.summary + } + } + + return ( +
+
+ {searchable.is_user && ( +
+ + + {searchable.title} + + +
+ )} + {!searchable.is_user && ( +
{labelContent}
+ )} +
+
+
+ {badgeContent} + +
+
+
+
+

+
+
+
+
+
+ {!['practice', 'page', 'user'].includes( + searchable.model_name + ) && ( + + )} +
+
+ {updatedAt} +
+
+ {searchable.is_comment_or_answer && ( +
+
+ {'('} + + {searchable.document_author_login_name} + {' '} + {searchable.model_name_with_i18n} + {')'} +
+
+ )} + {currentUser.roles.includes('admin') && canDisplayTalk && ( + + )} +
+
+
+
+
+
+ ) +} diff --git a/app/javascript/components/Searchables.jsx b/app/javascript/components/Searchables.jsx new file mode 100644 index 00000000000..d192cb3430b --- /dev/null +++ b/app/javascript/components/Searchables.jsx @@ -0,0 +1,79 @@ +import React from 'react' +import useSWR from 'swr' +import usePage from './hooks/usePage' +import Pagination from './Pagination' +import LoadingListPlaceholder from './LoadingListPlaceholder' +import Searchable from './Searchable' +import fetcher from '../fetcher' + +export default function Searchables({ word }) { + const per = 50 + const { page, setPage } = usePage() + const params = new URLSearchParams(location.search) + const url = `/api/searchables.json?${params}` + const { data, error } = useSWR(url, fetcher) + + if (error) { + console.warn(error) + return
failed to load
+ } + + return ( +
+ {!data && ( +
+ +
+ )} + + {data?.searchables.length === 0 && ( +
+
+
+ +
+

+ {word}に一致する情報は見つかりませんでした。 +

+
+
+ )} + + {data && data.searchables.length > 0 && ( +
+ {data.total_pages > 1 && ( + + )} +
+ {data.searchables.map((searchable) => ( + + ))} +
+ {data.total_pages > 1 && ( + + )} +
+ )} +
+ ) +} diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 9fde0cc59c7..9263c366460 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -34,7 +34,6 @@ import '../courses-practices.js' import '../no_learn.js' import '../survey_question.js' import '../survey.js' -import '../searchables.js' import '../niconico_calendar.js' import '../mentor-mode.js' import '../bookmark.js' diff --git a/app/javascript/searchables.js b/app/javascript/searchables.js deleted file mode 100644 index 0932779df8e..00000000000 --- a/app/javascript/searchables.js +++ /dev/null @@ -1,17 +0,0 @@ -import Vue from 'vue' -import Searchables from 'searchables.vue' - -document.addEventListener('DOMContentLoaded', () => { - const selector = '#js-searchables' - const searchables = document.querySelector(selector) - if (searchables) { - const documentType = searchables.getAttribute('document_type') - const word = searchables.getAttribute('word') - new Vue({ - render: (h) => - h(Searchables, { - props: { documentType: documentType, word: word } - }) - }).$mount(selector) - } -}) diff --git a/app/javascript/stylesheets/application/blocks/user/_a-list-item-badge.sass b/app/javascript/stylesheets/application/blocks/user/_a-list-item-badge.sass index 3ecbbd833c7..e6abdbe57cc 100644 --- a/app/javascript/stylesheets/application/blocks/user/_a-list-item-badge.sass +++ b/app/javascript/stylesheets/application/blocks/user/_a-list-item-badge.sass @@ -20,7 +20,7 @@ &.is-unread background-color: var(--danger) color: var(--reversal-text) - &.is-serchable + &.is-searchable border: solid 1px var(--muted-text) color: var(--muted-text) &.is-ended diff --git a/app/javascript/stylesheets/application/blocks/user/_user-metas.sass b/app/javascript/stylesheets/application/blocks/user/_user-metas.sass index 43e85af80e5..a97f747298a 100644 --- a/app/javascript/stylesheets/application/blocks/user/_user-metas.sass +++ b/app/javascript/stylesheets/application/blocks/user/_user-metas.sass @@ -53,3 +53,8 @@ margin-top: 0 *:last-child margin-bottom: 0 + +.user-metas__item-value-text + &.is-danger + color: $danger + font-weight: 700 diff --git a/app/javascript/stylesheets/atoms/_a-list-item-badge.sass b/app/javascript/stylesheets/atoms/_a-list-item-badge.sass index 3ecbbd833c7..e6abdbe57cc 100644 --- a/app/javascript/stylesheets/atoms/_a-list-item-badge.sass +++ b/app/javascript/stylesheets/atoms/_a-list-item-badge.sass @@ -20,7 +20,7 @@ &.is-unread background-color: var(--danger) color: var(--reversal-text) - &.is-serchable + &.is-searchable border: solid 1px var(--muted-text) color: var(--muted-text) &.is-ended diff --git a/app/models/cache.rb b/app/models/cache.rb index b9656ab5565..b2353db6b19 100644 --- a/app/models/cache.rb +++ b/app/models/cache.rb @@ -34,7 +34,7 @@ def delete_unassigned_product_count def self_assigned_no_replied_product_count(user_id) Rails.cache.fetch("#{user_id}-self_assigned_no_replied_product_count") do - Product.self_assigned_no_replied_products(user_id).unchecked.count + Product.unhibernated_user_products.self_assigned_no_replied_products(user_id).unchecked.count end end diff --git a/app/models/learning_cache_destroyer.rb b/app/models/learning_cache_destroyer.rb index 47b9ff43df8..e62c56939a1 100644 --- a/app/models/learning_cache_destroyer.rb +++ b/app/models/learning_cache_destroyer.rb @@ -4,5 +4,6 @@ class LearningCacheDestroyer def call(payload) user = payload[:user] Rails.cache.delete "/model/user/#{user.id}/completed_percentage" + Rails.logger.info "[LearningCacheDestroyer] Cache destroyed for user #{user.id}" end end diff --git a/app/models/link_checker/extractor.rb b/app/models/link_checker/extractor.rb index 36c082935a1..cdff6ff2edd 100644 --- a/app/models/link_checker/extractor.rb +++ b/app/models/link_checker/extractor.rb @@ -2,7 +2,7 @@ module LinkChecker module Extractor - MARKDOWN_LINK_REGEXP = %r{\[(.*?)\]\((#{URI::DEFAULT_PARSER.make_regexp}|/.*?)\)}.freeze + MARKDOWN_LINK_REGEXP = %r{\[(.*?)\]\((#{URI::DEFAULT_PARSER.make_regexp}|/.*?)\)} module_function diff --git a/app/models/user.rb b/app/models/user.rb index 9aeb1475626..db2d4fd8283 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -770,6 +770,10 @@ def delete_and_assign_new_organizer organizers.each(&:delete_and_assign_new) end + def scheduled_retire_at + hibernated_at&.advance(months: 6) + end + private def password_required? diff --git a/app/views/articles/_form.html.slim b/app/views/articles/_form.html.slim index f8e44c94a9e..bfa53ca8ddb 100644 --- a/app/views/articles/_form.html.slim +++ b/app/views/articles/_form.html.slim @@ -37,9 +37,11 @@ .form-item .row .col-md-6.col-xs-12 - = f.label :tag_list, 'タグを入力してください(カンマ区切り)', + = f.label :tag_list, 'タグを入力してください', class: 'a-form-label' = render 'tags_input', taggable: article + .a-form-help + p 入力してエンターキーを押すとタグになります(スペースは入力できません)。 .form-item .row diff --git a/app/views/articles/index.html.slim b/app/views/articles/index.html.slim index adc8b8a1040..a8a24cf14f8 100644 --- a/app/views/articles/index.html.slim +++ b/app/views/articles/index.html.slim @@ -19,6 +19,7 @@ description: 'オンラインプログラミングフィヨルドブートキャ .articles .articles__body .container.is-xl + = paginate @articles .articles__items .row - @articles.each do |article| diff --git a/app/views/courses/books/index.html.slim b/app/views/courses/books/index.html.slim index f7277ca4498..e676bbec496 100644 --- a/app/views/courses/books/index.html.slim +++ b/app/views/courses/books/index.html.slim @@ -14,14 +14,13 @@ header.page-header i.fas.fa-plus | 参考書籍登録 = render 'courses/tabs' -hr.a-border - .page-body - .container.is-md - nav.pill-nav - .container - ul.pill-nav__items - li.pill-nav__item - = link_to '全て', course_books_path, class: "pill-nav__item-link #{params[:status] == 'mustread' ? '' : 'is-active'}" - li.pill-nav__item - = link_to '必読', course_books_path(status: 'mustread'), class: "pill-nav__item-link #{params[:status] == 'mustread' ? 'is-active' : ''}" +.page-body + .container.is-md + nav.pill-nav + .container + ul.pill-nav__items + li.pill-nav__item + = link_to '全て', course_books_path, class: "pill-nav__item-link #{params[:status] == 'mustread' ? '' : 'is-active'}" + li.pill-nav__item + = link_to '必読', course_books_path(status: 'mustread'), class: "pill-nav__item-link #{params[:status] == 'mustread' ? 'is-active' : ''}" div(data-vue="CourseBooks" data-vue-is-admin:boolean="#{current_user.admin?}" data-vue-is-mentor:boolean="#{current_user.mentor?}" data-vue-course:json="#{@course.to_json}") diff --git a/app/views/products/_tabs.html.slim b/app/views/products/_tabs.html.slim index f61827962d7..89414700d14 100644 --- a/app/views/products/_tabs.html.slim +++ b/app/views/products/_tabs.html.slim @@ -15,7 +15,7 @@ = Cache.unassigned_product_count li.page-tabs__item = link_to products_self_assigned_index_path, class: "page-tabs__item-link #{current_link(/^products-self_assigned-index/)}" do - | 自分の担当 (#{Product.self_assigned_product(current_user.id).unchecked.size}) + | 自分の担当 (#{Product.unhibernated_user_products.self_assigned_product(current_user.id).unchecked.size}) - if Cache.self_assigned_no_replied_product_count(current_user.id).positive? .page-tabs__item-count.a-notification-count.is-only-mentor = Cache.self_assigned_no_replied_product_count(current_user.id) diff --git a/app/views/reports/index.html.slim b/app/views/reports/index.html.slim index b9cd2abb6a3..4a45d0bced7 100644 --- a/app/views/reports/index.html.slim +++ b/app/views/reports/index.html.slim @@ -14,4 +14,5 @@ header.page-header = render 'reports/tabs' -= react_component('Reports', all: true, practices: current_user.practices) +- practices = current_user.practices.as_json(only: %i[id title]) += react_component('Reports', all: true, practices: practices) diff --git a/app/views/searchables/index.html.slim b/app/views/searchables/index.html.slim index aebbade6f68..0a6e60d2b3d 100644 --- a/app/views/searchables/index.html.slim +++ b/app/views/searchables/index.html.slim @@ -7,4 +7,4 @@ header.page-header h2.page-header__title = title hr.a-border -#js-searchables(document_type="#{params[:document_type]}" word="#{params[:word]}") + = react_component('Searchables', word: params[:word].to_s) diff --git a/app/views/users/_hibernation_info.html.slim b/app/views/users/_hibernation_info.html.slim index 82b5b5279dd..d8508019a53 100644 --- a/app/views/users/_hibernation_info.html.slim +++ b/app/views/users/_hibernation_info.html.slim @@ -6,6 +6,15 @@ .user-metas__item-label 最後に休会した日時 .user-metas__item-value = l user.hibernated_at + .user-metas__item + .user-metas__item-label 休会期限日時 + .user-metas__item-value + - if !user.auto_retire + span.user-metas__item-value-text + | 企業都合休会中(#{user.hibernation_days}日) + - else + span.user-metas__item-value-text(class=user.countdown_danger_tag) + = user.retire_deadline - user.hibernations.each_with_index do |hibernation, i| - number = i + 1 .user-metas__items diff --git a/app/views/users/_metas.html.slim b/app/views/users/_metas.html.slim index 6e7d6597c0c..86be86806ec 100644 --- a/app/views/users/_metas.html.slim +++ b/app/views/users/_metas.html.slim @@ -18,6 +18,14 @@ | コース .user-metas__item-value = link_to user.course.title, course_practices_path(user.course), target: '_blank', rel: 'noopener' + .user-metas__item + .user-metas__item-label + | マシンのOS + .user-metas__item-value + - if user.os + = t("activerecord.enums.user.os.#{user.os}") + - else + | 回答なし .user-metas__item .user-metas__item-label | 日報 diff --git a/app/views/users/_user_secret_attributes.html.slim b/app/views/users/_user_secret_attributes.html.slim index d35917a9f7e..0a85a06e172 100644 --- a/app/views/users/_user_secret_attributes.html.slim +++ b/app/views/users/_user_secret_attributes.html.slim @@ -33,14 +33,6 @@ = t("activerecord.enums.user.job.#{user.job}") - else | 回答なし - .user-metas__item - .user-metas__item-label - | マシンのOS - .user-metas__item-value - - if user.os - = t("activerecord.enums.user.os.#{user.os}") - - else - | 回答なし .user-metas__item .user-metas__item-label | 経験 diff --git a/app/views/users/form/_os.html.slim b/app/views/users/form/_os.html.slim index afa3531639b..88a04b84ea5 100644 --- a/app/views/users/form/_os.html.slim +++ b/app/views/users/form/_os.html.slim @@ -1,5 +1,9 @@ .form-item = f.label :os, '学習に使うマシン・OS を選択してください', class: 'a-form-label is-required' + .a-form-help.mb-4 + p + | この情報は他のフィヨルドブートキャンプ参加者に公開されます。 + | 学習に使うマシン・OS が変わりましたら、この情報を更新してください。 .form-item__groups .form-item-group .form-item-group__header diff --git a/app/views/welcome/law.html.slim b/app/views/welcome/law.html.slim index 3fc1137701c..d541463ceb2 100644 --- a/app/views/welcome/law.html.slim +++ b/app/views/welcome/law.html.slim @@ -30,7 +30,7 @@ header.welcome-page-header | ※この電話番号からのご利用方法お問い合わせや営業はお受けできません。 tr th E-mail - td info@fjord.jp + td info@lokka.jp tr th 役務の提供の時期 td diff --git a/bin/lint b/bin/lint index 6c9e377111e..4ee9a6df08c 100755 --- a/bin/lint +++ b/bin/lint @@ -1,5 +1,6 @@ #!/bin/bash +set -e bundle exec rubocop -a bundle exec slim-lint app/views -c config/slim_lint.yml bin/yarn lint diff --git a/config/slim_lint.yml b/config/slim_lint.yml index 6f95deb3a15..8e1ea7b0fb1 100644 --- a/config/slim_lint.yml +++ b/config/slim_lint.yml @@ -8,7 +8,6 @@ linters: enabled: true ignored_cops: - Layout/ArgumentAlignment - - Layout/ArrayAlignment - Layout/BlockAlignment - Layout/EmptyLineAfterGuardClause - Layout/EndAlignment @@ -22,7 +21,6 @@ linters: - Layout/MultilineArrayBraceLayout - Layout/MultilineAssignmentLayout - Layout/MultilineHashBraceLayout - - Layout/MultilineMethodCallBraceLayout - Layout/MultilineMethodCallIndentation - Layout/MultilineMethodDefinitionBraceLayout - Layout/MultilineOperationIndentation diff --git a/db/fixtures/discord_profiles.yml b/db/fixtures/discord_profiles.yml index c29560d2acc..0197813a856 100644 --- a/db/fixtures/discord_profiles.yml +++ b/db/fixtures/discord_profiles.yml @@ -225,6 +225,31 @@ discord_profile_kyuukai: account_name: times_url: +discord_profile_autoretire-within-1-hour: + user: autoretire-within-1-hour + account_name: + times_url: + +discord_profile_autoretire-within-24-hour: + user: autoretire-within-24-hour + account_name: + times_url: + +discord_profile_autoretire-within-1-week: + user: autoretire-within-1-week + account_name: + times_url: + +discord_profile_autoretire-over-1-week: + user: autoretire-over-1-week + account_name: + times_url: + +discord_profile_not-autoretire: + user: not-autoretire + account_name: + times_url: + discord_profile_sotsugyoukigyoshozoku: user: sotsugyoukigyoshozoku account_name: diff --git a/db/fixtures/talks.yml b/db/fixtures/talks.yml index 15efbd6d02e..50383dd2345 100644 --- a/db/fixtures/talks.yml +++ b/db/fixtures/talks.yml @@ -191,3 +191,23 @@ talk_advisernocolleguetrainee: talk_nagai-kyuukai: user: nagai-kyuukai action_completed: true + +talk_autoretire-within-1-hour: + user: autoretire-within-1-hour + action_completed: true + +talk_autoretire-within-24-hour: + user: autoretire-within-24-hour + action_completed: true + +talk_autoretire-within-1-week: + user: autoretire-within-1-week + action_completed: true + +talk_autoretire-over-1-week: + user: autoretire-over-1-week + action_completed: true + +talk_not-autoretire: + user: not-autoretire + action_completed: true diff --git a/db/fixtures/users.yml b/db/fixtures/users.yml index 046c987edbb..073a04284fa 100644 --- a/db/fixtures/users.yml +++ b/db/fixtures/users.yml @@ -1051,6 +1051,147 @@ kyuukai: created_at: "2014-01-01 00:00:13" sent_student_followup_message: true +autoretire-within-1-hour: + login_name: autoretire-within-1-hour + email: autoretire-within-1-hour@fjord.jp + crypted_password: $2a$10$n/xv4/1luueN6plzm2OyDezWlZFyGHjQEf4hwAW1r3k.lCm0frPK. # testtest + salt: zW3kQ9ubsxQQtzzzs4ap + name: autoretire-within-1-hour + name_kana: 自動退会まで1時間のユーザー + customer_id: cus_12345678 + subscription_id: sub_12345678 + twitter_account: autoretire-within-1-hour + facebook_url: https://www.facebook.com/fjordllc/autoretire-within-1-hour + blog_url: https://example.com/autoretire-within-1-hour + description: "1時間以内に自動退会するユーザーです。" + customer_id: cus_LZ2wCJqYybuDlJ + subscription_id: sub_12345678 + course: course1 + job: office_worker + os: mac + experience: inexperienced + country_code: JP + subdivision_code: '04' + github_account: autoretire-within-1-hour + unsubscribe_email_token: k3a49_NwgTsiJS0oHGU2Fw + hibernated_at: <%= Time.current - 6.months + 30.minutes %> + updated_at: "2014-01-01 00:00:13" + created_at: "2014-01-01 00:00:13" + sent_student_followup_message: true + +autoretire-within-24-hour: + login_name: autoretire-within-24-hour + email: autoretire-within-24-hour@fjord.jp + crypted_password: $2a$10$n/xv4/1luueN6plzm2OyDezWlZFyGHjQEf4hwAW1r3k.lCm0frPK. # testtest + salt: zW3kQ9ubsxQQtzzzs4ap + name: autoretire-within-24-hour + name_kana: 自動退会まで24時間のユーザー + customer_id: cus_12345678 + subscription_id: sub_12345678 + twitter_account: autoretire-within-24-hour + facebook_url: https://www.facebook.com/fjordllc/autoretire-within-24-hour + blog_url: https://example.com/autoretire-within-24-hour + description: "24時間以内に自動退会するユーザーです。" + customer_id: cus_LZ2wCJqYybuDlJ + subscription_id: sub_12345678 + course: course1 + job: office_worker + os: mac + experience: inexperienced + country_code: JP + subdivision_code: '04' + github_account: autoretire-within-24-hour + unsubscribe_email_token: k3a49_NwgTsiJS0oHGU2Fw + hibernated_at: <%= Time.current - 6.months + 23.hour%> + updated_at: "2014-01-01 00:00:13" + created_at: "2014-01-01 00:00:13" + sent_student_followup_message: true + +autoretire-within-1-week: + login_name: autoretire-within-1-week + email: autoretire-within-1-week@fjord.jp + crypted_password: $2a$10$n/xv4/1luueN6plzm2OyDezWlZFyGHjQEf4hwAW1r3k.lCm0frPK. # testtest + salt: zW3kQ9ubsxQQtzzzs4ap + name: autoretire-within-1-week + name_kana: 自動退会まで1週間のユーザー + customer_id: cus_12345678 + subscription_id: sub_12345678 + twitter_account: autoretire-within-1-week + facebook_url: https://www.facebook.com/fjordllc/autoretire-within-1-week + blog_url: https://example.com/autoretire-within-1-week + description: "1週間以内に自動退会するユーザーです。" + customer_id: cus_LZ2wCJqYybuDlJ + subscription_id: sub_12345678 + course: course1 + job: office_worker + os: mac + experience: inexperienced + country_code: JP + subdivision_code: '04' + github_account: autoretire-within-1-week + unsubscribe_email_token: k3a49_NwgTsiJS0oHGU2Fw + hibernated_at: <%= Time.current - 6.months + 6.days%> + updated_at: "2014-01-01 00:00:13" + created_at: "2014-01-01 00:00:13" + sent_student_followup_message: true + +autoretire-over-1-week: + login_name: autoretire-over-1-week + email: autoretire-over-1-week@fjord.jp + crypted_password: $2a$10$n/xv4/1luueN6plzm2OyDezWlZFyGHjQEf4hwAW1r3k.lCm0frPK. # testtest + salt: zW3kQ9ubsxQQtzzzs4ap + name: autoretire-over-1-week + name_kana: 自動退会まで1週間以上のユーザー + customer_id: cus_12345678 + subscription_id: sub_12345678 + twitter_account: autoretire-over-1-week + facebook_url: https://www.facebook.com/fjordllc/autoretire-over-1-week + blog_url: https://example.com/autoretire-over-1-week + description: "自動退会まで1週間以上のユーザーです。" + customer_id: cus_LZ2wCJqYybuDlJ + subscription_id: sub_12345678 + course: course1 + job: office_worker + os: mac + experience: inexperienced + country_code: JP + subdivision_code: '04' + github_account: autoretire-over-1-week + unsubscribe_email_token: k3a49_NwgTsiJS0oHGU2Fw + hibernated_at: <%= Time.current - 6.months + 3.months%> + updated_at: "2014-01-01 00:00:13" + created_at: "2014-01-01 00:00:13" + sent_student_followup_message: true + +not-autoretire: + login_name: not-autoretire + email: not-autoretire@fjord.jp + crypted_password: $2a$10$n/xv4/1luueN6plzm2OyDezWlZFyGHjQEf4hwAW1r3k.lCm0frPK. # testtest + salt: zW3kQ9ubsxQQtzzzs4ap + name: not-autoretire + name_kana: 自動退会しないユーザー + customer_id: cus_12345678 + subscription_id: sub_12345678 + twitter_account: not-autoretire + facebook_url: https://www.facebook.com/fjordllc/not-autoretire + blog_url: https://example.com/not-autoretire + description: "自動退会しないユーザーです。" + customer_id: cus_LZ2wCJqYybuDlJ + subscription_id: sub_12345678 + course: course1 + job: office_worker + os: mac + experience: inexperienced + country_code: JP + subdivision_code: '04' + github_account: not-autoretire + unsubscribe_email_token: k3a49_NwgTsiJS0oHGU2Fw + hibernated_at: <%= Time.current - 6.months%> + updated_at: "2014-01-01 00:00:13" + created_at: "2014-01-01 00:00:13" + sent_student_followup_message: true + auto_retire: false + sotsugyoukigyoshozoku: login_name: sotsugyoukigyoshozoku email: sotsugyoukigyoshozoku@example.com diff --git a/test/active_decorator_test_case.rb b/test/active_decorator_test_case.rb new file mode 100644 index 00000000000..fb55ed66726 --- /dev/null +++ b/test/active_decorator_test_case.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ActiveDecoratorTestCase < ActiveSupport::TestCase + include ActionView::TestCase::Behavior + + setup do + ActiveDecorator::ViewContext.push(controller.view_context) + end + + private + + def decorate(instance) + ActiveDecorator::Decorator.instance.decorate(instance) + end +end diff --git a/test/decorators/book_decorator_test.rb b/test/decorators/book_decorator_test.rb index 17b50b9e408..6f9b1b86160 100644 --- a/test/decorators/book_decorator_test.rb +++ b/test/decorators/book_decorator_test.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true require 'test_helper' +require 'active_decorator_test_case' -class BookDecoratorTest < ActiveSupport::TestCase +class BookDecoratorTest < ActiveDecoratorTestCase setup do - @book1 = ActiveDecorator::Decorator.instance.decorate(books(:book1)) - @book2 = ActiveDecorator::Decorator.instance.decorate(books(:book2)) + @book1 = decorate(books(:book1)) + @book2 = decorate(books(:book2)) end test '#must_read_for_any_practices?' do diff --git a/test/decorators/bookmark_decorator_test.rb b/test/decorators/bookmark_decorator_test.rb index 268fcf5db5b..2eed0f36cd0 100644 --- a/test/decorators/bookmark_decorator_test.rb +++ b/test/decorators/bookmark_decorator_test.rb @@ -1,21 +1,13 @@ # frozen_string_literal: true require 'test_helper' +require 'active_decorator_test_case' -class BookmarkDecoratorTest < ActiveSupport::TestCase - def setup - @bookmark31 = ActiveDecorator::Decorator.instance.decorate(bookmarks(:bookmark31)) - @bookmark30 = ActiveDecorator::Decorator.instance.decorate(bookmarks(:bookmark30)) - @bookmark29 = ActiveDecorator::Decorator.instance.decorate(bookmarks(:bookmark29)) - @bookmark28 = ActiveDecorator::Decorator.instance.decorate(bookmarks(:bookmark28)) - @bookmark27 = ActiveDecorator::Decorator.instance.decorate(bookmarks(:bookmark27)) - end - +class BookmarkDecoratorTest < ActiveDecoratorTestCase test '#reported_on_or_created_at' do - assert_equal I18n.l(bookmarks(:bookmark31).bookmarkable.created_at), I18n.l(@bookmark31.reported_on_or_created_at) - assert_equal I18n.l(bookmarks(:bookmark30).bookmarkable.created_at), I18n.l(@bookmark30.reported_on_or_created_at) - assert_equal I18n.l(bookmarks(:bookmark29).bookmarkable.created_at), I18n.l(@bookmark29.reported_on_or_created_at) - assert_equal I18n.l(bookmarks(:bookmark28).bookmarkable.created_at), I18n.l(@bookmark28.reported_on_or_created_at) - assert_equal I18n.l(bookmarks(:bookmark27).bookmarkable.reported_on), I18n.l(@bookmark27.reported_on_or_created_at) + bookmark31 = decorate(bookmarks(:bookmark31)) + bookmark27 = decorate(bookmarks(:bookmark27)) + assert_equal bookmark31.bookmarkable.created_at, bookmark31.reported_on_or_created_at + assert_equal bookmark27.bookmarkable.reported_on, bookmark27.reported_on_or_created_at end end diff --git a/test/decorators/company_decorator_test.rb b/test/decorators/company_decorator_test.rb index e61da7772e7..1b534aa9d86 100644 --- a/test/decorators/company_decorator_test.rb +++ b/test/decorators/company_decorator_test.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true require 'test_helper' +require 'active_decorator_test_case' -class CompanyDecoratorTest < ActiveSupport::TestCase - def setup +class CompanyDecoratorTest < ActiveDecoratorTestCase + setup do controller = ApplicationController.new controller.request = ActionDispatch::TestRequest.create ActiveDecorator::ViewContext.push controller.view_context - @company1 = ActiveDecorator::Decorator.instance.decorate(companies(:company1)) + @company1 = decorate(companies(:company1)) end test '#adviser_sign_up_url' do diff --git a/test/decorators/user_decorator/retire_test.rb b/test/decorators/user_decorator/retire_test.rb new file mode 100644 index 00000000000..94dd37b9d68 --- /dev/null +++ b/test/decorators/user_decorator/retire_test.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'test_helper' +require 'active_decorator_test_case' + +module UserDecorator + class RetireTest < ActiveDecoratorTestCase + setup do + @student = decorate(users(:hajime)) + @hibernationed = decorate(users(:kyuukai)) + end + + test '#retire_countdown' do + travel_to Time.zone.local(2020, 6, 24, 9, 0, 0) do + assert_equal 1.week, @hibernationed.retire_countdown + assert_nil @student.retire_countdown + end + end + + test '#retire_deadline wihin 1 hour' do + travel_to Time.zone.local(2020, 7, 1, 8, 1, 0) do + assert_equal '2020年07月01日(水) 09:00 (自動退会まであと59分)', @hibernationed.retire_deadline + end + end + + test '#retire_deadline within 24 hours' do + travel_to Time.zone.local(2020, 6, 30, 10, 0, 0) do + assert_equal '2020年07月01日(水) 09:00 (自動退会まであと23時間)', @hibernationed.retire_deadline + end + end + + test '#retire_deadline within 1 week' do + travel_to Time.zone.local(2020, 6, 24, 9, 0, 0) do + assert_equal '2020年07月01日(水) 09:00 (自動退会まであと7日)', @hibernationed.retire_deadline + end + end + + test '#retire_deadline over 1 week' do + travel_to Time.zone.local(2020, 1, 1, 9, 0, 0) do + assert_equal '2020年07月01日(水) 09:00 (自動退会まであと182日)', @hibernationed.retire_deadline + end + end + + test '#countdown_danger_tag' do + travel_to Time.zone.local(2020, 7, 1, 8, 1, 0) do + assert_equal 'is-danger', @hibernationed.countdown_danger_tag + end + + travel_to Time.zone.local(2020, 1, 1, 9, 0, 0) do + assert_equal '', @hibernationed.countdown_danger_tag + end + end + end +end diff --git a/test/decorators/user_decorator/role_test.rb b/test/decorators/user_decorator/role_test.rb new file mode 100644 index 00000000000..29b7b2fce9d --- /dev/null +++ b/test/decorators/user_decorator/role_test.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'test_helper' +require 'active_decorator_test_case' + +module UserDecorator + class RoleTest < ActiveDecoratorTestCase + test '#staff_roles' do + admin_mentor = decorate(users(:komagata)) + student = decorate(users(:hajime)) + + assert_equal '管理者、メンター', admin_mentor.staff_roles + assert_equal '', student.staff_roles + end + + test '#roles_to_s' do + admin_mentor = decorate(users(:komagata)) + student = decorate(users(:hajime)) + graduated = decorate(users(:sotugyou)) + admin = decorate(users(:adminonly)) + adviser = decorate(users(:advijirou)) + mentor = decorate(users(:mentormentaro)) + trainee = decorate(users(:kensyu)) + retired = decorate(users(:taikai)) + hibernationed = decorate(users(:kyuukai)) + + assert_equal '管理者、メンター', admin_mentor.roles_to_s + assert_equal '', student.roles_to_s + assert_equal '卒業生', graduated.roles_to_s + assert_equal '管理者', admin.roles_to_s + assert_equal 'アドバイザー', adviser.roles_to_s + assert_equal 'メンター', mentor.roles_to_s + assert_equal '研修生', trainee.roles_to_s + assert_equal '退会ユーザー', retired.roles_to_s + assert_equal '休会ユーザー', hibernationed.roles_to_s + end + end +end diff --git a/test/decorators/user_decorator_test.rb b/test/decorators/user_decorator_test.rb index 4eb514442b5..a4d23e7cbcc 100644 --- a/test/decorators/user_decorator_test.rb +++ b/test/decorators/user_decorator_test.rb @@ -1,29 +1,18 @@ # frozen_string_literal: true require 'test_helper' +require 'active_decorator_test_case' -class UserDecoratorTest < ActiveSupport::TestCase - include ActionView::TestCase::Behavior - - def setup - ActiveDecorator::ViewContext.push(controller.view_context) - @admin_mentor_user = ActiveDecorator::Decorator.instance.decorate(users(:komagata)) - @student_user = ActiveDecorator::Decorator.instance.decorate(users(:hajime)) - @graduated_user = ActiveDecorator::Decorator.instance.decorate(users(:sotugyou)) - @admin_user = ActiveDecorator::Decorator.instance.decorate(users(:adminonly)) - @adviser_user = ActiveDecorator::Decorator.instance.decorate(users(:advijirou)) - @mentor_user = ActiveDecorator::Decorator.instance.decorate(users(:mentormentaro)) - @trainee_user = ActiveDecorator::Decorator.instance.decorate(users(:kensyu)) - @retired_user = ActiveDecorator::Decorator.instance.decorate(users(:taikai)) - @hibernationed_user = ActiveDecorator::Decorator.instance.decorate(users(:kyuukai)) - @japanese_user = ActiveDecorator::Decorator.instance.decorate(users(:kimura)) - @american_user = ActiveDecorator::Decorator.instance.decorate(users(:tom)) - @subdivision_not_registered_user = ActiveDecorator::Decorator.instance.decorate(users(:hatsuno)) - end - - test '#staff_roles' do - assert_equal '管理者、メンター', @admin_mentor_user.staff_roles - assert_equal '', @student_user.staff_roles +class UserDecoratorTest < ActiveDecoratorTestCase + setup do + @admin_mentor_user = decorate(users(:komagata)) + @student_user = decorate(users(:hajime)) + @graduated_user = decorate(users(:sotugyou)) + @mentor_user = decorate(users(:mentormentaro)) + @hibernationed_user = decorate(users(:kyuukai)) + @japanese_user = decorate(users(:kimura)) + @american_user = decorate(users(:tom)) + @subdivision_not_registered_user = decorate(users(:hatsuno)) end test '#icon_title' do @@ -44,18 +33,6 @@ def setup @graduated_user.enrollment_period end - test '#roles_to_s' do - assert_equal '管理者、メンター', @admin_mentor_user.roles_to_s - assert_equal '', @student_user.roles_to_s - assert_equal '卒業生', @graduated_user.roles_to_s - assert_equal '管理者', @admin_user.roles_to_s - assert_equal 'アドバイザー', @adviser_user.roles_to_s - assert_equal 'メンター', @mentor_user.roles_to_s - assert_equal '研修生', @trainee_user.roles_to_s - assert_equal '退会ユーザー', @retired_user.roles_to_s - assert_equal '休会ユーザー', @hibernationed_user.roles_to_s - end - test '#subdivisions_of_country' do assert_includes @japanese_user.subdivisions_of_country, %w[北海道 01] assert_includes @american_user.subdivisions_of_country, %w[アラスカ州 AK] @@ -66,4 +43,11 @@ def setup assert_equal 'ニューヨーク州 (米国)', @american_user.address assert_equal '日本', @subdivision_not_registered_user.address end + + test '#hibernation_days' do + travel_to Time.zone.local(2020, 2, 1, 9, 0, 0) do + assert_equal 31, @hibernationed_user.hibernation_days + assert_nil @student_user.hibernation_days + end + end end diff --git a/test/integration/api/products/unassigned_counts_test.rb b/test/integration/api/products/unassigned_counts_test.rb index 03d52d6e2ae..d463298ec8c 100644 --- a/test/integration/api/products/unassigned_counts_test.rb +++ b/test/integration/api/products/unassigned_counts_test.rb @@ -6,7 +6,7 @@ class API::Products::UnassignedTextTest < ActionDispatch::IntegrationTest fixtures :products test 'GET /api/products/unassigned/counts.txt' do - products(:product15).update_column(:checker_id, nil) # rubocop:disable Rails/SkipsModelValidations + products(:product15).update_column(:checker_id, nil) # rubocop:disable Rails/SkipsModelValidations get counts_api_products_unassigned_index_path(format: :text) assert_response :unauthorized diff --git a/test/models/user_test.rb b/test/models/user_test.rb index e3d9eccb3ef..59f4e766029 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -703,4 +703,9 @@ class UserTest < ActiveSupport::TestCase user.delete_and_assign_new_organizer end end + + test '#scheduled_retire_at' do + assert_equal '2020-07-01 09:00:00 +0900', users(:kyuukai).scheduled_retire_at.to_s + assert_nil users(:hatsuno).scheduled_retire_at + end end diff --git a/test/system/articles_test.rb b/test/system/articles_test.rb index 85c13a73f98..2c6428f32b0 100644 --- a/test/system/articles_test.rb +++ b/test/system/articles_test.rb @@ -134,7 +134,7 @@ class ArticlesTest < ApplicationSystemTestCase end visit_with_auth articles_url, 'komagata' - find 'nav.pagination' + assert_selector 'nav.pagination', count: 2 end test "general user can't see edit and delete buttons" do diff --git a/test/system/product/self_assigned_test.rb b/test/system/product/self_assigned_test.rb index c15581a1a16..fac94ec2387 100644 --- a/test/system/product/self_assigned_test.rb +++ b/test/system/product/self_assigned_test.rb @@ -147,4 +147,65 @@ class Product::SelfAssignedTest < ApplicationSystemTestCase visit_with_auth '/products/self_assigned?target=self_assigned_no_replied', 'mentormentaro' assert_text '未返信の担当提出物はありません' end + + test "the number of products displayed in a self assigned tab excludes hibernated users' products" do + checker = users(:mentormentaro) + + # 担当中かつ未返信の提出物が0個のとき、「自分の担当」タブの右肩の赤い数字がそもそも表示されないという状況を防ぐため、この時点で提出物を1件作成 + Product.create!( + body: 'test', + user: users(:kimura), + practice: practices(:practice5), + checker_id: checker.id + ) + + self_assigned_count = Product.unhibernated_user_products.self_assigned_product(checker.id).unchecked.count + self_assigned_no_replied_count = Product.unhibernated_user_products.self_assigned_no_replied_products(checker.id).unchecked.count + + visit_with_auth '/products/self_assigned', 'mentormentaro' + assert_selector '.page-tabs__item-link.is-active', text: "自分の担当 (#{self_assigned_count})" + assert_selector '.page-tabs__item-count.a-notification-count.is-only-mentor', text: self_assigned_no_replied_count.to_s + + Product.create!( + body: 'test', + user: users(:kyuukai), + practice: practices(:practice5), + checker_id: checker.id + ) + + visit_with_auth '/products/self_assigned', 'mentormentaro' + assert_selector '.page-tabs__item-link.is-active', text: "自分の担当 (#{self_assigned_count})" + assert_selector '.page-tabs__item-count.a-notification-count.is-only-mentor', text: self_assigned_no_replied_count.to_s + end + + test "not display hibernated users' products in a self assigned products list" do + checker = users(:mentormentaro) + unhibernated_user = users(:kimura) + hibernated_user = users(:kyuukai) + practice = practices(:practice5) + + Product.create!( + body: 'test', + user: unhibernated_user, + practice: practice, + checker_id: checker.id + ) + Product.create!( + body: 'test', + user: hibernated_user, + practice: practice, + checker_id: checker.id + ) + + unhibernated_users_displayed_name = "#{unhibernated_user.login_name} (#{unhibernated_user.name_kana})" + hibernated_users_displayed_name = "#{hibernated_user.login_name} (#{hibernated_user.name_kana})" + + visit_with_auth '/products/self_assigned?target=self_assigned_all', 'mentormentaro' + assert_text unhibernated_users_displayed_name + assert_no_text hibernated_users_displayed_name + + visit_with_auth '/products/self_assigned?target=self_assigned_no_replied', 'mentormentaro' + assert_text unhibernated_users_displayed_name + assert_no_text hibernated_users_displayed_name + end end