Initial commit

This commit is contained in:
Senad Uka
2020-05-31 22:38:19 +02:00
commit 858fafc3c5
1280 changed files with 65918 additions and 0 deletions

110
app/models/account.rb Normal file
View File

@@ -0,0 +1,110 @@
class Account < ApplicationRecord
include Syncable
include PgSearch
has_many :account_auths
has_many :users, through: :account_auths
has_many :projects, dependent: :destroy
has_many :videos, through: :projects
has_many :contract_templates, through: :projects
validates :name, presence: true
validates :slug, presence: true, uniqueness: true
before_validation :set_slug
scope :order_by_name, -> { order(name: :asc) }
has_one_attached :logo
pg_search_scope :search, {
against: [:name, :slug],
using: {
tsearch: {any_word: true, prefix: true},
trigram: {},
dmetaphone: {any_word: true}
}
}
def managers
users.where(account_auths: { role: :account_manager })
end
def current_month_video_duration_total
@current_month_video_duration_total ||=
Video.with_attached_file.where(project: projects, created_at: Time.zone.now.beginning_of_month..Time.zone.now.end_of_month).sum { |video| video.file.blob.metadata.dig("duration").to_f }
end
def video_duration_total
@video_duration_total ||=
Video.with_attached_file.where(project: projects).sum { |video| video.file.blob.metadata.dig("duration").to_f }
end
def storage_total
ActiveStorage::Blob.joins(:attachments).merge(
ActiveStorage::Attachment.where(record: [
AppearanceRelease.where(project: projects),
TalentRelease.where(project: projects),
MaterialRelease.where(project: projects),
LocationRelease.where(project: projects),
AcquiredMediaRelease.where(project: projects),
Import.where(project: projects),
MusicRelease.where(project: projects),
Video.where(project: projects),
Directory.where(project: projects),
Download.where(project: projects),
User.joins(:project_memberships).where(project_memberships: { project: projects }),
Broadcast.where(project: projects),
ZoomMeeting.where(project: projects),
self
])).sum(:byte_size).to_f
end
def to_param
slug
end
def me_suite_enabled?
plan_uid.to_s == "me_suite"
end
def deliverme_enabled?
plan_uid.to_s == "me_suite" || plan_uid.to_s == "deliverme"
end
def directme_enabled?
plan_uid.to_s == "me_suite" || plan_uid.to_s == "directme"
end
def releaseme_enabled?
plan_uid.to_s == "me_suite" || plan_uid.to_s == "releaseme"
end
def plan_name
case plan_uid.to_s
when "deliverme"
"DeliverME"
when "directme"
"DirectME"
when "releaseme"
"ReleaseME"
when "me_suite"
"ME Suite"
end
end
private
def set_slug
return if name.blank? || slug.present?
counter = 1
begin
if counter == 1
self.slug = name.parameterize
else
self.slug = [name.parameterize, counter].join("-")
end
counter += 1
end while self.class.exists?(slug: self.slug)
end
end

View File

@@ -0,0 +1,8 @@
class AccountAuth < ApplicationRecord
belongs_to :user
belongs_to :account
validates :user_id, uniqueness: { scope: :account_id, message: "already added to account." }
enum role: { associate: 0, project_manager: 1, account_manager: 2 }
end

View File

@@ -0,0 +1,59 @@
class AcquiredMediaRelease < ApplicationRecord
include Confirmable
include Contractable
include Exploitable
include Notable
include Releasable
include Searchable
include Taggable
include Signable
include Syncable
include PersonName
has_many :file_infos, as: :releasable, dependent: :destroy
accepts_nested_attributes_for :file_infos
composed_of :person_address,
class_name: "Address",
mapping: [
%w(person_address_street1 street1),
%w(person_address_street2 street2),
%w(person_address_city city),
%w(person_address_state state),
%w(person_address_zip zip),
%w(person_address_country country)
]
validates :name, presence: true
validates :person_email, email: true, allow_blank: true
# These validations apply to releases created natively by the system (i.e. not imported from elsewhere)
with_options on: :native do
validates :signature, attached: true
end
# These validations apply to releases imported to the system from an outside source
with_options on: :non_native do
validates :contract, attached: true
end
searchable_on %i[
name
person_address_street1 person_address_street2 person_address_city person_address_state person_address_zip person_address_country
]
CATEGORIES = ["Artwork", "Film Footage", "Video Footage", "Still Photograph"].freeze
def minor?
false
end
def contact_person
@contact_person ||= Contact.new(person_name, person_address, person_email, person_phone)
end
def uses_edl?
true
end
end

53
app/models/address.rb Normal file
View File

@@ -0,0 +1,53 @@
class Address
attr_accessor :street1, :street2, :city, :state, :zip, :country
def initialize(street1, street2, city, state, zip, country)
@street1 = street1
@street2 = street2
@city = city
@state = state
@zip = zip
@country = country
end
def to_s(format: nil)
case format
when :full
join_with_newline(
street1,
street2,
join_with_comma(city, state_and_zip),
country
)
when :condensed
join_with_newline(
join_with_comma(street1, street2),
join_with_comma(city, state_and_zip, country),
)
else
join_with_comma(street1, street2, city, state_and_zip, country)
end
end
def present?
[street1, street2, city, state, zip, country].any?(&:present?)
end
private
def join_without_blanks(*parts, separator: ", ")
parts.reject(&:blank?).join(separator)
end
def join_with_comma(*parts)
join_without_blanks(*parts)
end
def join_with_newline(*parts)
join_without_blanks(*parts, separator: "\n")
end
def state_and_zip
join_without_blanks(state, zip, separator: " ")
end
end

View File

@@ -0,0 +1,14 @@
class AdminSignedInConstraint
def matches?(request)
signed_in?(request) && signed_in_user_is_admin?(request)
end
def signed_in?(request)
Oath::Constraints::SignedIn.new.matches?(request)
end
def signed_in_user_is_admin?(request)
warden = request.env['warden']
warden && warden.user.admin?
end
end

View File

@@ -0,0 +1,56 @@
class AnalysisNotification
def self.build(type, job_id)
case type.to_s
when "audio"
AudioAnalysisNotification.new(job_id)
when "video"
VideoAnalysisNotification.new(job_id)
end
end
class VideoAnalysisNotification
attr_reader :job_id
def initialize(job_id)
@job_id = job_id
end
def video
# TODO: add index for analysis_uid
@video ||= Video.find_by!(analysis_uid: job_id)
end
def success!
video.analysis_success!
ProjectsChannel.broadcast_video_analysis_update(video)
end
def failure!
video.analysis_failure!
ProjectsChannel.broadcast_video_analysis_update(video)
end
end
class AudioAnalysisNotification
attr_reader :job_id
def initialize(job_id)
@job_id = job_id
end
def video
# TODO: add index for audio_analysis_uid
@video ||= Video.find_by!(audio_analysis_uid: job_id)
end
def success!
video.audio_analysis_success!
ProjectsChannel.broadcast_video_analysis_update(video)
end
def failure!
video.audio_analysis_failure!
ProjectsChannel.broadcast_video_analysis_update(video)
end
end
end

59
app/models/app_host.rb Normal file
View File

@@ -0,0 +1,59 @@
class AppHost
attr_reader :env_vars, :current_env
def initialize(env_vars = ENV, current_env = Rails.env)
@env_vars = env_vars
@current_env = current_env
end
def domain
env_vars.fetch("DOMAIN") { default_domain }
end
def port
env_vars.fetch("WEB_PORT") { default_port }
end
def domain_with_port
[domain, port].compact.join(":")
end
def protocol
using_ssl? ? :https : :http
end
def using_ssl?
env_vars.fetch("USE_SSL") { default_use_ssl }
end
private
DEFAULTS = {
development: {
host: "localhost",
port: 3000,
use_ssl: false,
},
test: {
host: "localhost",
port: 31337,
use_ssl: false,
},
production: {
host: "bigmedia.ai",
use_ssl: true,
}
}
def default_domain
DEFAULTS.dig(current_env.to_sym, :host)
end
def default_port
DEFAULTS.dig(current_env.to_sym, :port)
end
def default_use_ssl
DEFAULTS.dig(current_env.to_sym, :use_ssl)
end
end

View File

@@ -0,0 +1,120 @@
# frozen_string_literal: true
class AppearanceRelease < ApplicationRecord
include Confirmable
include Contractable
include Exploitable
include Notable
include Releasable
include Searchable
include Signable
include Syncable
include Taggable
include PersonName
include GuardianPhotoable
include GuardianName
has_one_attached :person_photo
# These validations apply to all releases
validates :person_email, email: true, allow_blank: true
validates :person_first_name, :person_last_name, presence: true
scope :order_by_name, -> { order(:person_last_name) }
def self.random_contract_number
rand(1_000_000..9_999_998) # random 7 digit number
end
# We don't care for the argument but method WILL receive option name
# when called from inside with_option block, hence * argument
def self.face_photo_acceptable_content_types(*)
['image/png', 'image/jpeg']
end
def self.acceptable_import_file_extensions
['.png', '.jpeg', '.jpg', '.pdf']
end
# These validations apply to releases being signed by a minor
with_options if: :minor? do
validates :guardian_first_name, :guardian_last_name, presence: true
end
validates :person_photo, content_type: face_photo_acceptable_content_types
# These validations apply to releases created natively by the system (i.e. not imported from elsewhere)
with_options on: :native do
validates :signature, attached: true
validates :person_photo, attached: true, content_type: face_photo_acceptable_content_types
validate :person_photo_is_acceptable, if: :new_record? # Only validate photos on new releases
end
def contract_or_photo_is_attached
return if person_photo.attached? || contract.attached?
errors[:base] << I18n.t('appearance_releases.index.imported_appearance_release_missing_attachment')
end
# These validations apply to releases imported to the system from an outside source
with_options on: :non_native do
validate :contract_or_photo_is_attached
validates :person_photo, content_type: face_photo_acceptable_content_types
end
scope :with_person_photo, -> { joins(:person_photo_attachment) }
scope :with_person_photo_and_contract, -> { joins(:person_photo_attachment, :contract_attachment) }
scope :without_person_photo_or_contract, -> { where.not(id: with_person_photo_and_contract) }
scope :complete, -> { with_person_photo_and_contract }
scope :incomplete, -> { without_person_photo_or_contract }
scope :having_no_person_photo, -> { left_joins(:person_photo_attachment).group(:id).having('COUNT(active_storage_attachments) = 0') }
scope :with_person_name, ->(name) { where('person_first_name ILIKE ? OR person_last_name ILIKE ?', "%#{name}%") }
searchable_on %i[person_first_name person_last_name person_address person_email person_phone]
# All releases must respond to the following messages
def name
person_name
end
def filename_suffix
"#{person_last_name} #{person_first_name}"
end
def photo
person_photo
end
def photos
photo.attached? ? [photo] : []
end
def contact_person
@contact_person ||= Contact.new(name, person_address, person_email, person_phone)
end
def uses_edl?
true
end
def contract_file_name
"#{project.name.parameterize}_#{contract_template.release_type}_#{(signed_at || created_at).strftime('%Y.%m.%d')}_#{release_number}_#{filename_suffix.parameterize}"
end
private
# Validates the quality of the person photo
def person_photo_is_acceptable
return unless person_photo.attached?
# Call a remote API to perform the validation
image_validation = BrayniacAI::Validation.create(bucket_name: ENV['AWS_BUCKET'], object_name: person_photo.key)
unless image_validation.valid
errors.add(:person_photo, image_validation.error)
end
rescue ActiveResource::ConnectionError => e
# TODO
Rails.logger.error(e)
end
end

View File

@@ -0,0 +1,4 @@
class ApplicableMedium < ApplicationRecord
include Freeformable
end

View File

@@ -0,0 +1,6 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
scope :order_by_recent, -> { order(created_at: :desc) }
scope :order_by_recently_updated, -> { order(updated_at: :desc) }
end

View File

@@ -0,0 +1,72 @@
class AudioAnalysis
def initialize(video)
@video = video
end
# Use the custom hash to generate JSON format
def as_json(*)
to_hash
end
def to_hash
{
edl_bucket_name: aws_bucket_name,
edl_object_name: video.audio_only_edl_file.key,
acquired_audio: acquired_audio_list,
original_music: original_music_list,
}
end
def results
response.results
end
private
attr_reader :video
def aws_bucket_name
ENV["AWS_BUCKET"]
end
def acquired_audio_list
video.project.acquired_media_releases.flat_map do |acquired_media_release|
acquired_media_release.file_infos.audio.map do |file_info|
{ id: file_info.id, filename: file_info.filename }
end
end
end
def original_music_list
video.project.music_releases.flat_map do |music_release|
music_release.file_infos.map do |file_info|
{ id: file_info.id, filename: file_info.filename, composers: music_release.composer_info, publishers: music_release.publisher_info }
end
end
end
def analysis_uid
video.audio_analysis_uid
end
def fps
edl_event_gateway.fps
end
def edl_offset_seconds
edl_event_gateway.edl_offset_seconds
end
def edl_event_gateway
# TODO: Eventually cache this on the video itself to avoid an extra API call, but for now keep it simple
@edl_event_gateway ||= begin
files_for_request = AudioFilesForRequest.new(video, video.edl_timecode_start)
EdlEventGateway.new(files_for_request, "00:00:00:00", "00:00:00:00")
end
end
def response
@response ||= BrayniacAI::AudioRecognition.find(analysis_uid,
params: { fps: fps, edl_offset_seconds: edl_offset_seconds })
end
end

View File

@@ -0,0 +1,25 @@
class AudioConfirmation < ApplicationRecord
belongs_to :video
validates :time_elapsed, presence: true
validates :confirmation_type, :inclusion => { :in => ["original_music", "library_music"] }
scope :original, -> { where(confirmation_type: "original_music") }
def appears_at
Timecode.from_seconds(time_elapsed.to_f).to_s
end
def presented_source_file_name
case (confirmation_type)
when "original_music"
"(O) #{source_file_name}"
when "library_music"
"(L) #{source_file_name}"
end
end
def confirmation_type_library?
confirmation_type == "library_music"
end
end

View File

@@ -0,0 +1,28 @@
class AudioFilesForRequest
attr_reader :start_timecode_offset
def initialize(video, start_timecode_offset)
@video = video
@start_timecode_offset = start_timecode_offset
end
def file_object_name
video.file.key if video.file.attached?
end
def edl_file_object_name
video.audio_only_edl_file.key if video.audio_only_edl_file.attached?
end
def aws_bucket_name
ENV["AWS_BUCKET"]
end
def job_id
video.analysis_uid
end
private
attr_reader :video
end

View File

