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

1
.browserslistrc Normal file
View File

@@ -0,0 +1 @@
defaults

29
.env.sample Normal file
View File

@@ -0,0 +1,29 @@
DOMAIN=
WEB_PORT=
AWS_ACCESS_KEY_ID=
AWS_BUCKET=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
BRAYNIAC_AI_API_ENDPOINT=https://z99cprg2eg.execute-api.us-east-1.amazonaws.com/dev/v0.0.1
SOURCE_AUDIO_API_ENDPOINT=https://bigmedia.sourceaudio.com/api
SOURCE_AUDIO_TOKEN=
# Optional
REDIS_URL=
# Required for Zoom.us integration
ZOOM_API_KEY=
ZOOM_API_SECRET=
ZOOM_PRO_USERS_LIMIT= # defaults to 3
ZOOM_USER_TYPE= # 'pro' / 'basic'
ZOOM_ENABLE_RECORDINGS=0 # 0 / 1
# Token for webhooks authorization
ZOOM_VERIFICATION_TOKEN=
# Required for creating live streams with Mux
MUX_TOKEN_ID=
MUX_TOKEN_SECRET=
MUX_BROADCAST_SERVER_URL=rtmp://global-live.mux.com:5222/app
MUX_TEST_MODE_DISABLED=

35
.gitignore vendored Normal file
View File

