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' 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/auth0_controller.rb b/app/controllers/auth0_controller.rb index b93d9cb1f..fdd263943 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.query.nil? + "state=#{params[:state]}" + else + "#{uri.query}&state=#{params[:state]}" + end + redirect_to(uri.to_s) else redirect_to('/') end 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/controllers/speaker_invitation_accepts_controller.rb b/app/controllers/speaker_invitation_accepts_controller.rb new file mode 100644 index 000000000..ed3daaa15 --- /dev/null +++ b/app/controllers/speaker_invitation_accepts_controller.rb @@ -0,0 +1,88 @@ +class SpeakerInvitationAcceptsController < ApplicationController + include SecuredSpeaker + before_action :set_speaker + + skip_before_action :logged_in_using_omniauth?, only: [:invite] + + 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 + + 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 = 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 + 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]) + speaker_param.delete(:speaker_invitation_id) + + @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 + @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..2c6fb677c --- /dev/null +++ b/app/controllers/speaker_invitations_controller.rb @@ -0,0 +1,35 @@ +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[:notice] = 'Invitation sent!' + redirect_to("/#{@conference.abbr}/speaker_dashboard") + else + flash[:alert] = "#{@invitation.email} への招待メール送信に失敗しました: #{@invitation.errors.full_messages.join(', ')}" + redirect_to(new_speaker_invitation_path(event: @conference.abbr, talk_id: @invitation.talk_id)) + end + end + end + + private + + def speaker_invitation_params + params.require(:speaker_invitation).permit(:email, :talk_id) + end +end 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..111c9c45d --- /dev/null +++ b/app/javascript/packs/controllers/crop_upload_controller.js @@ -0,0 +1,65 @@ +import { Controller } from "@hotwired/stimulus" +import Cropper from "cropperjs" + +export default class extends Controller { + static targets = [ "fileInput" ] + + connect() { + 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/mailers/speaker_invitation_mailer.rb b/app/mailers/speaker_invitation_mailer.rb new file mode 100644 index 000000000..df8452c6d --- /dev/null +++ b/app/mailers/speaker_invitation_mailer.rb @@ -0,0 +1,21 @@ +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, protocol:) + mail(to: @invitation.email, subject: "#{@conference.name} 共同スピーカー招待") + end + + private + + def protocol + Rails.env.production? ? 'https' : 'http' + end +end diff --git a/app/models/speaker_invitation.rb b/app/models/speaker_invitation.rb new file mode 100644 index 000000000..3db29b784 --- /dev/null +++ b/app/models/speaker_invitation.rb @@ -0,0 +1,55 @@ +# == 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, dependent: :destroy + + + 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:, 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:, email:) + .where('expires_at > ?', Time.current) + .where(speaker_invitation_accepts: { id: nil }) + .exists? + errors.add(:base, '指定したプロポーザル(セッション)について、既に招待済みです') + end + end +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..d4da42a83 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, dependent: :destroy has_many :proposal_items, autosave: true, dependent: :destroy has_many :profiles, through: :registered_talks 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" %> 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..747056397 --- /dev/null +++ b/app/views/speaker_dashboard/speakers/_form_speaker.html.erb @@ -0,0 +1,62 @@ +
+ <%= 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", + "data-crop-upload-target": "fileInput", + 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_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" } %>
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..e7d9b4069 --- /dev/null +++ b/app/views/speaker_invitation_accepts/new.html.erb @@ -0,0 +1,35 @@ +
+
+
+

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

+ <% flash.each do |type, message| %> + + <% end %> + + <% if flash.keys.size == 0 %> +

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

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

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

+ <% else %> +

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

+

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

+ <% end %> + + + <%= form_with(url: speaker_invitation_accepts_path, model: @speaker, method: :post) 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..820293628 --- /dev/null +++ b/app/views/speaker_invitations/new.html.erb @@ -0,0 +1,23 @@ +
+
+
+ <% flash.each do |key, value| %> + + <% end %> +

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

+ + <%= 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..7cf143ba8 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -125,3 +125,17 @@ Rack::Response.new(['302 Moved'], 302, 'Location' => new_path).finish end end + +if ENV['REVIEW_APP'] == 'true' + 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" + 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' + 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_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) } 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..7203b7279 --- /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(new_speaker_invitation_accept_path(token: speaker_invitation.token))) + 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..4f8d766b8 --- /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[: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(new_speaker_invitation_path(event: conference.abbr, talk_id: talk.id))) + end + end + end +end