@@ -0,0 +1,5 @@
class BigMediaTime
def self.time_zone_now
Time.zone.now
end
end

View File

@@ -0,0 +1,68 @@
# frozen_string_literal: true
class BlankContract
def initialize(releasable, copies = 1)
@releasable = releasable
integer_number_of_copies = copies.to_i
@copies = integer_number_of_copies.positive? ? integer_number_of_copies : 1
end
def to_pdf
kit = PDFKit.new(as_html)
kit.to_file("tmp/#{filename}")
end
def filename
"blank_#{contract_template.release_type}_release.pdf"
end
def render_attributes
attributes = {
layout: 'contract_pdf',
template: 'blank_contracts/pdf',
locals: { releasable: @releasable, contract_template: contract_template, copies: @copies }
}
add_logo_to_attributes attributes
add_codes_to_attributes attributes
attributes
end
private
def contract_template
@releasable.contract_template
end
def project
@releasable.project
end
def locale
@releasable.locale
end
def as_html
I18n.with_locale(locale) do
ApplicationController.render render_attributes
end
end
def add_logo_to_attributes(attributes)
logo = @releasable.project.account.logo
attributes[:locals][:logo] = logo if logo.attached?
end
def add_codes_to_attributes(attributes)
attributes[:locals][:qr_codes] = []
attributes[:locals][:serial_numbers] = []
@copies.times do
random_number = SecureRandom.hex 4
custom_url = "#{contract_template.to_global_id.to_s}/#{random_number}"
qr_code = QrCode.new(custom_url)
attributes[:locals][:qr_codes] << qr_code.to_base64_png
attributes[:locals][:serial_numbers] << random_number.to_s
end
end
end

11
app/models/bookmark.rb Normal file
View File

@@ -0,0 +1,11 @@
class Bookmark < ApplicationRecord
belongs_to :video
validates :time_elapsed, presence: true
enum category: { "Other": 0, "Audio": 1, "Graphics": 2, "Video": 3 }
def appears_at
Timecode.from_seconds(time_elapsed.to_f).to_s
end
end

62
app/models/broadcast.rb Normal file
View File

@@ -0,0 +1,62 @@
class Broadcast < ApplicationRecord
include PgSearch
belongs_to :project
has_many :broadcast_recordings, dependent: :destroy
has_many_attached :files
has_secure_token
validates :name, presence: true
enum status: [:created, :active, :idle]
enum streamer_status: [:idle, :connected, :recording, :disconnected], _prefix: "streamer"
# Should we use callbacks for this, or something else?
after_create :create_mux_live_stream
after_destroy :destroy_mux_live_stream
pg_search_scope :search, {
against: [:name],
using: {
tsearch: { any_word: true, prefix: true },
trigram: {},
dmetaphone: { any_word: true }
}
}
def stream_playback_url
# TODO: This may change when we start using signed playback policy
"https://stream.mux.com/#{stream_playback_uid}.m3u8"
end
def stream_server_url
ENV['MUX_BROADCAST_SERVER_URL']
end
def zoom_meeting_url
project.zoom_meeting_url
end
private
def create_mux_live_stream
stream = MuxLiveStream.new
self.stream_uid = stream.id
self.stream_key = stream.key
self.stream_playback_uid = stream.playback_id
self.save!
end
def destroy_mux_live_stream
begin
stream = MuxLiveStream.new
stream.destroy_stream(self.stream_uid)
rescue MuxRuby::NotFoundError
rescue MuxRuby::ApiError => e
Rails.logger.error("Failed to delete live stream id #{stream_uid}\n" + e.message)
raise ActiveRecord::Rollback
end
end
end

View File

@@ -0,0 +1,19 @@
class BroadcastRecording < ApplicationRecord
belongs_to :broadcast
delegate :name, to: :broadcast, prefix: :broadcast
validates :asset_uid, uniqueness: true
def download_url
"https://stream.mux.com/#{asset_playback_uid}/#{file_name}?download=#{download_file_name}"
end
def playback_url
"https://stream.mux.com/#{asset_playback_uid}/#{file_name}"
end
def download_file_name
"#{broadcast_name}_Date_#{created_at.strftime("%Y-%m-%d")}_Time_#{created_at.strftime("%T")}".parameterize
end
end

4
app/models/composer.rb Normal file
View File

@@ -0,0 +1,4 @@
class Composer < ApplicationRecord
belongs_to :music_release
validates :name, :affiliation, :percentage, presence: true
end

View File

View File

@@ -0,0 +1,40 @@
module Archivable
extend ActiveSupport::Concern
included do
scope :active, -> { where("updated_at > ?", Config.active_threshold_date) }
scope :inactive, -> { where(updated_at: Config.inactive_date_range) }
scope :archived, -> { where("updated_at < ?", Config.archive_threshold_date) }
def archive_status
if updated_at > Config.active_threshold_date
:active
elsif updated_at.between? Config.archive_threshold_date, Config.active_threshold_date
:inactive
else
:archived
end
end
private
class Config
ACTIVE_THRESHOLD_IN_DAYS = 90
ARCHIVE_THRESHOLD_IN_DAYS = 365
class << self
def active_threshold_date
ACTIVE_THRESHOLD_IN_DAYS.days.ago.beginning_of_day
end
def archive_threshold_date
ARCHIVE_THRESHOLD_IN_DAYS.days.ago.end_of_day
end
def inactive_date_range
archive_threshold_date..active_threshold_date
end
end
end
end
end

View File

@@ -0,0 +1,10 @@
module Confirmable
extend ActiveSupport::Concern
included do
has_many :video_release_confirmations, as: :releasable, dependent: :destroy
has_many :confirmed_videos, source: :video, through: :video_release_confirmations
scope :appearing_in, -> (video) { joins(:confirmed_videos).where(videos: { id: video }) }
end
end

View File

@@ -0,0 +1,15 @@
module Contractable
extend ActiveSupport::Concern
included do
has_one_attached :contract
validates :contract, content_type: ["application/pdf"]
scope :having_contract_attached, -> (release_ids) { left_joins(:contract_attachment).where(active_storage_attachments: { record_id: release_ids }).group(:id).having("COUNT(active_storage_attachments) > 0") }
def contract_file_name
"#{project.name.parameterize}_#{contract_template.release_type}_#{(signed_at || created_at).strftime("%Y.%m.%d")}_#{release_number}_#{name.parameterize}"
end
end
end

View File

@@ -0,0 +1,24 @@
module Exploitable
extend ActiveSupport::Concern
FIELDS = [:applicable_medium, :territory, :term, :restriction]
included do
FIELDS.each do |field|
belongs_to field, optional: true
define_method "#{field}_value" do
if respond_to?(:contract_template) && contract_template.present?
# If contract template is present, use the value from there
contract_template.public_send("#{field}_value")
else
# Otherwise use the value of the label or the text
field_assoc = public_send(field)
if field_assoc
field_assoc.other? ? public_send("#{field}_text") : field_assoc.label
end
end
end
end
end
end

View File

@@ -0,0 +1,45 @@
module Filterable
extend ActiveSupport::Concern
class_methods do
def filterable_by(*scopes)
@@filterer = Filterer.new(scopes)
end
def filter(name)
@@filterer.filter(name, self)
end
def filter!(name)
@@filterer.filter!(name, self)
end
private
class Filterer
def initialize(scopes)
@scopes = Array.wrap(scopes).map(&:to_s)
end
# Returns the original scope if filter has not been whitelisted
def filter(name, scope)
return scope.all unless filter_is_whitelisted?(name)
scope.send(name)
end
# Raises an exception unless filter has been whitelisted
def filter!(name, scope)
raise "Cannot filter #{scope} by `#{name}`" unless filter_is_whitelisted?(name)
scope.send(name)
end
private
def filter_is_whitelisted?(name)
@scopes.include?(name)
end
end
end
end

View File

@@ -0,0 +1,8 @@
module Freeformable
extend ActiveSupport::Concern
included do
def other?
label == "Other"
end
end
end

View File

@@ -0,0 +1,20 @@
module GuardianName
extend ActiveSupport::Concern
included do
def guardian_name
"#{guardian_first_name} #{guardian_last_name}".titleize
end
def guardian_name=(value)
if value.include?(' ')
split = value.split(" ", 2)
self.guardian_first_name = split.first
self.guardian_last_name = split.last
else
self.guardian_first_name = value
self.guardian_last_name = "(Not Given)"
end
end
end
end

View File

@@ -0,0 +1,9 @@
module GuardianPhotoable
extend ActiveSupport::Concern
included do
has_one_attached :guardian_photo
validates :guardian_photo, content_type: ["image/png", "image/jpeg"]
end
end

View File

@@ -0,0 +1,7 @@
module Notable
extend ActiveSupport::Concern
included do
has_many :notes, as: :notable, dependent: :destroy
end
end

View File

@@ -0,0 +1,20 @@
module PersonName
extend ActiveSupport::Concern
included do
def person_name
"#{person_first_name} #{person_last_name}".titleize
end
def person_name=(value)
if value.include?(' ')
split = value.split(" ", 2)
self.person_first_name = split.first
self.person_last_name = split.last
else
self.person_first_name = value
self.person_last_name = "(Not Given)"
end
end
end
end

View File

@@ -0,0 +1,13 @@
module Photoable
extend ActiveSupport::Concern
included do
has_many_attached :photos
validates :photos, content_type: ["image/png", "image/jpeg"]
end
def photo
MainPhoto.new(photos.first)
end
end

View File

@@ -0,0 +1,70 @@
# frozen_string_literal: true
module Releasable
extend ActiveSupport::Concern
included do
belongs_to :project, touch: true
belongs_to :contract_template, optional: true
end
def release_number
@release_number ||= ReleaseNumber.new(self).value
end
def signed_on
if respond_to?(:signed_at) && signed_at.present?
signed_at.strftime('%D')
else
created_at.strftime('%D')
end
end
def fill_with_dummy_data(previewed_contract_template)
self.contract_template = previewed_contract_template
self.project_id = contract_template.project_id
self.created_at = DateTime.current
self.name = 'Dummy Name' if has_attribute? :name
self.person_first_name = 'Dummy' if has_attribute? :person_first_name
self.person_last_name = 'Person' if has_attribute? :person_last_name
if has_attribute? :person_date_of_birth
self.person_date_of_birth = DateTime.current
end
if has_attribute? :person_address_street1
self.person_address_street1 = 'Street 1'
end
if has_attribute? :person_address_street2
self.person_address_street2 = 'Street 2'
end
self.person_address_city = 'City' if has_attribute? :person_address_city
self.person_address_state = 'State' if has_attribute? :person_address_state
self.person_address_zip = 12_345 if has_attribute? :person_address_zip
if has_attribute? :person_address_country
self.person_address_country = 'Country'
end
if has_attribute? :person_address
self.person_address = 'Street 1, Street 2, City, State 12345, Country'
end
self.person_phone = '00 111 222 333 4444' if has_attribute? :person_phone
self.person_email = 'email@email.com' if has_attribute? :person_email
self.description = 'Dummy description' if has_attribute? :description
self.filming_started_on = DateTime.current if has_attribute? :filming_started_on
self.filming_ended_on = DateTime.current if has_attribute? :filming_ended_on
return if contract_template.guardian_clause.blank?
self.guardian_name = 'Guardian name' if has_attribute? :guardian_name
if has_attribute? :guardian_address
self.guardian_address = 'Street 3, Street 4, City-2, State-2 112233, Country-2'
end
self.guardian_address_street1 = 'Street 3' if has_attribute? :guardian_address_street1
self.guardian_address_street2 = 'Street 4' if has_attribute? :guardian_address_street2
self.guardian_address_city = 'City-2' if has_attribute? :guardian_address_city
self.guardian_address_state = 'State-2' if has_attribute? :guardian_address_state
self.guardian_address_zip = '112233' if has_attribute? :guardian_address_zip
self.guardian_address_country = 'Country-2' if has_attribute? :guardian_address_country
self.guardian_phone = '00 123 456 7890' if has_attribute? :guardian_phone
self.guardian_email = 'guardian.email@mail.com' if has_attribute? :guardian_email
self.minor = true if has_attribute? :minor
end
end

View File

@@ -0,0 +1,27 @@
module Searchable
extend ActiveSupport::Concern
included do
include PgSearch
end
class_methods do
def searchable_on(fields)
search_opts = {
against: fields,
associated_against: {
notes: [:content],
tags: [:name],
internal_tags: [:name]
},
using: {
tsearch: { any_word: true, prefix: true },
trigram: {},
dmetaphone: { any_word: true },
}
}
send(:pg_search_scope, :search, search_opts)
end
end
end

View File

@@ -0,0 +1,30 @@
module Signable
extend ActiveSupport::Concern
included do
include ActiveStorageSupport::SupportForBase64
has_one_base64_attached :signature
scope :having_a_signature, -> { left_joins(:signature_attachment).group(:id).having("COUNT(active_storage_attachments) > 0") }
scope :having_no_signature, -> { left_joins(:signature_attachment).group(:id).having("COUNT(active_storage_attachments) = 0") }
# Create some descriptive aliases for scopes above relating to native vs. non-native
scope :native, -> { having_a_signature }
scope :non_native, -> { having_no_signature }
end
def native?
signature.attached?
end
def signature_base64
return nil
end
def signature_base64=(data_uri)
return if data_uri.blank?
signature.attach(data: data_uri, filename: "signature.png", content_type: "image/png", identify: "false")
end
end

View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true
module Syncable
extend ActiveSupport::Concern
def as_json(params = {})
json = super(params)
json.each do |key, value|
if key == "id"
json[key] = value.to_s
end
end
{
id: id.to_s,
type: model_name.param_key,
attributes: json
}
end
end

View File

@@ -0,0 +1,9 @@
module Taggable
extend ActiveSupport::Concern
included do
acts_as_taggable_on :internal_tags, :tags
enum tagging_status: %i[ pending started finished failed ], _prefix: "tagging"
end
end

10
app/models/contact.rb Normal file
View File

@@ -0,0 +1,10 @@
class Contact
attr_accessor :name, :address, :email, :phone
def initialize(name, address, email, phone)
@name = name
@address = address
@email = email
@phone = phone
end
end

55
app/models/contract.rb Normal file
View File

