diff --git a/app/controllers/admin/casting_call_interviews_controller.rb b/app/controllers/admin/casting_call_interviews_controller.rb
new file mode 100644
index 0000000..c9f0a92
--- /dev/null
+++ b/app/controllers/admin/casting_call_interviews_controller.rb
@@ -0,0 +1,64 @@
+class Admin::CastingCallInterviewsController < Admin::ApplicationController
+ before_action :set_casting_call_interview, only: [:edit, :update, :show, :complete]
+ before_action :build_casting_call_interview, only: [:new, :create]
+
+ def index
+ @casting_call_interviews = casting_call_interviews.order_by_recent.paginate(page: params[:page])
+ end
+
+ def new
+ @accounts = accounts
+ end
+
+ def create
+ @casting_call_interview.attributes = casting_call_interview_params
+
+ if @casting_call_interview.save
+ redirect_to [:admin, :casting_call_interviews], notice: t(".notice")
+ else
+ render :new
+ end
+ end
+
+ def edit
+ @accounts = accounts
+ end
+
+ def update
+ if @casting_call_interview.update(casting_call_interview_params)
+ redirect_to [:admin, :casting_call_interviews], notice: t(".notice")
+ else
+ render :edit
+ end
+ end
+
+ def complete
+ if @casting_call_interview.update(interviewed_at: Time.zone.now)
+ redirect_to [:admin, :casting_call_interviews], notice: t(".notice")
+ else
+ redirect_to [:admin, :casting_call_interviews], notice: t(".alert")
+ end
+ end
+
+ private
+
+ def casting_call_interview_params
+ params.require(:casting_call_interview).permit(:casting_call_id, :performer_name, :interview_date, :zoom_meeting_url)
+ end
+
+ def casting_call_interviews
+ policy_scope CastingCallInterview
+ end
+
+ def set_casting_call_interview
+ @casting_call_interview = authorize policy_scope(CastingCallInterview).find(params[:id])
+ end
+
+ def accounts
+ policy_scope Account
+ end
+
+ def build_casting_call_interview
+ @casting_call_interview = authorize policy_scope(CastingCallInterview).build
+ end
+end
\ No newline at end of file
diff --git a/app/controllers/casting_call_interviews_controller.rb b/app/controllers/casting_call_interviews_controller.rb
new file mode 100644
index 0000000..0bdb9f2
--- /dev/null
+++ b/app/controllers/casting_call_interviews_controller.rb
@@ -0,0 +1,28 @@
+class CastingCallInterviewsController < ApplicationController
+ before_action :set_project
+ before_action :set_casting_call_interview, only: [:show]
+
+ include ProjectLayout
+
+ def index
+ @casting_call_interviews = casting_call_interviews.completed.order_by_recent.paginate(page: params[:page])
+ end
+
+ def show
+ @files = @casting_call_interview.files.paginate(page: params[:page])
+ end
+
+ private
+
+ def set_project
+ @project = policy_scope(Project).find(params[:project_id])
+ end
+
+ def set_casting_call_interview
+ @casting_call_interview = authorize casting_call_interviews.find(params[:id])
+ end
+
+ def casting_call_interviews
+ authorize policy_scope(CastingCallInterview)
+ end
+end
diff --git a/app/controllers/casting_calls_controller.rb b/app/controllers/casting_calls_controller.rb
new file mode 100644
index 0000000..c06d729
--- /dev/null
+++ b/app/controllers/casting_calls_controller.rb
@@ -0,0 +1,76 @@
+class CastingCallsController < ApplicationController
+ layout "project"
+
+ before_action :set_project
+ before_action :build_casting_call, only: [:new, :create]
+ before_action :set_casting_call, only: [:show, :edit, :update, :cancel]
+
+ def index
+ @casting_calls = casting_calls.order_by_recent.paginate(page: params[:page])
+ end
+
+ def new
+ end
+
+ def create
+ @casting_call.attributes = casting_call_params_with_email
+
+ if @casting_call.save
+ log_create_analytics
+ castme_url = url_for([@project, @casting_call])
+ SubmitHubspotFormJob.perform_later(email: @casting_call.user_email, castme_url: castme_url, form_guid: ENV["HUBSPOT_CASTING_CALL_REQUEST_FORM_GUID"])
+ else
+ render :new
+ end
+ end
+
+ def show
+ render layout: 'application'
+ end
+
+ def edit
+ end
+
+ def update
+ if @casting_call.update(casting_call_params)
+ redirect_to [@project, :casting_calls], notice: t(".notice")
+ else
+ render :edit
+ end
+ end
+
+ def cancel
+ @casting_call.update(cancelled_at: Time.zone.now)
+ redirect_to [@project, :casting_calls], notice: t(".notice")
+ end
+
+ private
+
+ def casting_call_params
+ params.require(:casting_call).permit(:title, :description, :project_description, :interview_instructions, :interview_requirements, :questions)
+ end
+
+ def casting_call_params_with_email
+ casting_call_params.merge(user_email: Current.user.email)
+ end
+
+ def set_project
+ @project = policy_scope(Project).find(params[:project_id])
+ end
+
+ def set_casting_call
+ @casting_call = authorize casting_calls.find(params[:id])
+ end
+
+ def casting_calls
+ authorize policy_scope(@project.casting_calls)
+ end
+
+ def build_casting_call
+ @casting_call = authorize @project.casting_calls.build
+ end
+
+ def log_create_analytics
+ TrackAnalyticsJob.perform_later(Current.user, Current.account, :track_create_casting_call, user_agent: request.user_agent, user_ip: request.remote_ip)
+ end
+end
diff --git a/app/controllers/interview_downloads_controller.rb b/app/controllers/interview_downloads_controller.rb
new file mode 100644
index 0000000..46a9b57
--- /dev/null
+++ b/app/controllers/interview_downloads_controller.rb
@@ -0,0 +1,30 @@
+class InterviewDownloadsController < ApplicationController
+ include ProjectContext
+
+ before_action :set_project, only: [:create]
+ before_action :set_casting_call_interview, only: :create
+
+ include ProjectLayout
+
+ def create
+ download = @project.downloads.create!(name: @casting_call_interview.zip_file_name, release_type: "CastingCallInterview")
+
+ other_downloads_in_progress = @project.downloads.unfinished_desc_order.offset(1)
+
+ if other_downloads_in_progress.any?
+ in_progress_downloads_details = render_to_string "_other_pending_downloads", locals: { downloads: other_downloads_in_progress, release_type: "CastingCallInterview" }, :layout => false
+ ProjectsChannel.broadcast_download_generation_update(download, in_progress_downloads_details)
+ else
+ ProjectsChannel.broadcast_download_generation_update(download, I18n.t("interview_downloads.download.pending", release_type: "Casting Call Interview"))
+ end
+
+ GenerateInterviewFilesZipJob.perform_later(@project, download, @casting_call_interview)
+ end
+
+ private
+
+ def set_casting_call_interview
+ authorize(Download)
+ @casting_call_interview = policy_scope(@project.casting_call_interviews).find(params[:casting_call_interview_id])
+ end
+end
diff --git a/app/controllers/public/casting_call_interviews_controller.rb b/app/controllers/public/casting_call_interviews_controller.rb
new file mode 100644
index 0000000..8884ea9
--- /dev/null
+++ b/app/controllers/public/casting_call_interviews_controller.rb
@@ -0,0 +1,25 @@
+class Public::CastingCallInterviewsController < Public::BaseController
+ skip_after_action :verify_authorized
+ before_action :set_casting_call_interview, only: [:show, :update]
+
+ def show
+ end
+
+ def update
+ if @casting_call_interview.update(casting_call_interview_params)
+ redirect_to casting_call_interview_url(token: @casting_call_interview.token), notice: t(".notice")
+ else
+ render :show
+ end
+ end
+
+ private
+
+ def set_casting_call_interview
+ @casting_call_interview = CastingCallInterview.find_by_token(params[:token])
+ end
+
+ def casting_call_interview_params
+ params.require(:casting_call_interview).permit(files: [])
+ end
+end
diff --git a/app/controllers/public/casting_calls_controller.rb b/app/controllers/public/casting_calls_controller.rb
new file mode 100644
index 0000000..66bf280
--- /dev/null
+++ b/app/controllers/public/casting_calls_controller.rb
@@ -0,0 +1,14 @@
+class Public::CastingCallsController < Public::BaseController
+ skip_after_action :verify_authorized
+ before_action :set_casting_call, only: [:show]
+
+ def show
+ render layout: 'application'
+ end
+
+ private
+
+ def set_casting_call
+ @casting_call = CastingCall.find_by_token(params[:token])
+ end
+end
diff --git a/app/jobs/generate_interview_files_zip_job.rb b/app/jobs/generate_interview_files_zip_job.rb
new file mode 100644
index 0000000..a580427
--- /dev/null
+++ b/app/jobs/generate_interview_files_zip_job.rb
@@ -0,0 +1,43 @@
+class GenerateInterviewFilesZipJob < ApplicationJob
+ queue_as :default
+ include Rails.application.routes.url_helpers
+ include ActionView::Helpers::UrlHelper
+
+ before_perform do |job|
+ @project = job.arguments.first
+ @download = job.arguments.second
+ @casting_call_interview = job.arguments.third
+ @download.update!(status: :pending)
+ end
+
+ def perform(project, download, casting_call_interview)
+ ::InterviewFilesCollectionService.new(casting_call_interview.files, @download.name).build do |dir, files|
+ zipfile_name = "#{dir}/#{@download.name}.zip"
+ Zip::File.open(zipfile_name, Zip::File::CREATE) do |zipfile|
+ files.each do |attachment|
+ zipfile.add(attachment, File.join("#{dir}/", attachment))
+ end
+ end
+
+ @download.file.attach(io: File.open(zipfile_name), filename: @download.name)
+ end
+ rescue StandardError => e
+ Raven.extra_context(
+ message: "Failed to generate download for project (##{project.id})",
+ release_type: "CastingCallInterview"
+ )
+
+ @download.failure!
+ ProjectsChannel.broadcast_download_generation_update(@download, I18n.t("interview_downloads.download.failure"))
+ end
+
+ after_perform do |job|
+ if @download.pending? && @download.file.attached?
+ @download.success!
+
+ downloads_folder_link = link_to("Files > Downloads", project_downloads_path(I18n.locale, @project))
+ download_button = link_to("Download", rails_blob_path(@download.file, disposition: "attachment", only_path: true), class: "btn btn-success", target: :_blank)
+ ProjectsChannel.broadcast_download_generation_update(@download, I18n.t("interview_downloads.download.success", downloads_folder_link: downloads_folder_link, download_button: download_button, release_type: "Casting Call Interview"))
+ end
+ end
+end
diff --git a/app/models/casting_call.rb b/app/models/casting_call.rb
new file mode 100644
index 0000000..1ae7d07
--- /dev/null
+++ b/app/models/casting_call.rb
@@ -0,0 +1,18 @@
+class CastingCall < ApplicationRecord
+ belongs_to :project
+ has_many :casting_call_interviews, dependent: :destroy
+
+ has_secure_token
+
+ def status
+ if cancelled?
+ "Cancelled"
+ else
+ "Active"
+ end
+ end
+
+ def cancelled?
+ self.cancelled_at.present?
+ end
+end
diff --git a/app/models/casting_call_interview.rb b/app/models/casting_call_interview.rb
new file mode 100644
index 0000000..3427ed3
--- /dev/null
+++ b/app/models/casting_call_interview.rb
@@ -0,0 +1,22 @@
+class CastingCallInterview < ApplicationRecord
+ belongs_to :casting_call
+ has_many_attached :files
+
+ has_secure_token
+
+ validates :performer_name, presence: true
+
+ scope :completed, -> { where.not(interviewed_at: nil) }
+
+ def join_zoom_meeting_url
+ uri = URI.parse(self.zoom_meeting_url)
+ zoom_meeting_id = uri.path.gsub("/j/", "")
+ zoom_meeting_pwd = uri.query.gsub("pwd=", "")
+
+ "zoommtg://zoom.us/join?confno=#{zoom_meeting_id}&pwd=#{zoom_meeting_pwd}"
+ end
+
+ def zip_file_name
+ "#{self.casting_call.title.parameterize}_#{self.performer_name.parameterize}_#{Time.now.strftime('%Y-%m-%d_%H-%M-%S')}"
+ end
+end
diff --git a/app/policies/casting_call_interview_policy.rb b/app/policies/casting_call_interview_policy.rb
new file mode 100644
index 0000000..ac7a232
--- /dev/null
+++ b/app/policies/casting_call_interview_policy.rb
@@ -0,0 +1,29 @@
+class CastingCallInterviewPolicy < ApplicationPolicy
+ def index?
+ true
+ end
+
+ def show?
+ true
+ end
+
+ def create?
+ true
+ end
+
+ def destroy?
+ true
+ end
+
+ def update?
+ true
+ end
+
+ def complete?
+ true
+ end
+
+ def download?
+ true
+ end
+end
diff --git a/app/policies/casting_call_policy.rb b/app/policies/casting_call_policy.rb
new file mode 100644
index 0000000..a2b4596
--- /dev/null
+++ b/app/policies/casting_call_policy.rb
@@ -0,0 +1,25 @@
+class CastingCallPolicy < ApplicationPolicy
+ def index?
+ true
+ end
+
+ def show?
+ true
+ end
+
+ def create?
+ true
+ end
+
+ def destroy?
+ true
+ end
+
+ def update?
+ true
+ end
+
+ def cancel?
+ true
+ end
+end
\ No newline at end of file
diff --git a/app/services/interview_files_collection_service.rb b/app/services/interview_files_collection_service.rb
new file mode 100644
index 0000000..c99a6f8
--- /dev/null
+++ b/app/services/interview_files_collection_service.rb
@@ -0,0 +1,24 @@
+class InterviewFilesCollectionService
+ def initialize(files, folder_name)
+ @files = files
+ @folder_name = folder_name
+ end
+
+ def build
+ Dir.mktmpdir { |dir|
+ files.each do |file|
+ open("#{dir}/#{file.filename}", 'wb') do |tmp_file|
+ tmp_file << open(file.service_url.to_s).read
+ end
+ end
+
+ read_files = Dir.entries("#{dir}/").select { |f| !File.directory? f }
+ raise StandardError.new "Files not found." unless read_files.any?
+ yield(dir, read_files)
+ }
+ end
+
+ private
+
+ attr_reader :files, :folder_name
+end
\ No newline at end of file
diff --git a/app/views/admin/casting_call_interviews/_casting_call_interview.html.erb b/app/views/admin/casting_call_interviews/_casting_call_interview.html.erb
new file mode 100644
index 0000000..697743b
--- /dev/null
+++ b/app/views/admin/casting_call_interviews/_casting_call_interview.html.erb
@@ -0,0 +1,26 @@
+
+ |
+ <%= casting_call_interview.casting_call.project.account.name.titleize %>
+ |
+
+ <%= casting_call_interview.casting_call.title.titleize %>
+ |
+
+ <%= casting_call_interview.performer_name %>
+ |
+
+ <%= casting_call_interview.interview_date %>
+ |
+
+
+ <%= button_tag "Manage", class: "btn btn-light btn-sm dropdown-toggle border", data: { toggle: "dropdown", boundary: "window" }, aria: { haspopup: true, expanded: false } %>
+
+
+ |
+
diff --git a/app/views/admin/casting_call_interviews/_form.html.erb b/app/views/admin/casting_call_interviews/_form.html.erb
new file mode 100644
index 0000000..ffa30fe
--- /dev/null
+++ b/app/views/admin/casting_call_interviews/_form.html.erb
@@ -0,0 +1,15 @@
+<%= errors_summary_for casting_call_interview %>
+
+<%= bootstrap_form_with model: model, local: true do |form| %>
+ <%= form.text_field :performer_name, required: true %>
+ <%= form.grouped_collection_select(:casting_call_id, @accounts, :casting_calls, :name, :id, :title, { prompt: "Select a Casting Call", required: true, class: "form-control custom-select" }) %>
+ <%= form.text_field :interview_date, class: "datepicker-control" %>
+ <%= form.text_field :zoom_meeting_url %>
+
+
+ <%= link_to t("shared.cancel"), [:admin, :casting_call_interviews], class: "col-3 text-reset" %>
+
+ <%= form.submit class: class_string("btn btn-block", ["btn-success", "btn-primary"] => casting_call_interview.new_record?), data: { disable_with: t("shared.disable_with") } %>
+
+
+<% end %>
diff --git a/app/views/admin/casting_call_interviews/edit.html.erb b/app/views/admin/casting_call_interviews/edit.html.erb
new file mode 100644
index 0000000..bc03af7
--- /dev/null
+++ b/app/views/admin/casting_call_interviews/edit.html.erb
@@ -0,0 +1,6 @@
+
+ <%= card_header text: "Edit Casting Call Interview", close_action_path: [:admin, :casting_call_interviews] %>
+
+ <%= render "form", model: [:admin, @casting_call_interview], casting_call_interview: @casting_call_interview %>
+
+
\ No newline at end of file
diff --git a/app/views/admin/casting_call_interviews/index.html.erb b/app/views/admin/casting_call_interviews/index.html.erb
new file mode 100644
index 0000000..84d9d98
--- /dev/null
+++ b/app/views/admin/casting_call_interviews/index.html.erb
@@ -0,0 +1,32 @@
+
+ <% if policy(CastingCall).new? %>
+ <%= link_to fa_icon("plus", text: t(".actions.new")), [:new, :admin, :casting_call_interview], class: "btn btn-primary mb-3" %>
+ <% end %>
+
+
+
+
+
+
+ | Account Name |
+ Casting Call Request |
+ Perfomer's Name |
+ Interview Date |
+ |
+
+
+
+ <% if @casting_call_interviews.any? %>
+ <%= render @casting_call_interviews %>
+ <% else %>
+
+ | <%= t(".empty") %> |
+
+ <% end %>
+
+
+
+
+
diff --git a/app/views/admin/casting_call_interviews/new.html.erb b/app/views/admin/casting_call_interviews/new.html.erb
new file mode 100644
index 0000000..053d0ac
--- /dev/null
+++ b/app/views/admin/casting_call_interviews/new.html.erb
@@ -0,0 +1,6 @@
+
+ <%= card_header text: t(".heading"), close_action_path: [:admin, :casting_call_interviews] %>
+
+ <%= render "form", model: [:admin, @casting_call_interview], casting_call_interview: @casting_call_interview, casting_calls: @casting_calls %>
+
+
diff --git a/app/views/casting_call_interviews/_casting_call_interview.html.erb b/app/views/casting_call_interviews/_casting_call_interview.html.erb
new file mode 100644
index 0000000..ee1ff58
--- /dev/null
+++ b/app/views/casting_call_interviews/_casting_call_interview.html.erb
@@ -0,0 +1,27 @@
+
+ |
+ <%= casting_call_interview.casting_call.project.account.name.titleize %>
+ |
+
+ <%= casting_call_interview.casting_call.title&.titleize %>
+ |
+
+ <%= casting_call_interview.performer_name %>
+ |
+
+ <%= casting_call_interview.interviewed_at %>
+ |
+
+
+ <%= button_tag "Manage", class: "btn btn-light btn-sm dropdown-toggle border", data: { toggle: "dropdown", boundary: "window" }, aria: { haspopup: true, expanded: false } %>
+
+
+ |
+
diff --git a/app/views/casting_call_interviews/_file.html.erb b/app/views/casting_call_interviews/_file.html.erb
new file mode 100644
index 0000000..85fe6bb
--- /dev/null
+++ b/app/views/casting_call_interviews/_file.html.erb
@@ -0,0 +1,6 @@
+
+ | <%= file.filename %> |
+
+ <%= link_to fa_icon("download"), file, target: "_blank" %>
+ |
+
diff --git a/app/views/casting_call_interviews/index.html.erb b/app/views/casting_call_interviews/index.html.erb
new file mode 100644
index 0000000..ef64b1e
--- /dev/null
+++ b/app/views/casting_call_interviews/index.html.erb
@@ -0,0 +1,26 @@
+
+
+
+
+ | Account Name |
+ Casting Call Request |
+ Perfomer's Name |
+ Interviewed At |
+ |
+
+
+
+ <% if @casting_call_interviews.any? %>
+ <%= render @casting_call_interviews %>
+ <% else %>
+
+ | <%= t(".empty") %> |
+
+ <% end %>
+
+
+
+
+
diff --git a/app/views/casting_call_interviews/show.html.erb b/app/views/casting_call_interviews/show.html.erb
new file mode 100644
index 0000000..727d285
--- /dev/null
+++ b/app/views/casting_call_interviews/show.html.erb
@@ -0,0 +1,25 @@
+
+
Files:
+
+
+
+
+ | Filename |
+ |
+
+
+
+ <% if @files.any? %>
+ <%= render partial: "file", collection: @files %>
+ <% else %>
+
+ | <%= t(".empty") %> |
+
+ <% end %>
+
+
+
+ <%= will_paginate @files %>
+
+
+
\ No newline at end of file
diff --git a/app/views/casting_calls/_casting_call.html.erb b/app/views/casting_calls/_casting_call.html.erb
new file mode 100644
index 0000000..d152b28
--- /dev/null
+++ b/app/views/casting_calls/_casting_call.html.erb
@@ -0,0 +1,28 @@
+
+ |
+ <%= casting_call.created_at.strftime('%D') %>
+ |
+
+ <%= casting_call.title %>
+ |
+
+ <%= casting_call.status %>
+ |
+
+
+ <%= button_tag t(".actions.manage"), class: "btn btn-light btn-sm dropdown-toggle border", data: { toggle: "dropdown", boundary: "window" }, aria: { haspopup: true, expanded: false } %>
+
+
+ |
+
diff --git a/app/views/casting_calls/_form.html.erb b/app/views/casting_calls/_form.html.erb
new file mode 100644
index 0000000..b5434dc
--- /dev/null
+++ b/app/views/casting_calls/_form.html.erb
@@ -0,0 +1,22 @@
+<%= errors_summary_for casting_call %>
+
+<%= bootstrap_form_with model: model, url: [@project, @casting_call, show_chat: true], local: true do |form| %>
+
+ <%= fa_icon "info-circle" %>
+ <%= t '.info_message' %>
+
+
+ <%= form.text_field :title, label: t('.labels.title') %>
+ <%= form.text_area :description, label: t('.labels.description') %>
+ <%= form.text_area :project_description, label: t('.labels.project_description') %>
+ <%= form.text_area :interview_instructions, label: t('.labels.interview_instructions') %>
+ <%= form.text_area :interview_requirements, label: t('.labels.interview_requirements') %>
+ <%= form.text_area :questions, label: t('.labels.questions') %>
+
+
+ <%= link_to t("shared.cancel"), [project, :casting_calls], class: "col-3 text-reset" %>
+
+ <%= form.submit class: class_string("btn btn-block", ["btn-success", "btn-primary"] => casting_call.new_record?), data: { disable_with: t("shared.disable_with") } %>
+
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/casting_calls/create.html.erb b/app/views/casting_calls/create.html.erb
new file mode 100644
index 0000000..9a52e24
--- /dev/null
+++ b/app/views/casting_calls/create.html.erb
@@ -0,0 +1,2 @@
+<%= render "shared/initiate_hubspot_chat" %>
+<%= t '.success_message' %>
diff --git a/app/views/casting_calls/edit.html.erb b/app/views/casting_calls/edit.html.erb
new file mode 100644
index 0000000..d09dc0f
--- /dev/null
+++ b/app/views/casting_calls/edit.html.erb
@@ -0,0 +1,6 @@
+
+ <%= card_header text: t(".heading"), close_action_path: [@project, :casting_calls] %>
+
+ <%= render "form", model: [@project, @casting_call], casting_call: @casting_call, project: @project %>
+
+
\ No newline at end of file
diff --git a/app/views/casting_calls/index.html.erb b/app/views/casting_calls/index.html.erb
new file mode 100644
index 0000000..434e2af
--- /dev/null
+++ b/app/views/casting_calls/index.html.erb
@@ -0,0 +1,37 @@
+<%= product_wordmark :cast_me, class: "small mb-3" %>
+
+
+
+
+ <% if policy(CastingCall).new? %>
+ <%= link_to fa_icon("plus", text: t(".actions.new")), [:new, @project, :casting_call], class: "btn btn-primary mb-2" %>
+ <% end %>
+
+
+
+
+
+
+
+
+ | <%= t(".table_headers.casting_call_created_on") %> |
+ <%= t(".table_headers.casting_call_title") %> |
+ <%= t(".table_headers.casting_call_status") %> |
+ |
+
+
+
+ <% if @casting_calls.any? %>
+ <%= render @casting_calls %>
+ <% else %>
+
+ | <%= t(".empty") %> |
+
+ <% end %>
+
+
+
+
+
\ No newline at end of file
diff --git a/app/views/casting_calls/new.html.erb b/app/views/casting_calls/new.html.erb
new file mode 100644
index 0000000..d09dc0f
--- /dev/null
+++ b/app/views/casting_calls/new.html.erb
@@ -0,0 +1,6 @@
+
+ <%= card_header text: t(".heading"), close_action_path: [@project, :casting_calls] %>
+
+ <%= render "form", model: [@project, @casting_call], casting_call: @casting_call, project: @project %>
+
+
\ No newline at end of file
diff --git a/app/views/casting_calls/show.html.erb b/app/views/casting_calls/show.html.erb
new file mode 100644
index 0000000..a56078c
--- /dev/null
+++ b/app/views/casting_calls/show.html.erb
@@ -0,0 +1,38 @@
+<% content_for :header do %>
+
+<% end %>
+
+
+ <%= card_header text: @casting_call.title, close_action_path: [@project, :casting_calls] %>
+
+
+
+
+ <%= description_list_pair_for @casting_call, :title, append: ":" %>
+ <%= description_list_pair_for @casting_call, :description, append: ":" %>
+ <%= description_list_pair_for @casting_call, :project_description, append: ":" %>
+ <%= description_list_pair_for @casting_call, :created_at, append: ":" %>
+
+
+
+
+ <%= description_list_pair_for @casting_call, :status, append: ":" %>
+ <%= description_list_pair_for @casting_call, :interview_instructions, append: ":" %>
+ <%= description_list_pair_for @casting_call, :interview_requirements, append: ":" %>
+ <%= description_list_pair_for @casting_call, :questions, append: ":" %>
+
+
+
+ <% unless @casting_call.cancelled? %>
+
+ <%= link_to "Schedule an Audition", ENV["CASTME_AUDITION_BOOKING_URL"], target: "_blank", class: "btn btn-primary" %>
+
+ <% end %>
+
+
diff --git a/app/views/interview_downloads/_other_pending_downloads.html.erb b/app/views/interview_downloads/_other_pending_downloads.html.erb
new file mode 100644
index 0000000..d61bafb
--- /dev/null
+++ b/app/views/interview_downloads/_other_pending_downloads.html.erb
@@ -0,0 +1,17 @@
+Your <%= release_type.titleize %> files are being prepared for download. You will be notified when it's ready.
+
+The following downloads are also in progress:
+
+ <% downloads.each do |download| %>
+ <% if download.release_type == "reports"%>
+ - <%= download.release_type.titleize %> (as of <%= time_ago_in_words(download.created_at) %> ago)
+
+ <% elsif download.release_type == "CastingCallInterview"%>
+ - <%= download.release_type.titleize %> files (as of <%= time_ago_in_words(download.created_at) %> ago)
+
+ <% else %>
+ - <%= download.release_type.titleize %> contracts (as of <%= time_ago_in_words(download.created_at) %> ago)
+
+ <% end %>
+ <% end %>
+
diff --git a/app/views/public/casting_call_interviews/show.html.erb b/app/views/public/casting_call_interviews/show.html.erb
new file mode 100644
index 0000000..a0baedf
--- /dev/null
+++ b/app/views/public/casting_call_interviews/show.html.erb
@@ -0,0 +1,51 @@
+
+ <%= card_header text: "Casting call interview details" %>
+
+
+
+
+ <%= description_list_pair_for @casting_call_interview, :performer_name, append: ":" %>
+ <%= description_list_pair_for @casting_call_interview, :interview_date, append: ":" %>
+
+
+
+
INTERVIEW FILES:
+
+
+
+
+ <%= card_header text: t(".heading") %>
+
+ <%= errors_summary_for @casting_call_interview %>
+ <%= bootstrap_form_with model: @casting_call_interview, url: casting_call_interview_path(token: @casting_call_interview.token), local: true do |form| %>
+
+ <%= form.label :files %>
+ <%= form.file_field :files, disable: true, direct_upload: true, multiple: true, id: "casting_call_interivew_files", hide_label: true %>
+ <% @casting_call_interview.files.each do |file| %>
+ <% unless file.persisted? %>
+ <%= hidden_field_tag "#{@casting_call_interview.model_name.param_key}[files][]", file.signed_id %>
+ <% end %>
+ <% end %>
+
+
+
+
+
+ <%= form.button t(".update"), class: "btn btn-block btn-lg btn-success", data: { disable_with: t("shared.disable_with") } %>
+
+ <% end %>
+
+
+
+
+
+ <%= link_to "Start Interview", @casting_call_interview.join_zoom_meeting_url, target: "_blank", class: "btn btn-primary" %>
+
+
+
diff --git a/app/views/public/casting_calls/show.html.erb b/app/views/public/casting_calls/show.html.erb
new file mode 100644
index 0000000..c9ec824
--- /dev/null
+++ b/app/views/public/casting_calls/show.html.erb
@@ -0,0 +1,29 @@
+<% content_for :header do %>
+
+<% end %>
+
+
+ <%= card_header text: @casting_call.title %>
+
+
+
+
+ <%= description_list_pair_for @casting_call, :title, append: ":" %>
+ <%= description_list_pair_for @casting_call, :description, append: ":" %>
+ <%= description_list_pair_for @casting_call, :project_description, append: ":" %>
+
+
+
+ <% unless @casting_call.cancelled? %>
+
+ <%= link_to "Schedule an Audition", ENV["CASTME_AUDITION_BOOKING_URL"], target: "_blank", class: "btn btn-primary" %>
+
+ <% end %>
+
+
diff --git a/app/views/shared/_initiate_hubspot_chat.html.erb b/app/views/shared/_initiate_hubspot_chat.html.erb
new file mode 100644
index 0000000..900ac85
--- /dev/null
+++ b/app/views/shared/_initiate_hubspot_chat.html.erb
@@ -0,0 +1,15 @@
+<% if params[:show_chat] %>
+ <%= javascript_include_tag "//js.hs-scripts.com/7344617.js", defer: "defer", async: true, id: "hs-script-loader" %>
+ <%= javascript_tag nonce: true do %>
+ function onConversationsAPIReady() {
+ window.HubSpotConversations.widget.load({ widgetOpen: true });
+ window.HubSpotConversations.widget.open();
+ }
+ if (window.HubSpotConversations) {
+ onConversationsAPIReady();
+ } else {
+ window.hsConversationsOnReady = [onConversationsAPIReady];
+ }
+ <% end %>
+<% end %>
+
diff --git a/db/migrate/20200626044744_create_casting_calls.rb b/db/migrate/20200626044744_create_casting_calls.rb
new file mode 100644
index 0000000..0642635
--- /dev/null
+++ b/db/migrate/20200626044744_create_casting_calls.rb
@@ -0,0 +1,17 @@
+class CreateCastingCalls < ActiveRecord::Migration[6.0]
+ def change
+ create_table :casting_calls do |t|
+ t.references :project
+ t.string :title
+ t.string :user_email
+ t.text :description
+ t.text :project_description
+ t.text :interview_instructions
+ t.text :interview_requirements
+ t.text :questions
+ t.datetime :cancelled_at
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20200701121237_create_casting_call_interviews.rb b/db/migrate/20200701121237_create_casting_call_interviews.rb
new file mode 100644
index 0000000..fe4f4d7
--- /dev/null
+++ b/db/migrate/20200701121237_create_casting_call_interviews.rb
@@ -0,0 +1,12 @@
+class CreateCastingCallInterviews < ActiveRecord::Migration[6.0]
+ def change
+ create_table :casting_call_interviews do |t|
+ t.references :casting_call, foreign_key: true
+ t.string :performer_name
+ t.string :zoom_meeting_url
+ t.datetime :interview_date
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20200706193123_add_token_to_casting_calls.rb b/db/migrate/20200706193123_add_token_to_casting_calls.rb
new file mode 100644
index 0000000..57e8ea8
--- /dev/null
+++ b/db/migrate/20200706193123_add_token_to_casting_calls.rb
@@ -0,0 +1,6 @@
+class AddTokenToCastingCalls < ActiveRecord::Migration[6.0]
+ def change
+ add_column :casting_calls, :token, :string
+ add_index :casting_calls, :token, unique: true
+ end
+end
diff --git a/db/migrate/20200706230803_add_token_to_casting_call_interviews.rb b/db/migrate/20200706230803_add_token_to_casting_call_interviews.rb
new file mode 100644
index 0000000..9c5689c
--- /dev/null
+++ b/db/migrate/20200706230803_add_token_to_casting_call_interviews.rb
@@ -0,0 +1,6 @@
+class AddTokenToCastingCallInterviews < ActiveRecord::Migration[6.0]
+ def change
+ add_column :casting_call_interviews, :token, :string
+ add_index :casting_call_interviews, :token, unique: true
+ end
+end
diff --git a/db/migrate/20200707070522_add_interviewed_at_to_casting_call_interview.rb b/db/migrate/20200707070522_add_interviewed_at_to_casting_call_interview.rb
new file mode 100644
index 0000000..ab41369
--- /dev/null
+++ b/db/migrate/20200707070522_add_interviewed_at_to_casting_call_interview.rb
@@ -0,0 +1,5 @@
+class AddInterviewedAtToCastingCallInterview < ActiveRecord::Migration[6.0]
+ def change
+ add_column :casting_call_interviews, :interviewed_at, :datetime
+ end
+end
diff --git a/spec/controllers/admin/casting_call_interviews_controller_spec.rb b/spec/controllers/admin/casting_call_interviews_controller_spec.rb
new file mode 100644
index 0000000..d2ff7b2
--- /dev/null
+++ b/spec/controllers/admin/casting_call_interviews_controller_spec.rb
@@ -0,0 +1,106 @@
+require "rails_helper"
+
+RSpec.describe Admin::CastingCallInterviewsController, type: :controller do
+
+ let!(:current_user) { create(:user, :admin) }
+
+ before do
+ sign_in(current_user)
+ end
+
+ describe "#index" do
+ it "returns a successful response" do
+ get :index
+
+ expect(response).to be_successful
+ end
+ end
+
+ describe "#new" do
+ it "returns a successful response" do
+ get :new
+
+ expect(response).to be_successful
+ end
+
+ it "assigns user, accounts" do
+ get :new
+
+ expect(assigns(:casting_call_interview)).not_to be_nil
+ expect(assigns(:accounts)).to eq Account.all
+ end
+ end
+
+ describe "#create" do
+ it "does create a new record" do
+ expect {
+ post :create, params: { casting_call_interview: casting_call_interview_params }
+ }.to change(CastingCallInterview, :count)
+ end
+ end
+
+ describe "#edit" do
+ let(:casting_call_interview) { create(:casting_call_interview) }
+
+ it "returns a successful response" do
+ get :edit, params: { id: casting_call_interview }
+
+ expect(response).to be_successful
+ end
+
+ it "assigns casting call interview" do
+ get :edit, params: { id: casting_call_interview }
+
+ expect(assigns(:casting_call_interview)).to eq casting_call_interview
+ end
+ end
+
+ describe "#update" do
+ let(:casting_call_interview) { create(:casting_call_interview) }
+
+ it "redirects to casting call interviews page" do
+ patch :update, params: { id: casting_call_interview, casting_call_interview: update_params }
+
+ expect(response).to be_redirect
+ expect(response).to redirect_to admin_casting_call_interviews_path
+ end
+
+ it "sets a flash notice" do
+ patch :update, params: { id: casting_call_interview, casting_call_interview: update_params }
+
+ expect(flash.notice).to eq "The casting call interview has been updated"
+ end
+
+ it "updates casting call interview" do
+ patch :update, params: { id: casting_call_interview, casting_call_interview: update_params }
+
+ expect(casting_call_interview.reload.zoom_meeting_url).to eq("new_zoom_meeting_url")
+ end
+ end
+
+ describe "#complete" do
+ let(:casting_call_interview) { create(:casting_call_interview) }
+
+ it "sets interviewed_at on casting call interview" do
+ expect(casting_call_interview.interviewed_at).to be_nil
+
+ post :complete, params: { id: casting_call_interview }
+
+ expect(casting_call_interview.reload.interviewed_at).not_to be_nil
+ end
+ end
+
+ private
+
+ def casting_call_interview_params
+ casting_call = create(:casting_call)
+
+ attributes_for(:casting_call_interview).except(:interviewed_at).merge(casting_call_id: casting_call.id)
+ end
+
+ def update_params
+ {
+ zoom_meeting_url: "new_zoom_meeting_url"
+ }
+ end
+end
diff --git a/spec/controllers/casting_call_interviews_controller_spec.rb b/spec/controllers/casting_call_interviews_controller_spec.rb
new file mode 100644
index 0000000..2ac60aa
--- /dev/null
+++ b/spec/controllers/casting_call_interviews_controller_spec.rb
@@ -0,0 +1,43 @@
+require "rails_helper"
+
+RSpec.describe CastingCallInterviewsController, type: :controller do
+ render_views
+
+ let(:user) { create(:user) }
+ let(:account) { user.primary_account }
+ let(:project) { create(:project, account: user.primary_account) }
+ let(:casting_call) { create(:casting_call, project: project, title: "My Interview") }
+
+ before do
+ sign_in(user)
+ end
+
+ describe "#index" do
+ it "returns a successful response" do
+ get :index, params: { project_id: project }
+
+ expect(response).to be_successful
+ end
+
+ it "only shows completed interviews" do
+ create(:casting_call_interview, casting_call: casting_call, interviewed_at: Time.zone.now, performer_name: "John Doe")
+ create(:casting_call_interview, casting_call: casting_call, interviewed_at: nil, performer_name: "Jane Doe")
+
+ get :index, params: { project_id: project }
+
+ expect(response.body).to have_content("John Doe")
+ expect(response.body).not_to have_content("Jane Doe")
+ end
+ end
+
+ describe "#show" do
+ let!(:casting_call_interview) { create(:casting_call_interview, :with_files, casting_call: casting_call, interviewed_at: Time.zone.now, performer_name: "Jane Doe") }
+
+ it "shows files of casting call interview" do
+ get :show, params: { project_id: project, id: casting_call_interview.id }
+
+ expect(response.body).to have_content("Filename")
+ expect(response.body).to have_content("location_photo.png")
+ end
+ end
+end
diff --git a/spec/controllers/casting_calls_controller_spec.rb b/spec/controllers/casting_calls_controller_spec.rb
new file mode 100644
index 0000000..3c23bee
--- /dev/null
+++ b/spec/controllers/casting_calls_controller_spec.rb
@@ -0,0 +1,126 @@
+require 'rails_helper'
+
+RSpec.describe CastingCallsController, type: :controller do
+ render_views
+
+ let(:user) { create(:user) }
+ let(:account) { user.primary_account }
+ let(:project) { create(:project, account: user.primary_account) }
+
+ before do
+ sign_in user
+ end
+
+ describe "#index" do
+ it "responds successfully" do
+ get :index, params: { project_id: project }
+
+ expect(response).to be_successful
+ end
+
+ it "renders content" do
+ create(:casting_call, project: project)
+
+ get :index, params: { project_id: project }
+
+ expect(response.body).to have_link "Create Casting Call"
+ expect(response.body).to have_content "Active"
+ end
+
+ context "when there are many records" do
+ it "paginates the table" do
+ create_list(:casting_call, 20, project: project)
+
+ get :index, params: { project_id: project }
+
+ expect(response.body).to have_link("2", href: project_casting_calls_path(project, page: 2))
+ end
+ end
+ end
+
+ describe "#new" do
+ it "responds successfully" do
+ get :new, params: { project_id: project }
+
+ expect(response).to be_successful
+ expect(assigns(:casting_call)).to be_a_new(CastingCall)
+ expect(response).to render_template(:new)
+ end
+ end
+
+ describe "#create" do
+ it "does create a new record" do
+ expect {
+ post :create, params: { project_id: project.id, casting_call: casting_call_params }
+ }.to change(CastingCall, :count)
+ end
+
+ it "logs an event" do
+ expect {
+ post :create, params: { project_id: project.id, casting_call: casting_call_params }
+ }.to have_enqueued_job(TrackAnalyticsJob).with(user, account, :track_create_casting_call, user_agent: "Rails Testing", user_ip: "0.0.0.0")
+ end
+
+ it "submits data to hubspot form" do
+ expect {
+ post :create, params: { project_id: project.id, casting_call: casting_call_params }
+ }.to have_enqueued_job(SubmitHubspotFormJob)
+ end
+ end
+
+ describe "#update" do
+ let!(:casting_call) { create(:casting_call, project: project, description: "My description" ) }
+
+ it "updates casting call request" do
+ patch :update, params: { project_id: project.id, id: casting_call.id, casting_call: update_params }
+
+ expect(casting_call.reload.description).to eq("This is updated description")
+ end
+ end
+
+ describe "#show" do
+ let!(:casting_call) { create(:casting_call, project: project, description: "Casting Call Request") }
+
+ it "responds successfully" do
+ get :show, params: { project_id: project.id, id: casting_call.id }
+
+ expect(response).to be_successful
+ expect(assigns(:casting_call)).to eq(casting_call)
+ end
+
+ it "renders content" do
+ get :show, params: { project_id: project.id, id: casting_call.id }
+
+ expect(response.body).to have_content "Casting Call Request"
+ expect(response.body).to have_content "Active"
+ end
+ end
+
+ describe "#cancel" do
+ let!(:casting_call) { create(:casting_call, project: project, description: "Casting Call to be Cancelled") }
+
+ it "responds with redirect" do
+ post :cancel, params: { project_id: project.id, id: casting_call.id }
+
+ expect(response).to be_redirect
+ expect(response).to redirect_to(project_casting_calls_path(project))
+ expect(flash.notice).not_to be_nil
+ end
+
+ it "updates the status to 'Cancelled'" do
+ expect {
+ post :cancel, params: { project_id: project.id, id: casting_call.id }
+ }.to change { casting_call.reload.status }.from("Active").to("Cancelled")
+ end
+ end
+
+ private
+
+ def casting_call_params
+ attributes_for(:casting_call).except(:status, :user_email)
+ end
+
+ def update_params
+ { description: "This is updated description" }
+ end
+end
\ No newline at end of file
diff --git a/spec/controllers/interview_downloads_controller_spec.rb b/spec/controllers/interview_downloads_controller_spec.rb
new file mode 100644
index 0000000..119250f
--- /dev/null
+++ b/spec/controllers/interview_downloads_controller_spec.rb
@@ -0,0 +1,58 @@
+require "rails_helper"
+
+RSpec.describe InterviewDownloadsController, type: :controller do
+ render_views
+
+ let(:current_user) { create(:user) }
+ let(:project) { create(:project, :discovery_client, account: current_user.primary_account) }
+ let(:casting_call) { create(:casting_call, project: project, title: "My Title") }
+ let(:casting_call_interview) { create(:casting_call_interview, casting_call: casting_call, performer_name: "John Doe") }
+
+ before do
+ sign_in current_user
+ end
+
+ describe "#create" do
+ it "enqueues zip file generation job" do
+ expect {
+ post :create, params: { project_id: project.id, casting_call_interview_id: casting_call_interview.id }, format: :js
+ }.to have_enqueued_job(GenerateInterviewFilesZipJob)
+ end
+
+ it "creates a download record with 'not_started' status" do
+ expect {
+ post :create, params: { project_id: project.id, casting_call_interview_id: casting_call_interview.id }, format: :js
+ }.to change(Download, :count).by(1)
+
+ expect(Download.last.status).to eq('not_started')
+ end
+
+ context "When there is no existing job" do
+ it "shows a notification to user" do
+ allow(ProjectsChannel).to receive(:broadcast_download_generation_update).with(be_kind_of(Download), I18n.t("interview_downloads.download.pending", release_type: "Casting Call Interview"))
+
+ post :create, params: { project_id: project.id, casting_call_interview_id: casting_call_interview.id }, format: :js
+
+ expect(ProjectsChannel).to have_received(:broadcast_download_generation_update).with(be_kind_of(Download), I18n.t("interview_downloads.download.pending", release_type: "Casting Call Interview"))
+ end
+ end
+
+ context "When there are existing jobs" do
+ let(:appearance_release_download) { create(:download, project_id: project.id, name: "#{project.name.parameterize}_appearance-releases") }
+ let(:acquired_media_release_download) { create(:download, project_id: project.id, name: "#{project.name.parameterize}_acquired-media-releases", release_type: "AcquiredMediaRelease") }
+
+ before do
+ allow(Download).to receive_message_chain(:unfinished_desc_order, :offset).and_return([acquired_media_release_download, appearance_release_download])
+ allow(ProjectsChannel).to receive(:broadcast_download_generation_update)
+ end
+
+ it "shows names of other contracts in the notification, which are in progress" do
+ broadcast_message = "Your Casting Call Interview files are being prepared for download. You will be notified when it's ready.\n
\nThe following downloads are also in progress:
\n\n - Acquired Media Release contracts (as of less than a minute ago)\n
\n - Appearance Release contracts (as of less than a minute ago)\n
\n
\n"
+
+ post :create, params: { project_id: project.id, casting_call_interview_id: casting_call_interview.id }, format: :js
+
+ expect(ProjectsChannel).to have_received(:broadcast_download_generation_update).with(be_kind_of(Download), broadcast_message)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/public/casting_call_interviews_controller.rb b/spec/controllers/public/casting_call_interviews_controller.rb
new file mode 100644
index 0000000..fb08c74
--- /dev/null
+++ b/spec/controllers/public/casting_call_interviews_controller.rb
@@ -0,0 +1,44 @@
+require 'rails_helper'
+
+RSpec.describe Public::CastingCallInterviewsController, type: :controller do
+ render_views
+
+ describe "#show" do
+ let(:casting_call_interview) { create(:casting_call_interview) }
+
+ it "responds successfully" do
+ get :show, params: { token: casting_call_interview.token }
+
+ expect(response).to be_successful
+ expect(assigns(:casting_call_interview)).to eq(casting_call_interview)
+ end
+
+ it "shows casting call interview details" do
+ get :show, params: { token: casting_call_interview.token }
+
+ expect(response.body).to have_content(casting_call_interview.performer_name)
+ expect(response.body).to have_content(casting_call_interview.interview_date)
+ expect(response.body).to have_link("Start Interview")
+ end
+ end
+
+ describe "#update" do
+ let(:casting_call_interview) { create(:casting_call_interview) }
+
+ it "responds successfully" do
+ patch :update, params: { token: casting_call_interview.token, casting_call_interview: casting_call_interview_params }
+
+ expect(response).to redirect_to casting_call_interview_url(token: casting_call_interview.token)
+ expect(flash.notice).to be_present
+ end
+ end
+
+ private
+
+ def casting_call_interview_params
+ path = Rails.root.join("spec", "fixtures", "files", "contract.pdf")
+ file = Rack::Test::UploadedFile.new(path, "application/pdf")
+
+ { files: [file]}
+ end
+end
diff --git a/spec/controllers/public/casting_calls_controller_spec.rb b/spec/controllers/public/casting_calls_controller_spec.rb
new file mode 100644
index 0000000..e7ce2c5
--- /dev/null
+++ b/spec/controllers/public/casting_calls_controller_spec.rb
@@ -0,0 +1,28 @@
+require 'rails_helper'
+
+RSpec.describe Public::CastingCallsController, type: :controller do
+ render_views
+
+ describe "#show" do
+ let(:casting_call) { create(:casting_call) }
+
+ it "responds successfully" do
+ get :show, params: { token: casting_call.token }
+
+ expect(response).to be_successful
+ expect(assigns(:casting_call)).to eq(casting_call)
+ end
+
+ it "shows casting call details" do
+ get :show, params: { token: casting_call.token }
+
+ expect(response.body).to have_content(casting_call.title)
+ expect(response.body).to have_content(casting_call.description)
+ expect(response.body).to have_content(casting_call.project_description)
+ expect(response.body).to have_content(casting_call.interview_instructions)
+ expect(response.body).to have_content(casting_call.interview_requirements)
+ expect(response.body).to have_content(casting_call.questions)
+ expect(response.body).to have_link("Schedule an Audition")
+ end
+ end
+end
diff --git a/spec/factories/casting_call_interviews.rb b/spec/factories/casting_call_interviews.rb
new file mode 100644
index 0000000..389bd9d
--- /dev/null
+++ b/spec/factories/casting_call_interviews.rb
@@ -0,0 +1,13 @@
+FactoryBot.define do
+ factory :casting_call_interview do
+ association :casting_call
+ performer_name 'John Doe'
+ zoom_meeting_url 'https://us04web.zoom.us/j/1111111111?pwd=aDZCS1dzZ2lWdDZJcHBhVnNIclB4QT03'
+ interview_date { 10.days.from_now }
+ interviewed_at { nil }
+
+ trait :with_files do
+ files { [Rack::Test::UploadedFile.new('spec/fixtures/files/location_photo.png', 'image/png')] }
+ end
+ end
+end
diff --git a/spec/factories/casting_calls.rb b/spec/factories/casting_calls.rb
new file mode 100644
index 0000000..0db776a
--- /dev/null
+++ b/spec/factories/casting_calls.rb
@@ -0,0 +1,15 @@
+FactoryBot.define do
+ factory :casting_call do
+ association :project
+ user_email 'test@email.com'
+ description "Casting call description"
+ project_description "Casting call project description"
+ interview_instructions "Interview instructions"
+ interview_requirements "Interview requirements"
+ questions "Questions"
+
+ trait :cancelled do
+ cancelled_at { Time.zone.now }
+ end
+ end
+end
diff --git a/spec/features/user_managing_casiting_calls_spec.rb b/spec/features/user_managing_casiting_calls_spec.rb
new file mode 100644
index 0000000..cc023d2
--- /dev/null
+++ b/spec/features/user_managing_casiting_calls_spec.rb
@@ -0,0 +1,155 @@
+require "rails_helper"
+
+feature "User managing casting calls" do
+ let(:current_user) { create(:user) }
+ let(:project) { create(:project, account: current_user.primary_account) }
+
+ before :each do
+ sign_in current_user
+ end
+
+ scenario "casting calls table is visible" do
+ visit project_casting_calls_path(project)
+
+ expect(page).to have_content "Created On"
+ expect(page).to have_content "Title"
+ expect(page).to have_content "Status"
+ end
+
+ scenario "sees list of casting calls" do
+ visit project_casting_calls_path(project)
+
+ expect(page).to have_content no_casting_calls_label
+
+ casting_call = create(:casting_call, project: project)
+
+ visit project_casting_calls_path(project)
+
+ expect(page).not_to have_content no_casting_calls_label
+
+ expect(page).to have_content casting_call.created_at.try(:strftime, '%D')
+ expect(page).to have_content casting_call.title
+ expect(page).to have_content casting_call.status
+ end
+
+ scenario "can create casting call requests" do
+ visit project_casting_calls_path(project)
+
+ expect(page).to have_content no_casting_calls_label
+ click_on add_new_casting_call_label
+
+ fill_in title_field, with: "Title"
+ fill_in description_field, with: "Description"
+ fill_in project_description_field, with: "Project Description"
+ fill_in interview_instructions_field, with: "Interview instructions"
+ fill_in interview_requirements_field, with: "Interview requirements"
+ fill_in questions_field, with: "Questions"
+
+ click_on "Create Casting call"
+
+ expect(page).to have_content("Your casting call request was successfully submitted. Thank you. A chat window will pop up on the lower right in a few seconds.")
+ end
+
+ scenario "can update casting call requests" do
+ create(:casting_call, title: "Title", project: project)
+ visit project_casting_calls_path(project)
+
+ click_on manage_button
+ click_on "Edit"
+
+ fill_in title_field, with: "New Title"
+
+ click_on "Update Casting call"
+
+ expect(page).to have_content("The casting call request has been updated")
+ end
+
+ scenario "can cancel a casting call request" do
+ create(:casting_call, title: "Title", project: project)
+ visit project_casting_calls_path(project)
+
+ click_on manage_button
+ click_on "Cancel"
+
+ expect(page).to have_content("The casting call request has been cancelled")
+ end
+
+ scenario "can open casting call details" do
+ cc = create(:casting_call, title: "Dummy title", project: project)
+
+ visit project_casting_calls_path(project)
+
+ click_on manage_button
+ click_on view_button
+
+ expect(page).to have_content cc.title
+ expect(page).to have_content cc.description
+ expect(page).to have_content cc.project_description
+ expect(page).to have_content cc.created_at
+ expect(page).to have_content cc.status
+ expect(page).to have_content cc.interview_instructions
+ expect(page).to have_content cc.interview_requirements
+ expect(page).to have_content cc.questions
+ end
+
+ context "when signed out" do
+ scenario "user opens public accessible casting call URL" do
+ cc = create(:casting_call, title: "Dummy title", project: project)
+
+ sign_out
+ public_url = "/casting_calls/#{cc.token}"
+ visit public_url
+
+ expect(page).to have_content cc.title
+ expect(page).to have_content cc.description
+ expect(page).to have_content cc.project_description
+ expect(page).not_to have_content cc.created_at
+ expect(page).not_to have_content cc.status
+ expect(page).not_to have_content cc.interview_instructions
+ expect(page).not_to have_content cc.interview_requirements
+ expect(page).not_to have_content cc.questions
+ end
+ end
+
+ private
+
+ def no_casting_calls_label
+ "Casting calls will appear here"
+ end
+
+ def manage_button
+ t "casting_calls.casting_call.actions.manage"
+ end
+
+ def view_button
+ 'View'
+ end
+
+ def add_new_casting_call_label
+ t "casting_calls.index.actions.new"
+ end
+
+ def title_field
+ t "casting_calls.form.labels.title"
+ end
+
+ def description_field
+ t "casting_calls.form.labels.description"
+ end
+
+ def project_description_field
+ t "casting_calls.form.labels.project_description"
+ end
+
+ def interview_instructions_field
+ t "casting_calls.form.labels.interview_instructions"
+ end
+
+ def interview_requirements_field
+ t "casting_calls.form.labels.interview_requirements"
+ end
+
+ def questions_field
+ t "casting_calls.form.labels.questions"
+ end
+end
\ No newline at end of file
diff --git a/spec/jobs/generate_interview_files_zip_job_spec.rb b/spec/jobs/generate_interview_files_zip_job_spec.rb
new file mode 100644
index 0000000..0cb9eb4
--- /dev/null
+++ b/spec/jobs/generate_interview_files_zip_job_spec.rb
@@ -0,0 +1,60 @@
+require "rails_helper"
+
+describe GenerateInterviewFilesZipJob do
+ let(:project) { create(:project) }
+ let(:download) { create(:download, project: project, release_type: "CastingCallInterview", name: "my-title_john-doe") }
+ let(:casting_call) { create(:casting_call, project: project, title: "My Title") }
+ let(:casting_call_interview) { create(:casting_call_interview, casting_call: casting_call, performer_name: "John Doe") }
+
+ before do
+ dir = Rails.root.join("spec", "fixtures", "files")
+ files = ["contract.pdf", "AppearanceRelease.pdf"]
+ # Attachments in the test environment do not persist to cloud storage
+ # Therefore we want to stub calls to `open` with a cloud storage URL
+ allow_any_instance_of(InterviewFilesCollectionService).to receive(:open).and_return(StringIO.new("file data"))
+ allow_any_instance_of(InterviewFilesCollectionService).to receive(:build).and_yield(dir, files)
+ end
+
+ describe ".perform_later" do
+ it "enqueues a background job for generating zip file" do
+ expect {
+ GenerateInterviewFilesZipJob.perform_later(project, download, casting_call_interview)
+ }.to have_enqueued_job
+ end
+ end
+
+ describe ".perform_now" do
+ it "updates a download record and creates attachment for it" do
+ GenerateInterviewFilesZipJob.perform_now(project, download, casting_call_interview)
+
+ expect(download.project).to eq project
+ expect(download.release_type).to eq "CastingCallInterview"
+ expect(download.name).to eq "my-title_john-doe"
+ expect(download.status).to eq "success"
+ expect(download.file).to be_attached
+ end
+
+ context "When there are errors" do
+ let(:error) { StandardError.new("Casting Call Interview files not found.") }
+
+ before do
+ allow(ProjectsChannel).to receive(:broadcast_download_generation_update).with(download, I18n.t("interview_downloads.download.failure"))
+ allow_any_instance_of(InterviewFilesCollectionService).to receive(:build).and_raise(StandardError, "Casting Call Interview files not found.")
+ end
+
+ it "updates status to 'failure' and sends user a notification" do
+ GenerateInterviewFilesZipJob.perform_now(project, download, casting_call_interview)
+
+ expect(download.status).to eq "failure"
+ expect(ProjectsChannel).to have_received(:broadcast_download_generation_update).with(download, I18n.t("interview_downloads.download.failure"))
+ end
+ end
+ end
+
+ after do
+ # Delete the file created in fixture.
+ # Or the tests will fail on next run due to already existing files in existing zip.
+ path = Rails.root.join("spec", "fixtures", "files")
+ File.delete("#{path}/my-title_john-doe.zip") if File.exists? "#{path}/my-title_john-doe.zip"
+ end
+end
diff --git a/spec/models/casting_call_interview_spec.rb b/spec/models/casting_call_interview_spec.rb
new file mode 100644
index 0000000..8affc14
--- /dev/null
+++ b/spec/models/casting_call_interview_spec.rb
@@ -0,0 +1,12 @@
+require 'rails_helper'
+
+RSpec.describe CastingCallInterview, type: :model do
+ describe "associations" do
+ it { is_expected.to belong_to(:casting_call) }
+ it { is_expected.to have_secure_token(:token) }
+ end
+
+ describe "validations" do
+ it { is_expected.to validate_presence_of(:performer_name) }
+ end
+end
diff --git a/spec/models/casting_call_spec.rb b/spec/models/casting_call_spec.rb
new file mode 100644
index 0000000..c46e97f
--- /dev/null
+++ b/spec/models/casting_call_spec.rb
@@ -0,0 +1,11 @@
+require 'rails_helper'
+
+RSpec.describe CastingCall, type: :model do
+ describe "associations" do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe "validations" do
+ it { is_expected.to have_secure_token(:token) }
+ end
+end
diff --git a/spec/policies/casting_call_policy_spec.rb b/spec/policies/casting_call_policy_spec.rb
new file mode 100644
index 0000000..39aac8e
--- /dev/null
+++ b/spec/policies/casting_call_policy_spec.rb
@@ -0,0 +1,91 @@
+require "rails_helper"
+
+describe CastingCallPolicy do
+ subject { described_class }
+
+ let(:user_context) { build(:user_context, user: user, account: user.primary_account) }
+
+ context "for an associate" do
+ let(:user) { create(:user, :associate, admin: false) }
+
+ permissions :index? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+
+ permissions :create? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+
+ permissions :show? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+
+ permissions :destroy? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+
+ permissions :update? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+
+ permissions :cancel? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+ end
+
+ context "for a project manager" do
+ let(:user) { create(:user, :manager, admin: false) }
+
+ permissions :index? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+
+ permissions :create? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+
+ permissions :show? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+
+ permissions :destroy? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+
+ permissions :update? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+
+ permissions :cancel? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+ end
+
+ context "for account managers" do
+ let(:user) { create(:user, :account_manager, admin: false) }
+
+ permissions :index? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+
+ permissions :create? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+
+ permissions :show? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+
+ permissions :destroy? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+
+ permissions :update? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+
+ permissions :cancel? do
+ it { is_expected.to permit(user_context, subject) }
+ end
+ end
+end