@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
# Ignore bundler config.
/.bundle
# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep
# Ignore uploaded files in development
/storage/*
/node_modules
/yarn-error.log
/public/assets
.byebug_history
# Ignore master key for decrypting credentials and more.
/config/master.key
.env
.idea/*
/public/packs
/public/packs-test
/node_modules
/yarn-error.log
yarn-debug.log*
.yarn-integrity
rspec.log

9
.pairs Normal file
View File

@@ -0,0 +1,9 @@
pairs:
ac: Andrew Courter
cr: Carlos Ramirez III
mail:
prefix: pair
domain: releaseme.ai
no_solo_prefix: true
global: false # Set to true for git-pair to change git configuration for all your projects

3
.rspec Normal file
View File

@@ -0,0 +1,3 @@
--require spec_helper
--tag ~integration

1
.ruby-version Normal file
View File

@@ -0,0 +1 @@
2.6.3

201
Gemfile Normal file
View File

@@ -0,0 +1,201 @@
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby "2.6.3"
# Bundle edge Rails instead: gem "rails", github: "rails/rails"
gem "rails", "~> 6.0.0"
# Use postgresql as the database for Active Record
gem "pg", "~> 0.18"
# Use Puma as the app server
gem "puma", "~> 3.11"
# Use SCSS for stylesheets
gem "sass-rails", "~> 5.0"
gem "webpacker", "~> 4.0.7"
# Use Uglifier as compressor for JavaScript assets
gem "uglifier", "~> 4.1.20"
# Use CoffeeScript for .coffee assets and views
gem "coffee-rails", "~> 5.0"
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem "turbolinks", "~> 5"
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem "jbuilder", "~> 2.5"
# Use Redis adapter to run Action Cable in production
gem "redis", "~> 4.0", group: [:production, :review]
# Use ActiveStorage variant
gem "mini_magick", "~> 4.8"
gem "active_storage_base64", "~> 1.0.0"
gem "image_processing", "~> 1.2"
# Use Amazon Web Services S3 for file uploads in production
gem "aws-sdk-s3", "~> 1.31.0", require: false, group: [:production, :review]
# Allow AWS API requests to be signed using IAM credentials
gem "aws-sigv4", "~> 1.0.2"
# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", ">= 1.1.0", require: false
# Background processing library
gem "sidekiq", "~> 5.2.5"
# Sidekiq web console requires a specific version of Rack to work
gem "rack", github: "rack/rack", ref: "f690bb71425aa31d7b9b3113829af773950d8ab5"
# General-purpose JavaScript library
gem "jquery-rails", "~> 4.3.1"
# Pagination framework
gem "will_paginate", "~> 3.2.1"
# Front-end framework and associated helpers
gem "bootstrap", "~> 4.4.0"
gem "bootstrap_form", "~> 4.3.0"
gem "country_select", "~> 3.1.1"
gem "font-awesome-rails", "~> 4.7.0.2"
gem "will_paginate-bootstrap4", "~> 0.2.2"
# Engine for static pages
gem "high_voltage", "~> 3.0.0"
# Ruby wrapper for Segment analytics API
gem "analytics-ruby", require: "segment"
# A PDF generation library and pre-packaged binaries for WKHTMLTOPDF dependency
gem "pdfkit", "~> 0.8.2", require: "pdfkit"
gem "wkhtmltopdf-binary", "~> 0.12.3.1", group: [:development, :test]
gem "wkhtmltopdf-heroku", "~> 2.12.5.0", group: [:production, :review]
# A PDF library for merging files together
gem "hexapdf", "~> 0.9.0"
# Authentication framework
gem "oath", "~> 1.1.0"
gem "oath-generators", "~> 1.0.1"
# Authorization framework
gem "pundit", "~> 2.0.0"
# Standardized data migrations
gem "rails-data-migrations", "~> 1.2.0"
# Connects business objects and REST web services
gem "activeresource", "5.1.0"
# Markdown parser
gem "redcarpet", "~> 3.4.0"
# Additional middleware for Rack
gem "rack-contrib", "~> 2.0.1"
# Tagging plugin
gem "acts-as-taggable-on", github: "tbuehl/acts-as-taggable-on", branch: "fix/rails-6-and-failing-specs"
# Value class for SMPTE timecode information
gem "timecode", "~> 2.2.2"
# Generate xlsx (Excel) templates
gem "axlsx", "~> 3.0.0.pre"
gem "axlsx_rails", "~> 0.5.2"
gem "axlsx_styler", "~> 0.2.0"
# Shows errors when using SJR templates
gem "better_sjr", "~> 1.0.0"
# Enable full-text searching using PostgreSQL
gem "pg_search", "~> 2.1.2"
# Provides support for dealing with money and currency
gem "money-rails", "~> 1.13.0"
# Ruby library that encodes QR Codes
gem "rqrcode", "~> 0.10.1"
# API wrapper for Sentry exception tracking service
gem "sentry-raven", "~> 2.11.0"
# Store settings as a serialized Hash for ActiveRecord models
gem "ledermann-rails-settings", "~> 2.4.3"
gem "httparty", "~> 0.17.0"
gem "remember_me", "1.0.0", github: "exosyphon-earthvectors/remember_me", branch: "remove-mongoid-dependency"
# Helper for creating conditional CSS class strings
gem "css-class-string", "~> 0.1.1"
# Manages and displays breadcrumb trails
gem "loaf", "~> 0.8.1"
# Library for reading and writing zip files
gem 'rubyzip', '~> 1.2.4'
# Mux API library for live streaming
# TODO - Update the gem as soon as version 1.5.0 is out.
gem 'mux_ruby', git: 'git://github.com/muxinc/mux-ruby.git', branch: 'test-mode'
# CORS policies
gem 'rack-cors'
# Ruby wrappers for the HubSpot REST API
gem "hubspot-ruby"
group :development, :test, :review do
# Call "byebug" anywhere in the code to stop execution and get a debugger console
gem "byebug", "~> 11.0.1", platforms: [:mri, :mingw, :x64_mingw]
gem "rails-controller-testing", "~> 1.0.4"
# Testing framework
gem "rspec-rails", "~> 4.0.0.beta2"
# Fixture replacement for generating test data
gem "factory_bot_rails", "~> 4.8.2"
# Acceptance test framework for web applications
gem "capybara", "~> 3.28.0"
# Enable headless browser testing for Capybara using Chrome
gem "webdrivers", "~> 4.0"
# RSpec matchers for common testing expectations
gem "shoulda-matchers", "~> 4.0"
# Temporary table-backed ActiveRecord models
gem "temping", "~> 3.10.0"
# PDF parsing
gem "pdf-reader", "~> 2.1.0"
# Stubbing HTTP requests
gem "webmock"
end
group :development do
# Access an interactive console on exception pages or by calling "console" anywhere in the code.
gem "web-console", "~> 4.0.1"
gem "listen", ">= 3.0.5", "< 3.2"
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem "spring", "~> 2.1.0"
gem "spring-watcher-listen", "~> 2.0.0"
# Rake tasks for sorting YAML files
gem "i18n_yaml_sorter", "~> 0.2.0"
# Loading .env files for environment variables
gem "dotenv-rails", "~> 2.7.5"
# Shell commands for Heroku apps
gem "parity", "~> 3.2.0"
# Run scheduled jobs
gem "clockwork", "~> 2.0.4"
end
gem "jsonapi-rails"
gem "knock"
gem "rubocop"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby]
# Datepicker
gem 'bootstrap-datepicker-rails'
# Zoom.us integration
gem 'zoom_rb', github: 'blazejkotowski/zoom_rb', branch: 'roles-api'

599
Gemfile.lock Normal file
View File

@@ -0,0 +1,599 @@
GIT
remote: git://github.com/muxinc/mux-ruby.git
revision: 443238cc7195eb28c724e128378da0bfc25180a5
branch: test-mode
specs:
mux_ruby (1.4.0)
json (~> 2.1, >= 2.1.0)
typhoeus (~> 1.0, >= 1.0.1)
GIT
remote: https://github.com/blazejkotowski/zoom_rb.git
revision: 9d32753f0daa1936d1452a427cb69fd0d75f5be6
branch: roles-api
specs:
zoom_rb (0.10.0)
httparty (~> 0.13)
json (>= 1.8)
jwt
GIT
remote: https://github.com/exosyphon-earthvectors/remember_me.git
revision: 1582f2daa900949e6c3ebb74617eb9068f9dea17
branch: remove-mongoid-dependency
specs:
remember_me (1.0.0)
rails (>= 3.2)
GIT
remote: https://github.com/rack/rack.git
revision: f690bb71425aa31d7b9b3113829af773950d8ab5
ref: f690bb71425aa31d7b9b3113829af773950d8ab5
specs:
rack (2.2.0)
GIT
remote: https://github.com/tbuehl/acts-as-taggable-on.git
revision: 47b192b8d09079ac32f932a849f9d87630035515
branch: fix/rails-6-and-failing-specs
specs:
acts-as-taggable-on (6.0.1)
activerecord (>= 5.0, < 6.1)
GEM
remote: https://rubygems.org/
specs:
Ascii85 (1.0.3)
actioncable (6.0.0)
actionpack (= 6.0.0)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.0.0)
actionpack (= 6.0.0)
activejob (= 6.0.0)
activerecord (= 6.0.0)
activestorage (= 6.0.0)
activesupport (= 6.0.0)
mail (>= 2.7.1)
actionmailer (6.0.0)
actionpack (= 6.0.0)
actionview (= 6.0.0)
activejob (= 6.0.0)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.0)
actionview (= 6.0.0)
activesupport (= 6.0.0)
rack (~> 2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.0.0)
actionpack (= 6.0.0)
activerecord (= 6.0.0)
activestorage (= 6.0.0)
activesupport (= 6.0.0)
nokogiri (>= 1.8.5)
actionview (6.0.0)
activesupport (= 6.0.0)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_storage_base64 (1.0.0)
rails (~> 6.0)
activejob (6.0.0)
activesupport (= 6.0.0)
globalid (>= 0.3.6)
activemodel (6.0.0)
activesupport (= 6.0.0)
activemodel-serializers-xml (1.0.2)
activemodel (> 5.x)
activesupport (> 5.x)
builder (~> 3.1)
activerecord (6.0.0)
activemodel (= 6.0.0)
activesupport (= 6.0.0)
activeresource (5.1.0)
activemodel (>= 5.0, < 7)
activemodel-serializers-xml (~> 1.0)
activesupport (>= 5.0, < 7)
activestorage (6.0.0)
actionpack (= 6.0.0)
activejob (= 6.0.0)
activerecord (= 6.0.0)
marcel (~> 0.3.1)
activesupport (6.0.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.1, >= 2.1.8)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
afm (0.2.2)
analytics-ruby (2.2.7)
approximately (1.1.0)
ast (2.4.0)
autoprefixer-rails (9.7.3)
execjs
aws-eventstream (1.0.3)
aws-partitions (1.210.0)
aws-sdk-core (3.46.2)
aws-eventstream (~> 1.0)
aws-partitions (~> 1.0)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-kms (1.13.0)
aws-sdk-core (~> 3, >= 3.39.0)
aws-sigv4 (~> 1.0)
aws-sdk-s3 (1.31.0)
aws-sdk-core (~> 3, >= 3.39.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.0)
aws-sigv4 (1.0.3)
axlsx (3.0.0.pre)
htmlentities (~> 4.3, >= 4.3.4)
mimemagic (~> 0.3)
nokogiri (~> 1.8, >= 1.8.2)
rubyzip (~> 1.2, >= 1.2.1)
axlsx_rails (0.5.2)
actionpack (>= 3.1)
axlsx (>= 2.0.1)
axlsx_styler (0.2.0)
activesupport (>= 3.1)
axlsx (>= 2.0, < 4)
bcrypt (3.1.13)
better_sjr (1.0.0)
bindex (0.8.1)
bootsnap (1.4.5)
msgpack (~> 1.0)
bootstrap (4.4.1)
autoprefixer-rails (>= 9.1.0)
popper_js (>= 1.14.3, < 2)
sassc-rails (>= 2.0.0)
bootstrap-datepicker-rails (1.9.0.1)
railties (>= 3.0)
bootstrap_form (4.3.0)
actionpack (>= 5.0)
activemodel (>= 5.0)
builder (3.2.4)
byebug (11.0.1)
capybara (3.28.0)
addressable
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (~> 1.5)
xpath (~> 3.2)
childprocess (2.0.0)
rake (< 13.0)
chunky_png (1.3.11)
clockwork (2.0.4)
activesupport
tzinfo
cmdparse (3.0.4)
coffee-rails (5.0.0)
coffee-script (>= 2.2.0)
railties (>= 5.2.0)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.1.5)
connection_pool (2.2.2)
countries (2.1.4)
i18n_data (~> 0.8.0)
money (~> 6.9)
sixarm_ruby_unaccent (~> 1.1)
unicode_utils (~> 1.4)
country_select (3.1.1)
countries (~> 2.0)
sort_alphabetical (~> 1.0)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.6)
css-class-string (0.1.1)
diff-lcs (1.3)
dotenv (2.7.5)
dotenv-rails (2.7.5)
dotenv (= 2.7.5)
railties (>= 3.2, < 6.1)
erubi (1.9.0)
ethon (0.12.0)
ffi (>= 1.3.0)
execjs (2.7.0)
factory_bot (4.8.2)
activesupport (>= 3.0.0)
factory_bot_rails (4.8.2)
factory_bot (~> 4.8.2)
railties (>= 3.0.0)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
ffi (1.11.3)
font-awesome-rails (4.7.0.5)
railties (>= 3.2, < 6.1)
geom2d (0.2.0)
globalid (0.4.2)
activesupport (>= 4.2.0)
hashdiff (1.0.1)
hashery (2.1.2)
hexapdf (0.9.3)
cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.2)
high_voltage (3.0.0)
htmlentities (4.3.4)
httparty (0.17.0)
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
hubspot-ruby (0.9.0)
activesupport (>= 3.0.0)
httparty (>= 0.10.0)
i18n (1.8.2)
concurrent-ruby (~> 1.0)
i18n_data (0.8.0)
i18n_yaml_sorter (0.2.0)
image_processing (1.9.3)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.13, < 3)
jaro_winkler (1.5.4)
jbuilder (2.9.1)
activesupport (>= 4.2.0)
jmespath (1.4.0)
jquery-rails (4.3.5)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.3.0)
jsonapi-deserializable (0.2.0)
jsonapi-parser (0.1.1)
jsonapi-rails (0.4.0)
jsonapi-parser (~> 0.1.0)
jsonapi-rb (~> 0.5.0)
jsonapi-rb (0.5.0)
jsonapi-deserializable (~> 0.2.0)
jsonapi-serializable (~> 0.3.0)
jsonapi-renderer (0.2.2)
jsonapi-serializable (0.3.1)
jsonapi-renderer (~> 0.2.0)
jwt (1.5.6)
knock (2.1.1)
bcrypt (~> 3.1)
jwt (~> 1.5)
rails (>= 4.2)
ledermann-rails-settings (2.4.3)
activerecord (>= 3.1)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
loaf (0.8.1)
rails (>= 3.2)
loofah (2.4.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
method_source (0.9.2)
mime-types (3.3)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.0904)
mimemagic (0.3.3)
mini_magick (4.9.5)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.14.0)
monetize (1.9.2)
money (~> 6.12)
money (6.13.4)
i18n (>= 0.6.4, <= 2)
money-rails (1.13.2)
activesupport (>= 3.0)
monetize (~> 1.9.0)
money (~> 6.13.2)
railties (>= 3.0)
msgpack (1.3.1)
multi_xml (0.6.0)
multipart-post (2.1.1)
nio4r (2.5.1)
nokogiri (1.10.7)
mini_portile2 (~> 2.4.0)
oath (1.1.0)
bcrypt
rails
warden
oath-generators (1.0.1)
oath (>= 0.0.12)
parallel (1.19.1)
parity (3.2.0)
parser (2.6.5.0)
ast (~> 2.4.0)
pdf-reader (2.1.0)
Ascii85 (~> 1.0.0)
afm (~> 0.2.1)
hashery (~> 2.0)
ruby-rc4
ttfunk
pdfkit (0.8.4.1)
pg (0.21.0)
pg_search (2.1.7)
activerecord (>= 4.2)
activesupport (>= 4.2)
popper_js (1.14.5)
public_suffix (4.0.1)
puma (3.12.1)
pundit (2.0.1)
activesupport (>= 3.0.0)
rack-contrib (2.0.1)
rack (~> 2.0)
rack-cors (1.1.1)
rack (>= 2.0.0)
rack-protection (2.0.7)
rack
rack-proxy (0.6.5)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.0.0)
actioncable (= 6.0.0)
actionmailbox (= 6.0.0)
actionmailer (= 6.0.0)
actionpack (= 6.0.0)
actiontext (= 6.0.0)
actionview (= 6.0.0)
activejob (= 6.0.0)
activemodel (= 6.0.0)
activerecord (= 6.0.0)
activestorage (= 6.0.0)
activesupport (= 6.0.0)
bundler (>= 1.3.0)
railties (= 6.0.0)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.4)
actionpack (>= 5.0.1.x)
actionview (>= 5.0.1.x)
activesupport (>= 5.0.1.x)
rails-data-migrations (1.2.0)
rails (>= 4.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
railties (6.0.0)
actionpack (= 6.0.0)
activesupport (= 6.0.0)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
rainbow (3.0.0)
rake (12.3.3)
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
ffi (~> 1.0)
redcarpet (3.4.0)
redis (4.1.2)
regexp_parser (1.6.0)
rqrcode (0.10.1)
chunky_png (~> 1.0)
rspec-core (3.9.1)
rspec-support (~> 3.9.1)
rspec-expectations (3.9.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-rails (4.0.0.beta4)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
rspec-core (~> 3.9)
rspec-expectations (~> 3.9)
rspec-mocks (~> 3.9)
rspec-support (~> 3.9)
rspec-support (3.9.2)
rubocop (0.76.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.6)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
ruby-progressbar (1.10.1)
ruby-rc4 (0.1.5)
ruby-vips (2.0.14)
ffi (~> 1.9)
ruby_dep (1.5.0)
rubyzip (1.2.4)
safe_yaml (1.0.5)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sass-rails (5.1.0)
railties (>= 5.2.0)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3)
sassc (2.2.1)
ffi (~> 1.9)
sassc-rails (2.1.2)
railties (>= 4.0.0)
sassc (>= 2.0)
sprockets (> 3.0)
sprockets-rails
tilt
selenium-webdriver (3.142.4)
childprocess (>= 0.5, < 3.0)
rubyzip (~> 1.2, >= 1.2.2)
sentry-raven (2.11.0)
faraday (>= 0.7.6, < 1.0)
shoulda-matchers (4.2.0)
activesupport (>= 4.2.0)
sidekiq (5.2.7)
connection_pool (~> 2.2, >= 2.2.2)
rack (>= 1.5.0)
rack-protection (>= 1.5.0)
redis (>= 3.3.5, < 5)
sixarm_ruby_unaccent (1.2.0)
sort_alphabetical (1.1.0)
unicode_utils (>= 1.2.2)
spring (2.1.0)
spring-watcher-listen (2.0.1)
listen (>= 2.7, < 4.0)
spring (>= 1.2, < 3.0)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
temping (3.10.0)
activerecord (>= 4.2)
activesupport (>= 4.2)
thor (1.0.1)
thread_safe (0.3.6)
tilt (2.0.10)
timecode (2.2.2)
approximately (~> 1.1)
ttfunk (1.5.1)
turbolinks (5.2.0)
turbolinks-source (~> 5.2)
turbolinks-source (5.2.0)
typhoeus (1.3.1)
ethon (>= 0.9.0)
tzinfo (1.2.6)
thread_safe (~> 0.1)
uglifier (4.1.20)
execjs (>= 0.3.0, < 3)
unicode-display_width (1.6.0)
unicode_utils (1.4.0)
warden (1.2.8)
rack (>= 2.0.6)
web-console (4.0.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webdrivers (4.1.2)
nokogiri (~> 1.6)
rubyzip (~> 1.0)
selenium-webdriver (>= 3.0, < 4.0)
webmock (3.8.3)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webpacker (4.0.7)
activesupport (>= 4.2)
rack-proxy (>= 0.6.1)
railties (>= 4.2)
websocket-driver (0.7.1)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.4)
will_paginate (3.2.1)
will_paginate-bootstrap4 (0.2.2)
will_paginate (~> 3.0, >= 3.0.0)
wkhtmltopdf-binary (0.12.3.1)
wkhtmltopdf-heroku (2.12.5.0)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.2.2)
PLATFORMS
ruby
DEPENDENCIES
active_storage_base64 (~> 1.0.0)
activeresource (= 5.1.0)
acts-as-taggable-on!
analytics-ruby
aws-sdk-s3 (~> 1.31.0)
aws-sigv4 (~> 1.0.2)
axlsx (~> 3.0.0.pre)
axlsx_rails (~> 0.5.2)
axlsx_styler (~> 0.2.0)
better_sjr (~> 1.0.0)
bootsnap (>= 1.1.0)
bootstrap (~> 4.4.0)
bootstrap-datepicker-rails
bootstrap_form (~> 4.3.0)
byebug (~> 11.0.1)
capybara (~> 3.28.0)
clockwork (~> 2.0.4)
coffee-rails (~> 5.0)
country_select (~> 3.1.1)
css-class-string (~> 0.1.1)
dotenv-rails (~> 2.7.5)
factory_bot_rails (~> 4.8.2)
font-awesome-rails (~> 4.7.0.2)
hexapdf (~> 0.9.0)
high_voltage (~> 3.0.0)
httparty (~> 0.17.0)
hubspot-ruby
i18n_yaml_sorter (~> 0.2.0)
image_processing (~> 1.2)
jbuilder (~> 2.5)
jquery-rails (~> 4.3.1)
jsonapi-rails
knock
ledermann-rails-settings (~> 2.4.3)
listen (>= 3.0.5, < 3.2)
loaf (~> 0.8.1)
mini_magick (~> 4.8)
money-rails (~> 1.13.0)
mux_ruby!
oath (~> 1.1.0)
oath-generators (~> 1.0.1)
parity (~> 3.2.0)
pdf-reader (~> 2.1.0)
pdfkit (~> 0.8.2)
pg (~> 0.18)
pg_search (~> 2.1.2)
puma (~> 3.11)
pundit (~> 2.0.0)
rack!
rack-contrib (~> 2.0.1)
rack-cors
rails (~> 6.0.0)
rails-controller-testing (~> 1.0.4)
rails-data-migrations (~> 1.2.0)
redcarpet (~> 3.4.0)
redis (~> 4.0)
remember_me (= 1.0.0)!
rqrcode (~> 0.10.1)
rspec-rails (~> 4.0.0.beta2)
rubocop
rubyzip (~> 1.2.4)
sass-rails (~> 5.0)
sentry-raven (~> 2.11.0)
shoulda-matchers (~> 4.0)
sidekiq (~> 5.2.5)
spring (~> 2.1.0)
spring-watcher-listen (~> 2.0.0)
temping (~> 3.10.0)
timecode (~> 2.2.2)
turbolinks (~> 5)
tzinfo-data
uglifier (~> 4.1.20)
web-console (~> 4.0.1)
webdrivers (~> 4.0)
webmock
webpacker (~> 4.0.7)
will_paginate (~> 3.2.1)
will_paginate-bootstrap4 (~> 0.2.2)
wkhtmltopdf-binary (~> 0.12.3.1)
wkhtmltopdf-heroku (~> 2.12.5.0)
zoom_rb!
RUBY VERSION
ruby 2.6.3p62
BUNDLED WITH
1.17.3

3
Procfile Normal file
View File

@@ -0,0 +1,3 @@
web: bundle exec rails s
worker: bundle exec sidekiq -q default -q active_storage_analysis -q active_storage_purge -q mailers -c 5
release: ./scripts/release_tasks.sh

167
README.md Normal file
View File

@@ -0,0 +1,167 @@
### Setup
Install RVM to manage ruby version:
```bash
\curl -sSL https://get.rvm.io | bash
rvm install 2.6.3
```
Install external dependencies:
#### For MacOS:
```bash
brew tap heroku/brew && brew install heroku
brew install postgres
brew install redis
brew install ffmpeg
brew install yarn
```
#### For Linux (Ubuntu):
```bash
curl https://cli-assets.heroku.com/install.sh | sh
sudo apt-get install postgresql postgresql-contrib
sudo apt-get install redis-server
sudo add-apt-repository ppa:mc3man/trusty-media
sudo apt-get install ffmpeg
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update
sudo apt-get install yarn
```
Then run:
```bash
bin/setup
```
to create your databases
You will need to:
- Get environment variables from fellow teammates to populate into `.env` (See env.sample for examples)
- AWS account to have access to S3 buckets
- Heroku account to deploy
- Set DOMAIN and WEB_PORT variables - otherwise application won't work properly. Use DOMAIN=localhost and WEB_PORT=3000 for the default values
- Set ZOOM_API_KEY and ZOOM_API_SECRET to integrate with Zoom.us for automatic meeting creation.
After filling up the .env with correct values
Run setup again:
```bash
bin/setup
```
### Running tests
```bash
rspec
yarn test
```
### Running Data migrations
```bash
rails data:migrate
```
### Current deployment pipeline on Heroku
- Review -> Staging |-> Production
|-> Demo
#### Working with environments using Parity
[Parity](https://github.com/thoughtbot/parity) provides Heroku command wrappers for each environment under the `bin` folder.
### Useful Heroku commands
##### Manually Deploying to Review
The Review environment is auto-deployed from GitHub `master`. But if you need to manually deploy:
```bash
bin/review deploy
```
##### Running migrations manually (should be done automatically by Heroku via Procfile)
```bash
bin/review run db:migrate
```
##### Rails console into Review environment
```bash
bin/review console
```
##### We keep locale files sorted in alphabetical order. This can be done programmatically
```
rake i18n:sort
```
## Zoom.us integration
DirectMe app offers live broadcasting. Users are offered to paralelly connect to the Zoom meeting to have a video conference while the streaming happens. In order to use the Zoom functionality, the app needs to have the API keys provided. You need Zoom PRO account for this feature.
#### Zoom.us api keys
1. Log in to you zoom.us account
2. Go to https://marketplace.zoom.us/develop/create
3. Choose JWT application
4. Copy API Key and API Secret
## Working Locally
#### Polling for analysis status updates
When videos are uploaded, a video/audio analysis process is initiated. Typically the analysis process sends an SNS notification when it is completed. Those SNS notifications hit a webhook URL within the app to update the status accordingly. This requires a public URL, which you likely won't have in development.
As an alternative, we can poll for status updates using the following Rake task.
```bash
rake dev:poll_for_analysis_updates
```
To run this continuously, there is a [Clockwork](https://github.com/adamwiggins/clockwork) scheduler included.
```bash
bin/clockwork lib/dev_clockwork.rb
```
#### Running a real async background queue locally
1. Verify redis has been installed
1. Set REDIS_URL in your `.env` file
```bash
REDIS_URL=redis://127.0.0.1:6379
```
1. Start Sidekiq
```bash
bundle exec sidekiq -q default -q mailers -c 5
```
### Setup pairing script
[Check out the repo here](https://github.com/pivotal-legacy/git_scripts)
- Tool to add two or more authors on a commit message quickly
- Make sure to add yourself or new devs to the `.pairs` file in the repo
`cd /usr/local/bin && curl -L http://github.com/pivotal/git_scripts/tarball/master | gunzip | tar xvf - --strip=2`
then `git pair XX` where XX are your initials from the pairs file
Enjoy!
## Problems with setting up environment
### Database setup
If the setup fails in the point after at least one of the databases
have been set up.
1. Drop the databases
```bash
rails db:drop
```
1. Run the setup again.
## Optional helpers
# Run rubocop fix script as a pre-commit git hook
Add following as .git/hooks/pre-commit file
```
#!/bin/sh
exec ./bin/rubocopfix.sh
```

6
Rakefile Normal file
View File

@@ -0,0 +1,6 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require_relative 'config/application'
Rails.application.load_tasks

31
app.json Normal file
View File

@@ -0,0 +1,31 @@
{
"environments": {
"test": {
"env": {
"WD_CHROME_PATH": "/app/.apt/usr/bin/google-chrome-stable"
},
"addons": [
"heroku-redis:in-dyno",
"heroku-postgresql:in-dyno"
],
"buildpacks": [
{
"url": "heroku/ruby"
},
{
"url": "heroku/nodejs"
},
{
"url": "https://github.com/heroku/heroku-buildpack-google-chrome"
},
{
"url": "https://github.com/heroku/heroku-buildpack-activestorage-preview.git"
}
],
"scripts": {
"test-setup": "bin/rails assets:precompile",
"test": "rspec && rspec --tag integration"
}
}
}
}

View File

@@ -0,0 +1,3 @@
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css

0
app/assets/images/.keep Normal file
View File

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 395 100" style="enable-background:new 0 0 395 100;" xml:space="preserve">
<style type="text/css">
.st0{fill:#171314;}
.st1{fill:#EC1E24;}
.st2{fill:#EE363B;}
</style>
<g>
<g>
<g>
<g>
<path class="st0" d="M71.708,90.105h30.994c12.19,0,19.217-4.753,19.217-13.224v-0.414c0-7.851-6.199-13.017-19.629-13.017
H71.708V90.105z"/>
</g>
<g>
<path class="st0" d="M115.926,22.746v-0.414c0-7.851-6.199-12.397-18.183-12.397H71.708V35.35h25.208
C109.108,35.35,115.926,31.217,115.926,22.746z"/>
</g>
</g>
</g>
<circle class="st1" cx="191.732" cy="22.976" r="19.801"/>
<g>
<g>
<g>
<rect x="0.188" y="-0.438" class="st0" width="32.469" height="100.915"/>
</g>
<g>
<path class="st0" d="M220.974,50.02c0-19.586,7.145-37.202,19.286-50.457h-87.75c1.966,4.185,3.088,8.981,3.088,14.505v0.414
c0,16.736-9.505,26.654-22.316,32.233c17.564,5.786,28.722,15.704,28.722,35.333v0.414c0,6.768-1.519,12.806-4.415,18.017
h14.342v-46.12h39.602v46.12h28.215c-11.807-12.907-18.774-30.146-18.774-50.044V50.02z"/>
</g>
<g>
<path class="st0" d="M354.491-0.438l-20.494,24.629c-10.331-8.678-20.662-13.637-34.919-13.637
c-20.868,0-36.985,17.563-36.985,39.672v0.413c0,23.349,16.323,40.085,39.259,40.085c9.71,0,17.149-2.067,23.142-5.993V67.169
h-28.515v-29.34h66.532v62.648H395V-0.438H354.491z"/>
</g>
</g>
</g>
<circle class="st2" cx="191.732" cy="22.976" r="19.801"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48" version="1.1" width="48px" height="48px">
<g id="surface1">
<path style=" fill:#FFA000;" d="M 40 12 L 22 12 L 18 8 L 8 8 C 5.800781 8 4 9.800781 4 12 L 4 20 L 44 20 L 44 16 C 44 13.800781 42.199219 12 40 12 Z "/>
<path style=" fill:#FFCA28;" d="M 40 12 L 8 12 C 5.800781 12 4 13.800781 4 16 L 4 36 C 4 38.199219 5.800781 40 8 40 L 40 40 C 42.199219 40 44 38.199219 44 36 L 44 16 C 44 13.800781 42.199219 12 40 12 Z "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -0,0 +1,155 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="1003.765px" height="581.324px" viewBox="0 0 1003.765 581.324" enable-background="new 0 0 1003.765 581.324"
xml:space="preserve">
<g>
<g>
<g>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="355.3584" y1="337.6338" x2="667.3279" y2="21.1429">
<stop offset="0" style="stop-color:#D20000"/>
<stop offset="0.3602" style="stop-color:#020101"/>
<stop offset="0.7097" style="stop-color:#D20000"/>
<stop offset="1" style="stop-color:#020101"/>
</linearGradient>
<path fill="url(#SVGID_1_)" d="M644.937,312.306c4.518,2.543,8.047,6.601,8.795,11.832c2.063,14.346,3.597,28.706,4.766,43.094
V10.74H345.266v326.469c2.965-7.496,6.708-14.713,11.483-21.525c0.657-0.93,1.354-1.755,2.08-2.528V24.308h286.108V312.306z"/>
</g>
<g>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="313.311" y1="337.875" x2="663.7488" y2="337.875">
<stop offset="0" style="stop-color:#020101"/>
<stop offset="1" style="stop-color:#D20000"/>
</linearGradient>
<path fill="url(#SVGID_2_)" d="M660.762,367.118v-28.663c0-28.471-12.821-46.185-38.115-52.676
c-19.553-5.01-56.349-26.795-67.248-33.399c-11.131-4.921-13.731,8.246-8.204,10.883c9.652,5.906,49.704,29.923,72.083,35.659
c19.309,4.944,27.916,17.144,27.916,39.533v28.729c-0.006,0.533-0.526,13.157-24.29,22.262l-3.214,1.231
c-26.231,10.081-53.206,20.568-114.142,20.205l-7.334-0.085v0.085c-60.854,0.349-87.893-10.124-114.14-20.205l-3.203-1.231
c-23.766-9.104-24.295-21.729-24.304-22.328v-28.663c0-22.39,8.611-34.589,27.924-39.533
c22.347-5.727,62.296-29.678,72.032-35.636c5.171-3.533-1.755-15.134-8.293-10.826c-11.066,6.708-47.62,28.323-67.103,33.319
C355.829,292.271,343,309.99,343,338.455l0.009,28.428c-0.035,0.905-0.409,22.427,33.014,35.23l3.188,1.222
c27.042,10.397,54.949,21.125,116.447,21.12c2.029,0,4.112-0.01,6.224-0.033c65.956,0.793,94.745-10.35,122.676-21.087
l3.194-1.222C661.178,389.31,660.79,367.783,660.762,367.118z"/>
</g>
<g>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="410.1826" y1="98.9678" x2="410.1826" y2="116.1672">
<stop offset="0" style="stop-color:#D20000"/>
<stop offset="1" style="stop-color:#020101"/>
</linearGradient>
<path fill="url(#SVGID_3_)" d="M439.963,102.392c-1.394,4.345-2.333,8.878-2.762,13.572h-56.799v-13.572H439.963z"/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="593.5845" y1="98.9678" x2="593.5845" y2="116.1672">
<stop offset="0" style="stop-color:#D20000"/>
<stop offset="1" style="stop-color:#020101"/>
</linearGradient>
<path fill="url(#SVGID_4_)" d="M623.365,102.392v13.572h-56.797c-0.43-4.694-1.369-9.228-2.763-13.572H623.365z"/>
</g>
<g>
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="408.6396" y1="138.3242" x2="408.6396" y2="152.4173">
<stop offset="0" style="stop-color:#D20000"/>
<stop offset="1" style="stop-color:#020101"/>
</linearGradient>
<rect x="380.402" y="139.777" fill="url(#SVGID_5_)" width="56.476" height="13.568"/>
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="595.127" y1="138.3242" x2="595.127" y2="152.4173">
<stop offset="0" style="stop-color:#D20000"/>
<stop offset="1" style="stop-color:#020101"/>
</linearGradient>
<rect x="566.889" y="139.777" fill="url(#SVGID_6_)" width="56.476" height="13.568"/>
</g>
<g>
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="410.2896" y1="176.3193" x2="410.2896" y2="193.0743">
<stop offset="0" style="stop-color:#D20000"/>
<stop offset="1" style="stop-color:#020101"/>
</linearGradient>
<path fill="url(#SVGID_7_)" d="M437.254,177.159c0.483,4.694,1.475,9.223,2.923,13.572h-59.775v-13.572H437.254z"/>
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="593.4771" y1="176.3193" x2="593.4771" y2="193.0743">
<stop offset="0" style="stop-color:#D20000"/>
<stop offset="1" style="stop-color:#020101"/>
</linearGradient>
<path fill="url(#SVGID_8_)" d="M623.365,177.159v13.572h-59.775c1.448-4.35,2.441-8.878,2.924-13.572H623.365z"/>
</g>
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="423.3101" y1="146.2383" x2="580.458" y2="146.2383">
<stop offset="0" style="stop-color:#D20000"/>
<stop offset="1" style="stop-color:#020101"/>
</linearGradient>
<path fill="url(#SVGID_9_)" d="M580.19,115.964c-0.349-4.671-1.127-9.199-2.254-13.572
c-8.794-33.763-39.555-58.784-76.053-58.784s-67.257,25.021-76.053,58.784c-1.126,4.373-1.904,8.901-2.254,13.572
c-0.188,2.038-0.267,4.133-0.267,6.223v48.108c0,2.307,0.107,4.614,0.321,6.864c0.375,4.642,1.179,9.171,2.387,13.572
c8.983,33.438,39.582,58.139,75.865,58.139c36.284,0,66.882-24.701,75.866-58.139c1.208-4.401,2.012-8.931,2.388-13.572
c0.213-2.25,0.321-4.558,0.321-6.864v-48.108C580.458,120.097,580.378,118.002,580.19,115.964z M566.889,170.295
c0,2.307-0.134,4.614-0.376,6.864c-0.482,4.694-1.476,9.223-2.924,13.572c-8.555,25.876-32.984,44.595-61.706,44.595
c-28.721,0-53.15-18.719-61.706-44.595c-1.448-4.35-2.44-8.878-2.923-13.572c-0.241-2.25-0.376-4.558-0.376-6.864v-48.108
c0-2.09,0.107-4.185,0.323-6.223c0.429-4.694,1.368-9.228,2.762-13.572c8.394-26.201,32.958-45.212,61.92-45.212
s53.527,19.011,61.921,45.212c1.394,4.345,2.333,8.878,2.763,13.572c0.214,2.038,0.322,4.133,0.322,6.223V170.295z"/>
</g>
</g>
<g>
<path fill="#1D1D2C" d="M10.784,569.702V469.555H73.22c16.924,0.193,27.735,0.613,32.43,1.245
c8.265,1.075,13.988,4.793,17.168,11.151c2.103,4.256,3.155,10.199,3.155,17.828c0,5.773-0.318,10.161-0.954,13.171
c-0.637,3.01-1.761,5.416-3.375,7.228c-2.594,2.887-6.652,4.916-12.18,6.09c4.303,0.736,7.728,2.396,10.272,4.991
c1.908,2.052,3.13,4.496,3.669,7.336c0.537,2.835,0.807,8.463,0.807,16.874v14.233h-13.354v-9.614
c0-8.166-0.172-13.813-0.513-16.945c-0.979-6.359-4.574-10.077-10.787-11.152c-2.446-0.34-4.634-0.552-6.564-0.623
c-1.934-0.075-8.524-0.137-19.774-0.184H24.136v38.519H10.784z M24.136,519.441H73.22c13.011,0,21.35-0.179,25.019-0.547
c3.669-0.368,6.652-1.283,8.952-2.75c3.422-2.251,5.135-7.779,5.135-16.582c0-7.581-1.588-12.497-4.768-14.747
c-2.054-1.468-5.272-2.425-9.648-2.864c-4.378-0.438-12.607-0.66-24.689-0.66H24.136V519.441z"/>
<path fill="#1D1D2C" d="M221.71,548.204h11.52c-0.197,8.317-3.131,14.124-8.805,17.427c-5.674,3.302-15.552,4.953-29.64,4.953
c-11.642,0-20.482-0.453-26.523-1.358c-6.043-0.906-10.601-2.435-13.683-4.586c-7.044-4.991-10.566-14.846-10.566-29.569
c0-10.316,1.492-18.072,4.475-23.257c2.836-4.939,6.935-8.279,12.289-10.016c5.358-1.735,14.295-2.604,26.816-2.604
c11.984,0,20.642,0.637,25.975,1.905c5.33,1.274,9.585,3.718,12.765,7.341c2.643,3.033,4.452,6.553,5.431,10.563
c0.977,4.01,1.516,10.128,1.613,18.342h-77.551c0.294,8.661,2.005,14.601,5.136,17.832c2.102,2.198,5.466,3.703,10.087,4.51
c4.622,0.807,12.143,1.212,22.563,1.212c7.875,0,13.646-0.367,17.314-1.104c3.669-0.73,6.406-2.075,8.217-4.033
C220.512,554.247,221.37,551.729,221.71,548.204z M221.197,527.957c-0.048-6.803-1.859-11.544-5.429-14.238
c-2.348-1.807-5.639-3.066-9.868-3.778c-4.233-0.708-10.433-1.062-18.6-1.062c-7.142,0-12.583,0.33-16.325,0.991
c-3.741,0.66-6.665,1.82-8.768,3.485c-3.473,2.689-5.479,7.553-6.016,14.602H221.197z"/>
<path fill="#1D1D2C" d="M252.958,469.555h11.006v100.147h-11.006V469.555z"/>
<path fill="#1D1D2C" d="M361.682,548.204h11.52c-0.197,8.317-3.131,14.124-8.805,17.427c-5.674,3.302-15.554,4.953-29.64,4.953
c-11.642,0-20.483-0.453-26.523-1.358c-6.043-0.906-10.603-2.435-13.683-4.586c-7.045-4.991-10.566-14.846-10.566-29.569
c0-10.316,1.492-18.072,4.475-23.257c2.836-4.939,6.934-8.279,12.289-10.016c5.358-1.735,14.295-2.604,26.816-2.604
c11.983,0,20.642,0.637,25.974,1.905c5.331,1.274,9.586,3.718,12.766,7.341c2.643,3.033,4.452,6.553,5.431,10.563
c0.977,4.01,1.516,10.128,1.613,18.342h-77.551c0.294,8.661,2.004,14.601,5.135,17.832c2.103,2.198,5.466,3.703,10.088,4.51
c4.622,0.807,12.143,1.212,22.561,1.212c7.875,0,13.646-0.367,17.316-1.104c3.667-0.73,6.406-2.075,8.216-4.033
C360.484,554.247,361.34,551.729,361.682,548.204z M361.169,527.957c-0.048-6.803-1.86-11.544-5.43-14.238
c-2.347-1.807-5.637-3.066-9.868-3.778c-4.231-0.708-10.431-1.062-18.6-1.062c-7.141,0-12.583,0.33-16.324,0.991
c-3.741,0.66-6.665,1.82-8.768,3.485c-3.473,2.689-5.479,7.553-6.016,14.602H361.169z"/>
<path fill="#1D1D2C" d="M404.447,520.984h-11.445c-0.099-1.27-0.147-2.273-0.147-3.01c0-7.284,2.739-12.252,8.217-14.894
c5.477-2.642,15.75-3.962,30.816-3.962c11.836,0,20.347,0.542,25.532,1.613c5.183,1.08,9.245,3.132,12.179,6.165
c2.445,2.448,4.035,5.331,4.769,8.657c0.733,3.325,1.1,9.146,1.1,17.464v36.684h-10.93l0.22-8.143h-0.589
c-2.739,3.816-6.028,6.27-9.868,7.373c-3.84,1.1-11.089,1.651-21.752,1.651c-12.181,0-20.642-0.297-25.388-0.882
c-4.743-0.585-8.339-1.808-10.784-3.671c-2.446-1.76-4.146-3.858-5.1-6.307c-0.953-2.444-1.432-5.869-1.432-10.271
c0-5.382,0.722-9.515,2.166-12.402c1.442-2.882,3.875-5.038,7.3-6.453c3.716-1.618,12.4-2.425,26.045-2.425
c14.282,0,23.626,0.43,28.028,1.283c4.402,0.858,7.801,2.925,10.198,6.203h0.956v-7.486c0-6.114-1.103-10.516-3.304-13.204
c-1.955-2.396-4.927-4.024-8.914-4.883c-3.987-0.854-10.503-1.283-19.552-1.283c-11.887,0-19.579,0.698-23.076,2.095
c-3.498,1.392-5.246,4.439-5.246,9.133C404.447,520.276,404.447,520.593,404.447,520.984z M432.621,537.934
c-9.05,0-15.385,0.156-19.002,0.477c-3.622,0.316-6.189,0.916-7.705,1.798c-2.887,1.712-4.329,4.868-4.329,9.464
c0,4.547,1.712,7.633,5.135,9.246c2.789,1.32,12.179,1.98,28.174,1.98c11.69,0,19.418-0.759,23.186-2.278
c3.766-1.514,5.648-4.595,5.648-9.241c0-4.5-2.102-7.529-6.309-9.1C453.213,538.717,444.946,537.934,432.621,537.934z"/>
<path fill="#1D1D2C" d="M494.095,545.416h11.74c0,0.981,0,1.642,0,1.981c0,3.77,0.404,6.458,1.212,8.071
c0.807,1.613,2.408,2.859,4.803,3.741c3.18,1.128,12.327,1.688,27.442,1.688c9.438,0,16.005-0.207,19.7-0.622
c3.691-0.415,6.345-1.236,7.958-2.463c2.203-1.66,3.304-4.227,3.304-7.698c0-4.209-1.321-7.082-3.964-8.624
c-2.641-1.543-7.46-2.288-14.452-2.236c-2.201,0.048-12.792-0.075-31.769-0.368c-4.94-0.099-8.891-0.49-11.849-1.174
c-2.961-0.685-5.418-1.783-7.374-3.303c-3.718-2.887-5.576-7.751-5.576-14.601c0-7.236,2.322-12.497,6.969-15.775
c2.446-1.708,6.456-2.943,12.033-3.703c5.576-0.76,13.598-1.137,24.065-1.137c11.69,0,20.31,0.637,25.863,1.905
c5.55,1.274,9.624,3.524,12.216,6.751c1.27,1.566,2.092,3.218,2.459,4.954c0.367,1.735,0.548,4.66,0.548,8.765h-11.593
c-0.047-4.255-0.758-7.044-2.126-8.359c-1.516-1.52-4.45-2.618-8.804-3.303c-4.353-0.688-10.664-1.028-18.929-1.028
c-15.066,0-24.211,1.053-27.442,3.156c-2.347,1.566-3.52,4.203-3.52,7.921c0,3.816,1.368,6.335,4.107,7.558
c2.74,1.222,8.268,1.764,16.582,1.617c8.218-0.15,18.856,0.048,31.916,0.586c7.923,0.292,13.635,2.028,17.131,5.207
c3.498,3.18,5.247,8.219,5.247,15.115c0,4.156-0.612,7.534-1.835,10.124c-1.222,2.595-3.204,4.722-5.942,6.383
c-2.397,1.467-6.311,2.51-11.739,3.118c-5.43,0.613-13.281,0.92-23.552,0.92c-11.494,0-20.019-0.345-25.569-1.028
c-5.552-0.685-9.625-1.906-12.216-3.671c-2.641-1.759-4.487-3.887-5.54-6.382c-1.052-2.496-1.578-5.968-1.578-10.417
C494.022,548.496,494.046,547.275,494.095,545.416z"/>
<path fill="#1D1D2C" d="M677.144,548.204h11.518c-0.195,8.317-3.13,14.124-8.803,17.427c-5.676,3.302-15.555,4.953-29.642,4.953
c-11.642,0-20.482-0.453-26.523-1.358c-6.041-0.906-10.602-2.435-13.683-4.586c-7.043-4.991-10.566-14.846-10.566-29.569
c0-10.316,1.493-18.072,4.477-23.257c2.837-4.939,6.934-8.279,12.289-10.016c5.356-1.735,14.295-2.604,26.817-2.604
c11.982,0,20.64,0.637,25.972,1.905c5.331,1.274,9.587,3.718,12.767,7.341c2.641,3.033,4.45,6.553,5.429,10.563
s1.516,10.128,1.613,18.342h-77.551c0.294,8.661,2.006,14.601,5.136,17.832c2.103,2.198,5.466,3.703,10.088,4.51
c4.623,0.807,12.144,1.212,22.562,1.212c7.875,0,13.646-0.367,17.314-1.104c3.669-0.73,6.409-2.075,8.219-4.033
C675.946,554.247,676.801,551.729,677.144,548.204z M676.63,527.957c-0.049-6.803-1.858-11.544-5.43-14.238
c-2.348-1.807-5.636-3.066-9.868-3.778c-4.23-0.708-10.431-1.062-18.599-1.062c-7.144,0-12.583,0.33-16.326,0.991
c-3.741,0.66-6.664,1.82-8.768,3.485c-3.472,2.689-5.477,7.553-6.015,14.602H676.63z"/>
<path fill="#E40C2B" d="M709.343,469.555h26.707l54.367,84.886l53.486-84.886h26.854v100.147h-13.354v-87.751h-5.576
l-55.907,87.751h-10.86l-56.493-87.751h-5.87v87.751h-13.353V469.555z"/>
<path fill="#E40C2B" d="M894.74,569.702V469.555h98.167v11.736h-84.816v30.598h81.221v11.737h-81.221v34.334h84.889v11.742H894.74
z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,67 @@
function beforeUnloadHandler(e){
e.preventDefault();
e.returnValue = '';
}
function turbolinksClickHandler(){
return false;
}
let eventListenersAdded = false;
function addEventListenersForUploadProgresBar(){
if (eventListenersAdded) return;
eventListenersAdded = true;
addEventListener("direct-upload:initialize", event => {
const { detail } = event;
const { id, file } = detail;
const progressBar = App.FileUploadProgress.createProgressBar(id);
$('#upload-progress-container').append(progressBar);
});
addEventListener("direct-upload:start", event => {
const { id } = event.detail;
App.FileUploadProgress.showProgressBar(id);
})
addEventListener("direct-upload:progress", event => {
const { id, progress } = event.detail;
if (App.FileUploadProgress._findProgressBar(id)) {
App.FileUploadProgress.updateProgressBar(id, progress);
}
})
addEventListener("direct-upload:error", event => {
event.preventDefault();
const { id, error } = event.detail;
App.FileUploadProgress.showError(id, error);
})
addEventListener("direct-upload:end", event => {
const { id } = event.detail;
App.FileUploadProgress.removeProgressBar(id)
})
}
$(document).on("turbolinks:load", function() {
addEventListenersForUploadProgresBar();
window.removeEventListener('beforeunload', beforeUnloadHandler, true);
$(document).off('turbolinks:click', turbolinksClickHandler);
$("#import_appearance_releases").click(function(e) {
$('#appearance_release_file_upload').click();
});
$('#appearance_release_file_upload').change(function(e) {
$('#import_appearance_releases')
.html('Importing...')
.attr('disabled', 'disabled');
window.addEventListener('beforeunload', beforeUnloadHandler, true);
$(document).on('turbolinks:click', turbolinksClickHandler);
const fileUploadForm = document.getElementById('appearance_releases_import');
Rails.fire(fileUploadForm, 'submit');
});
});

View File

@@ -0,0 +1,21 @@
//= require rails-ujs
//= require jquery3
//= require activestorage
//= require turbolinks
//= require turbolinks-csp
//= require popper
//= require bootstrap-sprockets
//= require bootstrap-datepicker
//= require bs-custom-file-input/dist/bs-custom-file-input
//= require clappr/dist/clappr
//= require clappr-markers-plugin/dist/clappr-markers-plugin
//= require clappr-playback-rate-plugin/dist/clappr-playback-rate-plugin
//= require spark-md5/spark-md5
//= require js-sha256/src/sha256
//= require evaporate/evaporate
//= require exif-js
//= require signature_pad/dist/signature_pad
//= require signature_pad-extensions
//= require smpte-timecode/smpte-timecode
//= require trix/dist/trix
//= require_tree .

View File

@@ -0,0 +1,13 @@
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
//
//= require action_cable
//= require_self
//= require_tree ./channels
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);

View File

View File

@@ -0,0 +1,45 @@
$(document).on "turbolinks:load", ->
# Only connect if a broadcast-token meta tag is present
broadcast_meta = document.querySelector("meta[name=broadcast-token]")
return unless broadcast_meta
broadcastToken = broadcast_meta.getAttribute("content")
App.public = App.cable.subscriptions.create { channel: "BroadcastsChannel", token: broadcastToken },
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
return unless document.querySelector("meta[name=broadcast-token][content='#{broadcastToken}']")
switch data.event
when "broadcast_stream_update" then @refreshBroadcastVideo(data)
when "stream_recording_ready" then @showBroadcastRecordings(data)
when "file_upload_update" then @refreshFilesTab(data)
refreshBroadcastVideo: (data) ->
$("#broadcast_updates").html data.status_content
if data.streamer_status == 'recording' && data.status == 'active'
$("#broadcast_video").html data.video_content
new (Clappr.Player)(
parentId: '#broadcast_video'
source: data.playback_url
width: '100%',
height: '100%',
mute: true,
autoPlay: true,
hlsMinimumDvrSize: 1)
if data.streamer_status == "idle" && data.status == "idle"
$("#broadcast_video").html data.video_content
showBroadcastRecordings: (data) ->
$(".flash-message").html data.flash_content
$("#broadcast_recordings").html data.recordings_content
$("#broadcast_recordings_nav").html data.recordings_nav_content
refreshFilesTab: (data) ->
$("#broadcast_file_list").html data.files_content
$("#broadcast_files_pagination").html data.pagination_content

View File

@@ -0,0 +1,25 @@
# TODO: Need to unsubscribe as project changes
$(document).on "turbolinks:load", ->
# Only connect if a project-id meta tag is present
return unless meta = document.querySelector("meta[name=project-id]")
# Get the id of the project to subscribe to
projectId = meta.getAttribute("content")
App.projects = App.cable.subscriptions.create { channel: "ProjectsChannel", id: projectId },
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
switch data.event
when "video_status_update" then @showVideoStatusUpdate(data.content)
when "download_status_update" then @showDownloadStatusUpdate(data.content)
when "conference_recording_ready" then @showDownloadStatusUpdate(data.content)
showVideoStatusUpdate: (content) ->
$("[data-ujs-target='video-analysis-msg']").replaceWith content
showDownloadStatusUpdate: (content) ->
$(".flash-message").html content

View File

@@ -0,0 +1,18 @@
$(document).on("click", "[data-behavior=clipboard]", function(e) {
e.preventDefault();
// Copy the value of the link's `href` attribute
var value = $(this).attr("href");
// Creates a temporary input field to copy from
var copyToClipboard = function(value) {
var $temp = $("<input>");
$("body").append($temp);
$temp.val(value).select();
document.execCommand("copy");
$temp.remove();
}
// Perform the copying
copyToClipboard(value);
});

View File

@@ -0,0 +1,11 @@
$(document).on("change", "[data-toggle=collapse-select]", function(event) {
const select = event.target;
const target = select.dataset.target;
const showValues = JSON.parse(select.dataset.showValues);
if (showValues.indexOf(select.value) > -1) {
$(target).show("fast");
} else {
$(target).hide("fast");
}
});

View File

@@ -0,0 +1,64 @@
if (typeof(Storage) !== "undefined") {
class CollapseState {
static get _KEY() { return "collapseState"; }
set(id, value) {
const state = this._state;
state[id] = value;
window.localStorage.setItem(CollapseState._KEY, JSON.stringify(state));
}
get(id) {
const state = this._state;
return state[id];
}
get _state() {
if (window.localStorage.getItem(CollapseState._KEY) === null) {
window.localStorage.setItem(CollapseState._KEY, JSON.stringify({}));
}
const data = window.localStorage.getItem(CollapseState._KEY);
return JSON.parse(data);
}
}
// EVENT HANDLER - Store state when element is hidden
$(document).on("hidden.bs.collapse", (event) => {
const state = new CollapseState();
state.set(event.target.id, "hide");
});
// EVENT HANDLER - Store state when element is shown
$(document).on("shown.bs.collapse", (event) => {
const state = new CollapseState();
state.set(event.target.id, "show");
});
// Set initial state of elements from storage
$(document).on("turbolinks:load", function() {
const state = new CollapseState();
const collapsibles = document.querySelectorAll("[data-toggle=collapse][data-behavior=stateful]");
collapsibles.forEach( (collapsible) => {
const targetSelector = collapsible.dataset.target.toString().replace("#", "");
const target = document.getElementById(targetSelector);
const value = state.get(target.id);
switch (value) {
case "show":
collapsible.classList.remove("collapsed");
target.classList.add("show");
break;
case "hide":
collapsible.classList.add("collapsed");
target.classList.remove("show");
break;
}
})
});
} else {
console.log("No Web Storage support..");
}

View File

@@ -0,0 +1,3 @@
$(document).on("turbolinks:load", function() {
bsCustomFileInput.init()
})

View File

@@ -0,0 +1,55 @@
App.DigitalSignature = {
init: function(canvas) {
var signatureInput = $(canvas).data("signature-input");
var $clearButton = $("[data-behavior=clear-digital-signature]")
var opts = {
onEnd: function() {
// Save the signature to an input field
if ($(signatureInput).length) {
$(signatureInput).val(signaturePad.crop().toDataURL());
}
}
};
// Initialize using SignaturePad plugin
var signaturePad = new SignaturePad(canvas, opts);
// Handler for when the browser window is resized
// Stores the current signature and restores it after the resizing
function resizeCanvas() {
var cachedSignature = null;
if (!signaturePad.isEmpty()) {
var cachedSignature = signaturePad.toDataURL();
}
var ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext("2d").scale(ratio, ratio);
signaturePad.clear(); // otherwise isEmpty() might return incorrect value
if (cachedSignature !== null) {
signaturePad.fromDataURL(cachedSignature);
}
}
// Attach resize handler and trigger it once for an initial sizing
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
// Attach clear button handler
$clearButton.on("click", function(e) {
e.preventDefault();
signaturePad.clear();
$(signatureInput).val("");
});
},
};
$(document).on("turbolinks:load", function() {
$("[data-behavior=digital-signature]").each(function(index, element) {
App.DigitalSignature.init(element);
});
});

View File

@@ -0,0 +1,31 @@
$(document).on("click", "#video_submit_button", (event) => {
const input = $("#video_file");
if (input[0].files.length > 0) {
event.preventDefault();
const bucket = input.data("aws-bucket");
const awsAccessKeyId = input.data("aws-access-key-id");
const signerUrl = input.data("signer-url");
const uploader = Evaporate.create({
logging: false,
signerUrl: signerUrl,
aws_key: awsAccessKeyId,
bucket: bucket,
cloudfront: false,
computeContentMd5: true,
cryptoMd5Method: (d) => btoa(SparkMD5.ArrayBuffer.hash(d, true)),
cryptoHexEncodedHash256: sha256,
});
const submitButton = $(event.target);
submitButton.prop('disabled', true);
const allFilesCompleted = () => {
input.value = '';
$("#video_form").submit();
}
uploadFile(input[0].files[0], uploader, input, allFilesCompleted)
}
});

View File

@@ -0,0 +1,59 @@
App.FileUploadProgress = {
// Creates a new progress bar element
createProgressBar: id => {
// <div class="progress">
const progress = document.createElement("div")
progress.setAttribute("id", `progress_bar_${id}`)
progress.setAttribute("hidden", "hidden")
progress.classList.add("mt-2")
progress.classList.add("progress")
// <div class="progress-bar">
const progressBar = document.createElement("div")
progressBar.setAttribute("role", "progressbar")
progressBar.setAttribute("aria-valuenow", "0")
progressBar.setAttribute("aria-valuemin", "0")
progressBar.setAttribute("aria-valuemax", "100")
progressBar.classList.add("progress-bar")
progress.appendChild(progressBar)
return progress
},
// Unhides an existing progress bar
showProgressBar: id => {
const progressBar = App.FileUploadProgress._findProgressBar(id)
progressBar.removeAttribute("hidden")
progressBar.firstChild.setAttribute("style", "width: 0%")
},
// Updates the progress of an existing progress bar
updateProgressBar: (id, progress) => {
const progressBar = App.FileUploadProgress._findProgressBar(id)
progressBar.firstChild.setAttribute("style", `width: ${progress}%`)
progressBar.firstChild.setAttribute("aria-valuenow", progress)
},
// Shows an error message
showError: (id, error) => {
// <div class="text-danger>...</div>
const errorMsg = document.createElement("div")
errorMsg.classList.add("text-danger")
errorMsg.appendChild( document.createTextNode(error) )
const progressBar = App.FileUploadProgress._findProgressBar(id)
progressBar.parentNode.replaceChild(errorMsg, progressBar)
},
// Removes a progress bar element from the document
removeProgressBar: id => {
const progressBar = App.FileUploadProgress._findProgressBar(id)
progressBar.parentNode.removeChild(progressBar)
},
// Finds a progress bar element by the given ID
_findProgressBar: id => {
return document.getElementById(`progress_bar_${id}`)
}
}

View File

@@ -0,0 +1,50 @@
$(document).on("click", "[data-behavior=select_broadcast]", function() {
var _this = this;
var checkbox = $(this).children("input:checkbox");
selectBroadcast(_this, checkbox);
});
$(document).on("click", "[data-behavior=select_broadcast] input[type='checkbox']", function(e) {
e.stopPropagation();
var _this = this;
var checkbox = $(this);
selectBroadcast(_this, checkbox);
});
function selectBroadcast(clicked_element, checkbox) {
if (clicked_element.hasChildNodes()) {
if(checkbox.prop("checked")) {
checkbox.prop('checked', false);
} else {
checkbox.prop('checked', true);
}
}
var checked = checkbox.prop("checked");
var project_id = JSON.parse($('#multi_view_broadcasts').parent().attr('data-project-id'));
var broadcast_ids = JSON.parse($('#multi_view_broadcasts').parent().attr('data-broadcast-ids'));
var selected_broadcast_id = checkbox.val();
if (checked && !broadcast_ids.includes(selected_broadcast_id)) {
broadcast_ids.push(selected_broadcast_id);
} else if(!checked && broadcast_ids.includes(selected_broadcast_id)) {
broadcast_ids.splice( $.inArray(selected_broadcast_id, broadcast_ids), 1 );
}
$('#multi_view_broadcasts').parent().attr('data-broadcast-ids', JSON.stringify(broadcast_ids));
if (broadcast_ids.length >= 2) {
multi_view_ids = $.param({multi_view_ids: broadcast_ids});
broadcast_url = "/en/projects/" + project_id + "/broadcasts/" + broadcast_ids[0] + "?" + multi_view_ids
$("#multi_view_broadcasts").attr("href", broadcast_url);
$("#multi_view_broadcasts").attr("target", "_blank");
$("#multi_view_broadcasts").removeClass('disabled');
} else if (broadcast_ids.length < 2) {
$("#multi_view_broadcasts").attr("href", "javascript:void(0);");
$("#multi_view_broadcasts").addClass('disabled');
}
}

View File

@@ -0,0 +1,60 @@
App.PhotoPreview = {
init: function(element) {
var fileInput = $(element).data("file-input");
// Listen for changes on the file input field
$(fileInput).on("change", function(e) {
var input = e.target;
if (input.files && input.files[0]) {
var reader = new FileReader();
// Load a preview of the image and append to the container
reader.onload = function (e) {
App.PhotoPreview.set(element, e.target.result);
}
reader.readAsDataURL(input.files[0]);
}
});
},
set: function(element, imgSrc) {
var img = $("<img>").attr('src', imgSrc).addClass("img-thumbnail photo-preview");
$(element).html(img);
App.PhotoPreview.autoOrient(img.get(0));
},
autoOrient: function(image) {
// Get the EXIF data from the image
EXIF.getData(image, function() {
var orientation = EXIF.getTag(this, "Orientation");
// Rotate the image to ensure it appears right side up
switch (orientation) {
case 6:
$(image).css('transform', 'rotate(90deg)');
break;
case 8:
$(image).css('transform', 'rotate(270deg)');
break;
case 3:
$(image).css('transform', 'rotate(180deg)');
break;
}
});
}
}
$(document).on("turbolinks:load", function() {
$("[data-behavior=person-photo-preview]").each(function(index, element) {
App.PhotoPreview.init(element);
});
$("[data-behavior=guardian-photo-preview]").each(function(index, element) {
App.PhotoPreview.init(element);
});
$("[data-behavior=take-person-photo]").click(function(e) {
$("[data-ujs-target=person-photo-input]").trigger("click");
});
$("[data-behavior=take-guardian-photo]").click(function(e) {
$("[data-ujs-target=guardian-photo-input]").trigger("click");
});
});

View File

@@ -0,0 +1,24 @@
$(document).on("click", "[data-behavior=play_recording]", function() {
if ($(this).hasClass('active')){
return false;
}
var playback_url = $(this).attr("data-playback-url")
$("#broadcast_video").empty();
new Clappr.Player({
parentId: '#broadcast_video',
source: playback_url,
width: '100%',
height: '100%',
autoPlay: true
});
$(".dropdown-menu").children().removeClass('active');
$(".dropdown-menu").children().children('i').remove();
$(this).siblings().removeClass('active');
$(this).siblings().children("i").remove();
$(this).addClass('active');
$(this).prepend('<i class="fa fa-check">&nbsp;</i>');
});

View File

@@ -0,0 +1,9 @@
// $(document).on("turbolinks:load", function() {
// $('body').popover({
// selector: '[data-toggle=popover]'
// });
// });
$(document).on("turbolinks:load", function() {
$('[data-toggle=popover]').popover();
});

View File

@@ -0,0 +1,81 @@
// Makes releasables selectable and taggable.
$(document).on("click", "[data-behavior=all-selectable]", function() {
var checkbox = $(this).children("input:checkbox");
var checked = checkbox.prop("checked");
if (checked) {
checkbox.prop("checked", false);
} else {
checkbox.prop("checked", true);
}
var checkedNewValue = checkbox.prop("checked");
if(checkedNewValue) {
$("[data-behavior=select]").children('input:checkbox').each(function() {
if (!this.checked) {
$(this).parent().trigger('click')
}
});
} else {
$("[data-behavior=select]").children('input:checkbox').each(function() {
if (this.checked) {
$(this).parent().trigger('click')
}
});
}
});
$(document).on("click", "[data-behavior=select]", function() {
var checkbox = $(this).children("input:checkbox");
var checked = checkbox.prop("checked");
var releasable_ids = JSON.parse($('#tag_all').parent().attr('data-releasable-ids'));
var selected_releasable_id = $(this).children('input').val();
if (checked) {
checkbox.prop("checked", false);
} else {
checkbox.prop("checked", true);
}
var checkedNewValue = checkbox.prop("checked");
if (checkedNewValue && !releasable_ids.includes(selected_releasable_id)) {
releasable_ids.push(selected_releasable_id);
} else if(!checkedNewValue && releasable_ids.includes(selected_releasable_id)) {
releasable_ids.splice( $.inArray(selected_releasable_id, releasable_ids), 1 );
}
if (releasable_ids.length >= 1) {
$("#tag_all").prop('disabled', false);
} else if (releasable_ids.length < 1) {
$("#tag_all").prop('disabled', true);
}
$('#tag_all').parent().attr('data-releasable-ids', JSON.stringify(releasable_ids));
});
$(document).on("click", "#tag_all", function(event) {
event.preventDefault();
var releasable_ids = JSON.parse($(this).parent().attr('data-releasable-ids'));
if (releasable_ids.length >= 1) {
var input_ids = $('<input>').attr({ type: 'hidden', name: 'releasable_ids', value: JSON.stringify(releasable_ids) });
var input_name = $('<input>').attr({ type: 'hidden', name: 'releasable_name', value: $("tbody").attr("id") });
$(this).parent().append(input_ids);
$(this).parent().append(input_name);
Rails.fire($(this).parent()[0], 'submit');
}
});
$(document).on("click", "[data-behavior=select] input:checkbox", function() {
$(this).parent().trigger('click');
});
$(document).on("click", "[data-behavior=all-selectable] input:checkbox", function() {
$(this).parent().trigger('click');
});

View File

@@ -0,0 +1,5 @@
$(document).on("turbolinks:load", function() {
$('body').tooltip({
selector: '[data-toggle=tooltip]'
});
});

View File

@@ -0,0 +1,4 @@
// Do not allow file attachments in rich text content
addEventListener("trix-file-accept", function(event) {
event.preventDefault();
})

View File

@@ -0,0 +1,12 @@
// Include CSP nonce for every Turbolinks request (see: content_security_policy.rb)
document.addEventListener("turbolinks:request-start", function(event) {
var xhr = event.data.xhr;
xhr.setRequestHeader("X-Turbolinks-Nonce", $("meta[name='csp-nonce']").prop('content'));
});
// Ensure all <script> tags on Turbolinks cached pages include a nonce
document.addEventListener("turbolinks:before-cache", function() {
$('script[nonce]').each(function(index, element) {
$(element).attr('nonce', element.nonce);
})
})

View File

@@ -0,0 +1,135 @@
// TODO: Do we need to detach events when the page is changed?
App.VideoAnalysis = {
initPlayerWithAnalysis: function(videoElement, bookmarkData) {
// Initialize the video player
var player = App.VideoPlayer.init(videoElement);
// Attach custom keyboard shortcuts
var oldBindKeyEvents = player.core.mediaControl.bindKeyEvents.bind(player.core.mediaControl);
var oldUnbindKeyEvents = player.core.mediaControl.unbindKeyEvents.bind(player.core.mediaControl);
player.core.mediaControl.bindKeyEvents = function() {
oldBindKeyEvents();
player.core.mediaControl.bindKeyAndShow('d', function() {
var markersPlugin = player.getPlugin("markers-plugin");
markersPlugin.getAll().forEach(function(marker, index) {
if ( Math.abs(marker.getTime() - player.getCurrentTime()) < 2 ) {
markersPlugin.removeMarker(marker);
}
});
});
}
player.core.mediaControl.unbindKeyEvents = function() {
console.log("Unbinding key events");
oldUnbindKeyEvents();
if (player.core.mediaControl.kibo) {
player.core.mediaControl.kibo.off('d')
}
}
function ClassyMarker(time, tooltipText, cssClass) {
this._cssClass = cssClass || null;
ClapprMarkersPlugin.StandardMarker.call(this, time, tooltipText);
}
ClassyMarker.prototype = Object.create(ClapprMarkersPlugin.StandardMarker.prototype);
ClassyMarker.prototype.constructor = ClassyMarker;
ClassyMarker.prototype._buildMarkerEl = function() {
var $marker = Clappr.$('<div />').addClass('standard-marker' + ' ' + this._cssClass);
$marker.append(Clappr.$('<div />').addClass('standard-marker-inner'));
return $marker;
}
function RemovableMarker(time, tooltipText, cssClass, url) {
this._url = url || null;
ClassyMarker.call(this, time, tooltipText, cssClass);
}
RemovableMarker.prototype = Object.create(ClassyMarker.prototype);
RemovableMarker.prototype.constructor = RemovableMarker;
RemovableMarker.prototype.onDestroy = function() {
$.ajax({ url: this.source._url, type: "DELETE", headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') } });
};
// Add existing bookmarks to the timeline and set up handlers for adding new bookmarks
var addBookmark = function(bookmark) {
var markers = player.getPlugin("markers-plugin");
// TODO: Move the remove URL into a serializer?
var marker = new RemovableMarker(Number(bookmark.time_elapsed), bookmark.notes + " (Press 'D' to delete)", "bookmark-marker", "/bookmarks/" + bookmark.id);
markers.addMarker(marker);
}
bookmarkData.forEach(function(bookmark, index) {
addBookmark(bookmark)
});
$(document).on("addBookmark-" + $(videoElement).data("video-id"), function(event, bookmark) {
addBookmark(bookmark);
});
// TODO: Where to put this??
player.on("timeupdate", function(event) {
$("[data-ujs-target=bookmark-form] input[name='bookmark[time_elapsed]']").val(event.current);
$("[data-ujs-target=unreleased-appearance-form] input[name='unreleased_appearance[time_elapsed]']").val(event.current);
$("[data-ujs-target=video-release-confirmation-form] input[name='video_release_confirmation[time_elapsed]']").val(event.current);
$("[data-ujs-target=audio-confirmation-form] input[name='audio_confirmation[time_elapsed]']").val(event.current);
$("[data-ujs-target=graphics-element-form] input[name='graphics_element[time_elapsed]']").val(event.current);
$("[data-ujs-target=edl-event-form] input[name='edl_event[time_elapsed]']").val(event.current);
});
// // Add event handler to show recommendations as the video plays
player.on("timeupdate", function(event) {
$("#suggested_matches [data-time]").each(function(index, item) {
var time = $(item).data("time");
if (event.current > time) {
$(item).show();
} else {
$(item).hide();
}
});
$("#graphics_elements_matches [data-time]").each(function(index, item) {
var time = $(item).data("time");
if (event.current > time) {
$(item).show();
} else {
$(item).hide();
}
});
$("#audio_matches [data-time]").each(function(index, item) {
var time = $(item).data("time");
if (event.current > time) {
$(item).show();
} else {
$(item).hide();
}
});
});
$(document).on("click", "#seek_button", () => {
try {
const timecode = new Timecode($("#seek_input").val(), 30);
player.seek(timecode.frameCount / timecode.frameRate);
} catch(e) {
$("#seek_input").val("")
}
});
$(document).on('click',"[data-behavior~=seekable-timecode]", function(e){
e.preventDefault();
const timeCodeText = e.target.innerText;
let extractedTime = timeCodeText;
const startOfTimeCode = 8; // MOV TC: 00:00....
if (timeCodeText.startsWith('MOV')){
extractedTime = timeCodeText.slice(startOfTimeCode);
}
const timeCode = new Timecode(extractedTime, 30);
player.seek(timeCode.frameCount / timeCode.frameRate);
});
}
}
$(document).on("click", "#go_button", function () {
const timecode = $("#go_to").val()
const $edlEvent = $(`[data-timecode-in='${timecode}']`).first()
const edlEventContainer = $("#edl_events_list").parent();
if ($edlEvent.length > 0) {
edlEventContainer.scrollTop($edlEvent.offset().top - edlEventContainer.offset().top + edlEventContainer.scrollTop())
} else {
$("#go_to").val("")
}
});

View File

@@ -0,0 +1,97 @@
App.VideoPlayer = {
init: function(videoElement) {
// The source path for the video
var videoUrl = $(videoElement).attr("src");
// Replace the standard <video> element with the Clappr player
$(videoElement).replaceWith($("<div>", { id: "player" }));
var player = new Clappr.Player({
<%= "baseUrl: 'http://cdn.clappr.io/latest'," if Rails.env.test? %>
source: videoUrl, parentId: "#player", width: "100%",
focusElement: NotTypingSelectiveListenerElement(document), // Allows keyboard shortcuts to work even if the player itself isn't in focus
plugins: {
core: [ClapprMarkersPlugin, PlaybackRatePlugin]
},
markersPlugin: { markers: [] }
});
return player;
}
}
$(document).on("turbolinks:load", function() {
$("[data-behavior=video-player]").each(function(index, element) {
App.VideoPlayer.init(element);
});
});
// An implementation of SelectiveListenerElement which only listens when text areas are not in focus
var NotTypingSelectiveListenerElement = function(element) {
var inputInFocus = function() { return $('input:focus, textarea:focus, video:focus').length > 0; }
var noInputInFocus = function() { return !inputInFocus(); }
return new SelectiveListenerElement(element, noInputInFocus);
}
// A helper class that maintains a collection of handlers for a given event and callback
var HandlerCollection = function() {
this.handlers = [];
// Stores a handler for a given event and callback
this.store = function(eventName, originalCallback, handler) {
var item = {
eventName: eventName,
callback: originalCallback,
handler: handler,
}
this.handlers.push(item);
}
// Finds a handler for a given event and callback
this.find = function(eventName, callback) {
var handler = null;
for (var i = 0; i < this.handlers.length; i++) {
var item = this.handlers[i];
if (item.eventName == eventName && item.callback == callback) {
handler = item.handler;
}
}
return handler;
}
}
// A helper class to allow a DOM element to be wrapped and selectively ignore events that are triggered on it
var SelectiveListenerElement = function(element, shouldListenFn) {
// The element we are proxying
this.element = element;
// A function that determines if we should run any event callbacks or not
this.shouldRunCallbacks = shouldListenFn;
// Stores the functions to be called when an event listener needs to be removed
this.removeHandlers = new HandlerCollection();
this.addEventListener = function(eventName, callback, useCapture) {
// Create a new callback function that conditionally calls the original
var conditionalCallback = function() {
if (this.shouldRunCallbacks()) {
callback();
}
}.bind(this);
// Store the function to be called when removing the event listener
this.removeHandlers.store(eventName, callback, function() {
this.element.removeEventListener(eventName, conditionalCallback, useCapture);
}.bind(this));
// Attach an event listener to the same event with the conditional callback function
this.element.addEventListener(eventName, conditionalCallback, useCapture);
}
this.removeEventListener = function(eventName, callback, useCapture) {
var removeHandler = this.removeHandlers.find(eventName, callback);
if (removeHandler != null) {
removeHandler();
}
}
}

View File

@@ -0,0 +1,54 @@
// Bootstrap theme customization
$white: #FFFFFF;
$gray-100: #F7F8F9;
$gray-300: #DEE5EB;
$gray-400: #B2B6C2;
$gray-500: #A6ACBE;
$gray-800: #393C46;
$gray-900: #2A2D36;
$body-bg: $gray-100;
$body-color: #4A4A4A;
$primary: #6F89FF;
$blue: #0092ff;
$red: #F9002B;
$green: #51B61B;
$teal: #32C498;
$purple: #5139EE;
$dark: $gray-900;
$success: $teal;
$link-color: $body-color;
$input-border-color: $gray-300;
$dropdown-bg: $dark;
$dropdown-color: $gray-500;
$dropdown-link-color: $gray-500;
$dropdown-link-hover-color: lighten($gray-500, 5%);
$dropdown-link-hover-bg: $gray-800;
$dropdown-link-active-bg: $gray-800;
$font-size-base: 1rem;
$font-weight-normal: 400;
$font-weight-bold: 700;
$h1-font-size: $font-size-base * 2.25;
$h2-font-size: $font-size-base * 1.85;
$h3-font-size: $font-size-base * 1.5;
$h4-font-size: $font-size-base * 1.25;
$h5-font-size: $font-size-base * 1.15;
$h6-font-size: $font-size-base;
$headings-font-weight: $font-weight-bold;
$table-border-width: 0;
$table-head-bg: $white;
$table-head-color: $gray-400;
$breadcrumb-bg: transparent;
$breadcrumb-padding-x: 0;
$breadcrumb-padding-y: 0;
$breadcrumb-divider: "\f054";
$breadcrumb-divider-color: $gray-400;
$breadcrumb-active-color: $body-color;
$nav-tabs-link-active-bg: $white;
.thead-light th {
font-weight: $font-weight-normal;
}
.breadcrumb-item + .breadcrumb-item::before {
font-family: "FontAwesome";
}

View File

@@ -0,0 +1,36 @@
//
// Provides a drop-in pointer for the default Trix stylesheet that will format the toolbar and
// the trix-editor content (whether displayed or under editing). Feel free to incorporate this
// inclusion directly in any other asset bundle and remove this file.
//
//= require trix/dist/trix
// We need to override trix.csss image gallery styles to accommodate the
// <action-text-attachment> element we wrap around attachments. Otherwise,
// images in galleries will be squished by the max-width: 33%; rule.
.trix-content {
.attachment-gallery {
> action-text-attachment,
> .attachment {
flex: 1 0 33%;
padding: 0 0.5em;
max-width: 33%;
}
&.attachment-gallery--2,
&.attachment-gallery--4 {
> action-text-attachment,
> .attachment {
flex-basis: 50%;
max-width: 50%;
}
}
}
action-text-attachment {
.attachment {
padding: 0 !important;
max-width: 100% !important;
}
}
}

View File

@@ -0,0 +1,417 @@
@import "bootstrap_overrides";
@import "bootstrap";
@import "font-awesome";
@import "rails_bootstrap_forms";
@import 'trix/dist/trix';
@import "dropzone/dist/dropzone";
@import 'bootstrap-datepicker';
// Wordmarks
.suite-wordmark {
font-weight: normal;
span:first-child {
background-color: $red;
color: white;
padding: 5px;
text-transform: uppercase;
}
span:last-child {
color: $body-color;
margin-left: 5px;
}
}
.product-wordmark {
span:last-child {
font-weight: normal;
margin-left: 5px;
padding: 0 2px;
text-transform: uppercase;
}
&.release-me {
span:last-child {
background-color: $teal;
color: $body-color;
}
}
&.direct-me {
span:last-child {
background-color: $green;
color: $body-color;
}
}
&.edit-me {
span:last-child {
background-color: $blue;
color: $white;
}
}
&.deliver-me {
span:last-child {
background-color: $purple;
color: white;
}
}
&.disabled {
span:last-child {
background-color: $gray-500 !important;
color: $body-color;
}
}
}
// Vertically align all direct descendant elements to the middle
.align-all-middle tr td {
vertical-align: middle !important;
}
// Create horizontal padding on table rows by targeting table cells
@each $name, $value in $spacers {
.tr-px-#{$name} {
&, tr {
th, td {
&:first-child {
padding-left: $value;
}
&:last-child {
padding-right: $value;
}
}
}
}
}
// Show the state of a collapse panel using up and down chevron circle icons
.collapse-indicator {
cursor: pointer;
font-family: "FontAwesome";
&:after {
content: "\f13a";
}
}
.collapsed .collapse-indicator:after {
content: "\f139";
}
// Used for the person photo image upload preview
.photo-preview {
height: 200px;
width: 200px;
}
// Fix for Trix rich text editor element + Bootstrap
// Allows the text area to scroll as content is added
trix-editor.form-control {
height: 15rem;
overflow-y: auto;
}
// Provide some styling for required form fields
.required small:after, label.required:after {
color: $red;
content: "Required";
margin-left: 5px;
font-size: 0.5rem;
}
html[lang="es"] .required:after {
content: "Necesario";
}
// Used when a release has been confirmed for a given video
.releasable[data-confirmed="true"] {
button {
border-width: 5px !important;
opacity: 0.5;
}
}
// Used when a suggested_match has been confirmed for a given video
.releasable-match[data-confirmed="true"] {
button {
border-width: 5px !important;
}
}
// Overrode the above style for audio matches
#audio_matches .releasable-match[data-confirmed="true"] {
button {
border-width: 1px !important;
}
}
// Used when hiding checkmark next to releasable-match
.releasable-match[data-confirmed="false"] [data-ujs-target=match-confirmed-check] {
display: none;
}
// Used when hiding checkmark next to graphics-match
.graphics-match[data-confirmed="false"] [data-ujs-target=graphics-confirmed-check] {
display: none;
}
// Colored borders that correspond to the type of release
.border-appearance_release {
border-color: rgb(0,255,0) !important;
}
.border-talent_release {
border-color: rgb(0,0,255) !important;
}
// Used to hide confirmed releases
.hidden-when-confirmed[data-confirmed="true"] {
display: none;
}
// Markers
.markers-plugin .markers-plugin-markers .standard-marker.bookmark-marker .standard-marker-inner {
background-color: map-get($theme-colors, 'info');
}
.markers-plugin .markers-plugin-markers .standard-marker.bookmark-marker:hover .standard-marker-inner,
.markers-plugin .markers-plugin-markers .standard-marker.bookmark-marker.hover .standard-marker-inner {
background-color: map-get($theme-colors, 'info');
box-shadow: 0 0 0 8px rgba(map-get($theme-colors, 'info'), 0.2);
}
.markers-plugin .markers-plugin-markers .standard-marker.unreleased-appearance-marker .standard-marker-inner {
background-color: map-get($theme-colors, 'danger');
}
.markers-plugin .markers-plugin-markers .standard-marker.unreleased-appearancebookmark-marker:hover .standard-marker-inner,
.markers-plugin .markers-plugin-markers .standard-marker.unreleased-appearancebookmark-marker.hover .standard-marker-inner {
background-color: map-get($theme-colors, 'danger');
box-shadow: 0 0 0 8px rgba(map-get($theme-colors, 'danger'), 0.2);
}
.markers-plugin .markers-plugin-markers .standard-marker.unreleased-warning-marker .standard-marker-inner {
background-color: map-get($theme-colors, 'warning');
}
.markers-plugin .markers-plugin-markers .standard-marker.unreleased-warning-marker .standard-marker-inner {
background-color: map-get($theme-colors, 'warning');
}
.markers-plugin .markers-plugin-markers .standard-marker.unreleased-warning-marker:hover .standard-marker-inner,
.markers-plugin .markers-plugin-markers .standard-marker.unreleased-warning-marker.hover .standard-marker-inner {
background-color: map-get($theme-colors, 'warning');
box-shadow: 0 0 0 8px rgba(map-get($theme-colors, 'warning'), 0.2);
}
// Uses a dashed border style
.border-dashed {
border-style: dashed !important;
}
// Increases the width of the border
.border-thick {
border-width: 2px !important;
}
.border-thicker {
border-width: 5px !important;
}
// Style as a keyboard shortcut key
.keyboard-key {
background-color: map-get($grays, "100");
background-image: linear-gradient($white, map-get($grays, "100"));
border-radius: 3px;
border: 1px solid map-get($grays, "500");
color: $black;
font-family: $font-family-monospace;
font-size: $font-size-sm;
padding: map-get($spacers, 1);
text-transform: uppercase;
}
// Remove default button styles
.btn-no-style {
background-color: transparent;
border: none;
outline: none;
padding: 0;
}
// Cursors
.cursor-copy {
cursor: copy;
}
.cursor-not-allowed {
cursor: not-allowed;
}
// These borders only appear when an element is hovered
@each $color, $value in $theme-colors {
.border-hover-#{$color}:hover {
border-color: $value !important;
}
}
// Customized tooltip colors
@each $color, $value in $theme-colors {
.tooltip-#{$color} {
& > .tooltip-inner {
background-color: $value;
}
&.bs-tooltip-top > .arrow::before {
border-top-color: $value;
}
&.bs-tooltip-right > .arrow::before {
border-right-color: $value;
}
&.bs-tooltip-bottom > .arrow::before {
border-bottom-color: $value;
}
&.bs-tooltip-left > .arrow::before {
border-left-color: $value;
}
}
}
// Fix for custom file input label in an inline form
.form-inline .custom-file-label {
justify-content: flex-start;
}
// Customized carousel colors
.carousel-bg-black {
& > .carousel-inner {
background-color: $black;
}
}
// Text size
.x-small {
font-size: 60%;
}
// Fix long name wrapping in file inputs
.custom-file {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.hidden-file-input {
width: 0px;
height: 0px;
overflow: hidden;
}
[data-behavior=fillable-fields] {
cursor: pointer;
}
[data-behavior=seekable-timecode] {
cursor: pointer;
}
// Fixes known issue with collapsible elements which don't normally receive click events on iOS
// https://github.com/twbs/bootstrap/issues/16213#issuecomment-88995656
[data-toggle=collapse][data-target] {
cursor: pointer;
}
// Pins a modal to the right side of the screen on large devices
.modal-right {
@include media-breakpoint-up(lg) {
left: 25%;
right: 0%;
}
}
// Provides extra padding to main content area when there is a fixed bottom alert
.alert.fixed-bottom + main {
padding-bottom: $alert-padding-y * 5 !important;
}
// Allows <hr> divider to be used on a dark background
.divider-light {
border-color: rgba(255, 255, 255, 0.1);
}
// Sets line height to 0
.no-line-height {
line-height: 0;
}
// Even lighter text color
.text-more-muted {
color: $gray-500 !important;
}
// A dark version of the pills nav
.nav-pills-dark {
a {
color: $gray-500;
&:hover {
background-color: $gray-800;
color: lighten($gray-500, 15%);
}
&.active {
font-weight: $font-weight-bold;
}
}
}
// A comma separated list
ul.list-comma-separated {
li:not(:last-child):after {
content: ",";
}
}
a[data-behavior=seekable-timecode] {
text-decoration: underline;
&:hover {
text-decoration: underline;
}
}
.embeded-contract-preview {
display: block;
margin: 0 auto;
padding-top: 30px;
}
// Border radius
.rounded-pill-left {
border-top-left-radius: $rounded-pill;
border-bottom-left-radius: $rounded-pill;
}
.rounded-pill-right {
border-top-right-radius: $rounded-pill;
border-bottom-right-radius: $rounded-pill;
}
// A nav-pills component that uses white color for the active element
.nav-pills-white {
.nav-link.active,
.show > .nav-link {
color: $body-color;
background-color: $white;
}
}
// A custom background color that is semi-transparent
.bg-semi-transparent {
background-color: rgba($black, 0.05);
}
// Custom width
.w-65 {
width: 65% !important;
}
// Maximum height
.mh-30 {
max-height: 30rem;
}
// Fix height and width
.fix-h-and-w {
width: 308px;
height:308px;
}

View File

@@ -0,0 +1,54 @@
dd {
width: 60%;
}
dt {
font-weight: bold;
width: 33%;
}
dt, dd {
display: inline-block;
padding: 10px 0;
vertical-align: top;
}
ol li, ul li {
padding: 15px 0;
}
strong {
font-weight: bold;
}
u {
text-decoration: underline;
}
.heading {
text-align: center;
text-transform: uppercase;
}
.page {
page-break-before: always;
}
.logo {
text-align: left;
}
.heading-table {
table-layout: fixed;
width: 100%;
text-align: right;
}
.heading-table td {
width: 50%;
}
.serial-number {
text-transform: uppercase;
padding-right: 15px;
}

View File

@@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View File

@@ -0,0 +1,17 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
if verified_user = env['warden'].user
verified_user
end
end
end
end

View File

@@ -0,0 +1,47 @@
class BroadcastsChannel < ApplicationCable::Channel
def subscribed
broadcast = Broadcast.find_by_token!(params[:token])
stream_for broadcast
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def self.broadcast_stream_updates(broadcast)
status_content = ApplicationController.render partial: "broadcasts/broadcast_status", locals: { broadcast: broadcast }
video_content = ApplicationController.render partial: "broadcasts/video", locals: { broadcast: broadcast }
broadcast_to broadcast, {
event: :broadcast_stream_update,
status: broadcast.status,
playback_url: broadcast.stream_playback_url,
video_content: video_content,
status_content: status_content,
streamer_status: broadcast.streamer_status
}
end
def self.stream_recording_ready(broadcast, recordings, message)
flash_message = OpenStruct.new(notice: message, alert: nil)
flash_content = ApplicationController.render partial: "application/flash", locals: { flash: flash_message }
recordings_content = ApplicationController.render partial: "broadcasts/broadcast_recordings", locals: { recordings: recordings, broadcast: broadcast }
recordings_nav_content = ApplicationController.render partial: "broadcasts/broadcast_recording_nav", collection: recordings, as: :broadcast_recording
broadcast_to broadcast,
event: :stream_recording_ready,
flash_content: flash_content,
recordings_content: recordings_content,
recordings_nav_content: recordings_nav_content
end
def self.file_upload_updates(broadcast, files, pagination_content)
files_content = ApplicationController.render partial: "broadcasts/file", collection: files
broadcast_to broadcast, {
event: :file_upload_update,
files_content: files_content,
pagination_content: pagination_content
}
end
end

View File

@@ -0,0 +1,36 @@
class ProjectsChannel < ApplicationCable::Channel
def subscribed
# TODO: How can we get the current account in this context
project = current_user.accessible_projects_for(current_user.primary_account).find(params[:id])
stream_for project
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def self.broadcast_video_analysis_update(video)
content = ApplicationController.render partial: "video_analyses/video_status_updated", locals: { video: video }
broadcast_to video.project, event: :video_status_update, content: content
end
def self.broadcast_download_generation_update(download, notification)
if download.failure?
flash = OpenStruct.new(notice: nil, alert: notification)
else
flash = OpenStruct.new(notice: notification, alert: nil)
end
content = ApplicationController.render partial: "application/flash", locals: { flash: flash }
broadcast_to download.project, event: :download_status_update, content: content
end
def self.conference_recording_ready(project, recording)
link = ApplicationController.helpers.link_to('Download here.', recording.service_url, target: '_blank', class: 'alert-link')
notification = "A recording of your video conference is now available. #{link}"
flash = OpenStruct.new(notice: notification)
content = ApplicationController.render partial: 'application/flash', locals: { flash: flash }
broadcast_to project, event: :conference_recording_ready, content: content
end
end

View File

@@ -0,0 +1,83 @@
class AccountAuthsController < ApplicationController
before_action :set_account_auth, only: [:update, :destroy]
def index
@account = Current.account
if params[:account_id]
@account = accounts.find(params[:account_id])
end
@members = account_auths.joins(:user).where(account: @account).order('role DESC, users.email ASC')
@account_auth = account_auths.new(user: current_user, account: @account, role: :account_manager)
end
def create
email = account_auths_create_params[:user_email]
account_id = account_auths_create_params[:account_id]
user = User.find_by(email: email)
if user.nil?
user = sign_up({ email: email, password: SecureRandom.alphanumeric })
@account_auth = build_account_auth({ user: user, account_id: account_id, role: :account_manager })
@account_auth.save!
UserMailer.welcome(@account_auth.user, @account_auth.account).deliver_later
else
@account_auth = build_account_auth({ user: user, account_id: account_id, role: :account_manager })
@account_auth.save!
UserMailer.existing_account(@account_auth.user, @account_auth.account).deliver_later
end
redirect_to account_auths_path({ account_id: @account_auth.account_id }), notice: t(".notice")
end
def update
AccountAuth.transaction do
if @account_auth.update(account_auth_update_params)
if @account_auth.account_manager?
@account_auth.user.project_memberships.where(project: @account_auth.account.projects).destroy_all
end
flash.notice = t(".notice")
else
flash.alert = t(".alert")
end
end
redirect_to account_auths_path
end
def destroy
ActiveRecord::Base.transaction do
ProjectMembership.where(user: @account_auth.user, project: @account_auth.account.projects).destroy_all
@account_auth.destroy
end
redirect_to account_auths_path, alert: t(".alert")
end
private
def build_account_auth(auth_params)
@account_auth = authorize account_auths.build(auth_params)
end
def account_auths_create_params
params.require(:account_auth).permit(:user_email, :account_id)
end
def account_auth_update_params
params.require(:account_auth).permit(:role)
end
def account_auths
policy_scope(AccountAuth)
end
def find_account_auth
authorize account_auths.find(params[:id])
end
def accounts
policy_scope(Account)
end
def set_account_auth
@account_auth = find_account_auth
end
end

View File

@@ -0,0 +1,13 @@
class AccountSessionsController < ApplicationController
def update
authorize :account_session, :update?
session[:active_account] = account_session_params[:account_id]
redirect_to signed_in_root_path
end
private
def account_session_params
params.require(:account_session).permit(:account_id)
end
end

View File

@@ -0,0 +1,56 @@
class AccountsController < ApplicationController
before_action :set_account, only: [:update]
skip_before_action :require_login, only: [:new, :create]
skip_after_action :verify_authorized, only: [:new, :create]
def new
end
def create
account = Account.create(account_params_for_create)
if account.valid?
user = sign_up(user_params)
if user.valid?
user.account_auths << AccountAuth.create(user: user, account: account, role: "account_manager")
if sign_in(user)
TrackAnalyticsJob.perform_later(user, user.primary_account, :track_guest_sign_up, user_agent: request.user_agent, user_ip: request.remote_ip)
SubmitHubspotFormJob.perform_later(user.email, account.name, i_m_interested_in: user.interested_product_name)
redirect_to signed_in_root_path
else
redirect_to new_session_path, alert: t(".notice")
end
else
redirect_to new_account_path, alert: t(".error")
end
else
redirect_to new_account_path, alert: t(".error")
end
end
def update
authorize @account
@account.update(account_params_for_update)
end
private
def set_account
@account = Current.account
end
def account_params_for_create
{ name: params[:user][:account_name], plan_uid: :me_suite }
end
def account_params_for_update
params.require(:account).permit(:logo)
end
def user_params
params.require(:user).except(:account_name).permit(:email, :password, :first_name, :last_name, :interested_product_name)
end
end

View File

@@ -0,0 +1,106 @@
class AcquiredMediaReleasesController < ApplicationController
include ProjectContext, AcquiredMediaReleaseContext
before_action :set_project, only: [:index, :new, :create]
before_action :set_acquired_media_release, only: [:edit, :update, :destroy]
include ProjectLayout
def index
@acquired_media_releases = filtered_acquired_media_releases.order_by_recent.paginate(page: params[:page])
end
def new
@acquired_media_release = build_acquired_media_release
end
def create
@acquired_media_release = build_acquired_media_release(acquired_media_release_params)
if @acquired_media_release.save
log_create_analytics
SetTagsForReleasableJob.perform_later(@acquired_media_release)
redirect_to [@project, :acquired_media_releases], notice: t(".notice")
else
render :new
end
end
def edit
@project = @acquired_media_release.project
end
def update
@project = @acquired_media_release.project
if @acquired_media_release.update(acquired_media_release_params)
redirect_to [@project, :acquired_media_releases], notice: t(".notice")
else
render :edit
end
end
def destroy
@project = @acquired_media_release.project
@acquired_media_release.destroy
redirect_to [@project, :acquired_media_releases], alert: t(".alert")
end
private
def acquired_media_releases
if @project
policy_scope(@project.acquired_media_releases)
else
policy_scope(AcquiredMediaRelease)
end
end
def acquired_media_release_params
params.require(:acquired_media_release).permit(
:name,
:territory,
:term,
:person_name,
:person_phone,
:person_email,
:person_company,
:person_title,
:person_address_street1,
:person_address_street2,
:person_address_city,
:person_address_state,
:person_address_zip,
:person_address_country,
:contract,
:applicable_medium_id, :applicable_medium_text,
:territory_id, :territory_text,
:term_id, :term_text,
:restriction_id, :restriction_text,
categories: [],
file_infos_attributes: [
:filename,
:content_type,
:byte_size
]
)
end
def build_acquired_media_release(attrs = {})
authorize @project.acquired_media_releases.build(attrs)
end
def filtered_acquired_media_releases
results = acquired_media_releases
if params[:query].present?
results = results.search(params[:query])
end
results
end
def log_create_analytics
TrackAnalyticsJob.perform_later(Current.user, Current.account, :track_create_non_native_release, release_type: AcquiredMediaRelease.to_s, user_agent: request.user_agent, user_ip: request.remote_ip)
end
end

View File

@@ -0,0 +1,73 @@
class Admin::AccountsController < Admin::ApplicationController
before_action :set_account, only: [:show, :edit, :update]
def index
@accounts = filtered_accounts.order_by_name.paginate(page: params[:page])
end
def new
@account = build_account
end
def create
@account = build_account(account_params)
if @account.save
redirect_to account_auths_path({ account_id: @account.id }), notice: t(".notice")
else
render :new
end
end
def show
@videos = filtered_account_videos.order(created_at: :desc, project_id: :desc).paginate(page: params[:page])
end
def edit
end
def update
if @account.update(account_params)
redirect_to admin_accounts_path, notice: t(".notice")
else
render :edit
end
end
private
def set_account
@account = find_account
end
def find_account
authorize accounts.find_by(slug: params[:id])
end
def accounts
policy_scope(Account)
end
def build_account(params = {})
authorize accounts.new(params)
end
def account_params
params.require(:account).permit(:name, :plan_uid)
end
def filtered_accounts
if params[:query].present?
accounts.search(params[:query])
else
accounts
end
end
def filtered_account_videos
if params[:query].present?
@account.videos.search(params[:query])
else
@account.videos
end
end
end

View File

@@ -0,0 +1,18 @@
class Admin::ApplicationController < ActionController::Base
include Oath::ControllerHelpers # Methods for authentication
include Pundit # Methods for authorization
before_action :require_login
include SetCurrentRequestDetails
before_action :require_admin_login
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
private
def require_admin_login
if !Current.user.admin?
redirect_to signed_in_root_url, alert: "You are not authorized to access this"
end
end
end

View File

@@ -0,0 +1,33 @@
class Admin::MasqueradesController < Admin::ApplicationController
before_action :set_user, only: [:create]
skip_before_action :require_admin_login, only: [:destroy]
def create
authorize :masquerade, :create?
session[:admin_id] = current_user.id
sign_in @user
redirect_to signed_in_root_path
end
def destroy
authorize :masquerade, :destroy?
sign_in User.find session[:admin_id]
session.delete(:admin_id)
session.delete(:active_account)
redirect_to admin_users_path
end
private
def set_user
@user = find_user
end
def users
policy_scope User
end
def find_user
authorize users.find(params[:user_id])
end
end

View File

@@ -0,0 +1,90 @@
class Admin::UsersController < Admin::ApplicationController
before_action :set_user, only: [:edit, :update]
def index
@users = filtered_users.order("email")
end
def new
@user = build_user
@accounts = accounts
end
def create
auth_params = user_create_params.slice(:account_id, :role)
@user = authorize sign_up(user_create_params.except(:account_id, :role))
if @user.valid?
account = accounts.find(auth_params[:account_id])
@user.account_auths << AccountAuth.create(user: @user, account: account, role: auth_params[:role])
UserMailer.welcome(@user, account).deliver_later
redirect_to admin_users_path, notice: t(".notice")
else
@accounts = accounts
render :new
end
end
def edit
@accounts = accounts
end
def update
set_user_password
if @user.update(user_update_params.except(:password))
redirect_to admin_users_path, notice: t(".notice")
else
@accounts = accounts
render :edit
end
end
def destroy
@user = authorize User.find(params[:id])
@user.destroy
redirect_to admin_users_path, alert: t(".alert")
end
private
def set_user
@user = find_user
end
def accounts
policy_scope Account
end
def users
policy_scope User
end
def find_user
authorize users.find(params[:id])
end
def build_user(params = {})
authorize users.new(params)
end
def user_create_params
params.require(:user).permit(:email, :password, :admin, :account_id, :role)
end
def user_update_params
params.require(:user).permit(:email, :admin, :password)
end
def set_user_password
if user_update_params[:password].present?
Oath::Services::PasswordReset.new(@user, user_update_params[:password]).perform
end
end
def filtered_users
params[:query].present? ? users.search(params[:query]) : users
end
end

View File

@@ -0,0 +1,11 @@
class Api::AcquiredMediaReleasesController < Api::ReleasesController
deserializable_resource :acquired_media_release, only: [:create, :update]
def model_name
"acquired_media_release"
end
def attributes_for_index
[:name]
end
end

View File

@@ -0,0 +1,48 @@
class Api::ApiController < ActionController::Base
skip_before_action :verify_authenticity_token
include Knock::Authenticable
include Pundit
rescue_from Exception, :with => :return_error
before_action :authenticate_user
before_action do
Current.user = current_user if current_user.present?
end
def pundit_user
UserContext.new(Current.user, Current.account)
end
# Catch exception and return JSON-formatted error
def return_error(exception)
raise exception if Rails.env.test?
logger.error "==Handled======="
logger.error exception.message
logger.error exception.backtrace.join("\n")
logger.error "==Handled======="
case exception.class
when ActiveRecord::RecordNotFound
@status = 404
@message = 'Record not found'
when ActiveRecord::RecordInvalid
@status = 422
@message = 'Record invalid'
when ArgumentError
@status = 400
@message = 'Argument Error'
else
@status = 500
@message = 'Internal Error'
end
# for some reason render json_errors is not working
# simulating JSON API support
render json: {
errors: [{
status: @status.to_s,
title: @message
}]
}
end
end

View File

@@ -0,0 +1,23 @@
class Api::AppearanceReleasesController < Api::ReleasesController
deserializable_resource :appearance_release, only: [:create]
def model_name
"appearance_release"
end
def attributes_for_index
[:person_name]
end
def handle_attachments(release, _)
photo = release_create_params[:person_photo]
photo[:io] = StringIO.new(Base64.decode64(photo[:io]))
release.person_photo.attach(io: photo[:io], filename: photo[:filename])
guardian_photo = release_create_params[:guardian_photo]
if guardian_photo
guardian_photo[:io] = StringIO.new(Base64.decode64(guardian_photo[:io]))
release.guardian_photo.attach(io: guardian_photo[:io], filename: guardian_photo[:filename])
end
end
end

View File

@@ -0,0 +1,55 @@
# frozen_string_literal: true
class Api::BroadcastsController < Api::ApiController
deserializable_resource :broadcast, only: [:update]
include ProjectContext
before_action :set_project
before_action :set_broadcast, only: [:show, :update]
def index
render jsonapi: broadcasts, class: { Broadcast: SerializableBroadcast }
end
def show
render jsonapi:
@broadcast,
class: {
Broadcast: SerializableBroadcast,
"ActiveStorage::Attachment".to_sym => ActiveStorage::SerializableAttachment
},
include: [:files]
end
def update
file_params.each do |file|
file[:io] = StringIO.new(Base64.decode64(file[:io]))
@broadcast.files.attach(io: file[:io], filename: file[:filename])
end
@broadcast.save!
render jsonapi:
@broadcast,
class: {
Broadcast: SerializableBroadcast,
"ActiveStorage::Attachment".to_sym => ActiveStorage::SerializableAttachment
},
include: [:files]
end
private
def file_params
broadcast_params = params.require(:broadcast).permit!
broadcast_params[:files]
end
def broadcasts
policy_scope(@project.broadcasts.where(status: %w[created idle]))
end
def set_broadcast
@broadcast = authorize policy_scope(@project.broadcasts).find(params[:id])
end
end

View File

@@ -0,0 +1,63 @@
class Api::ContractTemplatesController < Api::ApiController
include ProjectContext
before_action :set_project, only: [:index]
before_action :set_contract_template, only: [:show]
def index
render jsonapi: contract_templates, class: { "ContractTemplate": index_serializable }
end
def show
handle_response(@contract_template)
end
def handle_response(contract_template, status = :ok)
mapping = {
"ContractTemplate": show_serializable,
"ActiveStorage::Attachment".to_sym => ActiveStorage::SerializableAttachment,
}
render jsonapi: contract_template,
status: status,
class: mapping
end
def attributes_for_index
[:name]
end
def index_serializable
name = "contract_template"
attributes_to_send = attributes_for_index
Class.new(JSONAPI::Serializable::Resource) do
type name
attributes_to_send.each do |attr|
attribute attr.to_sym
end
end
end
def show_serializable
name = "contract_template"
Class.new(JSONAPI::Serializable::Resource) do
type name
ContractTemplate.new.attributes.keys.each do |attr|
attribute attr.to_sym
end
end
end
private
def contract_templates
date = Date.parse(params.fetch(:updated_since, Date.new.to_s))
policy_scope(ContractTemplate.where(project_id: @project.id, updated_at: date..Float::INFINITY))
end
def set_contract_template
@contract_template = ContractTemplate.find(params[:id])
end
end

View File

@@ -0,0 +1,11 @@
class Api::LocationReleasesController < Api::ReleasesController
deserializable_resource :location_release, only: [:create, :update]
def model_name
"location_release"
end
def attributes_for_index
[:name]
end
end

View File

@@ -0,0 +1,11 @@
class Api::MaterialReleasesController < Api::ReleasesController
deserializable_resource :material_release, only: [:create, :update]
def model_name
"material_release"
end
def attributes_for_index
[:name]
end
end

View File

@@ -0,0 +1,35 @@
class Api::NotesController < Api::ApiController
before_action :set_release
deserializable_resource :note, only: [:create]
def index
render jsonapi: @release.notes, class: { Note: SerializableNote }
end
def create
note = @release.notes.new(note_create_params)
note.user_id = current_user.id
note.email = current_user.email
note.save!
render jsonapi: note, class: { Note: SerializableNote }, status: :created
end
private
def model_name
request.path.match(/(\w+_releases)/).captures.first.singularize
end
def model_constant
model_name.camelize.constantize
end
def set_release
@release = authorize model_constant.find(params["#{model_name}_id"])
end
def note_create_params
parameters = params.require(:note).permit!
parameters.slice(:content)
end
end

View File

@@ -0,0 +1,5 @@
class Api::ProfilesController < Api::ApiController
def show
render jsonapi: current_user
end
end

View File

@@ -0,0 +1,6 @@
class Api::ProjectsController < Api::ApiController
def index
projects = Current.user.accessible_projects_for(Current.account)
render jsonapi: projects
end
end

View File

@@ -0,0 +1,182 @@
class Api::ReleasesController < Api::ApiController
include ProjectContext
include CreateReleasableJobs
before_action :set_project, only: [:index]
before_action :set_release, only: [:show, :update]
before_action :set_template_and_project, only: [:create]
def index
render jsonapi: releases, class: { "#{model_name.camelize}": index_serializable }
end
def show
handle_response(@release)
end
def create
params_without_photo = release_create_params.except(:photos, :person_photo)
release = filtered_releases.new(params_without_photo)
release.project_id = @project.id
release.contract_template_id = @contract_template.id
handle_attachments(release, release_create_params[:photos])
release.save!(context: :native)
log_create_analytics
after_create(release)
handle_response(release, :created)
end
def update
if model_name == "acquired_media_release"
authorize @release, :update_file_infos?
@release.attributes = release_create_params
@release.save!
else
authorize @release, :update_photos?
handle_attachments(@release, release_create_params[:photos])
@release.save!
end
handle_response(@release)
end
def handle_response(release, status = :ok)
if model_name == "acquired_media_release"
mapping = {
"#{model_name.camelize}": SerializableAcquiredMediaRelease,
FileInfo: SerializableFileInfo
}
render jsonapi: release,
status: status,
class: mapping,
include: [:file_infos]
else
mapping = {
"#{model_name.camelize}": show_serializable,
"ActiveStorage::Attachment".to_sym => ActiveStorage::SerializableAttachment,
}
render jsonapi: release,
status: status,
class: mapping,
include: [:photos, :guardian_photos]
end
end
def model_name
raise "Please specify model name underscored and lowercase e.g. appearance_release"
end
def model_constant
model_name.camelize.constantize
end
def attributes_for_index
raise "Please specify attributes for index"
end
def index_serializable
name = model_name
attributes_to_send = attributes_for_index
Class.new(JSONAPI::Serializable::Resource) do
type name
attributes_to_send.each do |attr|
attribute attr.to_sym
end
end
end
def show_serializable
name = model_name
constant = model_constant
Class.new(JSONAPI::Serializable::Resource) do
type name
constant.new.attributes.keys.each do |attr|
attribute attr.to_sym
end
if ["appearance_release", "talent_release"].include?(name)
has_many :guardian_photos do
data do
[@object.guardian_photo.try(:attachment)].compact
end
meta do
{ count: @object.try(:guardian_photo).try(:attached?) ? 1 : 0 }
end
end
end
unless name == "acquired_media_release"
has_many :photos do
if name == "appearance_release"
data do
[@object.photo.attachment]
end
end
meta do
{ count: @object.photos.size }
end
end
end
end
end
def create_deserializable
constant = model_constant
Class.new(JSONAPI::Deserializable::Resource) do
constant.new.attributes.keys.except(:created_at, :updated_at, :id, :user_id).each do |attr|
attribute attr.to_sym
end
end
end
def handle_attachments(release, params_photos)
photos = params_photos || []
photos.each do |photo|
photo[:io] = StringIO.new(Base64.decode64(photo[:io]))
release.photos.attach(io: photo[:io], filename: photo[:filename])
end
end
private
def releases
date = Date.parse(params.fetch(:updated_since, Date.new.to_s))
table = model_constant.table_name
policy_scope(model_constant.where(["#{table}.project_id = ? and #{table}.updated_at >= ?", @project.id, date]))
end
def set_release
@release = model_constant.find(params[:id])
end
def set_template_and_project
@contract_template = ContractTemplate.find(params[:contract_template_id])
@project = @contract_template.project
end
def filtered_releases
if @project.present?
policy_scope(@project.public_send(model_name.pluralize))
else
releases
end
end
def release_create_params
parameters = params.require(model_name).permit!
keys = model_constant.new.attributes.keys + [:guardian_photo, :person_photo, :photos, :signature, :signature_base64, :file_infos_attributes]
parameters[:signature_base64] = parameters[:signature]
parameters = parameters.slice(*keys).except(:created_at, :updated_at, :id, :user_id, :signature)
parameters
end
def log_create_analytics
TrackAnalyticsJob.perform_later(Current.user, Current.account, :track_create_native_release, release_type: model_constant.to_s, account: @account, user_agent: request.user_agent, user_ip: request.remote_ip, application: :ios)
end
end

View File

@@ -0,0 +1,78 @@
# frozen_string_literal: true
class Api::SyncController < Api::ApiController
def index
accessible_projects = current_user.accessible_projects_for(Current.account)
@accounts = filter(current_user.accounts)
@projects = filter(Project).all
@contract_templates = filter(ContractTemplate.where(project: accessible_projects)).all
@acquired_media_releases = (AcquiredMediaRelease.where(project: accessible_projects))
@appearance_releases = (AppearanceRelease.where(project: accessible_projects))
@location_releases = (LocationRelease.where(project: accessible_projects))
@material_releases = (MaterialRelease.where(project: accessible_projects))
@talent_releases = (TalentRelease.where(project: accessible_projects))
@notes = notes_query(Note.where(notable: @appearance_releases + @location_releases + @material_releases + @talent_releases + @acquired_media_releases ))
render json: {
data: {
accounts: @accounts,
projects: @projects,
contract_templates: @contract_templates,
acquired_media_releases: releases_query(@acquired_media_releases),
appearance_releases: releases_query(@appearance_releases),
location_releases: releases_query(@location_releases),
material_releases: releases_query(@material_releases),
talent_releases: releases_query(@talent_releases),
notes: @notes
}
}
end
private
def releases_query(release)
filter(release).all.map { |release| release_json(release) }
end
def notes_query(notes)
filter(notes).all.map do |note|
json = note.as_json
json[:attributes][:email] = note.email
json
end
end
def release_json(release)
json = release.as_json
unless release.model_name.to_s == "AcquiredMediaRelease"
json[:attributes][:photos] = release.photos.map do |photo|
build_photo_object photo
end
end
if release.respond_to?(:guardian_photo)
photo = release.guardian_photo
json[:attributes][:guardian_photo] = photo.attached? ? build_photo_object(photo) : nil
end
json
end
def filter(relation)
policy_scope(relation)
end
def build_photo_object(photo)
{
id: photo.id.to_s,
type: 'active_storage_attachment',
attributes: {
filename: photo.filename.to_s,
content_type: photo.content_type,
url: Rails.application.routes.url_helpers.rails_blob_url(photo, host: AppHost.new.domain_with_port),
thumbnail_url: Rails.application.routes.url_helpers.rails_representation_url(photo.variant(resize: '150x150'), host: AppHost.new.domain_with_port)
}
}
end
end

View File

@@ -0,0 +1,21 @@
class Api::TalentReleasesController < Api::ReleasesController
deserializable_resource :talent_release, only: [:create, :update]
def model_name
"talent_release"
end
def attributes_for_index
[:person_name]
end
def handle_attachments(release, _)
super
guardian_photo = release_create_params[:guardian_photo]
if guardian_photo
guardian_photo[:io] = StringIO.new(Base64.decode64(guardian_photo[:io]))
release.guardian_photo.attach(io: guardian_photo[:io], filename: guardian_photo[:filename])
end
end
end

View File

@@ -0,0 +1,36 @@
class Api::UserTokenController < Knock::AuthTokenController
skip_before_action :verify_authenticity_token
rescue_from Exception, :with => :return_error
# Catch exception and return JSON-formatted error
def return_error(exception)
logger.error "==Handled======="
logger.error exception.message
logger.error exception.backtrace.join("\n")
logger.error "==Handled======="
case exception
when ActiveRecord::RecordNotFound
@status = 404
@message = 'Record not found'
when ActiveRecord::RecordInvalid
@status = 422
@message = 'Record invalid'
when ArgumentError
@status = 400
@message = 'Argument Error'
else
@status = 500
@message = 'Internal Error'
end
# for some reason render json_errors is not working
# simulating JSON API support
render json: {
errors: [{
status: @status.to_s,
title: @message
}]
}
end
end

View File

@@ -0,0 +1,89 @@
# frozen_string_literal: true
class AppearanceReleaseImportsController < ApplicationController
include AppearanceReleaseContext
include ProjectContext
include CreateReleasableJobs
before_action :set_project, only: [:create]
include ProjectLayout
def create
authorize AppearanceRelease
@failed_files = []
attachments = appearance_release_params
if attachments.nil?
alert_message = t 'appearance_releases.create.no_attachments'
else
attachments.each do |attachment|
create_imported_appearance_release attachment
end
end
unless @failed_files.empty?
alert_message = t 'appearance_releases.create.failed_import'
alert_message += '<br><ul>'
@failed_files.each { |file_name| alert_message += "<li>#{file_name}</li>" }
alert_message += '</ul>'
end
redirect_to [@project, :appearance_releases], alert: alert_message
end
private
def appearance_releases
if @project
policy_scope(@project.appearance_releases)
else
super
end
end
def appearance_release_params
params.require(:attachments)
end
def build_appearance_release(params = {})
authorize appearance_releases.build(params)
end
def log_create_analytics
TrackAnalyticsJob.perform_later(Current.user, Current.account, :track_create_non_native_release, release_type: AppearanceRelease.to_s, user_agent: request.user_agent, user_ip: request.remote_ip)
end
def create_imported_appearance_release(attachment)
blob = ActiveStorage::Blob.find_signed(attachment)
return if blob.nil?
extension = blob.filename.extension_with_delimiter
unless AppearanceRelease.acceptable_import_file_extensions.include? extension
blob.purge
@failed_files << blob.filename
return
end
random_contract_no = AppearanceRelease.random_contract_number.to_s
appearance_release_params = {
person_last_name: random_contract_no
}
if blob.image?
appearance_release_params[:person_photo] = attachment
appearance_release_params[:person_first_name] = I18n.t('appearance_releases.shared.imported_appearance_release_headshot_name')
elsif extension == '.pdf'
appearance_release_params[:contract] = attachment
appearance_release_params[:person_first_name] = I18n.t('appearance_releases.shared.imported_appearance_release_contract_name')
end
appearance_release = build_appearance_release(appearance_release_params)
if appearance_release.save(context: :non_native)
log_create_analytics
after_create appearance_release
else
@failed_files << blob.filename
end
end
end

View File

@@ -0,0 +1,100 @@
class AppearanceReleasesController < ApplicationController
include ProjectContext, AppearanceReleaseContext
before_action :set_project, only: [:index, :new, :create]
before_action :set_appearance_release, only: [:edit, :update, :destroy]
include ProjectLayout
def index
@appearance_releases = filtered_appearance_releases.order_by_recent.paginate(page: params[:page])
end
def new
@appearance_release = build_appearance_release
end
def create
@appearance_release = build_appearance_release(appearance_release_params)
if @appearance_release.save(context: :non_native)
log_create_analytics
AddHeadshotCollectionUidToProjectJob.perform_later(@project)
SetTagsForReleasableJob.perform_later(@appearance_release)
redirect_to [@project, :appearance_releases], notice: "The release has been imported. #{link_to_import_another}"
else
render :new
end
end
def edit
@project = @appearance_release.project
end
def update
@project = @appearance_release.project
@appearance_release.attributes = appearance_release_params
if @appearance_release.save(context: :non_native)
AddHeadshotCollectionUidToProjectJob.perform_later(@project)
redirect_to [@project, :appearance_releases], notice: "The release has been updated"
else
render :edit
end
end
def destroy
@project = @appearance_release.project
if @appearance_release.destroy
AddHeadshotCollectionUidToProjectJob.perform_later(@appearance_release.project)
redirect_to [@project, :appearance_releases], alert: "The release has been deleted"
end
end
private
def appearance_releases
if @project
policy_scope(@project.appearance_releases)
else
super
end
end
def filtered_appearance_releases
results = case params[:type_filter]
when 'complete'
appearance_releases.complete
when 'incomplete'
appearance_releases.incomplete
else
appearance_releases
end
results = results.search(params[:query]) if params[:query].present?
results
end
def appearance_release_params
params.require(:appearance_release).permit(:contract, :guardian_address, :guardian_first_name, :guardian_last_name, :guardian_phone, :guardian_photo, :minor,
:person_address, :person_first_name, :person_last_name, :person_phone, :person_email, :person_photo,
:applicable_medium_id, :applicable_medium_text,
:territory_id, :territory_text,
:term_id, :term_text, :person_date_of_birth,
:restriction_id, :restriction_text)
end
def build_appearance_release(params = {})
authorize appearance_releases.build(params)
end
def link_to_import_another
view_context.link_to "Import Another", [:new, @project, :appearance_release]
end
def log_create_analytics
TrackAnalyticsJob.perform_later(Current.user, Current.account, :track_create_non_native_release, release_type: AppearanceRelease.to_s, user_agent: request.user_agent, user_ip: request.remote_ip)
end
end

View File

@@ -0,0 +1,70 @@
class ApplicationController < ActionController::Base
include Oath::ControllerHelpers # Methods for authentication
include Pundit # Methods for authorization
include RememberMe::Controller
before_action :disable_browser_page_caching
before_action :set_locale
before_action :require_login
before_action :set_raven_context
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
include SetCurrentRequestDetails
before_action :redirect_accountless
private
def require_login
if !masquerading? && remembered_user = remember("user")
sign_in(remembered_user)
end
super
end
def redirect_accountless
if Current.user && Current.account.nil?
redirect_to accountless_user_path
end
end
def signed_in_as_admin?
signed_in? && current_user.admin?
end
helper_method :signed_in_as_admin?
# Ensure that all url helpers include the current locale
def default_url_options
super.merge(locale: I18n.locale) # Use merge to avoid clobbering any options set during config
end
# Set the locale for the current request
def set_locale
I18n.locale = params[:locale] || request.env["rack.locale"] || I18n.default_locale
end
# Run authorization against Current.user which will ensure it works in async jobs and channels as well
# All authorization depends on the Current.account as users will have a different role for each
def pundit_user
UserContext.new(Current.user, Current.account)
end
# Set the context for Sentry exception handling service
def set_raven_context
account_id = (session[:active_account] || try(:current_user).try(:accounts).try(:first))
Raven.user_context(id: current_user.id, account_id: account_id) if signed_in?
Raven.extra_context(params: params.to_unsafe_h, url: request.url)
end
def disable_browser_page_caching
response.headers["Cache-Control"] = "no-cache, no-store"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
end
def masquerading?
session[:admin_id].present?
end
helper_method :masquerading?
end

View File

@@ -0,0 +1,34 @@
class AudioReportsController < ApplicationController
def show
report = build_report(params[:type])
respond_to do |format|
format.xlsx { send_data(report.to_xls, download_attributes_xls(report)) }
end
end
private
def video
authorize policy_scope(Video).find(params[:video_id])
end
def build_report(type)
case type
when "big"
authorize ExcelReports::AudioReports::BrayInnovationGroupMusicCueReport.new(video)
when "nat_geo"
authorize ExcelReports::AudioReports::NatGeoMusicCueSheet.new(video)
when "nat_geo-original"
authorize ExcelReports::AudioReports::NatGeoOriginalMusicLog.new(video)
else
authorize ExcelReports::AudioReports::DiscoveryMusicCueReport.new(video)
end
end
def download_attributes_xls(report)
{
filename: report.filename,
type: Mime[:xlsx]
}
end
end

View File

@@ -0,0 +1,58 @@
class BookmarksController < ApplicationController
before_action :set_video, except: [:destroy, :edit, :update]
before_action :set_bookmark, only: [:destroy, :edit, :update]
def new
@bookmark = @video.bookmarks.build(bookmark_params)
end
def create
@bookmark = @video.bookmarks.build(bookmark_params)
@bookmark.save
@bookmarks = @video.bookmarks
end
def edit
end
def update
@video = @bookmark.video
@bookmark.update(bookmark_params)
@bookmarks = @video.bookmarks
end
def destroy
@video = @bookmark.video
@bookmark.destroy
@bookmarks = @video.bookmarks
end
private
def bookmark_params
params.require(:bookmark).permit(:notes, :time_elapsed, :category)
end
def bookmarks
if @video
policy_scope(@video.bookmarks)
else
policy_scope(Bookmark)
end
end
def videos
policy_scope(Video)
end
def set_bookmark
@bookmark = authorize bookmarks.find(params[:id])
end
def set_video
@video = authorize videos.find(params[:video_id])
end
end

View File

@@ -0,0 +1,109 @@
class BroadcastsController < ApplicationController
layout "project"
before_action :set_project
before_action :build_broadcast, only: [:new, :create]
before_action :set_broadcast, only: [:show, :destroy, :update]
before_action :set_multi_view_broadcasts, only: [:show]
def index
@broadcasts = filtered_broadcasts.order_by_recent.paginate(page: params[:page])
end
def new
end
def create
@broadcast.attributes = broadcast_params
if @broadcast.save
log_create_analytics
redirect_to [@project, :broadcasts], notice: t(".notice")
else
render :new
end
end
def update
@broadcast.update(broadcast_params)
@files = @broadcast.files.order("created_at DESC").paginate(page: 1)
pagination_content = ApplicationController.render html: helpers.will_paginate(@files, params: { active_tab: params[:active_tab] })
BroadcastsChannel.file_upload_updates(@broadcast, @files, pagination_content)
end
def show
@conference_url = url_for [@broadcast.project, @broadcast, :zoom_meeting]
@recordings = @broadcast.broadcast_recordings.order_by_recent.paginate(page: params[:page])
@files = @broadcast.files.order("created_at DESC").paginate(page: params[:page])
render layout: 'application'
end
def destroy
if @broadcast.destroy
redirect_to [@project, :broadcasts], alert: t(".alert")
else
redirect_to [@project, :broadcasts], alert: t(".api_error")
end
end
private
def broadcast_params
params.require(:broadcast).permit(:name, files: [])
end
def set_project
@project = policy_scope(Project).find(params[:project_id])
end
def set_broadcast
@broadcast = authorize policy_scope(Broadcast).find(params[:id])
end
def build_broadcast
@broadcast = authorize @project.broadcasts.build
end
def broadcasts
authorize policy_scope(@project.broadcasts)
end
def set_multi_view_broadcasts
authorized_broadcasts = authorize policy_scope(Broadcast).where(id: params[:multi_view_ids]).order_by_recent
@multi_view_broadcasts = authorized_broadcasts.map { |b| MultiViewBroadcast.new(b, params[:multi_view_ids]) }
end
def filtered_broadcasts
results = broadcasts
if params[:query].present?
results = results.search(params[:query])
end
results
end
def log_create_analytics
TrackAnalyticsJob.perform_later(Current.user, Current.account, :track_create_live_stream, user_agent: request.user_agent, user_ip: request.remote_ip)
end
class MultiViewBroadcast
include Rails.application.routes.url_helpers
delegate_missing_to :@broadcast
def initialize(broadcast, multi_view_ids)
@broadcast = broadcast
@multi_view_ids = multi_view_ids
end
def url
project_broadcast_path(@broadcast.project, @broadcast, multi_view_ids: @multi_view_ids, locale: I18n.locale)
end
def uid
id
end
end
end

View File

@@ -0,0 +1,40 @@
class BulkTaggingsController < ApplicationController
before_action :set_project
before_action :set_releasables
before_action :authorize_bulk_taggings, only: [:new, :create]
def new
@releasable_ids = releasable_params[:ids]
end
def create
@releasables.each do |releasable|
releasable.tag_list.add(params[:name])
releasable.save
end
all_releasables
end
private
def releasable_params
{ type: params[:releasable_name].classify.constantize, ids: JSON.parse(params[:releasable_ids]) }
end
def set_releasables
@releasables = releasable_params[:type].where(id: releasable_params[:ids])
end
def set_project
@project = policy_scope(Project).find(params[:project_id])
end
def authorize_bulk_taggings
authorize @releasables.first.tags
end
def all_releasables
@all_releasables = policy_scope(releasable_params[:type].where(project: @project)).order_by_recent.paginate(page: params[:page])
end
end

View File

View File

@@ -0,0 +1,13 @@
module AcquiredMediaReleaseContext
extend ActiveSupport::Concern
def acquired_media_releases
policy_scope(AcquiredMediaRelease)
end
def set_acquired_media_release
acquired_media_release_id = params[:acquired_media_release_id] || params[:id]
@acquired_media_release = authorize acquired_media_releases.find(acquired_media_release_id)
end
end

View File

@@ -0,0 +1,13 @@
module AppearanceReleaseContext
extend ActiveSupport::Concern
def appearance_releases
policy_scope(AppearanceRelease)
end
def set_appearance_release
appearance_release_id = params[:appearance_release_id] || params[:id]
@appearance_release = authorize appearance_releases.find(appearance_release_id)
end
end

View File

@@ -0,0 +1,21 @@
# frozen_string_literal: true
module CreateReleasableJobs
extend ActiveSupport::Concern
def after_create(releasable)
if headshots?(releasable)
AddHeadshotCollectionUidToProjectJob.perform_later(releasable.project)
end
SetTagsForReleasableJob.perform_later(releasable)
if releasable.contract_template.present?
AttachContractToReleasableJob.perform_later(releasable)
end
end
private
def headshots?(releasable)
[AppearanceRelease, TalentRelease].include?(releasable.class)
end
end

View File

@@ -0,0 +1,13 @@
module LocationReleaseContext
extend ActiveSupport::Concern
def location_releases
policy_scope(LocationRelease)
end
def set_location_release
location_release_id = params[:location_release_id] || params[:id]
@location_release = authorize location_releases.find(location_release_id)
end
end

View File

@@ -0,0 +1,13 @@
module MusicReleaseContext
extend ActiveSupport::Concern
def music_releases
policy_scope(MusicRelease)
end
def set_music_release
music_release_id = params[:music_release_id] || params[:id]
@music_release = authorize music_releases.find(music_release_id)
end
end

View File

@@ -0,0 +1,13 @@
module ProjectContext
extend ActiveSupport::Concern
def projects
policy_scope(Project)
end
def set_project
project_id = params[:project_id]
@project = projects.find(project_id)
end
end

View File

@@ -0,0 +1,15 @@
module ProjectLayout
extend ActiveSupport::Concern
included do
layout "project"
breadcrumb -> { t("shared.files") }, -> { project_for_layout }, match: :exact
breadcrumb -> { controller_name.titleize }, -> { [project_for_layout, controller_name] }, match: :exact
breadcrumb -> { action_name.titleize }, :url_for, only: [:new, :edit]
def project_for_layout
@project || instance_variable_get("@#{controller_name.singularize}").project
end
end
end

View File

@@ -0,0 +1,10 @@
module SetCurrentRequestDetails
extend ActiveSupport::Concern
included do
before_action do
Current.user = current_user if signed_in?
Current.account = Account.find(session[:active_account]) if session[:active_account]
end
end
end

View File

@@ -0,0 +1,13 @@
module TalentReleaseContext
extend ActiveSupport::Concern
def talent_releases
policy_scope(TalentRelease)
end
def set_talent_release
talent_release_id = params[:talent_release_id] || params[:id]
@talent_release = authorize talent_releases.find(talent_release_id)
end
end

View File

@@ -0,0 +1,34 @@
class ContractDownloadsController < ApplicationController
include ProjectContext
before_action :set_project, only: [:index, :create]
include ProjectLayout
def create
authorize policy_scope(Download).create
fetch_releases
download = @project.downloads.create!(release_type: params[:release_type])
other_downloads_in_progress = @project.downloads.unfinished_desc_order.offset(1)
if other_downloads_in_progress.any?
in_progress_downloads_details = render_to_string "_other_pending_downloads", locals: { downloads: other_downloads_in_progress, release_type: params[:release_type] }, :layout => false
ProjectsChannel.broadcast_download_generation_update(download, in_progress_downloads_details)
else
ProjectsChannel.broadcast_download_generation_update(download, I18n.t("contract_downloads.download.pending", release_type: params[:release_type].titleize))
end
GenerateContractsZipJob.perform_later(@project, download, params[:release_type], @releases.ids)
end
private
def fetch_releases
@releases = policy_scope(@project.public_send(releases))
end
def releases
params[:release_type].constantize.model_name.plural
end
end

View File

@@ -0,0 +1,69 @@
# frozen_string_literal: true
class ContractTemplates::BlankContractsController < ApplicationController
before_action :set_contract_template
def show
send_file contract.to_pdf, download_attributes
end
def new
authorize BlankContract
render 'blank_contracts/new'
end
def create
if number_of_copies_valid?
send_file contract.to_pdf, download_attributes
else
authorize BlankContract
redirect_back fallback_location: [:new, @contract_template, :blank_contracts], notice: t('.number_of_copies_invalid_notice')
end
end
private
def number_of_copies_valid?
number_of_copies = params[:number_of_copies].to_i
number_of_copies.positive?
rescue StandardError
false
end
def contract_templates
policy_scope(ContractTemplate)
end
def set_contract_template
@contract_template = contract_templates.find(params[:contract_template_id])
end
def releasable
create_releasable_instance
end
def create_releasable_instance
template_release_type = @contract_template[:release_type]
releasable = "#{template_release_type}_release".classify.safe_constantize.new
releasable.contract_template = @contract_template
releasable.project_id = @contract_template.project_id
releasable
end
def contract
authorize BlankContract.new(releasable, params[:number_of_copies])
end
def download_attributes
{
disposition: 'inline',
filename: contract.filename,
type: 'application/pdf'
}
end
def render_sample_html
# NOTE: For development purposes, this contract renders with the current locale, not the locale of the release itself
render contract.render_attributes
end
end

View File

@@ -0,0 +1,28 @@
class ContractTemplates::QrCodesController < ApplicationController
before_action :set_contract_template
def show
send_file qr_code.to_png, download_attributes
end
private
def contract_templates
policy_scope(ContractTemplate)
end
def set_contract_template
@contract_template = contract_templates.find(params[:contract_template_id])
end
def qr_code
authorize QrCode.build_from_contract_template(@contract_template)
end
def download_attributes
{
filename: qr_code.filename,
type: "image/png",
}
end
end

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