@@ -0,0 +1,55 @@
class Contract
def initialize(releasable, preview = false)
@releasable = releasable
@preview = preview
end
def to_pdf
kit = PDFKit.new(as_html)
kit.to_file("tmp/#{filename}")
end
def filename(extension = "pdf")
"#{@releasable.contract_file_name}.#{extension}"
end
def render_attributes
{
layout: "contract_pdf",
locals: { releasable: @releasable, contract_template: contract_template, preview: @preview },
template: "contracts/pdf",
}
end
def render_attributes_with_logo
{
layout: "contract_pdf",
locals: { releasable: @releasable, contract_template: contract_template, logo: @releasable.project.account.logo, preview: @preview },
template: "contracts/pdf",
}
end
private
def contract_template
@releasable.contract_template
end
def project
@releasable.project
end
def locale
@releasable.locale
end
def as_html
I18n.with_locale(locale) do
if @releasable.project.account.logo.attached?
ApplicationController.render render_attributes_with_logo
else
ApplicationController.render render_attributes
end
end
end
end

View File

@@ -0,0 +1,56 @@
# frozen_string_literal: true
class ContractTemplate < ApplicationRecord
include Exploitable
include Syncable
include PgSearch
belongs_to :project
belongs_to :parent, class_name: 'ContractTemplate', optional: true
has_many :duplicates, class_name: 'ContractTemplate', foreign_key: 'parent_id'
has_many :talent_releases, dependent: :restrict_with_error
has_many :appearance_releases, dependent: :restrict_with_error
has_many :acquired_media_releases, dependent: :restrict_with_error
has_many :location_releases, dependent: :restrict_with_error
has_many :material_releases, dependent: :restrict_with_error
monetize :fee_cents
has_rich_text :body
has_rich_text :guardian_clause
validates :name, presence: true
validates :release_type, presence: true
validates :fee_cents, numericality: {
greater_than_or_equal_to: 0,
less_than_or_equal_to: 99_999_999_99
}
pg_search_scope :search, {
against: [:name, :release_type],
associated_against: {project: [:name]},
using: {
tsearch: {any_word: true, prefix: true},
trigram: {},
dmetaphone: {any_word: true}
}
}
scope :non_archived, -> { where(archived_at: nil) }
scope :order_by_name, -> { order(:name) }
def fee?
!fee.zero?
end
def releases
public_send("#{release_type}_releases")
end
def duplicated?
parent.present?
end
def archive
update(archived_at: Time.zone.now)
end
end

View File

@@ -0,0 +1,29 @@
# frozen_string_literal: true
class ContractTemplatePreview
def initialize(contract_template)
@contract_template = contract_template
end
def build_releasable
template_release_type = @contract_template.release_type
fill_ct_empty_fields_with_dummy_data
releasable = "#{template_release_type}_release".classify.safe_constantize.new
releasable.fill_with_dummy_data(@contract_template)
releasable
end
private
def fill_ct_empty_fields_with_dummy_data
if @contract_template.name.blank?
@contract_template.name = 'Contract Template Name'
end
if @contract_template.body.blank?
@contract_template.body = 'Contract Template Body text goes here'
end
if @contract_template.guardian_clause.blank?
@contract_template.guardian_clause = 'Contract Template Guardian Clause text goes here'
end
end
end

16
app/models/current.rb Normal file
View File

@@ -0,0 +1,16 @@
class Current < ActiveSupport::CurrentAttributes
attribute :account, :user
resets { Time.zone = nil }
def user=(user)
super
# overwritten later by the active account from session
self.account = user.primary_account
Time.zone = user.time_zone
end
def account=(account)
super
end
end

View File

@@ -0,0 +1,24 @@
class CustomRankOrder
def initialize(attribute, ordered_values)
@attribute = attribute
@ordered_values = ordered_values
end
def sql
Arel.sql case_statement
end
private
attr_accessor :attribute, :ordered_values
def case_statement
["CASE", case_conditions, "END"].join(" ")
end
def case_conditions
ordered_values.map.with_index do |release, index|
"WHEN #{attribute}='#{release}' THEN '#{index+1}'"
end.join(" ")
end
end

19
app/models/directory.rb Normal file
View File

@@ -0,0 +1,19 @@
class Directory < ApplicationRecord
belongs_to :project
belongs_to :user
has_many_attached :files
validates :name, presence: true, uniqueness: { scope: :project_id }
enum permissions: { "Everyone": 0, "Account Managers & Project Managers": 1, "Account Managers Only": 2 }
enum category: { "Other": 0, "Finance": 1, "Scripts": 2, "Call Sheets": 3, "Photos": 4, "Videos": 5 }
scope :order_by_name, -> { order(name: :asc) }
scope :for_associates, -> { where(permissions: "Everyone") }
scope :for_project_managers, -> { where(permissions: ["Everyone", "Account Managers & Project Managers"]) }
def search_files(query)
files_attachments.joins(:blob).where("active_storage_blobs.filename ILIKE ?", "%#{query}%")
end
end

26
app/models/download.rb Normal file
View File

@@ -0,0 +1,26 @@
class Download < ApplicationRecord
include PgSearch
belongs_to :project
has_one_attached :file
enum status: { not_started: 0, pending: 1, success: 2, failure: 3 }
scope :unfinished_desc_order, -> { where(status: [:not_started, :pending]).order("created_at DESC") }
def self.searchable_on(fields)
search_opts = {
against: fields,
using: {
tsearch: { any_word: true, prefix: true },
trigram: {},
dmetaphone: { any_word: true },
}
}
send(:pg_search_scope, :search, search_opts)
end
searchable_on %i[name release_type]
end

View File

@@ -0,0 +1,48 @@
class DurationTimecode
def self.parse(timecode)
if match = DURATION_TIMECODE_REGEX.match(timecode)
hours = match[1].to_i
minutes = match[2].to_i
seconds = match[3].to_i
new(hours, minutes, seconds)
else
raise ArgumentError.new("Must be HH:MM:SS format")
end
end
def self.from_seconds(total_seconds)
hours = total_seconds / 60 / 60
minutes = total_seconds / 60 % 60
seconds = total_seconds % 60
DurationTimecode.new(hours, minutes, seconds)
end
attr_reader :hours, :minutes, :seconds
def initialize(hours, minutes, seconds)
@hours = hours
@minutes = minutes
@seconds = seconds
end
def to_s
[hours, minutes, seconds].map { |t| t.to_s.rjust(2,'0') }.join(':')
end
def to_i
[
hours * 60 * 60,
minutes * 60,
seconds
].sum
end
def +(other)
DurationTimecode.from_seconds(to_i + other.to_i)
end
private
DURATION_TIMECODE_REGEX = /\A(\d+):(\d+):(\d+)\z/
end

21
app/models/edl_event.rb Normal file
View File

@@ -0,0 +1,21 @@
class EdlEvent < Struct.new(
:channel,
:start_time,
:timecode_in,
:timecode_out,
:duration,
:source_file_name,
:clip_name,
:description,
:matches,
keyword_init: true
)
def attributes
to_h
end
def public_attributes
attributes.except(:start_time, :matches)
end
end

View File

@@ -0,0 +1,75 @@
class EdlEventGateway
def initialize(files_for_request, timecode_start, timecode_end, collection: {}, channel_filter: "")
@files_for_request = files_for_request
@timecode_start = timecode_start
@timecode_end = timecode_end
@collection = collection
@channel_filter = channel_filter
end
def edl_events
@edl_events ||= response_results.
filter { |response_edl_event| response_edl_event.channel.include?(channel_filter) }.
map do |response_edl_event|
EdlEvent.new(
channel: response_edl_event.channel,
start_time: response_edl_event.start_time,
timecode_in: response_edl_event.timecode_in,
timecode_out: response_edl_event.timecode_out,
duration: response_edl_event.duration,
source_file_name: response_edl_event.source_file_name,
clip_name: response_edl_event.clip_name,
description: response_edl_event.description,
matches: response_edl_event.matches,
)
end
end
def edl_timecode_start
return if response.nil?
response.edl_timecode_start
end
def fps
return if response.nil?
response.fps
end
def edl_offset_seconds
return if response.nil?
response.edl_offset_seconds
end
private
attr_reader :files_for_request, :timecode_start, :timecode_end, :collection, :channel_filter
def as_json(*)
{
job_id: files_for_request.job_id,
video_bucket_name: files_for_request.aws_bucket_name,
video_object_name: files_for_request.file_object_name,
edl_bucket_name: files_for_request.aws_bucket_name,
edl_object_name: files_for_request.edl_file_object_name,
timecode_start: timecode_start,
timecode_end: timecode_end,
collection: collection,
edl_timecode_start: files_for_request.start_timecode_offset,
}.compact
end
def response_results
return [] if response.nil?
response.results
end
def response
@response ||= BrayniacAI::EdlParse.create as_json
rescue ActiveResource::ServerError => e
nil
end
end

View File

@@ -0,0 +1,128 @@
module ExcelReports
module AudioReports
class AudioConfirmationData
def initialize(audio_confirmation)
@audio_confirmation = audio_confirmation
end
def cue_number
""
end
def timecode_in
audio_confirmation.timecode_in
end
def title_and_source_file_name
[audio_confirmation.title, audio_confirmation.source_file_name].reject(&:blank?).join(" - ")
end
def catalog
audio_confirmation.catalog
end
def use
"#{audio_confirmation.music_type} #{audio_confirmation.music_category}"
end
def interested_parties
"Composers:\n#{audio_confirmation.composer_info}\nPublishers:\n#{audio_confirmation.publisher_info}"
end
def composers
converted_composers.map do |composer|
"#{composer.name} (#{composer.affiliation})"
end.join("\n")
end
def composers_split
converted_composers.map(&:percentage).join("\n")
end
def composers_with_split
converted_composers.map do |composer|
"#{composer.name} (#{composer.affiliation}) #{composer.percentage}%"
end.join("\n")
end
def composers_cae_numbers
converted_composers.map(&:cae_number).join("\n")
end
def publishers
converted_publishers.map do |publisher|
"#{publisher.name} (#{publisher.affiliation})"
end.join("\n")
end
def publishers_split
converted_publishers.map(&:percentage).join("\n")
end
def publishers_with_split
converted_publishers.map do |publisher|
"#{publisher.name} (#{publisher.affiliation}) #{publisher.percentage}%"
end.join("\n")
end
def music_type
audio_confirmation.music_type.to_s.first
end
def music_category
audio_confirmation.music_category.to_s.first
end
def timecode_out
audio_confirmation.timecode_out
end
def duration
audio_confirmation.duration
end
def origin
if audio_confirmation.confirmation_type_library?
"Production Library (Non-affiliated)"
else
"Commissioned"
end
end
def ==(other)
audio_confirmation == other.audio_confirmation
end
protected
attr_reader :audio_confirmation
def converted_composers
composers = audio_confirmation.composer_info.split("|")
composers.map do |composer_info|
parts = composer_info.split(",")
if cae_number_index = parts.index { |part| part.include?("$cae") }
cae_number = parts.delete_at(cae_number_index)
end
Composer.new(
name: parts[0...-2].join(","),
affiliation: parts[-2].strip,
percentage: parts.last,
cae_number: cae_number.to_s.gsub("$cae:", "").strip
)
end
end
def converted_publishers
audio_confirmation.publisher_info.split("|").map do |publisher_info|
parts = publisher_info.split(",")
Publisher.new(
name: parts[0...-2].join(","),
affiliation: parts[-2].strip,
percentage: parts.last,
)
end
end
end
end
end

View File

@@ -0,0 +1,25 @@
module ExcelReports
module AudioReports
class BrayInnovationGroupMusicCueHeaderData
def initialize(video)
@video = video
end
def title
video.name
end
def company_name
video.project.producer_name
end
def company_address
video.project.producer_address
end
private
attr_reader :video
end
end
end

View File

@@ -0,0 +1,40 @@
module ExcelReports
module AudioReports
class BrayInnovationGroupMusicCueReport
def initialize(video)
@video = video
end
def to_xls
BrayInnovationGroupMusicCueSheet.build(workbook, report_data, report_header_data)
package.to_stream.read
end
def filename
"#{video.file.filename.to_s.parameterize}_big-cue-sheet.xlsx"
end
private
attr_reader :video
def report_data
video.audio_confirmations.order(timecode_in: :asc).map do |audio_confirmation|
AudioConfirmationData.new(audio_confirmation)
end
end
def report_header_data
BrayInnovationGroupMusicCueHeaderData.new(video)
end
def workbook
@workbook ||= package.workbook
end
def package
@package ||= Axlsx::Package.new
end
end
end
end

View File

@@ -0,0 +1,97 @@
module ExcelReports
module AudioReports
class BrayInnovationGroupMusicCueSheet < ::ExcelReports::Worksheet
def title
"BiG Music Cue Sheet"
end
def fill_content(sheet)
sheet.add_row ["TITLE", header_data.title]
sheet.add_row ["COMPANY NAME", header_data.company_name]
sheet.add_row ["ADDRESS", header_data.company_address]
sheet.add_row ["LENGTH", ""]
sheet.add_row ["TYPE", ""]
sheet.add_row
sheet.add_row table_headers
sheet.add_row table_subheaders
data.each do |datum|
sheet.add_row [
datum.cue_number,
datum.title_and_source_file_name,
datum.composers,
datum.composers_split,
datum.publishers,
datum.publishers_split,
datum.music_type,
datum.music_category,
datum.timecode_in,
datum.timecode_out,
datum.duration,
]
end
end
def format(sheet)
sheet.merge_cells "G7:H7"
sheet.column_widths *column_widths
end
def style(sheet)
sheet.add_style "A1:A5", Styles::PROGRAM_HEADER
sheet.add_style "B1:B5", Styles::HEADER_DATA
sheet.add_style "A7:K7", Styles::TABLE_HEADER
if data.any?
sheet.add_style "A#{data_start_index}:K#{data_end_index}", Styles::TABLE_DATA
end
end
private
def table_headers
[
"Cue #",
"Cue Title",
"Composer(s)/Affiliation",
"%",
"Publisher(s)/Affiliation",
"%",
"Use",
"",
"In Time",
"Out Time",
"Duration",
]
end
def table_subheaders
[
"",
"",
"",
"",
"",
"",
"I = Instr.\nV = Vocal",
"B = Bckgrnd\nF = Feature\nT = Theme",
"",
"",
"",
]
end
def column_widths
[15, 40, 20, 10, 20, 10, 20, 20, 15, 15, 15]
end
def data_start_index
9
end
def data_end_index
(data_start_index + data.size) - 1
end
end
end
end

View File

@@ -0,0 +1,29 @@
module ExcelReports
module AudioReports
class DiscoveryMusicCueHeaderData
def initialize(video)
@video = video
end
def title
video.name
end
def company_name
video.project.producer_name
end
def client_name
video.project.client_name
end
def episode_number
video.number
end
private
attr_reader :video
end
end
end

View File

