diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d1c2c0d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+# 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 the default SQLite database.
+/db/*.sqlite3
+/db/*.sqlite3-journal
+
+# Ignore all logfiles and tempfiles.
+/log/*
+!/log/.keep
+/tmp
+/.idea
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..20f921a
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,80 @@
+source 'https://rubygems.org'
+
+ruby '~> 2.3.1'
+# For RVM autoselect:
+#ruby=ruby-2.3.1
+#ruby-gemset=pintube
+
+
+# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
+gem 'rails', '4.2.6'
+# Use sqlite3 as the database for Active Record
+gem 'sqlite3'
+# Use SCSS for stylesheets
+gem 'sass-rails', '~> 5.0'
+# Use Uglifier as compressor for JavaScript assets
+gem 'uglifier', '>= 1.3.0'
+# Use CoffeeScript for .coffee assets and views
+# gem 'coffee-rails', '~> 4.1.0'
+# See https://github.com/rails/execjs#readme for more supported runtimes
+# gem 'therubyracer', platforms: :ruby
+
+# Use jquery as the JavaScript library
+gem 'jquery-rails'
+# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
+gem 'turbolinks'
+# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
+# gem 'jbuilder', '~> 2.0'
+# bundle exec rake doc:rails generates the API under doc/api.
+# gem 'sdoc', '~> 0.4.0', group: :doc
+
+# Use ActiveModel has_secure_password
+# gem 'bcrypt', '~> 3.1.7'
+
+# Use Unicorn as the app server
+gem 'unicorn'
+
+# Use Capistrano for deployment
+# gem 'capistrano-rails', group: :development
+
+# template language
+gem 'haml-rails'
+
+# bootstrap integration (I thought I would use it more actually. I would probably remove it if the product was final)
+gem 'twitter-bootstrap-rails'
+
+gem 'rest-client'
+
+group :development, :test do
+ # Call 'byebug' anywhere in the code to stop execution and get a debugger console
+ gem 'byebug'
+
+ gem 'table_print'
+ gem 'awesome_print'
+end
+
+group :test do
+ # gem 'minitest-reporters'
+ gem "spring-commands-testunit"
+
+ gem 'shoulda'
+ gem 'shoulda-matchers'
+
+ gem 'capybara'
+ gem 'launchy' # automatically opens saved file by capybara
+ gem 'capybara-webkit'
+end
+
+group :development do
+ # Access an IRB console on exception pages or by using <%= console %> in views
+ # gem 'web-console', '~> 2.0'
+
+ gem 'better_errors' # this is better than web-console imo
+ gem 'binding_of_caller'
+
+ gem 'rails-footnotes'
+
+ # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
+ gem 'spring'
+end
+
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..dd47e90
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,228 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ actionmailer (4.2.6)
+ actionpack (= 4.2.6)
+ actionview (= 4.2.6)
+ activejob (= 4.2.6)
+ mail (~> 2.5, >= 2.5.4)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
+ actionpack (4.2.6)
+ actionview (= 4.2.6)
+ activesupport (= 4.2.6)
+ rack (~> 1.6)
+ rack-test (~> 0.6.2)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
+ actionview (4.2.6)
+ activesupport (= 4.2.6)
+ builder (~> 3.1)
+ erubis (~> 2.7.0)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
+ activejob (4.2.6)
+ activesupport (= 4.2.6)
+ globalid (>= 0.3.0)
+ activemodel (4.2.6)
+ activesupport (= 4.2.6)
+ builder (~> 3.1)
+ activerecord (4.2.6)
+ activemodel (= 4.2.6)
+ activesupport (= 4.2.6)
+ arel (~> 6.0)
+ activesupport (4.2.6)
+ i18n (~> 0.7)
+ json (~> 1.7, >= 1.7.7)
+ minitest (~> 5.1)
+ thread_safe (~> 0.3, >= 0.3.4)
+ tzinfo (~> 1.1)
+ addressable (2.4.0)
+ arel (6.0.3)
+ awesome_print (1.6.1)
+ better_errors (2.1.1)
+ coderay (>= 1.0.0)
+ erubis (>= 2.6.6)
+ rack (>= 0.9.0)
+ binding_of_caller (0.7.2)
+ debug_inspector (>= 0.0.1)
+ builder (3.2.2)
+ byebug (9.0.5)
+ capybara (2.7.1)
+ addressable
+ mime-types (>= 1.16)
+ nokogiri (>= 1.3.3)
+ rack (>= 1.0.0)
+ rack-test (>= 0.5.4)
+ xpath (~> 2.0)
+ capybara-webkit (1.11.1)
+ capybara (>= 2.3.0, < 2.8.0)
+ json
+ coderay (1.1.1)
+ coffee-rails (4.1.1)
+ coffee-script (>= 2.2.0)
+ railties (>= 4.0.0, < 5.1.x)
+ coffee-script (2.4.1)
+ coffee-script-source
+ execjs
+ coffee-script-source (1.10.0)
+ commonjs (0.2.7)
+ concurrent-ruby (1.0.2)
+ debug_inspector (0.0.2)
+ erubis (2.7.0)
+ execjs (2.7.0)
+ globalid (0.3.6)
+ activesupport (>= 4.1.0)
+ haml (4.0.7)
+ tilt
+ haml-rails (0.9.0)
+ actionpack (>= 4.0.1)
+ activesupport (>= 4.0.1)
+ haml (>= 4.0.6, < 5.0)
+ html2haml (>= 1.0.1)
+ railties (>= 4.0.1)
+ html2haml (2.0.0)
+ erubis (~> 2.7.0)
+ haml (~> 4.0.0)
+ nokogiri (~> 1.6.0)
+ ruby_parser (~> 3.5)
+ i18n (0.7.0)
+ jquery-rails (4.1.1)
+ rails-dom-testing (>= 1, < 3)
+ railties (>= 4.2.0)
+ thor (>= 0.14, < 2.0)
+ json (1.8.3)
+ kgio (2.10.0)
+ launchy (2.4.3)
+ addressable (~> 2.3)
+ less (2.6.0)
+ commonjs (~> 0.2.7)
+ less-rails (2.7.1)
+ actionpack (>= 4.0)
+ less (~> 2.6.0)
+ sprockets (> 2, < 4)
+ tilt
+ loofah (2.0.3)
+ nokogiri (>= 1.5.9)
+ mail (2.6.4)
+ mime-types (>= 1.16, < 4)
+ mime-types (3.1)
+ mime-types-data (~> 3.2015)
+ mime-types-data (3.2016.0521)
+ mini_portile2 (2.0.0)
+ minitest (5.9.0)
+ nokogiri (1.6.7.2)
+ mini_portile2 (~> 2.0.0.rc2)
+ rack (1.6.4)
+ rack-test (0.6.3)
+ rack (>= 1.0)
+ rails (4.2.6)
+ actionmailer (= 4.2.6)
+ actionpack (= 4.2.6)
+ actionview (= 4.2.6)
+ activejob (= 4.2.6)
+ activemodel (= 4.2.6)
+ activerecord (= 4.2.6)
+ activesupport (= 4.2.6)
+ bundler (>= 1.3.0, < 2.0)
+ railties (= 4.2.6)
+ sprockets-rails
+ rails-deprecated_sanitizer (1.0.3)
+ activesupport (>= 4.2.0.alpha)
+ rails-dom-testing (1.0.7)
+ activesupport (>= 4.2.0.beta, < 5.0)
+ nokogiri (~> 1.6.0)
+ rails-deprecated_sanitizer (>= 1.0.1)
+ rails-footnotes (4.1.8)
+ rails (>= 3.2)
+ rails-html-sanitizer (1.0.3)
+ loofah (~> 2.0)
+ railties (4.2.6)
+ actionpack (= 4.2.6)
+ activesupport (= 4.2.6)
+ rake (>= 0.8.7)
+ thor (>= 0.18.1, < 2.0)
+ raindrops (0.16.0)
+ rake (11.1.2)
+ rest-client (1.6.7)
+ mime-types (>= 1.16)
+ ruby_parser (3.8.2)
+ sexp_processor (~> 4.1)
+ sass (3.4.22)
+ sass-rails (5.0.4)
+ railties (>= 4.0.0, < 5.0)
+ sass (~> 3.1)
+ sprockets (>= 2.8, < 4.0)
+ sprockets-rails (>= 2.0, < 4.0)
+ tilt (>= 1.1, < 3)
+ sexp_processor (4.7.0)
+ shoulda (3.5.0)
+ shoulda-context (~> 1.0, >= 1.0.1)
+ shoulda-matchers (>= 1.4.1, < 3.0)
+ shoulda-context (1.2.1)
+ shoulda-matchers (2.8.0)
+ activesupport (>= 3.0.0)
+ spring (1.7.1)
+ spring-commands-testunit (1.0.1)
+ spring (>= 0.9.1)
+ sprockets (3.6.0)
+ concurrent-ruby (~> 1.0)
+ rack (> 1, < 3)
+ sprockets-rails (3.0.4)
+ actionpack (>= 4.0)
+ activesupport (>= 4.0)
+ sprockets (>= 3.0.0)
+ sqlite3 (1.3.11)
+ table_print (1.5.6)
+ thor (0.19.1)
+ thread_safe (0.3.5)
+ tilt (2.0.5)
+ turbolinks (2.5.3)
+ coffee-rails
+ twitter-bootstrap-rails (3.2.2)
+ actionpack (>= 3.1)
+ execjs (>= 2.2.2, >= 2.2)
+ less-rails (>= 2.5.0)
+ railties (>= 3.1)
+ tzinfo (1.2.2)
+ thread_safe (~> 0.1)
+ uglifier (3.0.0)
+ execjs (>= 0.3.0, < 3)
+ unicorn (5.1.0)
+ kgio (~> 2.6)
+ raindrops (~> 0.7)
+ xpath (2.0.0)
+ nokogiri (~> 1.3)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ awesome_print
+ better_errors
+ binding_of_caller
+ byebug
+ capybara
+ capybara-webkit
+ haml-rails
+ jquery-rails
+ launchy
+ rails (= 4.2.6)
+ rails-footnotes
+ rest-client
+ sass-rails (~> 5.0)
+ shoulda
+ shoulda-matchers
+ spring
+ spring-commands-testunit
+ sqlite3
+ table_print
+ turbolinks
+ twitter-bootstrap-rails
+ uglifier (>= 1.3.0)
+ unicorn
+
+RUBY VERSION
+ ruby 2.2.5p319
+
+BUNDLED WITH
+ 1.12.5
diff --git a/README.md b/README.md
index 2b26bcb..c05c7a1 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,33 @@
-# PinTube
+# Notes and Configuration
+* See the secrets.yml file to set your Youtube API key (either edit the file or set the env variable)
+* Displays well in Chrome and Firefox (images overflow in flex items with Safari...)
+* Capybara Webkit Driver Installation (for JS integration testing): https://github.com/thoughtbot/capybara-webkit/wiki/Installing-Qt-and-compiling-capybara-webkit
+
+# Run the application
+Migrate the db (replace ? by the environment you want to run in: production or development)
+
+ RAILS_ENV=? bin/rake db:migrate
+
+Then run the server
+
+ bundle exec unicorn_rails -l 0.0.0.0:3000 -E ?
+
+For the production environment, you also need to compile the assets first with:
+
+ RAILS_ENV=production bin/rake assets:precompile
+
+Then point your browser (Firefox or Chrome) to
+
+ http://localhost:3000
+
+# PinTube (Requirements)
A simple rails web app that enables user to add a personal collection of favourite youtube videos. It's a like a Pinterest for YouTube videos.
This application is designed to gauge our applicant's:
- adherence to best practices
- familiarity with code patterns
-- ability to write tests and documentation, and
+- ability to write tests, and
- command of Ruby and Rails as a whole
## App Specifications:
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..ba6b733
--- /dev/null
+++ b/Rakefile
@@ -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 File.expand_path('../config/application', __FILE__)
+
+Rails.application.load_tasks
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
new file mode 100644
index 0000000..bcb49bf
--- /dev/null
+++ b/app/assets/javascripts/application.js
@@ -0,0 +1,17 @@
+// This is a manifest file that'll be compiled into application.js, which will include all the files
+// listed below.
+//
+// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
+// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
+//
+// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+// compiled file.
+//
+// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
+// about supported directives.
+//
+//= require jquery
+//= require jquery_ujs
+//= require twitter/bootstrap
+//= require turbolinks
+//= require_tree .
diff --git a/app/assets/javascripts/bootstrap.js b/app/assets/javascripts/bootstrap.js
new file mode 100644
index 0000000..4e87a7d
--- /dev/null
+++ b/app/assets/javascripts/bootstrap.js
@@ -0,0 +1,4 @@
+jQuery(function() {
+ $("a[rel~=popover], .has-popover").popover();
+ $("a[rel~=tooltip], .has-tooltip").tooltip();
+});
diff --git a/app/assets/javascripts/videos.js b/app/assets/javascripts/videos.js
new file mode 100644
index 0000000..b3cbade
--- /dev/null
+++ b/app/assets/javascripts/videos.js
@@ -0,0 +1,59 @@
+// # Place all the behaviors and hooks related to the matching controller here.
+// # All this logic will automatically be available in application.js.
+
+$(document).on('ready page:load', function(event) {
+
+ // opens the modal dialog with our video details (and player)
+ $('a.open-details').click(function() {
+ var link = $(this);
+
+ $.get('/videos/' + $(this).data('video-id'))
+ .done(function(data) {
+ displayInDialog(link.closest('.video').find('.video_title').text(), data);
+ })
+ .fail(function (_jqXhr, _textStatus, errorThrown) {
+ displayAjaxError('Unexpected Error', errorThrown);
+ });
+ });
+
+
+
+ // opens the modal dialog with a form to create a new video from the supplied url
+ $('form#add_video').submit(function(event) {
+ var form = $(this);
+
+ $.get('/videos/new', form.serialize())
+ .done(function(data) {
+ displayInDialog('Add Video', data);
+ })
+ .fail(function (_jqXhr, _textStatus, errorThrown) {
+ displayAjaxError('Unexpected Error', errorThrown);
+ });
+ event.preventDefault();
+ });
+
+
+
+ // triggers a reload of videos belonging to the selected board @todo via ajax
+ $('select#current_board_id').change(function() {
+ this.form.submit();
+ });
+});
+
+
+function displayAjaxError(title, errorThrown) {
+ var errorMessage = errorThrown;
+
+ // rather naive way of detecting that the server is gone or inaccessible, but that will do for now
+ if (errorMessage == '') {
+ errorMessage = 'Lost connection with server. Try again later.';
+ }
+
+ displayInDialog(title, '
'+ errorMessage +'
');
+}
+
+function displayInDialog(title, html) {
+ $('#modal .modal-title').text(title);
+ $('.modal-body').html(html);
+ $('#modal').modal('show');
+}
\ No newline at end of file
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
new file mode 100644
index 0000000..a174c7b
--- /dev/null
+++ b/app/assets/stylesheets/application.css
@@ -0,0 +1,49 @@
+/*
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
+ * listed below.
+ *
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
+ *
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
+ * compiled file so the styles you add here take precedence over styles defined in any styles
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
+ * file per style scope.
+ *
+ *= require_tree .
+ *= require_self
+ */
+
+body {
+ background-color: #e1e1e1;
+}
+
+.navbar-default {
+ background-color: white;
+ border-color: black;
+ box-shadow: 0 4px 8px 0 #636363;
+}
+.navbar-default .navbar-brand {
+ color: black;
+}
+
+.nav-bar-container {
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+}
+
+.board_forms form {
+ display: inline-block;
+}
+
+
+.layout-spacer {
+ width: 100%;
+ height: 4.5em;
+}
+
+.flash.alert {
+ margin-left: 1em;
+ margin-right: 1em;
+}
\ No newline at end of file
diff --git a/app/assets/stylesheets/boards.scss b/app/assets/stylesheets/boards.scss
new file mode 100644
index 0000000..5e95de8
--- /dev/null
+++ b/app/assets/stylesheets/boards.scss
@@ -0,0 +1,7 @@
+// Place all the styles related to the boards controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
+
+form.edit_board {
+ margin-bottom: 0.5em;
+}
\ No newline at end of file
diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css b/app/assets/stylesheets/bootstrap_and_overrides.css
new file mode 100644
index 0000000..be76d71
--- /dev/null
+++ b/app/assets/stylesheets/bootstrap_and_overrides.css
@@ -0,0 +1,9 @@
+/*
+ =require twitter-bootstrap-static/bootstrap
+
+ Use Font Awesome icons (default)
+ To use Glyphicons sprites instead of Font Awesome, replace with "require twitter-bootstrap-static/sprites"
+ ## =require twitter-bootstrap-static/fontawesome
+ =require twitter-bootstrap-static/sprites
+
+ */
\ No newline at end of file
diff --git a/app/assets/stylesheets/videos.scss b/app/assets/stylesheets/videos.scss
new file mode 100644
index 0000000..4a6dc1f
--- /dev/null
+++ b/app/assets/stylesheets/videos.scss
@@ -0,0 +1,39 @@
+// Place all the styles related to the videos controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
+
+.videos_row {
+ display: flex;
+}
+
+.video {
+ padding: 1em;
+ margin: 1em;
+
+ border: 1px solid #808080;
+ background-color: white;
+ box-shadow: 0 4px 8px 0 #888888;
+
+ flex-shrink: 1;
+}
+
+.video_desc {
+ white-space: pre-wrap;
+ font-size: 90%;
+}
+
+.video_desc_header {
+ margin-bottom: 0;
+}
+
+.new_video_title {
+ margin-top: 0;
+}
+//form.new_video {
+// margin-top: 1em;
+//}
+
+.assign_boards {
+ padding-bottom: 0.5em;
+ padding-top: 0.5em;
+}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
new file mode 100644
index 0000000..c42d6f7
--- /dev/null
+++ b/app/controllers/application_controller.rb
@@ -0,0 +1,15 @@
+class ApplicationController < ActionController::Base
+ # Prevent CSRF attacks by raising an exception.
+ # For APIs, you may want to use :null_session instead.
+ protect_from_forgery with: :exception
+
+ before_action :set_current_board
+
+ private
+
+ def set_current_board
+ session[:current_board_id] = params[:current_board_id] if params[:current_board_id]
+ @current_board = Board.find_by(id: session[:current_board_id])
+ end
+
+end
diff --git a/app/controllers/boards_controller.rb b/app/controllers/boards_controller.rb
new file mode 100644
index 0000000..ace0008
--- /dev/null
+++ b/app/controllers/boards_controller.rb
@@ -0,0 +1,43 @@
+class BoardsController < ApplicationController
+
+ def create
+ @board = Board.new board_params
+
+ if @board.save
+ flash['success'] = "Board #{@board.name} created."
+ else
+ flash['warning'] = "Couldn't create board #{@board.name}: #{@board.errors.full_messages.join(', ')}."
+ end
+ redirect_to root_path
+ end
+
+ def update
+ @board = Board.find params[:id]
+
+ if @board.update_attributes(board_params)
+ flash['success'] = "Board #{@board.name} updated."
+ else
+ flash['danger'] = "Failed updating board #{@board.name}: #{@board.errors.full_messages.join(', ')}."
+ end
+ redirect_to root_path(current_board_id: @board)
+ end
+
+ def destroy
+ @board = Board.find params[:id]
+
+ if @board.destroy
+ flash['success'] = "Board #{@board.name} deleted."
+ else
+ flash['danger'] = "Failed deleted board #{@board.name}."
+ end
+ redirect_to root_path(current_board_id: '') # we clear the current board since it has been destroyed
+ end
+
+
+
+ private
+
+ def board_params
+ params.require(:board).permit(:name)
+ end
+end
diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/app/controllers/videos_controller.rb b/app/controllers/videos_controller.rb
new file mode 100644
index 0000000..0d9f895
--- /dev/null
+++ b/app/controllers/videos_controller.rb
@@ -0,0 +1,78 @@
+class VideosController < ApplicationController
+ before_action :set_video, only: [:show, :update, :destroy]
+
+
+ def index
+
+ @videos = if @current_board
+ @current_board.videos
+ else
+ Video.all
+ end.default_order.to_a
+ end
+
+ def show
+ @boards = Board.default_order
+
+ render layout: nil
+ end
+
+ def new
+ @video = Video.new url: params[:video_url]
+ @video.boards << @current_board if set_current_board
+
+ @boards = Board.default_order
+
+ begin
+ @video.add_yt_data RetrieveYoutubeData.new(@video.yt_id).call
+ rescue ArgumentError => e
+ flash.now['warning'] = "Couldn't add the video with URL: #{@video.url}"
+ end
+ # The following could be placed before we call yt but here, we have a confirmation that the yt_id is valid
+ if flash.now['warning'].nil? && (duplicate = Video.find_by(yt_id: @video.yt_id))
+ flash.now['warning'] = "Video already added (#{duplicate.title})."
+ end
+
+ render layout: nil
+ end
+
+ def create
+ @video = Video.new video_params
+ @video.add_yt_data RetrieveYoutubeData.new(@video.yt_id).call
+
+ if @video.save
+ flash['success'] = 'Video added.'
+ else
+ flash['warning'] = "Video couldn't be added: #{@video.errors.full_messages.join(', ')}."
+ end
+ redirect_to root_path
+ end
+
+ def update
+ if @video.update_attributes(video_params)
+ flash['success'] = 'Video updated.'
+ end
+ redirect_to root_path
+ end
+
+ def destroy
+ if @video.destroy
+ flash['success'] = 'Video deleted.'
+ else
+ flash['warning'] = 'Couldn\'t delete video.'
+ end
+ redirect_to root_path
+ end
+
+
+
+ private
+
+ def video_params
+ params.require(:video).permit(:url, board_ids: [])
+ end
+
+ def set_video
+ @video = Video.find params[:id]
+ end
+end
diff --git a/app/helpers/videos_helper.rb b/app/helpers/videos_helper.rb
new file mode 100644
index 0000000..c9f6f6c
--- /dev/null
+++ b/app/helpers/videos_helper.rb
@@ -0,0 +1,11 @@
+module VideosHelper
+
+ def no_videos_message
+ if @current_board
+ 'No videos on this board yet. Click \'details\' on a video to add it to boards.'
+ elsif Video.count == 0
+ 'No videos added yet. Please add some with the form on the top right.'
+ end
+ end
+
+end
diff --git a/app/models/.keep b/app/models/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/app/models/board.rb b/app/models/board.rb
new file mode 100644
index 0000000..af10a65
--- /dev/null
+++ b/app/models/board.rb
@@ -0,0 +1,16 @@
+class Board < ActiveRecord::Base
+
+ # Relationships ==================================================================
+ has_and_belongs_to_many :videos
+
+ # Scopes =========================================================================
+ scope :default_order, ->{ order(:name) }
+
+ # Callbacks ======================================================================
+ before_validation {|b| b.name = b.name.squish.titleize if b.name}
+
+ # Validations ====================================================================
+ validates_presence_of :name
+ validates_uniqueness_of :name
+
+end
diff --git a/app/models/video.rb b/app/models/video.rb
new file mode 100644
index 0000000..63dc5f3
--- /dev/null
+++ b/app/models/video.rb
@@ -0,0 +1,64 @@
+class Video < ActiveRecord::Base
+
+ # src: http://stackoverflow.com/a/6557092/178266
+ YT_URL_VIDEO_ID_REGEX = /(?:.be\/|\/watch\?v=|\/(?=p\/))([\w\/\-]+)/
+
+ # Relationships ==================================================================
+ has_and_belongs_to_many :boards
+
+ # Scopes =========================================================================
+ scope :default_order, ->{ order created_at: :desc }
+
+ # Behaviours =====================================================================
+ serialize :yt_data, JSON
+
+ # Validations ====================================================================
+ validates_presence_of :url
+
+ validates :yt_id, presence: true, uniqueness: true
+
+ # Youtube data Interface =========================================================
+ # For now, we interface with the YouTube response data.
+ # In the future, as needed (speed or searchability), we can convert data to full-blown attributes with migrations
+
+ def add_yt_data data
+ self.yt_data = data
+ self.yt_id = yt_data.try(:fetch, 'id') # we set the yt_id once yt confirmed it
+ end
+
+ def yt_id
+ read_attribute(:yt_id) || extract_yt_id_from_url
+ end
+
+ def title
+ yt_snippet_data['title']
+ end
+ def description
+ yt_snippet_data['description']
+ end
+ def tags
+ yt_snippet_data['tags']
+ end
+ # etc...
+ def thumbnail_url(quality: 'medium')
+ yt_snippet_data.dig('thumbnails', quality, 'url')
+ end
+
+ def embed_url
+ "http://www.youtube-nocookie.com/embed/#{yt_id}?html5=1"
+ end
+
+
+
+ private
+
+ def yt_snippet_data
+ yt_data.try(:fetch, 'snippet') || {}
+ end
+
+ def extract_yt_id_from_url
+ if url && url.match(YT_URL_VIDEO_ID_REGEX)
+ Regexp.last_match[1]
+ end
+ end
+end
diff --git a/app/services/retrieve_youtube_data.rb b/app/services/retrieve_youtube_data.rb
new file mode 100644
index 0000000..b4cceb3
--- /dev/null
+++ b/app/services/retrieve_youtube_data.rb
@@ -0,0 +1,36 @@
+class RetrieveYoutubeData
+
+ YT_VIDEO_ENDPOINT = 'https://www.googleapis.com/youtube/v3/videos'
+ CACHE_NAMESPACE = 'yt_video_api'
+
+ def initialize video_id
+ @video_id = video_id
+ end
+
+ def call
+ # we cache the retrieved data for a while
+ response = Rails.cache.fetch @video_id, expires_in: 5.minutes, namespace: CACHE_NAMESPACE do
+ yt_api_call
+ end
+
+ if response['items'].empty?
+ raise ArgumentError, 'Not found'
+ else
+ response['items'].first
+ end
+ end
+
+
+
+ private
+
+ def yt_api_call
+ raw_response = RestClient.get YT_VIDEO_ENDPOINT, params:
+ {
+ key: Rails.application.secrets.youtube_api_key,
+ part: 'snippet',
+ id: @video_id
+ }
+ JSON.parse raw_response
+ end
+end
\ No newline at end of file
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
new file mode 100644
index 0000000..38c7cd7
--- /dev/null
+++ b/app/views/layouts/application.html.haml
@@ -0,0 +1,42 @@
+!!! 5
+%html(lang="en")
+ %head
+ %meta(charset="utf-8")
+ %meta(name="viewport" content="width=device-width, initial-scale=1.0")
+
+ %title Pintube
+
+ = csrf_meta_tags
+
+ = stylesheet_link_tag "application", :media => "all"
+
+ = javascript_include_tag "application"
+
+
+ %body
+ = nav_bar fixed: :top, brand: "Pintube", brand_link: root_path, responsive: true do
+
+ .nav-bar-container
+
+ .board_forms.navbar-form
+ = form_tag root_path, method: :get do
+ Choose Board
+ = select_tag :current_board_id,
+ options_from_collection_for_select(Board.default_order, :id, :name, session[:current_board_id]),
+ include_blank: '- All Videos -'
+
+ = form_for Board.new do |f|
+ = f.text_field :name, placeholder: 'New board name'
+ = submit_tag 'Create'
+
+ = form_tag new_video_path, method: :get, id: 'add_video', class: 'navbar-form pull-right' do
+ Add Video
+ = text_field_tag :video_url, nil, placeholder: 'Video URL eg youtu.be/sGE4HMvDe-Q'
+ = submit_tag 'Add'
+
+ .layout-spacer
+
+ = render 'shared/flashes'
+
+ .container
+ = yield
\ No newline at end of file
diff --git a/app/views/shared/_flashes.html.haml b/app/views/shared/_flashes.html.haml
new file mode 100644
index 0000000..acc8c55
--- /dev/null
+++ b/app/views/shared/_flashes.html.haml
@@ -0,0 +1,2 @@
+- flash.each do |key, msg|
+ .flash.alert{class: "alert-#{key}"}= msg
diff --git a/app/views/videos/_board_checkboxes.html.haml b/app/views/videos/_board_checkboxes.html.haml
new file mode 100644
index 0000000..6a27f85
--- /dev/null
+++ b/app/views/videos/_board_checkboxes.html.haml
@@ -0,0 +1,8 @@
+Assign to board(s):
+- if @boards.any?
+
+ = f.collection_check_boxes :board_ids, @boards, :id, :name do |b|
+ = b.label(class: 'checkbox-inline') { b.check_box + b.text }
+
+- else
+ %strong No boards created yet
\ No newline at end of file
diff --git a/app/views/videos/_board_header.html.haml b/app/views/videos/_board_header.html.haml
new file mode 100644
index 0000000..c36ef31
--- /dev/null
+++ b/app/views/videos/_board_header.html.haml
@@ -0,0 +1,10 @@
+- if @current_board
+ .board_header
+ %h2.board_title Videos on #{@current_board.name}
+
+ = form_for @current_board do |f|
+
+ = f.text_field :name
+ = f.submit 'Rename This Board'
+
+ = link_to 'Delete board', board_path(@current_board), title: 'Delete', method: :delete, data: { confirm: 'Do you really want to remove this board?' }
\ No newline at end of file
diff --git a/app/views/videos/index.html.haml b/app/views/videos/index.html.haml
new file mode 100644
index 0000000..5f53b80
--- /dev/null
+++ b/app/views/videos/index.html.haml
@@ -0,0 +1,34 @@
+= render :partial => 'board_header'
+
+- if @videos.any?
+
+ - @videos.in_groups_of(3).each do |group|
+
+ .videos_row
+ - group.compact.each do |video|
+ .video{id: dom_id(video)}
+
+ %h4.video_title= video.title
+
+ = image_tag video.thumbnail_url
+
+ %p= truncate video.description, length: 80
+
+ .links
+ = link_to('Details', 'javascript:void(0)', class: 'open-details', data: {video_id: video.id})
+ = link_to('Delete video', video_path(video), title: 'Delete', method: :delete, data: { confirm: 'Do you really want to remove this video?' })
+
+- else
+ .alert.alert-info= no_videos_message
+
+
+
+-# Our modal dialog to display a single video or add a new one
+#modal.bootstrap-modal.modal.fade(tabindex="-1" )
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %button.close(data-dismiss="modal" aria-hidden="true" )×
+ %h4.modal-title
+ .modal-body
+
diff --git a/app/views/videos/new.html.haml b/app/views/videos/new.html.haml
new file mode 100644
index 0000000..b29ca96
--- /dev/null
+++ b/app/views/videos/new.html.haml
@@ -0,0 +1,17 @@
+= render 'shared/flashes'
+
+- if flash.now[:warning].nil?
+
+ %h3.new_video_title= @video.title
+
+ = image_tag @video.thumbnail_url(quality: 'high')
+
+ = form_for @video do |f|
+ = f.hidden_field :url
+
+ .assign_boards= render 'board_checkboxes', f: f
+
+ = f.submit 'Add Video'
+
+ %h4.video_desc_header Description
+ %p.video_desc= @video.description
diff --git a/app/views/videos/show.html.haml b/app/views/videos/show.html.haml
new file mode 100644
index 0000000..05c898d
--- /dev/null
+++ b/app/views/videos/show.html.haml
@@ -0,0 +1,12 @@
+-# Youtube player frame
+%iframe(width="560" height="315" frameborder="0" allowfullscreen ){src: @video.embed_url}
+
+- if @boards.any?
+ .assign_boards
+ = form_for @video do |f|
+
+ = render 'board_checkboxes', f: f
+
+ = f.submit 'Assign', class: 'btn btn-default btn-sm'
+
+%p.video_desc= @video.description
\ No newline at end of file
diff --git a/bin/bundle b/bin/bundle
new file mode 100755
index 0000000..66e9889
--- /dev/null
+++ b/bin/bundle
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+load Gem.bin_path('bundler', 'bundle')
diff --git a/bin/rails b/bin/rails
new file mode 100755
index 0000000..0138d79
--- /dev/null
+++ b/bin/rails
@@ -0,0 +1,9 @@
+#!/usr/bin/env ruby
+begin
+ load File.expand_path('../spring', __FILE__)
+rescue LoadError => e
+ raise unless e.message.include?('spring')
+end
+APP_PATH = File.expand_path('../../config/application', __FILE__)
+require_relative '../config/boot'
+require 'rails/commands'
diff --git a/bin/rake b/bin/rake
new file mode 100755
index 0000000..d87d5f5
--- /dev/null
+++ b/bin/rake
@@ -0,0 +1,9 @@
+#!/usr/bin/env ruby
+begin
+ load File.expand_path('../spring', __FILE__)
+rescue LoadError => e
+ raise unless e.message.include?('spring')
+end
+require_relative '../config/boot'
+require 'rake'
+Rake.application.run
diff --git a/bin/setup b/bin/setup
new file mode 100755
index 0000000..acdb2c1
--- /dev/null
+++ b/bin/setup
@@ -0,0 +1,29 @@
+#!/usr/bin/env ruby
+require 'pathname'
+
+# path to your application root.
+APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
+
+Dir.chdir APP_ROOT do
+ # This script is a starting point to setup your application.
+ # Add necessary setup steps to this file:
+
+ puts "== Installing dependencies =="
+ system "gem install bundler --conservative"
+ system "bundle check || bundle install"
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?("config/database.yml")
+ # system "cp config/database.yml.sample config/database.yml"
+ # end
+
+ puts "\n== Preparing database =="
+ system "bin/rake db:setup"
+
+ puts "\n== Removing old logs and tempfiles =="
+ system "rm -f log/*"
+ system "rm -rf tmp/cache"
+
+ puts "\n== Restarting application server =="
+ system "touch tmp/restart.txt"
+end
diff --git a/bin/spring b/bin/spring
new file mode 100755
index 0000000..7fe232c
--- /dev/null
+++ b/bin/spring
@@ -0,0 +1,15 @@
+#!/usr/bin/env ruby
+
+# This file loads spring without using Bundler, in order to be fast.
+# It gets overwritten when you run the `spring binstub` command.
+
+unless defined?(Spring)
+ require 'rubygems'
+ require 'bundler'
+
+ if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m))
+ Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) }
+ gem 'spring', match[1]
+ require 'spring/binstub'
+ end
+end
diff --git a/config.ru b/config.ru
new file mode 100644
index 0000000..bd83b25
--- /dev/null
+++ b/config.ru
@@ -0,0 +1,4 @@
+# This file is used by Rack-based servers to start the application.
+
+require ::File.expand_path('../config/environment', __FILE__)
+run Rails.application
diff --git a/config/application.rb b/config/application.rb
new file mode 100644
index 0000000..a5dd1cd
--- /dev/null
+++ b/config/application.rb
@@ -0,0 +1,29 @@
+require File.expand_path('../boot', __FILE__)
+
+require 'rails/all'
+
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(*Rails.groups)
+
+module Pintube
+ class Application < Rails::Application
+ # Settings in config/environments/* take precedence over those specified here.
+ # Application configuration should go into files in config/initializers
+ # -- all .rb files in that directory are automatically loaded.
+
+ # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
+ # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
+ # config.time_zone = 'Central Time (US & Canada)'
+
+ # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
+ # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
+ # config.i18n.default_locale = :de
+
+ # Do not swallow errors in after_commit/after_rollback callbacks.
+ config.active_record.raise_in_transactional_callbacks = true
+ end
+end
+
+# Opens in Rubymine
+BetterErrors.editor='x-mine://open?file=%{file}&line=%{line}' if defined?(BetterErrors)
diff --git a/config/boot.rb b/config/boot.rb
new file mode 100644
index 0000000..6b750f0
--- /dev/null
+++ b/config/boot.rb
@@ -0,0 +1,3 @@
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+
+require 'bundler/setup' # Set up gems listed in the Gemfile.
diff --git a/config/database.yml b/config/database.yml
new file mode 100644
index 0000000..1c1a37c
--- /dev/null
+++ b/config/database.yml
@@ -0,0 +1,25 @@
+# SQLite version 3.x
+# gem install sqlite3
+#
+# Ensure the SQLite 3 gem is defined in your Gemfile
+# gem 'sqlite3'
+#
+default: &default
+ adapter: sqlite3
+ pool: 5
+ timeout: 5000
+
+development:
+ <<: *default
+ database: db/development.sqlite3
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: db/test.sqlite3
+
+production:
+ <<: *default
+ database: db/production.sqlite3
diff --git a/config/environment.rb b/config/environment.rb
new file mode 100644
index 0000000..ee8d90d
--- /dev/null
+++ b/config/environment.rb
@@ -0,0 +1,5 @@
+# Load the Rails application.
+require File.expand_path('../application', __FILE__)
+
+# Initialize the Rails application.
+Rails.application.initialize!
diff --git a/config/environments/development.rb b/config/environments/development.rb
new file mode 100644
index 0000000..b55e214
--- /dev/null
+++ b/config/environments/development.rb
@@ -0,0 +1,41 @@
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # In the development environment your application's code is reloaded on
+ # every request. This slows down response time but is perfect for development
+ # since you don't have to restart the web server when you make code changes.
+ config.cache_classes = false
+
+ # Do not eager load code on boot.
+ config.eager_load = false
+
+ # Show full error reports and disable caching.
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+
+ # Don't care if the mailer can't send.
+ config.action_mailer.raise_delivery_errors = false
+
+ # Print deprecation notices to the Rails logger.
+ config.active_support.deprecation = :log
+
+ # Raise an error on page load if there are pending migrations.
+ config.active_record.migration_error = :page_load
+
+ # Debug mode disables concatenation and preprocessing of assets.
+ # This option may cause significant delays in view rendering with a large
+ # number of complex assets.
+ config.assets.debug = true
+
+ # Asset digests allow you to set far-future HTTP expiration dates on all assets,
+ # yet still be able to expire them through the digest params.
+ config.assets.digest = true
+
+ # Adds additional error checking when serving assets at runtime.
+ # Checks for improperly declared sprockets dependencies.
+ # Raises helpful error messages.
+ config.assets.raise_runtime_errors = true
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
+end
diff --git a/config/environments/production.rb b/config/environments/production.rb
new file mode 100644
index 0000000..acca945
--- /dev/null
+++ b/config/environments/production.rb
@@ -0,0 +1,80 @@
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # Code is not reloaded between requests.
+ config.cache_classes = true
+
+ # Eager load code on boot. This eager loads most of Rails and
+ # your application in memory, allowing both threaded web servers
+ # and those relying on copy on write to perform better.
+ # Rake tasks automatically ignore this option for performance.
+ config.eager_load = true
+
+ # Full error reports are disabled and caching is turned on.
+ config.consider_all_requests_local = false
+ config.action_controller.perform_caching = true
+
+ # Enable Rack::Cache to put a simple HTTP cache in front of your application
+ # Add `rack-cache` to your Gemfile before enabling this.
+ # For large-scale production use, consider using a caching reverse proxy like
+ # NGINX, varnish or squid.
+ # config.action_dispatch.rack_cache = true
+
+ # Disable serving static files from the `/public` folder by default since
+ # Apache or NGINX already handles this.
+ config.serve_static_files = true
+ # config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?
+
+ # Compress JavaScripts and CSS.
+ config.assets.js_compressor = :uglifier
+ # config.assets.css_compressor = :sass
+
+ # Do not fallback to assets pipeline if a precompiled asset is missed.
+ config.assets.compile = false
+
+ # Asset digests allow you to set far-future HTTP expiration dates on all assets,
+ # yet still be able to expire them through the digest params.
+ config.assets.digest = true
+
+ # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
+
+ # Specifies the header that your server uses for sending files.
+ # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
+
+ # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
+ # config.force_ssl = true
+
+ # Use the lowest log level to ensure availability of diagnostic information
+ # when problems arise.
+ config.log_level = :debug
+
+ # Prepend all log lines with the following tags.
+ # config.log_tags = [ :subdomain, :uuid ]
+
+ # Use a different logger for distributed setups.
+ # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
+
+ # Use a different cache store in production.
+ # config.cache_store = :mem_cache_store
+
+ # Enable serving of images, stylesheets, and JavaScripts from an asset server.
+ # config.action_controller.asset_host = 'http://assets.example.com'
+
+ # Ignore bad email addresses and do not raise email delivery errors.
+ # Set this to true and configure the email server for immediate delivery to raise delivery errors.
+ # config.action_mailer.raise_delivery_errors = false
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation cannot be found).
+ config.i18n.fallbacks = true
+
+ # Send deprecation notices to registered listeners.
+ config.active_support.deprecation = :notify
+
+ # Use default logging formatter so that PID and timestamp are not suppressed.
+ config.log_formatter = ::Logger::Formatter.new
+
+ # Do not dump schema after migrations.
+ config.active_record.dump_schema_after_migration = false
+end
diff --git a/config/environments/test.rb b/config/environments/test.rb
new file mode 100644
index 0000000..1c19f08
--- /dev/null
+++ b/config/environments/test.rb
@@ -0,0 +1,42 @@
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # The test environment is used exclusively to run your application's
+ # test suite. You never need to work with it otherwise. Remember that
+ # your test database is "scratch space" for the test suite and is wiped
+ # and recreated between test runs. Don't rely on the data there!
+ config.cache_classes = true
+
+ # Do not eager load code on boot. This avoids loading your whole application
+ # just for the purpose of running a single test. If you are using a tool that
+ # preloads Rails for running tests, you may have to set it to true.
+ config.eager_load = false
+
+ # Configure static file server for tests with Cache-Control for performance.
+ config.serve_static_files = true
+ config.static_cache_control = 'public, max-age=3600'
+
+ # Show full error reports and disable caching.
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+
+ # Raise exceptions instead of rendering exception templates.
+ config.action_dispatch.show_exceptions = false
+
+ # Disable request forgery protection in test environment.
+ config.action_controller.allow_forgery_protection = false
+
+ # Tell Action Mailer not to deliver emails to the real world.
+ # The :test delivery method accumulates sent emails in the
+ # ActionMailer::Base.deliveries array.
+ config.action_mailer.delivery_method = :test
+
+ # Randomize the order test cases are executed.
+ config.active_support.test_order = :random
+
+ # Print deprecation notices to the stderr.
+ config.active_support.deprecation = :stderr
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
+end
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
new file mode 100644
index 0000000..01ef3e6
--- /dev/null
+++ b/config/initializers/assets.rb
@@ -0,0 +1,11 @@
+# Be sure to restart your server when you modify this file.
+
+# Version of your assets, change this if you want to expire all your assets.
+Rails.application.config.assets.version = '1.0'
+
+# Add additional assets to the asset load path
+# Rails.application.config.assets.paths << Emoji.images_path
+
+# Precompile additional assets.
+# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
+# Rails.application.config.assets.precompile += %w( search.js )
diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb
new file mode 100644
index 0000000..59385cd
--- /dev/null
+++ b/config/initializers/backtrace_silencers.rb
@@ -0,0 +1,7 @@
+# Be sure to restart your server when you modify this file.
+
+# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
+# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+
+# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
+# Rails.backtrace_cleaner.remove_silencers!
diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb
new file mode 100644
index 0000000..7f70458
--- /dev/null
+++ b/config/initializers/cookies_serializer.rb
@@ -0,0 +1,3 @@
+# Be sure to restart your server when you modify this file.
+
+Rails.application.config.action_dispatch.cookies_serializer = :json
diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb
new file mode 100644
index 0000000..4a994e1
--- /dev/null
+++ b/config/initializers/filter_parameter_logging.rb
@@ -0,0 +1,4 @@
+# Be sure to restart your server when you modify this file.
+
+# Configure sensitive parameters which will be filtered from the log file.
+Rails.application.config.filter_parameters += [:password]
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
new file mode 100644
index 0000000..ac033bf
--- /dev/null
+++ b/config/initializers/inflections.rb
@@ -0,0 +1,16 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format. Inflections
+# are locale specific, and you may define rules for as many different
+# locales as you wish. All of these examples are active by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.plural /^(ox)$/i, '\1en'
+# inflect.singular /^(ox)en/i, '\1'
+# inflect.irregular 'person', 'people'
+# inflect.uncountable %w( fish sheep )
+# end
+
+# These inflection rules are supported but not enabled by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.acronym 'RESTful'
+# end
diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb
new file mode 100644
index 0000000..dc18996
--- /dev/null
+++ b/config/initializers/mime_types.rb
@@ -0,0 +1,4 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new mime types for use in respond_to blocks:
+# Mime::Type.register "text/richtext", :rtf
diff --git a/config/initializers/rails_footnotes.rb b/config/initializers/rails_footnotes.rb
new file mode 100644
index 0000000..9a15363
--- /dev/null
+++ b/config/initializers/rails_footnotes.rb
@@ -0,0 +1,27 @@
+defined?(Footnotes) && Footnotes.setup do |f|
+ # Whether or not to enable footnotes
+ f.enabled = Rails.env.development?
+ # You can also use a lambda / proc to conditionally toggle footnotes
+ # Example :
+ # f.enabled = -> { User.current.admin? }
+ # Beware of thread-safety though, Footnotes.enabled is NOT thread safe
+ # and should not be modified anywhere else.
+
+ # Only toggle some notes :
+ # f.notes = [:session, :cookies, :params, :filters, :routes, :env, :queries, :log]
+
+ # Change the prefix :
+ # f.prefix = 'mvim://open?url=file://%s&line=%d&column=%d'
+
+ # Disable style :
+ # f.no_style = true
+
+ # Lock notes to top right :
+ # f.lock_top_right = true
+
+ # Change font size :
+ # f.font_size = '11px'
+
+ # Allow to open multiple notes :
+ # f.multiple_notes = true
+end
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
new file mode 100644
index 0000000..a31a17b
--- /dev/null
+++ b/config/initializers/session_store.rb
@@ -0,0 +1,3 @@
+# Be sure to restart your server when you modify this file.
+
+Rails.application.config.session_store :cookie_store, key: '_pintube_session'
diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb
new file mode 100644
index 0000000..33725e9
--- /dev/null
+++ b/config/initializers/wrap_parameters.rb
@@ -0,0 +1,14 @@
+# Be sure to restart your server when you modify this file.
+
+# This file contains settings for ActionController::ParamsWrapper which
+# is enabled by default.
+
+# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
+ActiveSupport.on_load(:action_controller) do
+ wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
+end
+
+# To enable root element in JSON for ActiveRecord objects.
+# ActiveSupport.on_load(:active_record) do
+# self.include_root_in_json = true
+# end
diff --git a/config/locales/en.bootstrap.yml b/config/locales/en.bootstrap.yml
new file mode 100644
index 0000000..8d75119
--- /dev/null
+++ b/config/locales/en.bootstrap.yml
@@ -0,0 +1,23 @@
+# Sample localization file for English. Add more files in this directory for other locales.
+# See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
+
+en:
+ breadcrumbs:
+ application:
+ root: "Index"
+ pages:
+ pages: "Pages"
+ helpers:
+ actions: "Actions"
+ links:
+ back: "Back"
+ cancel: "Cancel"
+ confirm: "Are you sure?"
+ destroy: "Delete"
+ new: "New"
+ edit: "Edit"
+ titles:
+ edit: "Edit %{model}"
+ save: "Save %{model}"
+ new: "New %{model}"
+ delete: "Delete %{model}"
diff --git a/config/locales/en.yml b/config/locales/en.yml
new file mode 100644
index 0000000..0653957
--- /dev/null
+++ b/config/locales/en.yml
@@ -0,0 +1,23 @@
+# Files in the config/locales directory are used for internationalization
+# and are automatically loaded by Rails. If you want to use locales other
+# than English, add the necessary files in this directory.
+#
+# To use the locales, use `I18n.t`:
+#
+# I18n.t 'hello'
+#
+# In views, this is aliased to just `t`:
+#
+# <%= t('hello') %>
+#
+# To use a different locale, set it with `I18n.locale`:
+#
+# I18n.locale = :es
+#
+# This would use the information in config/locales/es.yml.
+#
+# To learn more, please read the Rails Internationalization guide
+# available at http://guides.rubyonrails.org/i18n.html.
+
+en:
+ hello: "Hello world"
diff --git a/config/routes.rb b/config/routes.rb
new file mode 100644
index 0000000..b2b45cd
--- /dev/null
+++ b/config/routes.rb
@@ -0,0 +1,8 @@
+Rails.application.routes.draw do
+
+ resources :videos
+ resources :boards, only: [:create, :update, :destroy]
+
+ root 'videos#index'
+
+end
diff --git a/config/secrets.yml b/config/secrets.yml
new file mode 100644
index 0000000..7ffd231
--- /dev/null
+++ b/config/secrets.yml
@@ -0,0 +1,30 @@
+# Be sure to restart your server when you modify this file.
+
+# Your secret key is used for verifying the integrity of signed cookies.
+# If you change this key, all old signed cookies will become invalid!
+
+# Make sure the secret is at least 30 characters and all random,
+# no regular words or you'll be exposed to dictionary attacks.
+# You can use `rake secret` to generate a secure secret key.
+
+# Make sure the secrets in this file are kept private
+# if you're sharing your code publicly.
+
+default: &default
+ youtube_api_key: <%= ENV["YOUTUBE_API_KEY"] %>
+
+development:
+ <<: *default
+ secret_key_base: 822a769a8cdf1e2984a4693ac8766438ba7cc84051d7862bcacb587a2256b8599286f4dcf90c8d73b4bfe048c3541719255320e43151d709f057dbe32e8f778b
+
+test:
+ <<: *default
+ secret_key_base: 575c7405031f7c797261aa9df4f3f3a155af20bd74a585dfc6f83ae63cbe0dcefc3f3bf6c41d4f3cf2c73681d97b9a5f3c5da83e32f1ae9266b37f0934cc9369
+
+# Do not keep production secrets in the repository,
+# instead read values from the environment.
+production:
+ <<: *default
+ secret_key_base: 8e8142b33687fb908c19c3b872475ae904b80ee43a81453213b074b714ad23f6e516b38d173dde8df440a14b420a12239231a0cb0ca543682642bda0ce73f17b
+# in a real world application, you would use the following line and pass the secret as an env variable
+# secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
diff --git a/db/migrate/20160604145428_create_videos_and_boards.rb b/db/migrate/20160604145428_create_videos_and_boards.rb
new file mode 100644
index 0000000..bc1406b
--- /dev/null
+++ b/db/migrate/20160604145428_create_videos_and_boards.rb
@@ -0,0 +1,35 @@
+class CreateVideosAndBoards < ActiveRecord::Migration
+ def change
+
+ create_table :videos do |t|
+
+ t.string :url, null: false
+ t.text :yt_data
+ t.string :yt_id, null: false, index: true
+
+ t.timestamps null: false
+ end
+ add_index :videos, :created_at, order: {created_at: :desc}
+
+
+
+ create_table :boards do |t|
+
+ t.string :name, null: false
+
+ t.timestamps null: false
+ end
+ add_index :boards, :name, order: {name: :asc}
+
+
+
+ # association join table
+ create_table :boards_videos, id: false do |t|
+
+ t.belongs_to :board, index: true, null: false
+ t.belongs_to :video, index: true, null: false
+
+ end
+
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
new file mode 100644
index 0000000..d0b6496
--- /dev/null
+++ b/db/schema.rb
@@ -0,0 +1,43 @@
+# encoding: UTF-8
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# Note that this schema.rb definition is the authoritative source for your
+# database schema. If you need to create the application database on another
+# system, you should be using db:schema:load, not running all the migrations
+# from scratch. The latter is a flawed and unsustainable approach (the more migrations
+# you'll amass, the slower it'll run and the greater likelihood for issues).
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema.define(version: 20160604145428) do
+
+ create_table "boards", force: :cascade do |t|
+ t.string "name", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "boards", ["name"], name: "index_boards_on_name"
+
+ create_table "boards_videos", id: false, force: :cascade do |t|
+ t.integer "board_id", null: false
+ t.integer "video_id", null: false
+ end
+
+ add_index "boards_videos", ["board_id"], name: "index_boards_videos_on_board_id"
+ add_index "boards_videos", ["video_id"], name: "index_boards_videos_on_video_id"
+
+ create_table "videos", force: :cascade do |t|
+ t.string "url", null: false
+ t.text "yt_data"
+ t.string "yt_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "videos", ["created_at"], name: "index_videos_on_created_at"
+ add_index "videos", ["yt_id"], name: "index_videos_on_yt_id"
+
+end
diff --git a/db/seeds.rb b/db/seeds.rb
new file mode 100644
index 0000000..4edb1e8
--- /dev/null
+++ b/db/seeds.rb
@@ -0,0 +1,7 @@
+# This file should contain all the record creation needed to seed the database with its default values.
+# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
+#
+# Examples:
+#
+# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
+# Mayor.create(name: 'Emanuel', city: cities.first)
diff --git a/lib/assets/.keep b/lib/assets/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/lib/tasks/.keep b/lib/tasks/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/log/.keep b/log/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/public/404.html b/public/404.html
new file mode 100644
index 0000000..b612547
--- /dev/null
+++ b/public/404.html
@@ -0,0 +1,67 @@
+
+
+
+ The page you were looking for doesn't exist (404)
+
+
+
+
+
+
+
+
+
The page you were looking for doesn't exist.
+
You may have mistyped the address or the page may have moved.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/public/422.html b/public/422.html
new file mode 100644
index 0000000..a21f82b
--- /dev/null
+++ b/public/422.html
@@ -0,0 +1,67 @@
+
+
+
+ The change you wanted was rejected (422)
+
+
+
+
+
+
+
+
+
The change you wanted was rejected.
+
Maybe you tried to change something you didn't have access to.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/public/500.html b/public/500.html
new file mode 100644
index 0000000..061abc5
--- /dev/null
+++ b/public/500.html
@@ -0,0 +1,66 @@
+
+
+
+ We're sorry, but something went wrong (500)
+
+
+
+
+
+
+
+
+
We're sorry, but something went wrong.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..e69de29
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..3c9c7c0
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,5 @@
+# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
+#
+# To ban all spiders from the entire site uncomment the next two lines:
+# User-agent: *
+# Disallow: /
diff --git a/test/fixtures/.keep b/test/fixtures/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/boards.yml b/test/fixtures/boards.yml
new file mode 100644
index 0000000..43b2029
--- /dev/null
+++ b/test/fixtures/boards.yml
@@ -0,0 +1,13 @@
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+music:
+ name: Music
+ videos: chocolate, nooo_daaamn
+
+misc:
+ name: Misc
+ videos: macbook, nooo_daaamn
+
+ruby:
+ name: Ruby
+ videos: tenderlove
diff --git a/test/fixtures/videos.yml b/test/fixtures/videos.yml
new file mode 100644
index 0000000..297ee45
--- /dev/null
+++ b/test/fixtures/videos.yml
@@ -0,0 +1,22 @@
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+chocolate:
+ url: https://youtu.be/EwTZ2xpQwpA?list=LLqqGvuVycgY7ZC3ORaF40rA
+ yt_id: EwTZ2xpQwpA
+# yt_data:
+
+nooo_daaamn:
+ url: https://youtu.be/OIvX8g220Ls?list=FLqqGvuVycgY7ZC3ORaF40rA
+ yt_data: '{"kind":"youtube#video","etag":"\"mie-I9wWQF7ndS7wC10DLBkzLlg/x7i_ZQr0alCGtPoZRV2ztoejqhs\"","id":"OIvX8g220Ls","snippet":{"publishedAt":"2015-01-11T13:40:27.000Z","channelId":"UC0Zm6D7ZhHO3EgRQ88F01NQ","title":"The Best Dance In the World;He Killed I never seen before nooo daaamn","description":"He Killed I never seen before nooo daaamn","thumbnails":{"default":{"url":"https://i.ytimg.com/vi/OIvX8g220Ls/default.jpg","width":120,"height":90},"medium":{"url":"https://i.ytimg.com/vi/OIvX8g220Ls/mqdefault.jpg","width":320,"height":180},"high":{"url":"https://i.ytimg.com/vi/OIvX8g220Ls/hqdefault.jpg","width":480,"height":360}},"channelTitle":"D Max","categoryId":"10","liveBroadcastContent":"none","localized":{"title":"The Best Dance In the World;He Killed I never seen before nooo daaamn","description":"He Killed I never seen before nooo daaamn"}}}'
+ created_at: <%= 2.seconds.from_now %>
+
+macbook:
+ url: https://youtu.be/KHZ8ek-6ccc?list=FLqqGvuVycgY7ZC3ORaF40rA
+ created_at: <%= 2.days.ago %>
+# yt_data:
+
+tenderlove:
+ url: https://www.youtube.com/watch?v=xMFs9DTympQ
+ yt_id: xMFs9DTympQ
+ yt_data: '{"kind":"youtube#video","etag":"\"mie-I9wWQF7ndS7wC10DLBkzLlg/N9SJ8SpyW6PI-gTKnRGx5sLtHbI\"","id":"xMFs9DTympQ","snippet":{"publishedAt":"2016-05-13T16:58:29.000Z","channelId":"UCWnPjmqvljcafA0z2U1fwKQ","title":"RailsConf 2016 - Opening Day 3 Keynote","description":"Aaron was born","thumbnails":{"default":{"url":"https://i.ytimg.com/vi/xMFs9DTympQ/default.jpg","width":120,"height":90},"medium":{"url":"https://i.ytimg.com/vi/xMFs9DTympQ/mqdefault.jpg","width":320,"height":180},"high":{"url":"https://i.ytimg.com/vi/xMFs9DTympQ/hqdefault.jpg","width":480,"height":360},"standard":{"url":"https://i.ytimg.com/vi/xMFs9DTympQ/sddefault.jpg","width":640,"height":480},"maxres":{"url":"https://i.ytimg.com/vi/xMFs9DTympQ/maxresdefault.jpg","width":1280,"height":720}},"channelTitle":"Confreaks","tags":["rails","railsconf","ruby on rails","ruby","tenderlove"],"categoryId":"28","liveBroadcastContent":"none","defaultLanguage":"en"}}'
+ created_at: <%= 3.days.ago %>
\ No newline at end of file
diff --git a/test/integration/.keep b/test/integration/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/test/integration/board_int_test.rb b/test/integration/board_int_test.rb
new file mode 100644
index 0000000..5f2e4c0
--- /dev/null
+++ b/test/integration/board_int_test.rb
@@ -0,0 +1,72 @@
+require 'integration/integration_test_helper'
+
+class BoardIntTest < ActionDispatch::IntegrationTest
+
+ def test_index_current_board
+
+ # no board
+ refute_selector '.board_header'
+
+ # changing board
+ select_board 'Music'
+ assert_selector '.board_header .board_title', text: 'Music'
+
+ # walk out and back to see if current board is persisted
+ visit 'http://google.com'
+ visit root_path
+ assert_selector '.board_header .board_title', text: 'Music'
+
+ # make sure we clear the current board by selecting 'All Videos'
+ select_board
+ refute_selector '.board_header'
+ visit 'http://google.com'
+ visit root_path
+ refute_selector '.board_header'
+ end
+
+ def test_create_edit_delete
+
+ board_name = 'My Board'
+
+ # create a new board
+ within 'form#new_board' do
+ fill_in 'board_name', with: board_name
+ click_button 'Create'
+ end
+ assert_selector '.alert-success', text: "Board #{board_name} created."
+
+ # select it
+ select board_name
+ assert_selector '.alert-info', text: 'No videos on this board yet'
+
+ # rename it
+ within '.board_header' do
+ assert_selector '.board_title', text: board_name
+ board_name += '2'
+ fill_in 'board_name', with: board_name
+ click_button 'Rename This Board'
+ end
+ assert_selector '.alert-success'
+ assert_selector '.board_title', text: board_name
+
+ # assign a video to it
+ aaron = videos :tenderlove
+ board = Board.find_by! name: board_name
+ board.videos << aaron
+
+ visit root_path
+ assert_selector ".video##{dom_id aaron}"
+
+ # delete it
+ within('form.edit_board') do
+ accept_confirm { click_link 'Delete' }
+ end
+
+ assert_selector '.alert-success', text: "Board #{board_name} deleted."
+
+ # checking DB
+ refute Board.exists?(board.id)
+ refute_includes aaron.reload.boards, board
+ end
+
+end
\ No newline at end of file
diff --git a/test/integration/integration_test_helper.rb b/test/integration/integration_test_helper.rb
new file mode 100644
index 0000000..ae37bfa
--- /dev/null
+++ b/test/integration/integration_test_helper.rb
@@ -0,0 +1,47 @@
+require 'test_helper'
+
+
+require 'capybara/rails'
+Capybara.default_driver = :webkit
+
+Capybara::Webkit.configure do |config|
+ config.allow_unknown_urls
+ config.skip_image_loading
+end
+
+
+class ActionDispatch::IntegrationTest
+ # Make the Capybara DSL available in all integration tests
+ include Capybara::DSL
+
+ include ActionView::RecordIdentifier # to use dom_id() method in tests
+
+ self.use_transactional_fixtures = false
+
+ def setup
+ visit root_path
+ end
+
+ # Reset sessions
+ # Use super wherever this method is redefined in your individual test classes
+ def teardown
+ Capybara.reset_sessions!
+ end
+
+
+
+ #= helpers ===========================================
+
+ MODAL_BODY = '.modal-body'
+ DISMISS_BUTTON = 'button.close'
+
+ def select_board name = '- All Videos -'
+ select name, from: 'current_board_id', wait: 1
+ end
+
+ def select_board_and_assert_video board_name, video_title
+ select_board board_name
+ assert_selector '.board_title', text: board_name
+ assert_selector '.video .video_title', text: video_title
+ end
+end
\ No newline at end of file
diff --git a/test/integration/video_int_test.rb b/test/integration/video_int_test.rb
new file mode 100644
index 0000000..b1eb33a
--- /dev/null
+++ b/test/integration/video_int_test.rb
@@ -0,0 +1,129 @@
+require 'integration/integration_test_helper'
+
+class VideoIntTest < ActionDispatch::IntegrationTest
+
+ def test_index
+
+ videos = all('.video').to_a
+ assert_equal 4, videos.length
+
+ # check the ordering + display
+ [:nooo_daaamn, :chocolate, :macbook, :tenderlove].map{|v| videos v }.each_with_index do |v, i|
+ within videos[i] do
+ assert_selector '.video_title', text: v.title
+ end
+ end
+
+ # change board
+ select_board 'Music'
+ # we check that the associated videos are displayed
+ assert_selector '.video', count: 2
+ [:chocolate, :nooo_daaamn].each{|v| assert_selector "##{dom_id(videos(v))}" }
+
+ # try without videos
+ Video.delete_all
+
+ visit root_path
+ assert_selector '.alert-info', text: 'No videos on this board yet'
+ select_board
+ assert_selector '.alert-info', text: 'No videos added yet'
+ end
+
+
+ def test_add_video_with_boards
+
+ another_aaron_url = 'https://www.youtube.com/watch?v=7amxsgW2gZs'
+ title_snippet = 'Aaron Patterson'
+
+ # add a video
+ within 'form#add_video' do
+ find('#video_url').set another_aaron_url
+ click_button 'Add'
+ end
+ assert_selector '.modal-title', text: 'Add Video' # set by js
+ within MODAL_BODY do
+
+ assert_selector '.new_video_title', text: title_snippet
+ assert_selector '.video_desc', text: 'DHH'
+ # we select 2 boards for the video
+ within 'form#new_video' do
+ check 'Ruby'
+ check 'Music'
+ click_button 'Add Video'
+ end
+
+ end
+ refute_selector MODAL_BODY
+ assert_selector '.alert-success', text: 'Video added.'
+ assert_selector '.video .video_title', text: title_snippet
+
+ # verify it is displaying in selected boards
+ ['Ruby', 'Music'].each{|bn| select_board_and_assert_video bn, title_snippet }
+
+ # try again with same video url
+ within 'form#add_video' do
+ find('#video_url').set another_aaron_url
+ click_button 'Add'
+ end
+ within MODAL_BODY do
+ assert_selector '.alert-warning', text: 'already added'
+ end
+ find(DISMISS_BUTTON).click
+
+ # try again with an invalid URL
+ within 'form#add_video' do
+ find('#video_url').set 'invalid'
+ click_button 'Add'
+ end
+ within MODAL_BODY do
+ assert_selector '.alert-warning', text: "Couldn't add the video with URL"
+ end
+ find(DISMISS_BUTTON).click
+
+ # check note about no board created
+ Board.delete_all
+ Video.find_by!(url: another_aaron_url).destroy
+ within 'form#add_video' do
+ find('#video_url').set another_aaron_url
+ click_button 'Add'
+ end
+ within MODAL_BODY do
+ assert_selector 'strong', text: 'No boards created yet'
+ end
+
+ end
+
+ def test_show_and_add_to_boards
+
+ aaron_vid = videos :tenderlove
+
+ within("##{dom_id aaron_vid}") { click_link 'Details' }
+ # check the dialog
+ assert_selector '.modal-title', text: aaron_vid.title # set by js
+ within MODAL_BODY do
+ assert_selector '.video_desc', text: aaron_vid.description
+ # assigning boards
+ within '.edit_video' do
+ check 'Ruby'
+ check 'Music'
+
+ click_button 'Assign'
+ end
+ end
+ assert_selector '.alert-success', text: 'Video updated.'
+
+ # verify it is displaying in selected boards
+ ['Ruby', 'Music'].each{|bn| select_board_and_assert_video bn, aaron_vid.title }
+ end
+
+ def test_destroy
+ aaron_vid = videos :tenderlove
+
+ accept_confirm do
+ within("##{dom_id aaron_vid}") { click_link 'Delete' }
+ end
+
+ refute Video.exists?(aaron_vid.id)
+ end
+
+end
diff --git a/test/models/.keep b/test/models/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/test/models/board_test.rb b/test/models/board_test.rb
new file mode 100644
index 0000000..67192e9
--- /dev/null
+++ b/test/models/board_test.rb
@@ -0,0 +1,23 @@
+require 'test_helper'
+
+class BoardTest < ActiveSupport::TestCase
+
+ have_and_belong_to_many(:videos)
+
+ should validate_presence_of(:name)
+ should validate_uniqueness_of(:name)
+
+ def test_scopes
+ compare_block = ->(a,b) { a.name <= b.name }
+ # this makes sure it's not ordered by chance
+ refute Board.all.each_cons(2).all?(&compare_block)
+ assert Board.default_order.each_cons(2).all?(&compare_block)
+ end
+
+ def test_callbacks
+ b = Board.new name: ' fe esse '
+ b.save!
+ assert_equal 'Fe Esse', b.name
+ end
+
+end
diff --git a/test/models/video_test.rb b/test/models/video_test.rb
new file mode 100644
index 0000000..f8feec8
--- /dev/null
+++ b/test/models/video_test.rb
@@ -0,0 +1,74 @@
+require 'test_helper'
+
+class VideoTest < ActiveSupport::TestCase
+
+ should have_and_belong_to_many(:boards)
+
+ should validate_presence_of(:url)
+ should validate_presence_of(:yt_id)
+
+ def test_scopes
+ compare_block = ->(a,b) { a.created_at >= b.created_at }
+ refute Video.all.each_cons(2).all?(&compare_block)
+ assert Video.default_order.each_cons(2).all?(&compare_block)
+ end
+
+ def test_validations
+ # yt_id uniqueness
+ vid = Video.new url: 'the url', yt_id: videos(:chocolate).yt_id
+ refute vid.valid?
+ end
+
+ def test_yt_data
+ vid = videos :nooo_daaamn
+ # attributes interface
+ assert_equal 'The Best Dance In the World;He Killed I never seen before nooo daaamn', vid.title
+ assert_equal 'He Killed I never seen before nooo daaamn', vid.description
+ assert_nil vid.tags
+ assert_equal 'https://i.ytimg.com/vi/OIvX8g220Ls/mqdefault.jpg', vid.thumbnail_url
+
+ vid = Video.new
+ # if no data available, we fail gracefully
+ assert_nothing_raised do
+
+ assert_nil vid.yt_id
+ assert_nil vid.title
+ assert_nil vid.thumbnail_url
+ end
+ # faking yt data for this test
+ vid.add_yt_data(JSON.parse('{"kind":"youtube#video","etag":"\"etag\"","id":"yt_id","snippet":{"title":"The Best Dance"}}'))
+ # we check that the id is properly set when setting the yt data
+ assert_equal 'yt_id', vid.yt_id
+ assert_equal 'The Best Dance', vid.title
+ end
+
+ def test_yt_id
+ vid = Video.new url: 'youtu.be/sGE4HMvDe-Q'
+ # fallback on url
+ assert_equal 'sGE4HMvDe-Q', vid.yt_id
+ # as path (with protocol)
+ vid.url = 'https://youtu.be/OIvX8g220Ls?list=FLqqGvuVycgY7ZC3ORaF40rA'
+ assert_equal 'OIvX8g220Ls', vid.yt_id
+ # as url param (without protocol)
+ vid.url = 'www.youtube.com/watch?v=Lp7E973zozc&feature=relmfu'
+ assert_equal 'Lp7E973zozc', vid.yt_id
+ # malformed url
+ vid.url = 'fds fes es fe'
+ assert_nil vid.yt_id
+
+ # from yt_data
+ vid = videos :nooo_daaamn
+ assert_nil vid.read_attribute(:yt_id)
+ assert_equal 'OIvX8g220Ls', vid.yt_id
+ # from DB
+ vid = videos :chocolate
+ vid.yt_data = nil
+ assert_equal 'EwTZ2xpQwpA', vid.yt_id
+ end
+
+ def test_other_methods
+ vid = videos :nooo_daaamn
+
+ assert_equal 'http://www.youtube-nocookie.com/embed/OIvX8g220Ls?html5=1', vid.embed_url
+ end
+end
diff --git a/test/services/retrieve_youtube_data_test.rb b/test/services/retrieve_youtube_data_test.rb
new file mode 100644
index 0000000..312f647
--- /dev/null
+++ b/test/services/retrieve_youtube_data_test.rb
@@ -0,0 +1,117 @@
+require 'test_helper'
+
+class RetrieveYoutubeDataTest < ActiveSupport::TestCase
+
+ # @todo
+ # To avoid hitting Youtube during testing,
+ # be at the mercy of data changing
+ # and test when the api is down, we can use:
+ # https://github.com/bblimke/webmock
+
+ CACHE_THRESHOLD_TIME = 0.01
+
+ def test_service
+ Rails.cache.clear
+
+ # Normal flow
+ vid = videos(:macbook)
+ # First time we hit the API
+ time = Benchmark.realtime do
+ vid.yt_data = RetrieveYoutubeData.new(vid.yt_id).call
+ end
+ assert_operator time, :>, CACHE_THRESHOLD_TIME
+
+
+ # A second time, we should hit the cache
+ time = Benchmark.realtime do
+ RetrieveYoutubeData.new(vid.yt_id).call
+ end
+ assert_operator time, :<, CACHE_THRESHOLD_TIME
+
+
+ # did we get the correct data?
+ assert_equal "Apple Engineer Talks about the New 2015 Macbook", vid.title
+
+ # make sure the cache expires
+ travel 1.hour do
+ assert_nil Rails.cache.read(vid.yt_id, namespace: RetrieveYoutubeData::CACHE_NAMESPACE)
+ end
+
+ # we get an exception with wrong ID
+ assert_raises(Exception) { RetrieveYoutubeData.new('invalid id').call}
+
+ end
+end
+
+# Sample response (6.6.16)
+# {
+# "kind" => "youtube#videoListResponse",
+# "etag" => "\"mie-I9wWQF7ndS7wC10DLBkzLlg/1Hhkdan3ZM2Rx5TU8rKV8ibQZIw\"",
+# "pageInfo" => {
+# "totalResults" => 1,
+# "resultsPerPage" => 1
+# },
+# "items" => [
+# [0] {
+# "kind" => "youtube#video",
+# "etag" => "\"mie-I9wWQF7ndS7wC10DLBkzLlg/okaVZsQkDgSsvBMGF35JjIlutsI\"",
+# "id" => "KHZ8ek-6ccc",
+# "snippet" => {
+# "publishedAt" => "2015-03-11T17:29:20.000Z",
+# "channelId" => "UCXzySgo3V9KysSfELFLMAeA",
+# "title" => "Apple Engineer Talks about the New 2015 Macbook",
+# "description" => "an Apple engineer explains how they developed the new 2015 Macbook and the day Tim Cook saw it for the first time.\n\nOriginal Source: Risitas y las paelleras: https://youtu.be/cDphUib5iG4\n\n-----------------\n\nTo get the latest on my work follow me on G+ Instagram or Twitter\n\nGoogle+ - http://gplus.to/wicked4u2c\nInstagram - http://instagram.com/mobileg33k\nFollow me on Twitter - http://twitter.com/Wicked4u2c",
+# "thumbnails" => {
+# "default" => {
+# "url" => "https://i.ytimg.com/vi/KHZ8ek-6ccc/default.jpg",
+# "width" => 120,
+# "height" => 90
+# },
+# "medium" => {
+# "url" => "https://i.ytimg.com/vi/KHZ8ek-6ccc/mqdefault.jpg",
+# "width" => 320,
+# "height" => 180
+# },
+# "high" => {
+# "url" => "https://i.ytimg.com/vi/KHZ8ek-6ccc/hqdefault.jpg",
+# "width" => 480,
+# "height" => 360
+# },
+# "standard" => {
+# "url" => "https://i.ytimg.com/vi/KHZ8ek-6ccc/sddefault.jpg",
+# "width" => 640,
+# "height" => 480
+# },
+# "maxres" => {
+# "url" => "https://i.ytimg.com/vi/KHZ8ek-6ccc/maxresdefault.jpg",
+# "width" => 1280,
+# "height" => 720
+# }
+# },
+# "channelTitle" => "Armando Ferreira",
+# "tags" => [
+# [ 0] "Apple",
+# [ 1] "2015 Macbook",
+# [ 2] "Macbook",
+# [ 3] "Apple Inc. (Publisher)",
+# [ 4] "Tim Cook",
+# [ 5] "Steve Jobs",
+# [ 6] "MacBook Pro (Computer)",
+# [ 7] "Retina",
+# [ 8] "Risitas",
+# [ 9] "Engineer",
+# [10] "Apple Engineer",
+# [11] "Comedy",
+# [12] "Viral",
+# [13] "Funny"
+# ],
+# "categoryId" => "28",
+# "liveBroadcastContent" => "none",
+# "localized" => {
+# "title" => "Apple Engineer Talks about the New 2015 Macbook",
+# "description" => "an Apple engineer explains how they developed the new 2015 Macbook and the day Tim Cook saw it for the first time.\n\nOriginal Source: Risitas y las paelleras: https://youtu.be/cDphUib5iG4\n\n-----------------\n\nTo get the latest on my work follow me on G+ Instagram or Twitter\n\nGoogle+ - http://gplus.to/wicked4u2c\nInstagram - http://instagram.com/mobileg33k\nFollow me on Twitter - http://twitter.com/Wicked4u2c"
+# }
+# }
+# }
+# ]
+# }
\ No newline at end of file
diff --git a/test/test_helper.rb b/test/test_helper.rb
new file mode 100644
index 0000000..4291cf8
--- /dev/null
+++ b/test/test_helper.rb
@@ -0,0 +1,8 @@
+ENV['RAILS_ENV'] ||= 'test'
+require File.expand_path('../../config/environment', __FILE__)
+require 'rails/test_help'
+
+class ActiveSupport::TestCase
+ # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
+ fixtures :all
+end
diff --git a/vendor/assets/javascripts/.keep b/vendor/assets/javascripts/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/vendor/assets/stylesheets/.keep b/vendor/assets/stylesheets/.keep
new file mode 100644
index 0000000..e69de29