diff --git a/app/controllers/announcements_controller.rb b/app/controllers/announcements_controller.rb index 95615c58c6a..c1dbf079eb3 100644 --- a/app/controllers/announcements_controller.rb +++ b/app/controllers/announcements_controller.rb @@ -1,10 +1,17 @@ # frozen_string_literal: true class AnnouncementsController < ApplicationController + PAGER_NUMBER = 25 before_action :set_announcement, only: %i[show edit update destroy] before_action :rewrite_announcement, only: %i[update] - def index; end + def index + @announcements = Announcement.with_avatar + .preload(:comments) + .order(published_at: :desc, created_at: :desc) + .page(params[:page]) + .per(PAGER_NUMBER) + end def show @announcements = Announcement.with_avatar.where(wip: false).order(published_at: :desc).limit(10) @@ -14,23 +21,21 @@ def new @announcement = Announcement.new(target: 'students') if params[:id] - copy_announcement(params[:id]) + @announcement = Announcement.copy_announcement(params[:id]) + flash.now[:notice] = 'お知らせをコピーしました。' elsif params[:page_id] page = Page.find(params[:page_id]) - copy_template_by_resource('page_announcements.yml', page:) + @announcement = Announcement.copy_template_by_resource('page_announcements.yml', page:) elsif params[:event_id] event = Event.find(params[:event_id]) - copy_template_by_resource('event_announcements.yml', event:) + @announcement = Announcement.copy_template_by_resource('event_announcements.yml', event:) elsif params[:regular_event_id] regular_event = RegularEvent.find(params[:regular_event_id]) - organizers = regular_event.organizers.map { |organizer| "@#{organizer.login_name}" }.join("\n - ") - holding_cycles = ActiveDecorator::Decorator.instance.decorate(regular_event).holding_cycles - hold_national_holiday = "(祝日#{regular_event.hold_national_holiday ? 'も開催' : 'は休み'})" - copy_template_by_resource('regular_event_announcements.yml', - regular_event:, - organizers:, - holding_cycles:, - hold_national_holiday:) + params = { regular_event:, + organizers: regular_event.organizers.map { |organizer| "@#{organizer.login_name}" }.join("\n - "), + holding_cycles: ActiveDecorator::Decorator.instance.decorate(regular_event).holding_cycles, + hold_national_holiday: "(祝日#{regular_event.hold_national_holiday ? 'も開催' : 'は休み'})" } + @announcement = Announcement.copy_template_by_resource('regular_event_announcements.yml', params) end end @@ -107,18 +112,4 @@ def set_entered_announcement description: params['announcement']['description'], \ target: params['announcement']['target']) end - - def copy_announcement(announcement_id) - announcement = Announcement.find(announcement_id) - @announcement.title = announcement.title - @announcement.description = announcement.description - @announcement.target = announcement.target - flash.now[:notice] = 'お知らせをコピーしました。' - end - - def copy_template_by_resource(template_file, params = {}) - template = MessageTemplate.load(template_file, params:) - @announcement.title = template['title'] - @announcement.description = template['description'] - end end diff --git a/app/controllers/api/announcements_controller.rb b/app/controllers/api/announcements_controller.rb deleted file mode 100644 index b1761fce3e7..00000000000 --- a/app/controllers/api/announcements_controller.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -class API::AnnouncementsController < API::BaseController - before_action :require_admin_login_for_api, only: %i[destroy] - before_action :set_announcement, only: %i[show update destroy] - protect_from_forgery except: %i[create update] - - def index - @announcements = Announcement.with_avatar - .preload(:comments) - .order(published_at: :desc, created_at: :desc) - .page(params[:page]) - end - - def show; end - - def update - # announcement_params['wip']はユーザーが入力をするので、(文字列で'false'入力した場合等) - # boolean型のtrueの時のみ「更新」が出来る様、明示的にtureを書き込んでいます。 - if !current_user.admin? && announcement_params['wip'] != true - head :bad_request - elsif @announcement.update(announcement_params) - head :ok - else - head :bad_request - end - end - - def create - @announcement = Announcement.new(announcement_params) - @announcement.user_id = current_user.id - @announcement.wip = true unless current_user.admin? - if @announcement.save - render :create, status: :created - else - head :bad_request - end - end - - def destroy - @announcement.destroy - end - - private - - def announcement_params - params.fetch(:announcement).permit(:title, :description, :target, :wip) - end - - def set_announcement - @announcement = Announcement.find(params[:id]) - end -end diff --git a/app/javascript/components/announcement.vue b/app/javascript/components/announcement.vue deleted file mode 100644 index 87850cd3a6d..00000000000 --- a/app/javascript/components/announcement.vue +++ /dev/null @@ -1,50 +0,0 @@ - - diff --git a/app/javascript/components/announcements.vue b/app/javascript/components/announcements.vue deleted file mode 100644 index d4f52919790..00000000000 --- a/app/javascript/components/announcements.vue +++ /dev/null @@ -1,137 +0,0 @@ - - - diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 9ee5b8eb3bf..9f15579527f 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -58,7 +58,6 @@ import '../editor-selection-form.js' import '../user_mentor_memo.js' import VueMounter from '../VueMounter.js' -import Announcements from '../components/announcements.vue' import Books from '../components/books.vue' import ExternalEntries from '../components/external-entries.vue' import Questions from '../components/questions.vue' @@ -78,7 +77,6 @@ import CourseBooks from '../components/course-books.vue' import '../stylesheets/application' const mounter = new VueMounter() -mounter.addComponent(Announcements) mounter.addComponent(Books) mounter.addComponent(ExternalEntries) mounter.addComponent(Questions) diff --git a/app/models/announcement.rb b/app/models/announcement.rb index ba40f38e02f..c8dd0ebe188 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -25,4 +25,14 @@ class Announcement < ApplicationRecord columns_for_keyword_search :title, :description scope :wip, -> { where(wip: true) } + + def self.copy_announcement(announcement_id) + original = find(announcement_id) + new(title: original.title, description: original.description, target: original.target) + end + + def self.copy_template_by_resource(template_file, params = {}) + template = MessageTemplate.load(template_file, params:) + new(title: template['title'], description: template['description']) + end end diff --git a/app/views/announcements/_announcement.html.slim b/app/views/announcements/_announcement.html.slim index f1e6ff576b8..e95aa467016 100644 --- a/app/views/announcements/_announcement.html.slim +++ b/app/views/announcements/_announcement.html.slim @@ -1,68 +1,34 @@ -.announcement.page-content - header.page-content-header - .page-content-header__start - .page-content-header__user - = render 'users/icon', - user: announcement.user, - link_class: 'page-content-header__user-link', - image_class: 'page-content-header__user-icon' - .page-content-header__end - .page-content-header__row - .page-content-header__before-title - time.a-meta(datetime="#{announcement.created_at.to_datetime}" pubdate='pubdate') - = l announcement.updated_at - h1.page-content-header__title(class="#{announcement.wip? ? 'is-wip' : ''}") +.card-list-item + .card-list-item__inner + .card-list-item__user + = render 'users/icon', user: announcement.user, link_class: 'card-list-item__user-link', image_class: 'card-list-item__user-icon' + .card-list-item__rows + .card-list-item__row + header.card-list-item-title - if announcement.wip? - span.a-title-label.is-wip + span.a-list-item-badge.is-wip(class="#{announcement.wip? ? 'is-wip' : ''}") | WIP - = announcement.title - .page-content-header__row - .page-content-header-metas - .page-content-header-metas__start - .page-content-header-metas__meta + h2.card-list-item-title__title + = link_to announcement, itemprop: 'url', class: 'card-list-item-title__link a-text-link' do + = announcement.title + .card-list-item__row + .card-list-item-meta + .card-list-item-meta__items + .card-list-item-meta__item = link_to announcement.user, class: 'a-user-name' do = announcement.user.long_name - .page-content-header-metas__end - .page-content-header-metas__meta - - length = @announcement.comments.length - a.a-meta(href='#comments' class="#{length.zero? ? 'is-disabled' : ''}") - | コメント( - span#comment_count(class="#{length.zero? ? 'is-muted' : 'is-emphasized'}") - = length - | ) - .page-content-header__row - .page-content-header-actions - .page-content-header-actions__start - .page-content-header-actions__action - div(data-vue="WatchToggle" data-vue-watchable-id:number="#{@announcement.id}" data-vue-watchable-type='Announcement') - .page-content-header-actions__end - .page-content-header-actions__action - = link_to new_announcement_path(id: announcement), class: 'a-button is-sm is-secondary is-block', id: 'copy' do - i.fa-solid.fa-copy - | コピー - - .a-card - .card-body - .card__description - .a-long-text.is-md.js-markdown-view - = announcement.description - hr.a-border-tint - = render 'reactions/reactions', reactionable: @announcement - hr.a-border-tint - .card-footer - .card-main-actions - ul.card-main-actions__items(class="#{announcement.published_at ? 'is-sub-actions' : ''}") - - if announcement.published_at - li.card-main-actions__item.is-sub - = link_to edit_announcement_path(announcement), class: 'card-main-actions__muted-action' do - | 内容修正 - - else - li.card-main-actions__item - = link_to edit_announcement_path(announcement), class: 'card-main-actions__action a-button is-sm is-secondary is-block', id: 'js-shortcut-edit' do - i.fa-solid.fa-pen#new - | 内容修正 - - if admin_or_mentor_login? || @announcement.user_id == current_user.id - li.card-main-actions__item.is-sub - = link_to announcement_path(announcement), data: { confirm: '本当によろしいですか?' }, method: :delete, class: 'card-main-actions__muted-action' do - span#delete - | 削除する + .card-list-item__row + .card-list-item-meta__items + .card-list-item-meta__item + - if announcement.wip? + .a-meta + | お知らせ作成中 + - else + time.a-meta(datetime='announcement.published_at_date_time') + span.a-meta__label + | 公開 + span.a-meta__value + | #{l announcement.published_at} + .card-list-item-meta__item + .a-meta + | コメント(#{announcement.comments.length}) diff --git a/app/views/announcements/index.html.slim b/app/views/announcements/index.html.slim index 4c2142c2050..315ed3b39f4 100644 --- a/app/views/announcements/index.html.slim +++ b/app/views/announcements/index.html.slim @@ -12,4 +12,17 @@ header.page-header i.fa-regular.fa-plus | お知らせ作成 hr.a-border -div(data-vue="Announcements" data-vue-title="#{title}" data-vue-current-user-id:number="#{current_user.id}") +.page-body + - if @announcements.empty? + .container + .o-empty-message + .o-empty-message__icon + i.fa-regular.fa-smile + p.o-empty-message__text + | お知らせはありません + - else + .container.is-md + = paginate @announcements + .card-list.a-card + = render partial: 'announcement', collection: @announcements + = paginate @announcements diff --git a/app/views/announcements/show.html.slim b/app/views/announcements/show.html.slim index 1767bc2588d..4c3e74162c8 100644 --- a/app/views/announcements/show.html.slim +++ b/app/views/announcements/show.html.slim @@ -17,7 +17,74 @@ hr.a-border .page-body .page-body__inner.has-side-nav .container.is-md - = render 'announcement', announcement: @announcement + .announcement.page-content + header.page-content-header + .page-content-header__start + .page-content-header__user + = render 'users/icon', + user: @announcement.user, + link_class: 'page-content-header__user-link', + image_class: 'page-content-header__user-icon' + .page-content-header__end + .page-content-header__row + .page-content-header__before-title + time.a-meta(datetime="#{@announcement.created_at.to_datetime}" pubdate='pubdate') + = l @announcement.updated_at + h1.page-content-header__title(class="#{@announcement.wip? ? 'is-wip' : ''}") + - if @announcement.wip? + span.a-title-label.is-wip + | WIP + = @announcement.title + .page-content-header__row + .page-content-header-metas + .page-content-header-metas__start + .page-content-header-metas__meta + = link_to @announcement.user, class: 'a-user-name' do + = @announcement.user.long_name + .page-content-header-metas__end + .page-content-header-metas__meta + - length = @announcement.comments.length + a.a-meta(href='#comments' class="#{length.zero? ? 'is-disabled' : ''}") + | コメント( + span#comment_count(class="#{length.zero? ? 'is-muted' : 'is-emphasized'}") + = length + | ) + .page-content-header__row + .page-content-header-actions + .page-content-header-actions__start + .page-content-header-actions__action + div(data-vue="WatchToggle" data-vue-watchable-id:number="#{@announcement.id}" data-vue-watchable-type='Announcement') + .page-content-header-actions__end + .page-content-header-actions__action + = link_to new_announcement_path(id: @announcement), class: 'a-button is-sm is-secondary is-block', id: 'copy' do + i.fa-solid.fa-copy + | コピー + + .a-card + .card-body + .card__description + .a-long-text.is-md.js-markdown-view + = @announcement.description + hr.a-border-tint + = render 'reactions/reactions', reactionable: @announcement + hr.a-border-tint + .card-footer + .card-main-actions + ul.card-main-actions__items(class="#{@announcement.published_at ? 'is-sub-actions' : ''}") + - if @announcement.published_at + li.card-main-actions__item.is-sub + = link_to edit_announcement_path(@announcement), class: 'card-main-actions__muted-action' do + | 内容修正 + - else + li.card-main-actions__item + = link_to edit_announcement_path(@announcement), class: 'card-main-actions__action a-button is-sm is-secondary is-block', id: 'js-shortcut-edit' do + i.fa-solid.fa-pen#new + | 内容修正 + - if admin_or_mentor_login? || @announcement.user_id == current_user.id + li.card-main-actions__item.is-sub + = link_to announcement_path(@announcement), data: { confirm: '本当によろしいですか?' }, method: :delete, class: 'card-main-actions__muted-action' do + span#delete + | 削除する #js-comments(data-commentable-id="#{@announcement.id}" data-commentable-type='Announcement' data-current-user-id="#{current_user.id}") div(data-vue='Footprints' data-vue-footprintable-id="#{@announcement.id}" data-vue-footprintable-type='Announcement') = render partial: 'recent_announcements' diff --git a/app/views/api/announcements/_announcement.json.jbuilder b/app/views/api/announcements/_announcement.json.jbuilder deleted file mode 100644 index cf57b7ac8de..00000000000 --- a/app/views/api/announcements/_announcement.json.jbuilder +++ /dev/null @@ -1,21 +0,0 @@ -json.id announcement.id -json.user do - json.partial! "api/users/user", user: announcement.user -end -json.title announcement.title -json.description announcement.description -json.target announcement.target -json.wip announcement.wip -json.url announcement_path(id: announcement) -json.new_url new_announcement_path(id: announcement) -json.created_at l(announcement.created_at) -json.created_at_date_time announcement.created_at.to_datetime -json.updated_at l(announcement.updated_at) -json.updated_at_date_time announcement.updated_at.to_datetime - -if announcement.published_at? - json.published_at l(announcement.published_at) - json.published_at_date_time announcement.published_at.to_datetime -end - -json.commentsSize announcement.comments.size diff --git a/app/views/api/announcements/create.json.jbuilder b/app/views/api/announcements/create.json.jbuilder deleted file mode 100644 index cfaaad61c2d..00000000000 --- a/app/views/api/announcements/create.json.jbuilder +++ /dev/null @@ -1 +0,0 @@ -json.partial! "api/announcements/announcement", announcement: @announcement diff --git a/app/views/api/announcements/index.json.jbuilder b/app/views/api/announcements/index.json.jbuilder deleted file mode 100644 index 02b9666689c..00000000000 --- a/app/views/api/announcements/index.json.jbuilder +++ /dev/null @@ -1,6 +0,0 @@ -json.announcements do - json.array! @announcements do |announcement| - json.partial! "api/announcements/announcement", announcement: announcement - end -end -json.total_pages @announcements.page(1).total_pages diff --git a/app/views/api/announcements/show.json.jbuilder b/app/views/api/announcements/show.json.jbuilder deleted file mode 100644 index cfaaad61c2d..00000000000 --- a/app/views/api/announcements/show.json.jbuilder +++ /dev/null @@ -1 +0,0 @@ -json.partial! "api/announcements/announcement", announcement: @announcement diff --git a/config/routes/api.rb b/config/routes/api.rb index 9c184c69693..6a579e547e6 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -72,7 +72,6 @@ resource :passed, only: %i(show), controller: 'passed' end resources :products, only: %i(index show) - resources :announcements, except: %i(new edit) resources :searchables, only: %i(index) resources :niconico_calendars, only: %i(show) resources :bookmarks, only: %i(index create destroy) diff --git a/test/integration/api/announcements_test.rb b/test/integration/api/announcements_test.rb deleted file mode 100644 index 3329a68545e..00000000000 --- a/test/integration/api/announcements_test.rb +++ /dev/null @@ -1,155 +0,0 @@ -# frozen_string_literal: true - -require 'test_helper' - -class API::AnnouncementsTest < ActionDispatch::IntegrationTest - fixtures :announcements - - setup do - @announcement = announcements(:announcement1) - @my_announcement = announcements(:announcement4) - end - - test 'GET /api/announcements.json' do - get api_announcements_path(format: :json) - assert_response :unauthorized - - token = create_token('kimura', 'testtest') - get api_announcements_path(format: :json), - headers: { 'Authorization' => "Bearer #{token}" } - assert_response :ok - end - - test 'POST /api/announcements.json' do - post api_announcements_path(format: :json), - params: { - announcement: { - title: 'test', - description: 'postのテストです' - } - } - assert_response :unauthorized - - token = create_token('kimura', 'testtest') - post api_announcements_path(format: :json), - params: { - announcement: { - title: 'test', - description: 'postのテストです' - } - }, - headers: { 'Authorization' => "Bearer #{token}" } - assert_response :created - end - - test 'GET /api/announcements/1234.json' do - get api_announcement_path(@announcement.id, format: :json) - assert_response :unauthorized - - token = create_token('kimura', 'testtest') - get api_announcement_path(@announcement.id, format: :json), - headers: { 'Authorization' => "Bearer #{token}" } - assert_response :ok - end - - test 'PATCH /api/announcements/1234.json' do - patch api_announcement_path(@my_announcement.id, format: :json), - params: { - announcement: { - title: 'test', - description: 'patchのテストです' - } - } - assert_response :unauthorized - - token = create_token('kimura', 'testtest') - patch api_announcement_path(@my_announcement.id, format: :json), - params: { - announcement: { - title: 'test', - description: 'patchのテストです' - } - }, - headers: { 'Authorization' => "Bearer #{token}" } - assert_response :bad_request - - token = create_token('komagata', 'testtest') - patch api_announcement_path(@my_announcement.id, format: :json), - params: { - announcement: { - title: 'test', - description: 'patchのテストです' - } - }, - headers: { 'Authorization' => "Bearer #{token}" } - assert_response :ok - end - - test 'DELETE /api/announcements/1234.json' do - delete api_announcement_path(@my_announcement.id, format: :json) - assert_response :unauthorized - - token = create_token('kimura', 'testtest') - delete api_announcement_path(@announcement.id, format: :json), - headers: { 'Authorization' => "Bearer #{token}" } - assert_response :unauthorized - - token = create_token('komagata', 'testtest') - delete api_announcement_path(@announcement.id, format: :json), - headers: { 'Authorization' => "Bearer #{token}" } - assert_response :no_content - end - - test 'users except admin cannot publish an announcement when new' do - token = create_token('kimura', 'testtest') - post api_announcements_path(format: :json), - params: { - announcement: { - title: 'test', - description: 'postのテストです' - } - }, - headers: { 'Authorization' => "Bearer #{token}" } - assert_response :created - assert Announcement.last.wip - - token = create_token('komagata', 'testtest') - post api_announcements_path(format: :json), - params: { - announcement: { - title: 'test', - description: 'postのテストです', - wip: false - } - }, - headers: { 'Authorization' => "Bearer #{token}" } - assert_response :created - assert_not Announcement.last.wip - end - - test 'users except admin cannot publish an announcement when edit' do - token = create_token('kimura', 'testtest') - patch api_announcement_path(@my_announcement.id, format: :json), - params: { - announcement: { - title: 'test', - description: 'patchのテストです', - wip: false - } - }, - headers: { 'Authorization' => "Bearer #{token}" } - assert_response :bad_request - - token = create_token('komagata', 'testtest') - patch api_announcement_path(@my_announcement.id, format: :json), - params: { - announcement: { - title: 'test', - description: 'patchのテストです', - wip: false - } - }, - headers: { 'Authorization' => "Bearer #{token}" } - assert_response :ok - end -end diff --git a/test/models/announcement_test.rb b/test/models/announcement_test.rb new file mode 100644 index 00000000000..71925e3c335 --- /dev/null +++ b/test/models/announcement_test.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'test_helper' + +class AnnouncementTest < ActiveSupport::TestCase + test '.copy_announcement(announcement_id)' do + announcement = Announcement.create!( + user_id: users(:kimura).id, + title: 'お知らせのタイトルです', + description: 'お知らせの内容です', + target: 0 + ) + copied_announcement = Announcement.copy_announcement(announcement.id) + + assert_equal copied_announcement.title, announcement.title + assert_equal copied_announcement.description, announcement.description + assert_equal copied_announcement.target, announcement.target + end + + test '.copy_template_by_resource(template_file, params = {})' do + event = events(:event1) + template = MessageTemplate.load('event_announcements.yml', params: { event: }) + announcement = Announcement.new(title: template['title'], description: template['description']) + announcement_based_template = Announcement.copy_template_by_resource('event_announcements.yml', event:) + + assert_equal announcement_based_template.title, announcement.title + assert_equal announcement_based_template.description, announcement.description + end +end diff --git a/test/system/announcements_test.rb b/test/system/announcements_test.rb index 4a30294bb78..21691e526e0 100644 --- a/test/system/announcements_test.rb +++ b/test/system/announcements_test.rb @@ -26,7 +26,7 @@ class AnnouncementsTest < ApplicationSystemTestCase user = users(:komagata) Announcement.delete_all 26.times do - Announcement.create(title: 'test', description: 'test', user:) + Announcement.create(title: 'test', description: 'test', user:, published_at: DateTime.now) end visit_with_auth '/announcements', 'kimura' assert_selector 'nav.pagination', count: 2