@@ -0,0 +1,40 @@
module ExcelReports
module AudioReports
class DiscoveryMusicCueReport
def initialize(video)
@video = video
end
def to_xls
DiscoveryMusicCueSheet.build(workbook, report_data, report_header_data)
package.to_stream.read
end
def filename
"#{video.file.filename.to_s.parameterize}_discovery-cue-sheet.xlsx"
end
private
attr_reader :video
def report_header_data
DiscoveryMusicCueHeaderData.new(video)
end
def report_data
video.audio_confirmations.order(timecode_in: :asc).map do |audio_confirmation|
AudioConfirmationData.new(audio_confirmation)
end
end
def workbook
@workbook ||= package.workbook
end
def package
@package ||= Axlsx::Package.new
end
end
end
end

View File

@@ -0,0 +1,90 @@
module ExcelReports
module AudioReports
class DiscoveryMusicCueSheet < ::ExcelReports::Worksheet
def title
"Discovery Music Cue Sheet"
end
def fill_content(sheet)
sheet.add_row ["Original Production Title", header_data.title, "Original Series Title", "", "Broadcaster", ""]
sheet.add_row ["Alternative Production Title", "", "Alternative Series Title", "", "Production Company", header_data.company_name]
sheet.add_row ["Local Production Title", "", "Local Series Title", "", "", ""]
sheet.add_row ["Working Production Title", "", "Working Series Title", "", "Production Number", ""]
sheet.add_row ["Version Production Title", "", "Version Series Title", "", "Director", ""]
sheet.add_row ["Episode No.", header_data.episode_number, "Season No.", "", "Production Parent Identifier", ""]
sheet.add_row ["Year", "", "Country", "", "Soundmouse Legacy Identifier", ""]
sheet.add_row ["Duration", "", "Type", "", "Production ID", ""]
sheet.add_row ["First Transmission", "", "Source", ""]
sheet.add_row ["Product Name", ""]
sheet.add_row ["Client Name", header_data.client_name]
sheet.add_row ["Narrative", ""]
sheet.add_row ["End Line", ""]
sheet.add_row ["Clock Number", ""]
sheet.add_row ["ISAN", ""]
sheet.add_row
sheet.add_row table_headers
data.each_with_index do |datum, index|
sheet.add_row [
index + 1,
datum.timecode_in,
datum.title_and_source_file_name,
datum.catalog,
datum.use,
datum.interested_parties,
"",
datum.duration,
]
end
end
def format(sheet)
sheet.column_widths *column_widths
end
def style(sheet)
sheet.add_style "A1:A15", Styles::PROGRAM_HEADER
sheet.add_style "C1:C9", Styles::PROGRAM_HEADER
sheet.add_style "E1:E8", Styles::PROGRAM_HEADER
sheet.add_style "B1:B15", Styles::HEADER_DATA
sheet.add_style "D1:D9", Styles::HEADER_DATA
sheet.add_style "F1:F8", Styles::HEADER_DATA
sheet.add_style "A17:H17", Styles::TABLE_HEADER
if data.any?
sheet.add_style "A#{data_start_index}:H#{data_end_index}", Styles::TABLE_DATA
end
end
private
def table_headers
[
"No.",
"Timecode",
"Title",
"Music Origin",
"Use (Theme/Description)",
"Interested Parties",
"Identifiers",
"Duration",
]
end
def column_widths
[20, 20, 40, 20, 20, 20, 20, 20]
end
def data_start_index
18
end
def data_end_index
(data_start_index + data.size) - 1
end
end
end
end

View File

@@ -0,0 +1,36 @@
module ExcelReports
module AudioReports
class NatGeoMusicCueSheet
def initialize(video)
@video = video
end
def to_xls
NatGeoMusicCueSheets::MainSheet.build(workbook, report_data)
package.to_stream.read
end
def filename
"#{video.file.filename.to_s.parameterize}_music-cue-sheet.xlsx"
end
private
attr_reader :video
def report_data
video.audio_confirmations.order(timecode_in: :asc).map do |audio_confirmation|
AudioConfirmationData.new(audio_confirmation)
end
end
def workbook
@workbook ||= package.workbook
end
def package
@package ||= Axlsx::Package.new
end
end
end
end

View File

@@ -0,0 +1,97 @@
module ExcelReports
module AudioReports
module NatGeoMusicCueSheets
class MainSheet < Worksheet
def title
"Nat Geo Music Cue Sheet"
end
def fill_content(sheet)
sheet.add_row ["National Geographic", "", "", "", "", "", "", "", "", ""]
sheet.add_row ["Music Cue Sheet", "", "", "", "", "", "", "", "", ""]
sheet.add_row
sheet.add_row table_headers
data.each_with_index do |datum, index|
sheet.add_row [
index + 1,
datum.title_and_source_file_name,
datum.timecode_in,
datum.timecode_out,
datum.composers_with_split,
datum.publishers_with_split,
datum.catalog,
datum.origin,
datum.use,
datum.duration,
]
end
sheet.add_row ["", "", "", "", "", "", "", "", "Total Music Duration:", total_duration]
end
def format(sheet)
sheet.column_widths *column_widths
cells_to_merge.each { |cell| sheet.merge_cells cell }
end
def style(sheet)
sheet.add_style "A1:J1", Styles::BOLD, { sz: 18 }
sheet.add_style "A2:J2", Styles::BOLD, Styles::FULLY_CENTERED, { sz: 16 }
sheet.add_style "A#{data_start_index-1}:J#{data_start_index-1}", Styles::BOLD, Styles::THIN_BLACK_BORDER, Styles::WRAP_TEXT
if data.any?
sheet.add_style "A#{data_start_index}:J#{data_end_index}", Styles::THIN_BLACK_BORDER
sheet.add_style "E#{data_start_index}:F#{data_end_index}", Styles::WRAP_TEXT
sheet.add_style "I#{data_start_index}:I#{data_end_index}", Styles::WRAP_TEXT
end
sheet.add_style "I#{data_end_index + 1}", Styles::BOLD, Styles::WRAP_TEXT
end
private
def table_headers
[
"No.",
"Title",
"In",
"Out",
"Composer",
"Publisher",
"Record Label / Library",
"Music Origin",
"Use",
"Duration",
]
end
def column_widths
[10, 30, 20, 20, 50, 50, 20, 25, 25, 20]
end
def cells_to_merge
%w(A1:J1 A2:J2)
end
def data_start_index
5
end
def data_end_index
(data_start_index + data.size) - 1
end
def total_duration
data.map do |datum|
begin
DurationTimecode.parse(datum.duration)
rescue ArgumentError
DurationTimecode.new(0, 0, 0)
end
end.sum.to_s
end
end
end
end
end

View File

@@ -0,0 +1,36 @@
module ExcelReports
module AudioReports
class NatGeoOriginalMusicLog
def initialize(video)
@video = video
end
def to_xls
NatGeoOriginalMusicLogs::MainSheet.build(workbook, report_data)
package.to_stream.read
end
def filename
"#{video.file.filename.to_s.parameterize}_original-music-log.xlsx"
end
private
attr_reader :video
def report_data
video.audio_confirmations.original.order(timecode_in: :asc).map do |audio_confirmation|
AudioConfirmationData.new(audio_confirmation)
end
end
def workbook
@workbook ||= package.workbook
end
def package
@package ||= Axlsx::Package.new
end
end
end
end

View File

@@ -0,0 +1,74 @@
module ExcelReports
module AudioReports
module NatGeoOriginalMusicLogs
class MainSheet < Worksheet
def title
"Nat Geo Original Music Log"
end
def fill_content(sheet)
sheet.add_row ["National Geographic", "", "", "",]
sheet.add_row ["National Geographic Music", "", "", "",]
sheet.add_row ["Original Music Log", "", "", "",]
sheet.add_row ["[PROGRAM]", "", "", ""]
sheet.add_row ["[SEASON #]", "", "", ""]
sheet.add_row [BigMediaTime.time_zone_now.to_date.strftime("%D"), "", "", ""]
sheet.add_row table_headers
data.each_with_index do |datum, index|
sheet.add_row [
datum.title_and_source_file_name,
datum.composers_with_split,
datum.composers_cae_numbers,
datum.publishers_with_split,
]
end
end
def format(sheet)
sheet.column_widths *column_widths
cells_to_merge.each { |cell| sheet.merge_cells cell }
end
def style(sheet)
sheet.add_style "A1:D1", Styles::BOLD, { sz: 18 }
sheet.add_style "A2:D2", Styles::BOLD, Styles::FULLY_CENTERED, { sz: 16 }
sheet.add_style "A3:D3", Styles::FULLY_CENTERED, { sz: 14 }
sheet.add_style "A#{data_start_index-1}:D#{data_start_index-1}", Styles::BOLD, Styles::THIN_BLACK_BORDER, Styles::WRAP_TEXT, Styles::BG_GRAY
if data.any?
sheet.add_style "A#{data_start_index}:D#{data_end_index}", Styles::THIN_BLACK_BORDER
sheet.add_style "A#{data_start_index}:D#{data_end_index}", Styles::WRAP_TEXT
end
end
private
def table_headers
[
"CUE TITLE",
"COMPOSER(S), % SPLIT & PRO",
"COMPOSER CAE/IPI#",
"PUBLISHER(S), % SPLIT & PRO",
]
end
def column_widths
[25, 45, 25, 80]
end
def cells_to_merge
%w(A1:D1 A2:D2 A3:D3)
end
def data_start_index
8
end
def data_end_index
(data_start_index + data.size) - 1
end
end
end
end
end

View File

@@ -0,0 +1,68 @@
module ExcelReports
module GraphicReports
class DiscoveryGfxCueList
def initialize(video)
@video = video
end
def to_xls
DiscoveryGfxCueLists::TextedElementsSheet.build(workbook, graphics_elements_data, graphics_elements_header_data)
package.to_stream.read
end
def filename(format = "xlsx")
name = [@video.file.filename.to_s, "gfx-cue-list"].map(&:parameterize).join("_")
[name, format].join(".")
end
private
attr_reader :video
def graphics_elements_data
video.graphics_elements.order(timecode_in: :asc).map do |graphics_element|
GraphicsElementsData.new(
graphics_element.text,
graphics_element.timecode_in,
graphics_element.timecode_out,
graphics_element.duration,
)
end
end
def graphics_elements_header_data
GraphicsElementsHeaderData.new(
video.name,
video.number,
video.project.client_name,
video.project.account.name,
)
end
def workbook
@workbook ||= package.workbook
end
def package
@package ||= Axlsx::Package.new
end
class GraphicsElementsData < Struct.new(
:text,
:timecode_in,
:timecode_out,
:duration,
)
end
class GraphicsElementsHeaderData < Struct.new(
:program_title,
:episode,
:network,
:company,
)
end
end
end
end

View File

@@ -0,0 +1,56 @@
module ExcelReports
module GraphicReports
module DiscoveryGfxCueLists
class TextedElementsSheet < ::ExcelReports::Worksheet
def title
"Texted Elements List"
end
def fill_content(sheet)
sheet.add_row ["Texted Element / CG List"]
sheet.add_row
sheet.add_row ["Program Title", header_data.program_title]
sheet.add_row ["Episode", header_data.episode]
sheet.add_row ["Network", header_data.network]
sheet.add_row ["Executive Producer", ""]
sheet.add_row ["Production Company", header_data.company]
sheet.add_row
sheet.add_row ["Title/Subtitle/Graphics", "T/C IN", "T/C OUT", "TRT"]
data.each do |graphics_data|
sheet.add_row [graphics_data.text, graphics_data.timecode_in, graphics_data.timecode_out, graphics_data.duration]
end
end
def format(sheet)
sheet.column_widths *column_widths
end
def style(sheet)
sheet.add_style "A1", Styles::BOLD, { sz: 12 }
sheet.add_style "A9:D9", Styles::TABLE_HEADER
sheet.add_style "A3:A7", Styles::PROGRAM_HEADER
sheet.add_style "B3:B7", Styles::HEADER_DATA
if data.any?
sheet.add_style "A#{data_start_index}:D#{data_end_index}", Styles::TABLE_DATA
end
end
private
def column_widths
[60, 20, 20]
end
def data_start_index
10
end
def data_end_index
(data_start_index + data.size) - 1
end
end
end
end
end

View File

@@ -0,0 +1,42 @@
module ExcelReports
module GraphicReports
class NatGeoTextGraphicsLog
def initialize(video)
@video = video
end
def to_xls
NatGeoTextGraphicsLogs::InternalProgramGfxLogSheet.build(
workbook, graphics_elements_data, graphics_elements_header_data
)
package.to_stream.read
end
def filename(format = "xlsx")
name = [@video.file.filename.to_s, "text-graphics-log"].map(&:parameterize).join("_")
[name, format].join(".")
end
private
attr_reader :video
def graphics_elements_data
video.graphics_elements.order(timecode_in: :asc)
end
def graphics_elements_header_data
video
end
def workbook
@workbook ||= package.workbook
end
def package
@package ||= Axlsx::Package.new
end
end
end
end

View File

@@ -0,0 +1,78 @@
module ExcelReports
module GraphicReports
module NatGeoTextGraphicsLogs
class InternalProgramGfxLogSheet < Worksheet
def title
"Internal Program GFX Log"
end
def fill_content(sheet)
sheet.add_row ["NATIONAL GEOGRAPHIC", "", "", "", "", "", ""]
sheet.add_row ["Text-Graphics Log", "", "", "", "", "", ""]
sheet.add_row ["(Title Sequence, Maps, Lower Thirds, CGI, Subtitles, Captions, Text Identifiers, Chyrons, Credits etc. )", "", "", "", "", "", ""]
sheet.add_row
sheet.add_row ["", "Series Name:", "", "", "", "", ""]
sheet.add_row ["", "Episode Title:", header_data.name, "", "", "", ""]
sheet.add_row ["", "Episode Number:", header_data.number, "", "", "", ""]
sheet.add_row ["", "Traffic Code(s):", "", "", "", "", ""]
sheet.add_row ["", "Date:", BigMediaTime.time_zone_now.to_date.strftime("%D"), "", "", "", ""]
sheet.add_row
sheet.add_row table_headers
data.each do |graphics_data|
sheet.add_row [graphics_data.graphic_type, graphics_data.text, graphics_data.timecode_in, graphics_data.timecode_out, "", "", ""]
end
sheet.add_row(["*Clean Graphics should appear at the end of the program master. If all shots do not fit on the program master, a separate graphics master should be delivered."])
sheet.add_row(["*If delivering textless masters & no clean graphics at the end, denote N/A in the clean scene timecode section."])
end
def format(sheet)
sheet.column_widths *column_widths
cells_to_merge.each { |cell| sheet.merge_cells cell }
end
def style(sheet)
sheet.add_style "A1:A2", Styles::BOLD, Styles::FULLY_CENTERED, { sz: 12 }
sheet.add_style "A3", Styles::FULLY_CENTERED, { sz: 12 }
sheet.add_style "B5:B9", Styles::BOLD, Styles::HORIZONTAL_RIGHT
sheet.add_style "A11:G11", Styles::BOLD, Styles::FULLY_CENTERED, Styles::THIN_BLACK_BORDER, Styles::WRAP_TEXT
if data.any?
sheet.add_style "A#{data_start_index}:G#{data_end_index}", Styles::THIN_BLACK_BORDER
end
end
private
def table_headers
[
"Element\n(Ex: Title, Lower Third, Subtitle, Graphic, Credits, etc.)",
"Description",
"Program Timecode In",
"Program Timecode Out",
"Clean Scene Timecode In",
"Clean Scene Timecode Out",
"Font Information\n(Include Font Name, Style, Color, & Opacity)"
]
end
def column_widths
[25, 36, 18, 18, 18, 18, 23]
end
def cells_to_merge
%w(A1:G1 A2:G2 A3:G3 C5:E5 C6:E6 C7:E7 C8:E8 C9:E9)
end
def data_start_index
12
end
def data_end_index
(data_start_index + data.size) - 1
end
end
end
end
end

