From f3327be5692b0f16526fac63b704ae55bc388a79 Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Thu, 5 Sep 2024 22:04:47 +0900 Subject: [PATCH 01/22] feat: multiple speaker --- .gitignore | 1 + .../speaker_invitation_accepts_controller.rb | 78 +++++++++++++++ .../speaker_invitations_controller.rb | 34 +++++++ app/mailers/speaker_invitation_mailer.rb | 15 +++ app/models/speaker_invitation.rb | 33 +++++++ app/models/speaker_invitation_accept.rb | 33 +++++++ app/models/talk.rb | 1 + .../speaker_dashboard/speakers/_form.html.erb | 62 +----------- .../speakers/_form_speaker.html.erb | 61 ++++++++++++ .../speakers/_guidance_section_1.html.erb | 70 +++++++++++++ .../speakers/_guidance_section_2.html.erb | 23 +++++ .../speaker_dashboard/speakers/_talk.html.erb | 24 +++++ .../speakers/guidance.html.erb | 93 +----------------- .../invite.html.erb | 35 +++++++ .../speaker_invitation_accepts/new.html.erb | 29 ++++++ .../speaker_invitation_mailer/invite.text.erb | 12 +++ app/views/speaker_invitations/new.html.erb | 20 ++++ compose.yaml | 4 +- config/environments/development.rb | 2 + config/environments/production.rb | 16 +++ config/environments/test.rb | 2 + config/routes.rb | 3 + ...240905112725_create_speaker_invitations.rb | 13 +++ ...40927_create_speaker_invitation_accepts.rb | 15 +++ db/schema.rb | 34 ++++++- localstack/init/ready.d/create_queue.sh | 2 +- spec/factories/speaker_invitation_accepts.rb | 32 ++++++ spec/factories/speaker_invitations.rb | 32 ++++++ .../speaker_invitation_mailer_preview.rb | 3 + .../mailers/speaker_invitation_mailer_spec.rb | 5 + ...aker_invitation_accepts_controller_spec.rb | 98 +++++++++++++++++++ .../speaker_invitations_controller_spec.rb | 75 ++++++++++++++ 32 files changed, 805 insertions(+), 155 deletions(-) create mode 100644 app/controllers/speaker_invitation_accepts_controller.rb create mode 100644 app/controllers/speaker_invitations_controller.rb create mode 100644 app/mailers/speaker_invitation_mailer.rb create mode 100644 app/models/speaker_invitation.rb create mode 100644 app/models/speaker_invitation_accept.rb create mode 100644 app/views/speaker_dashboard/speakers/_form_speaker.html.erb create mode 100644 app/views/speaker_dashboard/speakers/_guidance_section_1.html.erb create mode 100644 app/views/speaker_dashboard/speakers/_guidance_section_2.html.erb create mode 100644 app/views/speaker_invitation_accepts/invite.html.erb create mode 100644 app/views/speaker_invitation_accepts/new.html.erb create mode 100644 app/views/speaker_invitation_mailer/invite.text.erb create mode 100644 app/views/speaker_invitations/new.html.erb create mode 100644 db/migrate/20240905112725_create_speaker_invitations.rb create mode 100644 db/migrate/20240905140927_create_speaker_invitation_accepts.rb create mode 100644 spec/factories/speaker_invitation_accepts.rb create mode 100644 spec/factories/speaker_invitations.rb create mode 100644 spec/mailers/previews/speaker_invitation_mailer_preview.rb create mode 100644 spec/mailers/speaker_invitation_mailer_spec.rb create mode 100644 spec/requests/speaker_invitation_accepts_controller_spec.rb create mode 100644 spec/requests/speaker_invitations_controller_spec.rb diff --git a/.gitignore b/.gitignore index 690e70b36..8336d9c37 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ package-lock.json localstack /app/assets/builds/* !/app/assets/builds/.keep +.aider* diff --git a/app/controllers/speaker_invitation_accepts_controller.rb b/app/controllers/speaker_invitation_accepts_controller.rb new file mode 100644 index 000000000..e98405d96 --- /dev/null +++ b/app/controllers/speaker_invitation_accepts_controller.rb @@ -0,0 +1,78 @@ +class SpeakerInvitationAcceptsController < ApplicationController + include SecuredSpeaker + before_action :set_speaker + + skip_before_action :logged_in_using_omniauth?, only: [:invite] + + def invite + return redirect_to(speaker_invitation_accepts_path) if from_auth0?(params) + @conference = Conference.find_by(abbr: params[:event]) + @speaker_invitation = SpeakerInvitation.find_by(token: params[:token]) + end + + def new + @speaker_invitation_accept = SpeakerInvitationAccept.new + @conference = Conference.find_by(abbr: params[:event]) + + @speaker_invitation = SpeakerInvitation.find_by(token: params[:token]) + unless @speaker_invitation + raise(ActiveRecord::RecordNotFound) + end + + if Time.zone.now > @speaker_invitation.expires_at + flash.now[:alert] = '招待メールが期限切れです。再度招待メールを送ってもらってください。' + end + @talk = @speaker_invitation.talk + @proposal = @talk.proposal + @speaker = Speaker.new(conference: @conference, email: current_user[:info][:email], name: current_user[:info][:name]) + end + + def create + begin + ActiveRecord::Base.transaction do + @conference = Conference.find_by(abbr: params[:event]) + @speaker_invitation = SpeakerInvitation.find(params[:speaker][:speaker_invitation_id]) + + speaker_param = speaker_invitation_accept_params.merge(conference: @conference, email: current_user[:info][:email], name: current_user[:info][:name]) + speaker_param.delete(:speaker_invitation_id) + + @speaker = Speaker.new(speaker_param) + @speaker.save! + + @talk = @speaker_invitation.talk + @talk.speakers << @speaker + @talk.save! + + @speaker_invitation_accept = SpeakerInvitationAccept.new(conference_id: @conference.id, speaker_invitation_id: @speaker_invitation.id, speaker_id: @speaker.id, talk_id: @talk.id) + @speaker_invitation_accept.save! + + + redirect_to(speaker_dashboard_path(event: @conference.abbr), notice: 'Speaker was successfully added.') + end + rescue ActiveRecord::RecordInvalid => e + render(:new, alert: e.message) + end + end + + def speaker_invitation_accept_params + params.require(:speaker).permit( + :speaker_invitation_id, + :name, + :name_mother_tongue, + :sub, + :email, + :profile, + :company, + :job_title, + :twitter_id, + :github_id, + :avatar, + :conference_id, + :additional_documents + ) + end + + def from_auth0?(params) + params[:state].present? + end +end diff --git a/app/controllers/speaker_invitations_controller.rb b/app/controllers/speaker_invitations_controller.rb new file mode 100644 index 000000000..e3b732d21 --- /dev/null +++ b/app/controllers/speaker_invitations_controller.rb @@ -0,0 +1,34 @@ +class SpeakerInvitationsController < ApplicationController + include SecuredSpeaker + before_action :set_speaker + def new + @speaker_invitation = SpeakerInvitation.new + @talk = Talk.find(params[:talk_id]) + if @speaker.talks.find(@talk.id).nil? + render_404 + end + end + + def create + ActiveRecord::Base.transaction do + @conference = Conference.find_by(abbr: params[:event]) + @invitation = SpeakerInvitation.new(speaker_invitation_params) + @invitation.conference_id = @conference.id + @invitation.token = SecureRandom.hex(50) + @invitation.expires_at = 1.days.from_now # 有効期限を1日後に設定 + if @invitation.save + SpeakerInvitationMailer.invite(@conference, @speaker, @invitation.talk, @invitation).deliver_now + flash.now[:notice] = 'Invitation sent!' + else + flash.now[:alert] = 'Failed to send invitation.' + end + redirect_to("/#{@conference.abbr}/speaker_dashboard") + end + end + + private + + def speaker_invitation_params + params.require(:speaker_invitation).permit(:email, :talk_id) + end +end diff --git a/app/mailers/speaker_invitation_mailer.rb b/app/mailers/speaker_invitation_mailer.rb new file mode 100644 index 000000000..358490927 --- /dev/null +++ b/app/mailers/speaker_invitation_mailer.rb @@ -0,0 +1,15 @@ +class SpeakerInvitationMailer < ApplicationMailer + include Rails.application.routes.url_helpers + + default from: 'CloudNative Days 実行委員会 ' + layout 'mailer' + + def invite(conference, invited_by, talk, invitation) + @conference = conference + @invited_by = invited_by + @talk = talk + @invitation = invitation + @new_accept_url = speaker_invitation_accepts_invite_url(event: @conference.abbr, token: @invitation.token) + mail(to: @invitation.email, subject: "#{@conference.name} 共同スピーカー招待") + end +end diff --git a/app/models/speaker_invitation.rb b/app/models/speaker_invitation.rb new file mode 100644 index 000000000..fcd5954b3 --- /dev/null +++ b/app/models/speaker_invitation.rb @@ -0,0 +1,33 @@ +# == Schema Information +# +# Table name: speaker_invitations +# +# id :bigint not null, primary key +# email :string(255) not null +# expires_at :datetime not null +# token :string(255) not null +# created_at :datetime not null +# updated_at :datetime not null +# conference_id :bigint not null +# talk_id :bigint not null +# +# Indexes +# +# index_speaker_invitations_on_conference_id (conference_id) +# index_speaker_invitations_on_talk_id (talk_id) +# +# Foreign Keys +# +# fk_rails_... (conference_id => conferences.id) +# fk_rails_... (talk_id => talks.id) +# +class SpeakerInvitation < ApplicationRecord + belongs_to :talk + belongs_to :conference + + has_one :speaker_invitation_accept + + + validates :email, presence: true + validates :token, presence: true +end diff --git a/app/models/speaker_invitation_accept.rb b/app/models/speaker_invitation_accept.rb new file mode 100644 index 000000000..240a53304 --- /dev/null +++ b/app/models/speaker_invitation_accept.rb @@ -0,0 +1,33 @@ +# == Schema Information +# +# Table name: speaker_invitation_accepts +# +# id :bigint not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# conference_id :bigint not null +# speaker_id :bigint not null +# speaker_invitation_id :bigint not null +# talk_id :bigint not null +# +# Indexes +# +# index_speaker_invitation_accepts_on_conference_id (conference_id) +# index_speaker_invitation_accepts_on_conference_speaker_talk (conference_id,speaker_id,talk_id) UNIQUE +# index_speaker_invitation_accepts_on_speaker_id (speaker_id) +# index_speaker_invitation_accepts_on_speaker_invitation_id (speaker_invitation_id) +# index_speaker_invitation_accepts_on_talk_id (talk_id) +# +# Foreign Keys +# +# fk_rails_... (conference_id => conferences.id) +# fk_rails_... (speaker_id => speakers.id) +# fk_rails_... (speaker_invitation_id => speaker_invitations.id) +# fk_rails_... (talk_id => talks.id) +# +class SpeakerInvitationAccept < ApplicationRecord + belongs_to :speaker_invitation + belongs_to :conference + belongs_to :speaker + belongs_to :talk +end diff --git a/app/models/talk.rb b/app/models/talk.rb index 9208c0b31..e750f9c30 100644 --- a/app/models/talk.rb +++ b/app/models/talk.rb @@ -59,6 +59,7 @@ class Talk < ApplicationRecord has_many :profiles, through: :registered_talks has_many :media_package_harvest_jobs has_many :check_in_talks + has_many :speaker_invitations has_many :proposal_items, autosave: true, dependent: :destroy has_many :profiles, through: :registered_talks diff --git a/app/views/speaker_dashboard/speakers/_form.html.erb b/app/views/speaker_dashboard/speakers/_form.html.erb index ae51c881b..63996b52c 100644 --- a/app/views/speaker_dashboard/speakers/_form.html.erb +++ b/app/views/speaker_dashboard/speakers/_form.html.erb @@ -2,67 +2,7 @@

登壇者情報

-
- <%= form.label :name, '講演者氏名(英語表記) - Your Name' %>* - <%= form.text_field :name, class: "form-control", required: true, placeholder: 'Taro Cloud' %> -
- -
- <%= form.label :name_mother_tongue, '講演者氏名(漢字)- Name "kanji"' %>* -

If you do not have a Kanji name, please set "N/A"

- <%= form.text_field :name_mother_tongue, class: "form-control", required: true, placeholder: 'クラウド 太郎' %> -
- -
- <%= form.label :profile, '講演者プロフィール - Biography' %>* -

200文字程度 (About 400 letters)

- <%= form.text_area :profile, class: "form-control", required: true %> -
- -
- <%= form.label :company, '会社名/所属団体名 - Company/Organizations(★)' %>* - <%= form.text_field :company, class: "form-control", required: true, placeholder: 'クラウドネイティブデイズ株式会社' %> -
- -
- <%= form.label :job_title, '肩書き - Job Title' %>* - <%= form.text_field :job_title, class: "form-control", required: true, placeholder: '凄腕エンジニア' %> -
- -
- <%= form.label :additional_documents, '過去の登壇実績が分かるスライド等 - Published Slides, etc.(★)' %> - <%= form.text_area :additional_documents, class: "form-control", required: false, placeholder: "https://speakerdeck.com/\nhttps://www.slideshare.net/" %> -
- -
- <%= form.label :twitter_id, 'X(Twitter) ID' %> - <%= form.text_field :twitter_id, class: "form-control", placeholder: 'cloudnativedays' %> -
- -
- <%= form.label :github_id, 'GitHub ID' %> - <%= form.text_field :github_id, class: "form-control", placeholder: 'cloudnativedaysjp' %> -
- -
- <%= form.label :avatar, '講演者写真 - Photographs' %> - - <%= form.text_field :avatar, - type: :file, - id: "avatar_upload", - class: "form-control" %> - - - <%= form.text_field :avatar, - type: :hidden, - error_handler: false, - class: "upload-data", - value: @speaker.nil? ? "" : @speaker.cached_avatar_data %> -
- -
- -
+ <%= render('speaker_dashboard/speakers/form_speaker', form: form) %>
diff --git a/app/views/speaker_dashboard/speakers/_form_speaker.html.erb b/app/views/speaker_dashboard/speakers/_form_speaker.html.erb new file mode 100644 index 000000000..22f323f80 --- /dev/null +++ b/app/views/speaker_dashboard/speakers/_form_speaker.html.erb @@ -0,0 +1,61 @@ +
+ <%= form.label :name, '講演者氏名(英語表記) - Your Name' %>* + <%= form.text_field :name, class: "form-control", required: true, placeholder: 'Taro Cloud' %> +
+ +
+ <%= form.label :name_mother_tongue, '講演者氏名(漢字)- Name "kanji"' %>* +

If you do not have a Kanji name, please set "N/A"

+ <%= form.text_field :name_mother_tongue, class: "form-control", required: true, placeholder: 'クラウド 太郎' %> +
+ +
+ <%= form.label :profile, '講演者プロフィール - Biography' %>* +

200文字程度 (About 400 letters)

+ <%= form.text_area :profile, class: "form-control", required: true %> +
+ +
+ <%= form.label :company, '会社名/所属団体名 - Company/Organizations(★)' %>* + <%= form.text_field :company, class: "form-control", required: true, placeholder: 'クラウドネイティブデイズ株式会社' %> +
+ +
+ <%= form.label :job_title, '肩書き - Job Title' %>* + <%= form.text_field :job_title, class: "form-control", required: true, placeholder: '凄腕エンジニア' %> +
+ +
+ <%= form.label :additional_documents, '過去の登壇実績が分かるスライド等 - Published Slides, etc.(★)' %> + <%= form.text_area :additional_documents, class: "form-control", required: false, placeholder: "https://speakerdeck.com/\nhttps://www.slideshare.net/" %> +
+ +
+ <%= form.label :twitter_id, 'X(Twitter) ID' %> + <%= form.text_field :twitter_id, class: "form-control", placeholder: 'cloudnativedays' %> +
+ +
+ <%= form.label :github_id, 'GitHub ID' %> + <%= form.text_field :github_id, class: "form-control", placeholder: 'cloudnativedaysjp' %> +
+ +
+ <%= form.label :avatar, '講演者写真 - Photographs' %> + + <%= form.text_field :avatar, + type: :file, + id: "avatar_upload", + class: "form-control" %> + + + <%= form.text_field :avatar, + type: :hidden, + error_handler: false, + class: "upload-data", + value: @speaker.nil? ? "" : @speaker.cached_avatar_data %> +
+ +
+ +
diff --git a/app/views/speaker_dashboard/speakers/_guidance_section_1.html.erb b/app/views/speaker_dashboard/speakers/_guidance_section_1.html.erb new file mode 100644 index 000000000..69f875bc5 --- /dev/null +++ b/app/views/speaker_dashboard/speakers/_guidance_section_1.html.erb @@ -0,0 +1,70 @@ +
+

この夏、CloudNative Days Summer 2024は札幌という新天地で6/15(土)に開催します!

+

CloudNative Daysは、これまでクラウドネイティブ技術に関する最新の知識や実践を共有する場として、多くの方々に支持されてきました。札幌での開催を通じて、新たな地でのクラウドネイティブ技術の魅力や可能性を一緒に探りましょう。

+

CloudNative Days 2024では、さまざまなバックグラウンドや経験を持つ参加者が集まり、新たな学びや繋がりを築く場を提供します。

+

今年もオンラインとオフラインのハイブリッド形式での開催を予定しており、どちらの形式でもイベントに参加できます。

+

CloudNative Days 2024へプロポーザルを提出して、抱えている悩みや知見を共有してみませんか?

+
+

例えば、このような発表をお待ちしております。

+
    +
  • Kubernetesを用いたマイクロサービスのデプロイと管理
  • +
  • GitHub ActionsとJenkinsによるCI/CDパイプラインの構築手法
  • +
  • ObservabilityとSLOによるデータに基づいた意思決定
  • +
  • Cloud Native Security Platform (CNSP)によるクラウドセキュリティの強化
  • +
  • 趣味プロジェクトから学ぶクラウド活用術: 実践者の体験談
  • +
  • ログ分析、トレース、ダンプ分析などの実践的なテクニック
  • +
  • クラウドネイティブの実践: 成功の秘訣と課題克服のストーリー
  • +
  • DevOps、SRE、GitOpsなどの文化とベストプラクティス
  • +
  • 最新クラウドネイティブ技術の動向:2024年の注目技術と展望
  • +
  • +
+

他にも過去のカンファレンスで発表されたセッションを参考にして、考えるのも良いと思います。

+ + +

以下のような方でもご安心ください。運営メンバーの多くがオンラインでの登壇やカンファレンス運営を経験しており、全力でサポートします!

+
    +
  • カンファレンスの登壇が初めて
  • +
  • 動画の事前収録方法が分からない
  • +
+

皆様のご応募を心よりお待ちしています。

+
+ +
+

CloudNative Days Summer 2024のテーマ

+

「Synergy 〜その先の、道へ。出会いと変化を紡ぐ場所〜」

+

この夏、CloudNative Daysは札幌という新天地で次の旅路をはじめます。
初めての地で、さまざまな背景や経験を持つ参加者が集まり、新たな学び、繋がりを築きます。

+

参加者の中には、あなた自身と、あるいは所属組織と同じ悩みや境遇を抱える方がいるかもしれません。
また、新たな一歩を踏み出せずにいる人もいることでしょう。

+

共感し合える仲間と出会い、時間を分かち合うことで新たなアイデアを創造したり、
先駆者の知見に触れることで次の道を探るための場がここにはあります。

+

出会いと変化を紡ぎ、未来への一歩を踏み出す1日にしましょう。
さぁ、その先の、道へ。

+
+ +
+

エントリーの流れ

+
    +
  1. 本ページから登壇者ポータルにログイン
  2. +
  3. 登壇内容を入力してエントリー
  4. +
  5. エントリー終了後、実行委員会で選考
  6. +
  7. 登壇者ポータルで選考結果をご連絡
  8. +
+
+ +
+

スケジュール

+
    +
  • 2024/04/08 (月) 23:59 プロポーザルのエントリー締め切り
  • +
  • 2024/04/12 (金) 選考結果をご連絡
  • +
  • 2024/06/15 (土) CloudNative Days Summer 2024開催
  • +
+
+ +
+

登壇方法

+
    +
  1. 現地会場(札幌)での登壇(質疑応答含め、40分セッション。)
  2. +
  3. 録画による配信(40分。事前に録画データを提供いただきます)
  4. +
+

※ プロポーザル応募後も、エントリー締め切り日までは登壇方法の変更が可能です。締め切り後は変更不可となりますので、予めご了承ください。

+
diff --git a/app/views/speaker_dashboard/speakers/_guidance_section_2.html.erb b/app/views/speaker_dashboard/speakers/_guidance_section_2.html.erb new file mode 100644 index 000000000..1acc1f9ec --- /dev/null +++ b/app/views/speaker_dashboard/speakers/_guidance_section_2.html.erb @@ -0,0 +1,23 @@ +
+

エントリーQ&A

+

応募内容は公開されますか?

+

透明性確保のため申請いただいたセッション情報は公開を前提としていますが、SNSアカウントを除き、応募者の個人情報は公開されません。

+ +

プロポーザルの選考はどのように行われますか?

+

X(Twitter)での反響、およびCNDS2024実行委員会による投票で絞り込みした上で、全体のバランスや多様性を考慮したディスカッションにより決定されます。詳細な選考方法については、こちらのブログをご参照ください。

+ +

一人で複数の応募は可能でしょうか?

+

可能です。

+ +

複数名での応募は可能でしょうか?

+

可能です。エントリーにあたっては、代表者の方がお申し込みください。

+ +

セッションの長さはどのくらいですか?

+

40分(質疑応答の時間含む)です。

+ +

選考の基準として重視される項目はなんですか?

+

記入欄に、採択への影響度を三段階で表示しています。記載された★マークが多いほど、考慮される度合いが高いです。

+ +

登壇者は札幌会場への移動の必要性や、札幌近辺の在住・勤務である必要はありますか?

+

札幌会場での現地登壇の他、事前収録も可能なため、様々な地域から登壇することができます。居住地や勤務地などの制限は行っておりません。

+
diff --git a/app/views/speaker_dashboard/speakers/_talk.html.erb b/app/views/speaker_dashboard/speakers/_talk.html.erb index cecc3cac5..d9503283c 100644 --- a/app/views/speaker_dashboard/speakers/_talk.html.erb +++ b/app/views/speaker_dashboard/speakers/_talk.html.erb @@ -17,6 +17,30 @@ <% end %> +
+ 共同発表者 +
+
+ <% if talk.speakers.reject{|speaker| speaker.id == @speaker.id}.size > 0 %> +
    + <% talk.speakers.reject{|speaker| speaker.id == @speaker.id}.each do |speaker| %> +
  • <%= speaker.name %>
  • + <% end %> +
+ <% else %> +

共同スピーカーはいません。

+ <% end %> + <% if talk.speaker_invitations.size > 0 %> +

招待状況

+
    + <% talk.speaker_invitations.each do |invitation| %> +
  • <%= invitation.email %>(<%= invitation.speaker_invitation_accept.present? ? '登録済み' : '未登録' %>)
  • + <% end %> +
+ <% end %> +

追加するには、<%= link_to 'こちら', new_speaker_invitation_path(talk_id: talk.id) %> から招待を行ってください。

+
+
概要
diff --git a/app/views/speaker_dashboard/speakers/guidance.html.erb b/app/views/speaker_dashboard/speakers/guidance.html.erb index 144478a14..a454fec58 100644 --- a/app/views/speaker_dashboard/speakers/guidance.html.erb +++ b/app/views/speaker_dashboard/speakers/guidance.html.erb @@ -1,80 +1,13 @@ <% provide(:title, 'プロポーザル募集') %>
-
-
-

CloudNative Days Summer 2024で登壇してみませんか?

-

この夏、CloudNative Days Summer 2024は札幌という新天地で6/15(土)に開催します!

-

CloudNative Daysは、これまでクラウドネイティブ技術に関する最新の知識や実践を共有する場として、多くの方々に支持されてきました。札幌での開催を通じて、新たな地でのクラウドネイティブ技術の魅力や可能性を一緒に探りましょう。

-

CloudNative Days 2024では、さまざまなバックグラウンドや経験を持つ参加者が集まり、新たな学びや繋がりを築く場を提供します。

-

今年もオンラインとオフラインのハイブリッド形式での開催を予定しており、どちらの形式でもイベントに参加できます。

-

CloudNative Days 2024へプロポーザルを提出して、抱えている悩みや知見を共有してみませんか?

-
-

例えば、このような発表をお待ちしております。

-
    -
  • Kubernetesを用いたマイクロサービスのデプロイと管理
  • -
  • GitHub ActionsとJenkinsによるCI/CDパイプラインの構築手法
  • -
  • ObservabilityとSLOによるデータに基づいた意思決定
  • -
  • Cloud Native Security Platform (CNSP)によるクラウドセキュリティの強化
  • -
  • 趣味プロジェクトから学ぶクラウド活用術: 実践者の体験談
  • -
  • ログ分析、トレース、ダンプ分析などの実践的なテクニック
  • -
  • クラウドネイティブの実践: 成功の秘訣と課題克服のストーリー
  • -
  • DevOps、SRE、GitOpsなどの文化とベストプラクティス
  • -
  • 最新クラウドネイティブ技術の動向:2024年の注目技術と展望
  • -
  • -
-

他にも過去のカンファレンスで発表されたセッションを参考にして、考えるのも良いと思います。

- - -

以下のような方でもご安心ください。運営メンバーの多くがオンラインでの登壇やカンファレンス運営を経験しており、全力でサポートします!

-
    -
  • カンファレンスの登壇が初めて
  • -
  • 動画の事前収録方法が分からない
  • -
-

皆様のご応募を心よりお待ちしています。

-
- -
-

CloudNative Days Summer 2024のテーマ

-

「Synergy 〜その先の、道へ。出会いと変化を紡ぐ場所〜」

-

この夏、CloudNative Daysは札幌という新天地で次の旅路をはじめます。
初めての地で、さまざまな背景や経験を持つ参加者が集まり、新たな学び、繋がりを築きます。

-

参加者の中には、あなた自身と、あるいは所属組織と同じ悩みや境遇を抱える方がいるかもしれません。
また、新たな一歩を踏み出せずにいる人もいることでしょう。

-

共感し合える仲間と出会い、時間を分かち合うことで新たなアイデアを創造したり、
先駆者の知見に触れることで次の道を探るための場がここにはあります。

-

出会いと変化を紡ぎ、未来への一歩を踏み出す1日にしましょう。
さぁ、その先の、道へ。

-
- -
-

エントリーの流れ

-
    -
  1. 本ページから登壇者ポータルにログイン
  2. -
  3. 登壇内容を入力してエントリー
  4. -
  5. エントリー終了後、実行委員会で選考
  6. -
  7. 登壇者ポータルで選考結果をご連絡
  8. -
-
-

スケジュール

-
    -
  • 2024/04/08 (月) 23:59 プロポーザルのエントリー締め切り
  • -
  • 2024/04/12 (金) 選考結果をご連絡
  • -
  • 2024/06/15 (土) CloudNative Days Summer 2024開催
  • -
-
- -
-

登壇方法

-
    -
  1. 現地会場(札幌)での登壇(質疑応答含め、40分セッション。)
  2. -
  3. 録画による配信(40分。事前に録画データを提供いただきます)
  4. -
-

※ プロポーザル応募後も、エントリー締め切り日までは登壇方法の変更が可能です。締め切り後は変更不可となりますので、予めご了承ください。

+

CloudNative Days Summer 2024で登壇してみませんか?

+ <%= render 'speaker_dashboard/speakers/guidance_section_1' %>
<% if logged_in? %> @@ -84,29 +17,9 @@ <% end %> <%= label_tag 'entry', 'エントリーにはCloudNative Daysへのサインアップが必要です' %>
-
-

エントリーQ&A

-

応募内容は公開されますか?

-

透明性確保のため申請いただいたセッション情報は公開を前提としていますが、SNSアカウントを除き、応募者の個人情報は公開されません。

-

プロポーザルの選考はどのように行われますか?

-

X(Twitter)での反響、およびCNDS2024実行委員会による投票で絞り込みした上で、全体のバランスや多様性を考慮したディスカッションにより決定されます。詳細な選考方法については、こちらのブログをご参照ください。

+ <%= render 'speaker_dashboard/speakers/guidance_section_2' %> -

一人で複数の応募は可能でしょうか?

-

可能です。

- -

複数名での応募は可能でしょうか?

-

可能です。エントリーにあたっては、代表者の方がお申し込みください。

- -

セッションの長さはどのくらいですか?

-

40分(質疑応答の時間含む)です。

- -

選考の基準として重視される項目はなんですか?

-

記入欄に、採択への影響度を三段階で表示しています。記載された★マークが多いほど、考慮される度合いが高いです。

- -

登壇者は札幌会場への移動の必要性や、札幌近辺の在住・勤務である必要はありますか?

-

札幌会場での現地登壇の他、事前収録も可能なため、様々な地域から登壇することができます。居住地や勤務地などの制限は行っておりません。

-
<% if logged_in? %> <%= button_to 'エントリーする!', speakers_entry_path, {method: :get, class: "btn btn-secondary btn-xl inline" } %> diff --git a/app/views/speaker_invitation_accepts/invite.html.erb b/app/views/speaker_invitation_accepts/invite.html.erb new file mode 100644 index 000000000..25e688a9b --- /dev/null +++ b/app/views/speaker_invitation_accepts/invite.html.erb @@ -0,0 +1,35 @@ +<% provide(:title, 'プロポーザル募集') %> + +
+
+ +
+

共同スピーカーとして招待された方へ

+

本ページは共同スピーカーとしての招待メールを受け取った方向けのページです。

+

下記の概要・Q&Aを確認していただき、問題なければ「エントリーする!」ボタンをクリックしてください。

+

その後、登壇者情報の入力フォームに遷移しますので、入力することで共同スピーカーとして登録されます。

+
+ + <%= render 'speaker_dashboard/speakers/guidance_section_1' %> + +
+ <% if logged_in? %> + <%= button_to 'エントリーする!', new_speaker_invitation_accept_path, {params: {token: @speaker_invitation.token}, method: :get, class: "btn btn-secondary btn-xl inline" } %> + <% else %> + <%= button_to 'エントリーする!', '/auth/auth0', {method: :post, class: "btn btn-secondary btn-xl inline" } %> + <% end %> + <%= label_tag 'entry', 'エントリーにはCloudNative Daysへのサインアップが必要です' %> +
+ + <%= render 'speaker_dashboard/speakers/guidance_section_2' %> + +
+ <% if logged_in? %> + <%= button_to 'エントリーする!', new_speaker_invitation_accept_path, {params: {token: @speaker_invitation.token}, method: :get, class: "btn btn-secondary btn-xl inline" } %> + <% else %> + <%= button_to 'エントリーする!', '/auth/auth0', {method: :post, class: "btn btn-secondary btn-xl inline" } %> + <% end %> + <%= label_tag 'entry', 'エントリーにはCloudNative Daysへのサインアップが必要です' %> +
+
+
diff --git a/app/views/speaker_invitation_accepts/new.html.erb b/app/views/speaker_invitation_accepts/new.html.erb new file mode 100644 index 000000000..45f2b52fd --- /dev/null +++ b/app/views/speaker_invitation_accepts/new.html.erb @@ -0,0 +1,29 @@ +
+
+
+

共同スピーカーとして登録する

+ <% flash.each do |type, message| %> + + <% end %> + + <% if flash.keys.size == 0 %> + プロポーザル 「<%= @talk.title %> (<%= @talk.speaker_names.join('/') %>)」に共同スピーカーとして登録します。登録する場合は下記の登壇者情報を入力し、「登録する」ボタンをクリックしてください。 + + + <%= form_with(url: speaker_invitation_accepts_path, model: @speaker) do |form| %> + <%= form.hidden_field :speaker_invitation_id, value: @speaker_invitation.id %> + +
+ <%= render('speaker_dashboard/speakers/form_speaker', form: form) %> +
+ +
+ <%= form.submit class: "btn btn-primary btn-lg btn-block" %> +
+ <% end %> + <% end %> +
+
+
diff --git a/app/views/speaker_invitation_mailer/invite.text.erb b/app/views/speaker_invitation_mailer/invite.text.erb new file mode 100644 index 000000000..1c2527cb3 --- /dev/null +++ b/app/views/speaker_invitation_mailer/invite.text.erb @@ -0,0 +1,12 @@ +<%= @invited_by.name %> さんから、下記のプロポーザルの共同スピーカーとして招待されました。 + +タイトル: <%= @talk.title %> +概要: <%= @talk.abstract %> + +共同スピーカーとして登録するには、以下のリンクをクリックしてください。 + +<%= @new_accept_url %> + +このURLは24時間有効です。 + +<%= render '/layouts/footer' %> diff --git a/app/views/speaker_invitations/new.html.erb b/app/views/speaker_invitations/new.html.erb new file mode 100644 index 000000000..bea920fe1 --- /dev/null +++ b/app/views/speaker_invitations/new.html.erb @@ -0,0 +1,20 @@ +
+
+
+

共同スピーカーに招待メールを送信する

+ + <%= form_with(model: @speaker_invitation) do |form| %> +
+ <%= form.label :email %> + <%= form.email_field :email, class: "form-control", required: true %> +
+ + <%= form.hidden_field :talk_id, value: @talk.id %> + +
+ <%= form.submit "招待メールを送信する", data: { turbo_confirm: '本当に公開しますか?' } %> +
+ <% end %> +
+
+
diff --git a/compose.yaml b/compose.yaml index 5f116cab9..013fba940 100644 --- a/compose.yaml +++ b/compose.yaml @@ -91,7 +91,7 @@ services: - NEXT_PUBLIC_API_BASE_URL=http://localhost:8080/ - NEXT_PUBLIC_EVENT_SALT=cndt2022 localstack: - image: localstack/localstack:latest + image: localstack/localstack:2.3.2 environment: - SERVICES=sqs - DEFAULT_REGION=ap-northeast-1 @@ -102,7 +102,7 @@ services: ports: - 4566:4566 healthcheck: - test: awslocal sqs list-queues --output=text | grep default + test: awslocal sqs list-queues --region ap-northeast-1 --output=text | grep default interval: 10s timeout: 60s retries: 10 diff --git a/config/environments/development.rb b/config/environments/development.rb index bcc9ebfeb..189fc543a 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -88,3 +88,5 @@ Bullet.rails_logger = true # Railsログに出力 end end + +Rails.application.routes.default_url_options[:host] = 'localhost:3000' diff --git a/config/environments/production.rb b/config/environments/production.rb index bd4f8d4d8..9ddb0335a 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -118,6 +118,14 @@ # config.active_record.database_selector = { delay: 2.seconds } # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session + if ENV['REVIEW_APP'] == 'true' + config.routes.default_url_options[:host] = "#{ENV['DREAMKAST_NAMESPACE'].gsub(/dreamkast-dk-/, '')}.dev.cloudnativedays.jp" + elsif ENV['S3_BUCKET'] == 'dreamkast-stg-bucket' + config.routes.default_url_options[:host] = 'staging.dev.cloudnativedays.jp' + elsif ENV['S3_BUCKET'] == 'dreamkast-prod-bucket' + config.routes.default_url_options[:host] = 'event.cloudnativedays.jp' + end + OmniAuth.config.on_failure = Proc.new do |env| message_key = env['omniauth.error.type'] error_description = Rack::Utils.escape(env['omniauth.error'].error_reason) @@ -125,3 +133,11 @@ Rack::Response.new(['302 Moved'], 302, 'Location' => new_path).finish end end + +if ENV['REVIEW_APP'] == 'true' + Rails.application.routes.default_url_options[:host] = "#{ENV['DREAMKAST_NAMESPACE'].gsub(/dreamkast-dk-/, '')}.dev.cloudnativedays.jp" +elsif ENV['S3_BUCKET'] == 'dreamkast-stg-bucket' + Rails.application.routes.default_url_options[:host] = 'staging.dev.cloudnativedays.jp' +elsif ENV['S3_BUCKET'] == 'dreamkast-prod-bucket' + Rails.application.routes.default_url_options[:host] = 'event.cloudnativedays.jp' +end diff --git a/config/environments/test.rb b/config/environments/test.rb index 5f6cef4d6..a396241ad 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -58,3 +58,5 @@ # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true end + +Rails.application.routes.default_url_options[:host] = 'localhost:3000' diff --git a/config/routes.rb b/config/routes.rb index fe897eeae..589c5df05 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -101,6 +101,9 @@ resources :speakers, only: [:new, :edit, :create, :update] resources :video_registrations, only: [:new, :create, :edit, :update] end + resources :speaker_invitations, only: [:index, :new, :create] + resources :speaker_invitation_accepts, only: [:index, :new, :create] + get '/speaker_invitation_accepts/invite' => 'speaker_invitation_accepts#invite' namespace :sponsor_dashboards do get '/login' => 'sponsor_dashboards#login' diff --git a/db/migrate/20240905112725_create_speaker_invitations.rb b/db/migrate/20240905112725_create_speaker_invitations.rb new file mode 100644 index 000000000..4a4acec0d --- /dev/null +++ b/db/migrate/20240905112725_create_speaker_invitations.rb @@ -0,0 +1,13 @@ +class CreateSpeakerInvitations < ActiveRecord::Migration[7.0] + def change + create_table :speaker_invitations do |t| + t.references :talk, null: false, foreign_key: true + t.references :conference, null: false, foreign_key: true + t.string :email, null: false + t.string :token, null: false + t.datetime :expires_at, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20240905140927_create_speaker_invitation_accepts.rb b/db/migrate/20240905140927_create_speaker_invitation_accepts.rb new file mode 100644 index 000000000..2497d4116 --- /dev/null +++ b/db/migrate/20240905140927_create_speaker_invitation_accepts.rb @@ -0,0 +1,15 @@ +class CreateSpeakerInvitationAccepts < ActiveRecord::Migration[7.0] + def change + create_table :speaker_invitation_accepts do |t| + t.references :speaker_invitation, null: false, foreign_key: true + t.references :conference, null: false, foreign_key: true + t.references :speaker, null: false, foreign_key: true + t.references :talk, null: false, foreign_key: true + + t.timestamps + end + + add_index :speaker_invitation_accepts, [:conference_id, :speaker_id, :talk_id], unique: true, name: 'index_speaker_invitation_accepts_on_conference_speaker_talk' + + end +end diff --git a/db/schema.rb b/db/schema.rb index cceff5552..4d8382190 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_08_11_144947) do +ActiveRecord::Schema[7.0].define(version: 2024_09_05_140927) do create_table "admin_profiles", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.bigint "conference_id", null: false t.string "sub" @@ -349,6 +349,32 @@ t.index ["conference_id"], name: "index_speaker_announcements_on_conference_id" end + create_table "speaker_invitation_accepts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.bigint "speaker_invitation_id", null: false + t.bigint "conference_id", null: false + t.bigint "speaker_id", null: false + t.bigint "talk_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["conference_id", "speaker_id", "talk_id"], name: "index_speaker_invitation_accepts_on_conference_speaker_talk", unique: true + t.index ["conference_id"], name: "index_speaker_invitation_accepts_on_conference_id" + t.index ["speaker_id"], name: "index_speaker_invitation_accepts_on_speaker_id" + t.index ["speaker_invitation_id"], name: "index_speaker_invitation_accepts_on_speaker_invitation_id" + t.index ["talk_id"], name: "index_speaker_invitation_accepts_on_talk_id" + end + + create_table "speaker_invitations", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.bigint "talk_id", null: false + t.bigint "conference_id", null: false + t.string "email", null: false + t.string "token", null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["conference_id"], name: "index_speaker_invitations_on_conference_id" + t.index ["talk_id"], name: "index_speaker_invitations_on_talk_id" + end + create_table "speakers", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "name" t.text "profile" @@ -592,6 +618,12 @@ add_foreign_key "speaker_announcement_middles", "speaker_announcements" add_foreign_key "speaker_announcement_middles", "speakers" add_foreign_key "speaker_announcements", "conferences" + add_foreign_key "speaker_invitation_accepts", "conferences" + add_foreign_key "speaker_invitation_accepts", "speaker_invitations" + add_foreign_key "speaker_invitation_accepts", "speakers" + add_foreign_key "speaker_invitation_accepts", "talks" + add_foreign_key "speaker_invitations", "conferences" + add_foreign_key "speaker_invitations", "talks" add_foreign_key "sponsor_attachments", "sponsors" add_foreign_key "sponsor_profiles", "conferences" add_foreign_key "sponsor_types", "conferences" diff --git a/localstack/init/ready.d/create_queue.sh b/localstack/init/ready.d/create_queue.sh index 630c99e0a..5ab299181 100755 --- a/localstack/init/ready.d/create_queue.sh +++ b/localstack/init/ready.d/create_queue.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -awslocal sqs create-queue --queue-name default +awslocal sqs create-queue --queue-name default --region ap-northeast-1 diff --git a/spec/factories/speaker_invitation_accepts.rb b/spec/factories/speaker_invitation_accepts.rb new file mode 100644 index 000000000..3fa3fa7a4 --- /dev/null +++ b/spec/factories/speaker_invitation_accepts.rb @@ -0,0 +1,32 @@ +# == Schema Information +# +# Table name: speaker_invitation_accepts +# +# id :bigint not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# conference_id :bigint not null +# speaker_id :bigint not null +# speaker_invitation_id :bigint not null +# talk_id :bigint not null +# +# Indexes +# +# index_speaker_invitation_accepts_on_conference_id (conference_id) +# index_speaker_invitation_accepts_on_conference_speaker_talk (conference_id,speaker_id,talk_id) UNIQUE +# index_speaker_invitation_accepts_on_speaker_id (speaker_id) +# index_speaker_invitation_accepts_on_speaker_invitation_id (speaker_invitation_id) +# index_speaker_invitation_accepts_on_talk_id (talk_id) +# +# Foreign Keys +# +# fk_rails_... (conference_id => conferences.id) +# fk_rails_... (speaker_id => speakers.id) +# fk_rails_... (speaker_invitation_id => speaker_invitations.id) +# fk_rails_... (talk_id => talks.id) +# +FactoryBot.define do + factory :speaker_invitation_accept do + speaker_invitation { nil } + end +end diff --git a/spec/factories/speaker_invitations.rb b/spec/factories/speaker_invitations.rb new file mode 100644 index 000000000..e382573e5 --- /dev/null +++ b/spec/factories/speaker_invitations.rb @@ -0,0 +1,32 @@ +# == Schema Information +# +# Table name: speaker_invitations +# +# id :bigint not null, primary key +# email :string(255) not null +# expires_at :datetime not null +# token :string(255) not null +# created_at :datetime not null +# updated_at :datetime not null +# conference_id :bigint not null +# talk_id :bigint not null +# +# Indexes +# +# index_speaker_invitations_on_conference_id (conference_id) +# index_speaker_invitations_on_talk_id (talk_id) +# +# Foreign Keys +# +# fk_rails_... (conference_id => conferences.id) +# fk_rails_... (talk_id => talks.id) +# +FactoryBot.define do + factory :speaker_invitation do + email { 'MyString' } + token { 'MyString' } + talk { nil } + conference { nil } + expires_at { '2024-09-05 20:27:25' } + end +end diff --git a/spec/mailers/previews/speaker_invitation_mailer_preview.rb b/spec/mailers/previews/speaker_invitation_mailer_preview.rb new file mode 100644 index 000000000..2275dcd5d --- /dev/null +++ b/spec/mailers/previews/speaker_invitation_mailer_preview.rb @@ -0,0 +1,3 @@ +# Preview all emails at http://localhost:3000/rails/mailers/speaker_invitation_mailer +class SpeakerInvitationMailerPreview < ActionMailer::Preview +end diff --git a/spec/mailers/speaker_invitation_mailer_spec.rb b/spec/mailers/speaker_invitation_mailer_spec.rb new file mode 100644 index 000000000..5c61a80c4 --- /dev/null +++ b/spec/mailers/speaker_invitation_mailer_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe(SpeakerInvitationMailer, type: :mailer) do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/requests/speaker_invitation_accepts_controller_spec.rb b/spec/requests/speaker_invitation_accepts_controller_spec.rb new file mode 100644 index 000000000..e9e66023c --- /dev/null +++ b/spec/requests/speaker_invitation_accepts_controller_spec.rb @@ -0,0 +1,98 @@ +require 'rails_helper' + +describe SpeakerInvitationAcceptsController, type: :request do + let!(:conference) { create(:cndt2020, :registered) } + let!(:speaker) { create(:speaker_alice, :with_talk1_registered, conference:) } + let!(:talk) { speaker.talks.first } + let!(:speaker_invitation) { create(:speaker_invitation, conference:, talk:, email: 'invited@example.com') } + + before do + allow_any_instance_of(ActionDispatch::Request::Session).to(receive(:[]).and_call_original) + allow_any_instance_of(ActionDispatch::Request::Session).to(receive(:[]).with(:userinfo).and_return( + { + info: { email: 'invited@example.com', name: 'Invited Speaker' }, + extra: { raw_info: { sub: 'auth0|123', 'https://cloudnativedays.jp/roles' => [] } } + } + )) + end + + describe 'GET /invite' do + it 'returns a successful response' do + get speaker_invitation_accepts_invite_path(event: conference.abbr, token: speaker_invitation.token) + expect(response).to(be_successful) + expect(response).to(have_http_status('200')) + expect(response.body).to(include('共同スピーカーとして招待された方へ')) + end + + it 'redirects if coming from Auth0' do + get speaker_invitation_accepts_invite_path(event: conference.abbr, token: speaker_invitation.token, state: 'some_state') + expect(response).to(have_http_status('302')) + expect(response).to(redirect_to(speaker_invitation_accepts_path)) + end + end + + describe 'GET /new' do + it 'returns a successful response' do + get new_speaker_invitation_accept_path(event: conference.abbr, token: speaker_invitation.token) + expect(response).to(be_successful) + expect(response).to(have_http_status('200')) + end + + it 'sets a flash alert if invitation is expired' do + speaker_invitation.update(expires_at: 1.day.ago) + get new_speaker_invitation_accept_path(event: conference.abbr, token: speaker_invitation.token) + expect(flash.now[:alert]).to(eq('招待メールが期限切れです。再度招待メールを送ってもらってください。')) + end + + it 'returns a 404 response' do + get new_speaker_invitation_accept_path(event: conference.abbr, token: 'invalid_token') + expect(response).to(have_http_status('404')) + end + end + + describe 'POST /create' do + let(:valid_attributes) do + { + speaker: { + speaker_invitation_id: speaker_invitation.id, + name: 'Invited Speaker', + profile: 'Test profile', + company: 'Test Company', + job_title: 'Developer', + twitter_id: 'twitter_handle', + github_id: 'github_handle' + } + } + end + + it 'creates a new speaker and associates with the talk' do + expect { + post(speaker_invitation_accepts_path(event: conference.abbr), params: valid_attributes) + }.to(change(Speaker, :count).by(1) + .and(change(SpeakerInvitationAccept, :count).by(1))) + + expect(response).to(redirect_to(speaker_dashboard_path(event: conference.abbr))) + expect(flash[:notice]).to(eq('Speaker was successfully added.')) + + new_speaker = Speaker.last + expect(new_speaker.talks).to(include(talk)) + end + + context 'with invalid parameters' do + let(:invalid_attributes) do + { + speaker: { + speaker_invitation_id: speaker_invitation.id, + name: '' # Invalid: name is required + } + } + end + + it 'does not create a new speaker' do + expect { + post(speaker_invitation_accepts_path(event: conference.abbr), params: invalid_attributes) + }.to_not(change(Speaker, :count)) + end + end + end +end diff --git a/spec/requests/speaker_invitations_controller_spec.rb b/spec/requests/speaker_invitations_controller_spec.rb new file mode 100644 index 000000000..8aed3ce71 --- /dev/null +++ b/spec/requests/speaker_invitations_controller_spec.rb @@ -0,0 +1,75 @@ +require 'rails_helper' + +describe(SpeakerInvitationsController, type: :request) do + let!(:conference) { create(:cndt2020, :registered) } + let!(:speaker) { create(:speaker_alice, :with_talk1_registered, conference:) } + let!(:talk) { speaker.talks.first } + + before do + ActionDispatch::Request::Session.define_method(:original, ActionDispatch::Request::Session.instance_method(:[])) + allow_any_instance_of(ActionDispatch::Request::Session).to(receive(:[]) do |*arg| + if arg[1] == :userinfo + session[:userinfo] + else + arg[0].send(:original, arg[1]) + end + end) + end + + describe 'GET /new' do + subject(:session) { { userinfo: { info: { email: 'alice@example.com', extra: { sub: 'aaa' } }, extra: { raw_info: { sub: 'aaa', 'https://cloudnativedays.jp/roles' => roles } } } } } + let(:roles) { [] } + + it 'returns a successful response' do + get new_speaker_invitation_path(talk_id: talk.id, event: conference.abbr) + expect(response).to(be_successful) + expect(response).to(have_http_status('200')) + end + + it "returns 404 if talk doesn't belong to speaker" do + other_talk = create(:talk2, conference:) + get new_speaker_invitation_path(talk_id: other_talk.id, event: conference.abbr) + expect(response).to(have_http_status('404')) + end + end + + describe 'POST /create' do + subject(:session) { { userinfo: { info: { email: 'alice@example.com', extra: { sub: 'aaa' } }, extra: { raw_info: { sub: 'aaa', 'https://cloudnativedays.jp/roles' => roles } } } } } + let(:roles) { [] } + let(:valid_attributes) { { email: 'co-speaker@example.com', talk_id: talk.id } } + + it 'returns a redirect response for authenticated user' do + post(speaker_invitations_path(event: conference.abbr), params: { speaker_invitation: valid_attributes }) + expect(response).to_not(be_successful) + expect(response).to(have_http_status('302')) + expect(response).to(redirect_to('/cndt2020/speaker_dashboard')) + expect(flash.now[:notice]).to(eq('Invitation sent!')) + end + + it 'creates a new speaker invitation' do + expect { + post(speaker_invitations_path(event: conference.abbr), params: { speaker_invitation: valid_attributes }) + }.to(change(SpeakerInvitation, :count).by(1)) + end + + context 'with invalid parameters' do + let(:invalid_attributes) { { email: '', talk_id: talk.id } } + + it 'does not create a new speaker invitation' do + expect { + post(speaker_invitations_path(event: conference.abbr), params: { speaker_invitation: invalid_attributes }) + }.not_to(change(SpeakerInvitation, :count)) + end + + it 'sets an error flash message' do + post speaker_invitations_path(event: conference.abbr), params: { speaker_invitation: invalid_attributes } + expect(flash.now[:alert]).to(eq('Failed to send invitation.')) + end + + it 'redirects to the speaker dashboard' do + post speaker_invitations_path(event: conference.abbr), params: { speaker_invitation: invalid_attributes } + expect(response).to(redirect_to("/#{conference.abbr}/speaker_dashboard")) + end + end + end +end From d98c5a1d4ed613157499943be5e3f9f4d0604029 Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Sun, 15 Sep 2024 19:01:25 +0900 Subject: [PATCH 02/22] fix: remove invalid config --- config/environments/production.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index 9ddb0335a..8e1e58a41 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -118,14 +118,6 @@ # config.active_record.database_selector = { delay: 2.seconds } # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session - if ENV['REVIEW_APP'] == 'true' - config.routes.default_url_options[:host] = "#{ENV['DREAMKAST_NAMESPACE'].gsub(/dreamkast-dk-/, '')}.dev.cloudnativedays.jp" - elsif ENV['S3_BUCKET'] == 'dreamkast-stg-bucket' - config.routes.default_url_options[:host] = 'staging.dev.cloudnativedays.jp' - elsif ENV['S3_BUCKET'] == 'dreamkast-prod-bucket' - config.routes.default_url_options[:host] = 'event.cloudnativedays.jp' - end - OmniAuth.config.on_failure = Proc.new do |env| message_key = env['omniauth.error.type'] error_description = Rack::Utils.escape(env['omniauth.error'].error_reason) From 5a4e217f5491779a650a5f957c79d2ecd9b6c6cc Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Mon, 16 Sep 2024 08:55:38 +0900 Subject: [PATCH 03/22] fix: fqdn for reviewapp --- config/environments/production.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index 8e1e58a41..67227f636 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -127,7 +127,13 @@ end if ENV['REVIEW_APP'] == 'true' - Rails.application.routes.default_url_options[:host] = "#{ENV['DREAMKAST_NAMESPACE'].gsub(/dreamkast-dk-/, '')}.dev.cloudnativedays.jp" + match = ENV['DREAMKAST_NAMESPACE'].match(/dreamkast-dev-dk-(\d+)-dk/) + if match + pr_number = match[1] + Rails.application.routes.default_url_options[:host] = "dreamkast-dk-#{pr_number}.dev.cloudnativedays.jp.dev.cloudnativedays.jp" + else + raise "DREAMKAST_NAMESPACE is not set correctly (#{ENV['DREAMKAST_NAMESPACE']}). Please set it to dreamkast-dev-dk--dk" + end elsif ENV['S3_BUCKET'] == 'dreamkast-stg-bucket' Rails.application.routes.default_url_options[:host] = 'staging.dev.cloudnativedays.jp' elsif ENV['S3_BUCKET'] == 'dreamkast-prod-bucket' From 7c0dc02e56e46c5351be61b0bda0398aedf6900c Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Mon, 16 Sep 2024 09:00:42 +0900 Subject: [PATCH 04/22] fix: use https if rails env is production --- app/mailers/speaker_invitation_mailer.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/mailers/speaker_invitation_mailer.rb b/app/mailers/speaker_invitation_mailer.rb index 358490927..df8452c6d 100644 --- a/app/mailers/speaker_invitation_mailer.rb +++ b/app/mailers/speaker_invitation_mailer.rb @@ -9,7 +9,13 @@ def invite(conference, invited_by, talk, invitation) @invited_by = invited_by @talk = talk @invitation = invitation - @new_accept_url = speaker_invitation_accepts_invite_url(event: @conference.abbr, token: @invitation.token) + @new_accept_url = speaker_invitation_accepts_invite_url(event: @conference.abbr, token: @invitation.token, protocol:) mail(to: @invitation.email, subject: "#{@conference.name} 共同スピーカー招待") end + + private + + def protocol + Rails.env.production? ? 'https' : 'http' + end end From 01dc460c939e6a98473a8d087df0254a9af542fc Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Mon, 16 Sep 2024 09:33:54 +0900 Subject: [PATCH 05/22] fix: uppy doesn't works well --- app/javascript/packs/application.js | 1 - app/javascript/packs/cndf2023.js | 1 - app/javascript/packs/cndo2021.js | 1 - app/javascript/packs/cnds2024.js | 1 - app/javascript/packs/cndt2023.js | 1 - .../controllers/crop_upload_controller.js | 67 +++++++++++++++++++ app/javascript/packs/controllers/index.js | 2 + app/javascript/packs/cropbox.js | 55 --------------- .../speakers/_form_speaker.html.erb | 3 +- 9 files changed, 71 insertions(+), 61 deletions(-) create mode 100644 app/javascript/packs/controllers/crop_upload_controller.js delete mode 100644 app/javascript/packs/cropbox.js diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index b60bcb118..dc85e2aae 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -17,6 +17,5 @@ import './bootstrap_custom.js' import './timetable.js' import './talks.js' import './speaker_form.js' -import './cropbox.js' //require.context('images/cndo201', true, /\.(png|jpg|jpeg|svg)$/) diff --git a/app/javascript/packs/cndf2023.js b/app/javascript/packs/cndf2023.js index 12f4a18d0..eb5f84f23 100644 --- a/app/javascript/packs/cndf2023.js +++ b/app/javascript/packs/cndf2023.js @@ -18,7 +18,6 @@ import '../stylesheets/cndf2023' import './bootstrap_custom.js' import './talks.js' import './speaker_form.js' -import './cropbox.js' import './timetable.js' diff --git a/app/javascript/packs/cndo2021.js b/app/javascript/packs/cndo2021.js index 8cba89eef..9d7c71537 100644 --- a/app/javascript/packs/cndo2021.js +++ b/app/javascript/packs/cndo2021.js @@ -17,7 +17,6 @@ Rails.start() // import '../stylesheets/cndo2021' import './bootstrap_custom.js' import './talks.js' -import './cropbox.js' import './timetable.js' import "particles.js"; import './speaker_form.js' diff --git a/app/javascript/packs/cnds2024.js b/app/javascript/packs/cnds2024.js index e0f1296f0..228b6be51 100644 --- a/app/javascript/packs/cnds2024.js +++ b/app/javascript/packs/cnds2024.js @@ -16,7 +16,6 @@ import "./controllers/index.js" // import '../stylesheets/cnds2024' import './bootstrap_custom.js' import './talks.js' -import './cropbox.js' import './timetable.js' import "particles.js"; import './speaker_form.js' diff --git a/app/javascript/packs/cndt2023.js b/app/javascript/packs/cndt2023.js index 5a162e292..69d94c110 100644 --- a/app/javascript/packs/cndt2023.js +++ b/app/javascript/packs/cndt2023.js @@ -17,7 +17,6 @@ Rails.start() import '../stylesheets/cndt2023' import './bootstrap_custom.js' import './talks.js' -import './cropbox.js' import './timetable.js' diff --git a/app/javascript/packs/controllers/crop_upload_controller.js b/app/javascript/packs/controllers/crop_upload_controller.js new file mode 100644 index 000000000..e9439a5eb --- /dev/null +++ b/app/javascript/packs/controllers/crop_upload_controller.js @@ -0,0 +1,67 @@ +import { Controller } from "@hotwired/stimulus" +import Cropper from "cropperjs" + +export default class extends Controller { + static targets = [ "fileInput" ] + + connect() { + console.log("1111111111111111111111") + console.log(this.fileInputTargets) + this.fileInputTargets.forEach(fileInput => { + console.log(fileInput) + this.cropUpload(fileInput) + }) + } + + cropUpload(fileInput) { + console.log(fileInput) + let formGroup = fileInput.parentNode + console.log(formGroup) + let hiddenInput = document.querySelector('.upload-data') + console.log(hiddenInput) + let imagePreview = document.querySelector('.image-preview img') + console.log(imagePreview) + + formGroup.removeChild(fileInput) + + let uppy = Uppy.Core({ + autoProceed: true, + }) + .use(Uppy.FileInput, { + target: formGroup, + locale: { strings: { chooseFiles: 'Choose file' } }, + }) + .use(Uppy.Informer, { + target: formGroup, + }) + .use(Uppy.ProgressBar, { + target: imagePreview.parentNode, + }) + .use(Uppy.ThumbnailGenerator, { + thumbnailWidth: 600, + }) + .use(Uppy.XHRUpload, { + endpoint: '/upload/avatar', + }) + + uppy.on('upload-success', function (file, response) { + imagePreview.src = response.uploadURL + + hiddenInput.value = JSON.stringify(response.body['data']) + + let copper = new Cropper(imagePreview, { + aspectRatio: 1, + viewMode: 1, + guides: false, + autoCropArea: 1.0, + background: false, + checkCrossOrigin: false, + crop: function (event) { + let data = JSON.parse(hiddenInput.value) + data['metadata']['crop'] = event.detail + hiddenInput.value = JSON.stringify(data) + } + }) + }) + } +} diff --git a/app/javascript/packs/controllers/index.js b/app/javascript/packs/controllers/index.js index dfd3bf845..76adb70a9 100644 --- a/app/javascript/packs/controllers/index.js +++ b/app/javascript/packs/controllers/index.js @@ -19,3 +19,5 @@ application.register("remove-link-field", RemoveLinkFieldController) import CopyController from "./copy_controller.js" application.register("copy", CopyController) +import CropUploadController from "./crop_upload_controller.js" +application.register("crop-upload", CropUploadController) diff --git a/app/javascript/packs/cropbox.js b/app/javascript/packs/cropbox.js deleted file mode 100644 index 3f98e3175..000000000 --- a/app/javascript/packs/cropbox.js +++ /dev/null @@ -1,55 +0,0 @@ -import Cropper from "cropperjs" - -function cropUpload(fileInput) { - let formGroup = fileInput.parentNode - let hiddenInput = document.querySelector('.upload-data') - let imagePreview = document.querySelector('.image-preview img') - - formGroup.removeChild(fileInput) - - let uppy = Uppy.Core({ - autoProceed: true, - }) - .use(Uppy.FileInput, { - target: formGroup, - locale: { strings: { chooseFiles: 'Choose file' } }, - }) - .use(Uppy.Informer, { - target: formGroup, - }) - .use(Uppy.ProgressBar, { - target: imagePreview.parentNode, - }) - .use(Uppy.ThumbnailGenerator, { - thumbnailWidth: 600, - }) - .use(Uppy.XHRUpload, { - endpoint: '/upload/avatar', - }) - - uppy.on('upload-success', function (file, response) { - imagePreview.src = response.uploadURL - - hiddenInput.value = JSON.stringify(response.body['data']) - - let copper = new Cropper(imagePreview, { - aspectRatio: 1, - viewMode: 1, - guides: false, - autoCropArea: 1.0, - background: false, - checkCrossOrigin: false, - crop: function (event) { - let data = JSON.parse(hiddenInput.value) - data['metadata']['crop'] = event.detail - hiddenInput.value = JSON.stringify(data) - } - }) - }) -} - -window.addEventListener('turbolinks:load', function () { - document.querySelectorAll('#avatar_upload').forEach(function (fileInput) { - cropUpload(fileInput) - }) -}); diff --git a/app/views/speaker_dashboard/speakers/_form_speaker.html.erb b/app/views/speaker_dashboard/speakers/_form_speaker.html.erb index 22f323f80..747056397 100644 --- a/app/views/speaker_dashboard/speakers/_form_speaker.html.erb +++ b/app/views/speaker_dashboard/speakers/_form_speaker.html.erb @@ -40,12 +40,13 @@ <%= form.text_field :github_id, class: "form-control", placeholder: 'cloudnativedaysjp' %>
-
+
<%= form.label :avatar, '講演者写真 - Photographs' %> <%= form.text_field :avatar, type: :file, id: "avatar_upload", + "data-crop-upload-target": "fileInput", class: "form-control" %> From 1fd9e30548a735398e5a67d1bd8ed161e429644e Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Mon, 16 Sep 2024 09:39:12 +0900 Subject: [PATCH 06/22] fix: remove debug log --- app/javascript/packs/controllers/crop_upload_controller.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/javascript/packs/controllers/crop_upload_controller.js b/app/javascript/packs/controllers/crop_upload_controller.js index e9439a5eb..111c9c45d 100644 --- a/app/javascript/packs/controllers/crop_upload_controller.js +++ b/app/javascript/packs/controllers/crop_upload_controller.js @@ -5,8 +5,6 @@ export default class extends Controller { static targets = [ "fileInput" ] connect() { - console.log("1111111111111111111111") - console.log(this.fileInputTargets) this.fileInputTargets.forEach(fileInput => { console.log(fileInput) this.cropUpload(fileInput) From 19c515ba4c9bd35e89ce8fff14541fe2ecb8abd2 Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Mon, 16 Sep 2024 09:39:35 +0900 Subject: [PATCH 07/22] fix: enable uppy by stimulus for public profile and admin profile --- app/views/admin/admin_profiles/edit.html.erb | 3 ++- app/views/public_profiles/_form.html.erb | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/views/admin/admin_profiles/edit.html.erb b/app/views/admin/admin_profiles/edit.html.erb index 445344912..45fcf2533 100644 --- a/app/views/admin/admin_profiles/edit.html.erb +++ b/app/views/admin/admin_profiles/edit.html.erb @@ -16,13 +16,14 @@
<% end %> -
+
<%= form.label :avatar, 'Profile Image' %> <%= form.text_field :avatar, type: :file, id: "avatar_upload", + "data-crop-upload-target": "fileInput", class: "form-control" %> diff --git a/app/views/public_profiles/_form.html.erb b/app/views/public_profiles/_form.html.erb index 0e2e393ca..ee18208b9 100644 --- a/app/views/public_profiles/_form.html.erb +++ b/app/views/public_profiles/_form.html.erb @@ -11,12 +11,13 @@ <% end %>
-
+
<%= form.label :avatar, '参加者アイコン' %> <%= form.text_field :avatar, type: :file, id: "avatar_upload", + "data-crop-upload-target": "fileInput", class: "form-control" %> From 1a2f819c3b6a7f342ddc8ff5c170e86cdddf1cb4 Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Mon, 16 Sep 2024 09:50:38 +0900 Subject: [PATCH 08/22] fix: fqdn --- config/environments/production.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index 67227f636..7cf143ba8 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -130,7 +130,7 @@ match = ENV['DREAMKAST_NAMESPACE'].match(/dreamkast-dev-dk-(\d+)-dk/) if match pr_number = match[1] - Rails.application.routes.default_url_options[:host] = "dreamkast-dk-#{pr_number}.dev.cloudnativedays.jp.dev.cloudnativedays.jp" + Rails.application.routes.default_url_options[:host] = "dreamkast-dk-#{pr_number}.dev.cloudnativedays.jp" else raise "DREAMKAST_NAMESPACE is not set correctly (#{ENV['DREAMKAST_NAMESPACE']}). Please set it to dreamkast-dev-dk--dk" end From 5a42aaf7689d8654ac76669c1474c3b4fa4b5824 Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Mon, 16 Sep 2024 09:51:03 +0900 Subject: [PATCH 09/22] fix: delete resources when deleting talk --- app/models/speaker_invitation.rb | 2 +- app/models/talk.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/speaker_invitation.rb b/app/models/speaker_invitation.rb index fcd5954b3..59a4feadf 100644 --- a/app/models/speaker_invitation.rb +++ b/app/models/speaker_invitation.rb @@ -25,7 +25,7 @@ class SpeakerInvitation < ApplicationRecord belongs_to :talk belongs_to :conference - has_one :speaker_invitation_accept + has_one :speaker_invitation_accept, dependent: :destroy validates :email, presence: true diff --git a/app/models/talk.rb b/app/models/talk.rb index e750f9c30..d4da42a83 100644 --- a/app/models/talk.rb +++ b/app/models/talk.rb @@ -59,7 +59,7 @@ class Talk < ApplicationRecord has_many :profiles, through: :registered_talks has_many :media_package_harvest_jobs has_many :check_in_talks - has_many :speaker_invitations + has_many :speaker_invitations, dependent: :destroy has_many :proposal_items, autosave: true, dependent: :destroy has_many :profiles, through: :registered_talks From 430304f2de3ef1240f19b09275626672c7b9fee6 Mon Sep 17 00:00:00 2001 From: Kazuto Kusama Date: Mon, 16 Sep 2024 11:05:37 +0900 Subject: [PATCH 10/22] Fix redirect issue --- app/controllers/auth0_controller.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/controllers/auth0_controller.rb b/app/controllers/auth0_controller.rb index b93d9cb1f..fb8ad2da1 100644 --- a/app/controllers/auth0_controller.rb +++ b/app/controllers/auth0_controller.rb @@ -7,7 +7,13 @@ def callback # Redirect to the URL you want after successful auth if request.env['omniauth.origin'] - redirect_to("#{request.env['omniauth.origin']}?state=#{params[:state]}") + uri = URI.parse(request.env['omniauth.origin']) + uri.query = if uri.params.nil? + "state=#{params[:state]}" + else + "#{uri.query}&state=#{params[:state]}" + end + redirect_to(uri.to_s) else redirect_to('/') end From d05032ae9959c593101f311004f488d523112a63 Mon Sep 17 00:00:00 2001 From: Kazuto Kusama Date: Mon, 16 Sep 2024 11:24:52 +0900 Subject: [PATCH 11/22] Fix --- app/controllers/auth0_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/auth0_controller.rb b/app/controllers/auth0_controller.rb index fb8ad2da1..fdd263943 100644 --- a/app/controllers/auth0_controller.rb +++ b/app/controllers/auth0_controller.rb @@ -8,7 +8,7 @@ def callback if request.env['omniauth.origin'] uri = URI.parse(request.env['omniauth.origin']) - uri.query = if uri.params.nil? + uri.query = if uri.query.nil? "state=#{params[:state]}" else "#{uri.query}&state=#{params[:state]}" From 4c5cb0553705a7456ff5fd54f122ba273eda9faf Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Wed, 18 Sep 2024 20:02:43 +0900 Subject: [PATCH 12/22] fix: redirect --- app/controllers/speaker_invitation_accepts_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/speaker_invitation_accepts_controller.rb b/app/controllers/speaker_invitation_accepts_controller.rb index e98405d96..8a5e37527 100644 --- a/app/controllers/speaker_invitation_accepts_controller.rb +++ b/app/controllers/speaker_invitation_accepts_controller.rb @@ -5,7 +5,7 @@ class SpeakerInvitationAcceptsController < ApplicationController skip_before_action :logged_in_using_omniauth?, only: [:invite] def invite - return redirect_to(speaker_invitation_accepts_path) if from_auth0?(params) + return redirect_to(new_speaker_invitation_accept_path(token: params[:token])) if from_auth0?(params) @conference = Conference.find_by(abbr: params[:event]) @speaker_invitation = SpeakerInvitation.find_by(token: params[:token]) end From 28dbfcb1641786bb2ca0f0422a25f603ee838450 Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Wed, 18 Sep 2024 21:05:08 +0900 Subject: [PATCH 13/22] chore: add validation --- .../speaker_invitations_controller.rb | 7 +++--- app/models/speaker_invitation.rb | 22 +++++++++++++++++++ app/views/speaker_invitations/new.html.erb | 3 +++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/app/controllers/speaker_invitations_controller.rb b/app/controllers/speaker_invitations_controller.rb index e3b732d21..2c6fb677c 100644 --- a/app/controllers/speaker_invitations_controller.rb +++ b/app/controllers/speaker_invitations_controller.rb @@ -18,11 +18,12 @@ def create @invitation.expires_at = 1.days.from_now # 有効期限を1日後に設定 if @invitation.save SpeakerInvitationMailer.invite(@conference, @speaker, @invitation.talk, @invitation).deliver_now - flash.now[:notice] = 'Invitation sent!' + flash[:notice] = 'Invitation sent!' + redirect_to("/#{@conference.abbr}/speaker_dashboard") else - flash.now[:alert] = 'Failed to send invitation.' + flash[:alert] = "#{@invitation.email} への招待メール送信に失敗しました: #{@invitation.errors.full_messages.join(', ')}" + redirect_to(new_speaker_invitation_path(event: @conference.abbr, talk_id: @invitation.talk_id)) end - redirect_to("/#{@conference.abbr}/speaker_dashboard") end end diff --git a/app/models/speaker_invitation.rb b/app/models/speaker_invitation.rb index 59a4feadf..1cfdbf00f 100644 --- a/app/models/speaker_invitation.rb +++ b/app/models/speaker_invitation.rb @@ -30,4 +30,26 @@ class SpeakerInvitation < ApplicationRecord validates :email, presence: true validates :token, presence: true + validate :unique_talk_and_mail_with_acceptance + validate :unique_talk_and_mail_with_non_expired_invitation + + private + + def unique_talk_and_mail_with_acceptance + if SpeakerInvitation.joins(:speaker_invitation_accept) + .where(talk_id: talk_id, email: email) + .exists? + errors.add(:base, "指定したプロポーザル(セッション)について、既に承認済の招待があります") + end + end + + def unique_talk_and_mail_with_non_expired_invitation + if SpeakerInvitation.left_joins(:speaker_invitation_accept) + .where(talk_id: talk_id, email: email) + .where('expires_at > ?', Time.current) + .where(speaker_invitation_accepts: { id: nil }) + .exists? + errors.add(:base, "指定したプロポーザル(セッション)について、既に招待済みです") + end + end end diff --git a/app/views/speaker_invitations/new.html.erb b/app/views/speaker_invitations/new.html.erb index bea920fe1..820293628 100644 --- a/app/views/speaker_invitations/new.html.erb +++ b/app/views/speaker_invitations/new.html.erb @@ -1,6 +1,9 @@
+ <% flash.each do |key, value| %> + + <% end %>

共同スピーカーに招待メールを送信する

<%= form_with(model: @speaker_invitation) do |form| %> From 5c5815fa0453eeb8bb2eab458721ef7dfd6fd7cd Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Wed, 18 Sep 2024 23:05:22 +0900 Subject: [PATCH 14/22] fix: rubocop --- app/models/speaker_invitation.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/speaker_invitation.rb b/app/models/speaker_invitation.rb index 1cfdbf00f..3db29b784 100644 --- a/app/models/speaker_invitation.rb +++ b/app/models/speaker_invitation.rb @@ -37,19 +37,19 @@ class SpeakerInvitation < ApplicationRecord def unique_talk_and_mail_with_acceptance if SpeakerInvitation.joins(:speaker_invitation_accept) - .where(talk_id: talk_id, email: email) + .where(talk_id:, email:) .exists? - errors.add(:base, "指定したプロポーザル(セッション)について、既に承認済の招待があります") + errors.add(:base, '指定したプロポーザル(セッション)について、既に承認済の招待があります') end end def unique_talk_and_mail_with_non_expired_invitation if SpeakerInvitation.left_joins(:speaker_invitation_accept) - .where(talk_id: talk_id, email: email) + .where(talk_id:, email:) .where('expires_at > ?', Time.current) .where(speaker_invitation_accepts: { id: nil }) .exists? - errors.add(:base, "指定したプロポーザル(セッション)について、既に招待済みです") + errors.add(:base, '指定したプロポーザル(セッション)について、既に招待済みです') end end end From 74e6744b6d0575ea12bd124165f7e7c0d3c31ef8 Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Wed, 18 Sep 2024 23:11:33 +0900 Subject: [PATCH 15/22] fix: don't set default value --- app/controllers/speaker_invitation_accepts_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/speaker_invitation_accepts_controller.rb b/app/controllers/speaker_invitation_accepts_controller.rb index 8a5e37527..6c3da1810 100644 --- a/app/controllers/speaker_invitation_accepts_controller.rb +++ b/app/controllers/speaker_invitation_accepts_controller.rb @@ -24,7 +24,7 @@ def new end @talk = @speaker_invitation.talk @proposal = @talk.proposal - @speaker = Speaker.new(conference: @conference, email: current_user[:info][:email], name: current_user[:info][:name]) + @speaker = Speaker.new(conference: @conference, email: current_user[:info][:email]) end def create @@ -33,7 +33,7 @@ def create @conference = Conference.find_by(abbr: params[:event]) @speaker_invitation = SpeakerInvitation.find(params[:speaker][:speaker_invitation_id]) - speaker_param = speaker_invitation_accept_params.merge(conference: @conference, email: current_user[:info][:email], name: current_user[:info][:name]) + speaker_param = speaker_invitation_accept_params.merge(conference: @conference, email: current_user[:info][:email]) speaker_param.delete(:speaker_invitation_id) @speaker = Speaker.new(speaker_param) From d2b9b129d83bfccb86d6239e1281a0d0bbab8c99 Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Wed, 18 Sep 2024 23:19:28 +0900 Subject: [PATCH 16/22] fix: test --- spec/requests/speaker_invitation_accepts_controller_spec.rb | 2 +- spec/requests/speaker_invitations_controller_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/requests/speaker_invitation_accepts_controller_spec.rb b/spec/requests/speaker_invitation_accepts_controller_spec.rb index e9e66023c..7203b7279 100644 --- a/spec/requests/speaker_invitation_accepts_controller_spec.rb +++ b/spec/requests/speaker_invitation_accepts_controller_spec.rb @@ -27,7 +27,7 @@ it 'redirects if coming from Auth0' do get speaker_invitation_accepts_invite_path(event: conference.abbr, token: speaker_invitation.token, state: 'some_state') expect(response).to(have_http_status('302')) - expect(response).to(redirect_to(speaker_invitation_accepts_path)) + expect(response).to(redirect_to(new_speaker_invitation_accept_path(token: speaker_invitation.token))) end end diff --git a/spec/requests/speaker_invitations_controller_spec.rb b/spec/requests/speaker_invitations_controller_spec.rb index 8aed3ce71..4f8d766b8 100644 --- a/spec/requests/speaker_invitations_controller_spec.rb +++ b/spec/requests/speaker_invitations_controller_spec.rb @@ -63,12 +63,12 @@ it 'sets an error flash message' do post speaker_invitations_path(event: conference.abbr), params: { speaker_invitation: invalid_attributes } - expect(flash.now[:alert]).to(eq('Failed to send invitation.')) + expect(flash[:alert]).to(eq(' への招待メール送信に失敗しました: Emailを入力してください')) end it 'redirects to the speaker dashboard' do post speaker_invitations_path(event: conference.abbr), params: { speaker_invitation: invalid_attributes } - expect(response).to(redirect_to("/#{conference.abbr}/speaker_dashboard")) + expect(response).to(redirect_to(new_speaker_invitation_path(event: conference.abbr, talk_id: talk.id))) end end end From 6ebdd4d580fbacbdf2e709f63cae837ceb90300b Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Wed, 18 Sep 2024 23:53:33 +0900 Subject: [PATCH 17/22] fix: invited user is already speaker, use it --- .../speaker_invitation_accepts_controller.rb | 14 ++++++++++++-- app/views/speaker_invitation_accepts/new.html.erb | 10 ++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/controllers/speaker_invitation_accepts_controller.rb b/app/controllers/speaker_invitation_accepts_controller.rb index 6c3da1810..ed3daaa15 100644 --- a/app/controllers/speaker_invitation_accepts_controller.rb +++ b/app/controllers/speaker_invitation_accepts_controller.rb @@ -7,6 +7,7 @@ class SpeakerInvitationAcceptsController < ApplicationController def invite return redirect_to(new_speaker_invitation_accept_path(token: params[:token])) if from_auth0?(params) @conference = Conference.find_by(abbr: params[:event]) + @speaker = Speaker.find_by(conference: @conference, email: current_user[:info][:email]) @speaker_invitation = SpeakerInvitation.find_by(token: params[:token]) end @@ -24,7 +25,11 @@ def new end @talk = @speaker_invitation.talk @proposal = @talk.proposal - @speaker = Speaker.new(conference: @conference, email: current_user[:info][:email]) + @speaker = if Speaker.where(email: current_user[:info][:email], conference: @conference).exists? + Speaker.find_by(conference: @conference, email: current_user[:info][:email]) + else + Speaker.new(conference: @conference, email: current_user[:info][:email]) + end end def create @@ -36,7 +41,12 @@ def create speaker_param = speaker_invitation_accept_params.merge(conference: @conference, email: current_user[:info][:email]) speaker_param.delete(:speaker_invitation_id) - @speaker = Speaker.new(speaker_param) + @speaker = if Speaker.where(email: current_user[:info][:email], conference: @conference).exists? + Speaker.find_by(conference: @conference, email: current_user[:info][:email]) + else + Speaker.new(conference: @conference, email: current_user[:info][:email]) + end + @speaker.update!(speaker_param) @speaker.save! @talk = @speaker_invitation.talk diff --git a/app/views/speaker_invitation_accepts/new.html.erb b/app/views/speaker_invitation_accepts/new.html.erb index 45f2b52fd..b938ca1a1 100644 --- a/app/views/speaker_invitation_accepts/new.html.erb +++ b/app/views/speaker_invitation_accepts/new.html.erb @@ -9,10 +9,16 @@ <% end %> <% if flash.keys.size == 0 %> - プロポーザル 「<%= @talk.title %> (<%= @talk.speaker_names.join('/') %>)」に共同スピーカーとして登録します。登録する場合は下記の登壇者情報を入力し、「登録する」ボタンをクリックしてください。 +

プロポーザル 「<%= @talk.title %> (<%= @talk.speaker_names.join('/') %>)」に共同スピーカーとして登録します。

+ <% if @speaker.present? %> +

スピーカーとして既に登録済みのため、フォームに既存の情報が入力済みです。

+

問題なければ「登録する」ボタンをクリックしてください。

+ <% else %> +

登録する場合はフォームに登壇者情報を入力し、「登録する」ボタンをクリックしてください。

+ <% end %> - <%= form_with(url: speaker_invitation_accepts_path, model: @speaker) do |form| %> + <%= form_with(url: speaker_invitation_accepts_path, model: @speaker, method: :post) do |form| %> <%= form.hidden_field :speaker_invitation_id, value: @speaker_invitation.id %>
From 8151d551ad5aa60889a7dc72438ef470ea8d7268 Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Thu, 19 Sep 2024 16:58:32 +0900 Subject: [PATCH 18/22] fix: condition --- app/views/speaker_invitation_accepts/new.html.erb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/speaker_invitation_accepts/new.html.erb b/app/views/speaker_invitation_accepts/new.html.erb index b938ca1a1..e3ec01d08 100644 --- a/app/views/speaker_invitation_accepts/new.html.erb +++ b/app/views/speaker_invitation_accepts/new.html.erb @@ -10,11 +10,11 @@ <% if flash.keys.size == 0 %>

プロポーザル 「<%= @talk.title %> (<%= @talk.speaker_names.join('/') %>)」に共同スピーカーとして登録します。

- <% if @speaker.present? %> + <% if @speaker.new_recor? %> +

登録する場合はフォームに登壇者情報を入力し、「登録する」ボタンをクリックしてください。

+ <% else %>

スピーカーとして既に登録済みのため、フォームに既存の情報が入力済みです。

問題なければ「登録する」ボタンをクリックしてください。

- <% else %> -

登録する場合はフォームに登壇者情報を入力し、「登録する」ボタンをクリックしてください。

<% end %> From 8a6fe5ea3a149cb9848e50de032b4d9bfe39b473 Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Thu, 19 Sep 2024 17:07:19 +0900 Subject: [PATCH 19/22] fix: avoid error of trivy 2024-09-19T08:04:03Z FATAL Fatal error image scan error: scan error: scan failed: failed analysis: analyze error: pipeline error: failed to analyze layer (sha256:7a2f3aacb6d75810011bdefa5d8a56afb01ded3afae301002ea1e37637123115): post analysis error: post analysis error: Unable to initialize the Java DB: Java DB update failed: DB download error: OCI repository error: 1 error occurred: --- .github/workflows/build-branch.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml index a8955fa2b..5ec118ca2 100644 --- a/.github/workflows/build-branch.yml +++ b/.github/workflows/build-branch.yml @@ -91,12 +91,12 @@ jobs: --tag ${image_tag_sha} \ ${image_tag_sha}-amd64 - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@master - with: - image-ref: '${{ steps.login-ecr.outputs.registry }}/dreamkast-ecs:${{ github.sha }}' - format: 'table' - exit-code: '0' - ignore-unfixed: true - vuln-type: 'os,library' - severity: 'CRITICAL,HIGH' +# - name: Run Trivy vulnerability scanner +# uses: aquasecurity/trivy-action@master +# with: +# image-ref: '${{ steps.login-ecr.outputs.registry }}/dreamkast-ecs:${{ github.sha }}' +# format: 'table' +# exit-code: '0' +# ignore-unfixed: true +# vuln-type: 'os,library' +# severity: 'CRITICAL,HIGH' From 99916e80ac45c1b306a7899ee6f055fe27340188 Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Thu, 19 Sep 2024 17:14:06 +0900 Subject: [PATCH 20/22] fix: typo --- app/views/speaker_invitation_accepts/new.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/speaker_invitation_accepts/new.html.erb b/app/views/speaker_invitation_accepts/new.html.erb index e3ec01d08..e7d9b4069 100644 --- a/app/views/speaker_invitation_accepts/new.html.erb +++ b/app/views/speaker_invitation_accepts/new.html.erb @@ -10,7 +10,7 @@ <% if flash.keys.size == 0 %>

プロポーザル 「<%= @talk.title %> (<%= @talk.speaker_names.join('/') %>)」に共同スピーカーとして登録します。

- <% if @speaker.new_recor? %> + <% if @speaker.new_record? %>

登録する場合はフォームに登壇者情報を入力し、「登録する」ボタンをクリックしてください。

<% else %>

スピーカーとして既に登録済みのため、フォームに既存の情報が入力済みです。

From d86d48e6bb70a60e19f95f8a44a5d1d9f20ba177 Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Fri, 20 Sep 2024 00:05:46 +0900 Subject: [PATCH 21/22] fix: don't display notice at speaker invitation page --- app/controllers/speaker_dashboard/speakers_controller.rb | 3 ++- app/views/speaker_dashboards/show.html.erb | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/controllers/speaker_dashboard/speakers_controller.rb b/app/controllers/speaker_dashboard/speakers_controller.rb index 4ce40939a..83c5be9be 100644 --- a/app/controllers/speaker_dashboard/speakers_controller.rb +++ b/app/controllers/speaker_dashboard/speakers_controller.rb @@ -51,7 +51,8 @@ def create r.each do |talk| SpeakerMailer.cfp_registered(@conference, @speaker, talk).deliver_later end - format.html { redirect_to("/#{@conference.abbr}/speaker_dashboard", notice: 'Speaker was successfully created.') } + flash.now[:notice] = 'スピーカーもしくはプロポーザルの作成・更新に成功しました。' + format.html { redirect_to("/#{@conference.abbr}/speaker_dashboard") } format.json { render(:show, status: :created, location: @speaker) } else format.html { render(:new) } diff --git a/app/views/speaker_dashboards/show.html.erb b/app/views/speaker_dashboards/show.html.erb index a70e3fb4e..2a7efee2f 100644 --- a/app/views/speaker_dashboards/show.html.erb +++ b/app/views/speaker_dashboards/show.html.erb @@ -2,6 +2,14 @@

登壇者ダッシュボード

+ <% flash.each do |message_type, message| %> + + <% end %> + +
<%= button_to 'CFP要項を確認する', speakers_guidance_path, {method: :get, class: "btn btn-secondary btn-xl inline" } %>
From 559a5798b3a6daeaa82632391809b546d462ecb6 Mon Sep 17 00:00:00 2001 From: Ryo Takaishi Date: Fri, 20 Sep 2024 09:06:13 +0900 Subject: [PATCH 22/22] fix: spec --- spec/requests/speaker_dashboard_spec.rb | 27 ++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/spec/requests/speaker_dashboard_spec.rb b/spec/requests/speaker_dashboard_spec.rb index 842197203..046a7232d 100644 --- a/spec/requests/speaker_dashboard_spec.rb +++ b/spec/requests/speaker_dashboard_spec.rb @@ -56,7 +56,14 @@ describe 'speaker logged in' do before do - allow_any_instance_of(ActionDispatch::Request::Session).to(receive(:[]).and_return(admin_userinfo[:userinfo])) + ActionDispatch::Request::Session.define_method(:original, ActionDispatch::Request::Session.instance_method(:[])) + allow_any_instance_of(ActionDispatch::Request::Session).to(receive(:[]) do |*arg| + if arg[1] == :userinfo + admin_userinfo[:userinfo] + else + arg[0].send(:original, arg[1]) + end + end) end describe "speaker doesn't registered" do @@ -86,7 +93,14 @@ context 'CNDT2020 is registered and speaker entry is enabled' do before do - allow_any_instance_of(ActionDispatch::Request::Session).to(receive(:[]).and_return(admin_userinfo[:userinfo])) + ActionDispatch::Request::Session.define_method(:original, ActionDispatch::Request::Session.instance_method(:[])) + allow_any_instance_of(ActionDispatch::Request::Session).to(receive(:[]) do |*arg| + if arg[1] == :userinfo + admin_userinfo[:userinfo] + else + arg[0].send(:original, arg[1]) + end + end) end context 'CFP result is visible' do @@ -197,7 +211,14 @@ before do create(:cndt2020) create(:alice, :on_cndt2020) - allow_any_instance_of(ActionDispatch::Request::Session).to(receive(:[]).and_return(admin_userinfo[:userinfo])) + ActionDispatch::Request::Session.define_method(:original, ActionDispatch::Request::Session.instance_method(:[])) + allow_any_instance_of(ActionDispatch::Request::Session).to(receive(:[]) do |*arg| + if arg[1] == :userinfo + admin_userinfo[:userinfo] + else + arg[0].send(:original, arg[1]) + end + end) end let!(:alice) { create(:speaker_alice) }