View File

@@ -0,0 +1,33 @@
module ExcelReports
module IssuesAndConcernsReports
class IssuesAndConcernsReport
def initialize(video)
@video = video
end
def to_xls
IssuesAndConcernsWorksheet.new(
workbook,
IssuesAndConcernsReportPresenter.new(video)
).build
package.to_stream.read
end
def filename
"#{video.file.filename.to_s.parameterize}_issues-and-concerns.xlsx"
end
private
attr_reader :video
def workbook
@workbook ||= package.workbook
end
def package
@package ||= Axlsx::Package.new
end
end
end
end

View File

@@ -0,0 +1,58 @@
module ExcelReports
module IssuesAndConcernsReports
class IssuesAndConcernsWorksheet < ::ExcelReports::Worksheet
def title
"Issues and Concerns Report"
end
def fill_content(sheet)
sheet.add_row ["ISSUES AND CONCERNS REPORT"]
sheet.add_row ["Project Name", data.project_name]
sheet.add_row ["Video Name", data.video_name]
sheet.add_row ["Date of Report", data.date_of_report]
sheet.add_row []
sheet.add_row ["Track", "TC In", "TC Out", "Clip Name", "Source File Name", "Notes"]
data.unreleased_appearances.each do |unreleased_appearance|
sheet.add_row [
unreleased_appearance.channel,
unreleased_appearance.timecode_in,
unreleased_appearance.timecode_out,
unreleased_appearance.clip_name,
unreleased_appearance.source_file_name,
unreleased_appearance.note_text
]
end
end
def format(sheet)
sheet.column_widths *column_widths
end
def style(sheet)
sheet.add_style "A1", Styles::BOLD, { sz: 14 }
sheet.add_style "A2:A4", Styles::PROGRAM_HEADER
sheet.add_style "B2:B4", Styles::HEADER_DATA
sheet.add_style "A6:F6", Styles::TABLE_HEADER
if data.unreleased_appearances.any?
sheet.add_style "A#{data_start_index}:F#{data_end_index}", Styles::TABLE_DATA
end
end
private
def data_start_index
7
end
def data_end_index
(data_start_index + data.unreleased_appearances.size) - 1
end
def column_widths
[15, 15, 15, 15, 15, 50]
end
end
end
end

View File

@@ -0,0 +1,74 @@
module ExcelReports
module VideoReports
class DiscoveryProductionElementsLog
attr_reader :video
def initialize(video)
@video = video
end
def to_xls
build
package.to_stream.read
end
def filename(format = "xlsx")
name = [video.file.filename.to_s, "production-elements-log"].map(&:parameterize).join("_")
[name, format].join(".")
end
private
def talent_data
@talent_data ||= data_for("TalentRelease")
end
def appearance_data
@appearance_data ||= data_for("AppearanceRelease")
end
def location_data
@location_data ||= data_for("LocationRelease")
end
def acquired_media_data
@acquired_media_data ||= data_for("AcquiredMediaRelease")
end
def music_data
@music_data ||= data_for("MusicRelease")
end
def material_data
@material_data ||= data_for("MaterialRelease")
end
def data_for(release_type)
video.
video_release_confirmations.
where(releasable_type: release_type).
order(timecode_in: :asc).
map { |confirmation| ReleasableDataAdapter.new(confirmation) }
end
def build
DiscoveryProductionElementsLogs::MediaRightsCertificationSheet.build(workbook)
DiscoveryProductionElementsLogs::AcquiredFootageAndStillsSheet.build(workbook, acquired_media_data)
DiscoveryProductionElementsLogs::MusicSheet.build(workbook, music_data)
DiscoveryProductionElementsLogs::TalentSheet.build(workbook, talent_data)
DiscoveryProductionElementsLogs::AppearanceSheet.build(workbook, appearance_data)
DiscoveryProductionElementsLogs::LocationSheet.build(workbook, location_data)
DiscoveryProductionElementsLogs::NameProductLogoSheet.build(workbook, material_data)
DiscoveryProductionElementsLogs::ProductIntegrationSheet.build(workbook)
end
def workbook
@workbook ||= package.workbook
end
def package
@package ||= Axlsx::Package.new
end
end
end
end

View File

@@ -0,0 +1,104 @@
module ExcelReports
module VideoReports
module DiscoveryProductionElementsLogs
class AcquiredFootageAndStillsSheet < ::ExcelReports::Worksheet
def title
"Acquired Footage & Stills"
end
def fill_content(sheet)
sheet.add_row ["ACQUIRED FOOTAGE/STILLS/ PUBLIC DOMAIN LOG (for all episodes/programs)"]
sheet.add_row [instructions]
sheet.add_row
sheet.add_row table_headers
sheet.add_row table_subheaders
data.each do |confirmation_data|
sheet.add_row [
confirmation_data.episode_number,
confirmation_data.episode_title,
confirmation_data.source_file_and_clip_names,
confirmation_data.timecode_in,
confirmation_data.timecode_out,
confirmation_data.duration,
confirmation_data.description,
licensor_with_phone_number(confirmation_data),
confirmation_data.applicable_media,
confirmation_data.territory,
confirmation_data.term,
confirmation_data.restrictions,
]
end
end
def format(sheet)
sheet.column_widths *column_widths
cells_to_merge.each { |cell| sheet.merge_cells cell }
end
def style(sheet)
sheet.add_style "A1", Styles::BOLD, { sz: 14 }
sheet.add_style "A2", { sz: 10 }
sheet.add_style "A4:L4", Styles::TABLE_HEADER
sheet.add_style "B5:L5", Styles::TABLE_HEADER
sheet.add_style "A4", Styles::WRAP_TEXT
sheet.add_style "L4", Styles::WRAP_TEXT
if data.any?
sheet.add_style "A#{data_start_index}:L#{data_end_index}", Styles::TABLE_DATA
end
end
private
def licensor_with_phone_number(confirmation)
[
confirmation.name,
confirmation.confirmation.releasable.person_phone
].reject(&:blank?)
.join("\n")
end
def data_start_index
6
end
def data_end_index
(data_start_index + data.size) - 1
end
def instructions
<<~INSTRUCTIONS
All licenses must conform to the DCI contractual requirements and must be fully executed.
Releases, licenses and agreements should only be logged once, based on the order of their appearance in the program.
If an image does not appear in the final Program, log that release after those in the final Program; indicate on the Log sheet NOT IN FINAL PROGRAM and list the camera tape only.
Refer to the source of all third party footage in the exact form as it appears on the release.
Please note that if it is not possible to deliver an English language agreement, an English language translation must accompany any agreement delivered in a foreign language (if applicable).
INSTRUCTIONS
end
def table_headers
["EPISODE NUMBER", "EPISODE TITLE", "CLIP #", "PROGRAM MASTER TC", "", "TOTAL TIME", "BRIEF VIDEO DESCRIPTION", "LICENSOR\n(incl phone number)", "EXPLOITABLE RIGHTS", "", "", "DCL Rights Waiver Uploaded?"]
end
def table_subheaders
["", "", "", "IN", "OUT", "", "", "", "MEDIA", "TERRITORY", "TERM", ""]
end
def column_widths
[10, 20, 25, 15, 15, 15, 20, 15, 15, 15, 15, 10]
end
def cells_to_merge
%w(
A1:M1 A2:M2 A4:A5 B4:B5 C4:C5 D4:E4 F4:F5 G4:G5 H4:H5 I4:K4 L4:L5
)
end
end
end
end
end

View File

@@ -0,0 +1,77 @@
module ExcelReports
module VideoReports
module DiscoveryProductionElementsLogs
class AppearanceSheet < ::ExcelReports::Worksheet
def title
"Appearance"
end
def fill_content(sheet)
sheet.add_row ["APPEARANCE LOG (for all episodes/programs)", "", "", "", "", ""]
sheet.add_row [instructions]
sheet.add_row
sheet.add_row ["EPISODE NUMBER(S) or LIST \"ALL\"", "EPISODE TITLE(S) or LIST \"ALL\"", "TIMECODE IN", "SOURCE TAPE / HARD DRIVE NUMBER", "NAME", "BRIEF VIDEO DESCRIPTION"]
data.each do |confirmation|
sheet.add_row [
confirmation.episode_number,
confirmation.episode_title,
confirmation.timecode_in,
confirmation.source_file_and_clip_names,
confirmation.name,
confirmation.description
]
end
sheet.add_row
sheet.add_row ["*On co-productions, releases & logs retained by Producer. Deliver upon request."]
end
def format(sheet)
sheet.column_widths *column_widths
cells_to_merge.each { |cell| sheet.merge_cells cell }
end
def style(sheet)
sheet.add_style "A1", Styles::BOLD, { sz: 14 }
sheet.add_style "A2", sz: 9
sheet.add_style "A4:F4", Styles::TABLE_HEADER
if data.any?
sheet.add_style "A#{data_start_index}:F#{data_end_index}", Styles::TABLE_DATA
end
end
private
def instructions
<<~INSTRUCTIONS
All licenses must conform to the DCI contractual requirements and must be fully executed.
Releases, licenses and agreements should only be logged once, based on the order of their appearance in the program.
If an image does not appear in the final Program, log that release after those in the final Program; indicate on the Log sheet NOT IN FINAL PROGRAM and list the camera tape only.
Please note that if it is not possible to deliver an English language agreement, an English language translation must accompany any agreement delivered in a foreign language (if applicable)."
INSTRUCTIONS
end
def data_start_index
5
end
def data_end_index
(data_start_index + data.size) - 1
end
def column_widths
[20, 25, 20, 20, 25, 30]
end
def cells_to_merge
%w[ A2:F2 ]
end
end
end
end
end

View File

@@ -0,0 +1,74 @@
module ExcelReports
module VideoReports
module DiscoveryProductionElementsLogs
class LocationSheet < ::ExcelReports::Worksheet
def title
"Location"
end
def fill_content(sheet)
sheet.add_row ["LOCATION LOG (for all episodes/programs)"]
sheet.add_row [instructions]
sheet.add_row
sheet.add_row table_headers
data.each do |confirmation|
sheet.add_row [
confirmation.episode_number,
confirmation.episode_title,
confirmation.timecode_in,
confirmation.source_file_and_clip_names,
[confirmation.name, confirmation.description].join(" ")
]
end
end
def format(sheet)
sheet.column_widths *column_widths
end
def style(sheet)
sheet.add_style "A1", Styles::BOLD, { sz: 14 }
sheet.add_style "A2", { sz: 9 }
sheet.add_style "A4:E4", Styles::TABLE_HEADER
sheet.add_style "A4:B4", Styles::WRAP_TEXT
sheet.add_style "D4", Styles::WRAP_TEXT
if data.any?
sheet.add_style "A#{data_start_index}:E#{data_end_index}", Styles::TABLE_DATA
end
end
private
def instructions
<<~INSTRUCTIONS
All licenses must conform to the DCI contractual requirements and must be fully executed.
Releases, licenses and agreements should only be logged once, based on the order of their appearance in the program.
If an image does not appear in the final Program, log that release after those in the final Program; indicate on the Log sheet "NOT IN FINAL PROGRAM" and list the camera tape only.
Please note that if it is not possible to deliver an English language agreement, an English language translation must accompany any agreement delivered in a foreign language (if applicable).
INSTRUCTIONS
end
def table_headers
["EPISODE NUMBER(S) or LIST \"ALL\"", "EPISODE TITLE(S) or LIST \"ALL\"", "TIMECODE IN", "SOURCE TAPE / HARD DRIVE NUMBER", "LOCATION NAME and BRIEF VIDEO DESCRIPTION"]
end
def column_widths
[20, 20, 15, 20, 40]
end
def data_start_index
5
end
def data_end_index
(data_start_index + data.size) - 1
end
end
end
end
end

View File

@@ -0,0 +1,61 @@
module ExcelReports
module VideoReports
module DiscoveryProductionElementsLogs
class MediaRightsCertificationSheet < ::ExcelReports::Worksheet
def title
"Media Rights Certification"
end
def fill_content(sheet)
sheet.add_row ["MEDIA RIGHTS CERTIFICATION (Required for final payment)", "", "", "", "", "", "", "", "", ""]
sheet.add_row ["By uploading this file I certify that:"]
sheet.add_row ["1) Check one"]
sheet.add_row ["", "Rights waivers have been submitted and approved by DCL for all Program elements with any restriction or limitation not permitted by the Programming Agreement (including, without limitation, all music, footage, photographic stills, graphics, talent, and releases)."]
sheet.add_row ["", "OR"]
sheet.add_row ["", "No rights waivers are required because all the Program elements are fully cleared or subject only to the restrictions or limitations permitted by the Programming Agreement."]
sheet.add_row ["AND"]
sheet.add_row ["2) Check one\n\nIn the event the Programming Agreement permits that third-party stills or stock footage can be licensed with certain term or media restrictions (e.g., 10 year license or no theatrical rights or US standard television rights), it is further certified that this Program (check one):"]
sheet.add_row ["", "Does contain third-party stills or stock footage secured subject to those restrictions."]
sheet.add_row ["", "OR"]
sheet.add_row ["", "Does not contain third-party stills or stock footage secured subject to those restrictions."]
sheet.add_row ["", "OR"]
sheet.add_row ["", "This Program does NOT contain any third-party stills or stock footage."]
sheet.add_row ["Submitted By _____________________________"]
sheet.add_row ["Uploading this document to the Producers Portal constitutes electronic signature"]
end
def format(sheet)
sheet.column_widths *column_widths
cells_to_merge.each { |cell| sheet.merge_cells cell }
end
def style(sheet)
sheet.add_style "A1", { sz: 18 }, Styles::BOLD, Styles::UNDERLINE
sheet.add_style "A2", { sz: 16 }, Styles::BOLD
sheet.add_style "A2", { sz: 12 }, Styles::BOLD
sheet.add_style "A7", { sz: 12 }, Styles::BOLD
sheet.add_style "A14", { sz: 12 }, Styles::BOLD
sheet.add_style "A15", { sz: 12 }, Styles::ITALIC, Styles::BOLD, Styles::COLOR_RED
sheet.add_style "B5", Styles::COLOR_BLUE
sheet.add_style "B10", Styles::COLOR_BLUE
sheet.add_style "B12", Styles::COLOR_BLUE
sheet.add_style "A8", Styles::WRAP_TEXT
sheet.add_style "B4", Styles::WRAP_TEXT
sheet.add_style "B6", Styles::WRAP_TEXT
end
private
def column_widths
[10, 20, 10, 10, 10, 10, 10, 10, 10, 10]
end
def cells_to_merge
%w[
A1:J1 A2:J2 A3:J3 B4:J4 B5:J5 B6:J6 A8:J8 B9:J9 B10:J10 B11:J11 B12:J12 B13:J13 A14:J14 A15:J15
]
end
end
end
end
end

View File

@@ -0,0 +1,67 @@
module ExcelReports
module VideoReports
module DiscoveryProductionElementsLogs
class MusicSheet < ::ExcelReports::Worksheet
def title
"Music"
end
def fill_content(sheet)
sheet.add_row ["MUSIC LOG (for all episodes/programs) for commissioned work-for-hire music only"]
sheet.add_row ["Not applicable if program contains 100% library"]
sheet.add_row ["Episode Title(s) or list \"All\"", "TRACK TITLE", "COMPOSER", "PUBLISHER"]
data.each do |confirmation_data|
sheet.add_row [
confirmation_data.episode_title,
confirmation_data.source_file_name,
composers(confirmation_data),
publishers(confirmation_data),
]
end
end
def format(sheet)
sheet.column_widths *column_widths
end
def style(sheet)
sheet.add_style "A1", Styles::BOLD, { sz: 14 }
sheet.add_style "A2", { sz: 14 }
sheet.add_style "A3:D3", Styles::TABLE_HEADER
sheet.add_style "A3", Styles::WRAP_TEXT
if has_data?
sheet.add_style "A#{data_start_index}:D#{data_end_index}", Styles::TABLE_DATA
end
end
private
def column_widths
[20, 20, 30, 60]
end
def has_data?
data.any?
end
def data_start_index
4
end
def data_end_index
(data_start_index + data.size) - 1
end
def composers(confirmation_data)
confirmation_data.confirmation.composer_info.split("|").join("\n")
end
def publishers(confirmation_data)
confirmation_data.confirmation.publisher_info.split("|").join("\n")
end
end
end
end
end

View File

@@ -0,0 +1,74 @@
module ExcelReports
module VideoReports
module DiscoveryProductionElementsLogs
class NameProductLogoSheet < ::ExcelReports::Worksheet
def title
"Name Product Logo"
end
def fill_content(sheet)
sheet.add_row ["NAME/PRODUCT/LOGO LOG (for all episodes/programs)"]
sheet.add_row [instructions]
sheet.add_row
sheet.add_row table_headers
data.each do |confirmation|
sheet.add_row [
confirmation.episode_number,
confirmation.episode_title,
confirmation.timecode_in,
confirmation.source_file_and_clip_names,
confirmation.name
]
end
end
def format(sheet)
sheet.column_widths *column_widths
end
def style(sheet)
sheet.add_style "A1", Styles::BOLD, { sz: 14 }
sheet.add_style "A2", { sz: 9 }
sheet.add_style "A4:E4", Styles::TABLE_HEADER
sheet.add_style "A4:B4", Styles::WRAP_TEXT
sheet.add_style "D4", Styles::WRAP_TEXT
if data.any?
sheet.add_style "A#{data_start_index}:E#{data_end_index}", Styles::TABLE_DATA
end
end
private
def instructions
<<~INSTRUCTIONS
All licenses must conform to the DCI contractual requirements and must be fully executed.
Releases, licenses and agreements should only be logged once, based on the order of their appearance in the program.
If an image does not appear in the final Program, log that release after those in the final Program; indicate on the Log sheet "NOT IN FINAL PROGRAM" and list the camera tape only.
Please note that if it is not possible to deliver an English language agreement, an English language translation must accompany any agreement delivered in a foreign language (if applicable).
INSTRUCTIONS
end
def table_headers
["EPISODE NUMBER(S) or LIST \"ALL\"", "EPISODE TITLE(S) or LIST \"ALL\"", "TIMECODE IN", "SOURCE TAPE / HARD DRIVE NUMBER", "PRODUCT/LOGO NAME"]
end
def column_widths
[15, 25, 15, 20, 40]
end
def data_start_index
5
end
def data_end_index
(data_start_index + data.size) - 1
end
end
end
end
end

View File

@@ -0,0 +1,56 @@
module ExcelReports
module VideoReports
module DiscoveryProductionElementsLogs
class ProductIntegrationSheet < ::ExcelReports::Worksheet
def title
"Product Integration"
end
def fill_content(sheet)
sheet.add_row ["Product Integration / Trade-Out Log"]
sheet.add_row [instructions]
sheet.add_row
sheet.add_row table_headers
sheet.add_row table_headers2
end
def format(sheet)
sheet.column_widths *column_widths
cells_to_merge.each { |cell| sheet.merge_cells cell }
end
def style(sheet)
sheet.add_style "A1", Styles::BOLD, { sz: 14 }
sheet.add_style "A2", Styles::BOLD, { sz: 12 }
sheet.add_style "A4:J4", Styles::TABLE_HEADER
sheet.add_style "A5:J5", Styles::TABLE_HEADER
sheet.add_style "A4:J4", Styles::WRAP_TEXT
sheet.add_style "A5:J5", Styles::WRAP_TEXT
sheet.add_style "I4:J4", Styles::BG_YELLOW
end
private
def instructions
"All contract agreements with companies providing trade-outs must be uploaded to the Producer's Portal"
end
def table_headers
["EPISODE #\nTitle\n(if applicable)", "PRODUCT ITEM(S)\n(ex: free airfare)", "COMPANY PROVIDING TRADE-OUT\n(ex: United Airlines)", "PRODUCT VALUE", "BUDGETED VALUE\nItem & Category in production budget", "OBLIGATIONS\n(ex: \"thanks to\" credit)", "AGREEMENT WITH COMPANY EXECUTION DATE", "DATE APPROVED BY DCI", "SELECT \"A\" OR \"B\"", ""]
end
def table_headers2
["", "", "", "", "", "", "", "", "A\nTrade will be savings to program List amount", "B\nTrade will be added value to program List amount"]
end
def column_widths
[15, 15, 15, 15, 15, 15, 15, 15, 15, 15]
end
def cells_to_merge
%w[ A4:A5 B4:B5 C4:C5 D4:D5 E4:E5 F4:F5 G4:G5 H4:H5 I4:J4 ]
end
end
end
end
end

View File

@@ -0,0 +1,49 @@
module ExcelReports
module VideoReports
module DiscoveryProductionElementsLogs
class TalentSheet < ::ExcelReports::Worksheet
def title
"Talent"
end
def fill_content(sheet)
sheet.add_row ["TALENT AGREEMENT LOG (for all episodes/programs)"]
sheet.add_row
sheet.add_row ["Episode Number(s) or list \"All\"", "Episode Title(s) or list \"All\"", "SERVICES", "NAME/COMPANY"]
data.each do |confirmation|
sheet.add_row [confirmation.episode_number, confirmation.episode_title, "", confirmation.name]
end
end
def format(sheet)
sheet.column_widths *column_widths
end
def style(sheet)
sheet.add_style "A1", Styles::BOLD, { sz: 14 }
sheet.add_style "A3:D3", Styles::TABLE_HEADER
sheet.add_style "A3", Styles::WRAP_TEXT
if data.any?
sheet.add_style "A#{data_start_index}:D#{data_end_index}", Styles::TABLE_DATA
end
end
private
def column_widths
[20, 20, 30, 60]
end
def data_start_index
4
end
def data_end_index
(data_start_index + data.size) - 1
end
end
end
end
end

View File

@@ -0,0 +1,61 @@
module ExcelReports
module VideoReports
class NatGeoLegalBinderLog
attr_reader :video
def initialize(video)
@video = video
end
def to_xls
build
package.to_stream.read
end
def filename
name = [video.file.filename.to_s, "legal-binder-log"].map(&:parameterize).join("_")
[name, "xlsx"].join(".")
end
private
def appearance_data
@appearance_data ||= data_for("AppearanceRelease")
end
def location_data
@location_data ||= data_for("LocationRelease")
end
def acquired_media_data
@acquired_media_data ||= data_for("AcquiredMediaRelease")
end
def data_for(release_type)
video.
video_release_confirmations.
where(releasable_type: release_type).
order(timecode_in: :asc).
map { |confirmation| ExcelReports::VideoReports::ReleasableDataAdapter.new(confirmation) }
end
def build
NatGeoLegalBinderLogs::LegalBinderChecklistSheet.build(workbook, [], video)
NatGeoLegalBinderLogs::AppearanceReleaseLogSheet.build(workbook, appearance_data)
NatGeoLegalBinderLogs::LocationReleaseLogSheet.build(workbook, location_data)
NatGeoLegalBinderLogs::AcquiredFootageLogSheet.build(workbook, acquired_media_data)
NatGeoLegalBinderLogs::ThirdPartyContractLogSheet.build(workbook, [])
NatGeoLegalBinderLogs::ProductionPersonnelLogSheet.build(workbook, [])
end
def workbook
@workbook ||= package.workbook
end
def package
@package ||= Axlsx::Package.new
end
end
end
end

View File

@@ -0,0 +1,78 @@
module ExcelReports
module VideoReports
module NatGeoLegalBinderLogs
class AcquiredFootageLogSheet < Worksheet
def title
"Acquired Footage-Stills Log"
end
def fill_content(sheet)
sheet.add_row
sheet.add_row
sheet.add_row(["", "", "", "", "", "RIGHTS OBTAINED", "", "", ""])
sheet.add_row(table_headers)
data.each_with_index do |confirmation, index|
sheet.add_row [
index + 1,
confirmation.description,
confirmation.timecode_in,
confirmation.timecode_out,
[confirmation.name, confirmation.contact_address].join("\n"),
confirmation.applicable_media,
confirmation.territory,
confirmation.term,
confirmation.restrictions,
]
end
end
def format(sheet)
sheet.column_widths *column_widths
cells_to_merge.each { |cell| sheet.merge_cells cell }
end
def style(sheet)
sheet.add_style "F3", Styles::BOLD, Styles::FULLY_CENTERED, Styles::BG_GRAY, Styles::THIN_BLACK_BORDER
sheet.add_style "A4:I4", Styles::BOLD, Styles::FULLY_CENTERED, Styles::THIN_BLACK_BORDER, Styles::WRAP_TEXT
if data.any?
sheet.add_style "A#{data_start_index}:I#{data_end_index}", Styles::THIN_BLACK_BORDER
end
end
private
def table_headers
[
"Contract Number",
"Brief description of footage or stills",
"Program Master Timecode IN",
"Program Master Timecode OUT",
"Vendor Name and Contact Information",
"Media",
"Territory",
"Term",
"Restrictions",
]
end
def data_start_index
5
end
def data_end_index
(data_start_index + data.size) - 1
end
def column_widths
[12, 25, 22, 22, 28, 18, 22, 22, 25]
end
def cells_to_merge
%w(F3:I3)
end
end
end
end
end

View File

@@ -0,0 +1,82 @@
module ExcelReports
module VideoReports
module NatGeoLegalBinderLogs
class AppearanceReleaseLogSheet < Worksheet
def title
"Appearance Release Log"
end
def fill_content(sheet)
instructions1 = "PLEASE ARRANGE CONTRACTS IN ORDER OF FIRST APPEARANCE IN FINAL PROGRAM."
instructions2 = "PLEASE INCLUDE ALL CONTRACTS SECURED FOR THE PROGRAM INCLUDING THOSE FOR ALL DIGITAL SOCIAL CONTENT."
instructions3 = "IF INDIVIDUAL DID NOT APPEAR IN THE FINAL SHOW, PLEASE INDICATE \"NOT IN FINAL PROGRAM\" UNDER TIME CODE COLUMN & LIST ALPHABETICALLY."
sheet.add_row([instructions1])
sheet.add_row([instructions2])
sheet.add_row([instructions3])
sheet.add_row
sheet.add_row(["", "", "", "RIGHTS OBTAINED", "", "", "", "", ""])
sheet.add_row(table_headers)
data.each_with_index do |confirmation, index|
sheet.add_row [
index + 1,
confirmation.timecode_in,
[confirmation.name, confirmation.contact_address].join("\n"),
confirmation.applicable_media,
confirmation.territory,
confirmation.term,
"",
confirmation.restrictions,
]
end
end
def format(sheet)
sheet.column_widths *column_widths
cells_to_merge.each { |cell| sheet.merge_cells cell }
end
def style(sheet)
sheet.add_style "D5", Styles::BOLD, Styles::FULLY_CENTERED, Styles::BG_GRAY, Styles::THIN_BLACK_BORDER
sheet.add_style "A6:H6", Styles::BOLD, Styles::FULLY_CENTERED, Styles::THIN_BLACK_BORDER, Styles::WRAP_TEXT
if data.any?
sheet.add_style "A#{data_start_index}:H#{data_end_index}", Styles::THIN_BLACK_BORDER
end
end
private
def table_headers
[
"Contract Number",
"Program Master Timecode",
"Person's Name and Contact Information",
"Media",
"Territory",
"Term",
"Program Specific Y/N",
"Restrictions"
]
end
def data_start_index
7
end
def data_end_index
(data_start_index + data.size) - 1
end
def column_widths
[12, 25, 30, 30, 30, 30, 15, 30]
end
def cells_to_merge
%w(A1:F1 A2:F2 A3:F3 D5:H5)
end
end
end
end
end

View File

@@ -0,0 +1,50 @@
module ExcelReports
module VideoReports
module NatGeoLegalBinderLogs
class LegalBinderChecklistSheet < Worksheet
def title
"Legal Binder Checklist"
end
def fill_content(sheet)
sheet.add_row ["SERIES TITLE:", ""]
sheet.add_row ["EPISODE NUMBER:", header_data.number]
sheet.add_row ["EPISODE TITLE:", header_data.name]
sheet.add_row
sheet.add_row
sheet.add_row ["", "INCLUDED", "PENDING", "N/A", "COMMENTS"]
sheet.add_row ["COPY OF CREDIT LIST", "", "", "", ""]
sheet.add_row ["COPY OF MUSIC CUE SHEET", "", "", "", ""]
sheet.add_row ["COPY OF E&O CERTIFICATE", "", "", "", ""]
sheet.add_row ["APPEARANCE RELEASES + LOG", "", "", "", ""]
sheet.add_row ["LOCATION RELEASES/PERMITS + LOG", "", "", "", ""]
sheet.add_row ["ACQUIRED FOOTAGE/STILLS CONTRACTS + LOG", "", "", "", ""]
sheet.add_row ["THIRD PARTY CONTRACTS (GRAPHICS, COMPOSER, MUSIC LIBRARY, NARRATOR, TALENT) + LOG", "", "", "", ""]
sheet.add_row ["PRODUCTION PERSONNEL LOG", "", "", "", ""]
end
def format(sheet)
sheet.column_widths *column_widths
cells_to_merge.each { |cell| sheet.merge_cells cell }
end
def style(sheet)
sheet.add_style "A1:A3", Styles::BOLD, { sz: 14 }
sheet.add_style "B6:E6", Styles::BOLD, Styles::THIN_BLACK_BORDER, Styles::FULLY_CENTERED, { sz: 10 }
sheet.add_style "A7:A14", Styles::BOLD, Styles::THIN_BLACK_BORDER, Styles::WRAP_TEXT, { sz: 10 }
sheet.add_style "B7:E14", Styles::THIN_BLACK_BORDER
end
private
def column_widths
[30, 12, 12, 10, 25]
end
def cells_to_merge
%w(B1:D1 B2:D2 B3:D3)
end
end
end
end
end

View File

@@ -0,0 +1,84 @@
module ExcelReports
module VideoReports
module NatGeoLegalBinderLogs
class LocationReleaseLogSheet < Worksheet
def title
"Location Release Log"
end
def fill_content(sheet)
instructions1 = "PLEASE ARRANGE CONTRACTS IN ORDER OF FIRST APPEARANCE IN FINAL PROGRAM."
instructions2 = "PLEASE INCLUDE ALL CONTRACTS SECURED FOR THE PROGRAM INCLUDING THOSE FOR ALL DIGITAL SOCIAL CONTENT."
instructions3 = "IF INDIVIDUAL DID NOT APPEAR IN THE FINAL SHOW, PLEASE INDICATE \"NOT IN FINAL PROGRAM\" UNDER TIME CODE COLUMN & LIST ALPHABETICALLY."
sheet.add_row([instructions1])
sheet.add_row([instructions2])
sheet.add_row([instructions3])
sheet.add_row
sheet.add_row(["", "", "", "RIGHTS OBTAINED", "", "", "", "", ""])
sheet.add_row(table_headers)
data.each_with_index do |confirmation, index|
sheet.add_row [
index + 1,
confirmation.timecode_in,
[confirmation.name, confirmation.confirmation.releasable.address].join("\n"),
"",
confirmation.applicable_media,
confirmation.territory,
confirmation.term,
"",
confirmation.restrictions,
]
end
end
def format(sheet)
sheet.column_widths *column_widths
cells_to_merge.each { |cell| sheet.merge_cells cell }
end
def style(sheet)
sheet.add_style "D5", Styles::BOLD, Styles::FULLY_CENTERED, Styles::BG_GRAY, Styles::THIN_BLACK_BORDER
sheet.add_style "A6:I6", Styles::BOLD, Styles::FULLY_CENTERED, Styles::THIN_BLACK_BORDER, Styles::WRAP_TEXT
if data.any?
sheet.add_style "A#{data_start_index}:I#{data_end_index}", Styles::THIN_BLACK_BORDER
end
end
private
def table_headers
[
"Contract Number",
"Program Master Timecode",
"Location's Name and Contact Information",
"Permit and/or Location Cost",
"Media",
"Territory",
"Term",
"Program Specific Y/N",
"Restrictions"
]
end
def data_start_index
7
end
def data_end_index
(data_start_index + data.size) - 1
end
def column_widths
[12, 25, 35, 35, 30, 30, 30, 15, 30]
end
def cells_to_merge
%w(A1:F1 A2:F2 A3:F3 D5:I5)
end
end
end
end
end

View File

@@ -0,0 +1,50 @@
module ExcelReports
module VideoReports
module NatGeoLegalBinderLogs
class ProductionPersonnelLogSheet < Worksheet
def title
"Production Personnel Log"
end
def fill_content(sheet)
sheet.add_row
sheet.add_row
sheet.add_row(table_headers)
sheet.add_row(["Producer", "", "", ""])
sheet.add_row(["Director", "", "", ""])
sheet.add_row(["Writer", "", "", ""])
sheet.add_row(["Editor", "", "", ""])
sheet.add_row(["Assistant Producer", "", "", ""])
sheet.add_row(["Production Manager", "", "", ""])
sheet.add_row(["Camera", "", "", ""])
sheet.add_row(["Audio", "", "", ""])
sheet.add_row(["Local Coordinator/Location Manager", "", "", ""])
end
def format(sheet)
sheet.column_widths *column_widths
end
def style(sheet)
sheet.add_style "A3:D3", Styles::BOLD, Styles::FULLY_CENTERED, Styles::THIN_BLACK_BORDER
sheet.add_style "A4:D12", Styles::FULLY_CENTERED, Styles::THIN_BLACK_BORDER, Styles::WRAP_TEXT
end
private
def table_headers
[
"Title",
"Name",
"Company",
"Contact Info (Address/Email/Phone)",
]
end
def column_widths
[30, 30, 35, 35]
end
end
end
end
end

View File

@@ -0,0 +1,61 @@
module ExcelReports
module VideoReports
module NatGeoLegalBinderLogs
class ThirdPartyContractLogSheet < Worksheet
def title
"Third Party Contract Log"
end
def fill_content(sheet)
sheet.add_row
sheet.add_row
sheet.add_row(["", "", "RIGHTS OBTAINED", "", "", "", ""])
sheet.add_row(table_headers)
sheet.add_row(["Graphics", "", "", "", "", "", ""])
sheet.add_row(["Composer", "", "", "", "", "", ""])
sheet.add_row(["Music Library", "", "", "", "", "", ""])
sheet.add_row(["Narrator", "", "", "", "", "", ""])
sheet.add_row(["Talent", "", "", "", "", "", ""])
sheet.add_row(["Equipment and/or Vehicle Rental (only applicable if equipment/vehicle appears on camera and contract contains rights language)", "", "", "", "", "", ""])
sheet.add_row(["Production Facility (only applicable if contract contains rights language)", "", "", "", "", "", ""])
sheet.add_row(["Post Production Facility (only applicable if contract contains rights language)", "", "", "", "", "", ""])
sheet.add_row(["Other Facilities (only applicable if contract contains rights language)", "", "", "", "", "", ""])
end
def format(sheet)
sheet.column_widths *column_widths
cells_to_merge.each { |cell| sheet.merge_cells cell }
end
def style(sheet)
sheet.add_style "C3", Styles::BOLD, Styles::FULLY_CENTERED, Styles::BG_GRAY, Styles::THIN_BLACK_BORDER
sheet.add_style "A4:G4", Styles::BOLD, Styles::FULLY_CENTERED, Styles::THIN_BLACK_BORDER
sheet.add_style "A5:A13", Styles::FULLY_CENTERED, Styles::THIN_BLACK_BORDER, Styles::WRAP_TEXT
sheet.add_style "B5:G13", Styles::FULLY_CENTERED, Styles::THIN_BLACK_BORDER
end
private
def table_headers
[
"Brief Description of Services",
"3rd Party Name and Contact Information",
"Media",
"Territory",
"Term",
"Program Specific Y/N",
"Restrictions",
]
end
def column_widths
[25, 30, 20, 20, 20, 15, 20]
end
def cells_to_merge
%w(C3:G3)
end
end
end
end
end

View File

@@ -0,0 +1,73 @@
module ExcelReports
module VideoReports
class ReleasableDataAdapter
attr_reader :confirmation
delegate :video, to: :confirmation
def initialize(release_confirmation)
@confirmation = release_confirmation
end
def description
confirmation.description || confirmation.video.project.description
end
def episode_number
video.number
end
def episode_title
video.name
end
def name
confirmation.releasable.name
end
def source_file_name
confirmation.source_file_name
end
def clip_name
confirmation.clip_name
end
def source_file_and_clip_names
[source_file_name, clip_name].reject(&:blank?).join(" - ")
end
def timecode_in
confirmation.timecode_in
end
def timecode_out
confirmation.timecode_out
end
def duration
confirmation.duration
end
def contact_address
confirmation.releasable.contact_person.address.to_s
end
def applicable_media
confirmation.releasable.applicable_medium_value
end
def territory
confirmation.releasable.territory_value
end
def term
confirmation.releasable.term_value
end
def restrictions
confirmation.releasable.restriction_value
end
end
end
end

View File

@@ -0,0 +1,53 @@
module ExcelReports
class Worksheet
attr_accessor :workbook, :data, :header_data
def self.build(workbook, data = nil, header_data = nil)
new(workbook, data, header_data).tap do |worksheet|
worksheet.build
end
end
def initialize(workbook, data = nil, header_data = nil)
@workbook = workbook
@data = data
@header_data = header_data
end
def build
workbook.add_worksheet(name: title) do |sheet|
fill_content(sheet)
format(sheet)
style(sheet)
end
end
module Styles
def self.merge_all(*styles)
styles.each_with_object({}) { |style, combined| combined.deep_merge!(style) }
end
BOLD = { b: true }
ITALIC = { i: true }
UNDERLINE = { u: true }
BG_GRAY = { bg_color: "C0C0C0" }
BG_LIGHT_BLUE = { bg_color: "97CBFC" }
BG_YELLOW = { bg_color: "FFFF9E" }
COLOR_WHITE = { fg_color: "FFFFFF" }
COLOR_BLUE = { fg_color: "0000FF" }
COLOR_RED = { fg_color: "FF0000" }
THIN_BLACK_BORDER = { border: { style: :thin, color: "000000" } }
THICK_BLACK_BORDER = { border: { style: :thick, color: "000000" } }
VERTICAL_CENTER = { alignment: { vertical: :center } }
HORIZONTAL_CENTER = { alignment: { horizontal: :center } }
HORIZONTAL_LEFT = { alignment: { horizontal: :left } }
HORIZONTAL_RIGHT = { alignment: { horizontal: :right } }
FULLY_CENTERED = merge_all(HORIZONTAL_CENTER, VERTICAL_CENTER)
WRAP_TEXT = { alignment: { wrap_text: true } }
TABLE_HEADER = merge_all(Styles::BG_LIGHT_BLUE, Styles::BOLD, Styles::THICK_BLACK_BORDER, Styles::FULLY_CENTERED, Styles::WRAP_TEXT, { sz: 8 })
TABLE_DATA = merge_all(Styles::THICK_BLACK_BORDER, Styles::WRAP_TEXT, { sz: 10 })
PROGRAM_HEADER = merge_all(Styles::BG_LIGHT_BLUE, Styles::BOLD, Styles::THICK_BLACK_BORDER, Styles::HORIZONTAL_CENTER, Styles::WRAP_TEXT, { sz: 12 })
HEADER_DATA = merge_all(Styles::THICK_BLACK_BORDER, Styles::WRAP_TEXT, Styles::HORIZONTAL_LEFT, { sz: 10 })
end
end
end

12
app/models/file_info.rb Normal file
View File

@@ -0,0 +1,12 @@
class FileInfo < ApplicationRecord
belongs_to :releasable, polymorphic: true
scope :audio, -> { where("content_type ILIKE ?", "%audio%") }
scope :video, -> { where("content_type ILIKE ?", "%video%") }
scope :photo, -> { where("content_type ILIKE ?", "%image%") }
scope :other, -> { where("NOT content_type ILIKE ?", "%(image|video|audio)%") }
def self.search_filename(query)
where("filename ILIKE ?", "%#{query}%")
end
end

View File

@@ -0,0 +1,28 @@
class FilesForRequest
def initialize(video)
@video = video
end
def start_timecode_offset
end
def file_object_name
video.file.key if video.file.attached?
end
def edl_file_object_name
video.edl_file.key if video.edl_file.attached?
end
def aws_bucket_name
ENV["AWS_BUCKET"]
end
def job_id
video.analysis_uid
end
private
attr_reader :video
end

21
app/models/filter_set.rb Normal file
View File

@@ -0,0 +1,21 @@
class FilterSet
def initialize(filters, current: nil, default: nil)
@filters = filters
@current = current
@default = default
end
def active?(filter)
current_filter == filter
end
def current_filter
@current || @default
end
def name_and_values(include_default: true)
@filters.map { |filter| [filter.titleize, filter] }.tap do |result|
result.prepend [@default.titleize, @default] if include_default
end
end
end

View File

@@ -0,0 +1,11 @@
class GraphicsElement < ApplicationRecord
belongs_to :video
validates :graphic_type, presence: true
validates :text, presence: true
validates :time_elapsed, presence: true
def appears_at
Timecode.from_seconds(time_elapsed.to_f).to_s
end
end

View File

@@ -0,0 +1,28 @@
class GraphicsFilesForRequest
attr_reader :start_timecode_offset
def initialize(video, start_timecode_offset)
@video = video
@start_timecode_offset = start_timecode_offset
end
def file_object_name
video.file.key if video.file.attached?
end
def edl_file_object_name
video.graphics_only_edl_file.key if video.graphics_only_edl_file.attached?
end
def aws_bucket_name
ENV["AWS_BUCKET"]
end
def job_id
video.analysis_uid
end
private
attr_reader :video
end

View File

@@ -0,0 +1,44 @@
# Represents a collection of releases with photos
class HeadshotCollection
attr_reader :collection_uid, :releasables
def self.for_project(project)
appearance_releases_with_photo = project.appearance_releases.with_person_photo
new(project.headshot_collection_uid, appearance_releases_with_photo + project.talent_releases)
end
def initialize(collection_uid, releasables)
@collection_uid = collection_uid
@releasables = releasables
end
# Use the custom hash to generate JSON format
def as_json(*)
to_hash
end
def to_hash
{
collection_uid: collection_uid.to_s,
bucket_name: aws_bucket_name,
ids_to_images: map_ids_to_images,
}.reject { |_, v| v.blank? }
end
private
def aws_bucket_name
ENV["AWS_BUCKET"]
end
def map_ids_to_images
releasables.each_with_object({}) do |release, hash|
hash[release_id(release)] = [release.photo.key] # An array of images is expected, even if there's only one image
end
end
def release_id(release)
[release.model_name.param_key, release.id].join("_")
end
end

10
app/models/import.rb Normal file
View File

@@ -0,0 +1,10 @@
class Import < ApplicationRecord
belongs_to :project
belongs_to :releasable, polymorphic: true, optional: true
enum status: %i[ pending started finished failed ]
has_one_attached :file
validates :file, content_type: ["application/pdf"]
end

View File

@@ -0,0 +1,70 @@
class LocationRelease < ApplicationRecord
include Confirmable
include Contractable
include Exploitable
include Notable
include Photoable
include Releasable
include Searchable
include Signable
include Syncable
include Taggable
include PersonName
composed_of :address,
mapping: [
%w(address_street1 street1),
%w(address_street2 street2),
%w(address_city city),
%w(address_state state),
%w(address_zip zip),
%w(address_country country)
]
composed_of :person_address,
class_name: "Address",
mapping: [
%w(person_address_street1 street1),
%w(person_address_street2 street2),
%w(person_address_city city),
%w(person_address_state state),
%w(person_address_zip zip),
%w(person_address_country country)
]
validates :name, presence: true
validates :person_email, email: true, allow_blank: true
validate :end_date_after_start_date
with_options on: :native do
validates :person_first_name, :person_last_name, presence: true
validates :signature, attached: true
end
searchable_on %i[
name
address_street1 address_street2 address_city address_state address_zip address_country
person_address_street1 person_address_street2 person_address_city person_address_state person_address_zip person_address_country
]
def contact_person
@contact_person ||= Contact.new(person_name, person_address, person_email, person_phone)
end
def minor?
false
end
def uses_edl?
true
end
private
def end_date_after_start_date
return true if filming_ended_on.blank? || filming_started_on.blank?
if filming_ended_on < filming_started_on
errors.add(:filming_ended_on, "must be after the filming started on date")
end
end
end

6
app/models/main_photo.rb Normal file
View File

@@ -0,0 +1,6 @@
# A class that allows a single item from a collection of attachments to function as a single attachment
class MainPhoto < SimpleDelegator
def attached?
__getobj__.present?
end
end

View File

@@ -0,0 +1,49 @@
class MaterialRelease < ApplicationRecord
include Confirmable
include Contractable
include Exploitable
include Notable
include Photoable
include Releasable
include Searchable
include Signable
include Syncable
include Taggable
include PersonName
composed_of :person_address,
class_name: "Address",
mapping: [
%w(person_address_street1 street1),
%w(person_address_street2 street2),
%w(person_address_city city),
%w(person_address_state state),
%w(person_address_zip zip),
%w(person_address_country country)
]
validates :name, presence: true
validates :person_email, email: true, allow_blank: true
with_options on: :native do
validates :person_first_name, :person_last_name, presence: true
validates :signature, attached: true
end
searchable_on %i[
name
person_address_street1 person_address_street2 person_address_city person_address_state person_address_zip person_address_country
]
def contact_person
@contact_person ||= Contact.new(person_name, person_address, person_email, person_phone)
end
def minor?
false
end
def uses_edl?
true
end
end

View File

@@ -0,0 +1,88 @@
class MusicRelease < ApplicationRecord
include Confirmable
include Contractable
include Exploitable
include Notable
include Releasable
include Searchable
include Taggable
include PersonName
has_many :file_infos, as: :releasable, dependent: :destroy
has_many :composers, dependent: :destroy
has_many :publishers, dependent: :destroy
accepts_nested_attributes_for :file_infos
accepts_nested_attributes_for :composers, reject_if: :all_blank
accepts_nested_attributes_for :publishers, reject_if: :all_blank
composed_of :person_address,
class_name: "Address",
mapping: [
%w(person_address_street1 street1),
%w(person_address_street2 street2),
%w(person_address_city city),
%w(person_address_state state),
%w(person_address_zip zip),
%w(person_address_country country)
]
validates :name, presence: true
validates :person_email, email: true, allow_blank: true
validates :composers, :length => { :minimum => 1, :message => "at least 1 required" }
validates :publishers, :length => { :minimum => 1, :message => "at least 1 required" }
validate :publisher_percentages_add_up_to_100
validate :composer_percentages_add_up_to_100
searchable_on %i[
name
person_address_street1 person_address_street2 person_address_city person_address_state person_address_zip person_address_country
]
def contact_person
@contact_person ||= Contact.new(person_name, person_address, person_email, person_phone)
end
def uses_edl?
true
end
def composer_info
composers.map do |composer|
[
composer.name,
composer.affiliation,
composer.percentage,
("$cae:#{composer.cae_number}" if composer.cae_number.present?),
].compact.join(", ")
end.join("|")
end
def publisher_info
publishers.map do |publisher|
[
publisher.name,
publisher.affiliation,
publisher.percentage,
].join(", ")
end.join("|")
end
def native?
false
end
private
def publisher_percentages_add_up_to_100
if publishers.size > 0 && publishers.sum(&:percentage).to_f != 100.0
errors.add(:base, "Publisher percentages must add up to 100%")
end
end
def composer_percentages_add_up_to_100
if composers.size > 0 && composers.sum(&:percentage).to_f != 100.0
errors.add(:base, "Composer percentages must add up to 100%")
end
end
end

View File

@@ -0,0 +1,63 @@
class MuxLiveStream
def id
live_stream.data.id
end
def key
live_stream.data.stream_key
end
def playback_id
playback.data.id
end
def destroy_stream(stream_uid)
client.delete_live_stream(stream_uid)
end
private
def live_stream
@live_stream ||= create_live_stream
end
def playback
@playback ||= create_playback
end
# Source: https://github.com/muxinc/mux-ruby/blob/1.4.0/examples/video/exercise-live-streams.rb
def create_live_stream
create_asset_request = MuxRuby::CreateAssetRequest.new
create_asset_request.playback_policy = [MuxRuby::PlaybackPolicy::PUBLIC]
create_asset_request.mp4_support = "standard"
create_live_stream_request = MuxRuby::CreateLiveStreamRequest.new
create_live_stream_request.new_asset_settings = create_asset_request
create_live_stream_request.playback_policy = [MuxRuby::PlaybackPolicy::PUBLIC]
create_live_stream_request.reduced_latency = reduced_latency_enabled?
create_live_stream_request.test = test_mode_enabled?
client.create_live_stream(create_live_stream_request)
end
def create_playback
create_playback_id_request = MuxRuby::CreatePlaybackIDRequest.new
# TODO: Use signed policy in the future
# create_playback_id_request.policy = MuxRuby::PlaybackPolicy::SIGNED
create_playback_id_request.policy = MuxRuby::PlaybackPolicy::PUBLIC
client.create_live_stream_playback_id(live_stream.data.id, create_playback_id_request)
end
def reduced_latency_enabled?
ENV["MUX_REDUCED_LATENCY_ENABLED"].present?
end
def test_mode_enabled?
!ENV["MUX_TEST_MODE_DISABLED"].present?
end
def client
@client ||= MuxRuby::LiveStreamsApi.new
end
end

7
app/models/note.rb Normal file
View File

@@ -0,0 +1,7 @@
class Note < ApplicationRecord
include Syncable
belongs_to :user, optional: true
belongs_to :notable, polymorphic: true
validates :content, presence: true
end

View File

@@ -0,0 +1,13 @@
class NullProject
def account
Account.new(plan_uid: "me_suite")
end
def name
"Projects"
end
def persisted?
false
end
end

View File

@@ -0,0 +1,121 @@
class PendingAnalysis
def self.poll
PendingVideoAnalysis.poll
PendingAudioAnalysis.poll
end
def self.expire(age_threshold)
PendingAudioAnalysis.expire(age_threshold)
PendingVideoAnalysis.expire(age_threshold)
end
class PendingVideoAnalysis
def initialize(analysis_uid)
@analysis_uid = analysis_uid
end
def self.poll
Video.
analysis_pending.
pluck(:analysis_uid).
each { |uid| new(uid).poll }
end
def self.expire(age)
Video.
analysis_pending.
video_analysis_started_before(age).
pluck(:analysis_uid).
each { |video| new(video).expire }
end
def poll
if results_available?
notification.success!
end
end
def expire
if results_available?
notification.success!
else
notification.failure!
end
end
private
attr_reader :analysis_uid
def notification
@notification ||= AnalysisNotification.build(:video, analysis_uid)
end
def results_available?
facial_recognition = BrayniacAI::FacialRecognition.find(analysis_uid)
facial_recognition.respond_to?(:results)
rescue ActiveResource::ResourceNotFound => e
false
rescue ActiveResource::ServerError => e
message = "-- Video Analysis polling for #{analysis_uid} failed due to: #{e}"
puts message
Rails.logger.error(message)
false
end
end
class PendingAudioAnalysis
def self.poll
Video.
audio_analysis_pending.
pluck(:audio_analysis_uid).
each { |uid| new(uid).poll }
end
def self.expire(age)
Video.
audio_analysis_pending.
audio_analysis_started_before(age).
pluck(:audio_analysis_uid).
each { |uid| new(uid).expire }
end
def initialize(analysis_uid)
@analysis_uid = analysis_uid
end
def poll
if results_available?
notification.success!
end
end
def expire
if results_available?
notification.success!
else
notification.failure!
end
end
private
attr_reader :analysis_uid
def notification
@notification ||= AnalysisNotification.build(:audio, analysis_uid)
end
def results_available?
audio_recognition = BrayniacAI::AudioRecognition.find(analysis_uid)
audio_recognition.respond_to?(:results)
rescue ActiveResource::ResourceNotFound => e
false
rescue ActiveResource::ServerError => e
message = "-- Audio Analysis polling for #{analysis_uid} failed due to: #{e}"
puts message
Rails.logger.error(message)
false
end
end
end

126
app/models/project.rb Normal file
View File

@@ -0,0 +1,126 @@
class Project < ApplicationRecord
include Archivable
include Filterable
include Syncable
SIGNABLE_RELEASE_TYPES = %w(talent appearance acquired_media location material)
AVAILABLE_RELEASE_TYPES = %w(appearance location material acquired_media talent music)
belongs_to :account
has_many :acquired_media_releases, dependent: :destroy
has_many :appearance_releases, dependent: :destroy
has_many :location_releases, dependent: :destroy
has_many :material_releases, dependent: :destroy
has_many :music_releases, dependent: :destroy
has_many :talent_releases, dependent: :destroy
has_many :videos, dependent: :destroy
has_many :imports, dependent: :destroy
has_many :contract_templates, dependent: :destroy
has_many :project_memberships, dependent: :destroy
has_many :users, through: :project_memberships
has_many :directories, dependent: :destroy
has_many :downloads, dependent: :destroy
has_many :broadcasts, dependent: :destroy
has_many :zoom_meetings, dependent: :destroy
accepts_nested_attributes_for :project_memberships
has_settings do |s|
s.key :features, defaults: {
acquired_media_release: false,
appearance_release: true,
location_release: false,
material_release: false,
music_release: false,
talent_release: false,
video_analysis: false,
}
end
validates :name, presence: true, uniqueness: { scope: :account_id }
filterable_by :active, :inactive, :archived
scope :order_by_name, -> { order(name: :asc) }
def all_releases_count
AVAILABLE_RELEASE_TYPES.sum { |name| public_send("#{name}_releases").count }
end
def members
users + account.managers
end
attr_reader :predefined_client_name
def predefined_client_name=(value)
@predefined_client_name = value
case value.to_s
when "discovery"
self.client_name = value.titleize
self.settings(:features).attributes = {
acquired_media_release: true,
appearance_release: true,
location_release: true,
material_release: true,
music_release: true,
talent_release: true,
video_analysis: true,
}
when "nat_geo"
self.client_name = value.titleize
self.settings(:features).attributes = {
acquired_media_release: true,
appearance_release: true,
location_release: true,
material_release: true,
music_release: true,
talent_release: true,
video_analysis: true,
}
else
# Do nothing - the features will be set manually
end
end
def features_settings=(values)
return if self.predefined_client_name.to_s != "other"
settings(:features).attributes = values.transform_values { |v| !v.to_i.zero? }
end
def feature_enabled?(name)
settings(:features).send(name)
end
def import_contract_templates(template_ids)
ContractTemplate.where(id: template_ids).each do |contract_template|
contract_templates << contract_template.dup.tap do |imported_contract_template|
imported_contract_template.parent = contract_template
# Rich text fields must be manually duplicated because they are associations
if contract_template.body.present?
imported_contract_template.body.body = contract_template.body.body.dup
end
if contract_template.guardian_clause.present?
imported_contract_template.guardian_clause.body = contract_template.guardian_clause.body.dup
end
end
end
save
end
def zoom_meeting_url
zoom_meeting.meeting_url
end
def zoom_meeting
current_zoom_meeting = zoom_meetings.active.first
unless current_zoom_meeting.present?
zoom_user = ZoomUser.free.first || ZoomUser.create
current_zoom_meeting = ZoomMeeting.create(zoom_user: zoom_user, project: self)
end
current_zoom_meeting
end
end

View File

@@ -0,0 +1,47 @@
class ProjectMembership < ApplicationRecord
belongs_to :project
belongs_to :user
validates :user_id, uniqueness: { scope: :project_id, message: "already belongs to this Project" }
validate :account_manager_is_prohibited
scope :with_account_auth, -> (account) { joins(user: :account_auths).where(account_auths: { account_id: account }) }
scope :order_by_user_role_and_user_email, -> (account) { with_account_auth(account).order("account_auths.role DESC, users.email") }
attr_accessor :user_email
def new_user?
@user_is_new
end
def save_and_update_account_membership
ApplicationRecord.transaction do
if User.exists?(email: user_email)
@user_is_new = false
self.user = User.find_by(email: user_email)
else
@user_is_new = true
self.user = Oath::Services::SignUp.new(email: user_email, password: SecureRandom.hex).perform
end
account_auth = user.account_auths.find_or_initialize_by(account: project.account)
if !(user.save && account_auth.save && save)
raise ActiveRecord::Rollback
return false
end
return true
end
end
private
def account_manager_is_prohibited
return if project.nil?
if user.account_manager?(project.account)
errors.add(:user, "is already an account manager")
end
end
end

4
app/models/publisher.rb Normal file
View File

@@ -0,0 +1,4 @@
class Publisher < ApplicationRecord
belongs_to :music_release
validates :name, :affiliation, :percentage, presence: true
end

Some files were not shown because too many files have changed in this diff Show More