diff --git a/Gemfile b/Gemfile new file mode 100755 index 0000000..30e26c9 --- /dev/null +++ b/Gemfile @@ -0,0 +1,85 @@ +source 'http://rubygems.org' + +# there is an issue with rake 0.9.0 +gem 'rake', '0.8.7' +# Gems for the development and production environment. +gem 'rails', '3.0.7' +# use the HAML templating engine +gem 'haml' +# generates haml files instead of ERB +gem 'haml-rails' +# use the SASS engine for CSS +gem 'sass' +# paginates large results +gem 'will_paginate' +# date validation +gem 'validates_timeliness' +# used for version control rake task +gem 'heroku' +# used for file upload +gem 'carrierwave' +gem 'fog' +# used for image processing +gem 'mini_magick' +# used for processing jobs asynchronously in the background +gem 'delayed_job_active_record' +# used to encode videos +gem 'zencoder' +# does modern web browser validation for us +gem 'browser' +# used to send a long running rake task into +# the background as a delayed_job since heroku times them out +gem 'delayed_task' +# used for getting xml data from YouTube and Vimeo links +gem 'httparty' +# used for memcached interaction +gem 'dalli' +# used for facebook/twitter integration +gem 'omniauth' +gem 'omniauth-facebook' +gem 'omniauth-twitter' +# used for accessing the facebook graph api +gem 'koala' +# used for accessing the twitter api +gem 'twitter', '~> 4.0' +# used for creating nested api templates +gem 'rabl' +# used as the JSON parser for the rabl gem +gem 'yajl-ruby' +# this is to get rid of an error with json 1.4.6 +gem 'json', '~> 1.7' + +# Gems for the production & staging environments only +group :production, :staging do + # full text search + gem 'thinking-sphinx' + gem 'flying-sphinx' + # autoscales delayed_job workers via hirefireapp.com + gem 'hirefireapp' + # use Factory Girl to create users and video posts + gem 'factory_girl_rails' + # use faker to generate pseudo information + gem 'faker' + gem 'pg' +end + +# Gems for the production environment only +group :production do + # logs all of our errors (rails/javascript) to airbrake + gem 'airbrake' +end + +# Gems for the local/dev/staging environment. Make sure to +# put test-only gems in this group so their generators +# and rake tasks are available in development mode: +group :development, :test do + gem 'sqlite3' + # use RSpec for testing instead of Test::Unit + gem "rspec-rails" + # use Webrat for RSpec helper functions + gem 'webrat' + # gives you the ability to launch a page at any point during test + gem 'launchy' + # wipes the db after each run instead of using transactional fixtures + gem 'database_cleaner' +end \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100755 index 0000000..9373660 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,265 @@ +GEM + remote: http://rubygems.org/ + specs: + abstract (1.0.0) + actionmailer (3.0.7) + actionpack (= 3.0.7) + mail (~> 2.2.15) + actionpack (3.0.7) + activemodel (= 3.0.7) + activesupport (= 3.0.7) + builder (~> 2.1.2) + erubis (~> 2.6.6) + i18n (~> 0.5.0) + rack (~> 1.2.1) + rack-mount (~> 0.6.14) + rack-test (~> 0.5.7) + tzinfo (~> 0.3.23) + activemodel (3.0.7) + activesupport (= 3.0.7) + builder (~> 2.1.2) + i18n (~> 0.5.0) + activerecord (3.0.7) + activemodel (= 3.0.7) + activesupport (= 3.0.7) + arel (~> 2.0.2) + tzinfo (~> 0.3.23) + activeresource (3.0.7) + activemodel (= 3.0.7) + activesupport (= 3.0.7) + activesupport (3.0.7) + addressable (2.3.4) + airbrake (3.1.12) + activesupport + builder + json + arel (2.0.10) + browser (0.1.6) + builder (2.1.2) + carrierwave (0.5.8) + activesupport (~> 3.0) + dalli (2.6.4) + database_cleaner (1.0.1) + delayed_job (3.0.5) + activesupport (~> 3.0) + delayed_job_active_record (0.4.4) + activerecord (>= 2.1.0, < 4) + delayed_job (~> 3.0) + delayed_task (0.2.0) + diff-lcs (1.2.4) + erubis (2.6.6) + abstract (>= 1.0.0) + excon (0.22.1) + factory_girl (4.2.0) + activesupport (>= 3.0.0) + factory_girl_rails (4.2.1) + factory_girl (~> 4.2.0) + railties (>= 3.0.0) + faker (1.1.2) + i18n (~> 0.5) + faraday (0.8.7) + multipart-post (~> 1.1) + faraday_middleware (0.9.0) + faraday (>= 0.7.4, < 0.9) + flying-sphinx (1.0.0) + faraday_middleware (~> 0.7) + multi_json (>= 1.3.0) + pusher-client (~> 0.3) + rash (~> 0.3.0) + riddle (>= 1.5.6) + thinking-sphinx + fog (1.11.1) + builder + excon (~> 0.20) + formatador (~> 0.2.0) + json (~> 1.7) + mime-types + net-scp (~> 1.1) + net-ssh (>= 2.1.3) + nokogiri (~> 1.5.0) + ruby-hmac + formatador (0.2.4) + haml (3.1.8) + haml-rails (0.3.4) + actionpack (~> 3.0) + activesupport (~> 3.0) + haml (~> 3.0) + railties (~> 3.0) + hashie (1.2.0) + heroku (2.39.4) + heroku-api (~> 0.3.7) + launchy (>= 0.3.2) + netrc (~> 0.7.7) + rest-client (~> 1.6.1) + rubyzip + heroku-api (0.3.11) + excon (~> 0.22.1) + hirefireapp (0.2.0) + httparty (0.11.0) + multi_json (~> 1.0) + multi_xml (>= 0.5.2) + httpauth (0.2.0) + i18n (0.5.0) + innertube (1.0.2) + json (1.8.0) + jwt (0.1.8) + multi_json (>= 1.5) + koala (1.6.0) + addressable (~> 2.2) + faraday (~> 0.8) + multi_json (~> 1.3) + launchy (2.3.0) + addressable (~> 2.3) + mail (2.2.20) + activesupport (>= 2.3.6) + i18n (>= 0.4.0) + mime-types (~> 1.16) + treetop (~> 1.4.8) + mime-types (1.23) + mini_magick (3.6.0) + subexec (~> 0.2.1) + multi_json (1.7.6) + multi_xml (0.5.4) + multipart-post (1.2.0) + net-scp (1.1.1) + net-ssh (>= 2.6.5) + net-ssh (2.6.7) + netrc (0.7.7) + nokogiri (1.5.9) + oauth (0.4.7) + oauth2 (0.8.1) + faraday (~> 0.8) + httpauth (~> 0.1) + jwt (~> 0.1.4) + multi_json (~> 1.0) + rack (~> 1.2) + omniauth (1.1.4) + hashie (>= 1.2, < 3) + rack + omniauth-facebook (1.4.1) + omniauth-oauth2 (~> 1.1.0) + omniauth-oauth (1.0.1) + oauth + omniauth (~> 1.0) + omniauth-oauth2 (1.1.1) + oauth2 (~> 0.8.0) + omniauth (~> 1.0) + omniauth-twitter (0.0.16) + multi_json (~> 1.3) + omniauth-oauth (~> 1.0) + pg (0.15.1) + polyglot (0.3.3) + pusher-client (0.3.0) + ruby-hmac (~> 0.4.0) + websocket (~> 1.0.0) + rabl (0.8.5) + activesupport (>= 2.3.14) + rack (1.2.8) + rack-mount (0.6.14) + rack (>= 1.0.0) + rack-test (0.5.7) + rack (>= 1.0) + rails (3.0.7) + actionmailer (= 3.0.7) + actionpack (= 3.0.7) + activerecord (= 3.0.7) + activeresource (= 3.0.7) + activesupport (= 3.0.7) + bundler (~> 1.0) + railties (= 3.0.7) + railties (3.0.7) + actionpack (= 3.0.7) + activesupport (= 3.0.7) + rake (>= 0.8.7) + thor (~> 0.14.4) + rake (0.8.7) + rash (0.3.2) + hashie (~> 1.2.0) + rest-client (1.6.7) + mime-types (>= 1.16) + riddle (1.5.6) + rspec-core (2.13.1) + rspec-expectations (2.13.0) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.13.1) + rspec-rails (2.13.2) + actionpack (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec-core (~> 2.13.0) + rspec-expectations (~> 2.13.0) + rspec-mocks (~> 2.13.0) + ruby-hmac (0.4.0) + rubyzip (0.9.9) + sass (3.2.9) + simple_oauth (0.2.0) + sqlite3 (1.3.7) + subexec (0.2.3) + thinking-sphinx (2.1.0) + activerecord (>= 3.0.3) + builder (>= 2.1.2) + innertube (~> 1.0.2) + riddle (>= 1.5.6) + thor (0.14.6) + timeliness (0.3.7) + treetop (1.4.14) + polyglot + polyglot (>= 0.3.1) + twitter (4.7.0) + faraday (~> 0.8, < 0.10) + multi_json (~> 1.0) + simple_oauth (~> 0.2) + tzinfo (0.3.37) + validates_timeliness (3.0.14) + timeliness (~> 0.3.6) + webrat (0.7.3) + nokogiri (>= 1.2.0) + rack (>= 1.0) + rack-test (>= 0.5.3) + websocket (1.0.7) + will_paginate (3.0.4) + yajl-ruby (1.1.0) + zencoder (2.4.4) + multi_json + +PLATFORMS + ruby + +DEPENDENCIES + airbrake + browser + carrierwave + dalli + database_cleaner + delayed_job_active_record + delayed_task + factory_girl_rails + faker + flying-sphinx + fog + haml + haml-rails + heroku + hirefireapp + httparty + json (~> 1.7) + koala + launchy + mini_magick + omniauth + omniauth-facebook + omniauth-twitter + pg + rabl + rails (= 3.0.7) + rake (= 0.8.7) + rspec-rails + sass + sqlite3 + thinking-sphinx + twitter (~> 4.0) + validates_timeliness + webrat + will_paginate + yajl-ruby + zencoder diff --git a/README.markdown b/README.markdown new file mode 100755 index 0000000..d41f2ed --- /dev/null +++ b/README.markdown @@ -0,0 +1,232 @@ +## Brevidy +Brevidy was a video social network that I built with Ruby on Rails 3.0.7, HAML, Bootstrap, and jQuery that was released into beta testing February 2012. Brevidy closed down shortly after due to the high costs of server hosting in addition to all of the add-on services such as video transcoding, email, error exception handling, database storage, etc. Unfortunately, it's very difficult to get investor interest without knowing a friend of a friend, so I wasn't able to afford the rising costs that accompanied the user growth. + +You can see a mostly (i.e. I've disabled certain features to save me money) working demo here: http://brevidy.heroku.com + +The idea of Brevidy was to create a place that people could upload their own videos or cross-post YouTube/Vimeo videos into public or private channels that other people could subscribe to. When you subscribe to other people's channels, all videos from those channels show up in an infinitely-scrolling stream and you can easily re-share, comment, or badge a video. It was a beautiful website, but video is a difficult and expensive medium so unfortunately it didn't work out. + +## Open Sourcing +I learned a lot about web programming by creating Brevidy and instead of it sitting and collecting dust, I wanted to open source it for others to learn from and use bits and pieces in their own projects. If you want to create your own video social network using Brevidy as a starting point, be my guest, but **do not** use the Brevidy name, logo, branding, or badges in your website. Just make sure you give me (Rob Phillips) credit in the About section and I welcome any and all PayPal donations + + + +The source code can be used in personal and commercial products for free as long as you give me attribution. Please also remember that Brevidy was only possible by using other open source projects, so be sure to give them any credit that is necessary according to their respective licenses. + +## Collaboration +I would love to see Brevidy come back to life one day, even if it's under a different name and owner. I welcome all collaborators on this project so please feel free to fork and issue pull requests to improve upon Brevidy. Much of it was built while I was teaching myself Ruby / Ruby on Rails so I'm sure there is a lot that could be easily improved upon. If you show enough interest, I would gladly accept people to start working as full-time collaborators and grant you read-write access to the repo so you can commit directly. + +## 3rd Party Services +Brevidy uses the following 3rd party services: + + * Zencoder - Very quickly handles all video transcoding from any number of formats for a fair price. The founder is a great guy and very helpful + * Amazon S3 & Cloudfront - Handles all image and video storage in addition to high-speed CDN streaming of videos that are hosted on Brevidy. Amazon is incredible and cheap + * SendGrid - Their customer service was pretty terrible, but it was the only option I had at the time for sending all of the emails + * Flying Sphinx - Great search tool for Thinking Sphinx. The founder is really quick to help and a nice guy + * HireFire - Manages hiring and firing worker processes on Heroku. I'm not sure why Heroku never built this automation into their system from the beginning, but necessity is the mother of all invention (and capitalism) so kudos to the creator + +## Getting Started Locally + + 1. [Download and unzip these files](http://brevidyassets.s3.amazonaws.com/Bucket_Setup.zip) into the root directory of your Amazon S3 bucket (you'll need to modify the `crossdomain.xml` and `clientaccesspolicy.xml` files before uploading videos will work) + 2. Clone this repository to a local directory on your computer + 3. Create a new ruby set using something like RVM and run `bundle install` in the local repo directory + 4. Run `rake db:reset` to reset and seed the database with default tables and some necessary data + 5. Run `rails generate delayed_job:active_record` and `rake db:migrate` to add the Delayed Jobs table + 6. Update the access keys, buckets, etc. throughout the app (see below for a list) + 7. Run `rails s` to start the Rails server + 8. Open up a new terminal window and run `rake jobs:work` to start the Delayed Jobs workers + +Access keys, buckets, etc. that you'll need to update: + +```ruby +app/models/video_graph.rb +config/amazon/amazon_cf.yml +config/amazon/amazon_s3.yml +config/amazon/amazon_s3_constants.yml +config/amazon/*.pem +config/application.rb +config/environments/development.rb +config/environments/production.rb +config/environments/staging.rb +config/environments/test.rb +config/initializers/airbrake.rb +config/initializers/secret_token.rb +config/initializers/omniauth.rb +lib/tasks/deploy.rake +``` + +Note: Brevidy's search is built on top of Flying Sphinx, which only runs on Heroku. So if you type something into the search box and yell "Rob, this is broken!!!" then you need to understand that search doesn't work locally, it only works on Heroku after you've set up Flying Sphinx. + +## Getting Started on Heroku + * Complete the tasks in the **Getting Started Locally** section to ensure the app builds and runs locally first + * Make sure you have a Heroku account and the [Heroku Toolbelt](https://toolbelt.heroku.com/) installed + * Open up the command line or terminal and type: + +```ruby +heroku create --remote production +``` + + * At a minimum you will need to install these Heroku add-ons: + +```ruby +memcache:5mb +sendgrid:starter +flying_sphinx:wooden +zencoder:1k +airbrake:developer +``` + +When Brevidy was running in production, these were some of the add-ons that I had installed (not sure if these add-on names have changed or not) + +```ruby +cron:hourly (This doesn't exist anymore, you'll have to convert it over to Heroku's Scheduler) +custom_domains:basic +custom_error_pages +deployhooks:email +flying_sphinx:ceramic +airbrake:developer_plus +logging:expanded +memcache:5mb +newrelic:standard +pgbackups:auto-week +sendgrid:bronze +shared-database:20gb +zencoder:1k +``` + + * Push the code up to your Heroku server that you just created using `git push -f git@heroku.com:your_app_name_here.git master:master` + * After pushing the production codebase, run `heroku run rake db:schema:load --app yourappname`, then `heroku run rake db:seed --app yourappname` to generate a seeded database with the data and tables necessary for running Brevidy + * You'll need to follow the [documentation for Flying Sphinx](http://flying-sphinx.com/docs) to configure it for search (and you'll get some errors in the Heroku logs if you try to create new users before configuring it) + +## SASS & Bootstrap +Brevidy uses the Bootstrap framework for much of it's CSS foundation. To compile the CSS (which is built using SASS files) into the final versions, run `rake css:compress` and then the output file will be in the `public/stylesheets` directory. You'll have to update the layouts to use the updated CSS files. + +Note: The source files for all SASS and Javascript are in the `assets` directory + +## Templating Engine +Brevidy uses the [HAML](http://haml-lang.com) templating engine for generating all views. All tabs in your text editor should be set to "Soft Tabs" with **2 spaces** + +To convert HTML (or ERB) to HAML: [http://html2haml.heroku.com](http://html2haml.heroku.com) +**Note:** You should *always* double check the output code for syntax errors or inefficiencies. + +## Testing Configuration +By default, Brevidy uses the [RSpec](http://rspec.info/rails) tool for unit testing with factories for test data. I was in a rush during the last iteration of Brevidy, so I didn't have time to update any of the controller tests. I'll leave it as an exercise for you and welcome any pull requests with test corrections. + +All spec and factory files should go in the `/spec` folder. + +Instead of fixtures, Brevidy uses [Factory Girl](https://github.com/thoughtbot/factory_girl_rails) for creating the following factories of default data and also [Faker](http://faker.rubyforge.org) for generating pseudo data for those objects: + + * Users + * Video Posts + * Comments & Video Responses + * Badges Given/Received + * Description Tags + * Subscribers/Subscriptions + +**Note:** If you do not currently have a test database setup, when you goto run `rspec spec` or `autotest` to run your test suite, it will return failures stating that it cannot find the object tables in the database. To fix this, make sure you clone the current database for test by running `rake db:test:clone` and all tests should pass after that. + +## Doing Work in the Background +Brevidy uses the [DelayedJob](https://github.com/collectiveidea/delayed_job) library for performing long running (background) tasks or tasks that are not time sensitive such as the following: + + * Deleting comments, video responses, users + * Creating thumbnails for videos and user images + * Uploading images to S3 and cleaning up on S3 + * Running certain rake tasks + +## Deploy Scripts +I wrote some deploy rake tasks to help out with deploying to multiple environments (testing, staging, production). Have a look in the `lib/tasks/deploy.rake` file to set them up. + +There are two options for each depending on if you need to run migrations on the database or not. + + rake deploy_staging + rake deploy_staging_with_migrations + +and + + rake deploy_production + rake deploy_production_with_migrations + +Here is a breakdown of what each task does: + +Command: + + rake deploy_#{staging OR production} + +What this does: + + * Sets the environment to know whether we want the staging or production app + * Calls `git push heroku master` to push the latest code to the targeted environment + * Restarts the Heroku server (it has been known to hang if you don't) + * Tags the git push in case you need to rollback a bad push. Tags are formatted like this: `#{APP}_release-#{Time.now.utc.strftime("%Y%m%d%H%M%S")}_#{current_commit_hash}` + +Command: + + rake deploy_#{staging OR production}_with_migrations + +What this does: + + * Sets the environment to know whether we want the staging or production app + * Calls `git push heroku master` to push the latest code to the targeted environment + * Calls `heroku maintenance:on` to put the app into Maintenance Mode (which shows our bloated hamster graphic) + * Runs the database migrations + * Restarts the Heroku server (it has been known to hang if you don't) + * Calls `heroku maintenance:off` to bring the app out of Maintenance Mode + * Tags the git push in case you need to rollback a bad push. Tags are formatted like this: `#{APP}_release-#{Time.now.utc.strftime("%Y%m%d%H%M%S")}_#{current_commit_hash}` + +## Rolling back a bad push +**Note: Pushes have to be tagged for this to work (which happens automatically if you use the rake tasks above). If you don't tag it, you are SOL and will have to depend on manually force pushing to a specific commit.** + +There are two options for each depending on if you want to rollback to the prior push or to a specific tag. + + rake rollback_staging + rake rollback_staging_to_tag + +and + + rake rollback_production + rake rollback_production_to_tag + +Here is a breakdown of what each task does: + +Command: + + rake rollback_#{staging OR production} + +What this does: + + * Sets the environment to know whether we want the staging or production app + * Calls `heroku maintenance:on` to put the app into Maintenance Mode (which shows our bloated hamster graphic) + * Checks out the LAST tagged push in a new branch on your local git repo + * Removes that tag + * Force pushes that release to Heroku master + * Deletes the bad, current release + * Retags the new push in case you need to repeat the process again. Tags are formatted like this: `#{APP}_release-#{Time.now.utc.strftime("%Y%m%d%H%M%S")}_#{current_commit_hash}` + * Switches back to the master branch + * Restarts the Heroku server (it has been known to hang if you don't) + * Calls `heroku maintenance:off` to bring the app out of Maintenance Mode + +Command: + + rake rollback_#{staging OR production}_to_tag + +To get a list of available tags: + + git tag + + # will output something like this: + # Robs-Laptop:rails robphillips$ git tag + # gotsoultesting_release-20110525195713_d94abc5dd3c30cc52c6859a374878043ebb4aaae + # gotsoultesting_release-20110527012602_d94abc5dd3c30cc52c6859a374878043ebb4aaae + # gotsoultesting_release-20110530144933_d94abc5dd3c30cc52c6859a374878043ebb4aaae + +What this does: + + * Sets the environment to know whether we want the staging or production app + * Calls `heroku maintenance:on` to put the app into Maintenance Mode (which shows our bloated hamster graphic) + * Checks out the SPECIFIED, tagged, prior release in a new branch on your local git repo + * Removes that tag + * Force pushes that release to Heroku master + * Deletes the bad, current release + * Retags the new push in case you need to repeat the process again. Tags are formatted like this: `#{APP}_release-#{Time.now.utc.strftime("%Y%m%d%H%M%S")}_#{current_commit_hash}` + * Switches back to the master branch + * Restarts the Heroku server (it has been known to hang if you don't) + * Calls `heroku maintenance:off` to bring the app out of Maintenance Mode \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100755 index 0000000..c5aa367 --- /dev/null +++ b/Rakefile @@ -0,0 +1,7 @@ +# 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__) +require 'rake' + +Brevidy::Application.load_tasks diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100755 index 0000000..899e99f --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,152 @@ +class ApplicationController < ActionController::Base + protect_from_forgery + + # Handles session / authentication + include SessionsHelper + + # Order of before_filters is important + before_filter :http_authenticate, :site_authenticate, :ensure_the_user_is_not_deactivated + + # Use HTTP BASIC authenticattion for test and development environments + def http_authenticate + if Rails.env.staging? + authenticate_or_request_with_http_basic do |username, password| + (username == Brevidy::Application::HTTP_AUTH_USERNAME && password == Brevidy::Application::HTTP_AUTH_PASSWORD) || + (username == Brevidy::Application::HTTP_AUTH_ZEN_USERNAME && password == Brevidy::Application::HTTP_AUTH_ZEN_PASSWORD) + end + end + end + + # Allows redirecting for AJAX calls as well as normal calls + def redirect_to(options = {}, response_status = {}) + if request.xhr? + render(:update) {|page| page.redirect_to(options)} + else + super(options, response_status) + end + end + + # Returns all errors for a given class name (klass) + def get_errors_for_class(klass) + if klass.errors.any? + klass.errors.full_messages.each do |msg| + msg + end + end + end + + # Handles unauthorized CSRF tokens + def handle_unverified_request + super # call the default behaviour which resets the session + cookies.delete(:remember_token) + redirect_to :login + end + + ############# + ## FILTERS ## + ############# + + # Sets instance variables for @user based on params + def set_user + @user ||= User.find_by_username(params[:username]) + render(:template => "errors/error_404", :status => 404) if @user.blank? + end + + # Sets instance variables for @video based on params + def set_video + @video ||= @user.videos.find_by_id(params[:video_id]) + render(:template => "errors/error_404", :status => 404) if (@video.blank? || !@video.is_status?(VideoGraph::READY)) + end + + # Sets a channel based on the params (if it exists) + def set_channel + channel_id = params[:channel_id] || params[:id] + @channel ||= @user.channels.find_by_id(channel_id) + render(:template => "errors/error_404", :status => 404) if @channel.blank? + end + + # Show the 4 latest featured videos for this user + def set_featured_videos + @latest_featured_videos = @user.featured_videos.limit(4) + end + + # Verifies that a given user is not blocking the current_user + def verify_current_user_is_not_blocked + unless current_user.blank? || current_user?(@user) + render(:template => "errors/error_404", :status => 404) if Blocking.where(:requesting_user => @user.id, :blocked_user => current_user.id).exists? + end + end + + # Verifies that a given user can access the channel containing the video + def verify_user_can_access_channel + unless user_can_access_channel + if (params[:controller] == "channels" && params[:action] == "show") + render(:template => "errors/private_channel", :status => 404) + else + render(:template => "errors/error_404", :status => 404) + end + end + end + + # Verifies that a given user can access either the channel or the videos within the channel + def verify_user_can_access_channel_or_video + public_token = params[:channel_token] + user_can_access_either_one = (public_token and public_token.strip == @video.channel.public_token) || user_can_access_channel + + render(:template => "errors/error_404", :status => 404) unless user_can_access_either_one + end + + # Helper for auth verifications + def user_can_access_channel + return @channel.is_accessible_by(current_user) if @channel + return @video.channel.is_accessible_by(current_user) if @video + end + + # Verifies the user owns the channel + def verify_user_owns_channel + render(:template => "errors/error_404", :status => 404) unless current_user_owns?(@channel) + end + + # Verifies the user owns the page + def verify_user_owns_page + render(:template => "errors/error_404", :status => 404) unless current_user?(@user) + end + + # Verifies the user owns the video + def verify_user_owns_video + render(:template => "errors/error_404", :status => 404) unless current_user_owns?(@video) + end + + # Redirects the user to their subscription stream if they are logged in + def redirect_to_stream_if_logged_in + redirect_to user_stream_path(current_user) if signed_in? + end + + private + # A before_filter to deny access to certain controller actions + # based on if the user is logged in or not. + def site_authenticate + deny_access unless signed_in? + end + + # A before_filter to check if the current_user has been deactivated. + # If so, they are shown an error message and told to check their email + # for the reason why. + def ensure_the_user_is_not_deactivated + unless current_user.nil? + if current_user.is_deactivated + sign_out + render(:template => "errors/error_deactivated", :status => 401) + end + end + end + + # A before_filter to check if the user is using a modern web browser. + # If they are not, we show an error message and ask them to upgrade to + # one of the supported browsers. + def check_for_modern_browser + if browser.ie6? || browser.ie7? + render(:template => "errors/error_old_browser", :status => 401) + end + end +end diff --git a/app/controllers/badges_controller.rb b/app/controllers/badges_controller.rb new file mode 100755 index 0000000..b633e60 --- /dev/null +++ b/app/controllers/badges_controller.rb @@ -0,0 +1,108 @@ +class BadgesController < ApplicationController + include ApplicationHelper + + before_filter :site_authenticate, :except => [:badges_dialog, :index] + before_filter :set_user, :set_browser_title + before_filter :verify_current_user_is_not_blocked, :only => [:index] + before_filter :set_video, :except => [:index] + before_filter :verify_user_can_access_channel_or_video, :only => [:badges_dialog, :create, :destroy] + before_filter :set_featured_videos, :only => [:index] + + # GET /:username/videos/:video_id/badges + def badges_dialog + @badges ||= @video.badges.all + + respond_to do |format| + format.js # badges_dialog.js.haml + end + end + + # GET /:username/badges + def index + @badges ||= sort_badges_by_count_for_user(get_all_active_badges, @user) + + respond_to do |format| + format.html # index.html.haml + end + end + + # POST /:username/videos/:video_id/badges + def create + new_badge ||= @video.badge_it(current_user, params[:badge_type]) + if new_badge.errors.any? + render :json => { :error => get_errors_for_class(new_badge).to_sentence }, + :status => :unprocessable_entity + else + video_owner ||= get_object_owner(@video) + total_video_badges ||= @video.badges.count + @viewing_via_token_access = (params[:channel_token] == @video.channel.public_token) + render :json => { :html => + render_to_string( :partial => 'badges/badge.html', + :locals => { :icon => new_badge, + :count => @video.badges_count_for_type(params[:badge_type]) } ), + :unbadge => + render_to_string( :partial => 'badges/unbadge.html', + :locals => { :badge => new_badge, + :video => @video, + :video_owner => video_owner, + :icon => Icon.find_by_id(params[:badge_type]) } ), + :view_all_badges_link => + render_to_string( :partial => 'badges/view_all_badges.html', + :locals => { :badges_count => total_video_badges, + :video_owner => video_owner, + :video => @video } ), + :total_video_badges => total_video_badges, + :video_id => @video.id }, + :status => :created + end + end + + # DELETE /:username/videos/:video_id/badges/:id + def destroy + badge ||= @video.badges.find_by_id(params[:id]) + if badge + # badge exists, so destroy it if user owns it + if badge.badge_from == current_user.id + video_owner ||= get_object_owner(@video) + # cache old badge + old_badge = badge + + if badge.destroy + badges_count ||= @video.badges.count + icon ||= Icon.find_by_id(badge.badge_type) + + render :json => { :badges_path => user_video_badges_dialog_path(video_owner, @video), + :this_badge_count => @video.badges_count_for_type(old_badge.badge_type), + :total_video_badges => badges_count, + :video_id => @video.id, + :badge_name => icon.name, + :give_a_badge => + render_to_string( :partial => 'badges/give_a_badge.html', + :locals => { :video => @video, + :video_owner => video_owner, + :icon => icon } ), + :view_all_badges_link => + render_to_string( :partial => 'badges/view_all_badges.html', + :locals => { :badges_count => badges_count, + :video_owner => video_owner, + :video => @video } ) }, + :status => :ok + else + render :json => { :error => "There was an error removing your badge." }, + :status => :unprocessable_entity + end + else + render :json => { :error => "You do not own that badge so you cannot remove it." }, + :status => :unauthorized + end + else + render :nothing => true, :status => :not_found + end + end + + private + # Sets a browser title for all actions + def set_browser_title + @browser_title ||= @user.name unless @user.blank? + end +end diff --git a/app/controllers/channels_controller.rb b/app/controllers/channels_controller.rb new file mode 100755 index 0000000..ce5085e --- /dev/null +++ b/app/controllers/channels_controller.rb @@ -0,0 +1,141 @@ +class ChannelsController < ApplicationController + include ApplicationHelper + + before_filter :site_authenticate, :except => [:index, :show, :show_by_token_access] + before_filter :set_user, :except => [:show_by_token_access] + before_filter :verify_current_user_is_not_blocked, :only => [:index] + before_filter :set_channel, :only => [:edit, :destroy, :show, :update] + before_filter :verify_user_owns_channel, :only => [:destroy, :edit, :update] + before_filter :set_featured_videos, :only => [:edit, :edit_featured_videos, :index, :show] + before_filter :verify_user_can_access_channel, :only => [:show] + + # GET /:username/channels/:id-slug-name-goes-here/edit + def edit + @subscribers = @channel.subscribers_as_people.paginate(:page => params[:page], :per_page => 100) + + respond_to do |format| + params[:page].to_i > 1 ? format.js : format.html + end + end + + # GET /:username/edit_featured_videos + def edit_featured_videos + # Do a quick security check to verify user + render(:template => "errors/error_404", :status => 404) and return unless current_user?(@user) + + @featured_videos = current_user.featured_videos.paginate(:page => params[:page], :per_page => 20, :order => 'featured_at DESC') + + respond_to do |format| + params[:page].to_i > 1 ? format.js : format.html + end + end + + # DELETE /:username/channels/:id-slug-name-goes-here + def destroy + if @channel.featured? + render :json => {:error => "You cannot delete your featured channel."}, :status => :unauthorized + else + @channel.destroy and redirect_to user_channels_path(current_user) + end + end + + # GET /:username/channels + def index + @channels = @user.channels.paginate(:page => params[:page], :per_page => 9, :order => 'created_at DESC') + + respond_to do |format| + params[:page].to_i > 1 ? format.js : format.html + end + end + + # GET /:username/channels/:id-slug-name-goes-here + def show + if current_user.blank? || !current_user?(@user) + # Only show videos that are in a :ready state + @videos ||= @channel.videos.joins(:video_graph).where(:video_graphs => { :status => VideoGraph.get_status_number(:ready) }). + paginate(:page => params[:page], :per_page => 10, :order => 'created_at DESC') + else + # Show all videos except ones that are uploading + @videos ||= @channel.videos.joins(:video_graph).where(:video_graphs => { :status => Video.statuses_to_show_to_current_user }). + paginate(:page => params[:page], :per_page => 10, :order => 'created_at DESC') + end + + respond_to do |format| + params[:page].to_i > 1 ? format.js : format.html + end + end + + # GET /c/:public_token + def show_by_token_access + if params[:public_token] + # Only accept the first 50 characters as the public token + @channel ||= Channel.where('public_token = ?', params[:public_token].strip.first(50)).first + + if @channel + @viewing_via_token_access = true + @user = get_object_owner(@channel) + @latest_featured_videos = @user.featured_videos.limit(4) + # Only show videos that are in a :ready state + @videos ||= @channel.videos.joins(:video_graph).where(:video_graphs => { :status => VideoGraph.get_status_number(:ready) }). + paginate(:page => params[:page], :per_page => 10, :order => 'created_at DESC') + + respond_to do |format| + params[:page].to_i > 1 ? format.js { render 'channels/show.js' } : format.html { render 'channels/show.html' } + end + else + # show an error page if we couldn't find the channel + render :template => "errors/error_404", :status => 404 + end + else + # no public token passed in + render :template => "errors/error_404", :status => 404 + end + end + + # PUT /:username/channels/:id-slug-name-goes-here/update + def update + # Update privacy params + if params[:privacy] + @channel.private = params[:privacy] + send_back_link = true + end + + # Update name + if params[:title] + @channel.title = params[:title] + end + + if @channel.save + if send_back_link + render :json => { :new_link => user_channel_url(@user, @channel, :privacy => @channel.private? ? "false" : "true") }, + :status => :accepted + else + render :nothing => true, :status => :accepted + end + else + render :json => { :error => get_errors_for_class(@channel).to_sentence }, + :status => :unprocessable_entity + end + end + + # PUT /:username/update_featured_videos + def update_featured_videos + @video ||= current_user.videos.find_by_id(params[:video_id]) + if @video + if @video == current_user.featured_videos.first + render :json => { :error => "That video is already at the top of your featured videos list :)" }, + :status => :unprocessable_entity + else + @video.featured_at = Time.now + @video.save + render :json => { :featured_video => render_to_string( :partial => 'shared/featured_video.html', + :locals => { :featured_video => @video }) }, + :status => :accepted + end + else + render :json => { :error => "Either that video does not exist or you do not have permission to move it." }, + :status => :unprocessable_entity + end + end + +end \ No newline at end of file diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb new file mode 100755 index 0000000..e3a2a88 --- /dev/null +++ b/app/controllers/comments_controller.rb @@ -0,0 +1,55 @@ +class CommentsController < ApplicationController + include ApplicationHelper + + before_filter :set_user + before_filter :set_video + before_filter :verify_user_can_access_channel_or_video + + # POST /:username/videos/:video_id/comments + def create + comment ||= @video.comments.new(:content => params[:content]) + comment.user_id = current_user.id + @viewing_via_token_access = (params[:channel_token] == @video.channel.public_token) + + video_owner ||= get_object_owner(@video) + if comment.save + render :json => { :html => + render_to_string( :partial => 'comments/comment.html', + :locals => { :video => @video, + :comment => comment, + :video_owner => video_owner, + :hidden_comment => false } ), + :video_id => @video.id, + :comments_count => @video.comments.count }, + :status => :created + + comment.notify_all_users_in_the_conversation(current_user, @video, video_owner) + else + render :json => { :error => get_errors_for_class(comment).to_sentence }, + :status => :unprocessable_entity + end + end + + # DELETE /:username/videos/:video_id/comments/:id + def destroy + comment ||= @video.comments.find_by_id(params[:id]) + if comment + # comment exists, so destroy it if user has permission + if current_user_owns?(comment) || current_user_owns?(@video) + if comment.destroy + render :json => { :comments_count => @video.comments.count }, + :status => :ok + else + render :json => { :error => "There was an error removing your comment." }, + :status => :unprocessable_entity + end + else + render :json => { :error => "You do not own that comment so you cannot delete it." }, + :status => :unauthorized + end + else + render :nothing => true, :status => :not_found + end + end + +end diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb new file mode 100755 index 0000000..cdce687 --- /dev/null +++ b/app/controllers/errors_controller.rb @@ -0,0 +1,13 @@ +class ErrorsController < ApplicationController + skip_before_filter :site_authenticate + + def error_social_auth + render :template => "errors/error_social_auth", :status => 401 + Airbrake.notify(:error_class => "Logged Error", :error_message => "SOCIAL CREDENTIALS: There was an OmniAuth authentication failure.") if Rails.env.production? + end + + def routing + @browser_title ||= "Oops" + render :template => "errors/error_404", :status => 404 + end +end \ No newline at end of file diff --git a/app/controllers/invitation_link_controller.rb b/app/controllers/invitation_link_controller.rb new file mode 100755 index 0000000..a6588a4 --- /dev/null +++ b/app/controllers/invitation_link_controller.rb @@ -0,0 +1,38 @@ +class InvitationLinkController < ApplicationController + before_filter :set_user, :verify_user_owns_page, :set_featured_videos + + # GET /:username/invitations + def index + @browser_title ||= "Invite People" + + respond_to do |format| + if User::USERS_CAN_INVITE_MORE_PEOPLE + format.html # index.html.haml + else + # don't show the page unless site-wide invites are enabled + format.html { redirect_to(current_user) } + end + end + end + + # POST /:username/invitations + def create + recipient_emails = params[:recipient_email] + if recipient_emails.blank? + render :json => { :error => "You have not specified any email addresses to invite." }, + :status => :unprocessable_entity + else + personal_message = params[:personal_message] + invitation_validation_errors = InvitationLink.invite_new_users!(recipient_emails, current_user, personal_message) + if invitation_validation_errors.blank? + render :json => { :message => "Thank you! We have sent an email inviting each person!" }, + :status => :ok + else + # return the errors + render :json => { :error => invitation_validation_errors }, + :status => :unprocessable_entity + end + end + end + +end diff --git a/app/controllers/profile_controller.rb b/app/controllers/profile_controller.rb new file mode 100755 index 0000000..9844043 --- /dev/null +++ b/app/controllers/profile_controller.rb @@ -0,0 +1,32 @@ +class ProfileController < ApplicationController + skip_before_filter :site_authenticate, :only => [:index] + before_filter :set_user + before_filter :verify_current_user_is_not_blocked, :only => [:index] + before_filter :set_featured_videos, :only => [:index] + + # GET /:username/profile + def index + @browser_title ||= @user.name + @profile = @user.profile + end + + # PUT /:username/profile/:id + def update + profile = current_user.profile + + if params[:profile].blank? + render :json => { :error => "There was no profile data passed in so your profile could not be saved." }, + :status => :unprocessable_entity + else + if profile.update_attributes(params[:profile]) + render :json => { :html => profile.categories_to_hash('html'), + :text => profile.categories_to_hash('text') }, + :status => :accepted + else + render :json => { :error => get_errors_for_class(profile).to_sentence }, + :status => :unprocessable_entity + end + end + end + +end diff --git a/app/controllers/public_controller.rb b/app/controllers/public_controller.rb new file mode 100755 index 0000000..29d5bb0 --- /dev/null +++ b/app/controllers/public_controller.rb @@ -0,0 +1,33 @@ +class PublicController < ApplicationController + skip_before_filter :site_authenticate + + def faq + @browser_title ||= "FAQ" + render(:template => "public/faq", :status => :ok) + end + + def video_faq + @browser_title ||= "Video FAQ" + render(:template => "public/video_faq", :status => :ok) + end + + def video_guidelines + @browser_title ||= "Video Guidelines" + render(:template => "public/video_guidelines", :status => :ok) + end + + def contact + @browser_title ||= "Contact" + render(:template => "public/contact", :status => :ok) + end + + def privacy + @browser_title ||= "Privacy Policy" + render(:template => "public/privacy", :status => :ok) + end + + def tos + @browser_title ||= "Terms of Service" + render(:template => "public/tos", :status => :ok) + end +end \ No newline at end of file diff --git a/app/controllers/public_videos_controller.rb b/app/controllers/public_videos_controller.rb new file mode 100755 index 0000000..fd7e3e1 --- /dev/null +++ b/app/controllers/public_videos_controller.rb @@ -0,0 +1,56 @@ +class PublicVideosController < ApplicationController + include ApplicationHelper + + skip_before_filter :http_authenticate, :site_authenticate + + # GET /embed/:public_token + def embed + if params[:public_token] + # Only accept the first 11 characters as the public token + @video ||= Video.where('public_token = ?', params[:public_token].strip.first(11)).first + + if @video && @video.is_status?(VideoGraph::READY) + render :template => "public_videos/embed", + :status => :ok, + :layout => "empty" + + # Create an entry for a video being played if it's not a known bot + # but give it an event_creator_id of 0 so we can distinguish it as an external play + UserEvent.create(:event_type => UserEvent.event_type_value(:video_play), + :event_object_id => @video.id, + :user_id => @video.user_id, + :event_creator_id => 0) + else + # show an error page if we couldn't find the video or the + # video is not yet done uploading/processing/etc + render :template => "errors/error_404", :status => 404 + end + else + # no public token passed in + render :template => "errors/error_404", :status => 404 + end + end + + # GET p/:public_token + def show + if params[:public_token] + # Only accept the first 11 characters as the public token + @video ||= Video.where('public_token = ?', params[:public_token].strip.first(11)).first + + if @video && @video.is_status?(VideoGraph::READY) + @user = get_object_owner(@video) + @latest_featured_videos = @user.featured_videos.limit(4) + + render 'videos/show' + else + # show an error page if we couldn't find the video or the + # video is not yet done uploading/processing/etc + render :template => "errors/error_404", :status => 404 + end + else + # no public token passed in + render :template => "errors/error_404", :status => 404 + end + end + +end \ No newline at end of file diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb new file mode 100755 index 0000000..abd1320 --- /dev/null +++ b/app/controllers/search_controller.rb @@ -0,0 +1,76 @@ +require 'riddle' +class SearchController < ApplicationController + skip_before_filter :site_authenticate + + # GET /search/users?q=[term] + def users + @browser_title ||= "Find People" + + @search_type = "user" + @term = params[:q] + searching_for_an_email = !@term.blank? && @term.match(User::EMAIL_REGEX) + @results_count = 0 + + # Only perform the search on Heroku + if Rails.env.production? || Rails.env.staging? + if @term.blank? + @users = User.search :order => "@random DESC", :page => params[:page], :per_page => 50 + else + # Build up search field conditions hash + @conditions = {} + @conditions = searching_for_an_email ? {:email => Riddle.escape(@term)} : {:name => Riddle.escape(@term)} + @users = User.search :conditions => @conditions, :order => :name, :page => params[:page], :per_page => 50 + end + @results_count = @users.total_entries + else + @users = [] + end + + respond_to do |format| + params[:page].to_i > 1 ? format.js : format.html + end + end + + # GET /search/videos?q=[term] or GET /search/videos?tag=[tag] + def videos + @browser_title ||= "Search Public Videos" + + @search_type = "video" + searching_for_a_tag = params[:q].blank? + @term = searching_for_a_tag ? params[:tag] : params[:q] + @results_count = 0 + + # Only perform the search on Heroku + if Rails.env.production? || Rails.env.staging? + if @term.blank? + # use the default extended mode matching to return random results + @results = Video.search :conditions => {:status => VideoGraph.get_status_number(:ready), :channel_is_private => 'f'}, + :order => "@random DESC", + :page => params[:page], + :per_page => 10 + else + if searching_for_a_tag + # only match video tags using the default extended mode matching + @results = Video.search :conditions => {:tags => Riddle.escape(@term), :status => VideoGraph.get_status_number(:ready), :channel_is_private => 'f'}, + :page => params[:page], + :per_page => 10, + :order => 'created_at DESC' + else + # use the default extended mode matching to search the video meta + @results = Video.search Riddle.escape(@term), :conditions => {:status => VideoGraph.get_status_number(:ready), :channel_is_private => 'f'}, + :page => params[:page], + :per_page => 10, + :order => 'created_at DESC' + end + end + @results_count = @results.total_entries + else + @results = [] + end + + respond_to do |format| + params[:page].to_i > 1 ? format.js : format.html + end + end + +end \ No newline at end of file diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100755 index 0000000..d0c0e82 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,126 @@ +class SessionsController < ApplicationController + skip_before_filter :site_authenticate + skip_before_filter :ensure_the_user_is_not_deactivated, :only => [:destroy] + + def new + if signed_in? + redirect_to user_stream_path(current_user) + else + @browser_title = "Login" + respond_to do |format| + format.html { render(:template => "sessions/new", :status => :ok, :layout => "signed_out") } + end + end + end + + # handles social signups / logins / associations + def create_social_session + social_params ||= request.env["omniauth.auth"] + if social_params + if signed_in? + # user is associating their FB / Twitter account with their Brevidy account + new_network ||= current_user.social_networks.new(:provider => social_params["provider"], :uid => social_params["uid"], :token => social_params["credentials"]["token"], :token_secret => social_params["credentials"]["secret"]) + + respond_to do |format| + if new_network.save + format.html { redirect_to user_account_path(current_user) } + format.json { render :json => { :success => true, + :message => nil, + :user_id => current_user.id }, + :status => :created } + else + error_message ||= get_errors_for_class(new_network).to_sentence + format.html { flash[:error] = error_message; redirect_to user_account_path(current_user) } + format.json { render :json => { :success => false, + :message => error_message, + :user_id => current_user.id }, + :status => :unprocessable_entity } + end + end + else + # user is either logging in or signing up via FB / Twitter + # check if a user with that UID already exists + social_credentials = SocialNetwork.find_by_provider_and_uid(social_params["provider"], social_params["uid"]) + + if social_credentials.blank? + # delete any old social image cookies so we don't set an incorrect image from a prior session + cookies.delete(:social_image_url) + cookies.delete(:social_bio) + + # create a new user and redirect to step 2 of the signup process + @user = User.create_via_fb_or_twitter(social_params) + # set cookies to remember the user's image and bio so we can set it after they are created + case social_params["provider"] + when "facebook" + cookies[:social_image_url] = social_params["user_info"]["image"].gsub("type=square", "type=large") rescue nil + cookies[:social_bio] = social_params["extra"]["user_hash"]["bio"] rescue nil + when "twitter" + cookies[:social_image_url] = social_params["extra"]["user_hash"]["profile_image_url_https"].gsub("_normal", "") rescue nil + cookies[:social_bio] = social_params["extra"]["user_hash"]["description"] rescue nil + end + + respond_to do |format| + format.html { render(:template => "users/signup", + :status => :ok, + :layout => "signed_out", + :locals => { :user => @user, + :provider => social_params["provider"], + :uid => social_params["uid"], + :oauth_token => social_params["credentials"]["token"], + :oauth_token_secret => social_params["credentials"]["secret"], + :social_signup => "true" }) } + end + else + # user already exists with that UID/provider combo, so they just want to login + sign_in User.find_by_id(social_credentials.user_id) + respond_to do |format| + format.html { redirect_back_or user_stream_path(current_user) } + end + end # end blank? + end # end signed_in? + else + respond_to do |format| + error_message = "There was an error communicating with Facebook or Twitter. Please try again in a few minutes!" + format.html { flash[:error] = error_message; redirect_to :login } + format.json { render :json => { :success => false, + :message => error_message, + :user_id => false }, + :status => :unauthorized } + end + end # end check for social params + end + + # handles regular logins (i.e. w/ email / password) + def create + # strip fields and downcase email + prepare_params_for_login + + user = User.authenticate(params[:email], + params[:password]) + + if user.nil? + respond_to do |format| + error_message = "Invalid login credentials." + format.html { flash[:error] = error_message; redirect_to :login } + format.json { render :json => { :success => false, + :message => error_message, + :user_id => false }, + :status => :unauthorized } + end + else + sign_in user + respond_to do |format| + format.html { redirect_back_or user_stream_path(current_user) } + format.json { render :json => { :success => true, + :message => nil, + :user_id => user.id }, + :status => :created } + end + end + end + + def destroy + sign_out + redirect_to root_path + end +end diff --git a/app/controllers/social_networks_controller.rb b/app/controllers/social_networks_controller.rb new file mode 100755 index 0000000..d6d2c0e --- /dev/null +++ b/app/controllers/social_networks_controller.rb @@ -0,0 +1,37 @@ +class SocialNetworksController < ApplicationController + skip_before_filter :site_authenticate, :http_authenticate + + # POST /auth/deauthorize + def deauthorize + # Currently, only Facebook has a deauthorization callback + request_params = params[:signed_request].split('.') + encoded_signature = SocialNetwork.base64_url_decode(request_params[0]) + user_payload = ActiveSupport::JSON.decode(SocialNetwork.base64_url_decode(request_params[1])) + Airbrake.notify(:error_class => "Non-Issue", :error_message => "Just letting you know that a user deauthorized Brevidy on Facebook :( The user was #{User.find_by_id(SocialNetwork.where(:uid => user_payload['user_id'], :provider => 'facebook').first).id}") if Rails.env.production? + + render :nothing => true, :status => :ok + end + + # DELETE /users/user_id/social_networks/:id + def destroy + social_credentials = SocialNetwork.find_by_user_id_and_provider(current_user.id, params[:provider]) + respond_to do |format| + if social_credentials.blank? + error_message ||= "We could not find a #{params[:provider].capitalize} account associated with your Brevidy account." + format.html { flash[:error] = error_message; redirect_to user_account_path(current_user) } + format.json { render :json => { :success => false, + :message => error_message, + :user_id => current_user.id }, + :status => :unprocessable_entity } + else + social_credentials.destroy + format.html { redirect_to user_account_path(current_user) } + format.json { render :json => { :success => true, + :message => nil, + :user_id => current_user.id }, + :status => :ok } + end + end + end + +end \ No newline at end of file diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb new file mode 100755 index 0000000..2a3ba4b --- /dev/null +++ b/app/controllers/subscriptions_controller.rb @@ -0,0 +1,112 @@ +class SubscriptionsController < ApplicationController + include ApplicationHelper + + before_filter :site_authenticate, :except => [:subscribers, :subscriptions] + before_filter :set_user + before_filter :set_channel, :except => [:subscribers, :subscriptions] + before_filter :verify_current_user_is_not_blocked, :only => [:create, :destroy, :subscribers, :subscriptions] + before_filter :verify_user_owns_channel, :only => [:handle_access_request, :remove_subscriber] + before_filter :set_featured_videos, :only => [:subscribers, :subscriptions] + + # POST /:username/channels/:id-slug-name-goes-here/subscribe + def create + subscription ||= @channel.subscribe!(current_user) + + if subscription.errors.any? + errors = get_errors_for_class(subscription).to_sentence + if errors.include?("requesting permission") + render :json => { :requesting_permission => true }, + :status => :ok + else + render :json => { :error => errors }, + :status => :unprocessable_entity + end + else + render :json => { :button => render_to_string( :partial => "channels/button_unsubscribe.html" , + :locals => { :user => current_user, + :channel => @channel, + :button_size => params[:ref] } ) }, + :status => :created + end + end + + # DELETE /:username/channels/:id-slug-name-goes-here/unsubscribe + def destroy + subscription_was_destroyed ||= @channel.unsubscribe!(current_user) + + if subscription_was_destroyed + is_private = @channel.private? + render :json => { :button => render_to_string( :partial => "channels/button_subscribe.html" , + :locals => { :user => current_user, + :channel => @channel, + :is_private => is_private, + :button_size => params[:ref] } ), + :is_private => is_private, + :private_area_message => is_private ? render_to_string(:partial => "channels/private_area_message.html") : "" }, + :status => :ok + else + render :json => { :error => "You are not currently subscribed to this channel." }, + :status => :not_found + end + end + + # GET /:username/channels/:id-slug-name-goes-here/request_access?approved=t&token=some_token_here + # GET /:username/channels/:id-slug-name-goes-here/request_access?ignored=t&token=some_token_here + def handle_access_request + channel_request = ChannelRequest.where(:channel_id => @channel.id, :token => params[:token]).first + if channel_request.blank? + flash[:error] = "Sorry, but we were unable to find a request to access this channel. You may have already approved it or it may have been cancelled by the other user." + else + requesting_user = User.find_by_id(channel_request.user_id) + if params[:approved] + subscription ||= @channel.subscribe!(requesting_user, true) + + if subscription.errors.any? + flash[:error] = get_errors_for_class(subscription).to_sentence + else + flash[:success] = "We have granted access to your private channel, #{@channel.title}, for #{requesting_user.name}" + end + elsif params[:ignored] + channel_request.ignored = true + channel_request.save + + flash[:notice] = "We have ignored the request to access your private channel, #{@channel.title}. Keep in mind that #{requesting_user.name} will not be notified about this." + else + flash[:error] = "Sorry, but we were unable to handle this access request. We have been notified about this issue." + Airbrake.notify(:error_class => "Logged Error", :error_message => "PRIVATE CHANNEL ACCESS: The approved or ignored param was missing in the query string clicked by #{current_user.email}") if Rails.env.production? + end + end + end + + # DELETE /:username/channels/:id-slug-name-goes-here/remove_subscriber?user_id=1234 + def remove_subscriber + user_to_remove = User.find_by_id(params[:user_id]) + subscription_was_destroyed ||= @channel.unsubscribe!(user_to_remove) if user_to_remove + + if subscription_was_destroyed + render :nothing => true, :status => :ok + else + render :json => { :error => "That person is not currently subscribed to this channel." }, + :status => :not_found + end + end + + # GET /:username/subscribers + def subscribers + @users = @user.subscribers_as_people.paginate(:page => params[:page], :per_page => 50, :order => 'created_at DESC') + + respond_to do |format| + params[:page].to_i > 1 ? format.js : format.html + end + end + + # GET /:username/subscriptions + def subscriptions + @subscriptions = @user.channel_subscriptions.paginate(:page => params[:page], :per_page => 9, :order => 'created_at DESC') + + respond_to do |format| + params[:page].to_i > 1 ? format.js : format.html + end + end + +end \ No newline at end of file diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb new file mode 100755 index 0000000..1169734 --- /dev/null +++ b/app/controllers/tags_controller.rb @@ -0,0 +1,22 @@ +class TagsController < ApplicationController + before_filter :set_user, :set_video, :verify_user_owns_page + + # DELETE /:username/videos/:video_id/tags/:id + def destroy + tagging ||= @video.taggings.where(:tag_id => params[:id]).first + if tagging + # tagging relationship exists, so destroy it + if tagging.destroy + render :json => { :video_id => @video.id, + :tags_count => @video.tags.count }, + :status => :ok + else + render :json => { :error => "There was an error removing your tag." }, + :status => :unprocessable_entity + end + else + render :nothing => true, :status => :not_found + end + end + +end diff --git a/app/controllers/user_events_controller.rb b/app/controllers/user_events_controller.rb new file mode 100755 index 0000000..7e6ec16 --- /dev/null +++ b/app/controllers/user_events_controller.rb @@ -0,0 +1,18 @@ +class UserEventsController < ApplicationController + before_filter :set_user, :verify_user_owns_page, :set_featured_videos + + # GET /:username/latest_activity + def show + @browser_title ||= "Latest Activity" + # return all user events other than video plays + @user_events = @user.notifications_to_show_user.paginate(:page => params[:page], :per_page => 25, :order => 'created_at DESC') + + # mark all user events for the current user as "seen" + UserEvent.where(:user_id => current_user.id, :seen_by_user => false).update_all(:seen_by_user => true) unless current_user.notifications_count == 0 + + respond_to do |format| + params[:page].to_i > 1 ? format.js : format.html + end + end + +end \ No newline at end of file diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100755 index 0000000..fafdaba --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,474 @@ +class UsersController < ApplicationController + require 'securerandom' + + # needed for truncate method + include ActionView::Helpers::TextHelper + + before_filter :site_authenticate, :only => [:block, :edit, :edit_banner, :latest_activity, :new_image, + :new_image_status, :subscriptions_stream, :unblock, :update, + :update_background_image, :update_banner_from_gallery, :update_notifications, + :update_password] + before_filter :redirect_to_stream_if_logged_in, :only => [:create, :forgotten_password, :new, :reset_password, :signup, + :validate_forgotten_password, :validate_reset_password] + before_filter :set_user, :except => [:create, :forgotten_password, :index, :new, :signup, :username_availability, :validate_forgotten_password] + before_filter :verify_current_user_is_not_blocked, :only => [:show] + before_filter :verify_user_owns_page, :only => [:edit, :edit_banner, :new_image, :new_image_status, :subscriptions_stream, + :update, :update_background_image, :update_banner_from_gallery, :update_notifications, + :update_password] + before_filter :set_featured_videos, :only => [:show, :subscriptions_stream] + before_filter :verify_tokens_match_and_token_is_fresh, :only => [:reset_password, :validate_reset_password] + + # Caching + caches_action :new + + # POST /:username/block + def block + blocking = Blocking.where(:requesting_user => current_user.id, :blocked_user => @user.id).first + if blocking + render :json => { :error => "You are already blocking that user." }, + :status => :unprocessable_entity + else + current_user.block!(@user) + render :nothing => true, :status => :created + end + end + + # POST /users + def create + @user = User.new(params[:user]) + + if @user.save + sign_in @user + + # associate them w/ a social login account if necessary + unless params[:social_signup].blank? + @user.associate_with_social_account(params, cookies[:social_image_url], cookies[:social_bio]) + end + + respond_to do |format| + format.html { redirect_to user_stream_path(current_user) } + format.json { render :json => { :success => true, + :message => nil, + :user_id => @user.id }, :status => :created } + end + + UserMailer.delay.celebrate_new_user(@user) if Rails.env.production? + else + # return errors via AJAX + respond_to do |format| + format.js + format.json { render :json => { :success => false, + :message => get_errors_for_class(@user).to_sentence, + :user_id => false }, :status => :unprocessable_entity } + end + end + end + + # GET /:username/account + def edit + @browser_title ||= "Edit Account" + @facebook_connected = SocialNetwork.find_by_user_id_and_provider(current_user.id, "facebook") + @twitter_connected = SocialNetwork.find_by_user_id_and_provider(current_user.id, "twitter") + end + + # GET /:username/edit_banner + def edit_banner + @banner_images = BannerImage.where(:active => true) + end + + # GET /account/forgotten_password + def forgotten_password + @browser_title ||= "Forgotten Password" + @show_password_reset = false + + respond_to do |format| + format.html { render(:template => "users/forgotten_password", :status => :ok, :layout => "signed_out") } + end + end + + # GET /users + def index + @browser_title ||= "Find People" + + # show the 4 latest featured videos for the Explore page + #@latest_featured_videos = User.find_by_username("brevidy").featured_videos.limit(4) + + @latest_featured_videos = [] + + respond_to do |format| + format.html + end + end + + # Main landing page w/ social sign up buttons + # GET brevidy.com + def new + @invitation ||= InvitationLink.handle_invite_token(params[:invitation_token]) + cookies[:invitation_token] = params[:invitation_token] if params[:invitation_token] + + respond_to do |format| + format.html { render(:template => "users/new", :status => :ok, :layout => "signed_out") } + end + end + + # GET /:username/account/image_status + def new_image_status + if current_user.image_status == 'processing' + render :json => { :job_status => 'processing' }, + :status => :ok + elsif current_user.image_status == 'success' + # refresh page to show new image + if params[:media_type] == 'banner' + redirect_to user_edit_banner_path(current_user) + else + redirect_to user_account_path(current_user) + end + elsif current_user.image_status == 'error' + # we had an issue updating the image + render :json => { :job_status => 'error' }, + :status => :ok + else + # we had an unknown state + render :json => { :job_status => 'error' }, + :status => :ok + end + end + + # PUT /:username/account/image + def new_image + if params[:filename].blank? || params[:media_type].blank? + Airbrake.notify(:error_class => "Logged Error", :error_message => "USER IMAGE: Error SAVING image for #{current_user.email}. REASON: Filename or media_type (image or banner) was blank.") if Rails.env.production? + render :json => { :error => "There was an error saving your new image. We have been notified of this issue." }, + :status => :unprocessable_entity + else + current_user.update_attribute(:image_status, 'processing') + new_temp_image = params[:filename] + + if params[:media_type] == 'banner' + # It's a banner image + old_banner_image = current_user.read_attribute(:banner_image) + + # Kick off resizing and setting the new image + # Also pass in the old image and new temp file so we can clean up after ourselves on S3 + current_user.set_new_user_image(old_banner_image, new_temp_image, true) + elsif params[:media_type] == 'image' + # It's a profile image + old_image = current_user.read_attribute(:image) + + # Kick off resizing and setting the new image + # Also pass in the old image and new temp file so we can clean up after ourselves on S3 + current_user.set_new_user_image(old_image, new_temp_image, false) + else + Airbrake.notify(:error_class => "Logged Error", :error_message => "There was a bad media_type passed in (#{params[:media_type]}) when saving a new image for #{current_user.email}.") if Rails.env.production? + return + end + + # return :ok back to the browser so it can start polling + # the image status to check when processing is complete + # + # we use :start_polling to tell the uploader to only do this + # for images and not for videos + render :json => { :start_polling => 'true' }, + :status => :ok + end + end + + # POST /:username/reset_password + def reset_password + if params[:password] == params[:password_confirmation] + # New passwords match so reset it + @user.update_attributes(:password => params[:password]) + User.should_encrypt_password = true + if @user.save + User.should_encrypt_password = false + sign_in @user + + # Clear out the reset token so it can't be reused + @user.reset_token = "" + @user.save + + redirect_to current_user and return + + else + flash.now[:error] = get_errors_for_class(@user) + @show_password_reset = true + end + User.should_encrypt_password = false + + else + flash.now[:error] = "Passwords do not match. Please re-enter and confirm your new password." + @show_password_reset = true + end + + respond_to do |format| + format.html { render(:template => "users/forgotten_password", :status => :ok, :layout => "signed_out") } + end + end + + # GET /:username + # GET /:username.js + def show + @browser_title ||= @user.name + + if current_user.blank? || !current_user?(@user) + # Only show public videos that are complete + @videos ||= @user.public_videos.paginate(:page => params[:page], :per_page => 10, :order => 'created_at DESC') + else + # Show all videos (public and private) except ones that are uploading + @videos ||= @user.all_videos.paginate(:page => params[:page], :per_page => 10, :order => 'created_at DESC') + end + + respond_to do |format| + params[:page].to_i > 1 ? format.js : format.html + end + end + + # Secondary sign up page for handling errors and showing old fashioned signup form + # GET /users/signup + def signup + @user = User.new + @invitation ||= InvitationLink.handle_invite_token(params[:invitation_token]) + + respond_to do |format| + format.html { render(:template => "users/signup", :status => :ok, :layout => "signed_out") } + end + end + + # GET /:username/stream + # GET /:username/stream.js + def subscriptions_stream + @browser_title ||= @user.name + @videos ||= current_user.all_videos_for_subscriptions.paginate(:page => params[:page], :per_page => 10, :order => 'created_at DESC') + + respond_to do |format| + params[:page].to_i > 1 ? format.js : format.html + end + end + + # POST /:username/unblock + def unblock + blocking = Blocking.where(:requesting_user => current_user.id, :blocked_user => @user.id).first + if blocking + if blocking.destroy + render :nothing => true, :status => :ok + else + render :json => { :error => "There was an issue unblocking that person. We have been notified of this issue." }, + :status => :not_found + Airbrake.notify(:error_class => "Logged Error", :error_message => "UNBLOCK: User (#{current_user.email}) was unable to unblock another user (#{@user.email})") if Rails.env.production? + end + else + render :json => { :error => "You are not currently blocking that user." }, + :status => :unprocessable_entity + end + end + + # PUT /:username/account/update + def update + # format the new birthday + new_birthday = "#{params[:birthday_year]}-#{params[:birthday_month]}-#{params[:birthday_day]}" + # cache the old username + old_username = current_user.username + + # update the other attributes + if current_user.update_attributes(params[:user]) && current_user.update_attributes(:birthday => new_birthday) + if current_user.username != old_username + current_user.update_attribute(:username_changed_at, DateTime.now) + redirect_to user_account_path(current_user) + else + render :nothing => true, :status => :accepted + end + else + render :json => { :error => get_errors_for_class(current_user).to_sentence }, + :status => :unprocessable_entity + end + end + + # PUT /:username/update_background_image + def update_background_image + background_image_id = params[:background_image_id] + if background_image_id.blank? + render :json => { :error => "Sorry, but we could not set your background image for you." }, + :status => :unprocessable_entity + Airbrake.notify(:error_class => "Logged Error", :error_message => "USER BACKGROUND: Could not set a background image since the background_image_id passed in was blank.") if Rails.env.production? + else + # Make sure a valid ID was passed in + if current_user.update_attributes(:background_image_id => background_image_id.to_i) + render :json => { :background_image_id => background_image_id }, + :status => :accepted + else + render :json => { :error => "Sorry, but we could not set your background image for you." }, + :status => :unprocessable_entity + Airbrake.notify(:error_class => "Logged Error", :error_message => "USER BACKGROUND: Could not set a background image since the background_image_id passed in was invalid.") if Rails.env.production? + end + end + end + + # PUT /:username/update_banner_from_gallery + def update_banner_from_gallery + banner_id = params[:banner_image_id] + if banner_id.blank? + render :json => { :error => "Sorry, but we could not set your banner image for you." }, + :status => :unprocessable_entity + Airbrake.notify(:error_class => "Logged Error", :error_message => "USER BANNER: Could not set a user banner from the gallery since the banner ID passed in was blank.") if Rails.env.production? + else + # Make sure a valid ID was passed in + if BannerImage.where(:id => banner_id, :active => true).exists? + current_user.update_attributes(:banner_image_id => banner_id) + render :json => { :image_path => current_user.get_banner_image_url(banner_id) }, + :status => :accepted + else + render :json => { :error => "Sorry, but we could not set your banner image for you." }, + :status => :unprocessable_entity + Airbrake.notify(:error_class => "Logged Error", :error_message => "USER BANNER: Could not set a user banner from the gallery since the banner ID passed in was invalid or for a banner that is no longer active.") if Rails.env.production? + end + end + end + + # PUT /:username/account/notifications + def update_notifications + the_settings = current_user.setting + if the_settings.update_attributes(params[:user]) + render :nothing => true, + :status => :accepted + else + render :json => { :error => get_errors_for_class(the_settings).to_sentence }, + :status => :unprocessable_entity + end + end + + # PUT /:username/account/password + def update_password + puts params[:old_password].strip + if current_user.has_password?(params[:old_password].strip) + new_password ||= params[:new_password].strip + confirm_new_password ||= params[:confirm_new_password].strip + if new_password == confirm_new_password + User.should_encrypt_password = true + if current_user.update_attributes(:password => new_password) + render :nothing => true, :status => :accepted + else + render :json => { :error => get_errors_for_class(current_user).to_sentence }, + :status => :unprocessable_entity + end + User.should_encrypt_password = false + else + render :json => { :error => "Your new password does not match the confirmation password. Please re-type them." }, + :status => :unprocessable_entity + end + else + render :json => { :error => "Your old password does not match the password we have on record." }, + :status => :unprocessable_entity + end + end + + # GET /username_availability + def username_availability + if params[:username].blank? + render :json => { :error => "No username was passed in." }, + :status => :unprocessable_entity + else + username = params[:username].downcase.strip + if (username.length > User::USERNAME_LENGTH) || !User::USERNAME_REGEX.match(username) || !User.verify_username_is_acceptable(username) || User.where(:username => username).exists? + render :json => { :availability_text => "not available" }, + :status => :ok + else + render :json => { :availability_text => "available" }, + :status => :ok + end + end + end + + # POST /account/validate_forgotten_password + def validate_forgotten_password + @show_password_reset = false + + if params[:email].blank? + flash.now[:error] = "Please enter an email address." and return + elsif is_not_a_valid_email(params[:email]) + flash.now[:error] = "Email address is invalid. Please enter a valid email address." and return + else + forgetful_user = User.find_by_email(params[:email]) + if forgetful_user + # Generate reset token for the forgetful user + forgetful_user.reset_token = generate_reset_token + # Record when pw reset was requested for token expiration + forgetful_user.pw_reset_timestamp = Date.today + + if forgetful_user.save + # Send email + UserMailer.delay(:priority => 0).reset_password_instructions(forgetful_user) + + # Show flash message + flash.now[:success] = "We have sent password reset instructions to that email address." + else + flash.now[:error] = "There was an error processing your password reset request. We have been notified of this issue." + Airbrake.notify(:error_class => "Logged Error", :error_message => "Could not save reset_token (#{forgetful_user.reset_token}) for User (#{forgetful_user.email}).") if Rails.env.production? + end + else + flash.now[:error] = "We could not find a user with that email address." + end + end + + respond_to do |format| + format.html { render(:template => "users/forgotten_password", :status => :ok, :layout => "signed_out") } + end + end + + # GET /:username/validate_reset_password?token=[token] + def validate_reset_password + @browser_title ||= "Reset Password" + @show_password_reset = true + + respond_to do |format| + format.html { render(:template => "users/forgotten_password", :status => :ok, :layout => "signed_out") } + end + end + + + private + # Verifies the user's reset password token matches and is less than 2 days old + def verify_tokens_match_and_token_is_fresh + @token = params[:token] + invalid_token_msg = "The reset password link you are attempting to use is invalid or has expired. Please enter your email below to generate a new link." + + # Make sure the token isn't blank + if @token.blank? + @show_password_reset = false + flash.now[:error] = invalid_token_msg + render(:template => "users/forgotten_password", :status => :ok, :layout => "signed_out") and return + end + + # See if the token is older than 2 days (it's expired if so) + unless @user.pw_reset_timestamp.blank? + date_then = Date.new(@user.pw_reset_timestamp.year, @user.pw_reset_timestamp.month, @user.pw_reset_timestamp.day) + @freshToken = (Date.today - date_then).to_i <= 2 + end + + # Make sure the tokens match up and the token is less than 2 days old + unless @token == @user.reset_token && @freshToken + # Clear the reset token since it's expired or someone is trying to guess it + @user.reset_token = "" + @user.save + + # Show a flash message and render the page + @show_password_reset = false + flash.now[:error] = invalid_token_msg + render(:template => "users/forgotten_password", :status => :ok, :layout => "signed_out") + end + end + + # Checks if email is valid + def is_not_a_valid_email(email) + reg = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i + return (reg.match(email))? false : true + end + + # Generates a 32 character random string for resetting the user's password + def generate_reset_token + loop do + token = SecureRandom.base64(32).tr('+/=', 'xyz') + break token unless User.where(:reset_token => token).exists? + end + end + +end diff --git a/app/controllers/videos_controller.rb b/app/controllers/videos_controller.rb new file mode 100755 index 0000000..debb1f5 --- /dev/null +++ b/app/controllers/videos_controller.rb @@ -0,0 +1,401 @@ +class VideosController < ApplicationController + include ApplicationHelper + + skip_before_filter :http_authenticate, :verify_authenticity_token, :only => [:encoder_callback] + skip_before_filter :site_authenticate, :only => [:embed_code, :encoder_callback, :explore, :flag, :flag_video_dialog, :share_via_email, :show] + before_filter :set_user, :except => [:explore] + before_filter :verify_current_user_is_not_blocked, :only => [:add_to_channel, :add_to_channel_dialog, :flag, :flag_video_dialog] + before_filter :set_video_based_on_user, :only => [:destroy, :edit, :embed_code, :flag, :flag_video_dialog, :share_via_email, :show] + before_filter :verify_user_can_access_channel, :only => [:share_via_email, :show] + before_filter :verify_user_can_access_channel_or_video, :only => [:embed_code] + before_filter :verify_user_owns_video, :only => [:destroy, :edit] + before_filter :set_featured_videos, :only => [:show] + + respond_to :js, :only => [:add_to_channel_dialog] + + # POST /:username/add_to_channel + # Access control is handled in the "add_video_to_channel!" method + def add_to_channel + new_shared_video ||= Video.add_video_to_channel!(current_user, + params[:video_id], + params[:channel_id], + params[:channel_name], + params[:channel_is_private]) + + if new_shared_video.errors.any? + render :json => { :error => get_errors_for_class(new_shared_video).to_sentence }, + :status => :unprocessable_entity + else + render :nothing => true, :status => :created + end + end + + # GET /:username/add_to_channel/dialog + # Access control is handled within this method + def add_to_channel_dialog + @video ||= Video.where(:id => params[:video_id]).joins(:video_graph).where(:video_graphs => { :status => VideoGraph.get_status_number(:ready) }).first + render :json => { :error => "That video could not be found." }, :status => :not_found and return if @video.blank? + + if @video.channel.private? && !current_user_owns?(@video) + render :json => { :error => "This video is in a private channel so it cannot be shared." }, + :status => :unprocessable_entity + else + @user ||= get_object_owner(@video) + if user_can_access_channel + respond_with(@user, @video) + else + render :json => { :error => "You do not have permission to share this video." }, + :status => :unauthorized + end + end + end + + # DELETE /:username/videos/:id + def destroy + if @video.destroy + render :nothing => true, :status => :ok + else + render :json => { :error => get_errors_for_class(@video).to_sentence }, + :status => :unprocessable_entity + end + end + + # GET /:username/videos/:id/edit + def edit + @browser_title ||= "Edit Video" + end + + # GET /:username/videos/:video_id/embed_code + def embed_code + render :json => { :html => render_to_string( :partial => 'videos/embed_code.html', + :locals => { :video => @video } ) + }, + :status => :ok + + # Create an entry for a video being played + UserEvent.create(:event_type => UserEvent.event_type_value(:video_play), + :event_object_id => @video.id, + :user_id => @video.user_id, + :event_creator_id => current_user.blank? ? 0 : current_user.id) + end + + # POST /:username/videos/:id/encoder_callback + def encoder_callback + @video = @user.videos.find_by_id(params[:video_id]) + if @video + # Check for expected JSON params in callback. + if !(params[:output].blank? or params[:output][:state].blank?) + job_state = params[:output][:state] + + # Check that video was in the expected state. + if @video.is_status?(VideoGraph::TRANSCODING) + + if job_state.downcase == "finished" + # Transcoding was successful + vg = @video.video_graph + vg.set_status(VideoGraph::READY) + vg.save + + # set the created_at time to right now so it shows + # at the top of the stream + @video.created_at = Time.now + @video.save + + # send the video to their social networks + video_owner = User.find_by_id(@video.user_id) + facebook_connected = SocialNetwork.find_by_user_id_and_provider(video_owner.id, "facebook") + twitter_connected = SocialNetwork.find_by_user_id_and_provider(video_owner.id, "twitter") + @video.send_to_facebook_or_twitter("facebook", facebook_connected) if (@video.send_to_facebook? && facebook_connected) + @video.send_to_facebook_or_twitter("twitter", twitter_connected) if (@video.send_to_twitter? && twitter_connected) + + UserMailer.delay.video_is_done_encoding(video_owner, @video) if video_owner.send_email_for_encoding_completion + else + # Transcoding had an error + # Handle the transcoding error by either retrying + # the job or treating it as a fatal error + @video.video_graph.handle_transcoding_error(params) + end + + render :text => "Video encoding status received.", + :status => :ok + else + # Don't modify the state, tell Zencoder we got this, and log an error so we can investigate + render :text => "Error: Video in unexpected state.", + :status => :ok + Airbrake.notify(:error_class => "Logged Error", :error_message => "ERROR: Callback received on file in incorrect state. Expected state: Transcoding. State: #{@video.status}") if Rails.env.production? + end + else + # Don't modify the state and return a malformed request error to Zencoder + render :text => "Error: Malformed callback.", + :status => :bad_request + Airbrake.notify(:error_class => "Logged Error", :error_message => "ERROR: Callback received with malformed parameters. Params: #{params}") if Rails.env.production? + end + else + render :text => "Error: Video not found (it might have been deleted).", + :status => :not_found + end +=begin + # Sample success response from zencoder + + {"output"=>{"state"=>"finished", "url"=>"http://brevidytest.s3.amazonaws.com/uploads/videos/101/111/enc1_f64d1d9f26314cb.mp4", "label"=>"f64d1d9f26314cb", "id"=>5264824}, + "job"=>{"test"=>true, "state"=>"finished", "id"=>4955170}, + "action"=>"encoder_callback", + "controller"=>"videos", + "user_id"=>"101", + "id"=>"111"} +=end + end + + # GET /explore + def explore + # show the 4 latest featured videos + #@latest_featured_videos = User.find_by_username("brevidy").featured_videos.limit(4) + + @latest_featured_videos = [] + + # only show most recent public videos that have a :ready state + @videos ||= Video.joins(:channel).where(:channels => { :private => false }).joins(:video_graph).where(:video_graphs => { :status => VideoGraph.get_status_number(:ready) }). + paginate(:page => params[:page], :per_page => 10, :order => 'created_at DESC') + + respond_to do |format| + params[:page].to_i > 1 ? format.js : format.html + end + end + + # POST /:username/videos/:video_id/flag + def flag + flag_id = params[:flag_id] + detailed_reason = params[:detailed_reason] + + if flag_id && Flag.where(:id => flag_id).exists? + + if signed_in? + # Check if the currently signed in user has already flagged this video for this reason + if VideoFlag.where(:flagged_by => current_user.id, :video_id => @video.id, :flag_id => flag_id).exists? + render :json => { :error => "You have already flagged this video for this reason. Thank you for your patience while we look into the issue for you." }, + :status => :unprocessable_entity and return + end + else + # Check if a cookie exists that says they have already flagged this video for this reason + unless session[:flagged_video].blank? + if session[:flagged_video][:video_id] == @video.id && session[:flagged_video][:flag_id] == flag_id + render :json => { :error => "You have already flagged this video for this reason. Thank you for your patience while we look into the issue for you." }, + :status => :unprocessable_entity and return + end + end + end + + # Video has not been flagged by the current user or the signed out user, so let's flag it + new_flagging = VideoFlag.new(:flag_id => flag_id, + :detailed_reason => detailed_reason) + new_flagging.video_id = @video.id + new_flagging.flagged_by = signed_in? ? current_user.id : nil + if new_flagging.save + render :nothing => true, + :status => :created + + # Send an email to support@brevidy.com + UserMailer.delay(:priority => 40).flagged_video(new_flagging, current_user) + + # Save a cookie about this event + session[:flagged_video] = { :video_id => @video.id, :flag_id => flag_id } + else + render :json => { :error => get_errors_for_class(new_flagging).to_sentence }, + :status => :unprocessable_entity + Airbrake.notify(:error_class => "Logged Error", :error_message => "FLAGGING: We were unable to flag a video for User (#{current_user.email unless current_user.blank?}), Video (#{video_id}), Flag Type (#{flag_id}), and Detailed Reason #{detailed_reason})") if Rails.env.production? + end + + else + render :json => { :error => "You must select one of the given options for why you want to flag the video!" }, + :status => :unprocessable_entity + end + end + + # GET /:username/videos/:video_id/flag_video_dialog + def flag_video_dialog + respond_to do |format| + format.js # flag_video_dialog.js.haml + end + end + + # GET /:username/videos/new + def new + # Make sure we don't cache this page (since it would allow the user to overwrite previous videos) + response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate" + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" + + @browser_title ||= "Upload a Video" + + # Create a temp video and video_graph object + # and break some conventions along the way + begin + @new_video_graph_object ||= current_user.video_graphs.create + @video ||= current_user.videos.new(:title => "Yayyy a new video!!!") + @video.video_graph_id = @new_video_graph_object.id + @video.channel_id = current_user.channels.first.id + @video.save + rescue + Airbrake.notify(:error_class => "Logged Error", :error_message => "ERROR CREATING VIDEO UPLOAD OBJECTS: We could not create the new video and video_graph objects for this user: #{current_user.email}") if Rails.env.production? + @error_creating_video_object = true + end + + @facebook_connected = SocialNetwork.find_by_user_id_and_provider(current_user.id, "facebook") + @twitter_connected = SocialNetwork.find_by_user_id_and_provider(current_user.id, "twitter") + + respond_to do |format| + format.html # new.html.haml + end + end + + # POST /:username/share + def share + new_shared_video ||= Video.create_shared_video!(current_user, + params[:shared_video_link], + params[:channel_id], + params[:channel_name], + params[:channel_is_private]) + + if new_shared_video.errors.any? + render :json => { :error => get_errors_for_class(new_shared_video).to_sentence }, + :status => :unprocessable_entity + else + redirect_to current_user + end + end + + # GET /:username/share_dialog + def share_dialog + respond_to do |format| + format.js # share_dialog.js.haml + end + end + + # POST /:username/videos/:id/share_via_email + def share_via_email + if @video.channel.private? && !current_user_owns?(@video) + render :json => { :error => "This video is in a private channel so it cannot be shared." }, + :status => :unprocessable_entity + else + if params[:recipient_email].blank? + render :json => { :error => "You have not specified any email addresses to send this video to" }, + :status => :unprocessable_entity + else + shared_errors = @video.share_via_email(current_user, params[:recipient_email], params[:personal_message]) + if shared_errors.blank? + render :json => { :message => "We have shared this video via email for you!" }, + :status => :ok + else + render :json => { :error => shared_errors }, + :status => :unprocessable_entity + end + end + end + end + + # GET /:username/videos/:id + def show + @browser_title ||= @user.name + + respond_to do |format| + format.html + end + end + + # PUT /:username/videos/:video_id/successful_upload + def successful_upload + if params[:video_id].blank? + Airbrake.notify(:error_class => "Logged Error", :error_message => "ERROR: Error SAVING new video info for #{current_user.email} during upload. REASON: video_id was blank.") if Rails.env.production? + render :json => { :error => "There was an error saving your new video. We have been notified of this issue." }, + :status => :unprocessable_entity + else + new_video = current_user.videos.find_by_id(params[:video_id]) + # Check the video exists + if new_video + # send back response + render :json => { :success_message => "Video uploaded! Go explore while we finish it up!", + :edit_video_path => edit_user_video_path(current_user, new_video) }, + :status => :accepted + + # Change the VideoGraph to a submitting state + # Kick off the Zencoder encoding as a delayed job + new_video_graph = new_video.video_graph + new_video_graph.set_status(VideoGraph::SUBMITTING) + new_video_graph.save + new_video_graph.delay(:priority => 0).encode + else + Airbrake.notify(:error_class => "Logged Error", :error_message => "ERROR: Error SAVING new video info for #{current_user.email} during upload. REASON: The video id passed in was not found within the current_user's videos. Maybe they tried modifying someone else's video by changing the form params?") if Rails.env.production? + render :json => { :error => "There was an error saving your video. The parameters did not match up with what was expected. We have been notified of this issue." }, + :status => :unprocessable_entity + end + end + end + + # PUT /:username/videos/:id + def update + @video ||= current_user.videos.find_by_id(params[:id]) + if @video + video_params = params[:video] + video_params[:channel_id] = params[:channel_id] + video_params[:channel_name] = params[:channel_name] + video_params[:channel_is_private] = params[:channel_is_private] + + if @video.update_attributes(video_params) + # Create new tags/taggings if necessary + @video.create_taggings(params[:video_tags]) + + if params[:redirect].blank? + # new video form + render :json => { :channel_select => render_to_string( :partial => 'videos/channel_options.html', + :locals => { :@video => @video } ) }, + :status => :accepted + else + # update video form + redirect_to(user_video_path(current_user, @video)) + end + else + render :json => { :error => get_errors_for_class(@video) }, + :status => :unprocessable_entity + end + else + render :json => { :error => "That video could not be found" }, + :status => :not_found + end + end + + # PUT /:username/video_upload_error + # we had an uploading error so capture the state + def upload_error + vg = Video.find_by_id(params[:video_id]).video_graph rescue nil + if vg + vg.set_status(VideoGraph::UPLOADING_ERROR) + detailed_error_msg = "Error: #{params[:error_message]} ; File Size: #{params[:file_size]} ; Percent Uploaded: #{params[:percent_uploaded]} ; Average Speed: #{params[:average_speed]} ; Moving Average Speed: #{params[:moving_average]}" + vg.error_message = detailed_error_msg + vg.save + # save the error for QA tracking and analytics + vg.video_errors.create(:user_id => vg.user_id, :error_status => vg.status, :error_message => detailed_error_msg) + + Airbrake.notify(:error_class => "Logged Error", :error_message => "UPLOAD ERROR: User #{vg.user_id} just had an uploading error... #{detailed_error_msg}") if Rails.env.production? + end + render :nothing => true, :status => :accepted + end + + private + # Sets a video based on the params (if it exists) + def set_video_based_on_user + video_id = params[:video_id] || params[:id] + + begin + if current_user?(@user) + @video ||= @user.videos.where(:id => video_id).joins(:video_graph).where(:video_graphs => { :status => Video.statuses_to_show_to_current_user }).first + else + @video ||= @user.videos.where(:id => video_id).joins(:video_graph).where(:video_graphs => { :status => VideoGraph.get_status_number(:ready) }).first + end + rescue ActiveRecord::StatementInvalid + @video = nil + end + + render(:template => "errors/error_404", :status => 404) if @video.blank? + end + +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100755 index 0000000..2062b5a --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,126 @@ +module ApplicationHelper + + # Generates a cache-buster path for Heroku js and CSS files + # This will break when we upgrade to Rails 3.1 and should be + # replaced with asset pipelines + # See: https://github.com/rails/rails/issues/1258 + def cache_buster_path(path) + return path << '?' << rails_asset_id(path) + end + + ##################### + ## Sorting Helpers ## + ##################### + + # For each icon, look up badge count and put it in array hash along with name and css_class + def sort_badges_by_count_for_user(badges, the_user) + sorted_badges = Array.new + badges.each do |badge| + sorted_badges << {:name => badge.name, :css_class => badge.css_class, :count => the_user.badges_count_for_type(badge.id)} + end + return sorted_badges.sort_by{|bdgs| bdgs[:count]}.reverse + end + + ####################### + ## Ownership Helpers ## + ####################### + + # Returns the owner (as a User object) for a given object (comment, video, etc) + def get_object_owner(object) + User.find_by_id(object.user_id) + end + + # Returns if the current user owns a given object (comment, video, etc) + def current_user_owns?(object) + signed_in? ? object.user_id == current_user.id : false + end + + #################### + ## Finder Helpers ## + #################### + + # Returns a badge object owned by current user for a given badge type and associated video + def find_badge_for_video(badge_type, video) + video.badges.where(:badge_from => current_user, :badge_type => badge_type).first + end + + # Returns a video object given a video ID + def get_video_by_id(id) + Video.find_by_id(id) + end + + # Returns all available, active badges + def get_all_active_badges + Icon.active.badges.order_by_name + end + + ###################### + ## Standard Helpers ## + ###################### + + # Returns whether we should be using a light or dark bg for the user + def get_background_for_user + return "light" if current_user.blank? || current_user.background_image_id == 0 + return "dark" if current_user.background_image_id == 1 + # catch all just in case... + return "light" + end + + # Returns whether or not we should be showing the Facebook OpenGraph meta tags + def we_should_show_og_tags + (controller.controller_name == "public_videos" && controller.action_name == "show") || + (controller.controller_name == "videos" && controller.action_name == "show") + end + + # Returns whether current users can invite people or not + def more_people_can_be_invited? + User::USERS_CAN_INVITE_MORE_PEOPLE + end + + # Returns whether or not to highlight Latest Activity on the left nav + def highlight_latest_activity? + controller.controller_name == "user_events" && controller.action_name == "show" + end + + # Check helper for whether or not to show navigation footer items + def infinite_scrolling_shown? + @infinite_scrolling_shown + end + + # Defines whether or not to show navigation footer items + def infinite_scrolling(trueOrNil) + @infinite_scrolling_shown = trueOrNil + end + + # Builds up all flash and validation error messages into a @flash_msg object + def compact_flash_messages + if !flash.empty? + [:error, :success, :notice, :warning].each do |key| + unless flash[key].blank? + @flash_key = key + if flash[key].kind_of?(Array) && flash[key].size > 1 + @flash_msg = flash[key].join(' & ') + elsif flash[key].kind_of?(Array) && flash[key].size == 1 + @flash_msg = flash[key].first + elsif flash[key].kind_of?(String) + @flash_msg = flash[key] + end + end + end + end + return + end + + # Defines the site-wide Title format + # Titles are stored in their respective controllers + def browser_title + default_title = "Brevidy - The soul of video" + base_title = "Brevidy" + if @browser_title.nil? + default_title + else + browser_title = "#{@browser_title} - #{base_title}" + end + end + +end diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb new file mode 100755 index 0000000..1e6b437 --- /dev/null +++ b/app/helpers/sessions_helper.rb @@ -0,0 +1,82 @@ +module SessionsHelper + + # Stores the request path into a session cookie called :return_to + def store_location + session[:return_to] = request.fullpath + end + + # Stores the request path and redirects to the login page to authenticate the user + # prior to letting them access a certain page + def deny_access + store_location + redirect_to :login, :notice => "Please login to access that page." + end + + # Sets the current_user object and optionally creates a Remember Me cookie + def sign_in(user) + cookies.permanent.signed[:remember_token] = [user.id, user.salt] + self.current_user = user + end + + # Checks if a current_user object has been instantiated or not + def signed_in? + !current_user.nil? + end + + # Destroys any Remember Me or session cookies and sets the current_user object to nil + def sign_out + cookies.delete(:remember_token) + session[:remember_token] = [nil, nil] + self.current_user = nil + end + + # Setter for the current_user object + def current_user=(user) + @current_user = user + end + + # Getter for the current_user object + # (either from instance variable or Remember Me cookie) + def current_user + @current_user ||= user_from_remember_token + end + + # Verifies if a given user is equal to the current_user + def current_user?(user) + signed_in? ? user == current_user : false + end + + # Redirects back or to a stored location + def redirect_back_or(default) + redirect_to(session[:return_to] || default) + clear_return_to + end + + # Strips all login params and downcases email field + # to prepare the params for authentication + def prepare_params_for_login + if !params[:email].blank? + params[:email].strip! + params[:email].downcase! + end + if !params[:password].blank? + params[:password].strip! + end + end + + private + # Authenticates a user from the Remember Me cookie + def user_from_remember_token + User.authenticate_with_salt(*remember_token) + end + + # Getter for the Remember Me cookie + def remember_token + cookies.signed[:remember_token] || session[:remember_token] || [nil, nil] + end + + # Clears the return_to session cookie used for storing a path during login + def clear_return_to + session[:return_to] = nil + end +end diff --git a/app/helpers/swf_uploads_helper.rb b/app/helpers/swf_uploads_helper.rb new file mode 100755 index 0000000..afce860 --- /dev/null +++ b/app/helpers/swf_uploads_helper.rb @@ -0,0 +1,300 @@ +require 'digest' +module SwfUploadsHelper + + # Creates an instance of an S3 file uploader + def s3_swf_uploader(media_type) + @media_type = media_type + # Set specific options based on media type we are uploading + case media_type + when 'banner', 'image' + @temporary_filename = filename_for_image_upload + @success_response_path = user_account_image_path(current_user) + @image_status_path = user_account_image_status_path(current_user, :media_type => media_type) + @s3_storage_folder = "#{Brevidy::Application::S3_IMAGES_RELATIVE_PATH}/#{current_user.id}" + @acl = 'public-read' + @content_type = 'image/jpg' + @filter_title = 'Images' + @filter_extensions = 'jpg,jpeg,gif,png' + @max_filesize = (media_type == 'banner') ? 5.megabytes : 2.megabytes + when 'video' + @base_filename = @new_video_graph_object.base_filename + @temporary_filename = "orig_#{@base_filename}_#{current_user.id}" + @success_response_path = user_video_successful_upload_path(current_user, @video) + @video_upload_error_path = user_video_upload_error_path(current_user, @video) + @s3_storage_folder = "#{Brevidy::Application::S3_VIDEOS_RELATIVE_PATH}/#{current_user.id}" + @acl = 'private' + @content_type = 'video/' + @filter_title = 'Videos' + @swf_filter_extensions = '*.3gp;*.3gpp;*.mov;*.avi;*.mp4;*.m4v;*.mpg;*.mpeg;*.rm;*.ram;*.ra;*.flv;*.f4v;*.ogm;*.asf;*.wma;*.ivf;*.wmv;*.ogv;*.3gp;*.swf;*.vob;*.divx;*.mts;*.m2ts;' + @max_filesize = 750.megabytes + end + + # Set generic defaults + @s3_base_url = Brevidy::Application::S3_BASE_URL + @s3_file_key = "#{@s3_storage_folder}/#{@temporary_filename}" + @expiration_date = 10.hours.from_now.utc.iso8601 + + # Generate a policy based on the above + @policy = swf_generated_policy + + # Generate a signature based on the policy + @signature = swf_generated_signature(@policy) + + # Return the javascript given all of the above + return swfupload_uploader_javascript + end + + private + # Returns the proper S3 bucket based on the Rails environment + def swf_bucket + return Brevidy::Application::S3_BUCKET + end + + # Returns the proper S3 access key based on the Rails environment + def swf_access_key + return Brevidy::Application::S3_ACCESS_KEY_ID + end + + # Returns the proper S3 secret access key based on the Rails environment + def swf_secret_access_key + return Brevidy::Application::S3_SECRET_ACCESS_KEY + end + + # Returns a generated S3 policy which places restrictions and sets configs + # for all uploads + def swf_generated_policy + return policy = Base64.encode64("{'expiration': '#{@expiration_date}', + 'conditions': [ + {'bucket': '#{swf_bucket}'}, + {'acl': '#{@acl}'}, + {'success_action_status': '201'}, + ['content-length-range', 0, #{@max_filesize}], + ['starts-with', '$key', '#{@s3_file_key}'], + ['starts-with', '$Content-Type', ''], + ['starts-with', '$Filename', ''] + ] + }").gsub(/\n|\r/, '') + end + + # Returns a generated S3 signature based on the secret key and policy + def swf_generated_signature(policy) + return signature = Base64.encode64( + OpenSSL::HMAC.digest( + OpenSSL::Digest::Digest.new('sha1'), + swf_secret_access_key, policy)).gsub("\n","") + end + + # Returns a string of javascript for instantiating a SWFUpload uploader object + def swfupload_uploader_javascript(options = {}) + uploader_js = "" + uploader_js << javascript_include_tag("uploader/swfupload.and.speed.min") + uploader_js << javascript_tag(" + $(function() { + var new_video = #{@media_type == 'video'}; + var percent_uploaded = 0; + + var swf_uploader = new SWFUpload({ + // SWF Settings + flash_url: '/javascripts/uploader/swfupload.swf', + prevent_swf_caching: false, + + // Button Settings + button_action: SWFUpload.BUTTON_ACTION.SELECT_FILE, + button_image_url: '#{cache_buster_path("/javascripts/uploader/select_#{@media_type}_v1.png")}', + button_placeholder_id: 'select-#{@media_type}', + button_width: 103, + button_height: 28, + button_window_mode: 'transparent', + button_cursor: SWFUpload.CURSOR.HAND, + + // S3 settings + upload_url: '#{@s3_base_url.gsub('https://', 'http://')}', + file_post_name: 'file', + post_params: { + 'key': '#{@s3_file_key}', + 'Filename': '${filename}', + 'acl': '#{@acl}', + 'Content-Type': '#{@content_type}', + 'success_action_status': '201', + 'AWSAccessKeyId': '#{swf_access_key}', + 'policy': '#{@policy}', + 'signature': '#{@signature}' + }, + + // File settings + file_size_limit: '#{@max_filesize / 1048576} MB', + file_types: '#{@swf_filter_extensions}', + file_types_description: '#{@filter_title}', + file_upload_limit: 1, + file_queue_limit: 1, + + // Event handler settings + http_success : [201], + file_dialog_complete_handler: fileDialogComplete, + file_queue_error_handler: fileQueueError, + upload_start_handler: uploadStart, + upload_progress_handler: uploadProgress, + upload_error_handler: uploadError, + upload_success_handler: uploadSuccess, + + // Debug settings + debug: #{Rails.env.development?} + }); + + function fileDialogComplete() { swf_uploader.startUpload(); } + + function uploadStart(file) { + // Move the uploader browse button off screen + $('.uploader-area').css({'position':'relative','top':'-9999999px','height':'0'}); + swf_uploader.setButtonDisabled(true); + + $('#progress-bar span').show(); + $('#progress-bar').show('fast', function () { + $('#new-video-form').slideDown('fast'); + }); + + // warn user if they are still uploading to not leave the page + window.onbeforeunload = function() { + return 'You are currently uploading a file. Are you sure you want to leave this page and cancel the upload?'; + }; + } + + function uploadSuccess(file, serverData) { + $('#progress-bar .progress').css('width', '100%'); + $('#progress-bar span').text('Processing...'); + + if (new_video) { + var ajax_data = { }; + $('#new-video-form').submit(); + } else { + var ajax_data = { 'media_type':'#{@media_type}', + 'filename':'#{@temporary_filename}' }; + } + + $.ajax({ + data: ajax_data, + type: 'PUT', + url: '#{@success_response_path}', + success: function(json) { + // Update progress bar + $('#progress-bar span').text(json.success_message); + + // Start polling for image uploads + if (new_video) { + $('.success-message.video-saved p').html('Video information saved. You can edit it by clicking here'); + } else { + // Start polling for image uploads + simple_poll_request(); + } + }, + error: function(response) { + // show failure on progress bar + $('#progress-bar .progress').addClass('error').css('width', '100%'); + $('#progress-bar span').text('Upload Failed :('); + + brevidy.dialog('Error', 'There was an error uploading your file. Please e-mail us at support@brevidy.com if this continues to happen.', 'error'); + } + }); + + // clear out the user warning + window.onbeforeunload = null; + } + + var progressSpeed = 0; + function uploadProgress(file, bytesLoaded, bytesTotal) { + if (bytesTotal) { + percent_uploaded = (bytesLoaded / bytesTotal) * 100; + $('#progress-bar .progress').css('width', percent_uploaded + '%'); + // Convert to bytes per second and show time left (currentSpeed is bits per second) + var speed = SWFUpload.speed.formatBytes(Math.round(file.currentSpeed) * 0.125) + '/s (' + SWFUpload.speed.formatTime(file.timeRemaining) + ' left)'; + if (progressSpeed == 5 || percent_uploaded > 95) { $('#progress-bar span').text('Uploading... ' + speed); progressSpeed = 1;} + progressSpeed++; + } + } + + function fileQueueError(file, errorCode, message) { + switch (errorCode) { + case SWFUpload.QUEUE_ERROR.QUEUE_LIMIT_EXCEEDED: + var error_message = 'You can only upload one file at a time.'; + break; + case SWFUpload.QUEUE_ERROR.FILE_EXCEEDS_SIZE_LIMIT: + var error_message = 'The file you chose was too large (it cannot be larger than ' + #{@max_filesize / 1048576} + ' MB). Please resize the file or choose a different one to upload.'; + break; + case SWFUpload.QUEUE_ERROR.ZERO_BYTE_FILE: + var error_message = 'The file you selected is empty. Please select another file.'; + break; + case SWFUpload.QUEUE_ERROR.INVALID_FILETYPE: + var error_message = 'The file you choose is not an allowed file type.'; + break; + default: + var error_message = 'An error occurred in the upload. Please email us at support@brevidy.com if this continues to happen.'; + break; + } + brevidy.dialog('Error', error_message, 'error'); + return; + } + + function uploadError(file, errorCode, message) { + if (errorCode == SWFUpload.UPLOAD_ERROR.FILE_CANCELLED) { return; } + switch (errorCode) { + case SWFUpload.UPLOAD_ERROR.HTTP_ERROR: + var error_message = message; + break; + case SWFUpload.UPLOAD_ERROR.UPLOAD_FAILED: + var error_message = 'Upload failed'; + break; + case SWFUpload.UPLOAD_ERROR.IO_ERROR: + var error_message = 'IO error (check internet connection)'; + break; + case SWFUpload.UPLOAD_ERROR.SECURITY_ERROR: + var error_message = 'Security error'; + break; + case SWFUpload.UPLOAD_ERROR.UPLOAD_LIMIT_EXCEEDED: + var error_message = 'Upload limit exceeded'; + break; + case SWFUpload.UPLOAD_ERROR.FILE_VALIDATION_FAILED: + var error_message = 'Failed validation so upload was skipped'; + break; + case SWFUpload.UPLOAD_ERROR.UPLOAD_STOPPED: + var error_message = 'Upload was stopped'; + break; + default: + msg = 'Unknown Error (' + errorCode + ')'; + break; + } + // Show the user an error box + brevidy.dialog('Error', 'There was an error during the upload: ' + error_message, 'error'); + + // Show failure on progress bar + $('#progress-bar .progress').addClass('error').css('width', '100%'); + $('#progress-bar span').text('Upload Failed :('); + + // Send off error to the server + if (new_video) { + $.ajax({ + data: { 'error_message':error_message, + 'file_size':SWFUpload.speed.formatBytes(file.size), + 'percent_uploaded':Math.round(percent_uploaded), + 'average_speed':SWFUpload.speed.formatBytes(file.averageSpeed), + 'moving_average':SWFUpload.speed.formatBytes(file.movingAverageSpeed) }, + type: 'PUT', + url: '#{@video_upload_error_path}' + }); + } + + // hide the meta area + $('#new-video-form').slideUp('fast'); + + // clear out the user warning + window.onbeforeunload = null; + } + });") + end + + # Generates a random string for a temporary image upload (prior to processing the image) + def filename_for_image_upload + random_token = Digest::SHA2.hexdigest("#{Time.now.utc}--#{current_user.id.to_s}").first(15) + "temp_upload_#{random_token}" + end + +end \ No newline at end of file diff --git a/app/helpers/uploads_helper.rb b/app/helpers/uploads_helper.rb new file mode 100755 index 0000000..5732497 --- /dev/null +++ b/app/helpers/uploads_helper.rb @@ -0,0 +1,271 @@ +require 'digest' +module UploadsHelper + + # Creates an instance of an S3 file uploader + def s3_uploader(media_type) + # Set specific options based on media type we are uploading + case media_type + when 'banner', 'image' + temporary_filename = filename_for_image_upload + success_response_path = user_account_image_path(current_user) + image_status_path = user_account_image_status_path(current_user, :media_type => media_type) + s3_storage_folder = "#{Brevidy::Application::S3_IMAGES_RELATIVE_PATH}/#{current_user.id}" + acl = 'public-read' + content_type = 'image/jpg' + filter_title = 'Images' + filter_extensions = 'jpg,jpeg,gif,png' + max_filesize = (media_type == 'banner') ? 5.megabytes : 2.megabytes + when 'video' + base_filename = @new_video_graph_object.base_filename + temporary_filename = "orig_#{base_filename}_#{current_user.id}" + success_response_path = user_video_successful_upload_path(current_user, @video) + video_upload_error_path = user_video_upload_error_path(current_user, @video) + s3_storage_folder = "#{Brevidy::Application::S3_VIDEOS_RELATIVE_PATH}/#{current_user.id}" + acl = 'private' + content_type = 'video/' + filter_title = 'Videos' + filter_extensions = '3gp,3gpp,mov,avi,mp4,m4v,mpg,mpeg,rm,ram,ra,flv,f4v,ogm,asf,wma,ivf,wmv,ogv,3gp,swf,vob,divx,mts,m2ts' + max_filesize = 750.megabytes + end + + # Set generic defaults + s3_base_url = Brevidy::Application::S3_BASE_URL + s3_file_key = "#{s3_storage_folder}/#{temporary_filename}" + expiration_date = 10.hours.from_now.utc.iso8601 + + # Generate a policy based on the above + policy = generated_policy(expiration_date, bucket, acl, max_filesize, s3_file_key) + + # Generate a signature based on the policy + signature = generated_signature(policy) + + # Return the javascript given all of the above + return uploader_javascript(:media_type => media_type, :max_filesize => max_filesize, :acl => acl, + :s3_file_key => s3_file_key, :policy => policy, :signature => signature, + :s3_base_url => s3_base_url, :content_type => content_type, + :filter_title => filter_title, :filter_extensions => filter_extensions, + :temporary_filename => temporary_filename, :base_filename => base_filename, + :success_response_path => success_response_path, :image_status_path => image_status_path, + :video_upload_error_path => video_upload_error_path) + end + + private + # Returns the proper S3 bucket based on the Rails environment + def bucket + return Brevidy::Application::S3_BUCKET + end + + # Returns the proper S3 access key based on the Rails environment + def access_key + return Brevidy::Application::S3_ACCESS_KEY_ID + end + + # Returns the proper S3 secret access key based on the Rails environment + def secret_access_key + return Brevidy::Application::S3_SECRET_ACCESS_KEY + end + + # Returns a generated S3 policy which places restrictions and sets configs + # for all uploads + def generated_policy(expiration_date, bucket, acl, max_filesize, s3_file_key) + return policy = Base64.encode64("{'expiration': '#{expiration_date}', + 'conditions': [ + {'bucket': '#{bucket}'}, + {'acl': '#{acl}'}, + {'success_action_status': '201'}, + ['content-length-range', 0, #{max_filesize}], + ['starts-with', '$key', '#{s3_file_key}'], + ['starts-with', '$Content-Type', ''], + ['starts-with', '$name', ''], + ['starts-with', '$Filename', ''] + ] + }").gsub(/\n|\r/, '') + end + + # Returns a generated S3 signature based on the secret key and policy + def generated_signature(policy) + return signature = Base64.encode64( + OpenSSL::HMAC.digest( + OpenSSL::Digest::Digest.new('sha1'), + secret_access_key, policy)).gsub("\n","") + end + + # Returns a string of javascript for instantiating an uploader + def uploader_javascript(options = {}) + uploader_js = "" + uploader_js << javascript_include_tag("plupload/plupload.full") + uploader_js << javascript_tag(" + $(function() { + var plupload_max_file_size = '#{options[:max_filesize] / 1048576} MB'; + var new_video = #{options[:media_type] == 'video'}; + var video_id = 0; + var uploader_dom_id; + + var uploader = new plupload.Uploader({ + runtimes : 'flash', + browse_button : 'select-#{options[:media_type]}', + max_file_size : plupload_max_file_size, + url : '#{options[:s3_base_url].gsub('https://', 'http://')}', + flash_swf_url: '/javascripts/plupload/plupload.flash.swf', + filters : [ {title : '#{options[:filter_title]}', extensions : '#{options[:filter_extensions]}'} ], + init : { + // Called as soon as a file has been added and prior to upload + FilesAdded: function(up, files) { + uploader_dom_id = up.id + '_' + up.runtime + '_container'; + + // Start the uploader + uploader.start(); + + // hide the add files button and show progress bar + $('#select-#{options[:media_type]}').fadeOut('fast', function() { + $(this).remove(); + }); + $('#progress-bar span').show(); + $('#progress-bar').show('fast', function () { + $('#new-video-form').slideDown('fast'); + }); + + plupload.each(files, function(file) { + if (up.files.length > 1) { up.removeFile(file); } + }); + + // warn user if they are still uploading to not leave the page + window.onbeforeunload = function() { + return 'You are currently uploading a file. Are you sure you want to leave this page and cancel the upload?'; + }; + + }, + FileUploaded: function(up, file, info) { + $('#progress-bar .progress').css('width', '100%'); + + if (new_video) { + var ajax_data = { }; + $('#new-video-form').submit(); + } else { + var ajax_data = { 'media_type':'#{options[:media_type]}', + 'filename':'#{options[:temporary_filename]}' }; + } + + $.ajax({ + data: ajax_data, + type: 'PUT', + url: '#{options[:success_response_path]}', + success: function(json) { + // Update progress bar + $('#progress-bar span').text(json.success_message); + + // Start polling for image uploads + if (new_video) { + $('.success-message.video-saved p').html('Video information saved. You can edit it by clicking here'); + } else { + // Start polling for image uploads + simple_poll_request(); + } + }, + error: function(response) { + // show failure on progress bar + $('#progress-bar .progress').addClass('error').css('width', '100%'); + $('#progress-bar span').text('Upload Failed :('); + + brevidy.dialog('Error', 'There was an error uploading your file. Please e-mail us at support@brevidy.com if this continues to happen.', 'error'); + } + }); + + // clear out the user warning + window.onbeforeunload = null; + + }, + UploadProgress: function(up, file) { + // Move the uploader browse button off screen + $('#' + uploader_dom_id).css('top', '-9999999px'); + + // Binds progress to progress bar + if(file.percent < 100){ + $('#progress-bar .progress').css('width', file.percent+'%'); + $('#progress-bar span').text('Uploading... '+ file.percent +'%'); + } else { + $('#progress-bar .progress').css('width', '100%'); + $('#progress-bar span').text('Processing... please wait'); + } + }, + Error: function(up, error) { + var error_message; + + // shows error object + if (error.message.indexOf('File size') !== -1) { + brevidy.dialog('Error', 'The file you chose was too large (it cannot be larger than ' + plupload_max_file_size + '). Please resize the file or choose a different one to upload.', 'error'); + } else { + // show failure on progress bar + $('#progress-bar .progress').addClass('error').css('width', '100%'); + $('#progress-bar span').text('Upload Failed :('); + + error_message = error.message; + brevidy.dialog('Error', 'There was an error uploading your file: ' + error_message + ' Please e-mail us at support@brevidy.com if this continues to happen.', 'error'); + + if (new_video) { + $.ajax({ + data: { 'error_message':error_message }, + type: 'PUT', + url: '#{options[:video_upload_error_path]}' + }); + } + + } + + // hide the meta area + $('#new-video-form').slideUp('fast'); + + // clear out the user warning + window.onbeforeunload = null; + + } + }, + multi_selection: false, + multipart: true, + multipart_params: { + 'key': '#{options[:s3_file_key]}', + 'Filename': '${filename}', + 'acl': '#{options[:acl]}', + 'Content-Type': '#{options[:content_type]}', + 'success_action_status': '201', + 'AWSAccessKeyId' : '#{access_key}', + 'policy': '#{options[:policy]}', + 'signature': '#{options[:signature]}' + }, + file_data_name: 'file' + }); + + // instantiates the uploader + uploader.init(); + + + // A recursive polling function for image processing status + function simple_poll_request() { + $.ajax({ + type: 'GET', + url: '#{options[:image_status_path]}', + success: function (json) { + if (json.job_status == 'processing') { + // wait 1 second and try again + setTimeout(function() { simple_poll_request(); }, 1000); + } else if (json.job_status == 'success') { + // page will redirect + } else if (json.job_status == 'error') { + $('#progress-bar .progress').addClass('error'); + $('#progress-bar span').text('Processing Error :('); + brevidy.dialog('Error', 'There was an error processing your photo. Please try a different image or e-mail us at support@brevidy.com if this continues to happen.', 'error'); + } + } + }); + }; + + });") + end + + # Generates a random string for a temporary image upload (prior to processing the image) + def filename_for_image_upload + random_token = Digest::SHA2.hexdigest("#{Time.now.utc}--#{current_user.id.to_s}").first(15) + "temp_upload_#{random_token}" + end + +end \ No newline at end of file diff --git a/app/helpers/user_events_helper.rb b/app/helpers/user_events_helper.rb new file mode 100755 index 0000000..5f74efe --- /dev/null +++ b/app/helpers/user_events_helper.rb @@ -0,0 +1,18 @@ +module UserEventsHelper + def render_content_for_event(event) + event_class ||= event.get_event_class_type + event_objects ||= event.get_event_objects(event_class[0]) + + # Event Objects will be blank if there was an error getting info about that particular event + unless event_objects.blank? + # Renders the correct HTML partial given the event info + render :partial => 'user_events/user_event.html', + :locals => { :event_type => event_class[1], + :object_class => event_objects[1].class, + :object => event_objects[1], + :user => event_objects[0], + :video => event_objects[2], + :seen_by_user => event.seen_by_user } + end + end +end \ No newline at end of file diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb new file mode 100755 index 0000000..56a584a --- /dev/null +++ b/app/mailers/user_mailer.rb @@ -0,0 +1,222 @@ +class UserMailer < ActionMailer::Base + default :from => "Brevidy " + + # Notifies the user that they have been permanently banned + def banned(user_email, reason) + @user_email = user_email + @reason = reason + + mail( :to => @user_email, + :subject => "Your Brevidy account has been banned") + end + + # Sends a celebratory email for a new user + def celebrate_new_user(user) + @user = user + @user_count = User.count + mail( :to => "support@brevidy.com", + :subject => "Yay!!! A new user signed up!") + end + + # Sends the user instructions on how to confirm their account + def confirmation_instructions(user) + @user = user + @url = user_confirm_url(@user, :token => @user.confirmation_token) + mail( :to => @user.email, + :subject => "Please confirm your email address") + end + + # Notifies the user that their account has been deactivated + def deactivated(user, reason) + @user = user + @reason = reason + + mail( :to => @user.email, + :subject => "Your Brevidy account has been deactivated") + end + + # Notifies the user of a fatal error with their video + def fatal_error_on_video(user) + @user = user + + mail( :to => @user.email, + :subject => "There was an error processing your video") + end + + # Notifies the user when a video they posted has been featured by Brevidy + def featured_video(user) + @user = user + @url = explore_url + mail( :to => @user.email, + :subject => "Yay! Your video was featured!" ) + end + + # Notifies support@brevidy.com about a flagged video + def flagged_video(video_flag, current_user) + @video_flag = video_flag + @reason = @video_flag.detailed_reason + @flag_type_description = Flag.find_by_id(@video_flag.flag_id).reason + @video = Video.find_by_id(@video_flag.video_id) + @video_owner = User.find_by_id(@video.user_id) + @url_to_flagged_by = user_url(current_user) unless current_user.blank? + @url_to_video = user_video_url(@video_owner, @video) + @url_to_owner = user_url(@video_owner) + + mail( :to => "support@brevidy.com", + :subject => "FLAGGED: Video #{@video.id} has been flagged for review") + end + + # Sends an invitation email out + def invitation(invitation, recipient_email, personal_message) + @personal_message = personal_message + @sender = User.find_by_id(invitation.user_id) + @token = invitation.token + @site_url = root_url + @url = signup_via_invitation_url(:invitation_token => @token) + + if @sender.blank? + subject = "Invitation to join Brevidy!" + else + subject = "#{@sender.name} invited you to join Brevidy!" + end + + mail( :to => recipient_email, + :subject => subject) + end + + # Notifies the user that someone just subscribed to one of their channels + def new_subscriber(publisher, subscriber, channel) + @publisher = publisher + @subscriber = subscriber + @channel = channel + @url = user_url(@subscriber) + @account_url = user_account_url(@publisher) + + mail( :to => @publisher.email, + :subject => "#{@subscriber.name} subscribed to one of your channels on Brevidy" ) + end + + # Notifies the user of a new comment on their video + def new_comment(comment, person_we_are_emailing, the_comment_is_a_reply) + @comment = comment + @person_we_are_emailing = person_we_are_emailing + @the_comment_is_a_reply = the_comment_is_a_reply + @video = Video.find_by_id(@comment.video_id) + @video_owner = User.find_by_id(@video.user_id) + @commenter = User.find_by_id(@comment.user_id) + @url = user_video_url(@video_owner, @video) + @default_image = "#{Brevidy::Application::S3_BASE_URL}/images/default_user_50px.jpg" + @account_url = user_account_url(@person_we_are_emailing) + + @the_comment_is_a_reply ? (subject = "#{@commenter.name} also commented on #{@video_owner.name}'s video") : (subject = "#{@commenter.name} just commented on your video") + mail( :to => @person_we_are_emailing.email, + :subject => subject ) + end + + # Notifies the user of a new badge on their video + def new_badge(badge) + @badge = badge + @video = Video.find_by_id(@badge.video_id) + @video_owner = User.find_by_id(@video.user_id) + @badge_from = User.find_by_id(@badge.badge_from) + @icon = Icon.find_by_id(@badge.badge_type) + @url = user_video_url(@video_owner, @video) + @default_image = "#{Brevidy::Application::S3_BASE_URL}/images/default_user_50px.jpg" + @account_url = user_account_url(@video_owner) + + mail( :to => @video_owner.email, + :subject => "New badge on your video") + end + + # Notifies a user that their request to access a channel was approved + def private_channel_request_approved(requesting_user, channel) + @requesting_user = requesting_user + @channel = channel + @channel_owner = User.find_by_id(@channel.user_id) + @channel_url = user_channel_url(@channel_owner, @channel) + @account_url = user_account_url(@requesting_user) + + mail( :to => @requesting_user.email, + :subject => "#{@channel_owner.name} has approved your access request" ) + end + + # Notifies the user that their account has been reactivated + def reactivated(user) + @user = user + + mail( :to => @user.email, + :subject => "Your Brevidy account has been reactivated") + end + + # Sends out the personal feedback email + def redesign_feedback(user) + first_name = user.name.split(' ')[0] + @user_first_name = first_name.blank? ? user.name : first_name + @user_url = user_url(user) + + mail( :from => "Rob Phillips ", + :to => user.email, + :subject => "A word about the new Brevidy") + end + + # Notifies a user that someone wants access to their private channel + def request_channel_approval(current_user, channel, channel_request) + @requesting_user = current_user + @channel = channel + @channel_owner = User.find_by_id(@channel.user_id) + @approve_url = user_channel_request_access_url(@channel_owner, @channel, :approved => true, :token => channel_request.token) + @ignore_url = user_channel_request_access_url(@channel_owner, @channel, :ignored => true, :token => channel_request.token) + @account_url = user_account_url(@channel_owner) + @requesting_user_url = user_url(@requesting_user) + + mail( :to => @channel_owner.email, + :subject => "#{@requesting_user.name} is requesting access to one of your private channels" ) + end + + # Sends reset password instructions + def reset_password_instructions(user) + @user = user + @url = user_validate_reset_password_url(@user, :token => @user.reset_token) + mail( :to => @user.email, + :subject => "Reset password instructions") + end + + # Newsletters + def september_2011_newsletter(the_user) + @user = the_user + @latest_activity_url = user_latest_activity_url(@user) + @invitation_link_url = signup_via_invitation_url(:invitation_token => @user.invitation_link.token) + @whats_happening_url = whats_happening_url() + @account_url = user_account_url(@user) + @new_video_url = new_user_video_url(@user) + @forgotten_password_url = forgotten_password_url() + + mail( :to => @user.email, + :subject => "Latest updates on Brevidy!") + end + + # Shares a video via email + def share_video(current_user, video, recipient_email, personal_message) + @user = current_user + @link_to_video_url = public_video_url(:public_token => video.public_token) + @personal_message = personal_message + + if current_user.blank? + mail( :to => recipient_email, + :subject => "Someone has shared a video with you!" ) + else + mail( :to => recipient_email, + :subject => "#{current_user.name} has shared a video with you!" ) + end + end + + # Tells the user that their video is done encoding + def video_is_done_encoding(video_owner, video) + @user = video_owner + @link_to_video_url = user_video_url(@user, video) + @account_url = user_account_url(@user) + + mail( :to => @user.email, + :subject => "Your video is ready to be watched on Brevidy!" ) + end +end diff --git a/app/models/badge.rb b/app/models/badge.rb new file mode 100755 index 0000000..3037390 --- /dev/null +++ b/app/models/badge.rb @@ -0,0 +1,51 @@ +class Badge < ActiveRecord::Base + # Defines which attributes can be mass-assigned via a form POST (be careful here) + attr_accessible :badge_type + + # validates these attribute conditions are met + validates :video_id, :presence => { :message => "^There was no video ID passed in" } + validates :badge_type, :presence => { :message => "^There was no badge type passed in" } + validates :badge_from, :presence => { :message => "^There was no person passed in for who the badge is from" } + + # Active Record relationships + belongs_to :video + belongs_to :icon + + scope :most_recent, limit(50) + + # Returns the proper name for a given badge type + def name + Icon.find_by_id(self.badge_type).name + end + # Returns the CSS class name for a given badge type + def css_class + Icon.find_by_id(self.badge_type).css_class + end + def from + User.find_by_id(self.badge_from) + end + + class << self + # returns how many badges and of what type a user has that are viewable by current user + def badges_count_for_type(badge_type) + Badge.where(:badge_type => badge_type).count + end + end + +end + + + + +# == Schema Information +# +# Table name: badges +# +# id :integer not null, primary key +# video_id :integer +# badge_type :integer +# badge_from :integer +# created_at :datetime +# updated_at :datetime +# + diff --git a/app/models/banned_user.rb b/app/models/banned_user.rb new file mode 100755 index 0000000..9b90ee0 --- /dev/null +++ b/app/models/banned_user.rb @@ -0,0 +1,21 @@ +class BannedUser < ActiveRecord::Base + validates :email, :presence => true + validates :reason, :presence => true + validates :detailed_reason, :presence => true +end + + + + +# == Schema Information +# +# Table name: banned_users +# +# id :integer not null, primary key +# email :string(255) +# reason :text +# created_at :datetime +# updated_at :datetime +# detailed_reason :text +# + diff --git a/app/models/banner_image.rb b/app/models/banner_image.rb new file mode 100755 index 0000000..ed2ca50 --- /dev/null +++ b/app/models/banner_image.rb @@ -0,0 +1,16 @@ +class BannerImage < ActiveRecord::Base +end + + +# == Schema Information +# +# Table name: banner_images +# +# id :integer not null, primary key +# path :string(255) +# filename :string(255) +# active :boolean default(TRUE) +# created_at :datetime +# updated_at :datetime +# + diff --git a/app/models/blocking.rb b/app/models/blocking.rb new file mode 100755 index 0000000..889c8d9 --- /dev/null +++ b/app/models/blocking.rb @@ -0,0 +1,23 @@ +class Blocking < ActiveRecord::Base + # protect all attributes from mass-assignment + attr_protected :id, :requesting_user, :blocked_user, :created_at, :updated_at + + validates :requesting_user, :presence => true + validates :blocked_user, :presence => true + + belongs_to :blocked_people, :foreign_key => "blocked_user", :class_name => "User" +end + + + +# == Schema Information +# +# Table name: blockings +# +# id :integer not null, primary key +# requesting_user :integer +# blocked_user :integer +# created_at :datetime +# updated_at :datetime +# + diff --git a/app/models/channel.rb b/app/models/channel.rb new file mode 100755 index 0000000..5e6a1e7 --- /dev/null +++ b/app/models/channel.rb @@ -0,0 +1,187 @@ +class Channel < ActiveRecord::Base + attr_accessible :title + + before_destroy :make_sure_they_dont_destroy_featured_channel + + # Validations + before_validation :strip_title + before_validation :generate_public_token, :on => :create + validates :title, :presence => { :message => "^Please name your channel" }, + :length => { :maximum => 30, :message => "^Channel name is too long (maximum of 30 characters)" } + validates_uniqueness_of :title, :scope => [:user_id], + :case_sensitive => false, + :message => "^You already have a channel with that name" + validate :featured_channel_stays_public, :user_cant_change_featured_channel_title, :on => :update + validates_presence_of :public_token, :unless => :featured + + belongs_to :user + has_many :videos, :dependent => :destroy, :order => 'created_at DESC' + has_many :subscribers, :foreign_key => "channel_id", + :class_name => "Subscription", + :dependent => :destroy + has_many :subscribers_as_people, :through => :subscribers, :source => :subscriber_people, :order => 'UPPER(name) ASC' + + # User requests to access private channels + has_many :channel_requests, :dependent => :destroy + + + ################ + ## SEO Params ## + ################ + + # Creates an SEO friendly slug in the URL + # i.e. http://brevidy.com/rob/channels/3-my-public-videos + def to_param + "#{id}-#{title.parameterize}" + end + + + ############# + ## Helpers ## + ############# + + # Returns 9 videos that have a :ready status + def videos_for_preview + self.videos.joins(:video_graph).where(:video_graphs => { :status => VideoGraph.get_status_number(:ready) }).limit(9) + end + + + ################# + ## Permissions ## + ################# + + # Returns whether or not the given user can access the channel + def is_accessible_by(current_user) + return true if !self.private? + + unless current_user.blank? + return false if Blocking.where(:requesting_user => self.user_id, :blocked_user => current_user.id).exists? + return true if self.user_id == current_user.id + return true if current_user.is_subscribed_to?(self) + end + + # If we got this far, they can't access it. + return false + end + + # Regenerates the public token for a private channel + def regenerate_public_token! + self.regenerate_public_token and self.save + end + def regenerate_public_token + loop do + new_token = SecureRandom.base64(50).tr('+/=', 'xyz').first(50) + break self.public_token = new_token unless Channel.where(:public_token => new_token).exists? + end + end + + + ################### + ## Subscriptions ## + ################### + + def subscribe!(current_user, request_approved = false) + subscription ||= current_user.subscriptions.new + subscription.errors.add(:id, "^You cannot subscribe to your own channel.") and return subscription if self.user_id == current_user.id + subscription.errors.add(:channel_id, "^You are already subscribed to this channel.") and return subscription if Subscription.where(:subscriber_id => current_user.id, :channel_id => self.id).exists? + + channel_owner = User.find_by_id(self.user_id) + if self.private? && request_approved == false + if ChannelRequest.where(:user_id => current_user.id, :channel_id => self.id).exists? + subscription.errors.add(:channel_id, "^You have already requested access to this channel. Please be patient while this person responds to the request.") + else + # Add channel request + channel_request = ChannelRequest.new + channel_request.user_id = current_user.id + channel_request.channel_id = self.id + channel_request.save + + # Request permission + subscription.errors.add(:subscriber_id, "^requesting permission") + UserMailer.delay.request_channel_approval(current_user, self, channel_request) if channel_owner.send_email_for_private_channel_request + + # Add event to activity feed + UserEvent.delay(:priority => 40).create(:event_type => UserEvent.event_type_value(:channel_request), + :event_object_id => channel_request.id, + :user_id => self.user_id, + :event_creator_id => current_user.id) + end + else + subscription.subscriber_id = current_user.id + subscription.publisher_id = self.user_id + subscription.channel_id = self.id + subscription.save + + # Send an email to the subscriber if this was a request approval + UserMailer.delay.private_channel_request_approved(current_user, self) if request_approved + + # Send an email to the channel owner about the new subscriber + # unless they just approved the subscription request or their settings say not to + UserMailer.delay.new_subscriber(channel_owner, current_user, self) if channel_owner.send_email_for_new_subscriber && !request_approved + + # Add event to activity feed + UserEvent.delay(:priority => 40).create(:event_type => UserEvent.event_type_value(:subscription), + :event_object_id => subscription.id, + :user_id => self.user_id, + :event_creator_id => current_user.id) + end + + return subscription + end + + def unsubscribe!(current_user) + subscription ||= current_user.subscriptions.where(:channel_id => self.id).first + if subscription.blank? + return false + else + subscription.destroy + return true + end + end + + + private + # Populates the public token for a public/private channel + def generate_public_token + self.regenerate_public_token unless self.featured? + end + + # Ensures the featured channel always stays public + def featured_channel_stays_public + errors.add(:private, "^You cannot make the featured channel private") if self.featured? && self.private? + end + + # Ensures the featured channel's title always stays the same + def user_cant_change_featured_channel_title + errors.add(:title, "^You cannot change the name for the featured videos channel") if self.featured? && self.title != "Featured Videos" + end + + # Prepares params for validations + def strip_title + self.title = self.title.strip unless self.title.blank? + end + + # Ensure the user does not destroy the featured channel + def make_sure_they_dont_destroy_featured_channel + return false if self.featured? + end +end + + + + +# == Schema Information +# +# Table name: channels +# +# id :integer not null, primary key +# user_id :integer +# title :string(255) +# private :boolean default(FALSE) +# featured :boolean default(FALSE) +# created_at :datetime +# updated_at :datetime +# public_token :string(255) +# recommended :boolean default(FALSE) +# + diff --git a/app/models/channel_request.rb b/app/models/channel_request.rb new file mode 100755 index 0000000..abbc40d --- /dev/null +++ b/app/models/channel_request.rb @@ -0,0 +1,43 @@ +require 'securerandom' + +class ChannelRequest < ActiveRecord::Base + attr_protected :channel_id, :user_id, :ignored, :token + + before_validation :generate_token, :on => :create + validates :channel_id, :user_id, :token, :presence => true + validates_uniqueness_of :channel_id, :scope => [:user_id] + validate :request_is_not_from_channel_owner + + belongs_to :user + belongs_to :channel + + private + # Validates the channel owner can't create a request to their own channel + def request_is_not_from_channel_owner + channel_owner = Channel.find_by_id(self.channel_id).user rescue nil + (errors.add(:user_id, "^You cannot request access to your own channel, silly.") if self.user_id == channel_owner.id) unless channel_owner.blank? + end + + # Generate a token for accessing a private channel + def generate_token + loop do + security_token = SecureRandom.base64(50).tr('+/=', 'xyz').first(50) + break self.token = security_token unless ChannelRequest.where(:token => security_token).exists? + end + end +end + + +# == Schema Information +# +# Table name: channel_requests +# +# id :integer not null, primary key +# channel_id :integer +# user_id :integer +# token :string(255) +# ignored :boolean default(FALSE) +# created_at :datetime +# updated_at :datetime +# + diff --git a/app/models/comment.rb b/app/models/comment.rb new file mode 100755 index 0000000..d6057c5 --- /dev/null +++ b/app/models/comment.rb @@ -0,0 +1,66 @@ +class Comment < ActiveRecord::Base + # Defines which attributes can be mass-assigned via a form POST (be careful here) + attr_accessible :content + + # validates these attribute conditions are met + validates :video_id, :presence => { :message => "^There was no video ID passed in so we were unable to save your comment" } + validates :user_id, :presence => { :message => "^There was no user ID passed in so we were unable to save your comment" } + validates :content, :presence => { :message => "^You cannot leave a blank comment" }, + :length => { :maximum => 2500, + :message => "^Your comment must be less than 2500 characters in length" } + + # Active Record relationships + belongs_to :video + + # Notifies everyone who has commented on the video as well as + # the video owner that someone has commented on that video + def notify_all_users_in_the_conversation(current_user, video, video_owner) + # send e-mail to the video owner unless the person commented on their own video + # or their notification settings say not to + unless (video.user_id == self.user_id) + # for whatever reason, delayed_job won't send the email unless you do it as a delayed_job + UserMailer.delay(:priority => 40).new_comment(self, video_owner, false) if video_owner.send_email_for_new_comments + # Add event to activity feed + UserEvent.delay(:priority => 40).create(:event_type => UserEvent.event_type_value(:comment), + :event_object_id => self.id, + :user_id => video_owner.id, + :event_creator_id => current_user.id) + end + # send e-mail to other people (except the video owner, or the person that just commented) + # that have commented on the video unless their notification settings say not to + users_we_will_not_notify = [] + users_we_will_not_notify << video_owner.id << self.user_id + + commenters = Comment.where('video_id = ? AND user_id NOT IN (?)', self.video_id, users_we_will_not_notify).group_by(&:user_id) + unless commenters.blank? + commenters.each do |comment_owner_id, comment| + the_commenter = User.find_by_id(comment_owner_id) + # for whatever reason, delayed_job won't send the email unless you do it as a delayed_job + UserMailer.delay(:priority => 40).new_comment(self, the_commenter, true) if the_commenter.send_email_for_replies_to_a_prior_comment + # Add event to activity feed + UserEvent.delay(:priority => 40).create(:event_type => UserEvent.event_type_value(:comment_response), + :event_object_id => self.id, + :user_id => the_commenter.id, + :event_creator_id => current_user.id) + end + end + end + handle_asynchronously :notify_all_users_in_the_conversation, :priority => 40 + +end + + + + +# == Schema Information +# +# Table name: comments +# +# id :integer not null, primary key +# video_id :integer +# created_at :datetime +# updated_at :datetime +# content :text +# user_id :integer +# + diff --git a/app/models/favorite.rb b/app/models/favorite.rb new file mode 100755 index 0000000..afb03f6 --- /dev/null +++ b/app/models/favorite.rb @@ -0,0 +1,34 @@ +class Favorite < ActiveRecord::Base + # Defines which attributes can be mass-assigned via a form POST (be careful here) + attr_accessible :video_id + + # validates these attribute conditions are met + validates :user_id, + :presence => { :message => "^There was no user ID passed in so we were unable to save this to your favorites" }, + :uniqueness => { :scope => :video_id, + :message => "^You have already favorited that video"} + + validates :video_id, + :presence => { :message => "^There was no video ID passed in so we were unable to save this to your favorites" } + + validates :video_owner_id, + :presence => { :message => "^There was no video owner ID passed in so we were unable to save this to your favorites." } + + # Active Record Relationships + belongs_to :user + belongs_to :video + +end + + +# == Schema Information +# +# Table name: favorites +# +# id :integer not null, primary key +# user_id :integer +# video_id :integer +# created_at :datetime +# updated_at :datetime +# video_owner_id :integer +# diff --git a/app/models/flag.rb b/app/models/flag.rb new file mode 100755 index 0000000..0101263 --- /dev/null +++ b/app/models/flag.rb @@ -0,0 +1,19 @@ +class Flag < ActiveRecord::Base + # Defines which attributes can be mass-assigned via a form POST (be careful here) + attr_accessible :reason + + validates :reason, :presence => true +end + + + +# == Schema Information +# +# Table name: flags +# +# id :integer not null, primary key +# reason :string(255) +# created_at :datetime +# updated_at :datetime +# + diff --git a/app/models/icon.rb b/app/models/icon.rb new file mode 100755 index 0000000..1bbd3cc --- /dev/null +++ b/app/models/icon.rb @@ -0,0 +1,35 @@ +class Icon < ActiveRecord::Base + # validates these attribute conditions are met + validates :icon_type, :presence => true + validates :name, :presence => true + validates :active, :inclusion => { :in => [true, false] } + + scope :active, where(:active => true) + scope :order_by_name, order(:name) + scope :badges, where(:icon_type => "badge") + + has_many :badges, :dependent => :destroy, + :foreign_key => 'badge_type' + + # Returns the "type" of icon based on primary key + def badge_type + self.id + end +end + + + + +# == Schema Information +# +# Table name: icons +# +# id :integer not null, primary key +# name :string(255) +# css_class :string(255) +# active :boolean +# created_at :datetime +# updated_at :datetime +# icon_type :string(255) +# + diff --git a/app/models/invitation_link.rb b/app/models/invitation_link.rb new file mode 100755 index 0000000..1248bbc --- /dev/null +++ b/app/models/invitation_link.rb @@ -0,0 +1,169 @@ +class InvitationLink < ActiveRecord::Base + # Defines which attributes can be mass-assigned via a form POST (be careful here) + attr_accessible :email_asking_for_invite + + # Lifecycle actions + after_create :set_invitation_defaults + + # Constants + # sets whether we should invite people immediately or not (might help signups) + WE_SHOULD_INVITE_THEM_IMMEDIATELY = true + + # Model relationships + belongs_to :user + + # Validations for beta signups + before_validation :generate_token, :on => :create + validate :recipient_has_not_already_been_invited, :on => :create, :if => :email_asking_for_invite + validates :token, :presence => true + + class << self + # Increments click counts and returns InvitationLink object if there is one + def handle_invite_token(token) + invitation_token ||= token.strip rescue nil + if invitation_token + invitation = InvitationLink.where(:token => invitation_token).first + invitation.increment_click_count! if invitation + end + + return invitation + end + + # Handles inviting new users + def invite_new_users!(recipient_emails, invite_owner, personal_message) + invite_errors = [] + blank_email_count = 0 + + # strip out anything between quotes + recipient_emails = self.strip_quotes(recipient_emails) + + # split the emails by looking for a comma + recipient_emails = recipient_emails.split(',') + + # Process each split entity + recipient_emails.each do |recipient_email| + # Strip off leading/trailing whitespace and make everything lower case + recipient_email = recipient_email.strip.downcase + + # Checks for and ignores a blank email address between commas + if !recipient_email.blank? + recipient_email = self.extract_email_address(recipient_email) + if self.this_is_a_valid_email?(recipient_email) + if User.where(:email => recipient_email).exists? + invite_errors << "#{recipient_email} is already a member of Brevidy" + else + # check if we are doing a beta signup or if a current user is inviting new people + if invite_owner.blank? + # a new person is signing up for beta + @invitation_link = InvitationLink.new(:email_asking_for_invite => recipient_email) + + if !@invitation_link.save + invite_errors << @invitation_link.errors.full_messages + end + else + # the current user is inviting someone new with their link + @invitation_link = invite_owner.invitation_link + UserMailer.delay(:priority => 40).invitation(@invitation_link, recipient_email, personal_message) + end + end + else + invite_errors << "#{recipient_email} is an invalid email address" + end + else + # Keep track of all blank email addresses + blank_email_count += 1 + end + end + + # This checks for situation where only blank email addresses were detected. + if blank_email_count == recipient_emails.size + invite_errors << "You have not specified any email addresses to invite" + end + # return true unless there were errors and then return the errors + return invite_errors.flatten unless invite_errors.empty? + end + end + + # increments the clicked count for invite analytics tracking + # to show how many times the invitation link was clicked or navigated to + def increment_click_count! + self.increment! :click_count + end + + private + # Validation checks + # beta signups + def recipient_has_not_already_been_invited + # this will catch if a person wanting beta access already invited themselves + email = email_asking_for_invite.strip.downcase + errors.add(:email_asking_for_invite, "^#{email_asking_for_invite} has already been added to the invitation list") if InvitationLink.where(:email_asking_for_invite => email).exists? + end + + class << self + # Validates an email address + def this_is_a_valid_email?(email) + email.match(User::EMAIL_REGEX) + end + + # Strips anything with double quotes from the email address field + # i.e. "Rob Phillips" would return just + def strip_quotes(emails) + return emails.gsub(/".*?"/, '') + end + + # If email address is within <>, extract out address, or return original address if not between <>. + def extract_email_address(email) + # Check for <@> pattern and extract + result = email[/<.+@.+>/] + if result.nil? + # If not found, return original email address for further validation + return email + else + # If found return address minus the <> and leading/trailing spaces + return result.gsub(/[<>]/,'<' => '', '>' => '').strip + end + end + end + + # sets the default invitation limit and boolean depending + # on if it's a beta signup or not + def set_invitation_defaults + if self.email_asking_for_invite + self.invitation_limit = 1 + self.has_been_invited = false + self.save + else + self.has_been_invited = true + self.save + end + end + + # Generates a random invite token + def generate_token + loop do + new_token = Digest::SHA1.hexdigest([Time.now, rand].join).first(35) + break self.token = new_token unless InvitationLink.where(:token => new_token).exists? + end + end +end + + + + + + +# == Schema Information +# +# Table name: invitation_links +# +# id :integer not null, primary key +# user_id :integer +# email_asking_for_invite :string(255) +# invitation_limit :integer +# token :string(255) +# created_at :datetime +# updated_at :datetime +# has_been_invited :boolean default(TRUE) +# click_count :integer default(0) +# + diff --git a/app/models/profile.rb b/app/models/profile.rb new file mode 100755 index 0000000..aaada25 --- /dev/null +++ b/app/models/profile.rb @@ -0,0 +1,72 @@ +class Profile < ActiveRecord::Base + include ActionView::Helpers::TextHelper + include ActionView::Helpers::TagHelper + include ActionView::Helpers::SanitizeHelper + + # Defines which attributes can be mass-assigned via a form POST (be careful here) + attr_accessible :website, :bio, :interests, :favorite_music, :favorite_movies, :favorite_books, + :favorite_foods, :favorite_people, :things_i_could_live_without, + :one_thing_i_would_change_in_the_world, :quotes_to_live_by + + belongs_to :user + + # validates these attribute conditions are met + validates :user_id, + :presence => { :message => "No user ID was passed in so a profile couldn't be created." } + validates :bio, :length => { :maximum => 140, :message => "can only be a maximum of 140 characters long" } + validates :interests,:favorite_music,:favorite_movies,:favorite_books,:favorite_people, + :favorite_foods,:things_i_could_live_without,:one_thing_i_would_change_in_the_world, + :length => { :maximum => 1000, :message => "can only be a maximum of 1000 characters long" } + validates :quotes_to_live_by, + :length => { :maximum => 3000, :message => "can only be a maximum of 3000 characters long" } + validates :website, :format => { :with => /\A#{URI::regexp(%w(http https))}\z/, + :message => "^The website you provided is invalid. Please make sure it starts with 'http://' or 'https://'" }, + :length => { :maximum => 250, :message => "can only be a maximum of 250 characters long" }, + :allow_nil => true, + :allow_blank => true + + # create a hash of profile attributes + def profile_hash + %w{website bio interests favorite_music favorite_movies favorite_books favorite_foods + favorite_people things_i_could_live_without one_thing_i_would_change_in_the_world + quotes_to_live_by} + end + def categories_to_hash(type) + profile_hash.inject({}) do |hash, property| + if type == 'html' + hash[property] = simple_format(auto_link(h(self.send(property)), :html => { :target => "_blank" }), {}, :sanitize => false) + else + hash[property] = self.send(property) + end + hash + end + end + +end + + + + + + +# == Schema Information +# +# Table name: profiles +# +# id :integer not null, primary key +# user_id :integer +# interests :text +# favorite_music :text +# favorite_movies :text +# favorite_books :text +# favorite_people :text +# things_i_could_live_without :text +# one_thing_i_would_change_in_the_world :text +# quotes_to_live_by :text +# created_at :datetime +# updated_at :datetime +# favorite_foods :text +# bio :string(255) +# website :string(255) +# + diff --git a/app/models/restricted_username.rb b/app/models/restricted_username.rb new file mode 100755 index 0000000..2763db7 --- /dev/null +++ b/app/models/restricted_username.rb @@ -0,0 +1,14 @@ +class RestrictedUsername < ActiveRecord::Base +end + +# == Schema Information +# +# Table name: restricted_usernames +# +# id :integer not null, primary key +# username :string(255) +# inclusive :boolean default(FALSE) +# created_at :datetime +# updated_at :datetime +# + diff --git a/app/models/setting.rb b/app/models/setting.rb new file mode 100755 index 0000000..cfcd80f --- /dev/null +++ b/app/models/setting.rb @@ -0,0 +1,36 @@ +class Setting < ActiveRecord::Base + # Defines which attributes can be mass-assigned via a form POST (be careful here) + attr_accessible :hide_getting_started, :send_email_for_new_badges, + :send_email_for_new_comments, :send_email_for_replies_to_a_prior_comment, + :send_email_for_new_subscriber, :send_email_for_featured_video, + :send_email_for_private_channel_request, :send_email_for_encoding_completion + + belongs_to :user + + validates :user_id, :presence => true + validates :hide_getting_started, :send_email_for_new_badges, + :send_email_for_new_comments, :send_email_for_replies_to_a_prior_comment, + :send_email_for_new_subscriber, :send_email_for_featured_video, + :send_email_for_private_channel_request, :send_email_for_encoding_completion, + :inclusion => { :in => [true, false] } +end + + +# == Schema Information +# +# Table name: settings +# +# id :integer not null, primary key +# created_at :datetime +# updated_at :datetime +# user_id :integer +# hide_getting_started :boolean default(FALSE) +# send_email_for_new_badges :boolean default(TRUE) +# send_email_for_new_comments :boolean default(TRUE) +# send_email_for_replies_to_a_prior_comment :boolean default(TRUE) +# send_email_for_new_subscriber :boolean default(TRUE) +# send_email_for_featured_video :boolean default(TRUE) +# send_email_for_private_channel_request :boolean default(TRUE) +# send_email_for_encoding_completion :boolean default(TRUE) +# + diff --git a/app/models/social_network.rb b/app/models/social_network.rb new file mode 100755 index 0000000..5ba9f27 --- /dev/null +++ b/app/models/social_network.rb @@ -0,0 +1,43 @@ +class SocialNetwork < ActiveRecord::Base + + belongs_to :user + + validates :user_id, :uid, :provider, :presence => { :message => "^We were unable to retrieve your social graph data." } + validates :provider, :inclusion => { :in => ["facebook", "twitter"] } + validate :uid_and_provider_are_unique, :on => :create + + class << self + # decodes Base64 URL encoded strings + def base64_url_decode(str) + str += '=' * (4 - str.length.modulo(4)) + Base64.decode64(str.tr('-_','+/')) + end + end + + private + # custom validations + def uid_and_provider_are_unique + errors.add(:uid, "^There is already a Brevidy account associated with these #{provider.capitalize} credentials.") if SocialNetwork.where(:uid => uid, :provider => provider).exists? + end + +end + + + + + + +# == Schema Information +# +# Table name: social_networks +# +# id :integer not null, primary key +# uid :string(255) +# provider :string(255) +# created_at :datetime +# updated_at :datetime +# token :string(255) +# user_id :integer +# token_secret :string(255) +# + diff --git a/app/models/subscription.rb b/app/models/subscription.rb new file mode 100755 index 0000000..33ed847 --- /dev/null +++ b/app/models/subscription.rb @@ -0,0 +1,40 @@ +class Subscription < ActiveRecord::Base + attr_protected :publisher_id, :subscriber_id, :channel_id + + belongs_to :channel + # Used for associations in other models + belongs_to :channels_subscribed_to, :foreign_key => "channel_id", :class_name => "Channel" + belongs_to :subscriber_people, :foreign_key => "subscriber_id", :class_name => "User" + + # validations + validates :publisher_id, :presence => true + validates :subscriber_id, :presence => true + validates :channel_id, :presence => true + validates_uniqueness_of :channel_id, :scope => :subscriber_id + + # Lifecycle actions + after_create :destroy_channel_request + + private + # Delete any old channel requests (since they were approved) + def destroy_channel_request + channel_request = ChannelRequest.where(:user_id => self.subscriber_id, :channel_id => self.channel_id).first + channel_request.destroy unless channel_request.blank? + end +end + + + +# == Schema Information +# +# Table name: subscriptions +# +# id :integer not null, primary key +# subscriber_id :integer +# publisher_id :integer +# created_at :datetime +# updated_at :datetime +# channel_id :integer +# collaborator :boolean default(FALSE) +# + diff --git a/app/models/tag.rb b/app/models/tag.rb new file mode 100755 index 0000000..151fd5d --- /dev/null +++ b/app/models/tag.rb @@ -0,0 +1,38 @@ +class Tag < ActiveRecord::Base + # Defines which attributes can be mass-assigned via a form POST (be careful here) + attr_accessible :content + + # strip and downcase the content before saving + before_validation :prepare_tag_content, :on => :create + + # validates these attribute conditions are met + validates :content, :presence => { :message => "^Your tag can't be blank" }, + :length => { :maximum => 250, + :message => "^Tags can only be a maximum of 250 characters long" } + + # Active Record relationships + has_many :videos, :through => :taggings + has_many :taggings, :dependent => :destroy + + private + def prepare_tag_content + self.content = self.content.downcase.strip unless self.content.blank? + end +end + + + + + + + +# == Schema Information +# +# Table name: tags +# +# id :integer not null, primary key +# created_at :datetime +# updated_at :datetime +# content :string(255) +# + diff --git a/app/models/tagging.rb b/app/models/tagging.rb new file mode 100755 index 0000000..488a723 --- /dev/null +++ b/app/models/tagging.rb @@ -0,0 +1,48 @@ +class Tagging < ActiveRecord::Base + # Defines which attributes can be mass-assigned via a form POST (be careful here) + attr_accessible :tag_id + + before_create :populate_video_owner_id_from_video + + # update the Video delta flag for Sphinx index after save/destroy + after_save :set_video_delta_flag + before_destroy :set_video_delta_flag + + + # validations + validates :tag_id, :presence => { :message => "^The tagging does not have a tag ID." } + validates :video_id, :presence => { :message => "^The tagging does not have a video ID." } + + belongs_to :video + belongs_to :tag + + private + def populate_video_owner_id_from_video + self.video_owner_id = Video.find_by_id(self.video_id).user_id + end + + def set_video_delta_flag + # check if blank (mostly to fix failing tests) + unless video.blank? + video.delta = true + video.save + end + end +end + + + + + +# == Schema Information +# +# Table name: taggings +# +# id :integer not null, primary key +# video_id :integer +# tag_id :integer +# created_at :datetime +# updated_at :datetime +# video_owner_id :integer +# + diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100755 index 0000000..0576640 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,569 @@ +require 'digest' + +class User < ActiveRecord::Base + # custom module used for cleaning S3 after new profile images + include Brevidy::Fog + # needed for truncate method + include ActionView::Helpers::TextHelper + + # Defines which attributes can be mass-assigned via a form POST (be careful here) + attr_accessible :name, :email, :password, :gender, :birthday, :location, + :banner_image, :image, :image_status, :username, :background_image_id, :banner_image_id + + # user image uploader + mount_uploader :image, ImageUploader + mount_uploader :banner_image, BannerUploader + + # Lifecycle actions + after_create :create_username_changed_timestamp, :create_default_channels, :create_profile, + :create_default_settings, :create_invitation_link + before_save :encrypt_password + + # Constants + # sets the standard regular expressions for verification + NAME_REGEX = /\A[a-z\x20.']+\z/i + EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i + USERNAME_REGEX = /\A_?[a-z]_?(?:[a-z0-9]_?)*\z/i + USERNAME_LENGTH = 20 + + # sets whether users can invite others or not + USERS_CAN_INVITE_MORE_PEOPLE = true + + # Validations + before_validation :prepare_params_for_validation + validate :email_address_has_not_been_banned + + validates :name, :presence => true, + :format => { :with => NAME_REGEX, + :message => "^Name contains invalid characters." }, + :length => { :maximum => 30 } + validates :email, :presence => true, + :length => { :maximum => 250 }, + :format => { :with => EMAIL_REGEX, + :message => "^The e-mail address you provided is invalid"}, + :uniqueness => { :case_sensitive => false, + :message => "^There is already an account associated with this email" } + validates :location, :length => { :maximum => 50 } + validates :password, :presence => true, + :length => { :minimum => 6, :maximum => 250 } + validates :gender, :allow_nil => true, + :allow_blank => true, + :inclusion => { :in => ["Male", "Female"] } + validates :birthday, :presence => true + validates_date :birthday, :before => lambda { 13.years.ago }, + :before_message => "^You must be at least 13 years old to use Brevidy" + validates :background_image_id, :inclusion => 0..1 + validates :username, :presence => true, + :format => { :with => USERNAME_REGEX, + :message => "^Your username can only contain letters A-Z, numbers 0-9, and underscores." }, + :length => { :maximum => USERNAME_LENGTH }, + :uniqueness => { :message => "^That username is not available", + :case_sensitive => false } + validate :username_changed_more_than_one_month_ago, :on => :update + validate :username_is_acceptable + + has_one :profile, :dependent => :destroy + delegate :website, :bio, :interests, :favorite_music, :favorite_movies, :favorite_foods, :favorite_books, + :favorite_people, :things_i_could_live_without, :one_thing_i_would_change_in_the_world, + :quotes_to_live_by, :to => :profile + + has_many :channels, :dependent => :destroy, :order => 'updated_at DESC' + has_many :subscriptions, :foreign_key => "subscriber_id", + :dependent => :destroy + has_many :subscribers, :foreign_key => "publisher_id", + :class_name => "Subscription", + :dependent => :destroy + has_many :channel_subscriptions, :through => :subscriptions, :source => :channels_subscribed_to + has_many :subscribers_as_people, :through => :subscribers, :source => :subscriber_people, :select => "DISTINCT users.*" + # User requests to access private channels + has_many :channel_requests, :dependent => :destroy + + has_many :video_graphs + has_many :videos, :dependent => :destroy, :order => 'created_at DESC' + has_many :comments, :through => :videos, :dependent => :destroy, :order => 'created_at DESC' + has_many :badges, :through => :videos, :dependent => :destroy, :order => 'created_at DESC' + has_many :tags, :through => :videos + + # These next associations are to ensure the associated objects are destroyed + # when the parent object is destroyed + has_many :comments, :dependent => :destroy + has_many :blockings_by_them, :dependent => :destroy, + :foreign_key => "requesting_user", + :class_name => "Blocking" + has_many :blockings_by_others, :dependent => :destroy, + :foreign_key => 'blocked_user', + :class_name => "Blocking" + has_many :events_happening_to_them, :dependent => :destroy, + :class_name => "UserEvent" + has_many :events_created_by_them, :dependent => :destroy, + :foreign_key => 'event_creator_id', + :class_name => "UserEvent" + + has_many :people_they_are_blocking, :through => :blockings_by_them, :source => :blocked_people + + has_one :setting, :dependent => :destroy + delegate :hide_getting_started, :send_email_for_new_badges, + :send_email_for_new_comments, :send_email_for_replies_to_a_prior_comment, + :send_email_for_new_subscriber, :send_email_for_featured_video, + :send_email_for_private_channel_request, :send_email_for_encoding_completion, + :to => :setting + + has_one :invitation_link, :dependent => :destroy + delegate :invitation_limit, :to => :invitation_link + + has_many :social_networks, :dependent => :destroy + + # Use the :username instead of :id + def to_param + username + end + + ####################### + # Notification Helper # + ####################### + + # Returns all notifications for a user (except video plays) + def notifications_to_show_user + notifications ||= UserEvent.where('user_id = ? AND error_during_render = ? AND event_type != ?', self.id, false, UserEvent.event_type_value(:video_play)) + end + # Returns how many "unseen" latest activity events the user has + def notifications_count + notifications_count ||= self.notifications_to_show_user.where(:seen_by_user => false).count + end + + ################# + # Video Helpers # + ################# + + # Returns all videos that have been featured by a given user + def featured_videos + Video.joins(:video_graph).where(:video_graphs => { :status => VideoGraph.get_status_number(:ready) }).where(:channel_id => self.channels.where(:featured => true).collect(&:id)).order('featured_at DESC') + end + # Returns videos from only the public channels for a given user + def public_videos + Video.joins(:video_graph).where(:video_graphs => { :status => VideoGraph.get_status_number(:ready) }).where(:channel_id => self.channels.where(:private => false).collect(&:id)) + end + # Returns videos from ALL public AND private channels for a given user + def all_videos + Video.joins(:video_graph).where(:video_graphs => { :status => Video.statuses_to_show_to_current_user }).where(:channel_id => self.channels.collect(&:id)) + end + # Returns videos from the public AND private channels the user is subscribed to + def all_videos_for_subscriptions + Video.joins(:video_graph).where(:video_graphs => { :status => VideoGraph.get_status_number(:ready) }).where(:channel_id => self.channel_subscriptions.collect(&:id)) + end + + ################### + # Channel Helpers # + ################### + + # Returns the featured channel for a given user + def featured_channel + self.channels.where(:featured => true).first + end + + ################## + ## User Helpers ## + ################## + + # Returns a random set of users for the Find People page + def self.show_random_people + where('image IS NOT NULL').limit(100).order("RANDOM()") + end + + ###################### + ## Sphinx Indexing ## + ## (only on heroku) ## + ###################### + + if Rails.env.production? || Rails.env.staging? + define_index do + indexes :name, :sortable => true + indexes email + + has :id, created_at, updated_at + + set_property :delta => FlyingSphinx::DelayedDelta + end + end + + + #################### + ## Social Sign Up ## + #################### + + # Associates a created user with the social attributes after sign up + def associate_with_social_account(social_params, social_image_cookie, social_bio_cookie) + if social_params["provider"].blank? || social_params["uid"].blank? + Airbrake.notify(:error_class => "Logged Error", :error_message => "SOCIAL CREDENTIALS: The social credentials for #{self.id} did not get passed in from the sign up form. This is what we received: Provider = #{social_params["provider"]} ; UID = #{social_params["uid"]}") if Rails.env.production? + else + # create a new social network record for storing their auth data in + new_network ||= self.social_networks.new(:provider => social_params["provider"].strip.downcase, :uid => social_params["uid"].strip.downcase, :token => social_params["oauth_token"], :token_secret => social_params["oauth_token_secret"]) + if !new_network.save + Airbrake.notify(:error_class => "Logged Error", :error_message => "SOCIAL CREDENTIALS: Error creating social credentials for #{self.id} with these params: Provider = #{social_params["provider"]} ; UID = #{social_params["uid"]}") if Rails.env.production? + end + end + + # upload their image + begin + self.set_new_user_image(nil, social_image_cookie, false, true) + rescue + Airbrake.notify(:error_class => "Logged Error", :error_message => "PROFILE IMAGE: Error SAVING image from a social signup for #{self.email}. The image was #{social_image_cookie}") if Rails.env.production? + end + + # set their bio + self.profile.update_attribute('bio', truncate(social_bio_cookie, :length => 140, :omission => '...')) + end + + class << self + # Returns a new user instance with attributes already pre-filled from social params + def create_via_fb_or_twitter(auth_hash) + user = User.new + + begin + user.username = auth_hash["user_info"]["nickname"].first(30) rescue nil + user.name = auth_hash["user_info"]["name"].first(25) rescue nil + + case auth_hash["provider"] + when "facebook" + user.email = auth_hash["user_info"]["email"] rescue nil + user.birthday = Date.strptime(auth_hash["extra"]["user_hash"]["birthday"], '%m/%d/%Y') rescue nil + user.location = auth_hash["extra"]["user_hash"]["location"]["name"].first(20) rescue nil + user.gender = auth_hash["extra"]["user_hash"]["gender"].capitalize rescue nil + when "twitter" + user.location = auth_hash["user_info"]["location"].first(20) rescue nil + end + rescue Exception => e + Airbrake.notify(:error_class => "Logged Error", :error_message => "SOCIAL SIGNUP: Just rescued an error during a social sign up. Error was: #{e.inspect}") if Rails.env.production? + return user + end + + return user + end + end + + + ################# + ## User Images ## + ################# + + # Retrieves the image location for a user banner image chosen from the Brevidy gallery + def get_banner_image_url(banner_image_id) + banner_image = BannerImage.where(:id => banner_image_id, :active => true).first + if banner_image + return "#{Brevidy::Application::S3_BASE_URL}/#{banner_image.path}/#{banner_image.filename}" + else + # The banner image is either inactive or could not be found so return the default one + return "#{Brevidy::Application::S3_BASE_URL}/#{BannerImage.find_by_filename('banner-1.jpg').path}/#{BannerImage.find_by_filename('banner-1.jpg').filename}" + end + end + + # Task for processing and setting the new user images (profile or banner) + def set_new_user_image(old_image, new_temp_image, banner_image, image_from_social_signup = false) + # word of caution: if you try to remove the old image prior to setting + # a new image, it will mysteriously not update the user with the new image + # so instead just delete the old image as a delayed job + + # + # IMPORTANT: do not put a leading / on the path + # since we use the path to also clean up after ourselves + # + s3_path = "#{Brevidy::Application::S3_IMAGES_RELATIVE_PATH}/#{self.id}/" + + new_image_s3_url = image_from_social_signup ? new_temp_image : "#{Brevidy::Application::S3_BASE_URL}/#{s3_path}#{new_temp_image}" + + begin + if banner_image + self.remote_banner_image_url = new_image_s3_url + else + self.remote_image_url = new_image_s3_url + end + + # tell the browser (which is polling) that we are through + self.image_status = 'success' + + if self.save + # reset banner_image_id if necessary so we know to use the new banner image they just uploaded + self.update_attribute(:banner_image_id, 0) if banner_image + + # delete the old images as a delayed job + clean_up_after_new_user_image(s3_path, old_image, new_temp_image) + else + self.update_attribute(:image_status, 'error') + Airbrake.notify(:error_class => "Logged Error", :error_message => "USER IMAGE: Error SAVING (banner? #{banner_image}) image for #{self.email}") if Rails.env.production? + end + + rescue Exception => e + # tell the browser (which is polling) that we had an error + self.update_attribute(:image_status, 'error') + + Airbrake.notify(:error_class => "Logged Error", :error_message => "USER IMAGE: Error SAVING (banner? #{banner_image}) image for #{self.email}. The exception thrown was #{e}") if Rails.env.production? + end + end + + # Removes old user images from S3 via a delayed job + def clean_up_after_new_user_image(s3_path, old_image, new_temp_image) + f = Brevidy::Fog::S3::File.new + f.delete(s3_path, new_temp_image) unless new_temp_image.blank? + unless old_image.blank? + f.delete(s3_path, old_image) + f.delete(s3_path, 'small_profile_' + old_image) + f.delete(s3_path, 'medium_profile_' + old_image) + f.delete(s3_path, 'large_profile_' + old_image) + end + end + handle_asynchronously :clean_up_after_new_user_image, :priority => 50 + + + ################### + ## Subscriptions ## + ################### + + # Determines if a user is subscribed to a specific channel + def is_subscribed_to?(channel) + Subscription.where(:subscriber_id => self.id, :channel_id => channel.id).exists? + end + + + ############## + ## Blocking ## + ############## + + # Blocks a given user + def block!(user) + User.transaction do + subscriptions = Subscription.where(:subscriber_id => user.id, :publisher_id => self.id) + subscriptions.destroy_all + + new_blocking = Blocking.new + new_blocking.requesting_user = self.id + new_blocking.blocked_user = user.id + new_blocking.save! + end + end + + + ################### + ## Count Helpers ## + ################### + + # Returns how many videos the user has with a :ready state + def videos_count + videos.joins(:video_graph).where(:video_graphs => { :status => VideoGraph.get_status_number(:ready) }).count + end + + # Returns how many unique people the user is subscribed to + def subscriptions_count + subscriptions.count + end + + # Returns how many unique subscribers the person has + def subscribers_count + subscribers_as_people.uniq.count + end + + # Returns how many badges the user has + def badges_count + badges.count + end + + # Returns how many channels the user has + def channels_count + channels.count + end + + # returns how many badges and of what type a user has + def badges_count_for_type(badge_type) + self.badges.where(:badge_type => badge_type).count + end + + + #################### + ## Authentication ## + #################### + + class << self + def authenticate(email, submitted_password) + user = find_by_email(email) + (user && user.has_password?(submitted_password)) ? user : nil + end + + def authenticate_with_salt(id, cookie_salt) + user = find_by_id(id) + (user && user.salt == cookie_salt) ? user : nil + end + + # Setter for password encryption flag + def should_encrypt_password=(flag) + @encrypt_password_flag = flag + end + + # Getter for password encryption flag + def should_encrypt_password + @encrypt_password_flag || false + end + end + + def has_password?(submitted_password) + password == encrypt(submitted_password) + end + + + ################# + ## Validations ## + ################# + + # Returns whether or not the given username is acceptable + class << self + def verify_username_is_acceptable(username) + return false if Rails.application.routes.routes.map(&:path).join("\n").scan(/\s\/(\w+)/).flatten.compact.uniq.include?(username) + return false if RestrictedUsername.where(:username => username, :inclusive => false).exists? + inclusive_restrictions ||= RestrictedUsername.where(:inclusive => true) + (return false if username =~ /#{inclusive_restrictions.collect(&:username).join('|')}/i) unless inclusive_restrictions.blank? + + # if we got here, the username is okay + return true + end + end + + # Cleans up the user params prior to validation + def prepare_params_for_validation + self.username = username.downcase.strip unless username.blank? + self.name = name.strip unless name.blank? + self.email = email.downcase.strip unless email.blank? + self.password = password.strip unless password.blank? + end + + private + ######################## + ## Custom Validations ## + ######################## + + # Verifies the email address given has not been banned + def email_address_has_not_been_banned + errors.add(:email, "^That e-mail address has been banned") if BannedUser.where(:email => email).exists? + end + + # Ensures that the user can only change their username once a month + def username_changed_more_than_one_month_ago + errors.add(:username, "^You cannot change your username more than once a month") if self.username_changed? && self.username_changed_at > 1.month.ago + end + + # Ensures that the username given is allowable + def username_is_acceptable + errors.add(:username, "^That username is unacceptable") unless User.verify_username_is_acceptable(self.username) + end + + ################ + ## User Setup ## + ################ + + # after_create callback to create the default username_updated_at datetime + def create_username_changed_timestamp + self.update_attribute(:username_changed_at, 2.months.ago) + end + + # after_create callback to create the default channels for each user + def create_default_channels + private_chan = Channel.new + private_chan.user_id = self.id + private_chan.title = "Private Videos" + private_chan.private = true + private_chan.save + + public_chan = Channel.new + public_chan.user_id = self.id + public_chan.title = "Public Videos" + public_chan.save + + featured_chan = Channel.new + featured_chan.user_id = self.id + featured_chan.title = "Featured Videos" + featured_chan.featured = true + featured_chan.save + end + + # after_create callback to create a new profile associated with the user + def create_profile + new_profile = Profile.new + new_profile.user_id = self.id + new_profile.save! + end + + # after_create callback to create default settings for the user + def create_default_settings + setting = Setting.new + setting.user_id = self.id + setting.save! + end + + # after_create callback to create an invitation link for the user to give out + def create_invitation_link + new_invitation_link = InvitationLink.new + new_invitation_link.user_id = self.id + new_invitation_link.invitation_limit = 100 + new_invitation_link.save! + end + + # after_create callback to subscribe the user to the default brevidy channels + def subscribe_to_default_channels + #User.find_by_username("brevidy").channels.where(:recommended => true).each { |c| c.subscribe!(self) } unless Rails.env.test? + end + + ##################### + ## Password / Salt ## + ##################### + + def encrypt_password + self.salt = make_salt if new_record? + self.password = encrypt(password) if new_record? || User.should_encrypt_password + end + + def encrypt(string) + secure_hash("#{salt}--#{string}") + end + + def make_salt + secure_hash("#{Time.now.utc}--#{password}") + end + + def secure_hash(string) + Digest::SHA2.hexdigest(string) + end + +end + + + + + + + + + +# == Schema Information +# +# Table name: users +# +# id :integer not null, primary key +# email :string(255) +# password :string(255) +# salt :string(255) +# name :string(255) +# image :string(255) +# birthday :date +# gender :string(255) +# created_at :datetime +# updated_at :datetime +# location :string(255) +# reset_token :string(255) +# pw_reset_timestamp :datetime +# image_status :string(255) +# is_deactivated :boolean default(FALSE) +# delta :boolean default(TRUE), not null +# username :string(255) +# banner_image :string(255) +# banner_image_id :integer default(1) +# username_changed_at :datetime +# background_image_id :integer default(0) +# + diff --git a/app/models/user_event.rb b/app/models/user_event.rb new file mode 100755 index 0000000..67ba80c --- /dev/null +++ b/app/models/user_event.rb @@ -0,0 +1,98 @@ +class UserEvent < ActiveRecord::Base + # Validations + validates :event_type, :event_object_id, :user_id, :event_creator_id, :presence => true + + # Returns model and event types for a given event + def get_event_class_type + case self.event_type + when UserEvent.event_type_value(:badge) + model_name = Badge + event_type = 'badge' + when UserEvent.event_type_value(:comment) + model_name = Comment + event_type = 'comment' + when UserEvent.event_type_value(:comment_response) + model_name = Comment + event_type = 'comment_response' + when UserEvent.event_type_value(:subscription) + model_name = Subscription + event_type = 'subscription' + when UserEvent.event_type_value(:channel_request) + model_name = ChannelRequest + event_type = 'channel_request' + else + false + end + + return [model_name, event_type] + end + + # Instantiates objects based on model type and the object ids + def get_event_objects(model_name) + begin + events_not_associated_with_videos = [UserEvent.event_type_value(:subscription), UserEvent.event_type_value(:channel_request)] + associated_with_video = !events_not_associated_with_videos.include?(self.event_type) + model_object = model_name.find(self.event_object_id) + if associated_with_video + video_id = model_object.video_id + video = Video.find(video_id) + else + video = nil + end + + # returns User who created event, Object for event, and Video for event if necessary + return [User.find_by_id(self.event_creator_id), model_object, video] + rescue + # If there was an error, set the error flag for the UserEvent object + # and return nil so we don't display that event to the user + self.update_attributes(:error_during_render => true) + return nil + end + end + + class << self + # Returns an integer for a event type + # Latest # is 8 + # Prior deleted #'s are: 4, 6 + def event_type_value(event) + return case event + when :badge + 1 + when :comment + 2 + when :comment_response + 3 + when :subscription + 5 + when :video_play + 7 + when :channel_request + 8 + else + false + end + end + end # end self class + +end + + + + + + +# == Schema Information +# +# Table name: user_events +# +# id :integer not null, primary key +# event_type :integer +# event_object_id :integer +# user_id :integer +# created_at :datetime +# updated_at :datetime +# event_creator_id :integer +# error_during_render :boolean default(FALSE) +# seen_by_user :boolean default(FALSE) +# + diff --git a/app/models/video.rb b/app/models/video.rb new file mode 100755 index 0000000..1ecc414 --- /dev/null +++ b/app/models/video.rb @@ -0,0 +1,640 @@ +require 'digest' + +class Video < ActiveRecord::Base + # custom module used for cleaning S3 after videos are deleted + include Brevidy::Fog + # used for getting information from YouTube and Vimeo links + include RemoteVideoLinks + # needed for truncate method + include ActionView::Helpers::TextHelper + + # Lifecycle actions + after_create :generate_public_token!, :touch_channel! + before_validation :set_featured_at, :on => :create + before_validation :trim_newlines_from_fields + before_destroy :clean_up_if_necessary + + # whitelist to define which attributes can be mass-assigned + attr_accessible :title, :description, :selected_thumbnail, :send_to_facebook, :send_to_twitter + + # validates these attribute conditions are met + validates :user_id, :featured_at, :video_graph_id, :channel_id, :presence => true + validates :selected_thumbnail, :inclusion => 0..3 + validates :title, :presence => true, + :length => { :maximum => 75 } + validates :description, :length => { :maximum => 1000 } + + # Active Record relationships + belongs_to :user + belongs_to :video_graph + belongs_to :channel + + # UNCOMMENT AFTER MIGRATION + delegate :thumbnail_path, :path, :base_filename, :encoding_type, :thumbnail_type, + :status, :zencoder_job_id, :remote_host, :remote_video_id, :remote_thumbnail, :to => :video_graph + + has_many :comments, :dependent => :destroy, :order => 'created_at ASC' + has_many :badges, :dependent => :destroy, :order => 'created_at DESC' + + has_many :taggings, :dependent => :destroy + has_many :tags, :through => :taggings + + + ############### + ## Constants ## + ############### + + # Video Player Constants + PLAYER_WIDTH = 786 + PLAYER_HEIGHT = 480 + PUBLIC_PLAYER_WIDTH = 786 + PUBLIC_PLAYER_HEIGHT = 480 + + + ###################### + ## Sphinx Indexing ## + ## (only on heroku) ## + ###################### + + if Rails.env.production? || Rails.env.staging? + define_index do + indexes :title + indexes :description + indexes user(:name), :as => :posted_by + indexes tags(:content), :as => :tags + indexes video_graph(:status), :as => :status + indexes channel(:private), :as => :channel_is_private + + has user_id, created_at + + set_property :delta => FlyingSphinx::DelayedDelta + end + end + + ################ + ## SEO Params ## + ################ + + # Creates an SEO friendly slug in the URL + # i.e. http://brevidy.com/rob/videos/3-some-title-goes-here + def to_param + "#{id}-#{title.parameterize}" + end + + #################### + ## Update Helpers ## + #################### + + # Overrides update attributes to update the channel information for a given video + def update_attributes(attributes) + video_owner = User.find_by_id(self.user_id) + channel_id = attributes[:channel_id] + + if channel_id == "add_to_new_channel" + new_channel ||= video_owner.channels.new(:title => attributes[:channel_name]) + new_channel.private = !attributes[:channel_is_private].blank? + if new_channel.save + channel_id = new_channel.id + else + self.errors.add(:channel_id, "^#{new_channel.errors.full_messages.to_sentence}") + return false + end + end + + if video_owner.channels.where(:id => channel_id.to_i).exists? + self.channel_id = channel_id + else + self.errors.add(:channel_id, "^The channel you selected was invalid. Please select one of your channels.") + return false + end + + # Remove the unnecessary params + attributes.delete("channel_id") + attributes.delete("channel_name") + attributes.delete("channel_is_private") + + super(attributes) + end + + + ###################### + ## Creation Helpers ## + ###################### + + class << self + + # Adds an existing video to a channel (i.e. reshares it) + def add_video_to_channel!(current_user, video_id, channel_id, channel_name, is_private) + new_video = current_user.videos.new + + # Do a quick security check to see if the current user can access + # the video they are attempting to share + video_to_share = Video.find_by_id(video_id) + if video_to_share + owning_channel = video_to_share.channel + unless owning_channel.is_accessible_by(current_user) + new_video.errors.add(:id, "^You do not have permission to share this video.") + Airbrake.notify(:error_class => "Logged Error", :error_message => "ADD TO CHANNEL: User ##{current_user.id} tried sharing a video (#{video_to_share.id}) that they did not have access to.") if Rails.env.production? + return new_video + end + else + new_video.errors.add(:id, "^This video either could not be found or it might have been removed by the owner.") + return new_video + end + + # Check if video is in a private channel and the user doesn't own it + # If so, don't let them share it + if video_to_share.channel.private? && (video_to_share.channel.user_id != current_user.id) + new_video.errors.add(:id, "^This video is in a private channel so it cannot be shared.") + return new_video + end + + # Create a new channel if necessary + if channel_id == "add_to_new_channel" + new_channel ||= current_user.channels.new(:title => channel_name) + new_channel.private = !is_private.blank? + if new_channel.save + channel_id = new_channel.id + else + new_video.errors.add(:channel_id, "^#{new_channel.errors.full_messages.to_sentence}") + return new_video + end + end + + # Find the channel and add the new video to it + if current_user.channels.where(:id => channel_id.to_i).exists? + # Set the video graph ID and save it + new_video.title = video_to_share.title + new_video.description = video_to_share.description + new_video.video_graph_id = video_to_share.video_graph_id + new_video.channel_id = channel_id + new_video.save + else + # User tried to add the video to a channel that wasn't theirs or had issues + new_video.errors.add(:channel_id, "^The channel you selected was invalid. Please select one of your channels.") + Airbrake.notify(:error_class => "Logged Error", :error_message => "SHARE A LINK: User ##{current_user.id} tried sharing a video into an invalid channel (##{channel_id})") if Rails.env.production? + end + + return new_video + end + + # Creates a new video from a remote (YouTube/Vimeo) link and shares it to a channel + def create_shared_video!(current_user, remote_link, channel_id, channel_name, is_private) + new_video = current_user.videos.new + + # Poll the video information + remote_link_params ||= RemoteVideoLinks.get_video_information_from_link(remote_link) + + # Get the necessary params from the remote link + remote_video_id = remote_link_params[0] + remote_host = remote_link_params[1] + remote_video_title = remote_link_params[2] + remote_video_description = remote_link_params[3] + remote_video_thumbnail = remote_link_params[4] + + # Check if any of the remote params came in bad + if remote_video_id.blank? || remote_host.blank? || remote_video_thumbnail.blank? + new_video.errors.add(:id, "^Error getting the video information. Please verify the link is correct.") + else + # Create a new channel if necessary + if channel_id == "add_to_new_channel" + new_channel ||= current_user.channels.new(:title => channel_name) + new_channel.private = !is_private.blank? + if new_channel.save + channel_id = new_channel.id + else + new_video.errors.add(:channel_id, "^#{new_channel.errors.full_messages.to_sentence}") + return new_video + end + end + + # Find the channel and add the new video to it + if current_user.channels.where(:id => channel_id.to_i).exists? + # Find or create the video graph object + video_graph ||= VideoGraph.where(:remote_host => remote_host, :remote_video_id => remote_video_id, :deleted => false).first + if video_graph.blank? + video_graph = current_user.video_graphs.create + video_graph.upload_remote_thumbnail(remote_video_thumbnail) + video_graph.set_status(VideoGraph::READY) + video_graph.remote_host = remote_host.downcase.strip + video_graph.remote_video_id = remote_video_id.strip + video_graph.save + end + # Set the video graph ID and save it + remote_video_title.blank? ? new_video.title = "Untitled" : new_video.title = remote_video_title + new_video.description = remote_video_description + new_video.video_graph_id = video_graph.id + new_video.channel_id = channel_id + new_video.save + else + # User tried to add the video to a channel that wasn't theirs or had issues + new_video.errors.add(:channel_id, "^The channel you selected was invalid. Please select one of your channels.") + Airbrake.notify(:error_class => "Logged Error", :error_message => "SHARE A LINK: User ##{current_user.id} tried sharing a video into an invalid channel (##{channel_id})") if Rails.env.production? + end + end + + return new_video + end + + end + + + ###################### + ## Standard Helpers ## + ###################### + + class << self + # Returns an array of states viewable by the video owner + def statuses_to_show_to_current_user + return [ VideoGraph.get_status_number(:submitting), VideoGraph.get_status_number(:submitting_error), + VideoGraph.get_status_number(:transcoding), VideoGraph.get_status_number(:transcoding_error), + VideoGraph.get_status_number(:ready) ] + end + + # Returns all available flag types + def get_all_flags + Flag.all + end + + end + + # Check for video lifecycle status + def is_status?(status) + return case status + when :uploading + # Video is uploading to S3 + self.status == VideoGraph.get_status_number(:uploading) + when :uploading_error + # There was an error in video being uploaded to S3 + self.status == VideoGraph.get_status_number(:uploading_error) + when :submitting + # Video is to be sent to to Zen + self.status == VideoGraph.get_status_number(:submitting) + when :submitting_error + # The were errors when sending the video to Zen + self.status == VideoGraph.get_status_number(:submitting_error) + when :transcoding + # Video is being encoded by Zen + self.status == VideoGraph.get_status_number(:transcoding) + when :transcoding_error + # There were errors when encoding the video + self.status == VideoGraph.get_status_number(:transcoding_error) + when :ready + # Video is ready for display + self.status == VideoGraph.get_status_number(:ready) + when :fatal_error + # Video cannot be encoded + self.status == VideoGraph.get_status_number(:fatal_error) + when :deleting + # Video record and associated files marked for deletion + self.status == VideoGraph.get_status_number(:deleting) + when :deleting_error + # Video record could not be deleted + self.status == VideoGraph.get_status_number(:deleting_error) + else + false + end + end + + + #################### + ## Social Helpers ## + #################### + + # Posts the video to facebook or twitter after it's ready + def send_to_facebook_or_twitter(social_network, social_settings) + token = social_settings.token + + begin + case social_network + when "facebook" + graph = Koala::Facebook::API.new(token) + graph.put_object("me", "feed", :message => "", + :picture => "#{self.get_thumbnail_url(self.selected_thumbnail)}", + :link => "#{Rails.application.routes.url_helpers.public_video_url(:public_token => self.public_token, :host => 'brevidy.heroku.com')}", + :name => "#{self.title unless self.title.blank?}", + :caption => "brevidy.com", + :description => "#{self.description}") + when "twitter" + token_secret = social_settings.token_secret + Twitter.consumer_key = Brevidy::Application::TWITTER_CONSUMER_KEY + Twitter.consumer_secret = Brevidy::Application::TWITTER_CONSUMER_SECRET + Twitter.oauth_token = token + Twitter.oauth_token_secret = token_secret + description = truncate(self.description, :length => 70, :omission => '...') + tweet = "Check out this video! #{description} #{Rails.application.routes.url_helpers.public_video_url(:public_token => self.public_token, :host => 'brevidy.heroku.com')} (via @brevidy)" + Twitter.update(tweet) + end + + rescue Exception => e + # TODO: need to add error catch if they deauthorize us + # so we clear out their social credentials + + Airbrake.notify(:error_class => "Logged Error", :error_message => "POST TO SOCIAL NETWORK: Error posting video ID #{self.id} to #{social_network} ... error was: #{e}") if Rails.env.production? + end + end + handle_asynchronously :send_to_facebook_or_twitter, :priority => 20 + + + ################### + ## Badge Helpers ## + ################### + + # Badges a video + def badge_it(badge_from, badge_type) + new_badge = self.badges.new(:badge_type => badge_type) + + if self.badges.where(:badge_from => badge_from.id, :badge_type => badge_type).first + new_badge.errors.add(:badge_from, "^You have already badged this video using the #{Icon.find(badge_type).name} badge") + else + new_badge.badge_from = badge_from.id + if new_badge.save + video_owner ||= User.find_by_id(self.user_id) + # Send an e-mail and add an activity feed item unless the person badged their own + # video or their notification settings say not to + unless new_badge.badge_from == video_owner.id + # Send e-mail + UserMailer.delay(:priority => 40).new_badge(new_badge) if video_owner.send_email_for_new_badges + # Activity feed item + UserEvent.delay(:priority => 40).create(:event_type => UserEvent.event_type_value(:badge), + :event_object_id => new_badge.id, + :user_id => video_owner.id, + :event_creator_id => badge_from.id) + end + end + end + + return new_badge + end + + # Returns whether or not a user has badged a video using a certain badge type + def has_been_badged_with?(badge, user) + badges.where(:badge_from => user, :badge_type => badge).first + end + + # Returns an array of unique badge types and associated counts for a given video + def unique_badge_types_and_counts + unique_badges = [] + all_badge_types = [] + + self.badges.group_by(&:badge_type).each do |badge_type, array_of_badges| + # put it into an array that we can use + unique_badges << [ Icon.badges.where('id = ?', badge_type).first, array_of_badges.size ] + all_badge_types << badge_type + end + return {:badge_sets => unique_badges.sort_by{|bdgs| bdgs[1]}.reverse, :all_badge_types => all_badge_types } + end + + # Returns the count for a particular badge type on specified video + def badges_count_for_type(badge_type) + self.badges.where(:badge_type => badge_type).count + end + + + ################# + ## Tag Helpers ## + ################# + + # Creates tags and/or a tagging relationship for a video + def create_taggings(video_tag_string) + # separate each tag by commas + video_tags = video_tag_string.split(",") rescue nil + unless video_tags.blank? + video_tags.each do |video_tag| + tag = Tag.find_or_create_by_content(video_tag.downcase.strip) + self.taggings.find_or_create_by_tag_id(tag.id) + end + end + end + + + ################# + ## URL Helpers ## + ################# + + # Returns the thumbnail url for a given thumbnail number + def get_thumbnail_url(thumbnail_number) + thumbnail_url = "#{Brevidy::Application::S3_BASE_URL}/#{self.thumbnail_path}/#{self.thumbnail_type}_#{self.base_filename}_000#{thumbnail_number}.png" + if self.remote_video_id.blank? + # show the standard thumbnail + return thumbnail_url + else + if self.remote_thumbnail.blank? + # remote video thumbnail is still processing so show the + # processing thumbnail for now + return "#{Brevidy::Application::S3_BASE_URL}/images/shared_processing.png" + else + # remote video thumbnail is ready so show it + return thumbnail_url + end + end + end + + + ################# + ## Secure URLs ## + ################# + + # Generates a time-sensitive, secure URL to a private video object on Amazon S3 + # Output looks something like: + # https://brevidytest.s3.amazonaws.com/videos/ok.m4v?AWSAccessKeyId=AKIAJPLELNYGJL5SYEEQ + # &Expires=1308755288&Signature=Amd6%2Fj4n4cFNOAAYz5MWIrb2Hgk%3D + def generate_secure_s3_url + # this was built using these instructions: + # http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?S3_QSAuth.html + # http://aws.amazon.com/code/199?_encoding=UTF8&jiveRedirect=1 + + s3_video_key = self.path + "/#{self.encoding_type}_#{self.base_filename}.mp4" # i.e. uploads/videos/101/115/enc1_3064220bcff92be.mp4 + s3_base_url = Brevidy::Application::S3_BASE_URL + bucket = Brevidy::Application::S3_BUCKET + access_key_id = Brevidy::Application::S3_ACCESS_KEY_ID + secret_access_key = Brevidy::Application::S3_SECRET_ACCESS_KEY + expiration_date = 2.days.from_now.utc.to_i # epoch/UNIX time + + # this string needs to be formatted exactly as it is below or it will fail + string_to_sign = "GET\n\n\n#{expiration_date}\n/#{bucket}/#{s3_video_key}".encode("UTF-8") + + # this must be CGI/URL encoded since a signature containing / or + characters will fail + signature = CGI.escape( Base64.encode64( + OpenSSL::HMAC.digest( + OpenSSL::Digest::Digest.new('sha1'), + secret_access_key, string_to_sign)).gsub("\n","") ) + + # this needs to be all on one line or the video player poops it's pants + return "#{s3_base_url}/#{s3_video_key}?AWSAccessKeyId=#{access_key_id}" + + "&Expires=#{expiration_date}" + + "&Signature=#{signature}" + end + + # Generates a time-sensitive, secure URL to a private video object on Amazon CloudFront + # Output looks something like: + # http://d3rbgx27at2ch0.cloudfront.net/uploads/videos/1/1435/enc1_74692eda29c9c96.mp4?Expires=1328858020 + # &Signature=ZImvIB262xVUF7JQ1nmK3aoNvT9IwUrlvwl7C6bUKZF52YeSoej2l1CgmQQMlj5vX6yZJQi7lomISHNgl7U0pvPPv6Eo2qY0oZ1BDXr4rHC5ATxuP2W2hmBSGX3BVw1gFEiuzh8qIXnz6pl-uPWhluXbHV3-mjTJ13FkSxlaqHs_ + # &Key-Pair-Id=APKAIISGSMRX2LN43Q6A + def generate_secure_cf_url + cf_base_url = Brevidy::Application::CLOUDFRONT_BASE_URL + video_key = self.path + "/#{self.encoding_type}_#{self.base_filename}.mp4" # i.e. uploads/videos/101/115/enc1_3064220bcff92be.mp4 + + # Pass in path to the private CloudFront key from AWS + signer = AwsCfSigner.new("#{Rails.root}/config/amazon/pk-APKAIISGSMRX2LN43Q6A.pem") + + # Generate the signed URL + url = signer.sign("#{cf_base_url}/#{video_key}", :ending => 14.days.from_now.utc.to_i) + end + + + ################### + ## Embed Helpers ## + ################### + + # Returns a video player object which handles Flash or HTML5 scenarios + def get_html5_iframe_code(page_type = "individual") + return RemoteVideoLinks.embed_html5_video_link(self.remote_host, self.remote_video_id, page_type, self.id).gsub("\n","") + end + + + ##################### + ## Sharing Helpers ## + ##################### + + # Shares a video via email + def share_via_email(current_user, recipient_emails, personal_message) + share_errors = [] + blank_email_count = 0 + + # strip out anything between quotes + recipient_emails = Video.strip_quotes(recipient_emails) + + # split the emails by looking for a comma + recipient_emails = recipient_emails.split(',') + + # Process each split entity + recipient_emails.each do |recipient_email| + # Strip off leading/trailing whitespace and make everything lower case + recipient_email = recipient_email.strip.downcase + + # Checks for and ignores a blank email address between commas + if !recipient_email.blank? + recipient_email = Video.extract_email_address(recipient_email) + if Video.this_is_a_valid_email?(recipient_email) + UserMailer.delay(:priority => 40).share_video(current_user, self, recipient_email, personal_message) + else + share_errors << "#{recipient_email} is an invalid email address" + end + else + # Keep track of all blank email addresses + blank_email_count += 1 + end + end + + # This checks for a situation where only blank email addresses were detected. + if blank_email_count == recipient_emails.size + share_errors << "You have not specified any email addresses to send this video to" + end + # return true unless there were errors and then return the errors + return share_errors.flatten unless share_errors.empty? + end + + + private + # Sets the featured_at time + def set_featured_at + self.featured_at = Time.now + end + + # Removes the associated video graph object if this is the last video object using it + def clean_up_if_necessary + unless Video.where('id != ? AND video_graph_id = ?', self.id, self.video_graph_id).exists? + # mark the video graph for deletion (so no other video object uses it) + vg = self.video_graph + vg.deleted = true + vg.save + + # cache the params to destroy + cached_attributes ||= vg.get_attributes_needed_for_deleting + + # check if the video is in the middle of transcoding + # to see if we're about to orphan some files on S3 + self.is_status?(VideoGraph::TRANSCODING) ? (orphaned = true) : (orphaned = false) + + # check if the S3 files were orphaned and use the appropriate clean up method + if orphaned + # clean up after ourselves after a long delay so we give + # Zencoder ample time to place the orphaned files in the bucket + VideoGraph.clean_up_on_S3_after_12_hours(cached_attributes) + else + # clean up after ourselves on S3 without a long delay + VideoGraph.clean_up_on_S3(cached_attributes) + end + end + end + + # If you don't do this, the length validation throws an error for whatever reason + def trim_newlines_from_fields + self.title = self.title.gsub(/\n|\r/, '') unless self.title.blank? + end + + # Generates an 11 character public token for displaying videos without having to be authenticated + def generate_public_token! + loop do + new_token = SecureRandom.base64(11).tr('+/=', 'xyz').first(11) + break self.public_token = new_token unless Video.where(:public_token => new_token).exists? + end + self.save + end + + # Updates the updated_at field for the associated channel to bring it to the top of the list + def touch_channel! + self.channel.update_attribute(:updated_at, Time.now) unless self.channel.blank? + end + + # Email validations for sharing videos via email + class << self + # Validates an email address + def this_is_a_valid_email?(email) + email.match(User::EMAIL_REGEX) + end + + # Strips anything with double quotes from the email address field + # i.e. "Rob Phillips" would return just + def strip_quotes(emails) + return emails.gsub(/".*?"/, '') + end + + # If email address is within <>, extract out address, or return original address if not between <>. + def extract_email_address(email) + # Check for <@> pattern and extract + result = email[/<.+@.+>/] + if result.nil? + # If not found, return original email address for further validation + return email + else + # If found return address minus the <> and leading/trailing spaces + return result.gsub(/[<>]/,'<' => '', '>' => '').strip + end + end + end +end + + + +# == Schema Information +# +# Table name: videos +# +# id :integer not null, primary key +# user_id :integer +# created_at :datetime +# updated_at :datetime +# delta :boolean default(TRUE), not null +# selected_thumbnail :integer default(0) +# public_token :string(255) +# send_to_facebook :boolean default(FALSE) +# send_to_twitter :boolean default(FALSE) +# video_graph_id :integer +# channel_id :integer +# title :string(255) +# description :text +# featured_at :datetime +# + diff --git a/app/models/video_error.rb b/app/models/video_error.rb new file mode 100755 index 0000000..c192c78 --- /dev/null +++ b/app/models/video_error.rb @@ -0,0 +1,20 @@ +class VideoError < ActiveRecord::Base +end + + + + + +# == Schema Information +# +# Table name: video_errors +# +# id :integer not null, primary key +# video_graph_id :integer +# error_status :integer +# created_at :datetime +# updated_at :datetime +# error_message :text +# user_id :integer +# + diff --git a/app/models/video_flag.rb b/app/models/video_flag.rb new file mode 100755 index 0000000..d5ad5ac --- /dev/null +++ b/app/models/video_flag.rb @@ -0,0 +1,21 @@ +class VideoFlag < ActiveRecord::Base + attr_accessible :flag_id, :detailed_reason + + validates :flag_id, :video_id, :presence => true +end + + + +# == Schema Information +# +# Table name: video_flags +# +# id :integer not null, primary key +# flag_id :integer +# video_id :integer +# flagged_by :integer +# detailed_reason :text +# created_at :datetime +# updated_at :datetime +# + diff --git a/app/models/video_graph.rb b/app/models/video_graph.rb new file mode 100755 index 0000000..669edad --- /dev/null +++ b/app/models/video_graph.rb @@ -0,0 +1,496 @@ +require 'timeout' + +class VideoGraph < ActiveRecord::Base + # whitelist to define which attributes can be mass-assigned + attr_accessible :remote_thumbnail + + # handles uploading thumbnails + # UNCOMMENT AFTER MIGRATION + mount_uploader :remote_thumbnail, ThumbnailUploader + + # Lifecycle actions + # UNCOMMENT AFTER MIGRATION + before_validation :set_base_filename, :on => :create + after_create :set_path!, :set_thumbnail_path! + + # Active Record relationships + belongs_to :user + has_many :videos, :dependent => :destroy + has_many :video_errors + + # validates these attribute conditions are met + validates :user_id, :base_filename, :encoding_type, :thumbnail_type, :status, :presence => true + validates :remote_host, :on => :create, + :inclusion => { :in => ["vimeo.com", "youtube.com", "youtu.be"], + :message => "^We only allow YouTube or Vimeo videos at this time"}, + :allow_blank => true, + :allow_nil => true + ############### + ## Constants ## + ############### + + # Status Constants + CREATED = :created # VideoGraph object waiting for upload to start + UPLOADING = :uploading # Video is uploading to S3 + UPLOADING_ERROR = :uploading_error # There was an error in video being uploaded to S3 + SUBMITTING = :submitting # Video is to be sent to to Zencoder + SUBMITTING_ERROR = :submitting_error # The were errors when sending the video to Zencoder + TRANSCODING = :transcoding # Video is being encoded by Zencoder + TRANSCODING_ERROR = :transcoding_error # There were errors when encoding the video + READY = :ready # Video is ready for display + FATAL_ERROR = :fatal_error # Video cannot be encoded + DELETING = :deleting # Video record and associated files marked for deletion + DELETING_ERROR = :deleting_error # Video record could not be deleted + + ################ + ## S3 Helpers ## + ################ + + def upload_remote_thumbnail(remote_video_thumbnail) + begin + # download, process, and save the remote video thumbnail on S3 + self.remote_remote_thumbnail_url = remote_video_thumbnail + self.save + + rescue + # tell us about the error + Airbrake.notify(:error_class => "Logged Error", :error_message => "SHARING LINK ERROR: There was an error uploading the thumbnail for Video ID: #{self.id} | Thumbnail: #{remote_video_thumbnail}") if Rails.env.production? + end + end + # test real-time uploads of thumbnails + # handle_asynchronously :upload_remote_thumbnail, :priority => 0 + + + ################## + ## Meta Helpers ## + ################## + + # Gets the user who originally uploaded this video so we properly attribute it + def get_user_who_uploaded_this + User.find_by_id(self.user_id) rescue nil + end + + + ############## + ## Encoding ## + ############## + + # Send a video to Zencoder for encoding. + def encode + # Set up parameters for encoding + user = User.find_by_id(self.user_id) + video = Video.find_by_video_graph_id(self.id) + base_path = "#{Brevidy::Application::S3_BASE_URL}/#{Brevidy::Application::S3_VIDEOS_RELATIVE_PATH}" + input_file = "#{base_path}/#{user.id}/orig_#{self.base_filename}_#{user.id}" + output_path = "#{base_path}/#{user.id}/#{self.id.to_s}/" + enc_file_name = "#{self.encoding_type}_#{self.base_filename}.mp4" + thumb_file_name = "#{self.thumbnail_type}_#{self.base_filename}" + + # Figure out callback information + if Rails.env.production? || Rails.env.staging? + ze_host = Rails.env.staging? ? "brevidystaging.heroku.com" : "brevidy.heroku.com" + callback_url = Rails.application.routes.url_helpers.user_video_encoder_callback_url(user, video, :host => ze_host) + + # Set up how we want Zencoder to notify us after transcoding success/failure + notifications = [{ :format => "json", + :url => callback_url }] + else + # We won't get a notification from Zencoder if we are in dev or test environments + notifications = nil + end + + # Submit job to Zencoder with a 30 second timeout. + begin + Timeout::timeout(30) { + response = Zencoder::Job.create({:input => input_file, + :pass_through => video.id, + :outputs => [{ + :access_control => [ + { :permission => "READ", + :grantee => "your_key_goes_here" }, + { :permission => "FULL_CONTROL", + :grantee => "your_key_goes_here" } + ], + :format => 'mp4', + :audio_codec => 'aac', + :video_codec => 'h264', + :label => self.base_filename, + :base_url => output_path, + :filename => enc_file_name, + :clip_length => 600, + :width => 1264, + :height => 711, + :public => false, + :thumbnails => [{ + :access_control => [ + { :permission => "READ", + :grantee => "http://acs.amazonaws.com/groups/global/AllUsers" }, + { :permission => "FULL_CONTROL", + :grantee => "your_key_goes_here" } + ], + :label => "thumbs", + :number => 4, + :base_url => output_path, + :height => 134, + :width => 250, + :aspect_mode => "pad", + :prefix => thumb_file_name, + :public => true, + :rrs => true + }], + :notifications => notifications + }] + }); + + if response.code.to_i == 201 + # reset submitting error count + self.submitting_error_count = 0 + # set the zencoder job id + self.zencoder_job_id = response.body["id"].to_i + + if Rails.env.production? || Rails.env.staging? + self.set_status(VideoGraph::TRANSCODING) + else + # Set to done because if we aren't on heroku then we can't get zencoder callbacks. + self.set_status(VideoGraph::READY) + end + else + # Store off error message and set the status to :submitting_error + # so we retry the connection every minute until successful + self.error_message = response.inspect.to_s + self.set_status(VideoGraph::SUBMITTING_ERROR) + # save the error for QA tracking and analytics + self.video_errors.create(:user_id => user.id, :error_status => self.status, :error_message => self.error_message) + # increment the submitting error count + self.increment :submitting_error_count + Airbrake.notify(:error_class => "Logged Error", :error_message => "ZENCODER CONNECTION ERROR: Zen response code was #{response.code}") if Rails.env.production? + end + + # save the video state + self.save + } # end of timeout + + # Rescue any connection error. The Zencoder plugin + # abstracts these as Zencoder::HTTPError. + rescue Timeout::Error, Zencoder::HTTPError + # Set the status to :transfer_error so we retry the + # connection every minute until successful + self.set_status(VideoGraph::SUBMITTING_ERROR) + # save the error for QA tracking and analytics + self.video_errors.create(:user_id => user.id, :error_status => self.status, :error_message => "Timeout::Error or Zencoder::HTTPError") + # increment the submitting error count + self.increment :submitting_error_count + self.save + Airbrake.notify(:error_class => "Logged Error", :error_message => "ZENCODER CONNECTION ERROR: Connection timed out or Zencoder returned an HTTPError") if Rails.env.production? + end + end + + # Handles what happens when we have an error with the transcoding process + def handle_transcoding_error(errors_hash) + self.error_message = errors_hash.to_s + self.increment :transcoding_error_count + + if self.error_message.include?("TranscodingError") || self.error_message.include?("WorkerTimeoutError") + self.set_status(VideoGraph::TRANSCODING_ERROR) + if (self.transcoding_error_count >= 3 ) + Airbrake.notify(:error_class => "Logged Error", :error_message => "MULTIPLE TRANSCODING ERRORS: We should investigate. There were multiple transcoding errors for video graph ID: #{self.id}") if Rails.env.production? + end + else + self.set_status(VideoGraph::FATAL_ERROR) + Airbrake.notify(:error_class => "Logged Error", :error_message => "FATAL TRANSCODING ERROR: Zencoder said it was impossible to encode video graph ID: #{self.id}.") if Rails.env.production? + # Send an e-mail to the user about it + UserMailer.delay(:priority => 40).fatal_error_on_video(User.find_by_id(self.user_id)) + end + # save the record + self.save + + # save the error for QA tracking and analytics + self.video_errors.create(:user_id => self.user_id, :error_status => self.status, :error_message => self.error_message) + end + + + #################### + ## Status Helpers ## + #################### + + # Setter for video lifecycle status + def set_status(status) + case status + when :created + # VideoGraph object waiting for upload to start + self.status = VideoGraph.get_status_number(:created) + when :uploading + # Video is uploading to S3 + self.status = VideoGraph.get_status_number(:uploading) + when :uploading_error + # There was an error in video being uploaded to S3 + self.status = VideoGraph.get_status_number(:uploading_error) + when :submitting + # Video is to be sent to to Zen + self.status = VideoGraph.get_status_number(:submitting) + when :submitting_error + # The were errors when sending the video to Zen + self.status = VideoGraph.get_status_number(:submitting_error) + when :transcoding + # Video is being encoded by Zen + self.status = VideoGraph.get_status_number(:transcoding) + when :transcoding_error + # There were errors when encoding the video + self.status = VideoGraph.get_status_number(:transcoding_error) + when :ready + # Video is ready for display + self.status = VideoGraph.get_status_number(:ready) + when :fatal_error + # Video cannot be encoded + self.status = VideoGraph.get_status_number(:fatal_error) + when :deleting + # Video record and associated files marked for deletion + self.status = VideoGraph.get_status_number(:deleting) + when :deleting_error + # Video record could not be deleted + self.status = VideoGraph.get_status_number(:deleting_error) + end + return nil + end + + # Returns a string for a video lifecycle status + def translate_status + return case self.status + when 0 + "created" + when 1 + "uploading" + when 2 + "uploading_error" + when 3 + "submitting" + when 4 + "submitting_error" + when 5 + "transcoding" + when 6 + "transcoding_error" + when 7 + "ready" + when 8 + "fatal_error" + when 9 + "deleting" + when 10 + "deleting_error" + else + false + end + end + + class << self + # Returns an integer for a video lifecycle status + def get_status_number(status) + return case status + when :created + 0 + when :uploading + 1 + when :uploading_error + 2 + when :submitting + 3 + when :submitting_error + 4 + when :transcoding + 5 + when :transcoding_error + 6 + when :ready + 7 + when :fatal_error + 8 + when :deleting + 9 + when :deleting_error + 10 + else + false + end + end + + ####################### + ## Video Job Helpers ## + ####################### + + # removes all video files and thumbnails on S3 + # you cannot do this in a before_destroy since delayed_job will + # give you a deserialization error for acting on a destroyed object + def clean_up_on_S3(cached_attributes) + video_owner_id = cached_attributes[0] + video_path = cached_attributes[1] + base_filename = cached_attributes[2] + encoding_type = cached_attributes[3] + thumbnail_type = cached_attributes[4] + + f = Brevidy::Fog::S3::File.new + + unless base_filename.blank? || base_filename == "sample_populated" + # delete the original video file + original_video_path = "#{Brevidy::Application::S3_VIDEOS_RELATIVE_PATH}/#{video_owner_id}/" + f.delete(original_video_path, "orig_#{base_filename}_#{video_owner_id}") + + # now delete the processed video and thumbnails + # these paths require a trailing slash since the video path doesn't come with one + f.delete("#{video_path}/", "#{encoding_type}_#{base_filename}.mp4") + f.delete("#{video_path}/", "#{thumbnail_type}_#{base_filename}_0000.png") + f.delete("#{video_path}/", "#{thumbnail_type}_#{base_filename}_0001.png") + f.delete("#{video_path}/", "#{thumbnail_type}_#{base_filename}_0002.png") + f.delete("#{video_path}/", "#{thumbnail_type}_#{base_filename}_0003.png") + + # Remove the video graph object + vg = VideoGraph.find_by_base_filename(base_filename) + vg.destroy unless vg.blank? + end + end + handle_asynchronously :clean_up_on_S3, :priority => 50 + + # Removes files from S3 that had their video objects deleted while the + # video was still transcoding on Zencoder. This runs after 12 hours have passed + # (i.e. Zencoder will still place the orphaned files on S3 after it's done) + def clean_up_on_S3_after_12_hours(cached_attributes) + # clean up after ourselves on S3 + VideoGraph.clean_up_on_S3(cached_attributes) + end + handle_asynchronously :clean_up_on_S3_after_12_hours, :priority => 50, :run_at => Proc.new { 12.hours.from_now } + + # Runs every minute to check for videos that are in the pending state + # and attempts to re-run them again via delayed_job since cron jobs + # are only hourly on Heroku + def run_pending_or_failed_video_encodings + # Find all that had less than or equal to 3 submitting error responses from Zencoder + submitting_error_videos = VideoGraph.where('status = ? AND submitting_error_count <= ?', VideoGraph.get_status_number(:submitting_error), 3) + submitting_error_videos.each do |sev| + sev.set_status(VideoGraph::SUBMITTING) + sev.save + sev.encode + end + + # Find all that had failures from Zencoder and + # only retry ones with TranscodingError or WorkerTimeoutError + # https://app.zencoder.com/docs/guides/advanced-integration/retrying-failed-jobs + failed_videos = VideoGraph.where("status = ? AND + (error_message LIKE '%TranscodingError%' OR error_message LIKE '%WorkerTimeoutError%') AND + transcoding_error_count <= ?", VideoGraph.get_status_number(:transcoding_error), 3) + failed_videos.each do |fv| + fv.set_status(VideoGraph::SUBMITTING) + fv.save + fv.encode + end + + # run again in 1 minute + VideoGraph.run_pending_or_failed_video_encodings + end + handle_asynchronously :run_pending_or_failed_video_encodings, :priority => 50, :run_at => Proc.new { 1.minute.from_now } + + # Set uploading_error for any videos that have been in the + # uploading state for more than 7 days + def check_videos_for_stale_uploading_states + stale_videos = VideoGraph.where('created_at < ? AND status = ?', 7.days.ago, VideoGraph.get_status_number(:uploading)) + stale_videos.each do |sv| + sv.set_status(VideoGraph::UPLOADING_ERROR) + sv.save + end + end + handle_asynchronously :check_videos_for_stale_uploading_states, :priority => 50 + + # Removes all videos that are considered irreparable from the db and S3 + # since all quality assurance data about failures is stored in video_errors table + def remove_irreparable_videos + irreparable_statuses = [ VideoGraph.get_status_number(:uploading_error), + VideoGraph.get_status_number(:fatal_error) ] + irreparable_videos = VideoGraph.where('status IN (?)', irreparable_statuses) + irreparable_videos.each do |iv| + cached_attributes ||= iv.get_attributes_needed_for_deleting + if iv.destroy + # clean up after ourselves on S3 + VideoGraph.clean_up_on_S3(cached_attributes) + else + # set video status to deleting error so admins can investigate + iv.set_status(VideoGraph::DELETING_ERROR) + iv.save + end + end + end + handle_asynchronously :remove_irreparable_videos, :priority => 50 + + # Attempts to resubmit videos with more than 3 submitting errors every hour via cron + def resubmit_videos_with_submitting_errors + submitting_error_videos = VideoGraph.where('status = ? AND submitting_error_count > ?', VideoGraph.get_status_number(:submitting_error), 3) + submitting_error_videos.each do |sev| + sev.set_status(VideoGraph::SUBMITTING) + sev.save + sev.encode + end + end + handle_asynchronously :resubmit_videos_with_submitting_errors, :priority => 0 + + end + + # Returns an array of attributes needed to remove videos from S3 + def get_attributes_needed_for_deleting + # returns path, base_filename, encoding_type, and thumbnail_type + return [ self.user_id, self.path, self.base_filename, self.encoding_type, self.thumbnail_type ] + end + + private + + # Sets the base filename (20 character SHA2 hex) for a new VideoGraph object + def set_base_filename + loop do + random_token = Digest::SHA2.hexdigest("#{Time.now.utc}--#{rand(99999)}").first(20) + break self.base_filename = random_token unless VideoGraph.where(:base_filename => random_token).exists? + end + end + + # Sets the path for video based upon final expected path after encoding + def set_path! + self.path = "#{Brevidy::Application::S3_VIDEOS_RELATIVE_PATH}/#{self.user_id}/#{self.id}" + self.save + end + + # Sets the path for thumbnail of the video based upon final expected path after encoding + def set_thumbnail_path! + self.thumbnail_path = "#{Brevidy::Application::S3_VIDEOS_RELATIVE_PATH}/#{self.user_id}/#{self.id}" + self.save + end + +end + + + + + + + +# == Schema Information +# +# Table name: video_graphs +# +# id :integer not null, primary key +# thumbnail_path :string(255) +# path :string(255) +# callback_url :string(255) +# base_filename :string(255) +# encoding_type :string(255) default("enc1") +# thumbnail_type :string(255) default("thumb1") +# status :integer default(0) +# zencoder_job_id :integer +# remote_host :string(255) +# remote_video_id :string(255) +# remote_thumbnail :string(255) +# delta :boolean default(TRUE), not null +# created_at :datetime +# updated_at :datetime +# submitting_error_count :integer default(0) +# transcoding_error_count :integer default(0) +# error_message :text +# user_id :integer +# deleted :boolean default(FALSE) +# + diff --git a/app/uploaders/banner_uploader.rb b/app/uploaders/banner_uploader.rb new file mode 100755 index 0000000..399cca1 --- /dev/null +++ b/app/uploaders/banner_uploader.rb @@ -0,0 +1,25 @@ +class BannerUploader < CarrierWave::Uploader::Base + include CarrierWave::MiniMagick + + # Override the directory where uploaded files will be stored. + def store_dir + "uploads/images/#{model.id}" + end + + # Generate a banner image + version :resized do + process :resize_to_fill => [850, 315] + end + + # Set the filename for versioned files + def filename + # appending .jpg onto the end causes MiniMagick to + # automatically convert the image to that format + random_token = Digest::SHA2.hexdigest("#{Time.now.utc}--#{model.id.to_s}").first(15) + ivar = "@#{mounted_as}_secure_token" + token = model.instance_variable_get(ivar) + token ||= model.instance_variable_set(ivar, random_token) + "banner_#{model.id}_#{token}.jpg" if original_filename + end + +end diff --git a/app/uploaders/image_uploader.rb b/app/uploaders/image_uploader.rb new file mode 100755 index 0000000..c14a560 --- /dev/null +++ b/app/uploaders/image_uploader.rb @@ -0,0 +1,66 @@ +class ImageUploader < CarrierWave::Uploader::Base + # include MiniMagick support for resizing images + include CarrierWave::MiniMagick + + # Choose what kind of storage to use for this uploader: + # (handled in the initializer file) + # storage :file + # storage :fog + + # Override the directory where uploaded files will be stored. + def store_dir + "uploads/images/#{model.id}" + end + + # Create different versions of your uploaded files: + version :large_profile do + # returns a 150x150 image with rounded corners + process :resize_to_fill => [150, 150] + # process :rounded_corners => [20] + end + version :medium_profile do + # returns a 50x50 image + process :resize_to_fill => [50, 50] + end + version :small_profile do + # returns a 35x35 image + process :resize_to_fill => [35, 35] + end + + # Set the filename for versioned files + def filename + # appending .jpg onto the end causes MiniMagick to + # automatically convert the image to that format + random_token = Digest::SHA2.hexdigest("#{Time.now.utc}--#{model.id.to_s}").first(10) + ivar = "@#{mounted_as}_secure_token" + token = model.instance_variable_get(ivar) + token ||= model.instance_variable_set(ivar, random_token) + "#{model.id}_#{token}.jpg" if original_filename + end + +=begin +# Code for rounding corners of an image +# Only works on PNG files and with RMagick though :\ + def rounded_corners(radius) + manipulate! do |img| + mask = ::Magick::Image.new(img.columns, img.rows) {self.background_color = 'black'} + + gc = ::Magick::Draw.new + gc.stroke('white').fill('white') + gc.roundrectangle(0, 0, img.columns - 1, img.rows - 1, radius, radius) + gc.draw(mask) + + mask.matte = false + img.matte = true + + thumb = img.composite(mask, Magick::CenterGravity, Magick::CopyOpacityCompositeOp) + thumb.alpha(Magick::ActivateAlphaChannel) + thumb.format = 'png' + #thumb.display + puts "has alpha? #{thumb.alpha?} and returned #{thumb.inspect}" + thumb + end + end +=end + +end diff --git a/app/uploaders/thumbnail_uploader.rb b/app/uploaders/thumbnail_uploader.rb new file mode 100755 index 0000000..680e72c --- /dev/null +++ b/app/uploaders/thumbnail_uploader.rb @@ -0,0 +1,19 @@ +class ThumbnailUploader < CarrierWave::Uploader::Base + # include MiniMagick support for resizing images + include CarrierWave::MiniMagick + + # Override the directory where uploaded files will be stored. + def store_dir + "#{model.thumbnail_path}" + end + + process :resize_to_fill => [250, 134] + + # Set the filename + def filename + # appending .png onto the end causes MiniMagick to + # automatically convert the image to that format + "#{model.thumbnail_type}_#{model.base_filename}_0000.png" if original_filename + end + +end diff --git a/app/views/badges/_badge.html.haml b/app/views/badges/_badge.html.haml new file mode 100755 index 0000000..5d2b8c3 --- /dev/null +++ b/app/views/badges/_badge.html.haml @@ -0,0 +1,7 @@ +%li{"data-badge-type" => icon.badge_type} + %i{:class => "#{icon.css_class + '_Large'} tooltip-with-icon", :title => "#{icon.name}"} + %span + - if count.to_i > 9999 + 9,999+ + -else + = number_with_delimiter(count, :delimiter => ',') \ No newline at end of file diff --git a/app/views/badges/_badge_activity.html.haml b/app/views/badges/_badge_activity.html.haml new file mode 100755 index 0000000..30e835a --- /dev/null +++ b/app/views/badges/_badge_activity.html.haml @@ -0,0 +1,7 @@ +- icon ||= Icon.find(badge_activity.badge_type) +%li + %i{:class => "#{icon.css_class}_Large"} + %span + was given by + = link_to("#{badge_activity.from.name}", user_path(badge_activity.from)) + #{time_ago_in_words(badge_activity.created_at)} ago \ No newline at end of file diff --git a/app/views/badges/_give_a_badge.html.haml b/app/views/badges/_give_a_badge.html.haml new file mode 100755 index 0000000..a0b37b8 --- /dev/null +++ b/app/views/badges/_give_a_badge.html.haml @@ -0,0 +1,8 @@ +- if signed_in? + = link_to(user_video_badges_path(video_owner, video), :class => "give-a-badge", + "data-badge-type" => "#{icon.id}", "data-video-id" => "#{video.id}") do + %i{:title => "#{icon.name}", :class => "tooltip-with-icon #{icon.css_class}"} + +- else + = link_to(login_path, :class => "show-msg-modal", "data-modal-title" => "Please login or sign up", "data-modal-message" => "You must Login or Sign up before you can badge a video.") do + %i{:title => "#{icon.name}", :class => "tooltip-with-icon #{icon.css_class}"} \ No newline at end of file diff --git a/app/views/badges/_give_badges.html.haml b/app/views/badges/_give_badges.html.haml new file mode 100755 index 0000000..316f114 --- /dev/null +++ b/app/views/badges/_give_badges.html.haml @@ -0,0 +1,18 @@ +- if all_badge_types.include?(give_badges.id) + - # this video has been given a certain badge, see if it's the + - # current user that gave it + - if video.has_been_badged_with?(give_badges, current_user) + - badge ||= find_badge_for_video(give_badges, video) + = render :partial => 'badges/unbadge.html', :locals => { :badge => badge, + :video => video, + :video_owner => video_owner, + :icon => give_badges } + - else + = render :partial => 'badges/give_a_badge.html', :locals => { :video => video, + :video_owner => video_owner, + :icon => give_badges } + +- else + = render :partial => 'badges/give_a_badge.html', :locals => { :video => video, + :video_owner => video_owner, + :icon => give_badges } \ No newline at end of file diff --git a/app/views/badges/_unbadge.html.haml b/app/views/badges/_unbadge.html.haml new file mode 100755 index 0000000..b963a0d --- /dev/null +++ b/app/views/badges/_unbadge.html.haml @@ -0,0 +1,4 @@ += link_to(user_video_badges_path(video_owner, video), :class => "unbadge", + "data-badge-type" => "#{icon.id}", "data-video-id" => "#{video.id}", + "data-badge-id" => "#{badge.id}") do + %i{:rel => "twipsy", :class => "#{icon.css_class}", :title => "Unbadge", :style => "opacity: 0.3"} \ No newline at end of file diff --git a/app/views/badges/_view_all_badges.html.haml b/app/views/badges/_view_all_badges.html.haml new file mode 100755 index 0000000..4191046 --- /dev/null +++ b/app/views/badges/_view_all_badges.html.haml @@ -0,0 +1,3 @@ +- badges_count == 1 ? badge_message = "view badge" : badge_message = "view all #{badges_count} badges" +- path_to_this = @viewing_via_token_access ? user_video_badges_dialog_path(video_owner, video, :channel_token => video.channel.public_token) : user_video_badges_dialog_path(video_owner, video) +(#{link_to(badge_message, path_to_this, "data-video-id" => "#{video.id}", :remote => true, "data-method" => "GET")}) \ No newline at end of file diff --git a/app/views/badges/badges_dialog.js.haml b/app/views/badges/badges_dialog.js.haml new file mode 100755 index 0000000..703b9df --- /dev/null +++ b/app/views/badges/badges_dialog.js.haml @@ -0,0 +1,2 @@ +:plain + brevidy.dialog("Viewing all badges", "
    #{escape_javascript(render(:partial => 'badges/badge_activity.html', :collection => @badges, :locals => { :user => @user }))}
", "message"); \ No newline at end of file diff --git a/app/views/badges/base.json.rabl b/app/views/badges/base.json.rabl new file mode 100755 index 0000000..ae78c68 --- /dev/null +++ b/app/views/badges/base.json.rabl @@ -0,0 +1,9 @@ +attributes :id +if highlight_latest_activity? + child(:video) { extends "videos/base" } +else + attributes :video_id +end +node(:from) { |b| puts b.inspect; partial("users/base", :object => User.find_by_id(b.badge_from)) } +node(:type) { |b| b.badge_type } +node(:description) { |b| Icon.find(b.badge_type).name } \ No newline at end of file diff --git a/app/views/badges/index.html.haml b/app/views/badges/index.html.haml new file mode 100755 index 0000000..37be2ae --- /dev/null +++ b/app/views/badges/index.html.haml @@ -0,0 +1,31 @@ +- # User Banner Image += render 'users/top_banner' + +- # Middle User Info Section += render 'users/middle_user_banner' + +- # Dark Content Wrapper +.lower-wrapper + = render 'users/featured_videos' + + - # Main (White) Content Wrapper + .content-wrapper + = render 'shared/content_titles' + + .info-message.mhl.centered + %h4 Badges Given to #{@user.name} + + .badges-grid + - @badges.each do |b| + .grid-item.thumbnail + %p + %strong #{b[:name]} + %i{:class => "#{b[:css_class] + '_Preview'}"} + %p + Received + %br/ + #{b[:count]} + + +- # Tell the page to scroll down to the content wrapper upon page load += render 'shared/scroll_to_content_wrapper' \ No newline at end of file diff --git a/app/views/badges/index.json.rabl b/app/views/badges/index.json.rabl new file mode 100755 index 0000000..99a97da --- /dev/null +++ b/app/views/badges/index.json.rabl @@ -0,0 +1,5 @@ +collection @badges => :badges + +node(:type) { |b| Icon.find_by_name(b[:name]).id } +node(:description) { |b| b[:name] } +node(:count) { |b| b[:count] } diff --git a/app/views/channels/_button_subscribe.html.haml b/app/views/channels/_button_subscribe.html.haml new file mode 100755 index 0000000..9ebdbe7 --- /dev/null +++ b/app/views/channels/_button_subscribe.html.haml @@ -0,0 +1,2 @@ +- link_text = is_private ? "Request Access" : "Subscribe" += link_to(link_text, user_channel_subscribe_path(get_object_owner(channel), channel, :ref => button_size), :class => "subscribe btn #{button_size}", :remote => true, :method => "POST") \ No newline at end of file diff --git a/app/views/channels/_button_unsubscribe.html.haml b/app/views/channels/_button_unsubscribe.html.haml new file mode 100755 index 0000000..30babd8 --- /dev/null +++ b/app/views/channels/_button_unsubscribe.html.haml @@ -0,0 +1 @@ += link_to("Subscribed", user_channel_unsubscribe_path(get_object_owner(channel), channel, :ref => button_size), :class => "unsubscribe btn success #{button_size}", :remote => true, :method => "DELETE") \ No newline at end of file diff --git a/app/views/channels/_channel.html.haml b/app/views/channels/_channel.html.haml new file mode 100755 index 0000000..efc54fd --- /dev/null +++ b/app/views/channels/_channel.html.haml @@ -0,0 +1,94 @@ +- # For the "content_for" blocks, you must key off the channel ID +- # otherwise it will keep appending data to a regular symbol like :channel_view +- # so by the time you get to the 2nd or 3rd channel, you will have 2 or 3 sets +- # of channel data attached to the "content_for" store + +- channel_owner = get_object_owner(channel) + +- content_for "channel_view_#{channel.id}" do + - if controller.controller_name == 'subscriptions' && controller.action_name == 'subscriptions' + .subscriptions-view + = link_to(user_path(channel_owner)) do + = image_tag("#{channel_owner.image.blank? ? 'default_user_35px.jpg' : channel_owner.image_url(:small_profile) }", + :alt => "#{channel_owner.name}", + :size => "35x35") + + .meta-area + %h5 + = link_to(user_channel_path(channel_owner, channel), :class => "lighten-to-blue") do + #{channel.title} + %p + = link_to(user_path(channel_owner), :class => "lighten-to-blue") do + by #{channel_owner.name} + + - else + %h5 + - if channel.featured? + = image_tag("featured_star.png", :alt => "Featured Channel", :size => "16x16") + - elsif channel.private? + = image_tag("private_small.png", :alt => "Private Channel", :size => "16x16") + - else + = image_tag("public.png", :alt => "Public Channel", :size => "16x16") + + = link_to(user_channel_path(channel_owner, channel), :class => "lighten-to-blue") do + #{channel.title} + + - # Thumbnail view + = link_to(user_channel_path(channel_owner, channel), :class => "channel-thumbnail-area") do + .thumbnails + - most_recent_videos = channel.videos_for_preview + - most_recent_videos.each do |v| + .thumb-view + = image_tag(v.get_thumbnail_url(v.selected_thumbnail), :alt => v.title, :size => "190x102") + +- content_for "private_channel_message_#{channel.id}" do + - if controller.controller_name == 'subscriptions' && controller.action_name == 'subscriptions' + .subscriptions-view + = link_to(user_path(channel_owner)) do + = image_tag("#{channel_owner.image.blank? ? 'default_user_35px.jpg' : channel_owner.image_url(:small_profile) }", + :alt => "#{channel_owner.name}", + :size => "35x35") + + .meta-area + %h5 + #{channel.title} + %p + = link_to(user_path(channel_owner), :class => "lighten-to-blue") do + by #{channel_owner.name} + - else + %h5 + = image_tag("private_small.png", :alt => "Private Channel", :size => "16x16") + #{channel.title} + + = render :partial => "channels/private_area_message" + +.channel-wrapper{"data-channel-id" => channel.id} + - # Channel Content Area + - if current_user_owns?(channel) + = content_for "channel_view_#{channel.id}" + - elsif channel.private? + - if current_user.blank? + = content_for "private_channel_message_#{channel.id}" + - else + - if current_user.is_subscribed_to?(channel) + = content_for "channel_view_#{channel.id}" + - else + = content_for "private_channel_message_#{channel.id}" + - else + = content_for "channel_view_#{channel.id}" + + + .button-area + - if current_user_owns?(channel) + = link_to("Manage Channel", edit_user_channel_path(channel_owner, channel), :class => "btn xlarge") + - else + - if current_user.blank? + = link_to("Login to Subscribe", root_path, :class => "btn xlarge") + - else + - if current_user.is_subscribed_to?(channel) + = render :partial => "channels/button_unsubscribe", :locals => { :user => channel_owner, :channel => channel, :button_size => 'xlarge' } + - else + - if channel.private? + = render :partial => "channels/button_subscribe", :locals => { :user => channel_owner, :channel => channel, :is_private => true, :button_size => 'xlarge' } + - else + = render :partial => "channels/button_subscribe", :locals => { :user => channel_owner, :channel => channel, :is_private => false, :button_size => 'xlarge' } \ No newline at end of file diff --git a/app/views/channels/_featured_video.html.haml b/app/views/channels/_featured_video.html.haml new file mode 100755 index 0000000..0306634 --- /dev/null +++ b/app/views/channels/_featured_video.html.haml @@ -0,0 +1,21 @@ +.featured-video-post{"data-video-id" => "#{featured_video.id}"} + .update-position-area + .move-to-top + = link_to(user_update_featured_videos_path(current_user, :video_id => featured_video.id), :class => "move-video-to-top", + "data-video-id" => "#{featured_video.id}", :remote => true, :method => "PUT") do + + = image_tag("up_arrow.png", :size => "20x10", :alt => "Move to Top") + Move to Top + + .thumbnail + = image_tag(featured_video.get_thumbnail_url(featured_video.selected_thumbnail), :alt => featured_video.title, :size => "190x102") + + .featured-video-meta-area + %h3 + = featured_video.title + + %span.label.important Featured + + %span.gray + #{time_ago_in_words(featured_video.featured_at)} ago + \ No newline at end of file diff --git a/app/views/channels/_private_area_message.html.haml b/app/views/channels/_private_area_message.html.haml new file mode 100755 index 0000000..34d3fb2 --- /dev/null +++ b/app/views/channels/_private_area_message.html.haml @@ -0,0 +1,7 @@ +.private-channel-message + = image_tag("private_large.png", :alt => "This channel is private", :size => "117x110") + %p + This channel is private. + %br/ + %br/ + You need to request access to view any videos in this channel. \ No newline at end of file diff --git a/app/views/channels/_private_channel_link.html.haml b/app/views/channels/_private_channel_link.html.haml new file mode 100755 index 0000000..86c4d82 --- /dev/null +++ b/app/views/channels/_private_channel_link.html.haml @@ -0,0 +1,3 @@ +%a{:href => "mailto:?body=#{public_channel_url(:public_token => channel.public_token)}&subject=#{channel.title}"} + Want to share this private channel? You can give your friends or family this link: +%p{:class => defined?(with_margin) ? "mhxl" : ""}=public_channel_url(:public_token => channel.public_token) \ No newline at end of file diff --git a/app/views/channels/_subscriber.html.haml b/app/views/channels/_subscriber.html.haml new file mode 100755 index 0000000..437e0d5 --- /dev/null +++ b/app/views/channels/_subscriber.html.haml @@ -0,0 +1,12 @@ +%li.zebra-striped + = link_to(user_path(subscriber), :class => "user-image") do + = image_tag("#{subscriber.image.blank? ? 'default_user_50px.jpg' : subscriber.image_url(:medium_profile) }", + :alt => "#{subscriber.name}", + :size => "50x50") + + .user-meta + = link_to(subscriber.name, user_path(subscriber)) + + .buttons + = link_to("Remove", user_channel_remove_subscriber_path(@user, @channel, :user_id => subscriber.id), :class => "small btn remove-subscriber") + = link_to("Block", user_block_path(subscriber), :class => "block-user small btn") \ No newline at end of file diff --git a/app/views/channels/edit.html.haml b/app/views/channels/edit.html.haml new file mode 100755 index 0000000..88e94ff --- /dev/null +++ b/app/views/channels/edit.html.haml @@ -0,0 +1,82 @@ +- # User Banner Image += render 'users/top_banner' + +- # Middle User Info Section += render 'users/middle_user_banner' + +- # Dark Content Wrapper +.lower-wrapper + = render 'users/featured_videos' + + - # Main (White) Content Wrapper + .content-wrapper + = render 'shared/content_titles' + + .edit-channel-area + - unless @channel.featured? + %label{:for => "channel_title"} Name: + .channel-name + = form_tag(user_channel_path(@user, @channel), :id => "update-channel-name", :method => "PUT", :remote => true) do + %input{:name => "title", :type => "text", :maxlength => "30", :placeholder => "Give the channel a name", :value => @channel.title} + = submit_tag("Update", :class => "medium btn") + = image_tag("ajax.gif", :class => "ajax-animation", :size => "16x16") + = image_tag("green_checkmark.png", :class => "green-checkmark", :size => "16x16") + + + .channel-privacy + %label Channel is Private? + .toggles + = link_to(@channel.private? ? "Yes" : "No", user_channel_path(@user, @channel, :privacy => @channel.private? ? "false" : "true"), :id => "toggle-channel-privacy", :remote => true, :method => "PUT", :class => "medium btn #{@channel.private? ? 'success' : ''}") + + .informational-msg + %p + %span + %strong * + Making a channel private means that you must approve of everyone who wants to subscribe to this channel before + they can view any videos in it. + + .info-message.centered.private-channel-link{:style => @channel.private? ? "" : "display:none"} + = render :partial => "private_channel_link", :locals => { :channel => @channel } + + + .delete-channel-area + %label Delete Channel + %span + %a.show-delete-area{:href => "#"} Are you sure? + .alert-message.block-message.error + %p + %strong Warning... + Deleting this channel will remove: + %ul + %li All videos in the channel + %li All people who are currently subscribed to or collaborating on the channel + %li All comments and badges associated with the videos in the channel + .alert-actions + = link_to("I understand the consequences", user_channel_path, :class => "medium btn delete-channel") + + .collaboration + %label Manage Subscribers + + -#.informational-msg + -# %p + -# %span + -# %strong * + -# Making someone a collaborator will allow them to publish videos to this channel which will then be viewable by + -# anyone who is subscribed to this channel. + -# %strong Note: + -# If you want to make someone a collaborator, they MUST be subscribed to this channel before you can do so. + + - if @subscribers.blank? + .info-message + No one is currently subscribed to this channel. + + - else + %ul.subscriber-listing + = render :partial => 'subscriber', :collection => @subscribers + + = will_paginate @subscribers + + = render :partial => "shared/infinite_scrolling", :locals => { :item_selector => ".zebra-striped" } + +- # Tell the page to scroll down to the content wrapper upon page load += render 'shared/scroll_to_content_wrapper' \ No newline at end of file diff --git a/app/views/channels/edit.js.haml b/app/views/channels/edit.js.haml new file mode 100755 index 0000000..2dd2840 --- /dev/null +++ b/app/views/channels/edit.js.haml @@ -0,0 +1 @@ += render :partial => 'subscriber', :collection => @subscribers \ No newline at end of file diff --git a/app/views/channels/edit_featured_videos.html.haml b/app/views/channels/edit_featured_videos.html.haml new file mode 100755 index 0000000..ece9eed --- /dev/null +++ b/app/views/channels/edit_featured_videos.html.haml @@ -0,0 +1,27 @@ +- # User Banner Image += render 'users/top_banner' + +- # Middle User Info Section += render 'users/middle_user_banner' + +- # Dark Content Wrapper +.lower-wrapper + = render 'users/featured_videos' + + - # Main (White) Content Wrapper + .content-wrapper + = render 'shared/content_titles' + + - if @featured_videos.blank? + .info-message.mhl + You have not added any videos to your Featured Videos channel. Please add some and then come back to edit their order. + - else + = render :partial => 'channels/featured_video', :collection => @featured_videos + + = will_paginate @featured_videos + + = render :partial => "shared/infinite_scrolling", :locals => { :item_selector => ".featured-video-post" } + + +- # Tell the page to scroll down to the content wrapper upon page load += render 'shared/scroll_to_content_wrapper' \ No newline at end of file diff --git a/app/views/channels/edit_featured_videos.js.haml b/app/views/channels/edit_featured_videos.js.haml new file mode 100755 index 0000000..f546bec --- /dev/null +++ b/app/views/channels/edit_featured_videos.js.haml @@ -0,0 +1 @@ += render :partial => 'channels/featured_video', :collection => @featured_videos \ No newline at end of file diff --git a/app/views/channels/index.html.haml b/app/views/channels/index.html.haml new file mode 100755 index 0000000..e1e5d35 --- /dev/null +++ b/app/views/channels/index.html.haml @@ -0,0 +1,22 @@ +- # User Banner Image += render 'users/top_banner' + +- # Middle User Info Section += render 'users/middle_user_banner' + +- # Dark Content Wrapper +.lower-wrapper + = render 'users/featured_videos' + + - # Main (White) Content Wrapper + .content-wrapper + = render 'shared/content_titles' + + = render @channels + + = will_paginate @channels + + = render :partial => "shared/infinite_scrolling", :locals => { :item_selector => ".channel-wrapper" } + +- # Tell the page to scroll down to the content wrapper upon page load += render 'shared/scroll_to_content_wrapper' \ No newline at end of file diff --git a/app/views/channels/index.js.haml b/app/views/channels/index.js.haml new file mode 100755 index 0000000..a04d774 --- /dev/null +++ b/app/views/channels/index.js.haml @@ -0,0 +1 @@ += render @channels \ No newline at end of file diff --git a/app/views/channels/show.html.haml b/app/views/channels/show.html.haml new file mode 100755 index 0000000..fefc5b9 --- /dev/null +++ b/app/views/channels/show.html.haml @@ -0,0 +1,53 @@ +- # User Banner Image += render 'users/top_banner' + +- # Middle User Info Section += render 'users/middle_user_banner' + +- # Dark Content Wrapper +.lower-wrapper + = render 'users/featured_videos' + + - # Main (White) Content Wrapper + .content-wrapper + = render 'shared/content_titles' + + .info-message.mhl.clearfix.channel-show-info + %span.pull-left + Now viewing: #{@channel.title} + + .pull-right + - if current_user_owns?(@channel) + = link_to(edit_user_channel_path(@user, @channel), :class => "small btn") do + = image_tag("edit_icon.png", :size => "11x11") + Manage Channel + + - else + - if current_user.blank? + = link_to("Login to Subscribe", root_path, :class => "btn small") + - else + - if current_user.is_subscribed_to?(@channel) + = render :partial => "channels/button_unsubscribe", :locals => { :user => @user, :channel => @channel, :button_size => 'small' } + - else + - if @channel.private? + = render :partial => "channels/button_subscribe", :locals => { :user => @user, :channel => @channel, :is_private => true, :button_size => 'small' } + - else + = render :partial => "channels/button_subscribe", :locals => { :user => @user, :channel => @channel, :is_private => false, :button_size => 'small' } + + .clear + - if current_user_owns?(@channel) and @channel.private? + = render :partial => "private_channel_link", :locals => { :channel => @channel, :with_margin => true } + + + - if @viewing_via_token_access + %input{:name => "channel_access_token", :type => "hidden", :value => "#{@channel.public_token}"}/ + + - if @videos.blank? + .info-message.mhl + There are no videos in this channel to show. + - else + = render @videos + + = will_paginate @videos + + = render :partial => "shared/infinite_scrolling", :locals => { :item_selector => ".video-post" } \ No newline at end of file diff --git a/app/views/channels/show.js.haml b/app/views/channels/show.js.haml new file mode 100755 index 0000000..9ea505c --- /dev/null +++ b/app/views/channels/show.js.haml @@ -0,0 +1 @@ += render @videos \ No newline at end of file diff --git a/app/views/comments/_comment.html.haml b/app/views/comments/_comment.html.haml new file mode 100755 index 0000000..9d2e2e0 --- /dev/null +++ b/app/views/comments/_comment.html.haml @@ -0,0 +1,17 @@ +%li{:class => hidden_comment ? "hidden-comment" : "", "data-video-id" => video.id} + - comment_owner ||= get_object_owner(comment) + = link_to(user_path(comment_owner)) do + = image_tag("#{comment_owner.image.blank? ? 'default_user_35px.jpg' : comment_owner.image_url(:small_profile) }", + :alt => "#{comment_owner.name}", :size => "35x35") + + - # Generate link for deleting the comment + - if current_user_owns?(comment) || current_user_owns?(video) + - path_to_this = @viewing_via_token_access ? user_video_comment_path(video_owner, video, comment, :channel_token => video.channel.public_token) : user_video_comment_path(video_owner, video, comment) + = link_to("", path_to_this, :class => "delete-comment tooltip", :title => "Delete Comment", :remote => true, :method => "DELETE") + + .comment-meta + %a.comment-owner{:href => "#{user_path(comment_owner)}"} #{comment_owner.name} + %span.content + #{auto_link(h(comment.content), :html => {:target => "_blank" })} + %p.time-ago + #{time_ago_in_words(comment.created_at)} ago \ No newline at end of file diff --git a/app/views/comments/base.json.rabl b/app/views/comments/base.json.rabl new file mode 100755 index 0000000..c4e756a --- /dev/null +++ b/app/views/comments/base.json.rabl @@ -0,0 +1,8 @@ +attributes :id, :content +if highlight_latest_activity? + child(:video) { extends "videos/base" } +else + attributes :video_id + node(:created_at) { |c| c.created_at.to_i } +end +node(:from) { |c| partial("users/base", :object => User.find_by_id(c.user_id)) } \ No newline at end of file diff --git a/app/views/errors/error_404.html.haml b/app/views/errors/error_404.html.haml new file mode 100755 index 0000000..4999e28 --- /dev/null +++ b/app/views/errors/error_404.html.haml @@ -0,0 +1,16 @@ +- # Dark Content Wrapper +.lower-wrapper.no-header + + - # Main (White) Content Wrapper + .content-wrapper.standard-padding + .info-message.mtl + %h3 Error + + %p + We're sorry, but we were either unable to find that page or you do not have the proper permissions to view it. + %br + %br + = link_to "Go back", :back + + .okayguy + = image_tag("okayguy.png", :alt => "Okay", :size => "256x275") \ No newline at end of file diff --git a/app/views/errors/error_deactivated.html.haml b/app/views/errors/error_deactivated.html.haml new file mode 100755 index 0000000..b70b73a --- /dev/null +++ b/app/views/errors/error_deactivated.html.haml @@ -0,0 +1,21 @@ +- # Dark Content Wrapper +.lower-wrapper.no-header + + - # Main (White) Content Wrapper + .content-wrapper.standard-padding + .info-message.mtl + %h3 Error: Your account has been deactivated + + %p + We're sorry, but your account has been deactivated and is currently under review, possibly due to a + = link_to("Terms of Service", tos_path) + violation. + Typically we send an e-mail to your current e-mail address when we deactivate an account so please check your + inbox and junk-mail folders for that e-mail and more information. + %br + %br + If you feel this has been done in error or you have a question about this, please contact + #{mail_to "support@brevidy.com", "Support", :encode => "hex", :subject => "Account Deactivation"} + + .okayguy + = image_tag("okayguy.png", :alt => "Okay", :size => "256x275") \ No newline at end of file diff --git a/app/views/errors/error_old_browser.html.haml b/app/views/errors/error_old_browser.html.haml new file mode 100755 index 0000000..8633335 --- /dev/null +++ b/app/views/errors/error_old_browser.html.haml @@ -0,0 +1,40 @@ +- # Dark Content Wrapper +.lower-wrapper.no-header + + - # Main (White) Content Wrapper + .content-wrapper.standard-padding + .info-message.mtl + %h3 Error: Please upgrade your web browser + + %p + We're sorry, but you are using an old version of Internet Explorer (6, 7, 8) which is not supported on Brevidy. + %br/ + %br/ + To access Brevidy, install a modern web browser such as + #{link_to("Google Chrome", "http://www.google.com/chrome")}, + #{link_to("Mozilla Firefox", "http://www.mozilla.com/firefox")} or + #{link_to("Internet Explorer 9+", "http://ie.microsoft.com")} + + %div{:style => "margin: 20px 0"} + %a{:href => "http://www.google.com/chrome", :style => "margin: 0 110px; text-decoration: none"} + = image_tag("browsers/google_chrome_logo.png", :size => "100x100") + %a{:href => "http://www.mozilla.com/firefox", :style => "margin: 0 10px; text-decoration: none"} + = image_tag("browsers/firefox_logo.png", :size => "100x100") + %a{:href => "http://ie.microsoft.com", :style => "margin: 0 110px; text-decoration: none"} + = image_tag("browsers/ie9_logo.png", :size => "100x100") + + %p{:style => "border-bottom: 1px dotted gray"} + We apologize for any inconvenience this may have caused and appreciate your understanding! + %br/ + %br/ + Sincerely, + %br/ + The Brevidy Team + %br/ + %br/ + %p{:style => "color: gray"} + Internet Explorer and the logo shown are registered trademarks of Microsoft, Inc. + %br/ + Google Chrome and the logo shown are registered trademarks of Google, Inc. + %br/ + Mozilla Firefox and the logo shown are registered trademarks of Mozilla Corp. \ No newline at end of file diff --git a/app/views/errors/error_social_auth.html.haml b/app/views/errors/error_social_auth.html.haml new file mode 100755 index 0000000..ee5123b --- /dev/null +++ b/app/views/errors/error_social_auth.html.haml @@ -0,0 +1,16 @@ +- # Dark Content Wrapper +.lower-wrapper.no-header + + - # Main (White) Content Wrapper + .content-wrapper.standard-padding + .info-message.mtl + %h3 Error communicating w/ Facebook or Twitter + + %p + We're sorry, but there was an issue communicating with either Facebook or Twitter. Please try again in a few minutes! + %br + %br + = link_to "Go back", :back + + .okayguy + = image_tag("okayguy.png", :alt => "Okay", :size => "256x275") \ No newline at end of file diff --git a/app/views/errors/private_channel.html.haml b/app/views/errors/private_channel.html.haml new file mode 100755 index 0000000..af71948 --- /dev/null +++ b/app/views/errors/private_channel.html.haml @@ -0,0 +1,28 @@ +- # User Banner Image += render 'users/top_banner' + +- # Middle User Info Section += render 'users/middle_user_banner' + +- # Dark Content Wrapper +.lower-wrapper + = render 'users/featured_videos' + + - # Main (White) Content Wrapper + .content-wrapper + = render 'shared/content_titles' + + .private-channel-error + = image_tag("private_large.png", :alt => "This channel is private", :size => "117x110") + + .message-area + %h4 This channel is private. + %p You need to request access to view any videos in this channel. + + - if signed_in? + = link_to("Request Access", user_channel_subscribe_path(@user, @channel), :class => "subscribe btn large", :remote => true, :method => "POST") + - else + #{link_to("Sign up", signup_path)} or #{link_to("Login", login_path)} to request access. + +- # Tell the page to scroll down to the content wrapper upon page load += render 'shared/scroll_to_content_wrapper' \ No newline at end of file diff --git a/app/views/invitation_link/index.html.haml b/app/views/invitation_link/index.html.haml new file mode 100755 index 0000000..9988c79 --- /dev/null +++ b/app/views/invitation_link/index.html.haml @@ -0,0 +1,54 @@ +- # User Banner Image += render 'users/top_banner' + +- # Middle User Info Section += render 'users/middle_user_banner' + +- # Dark Content Wrapper +.lower-wrapper + = render 'users/featured_videos' + + - # Main (White) Content Wrapper + .content-wrapper + - invitation_link = current_user.invitation_link + - invitation_url = signup_via_invitation_url(:invitation_token => invitation_link.token) + + .invitations-area + %h3.content-wrapper-title Invite other people to join Brevidy + + %h4 + %strong Option A: + Share it yourself! + .share-it-yourself + %p + Send your friends this invitation link + %input{:type => "text", :readonly => true, :value => invitation_url} + %p.social-area + Or share it to your social networks! + / AddThis Button BEGIN + .addthis_toolbox.addthis_default_style.addthis_32x32_style{"addthis:url" => invitation_url, "addthis:title" => "I have some invites to Brevidy.com, a place to watch and share videos!" } + %a.addthis_button_facebook + %a.addthis_button_twitter + %a.addthis_button_email + %a.addthis_button_compact + / AddThis Button END + + %h4.email-area + %strong Option B: + Send them an email asking them to join (we'll include the link in the email) + + = form_tag(user_invite_people_path(current_user, @invitation), :class => "send-invites", :remote => true, :method => "POST") do + %label + Email addresses to invite: + = text_area_tag(:recipient_email, nil, :autocomplete => :off, + :placeholder => '"Smith, John" , averagejoe@brevidy.com, ') + + %label Include a personal message to send with the invitation: + = text_area_tag(:personal_message, nil, :autocomplete => :off, :maxlength => "250", + :placeholder => "(Optional) Personalize your invitation by including a short message" ) + %label + %input.primary.medium.btn{:type => "submit", :value => "Send Invites"} + + +- # Tell the page to scroll down to the content wrapper upon page load += render 'shared/scroll_to_content_wrapper' \ 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 100755 index 0000000..0a5e3c7 --- /dev/null +++ b/app/views/layouts/application.html.haml @@ -0,0 +1,81 @@ +!!! +%html{:lang => "en", "xml:lang" => "en", :xmlns => "http://www.w3.org/1999/xhtml"} + %head + %meta{:content => "chrome=1", "http-equiv" => "X-UA-Compatible"}/ + %meta{:content => "text/html; charset=utf-8", "http-equiv" => "Content-Type"} + %meta{:content => "en", "http-equiv" => "Content-Language"} + %meta{:content => "noodp,noydir", :name => "robots"}/ + %meta{:name => "keywords", :content => "brevidy,brevity,video,social,social network,networking,life,capture,camera,video camera,mobile camera,mobile,videography,people,connect,relationships"}/ + + - if we_should_show_og_tags && !@video.blank? + - # Facebook OpenGraph protocol for embedding our video link back to Brevidy + %meta{:property => "fb:app_id", :content => "178344348908217" }/ + %meta{:property => "og:url", :content => public_video_url(:public_token => @video.public_token) }/ + %meta{:property => "og:title", :content => @video.title }/ + %meta{:property => "og:description", :content => @video.description }/ + %meta{:property => "og:type", :content => "video" }/ + %meta{:property => "og:image", :content => @video.get_thumbnail_url(@video.selected_thumbnail) }/ + %meta{:property => "og:video", :content => public_video_url(:public_token => @video.public_token) }/ + %meta{:property => "og:video:type", :content => "application/x-shockwave-flash" }/ + %meta{:property => "og:video:width", :content => "398" }/ + %meta{:property => "og:video:height", :content => "224" }/ + %meta{:property => "og:site_name", :content => "Brevidy"}/ + - else + - # Standard meta tags + %meta{:property => "fb:page_id", :content => "247345075276137"}/ + %meta{:name => "description", :content => "Brevidy allows you to capture the fleeting moments of your life on video and share them with those around you."}/ + %link{:rel => "image_src", :href => "#{image_path('meta_tag_logo.png')}"}/ + + / + \.b. + \.b. .e.e.e. .v v. . .d.d. .y. .y. + \.b.b.b. .r.r.r. .e .v v. .d. .d y y + \.b. .b. .r. .e.e.e. .v v. .i. .d. d .y. + \.b. .b .r. .e .v v. .i. .d. .d .y. + \.b.b.b. .r. .e.e.e. .v. .i. .d.d. .y. + \ + come help us create something amazing: jobs@brevidy.com + + %title + = browser_title + + - # Logged In CSS + %link{:href => "#{cache_buster_path('/stylesheets/i_love_lamp-1.0.3.min.css')}", :media => "screen", :rel => "stylesheet", :type => "text/css"}/ + + - # Site-wide JS + = javascript_include_tag "https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" + %script{:src => "#{cache_buster_path('/javascripts/functions.min.js')}", :type => "text/javascript"} + %script{:src => "#{cache_buster_path('/javascripts/i_love_lamp-1.0.3.min.js')}", :type => "text/javascript"} + = javascript_include_tag "player/player.js" + + /[if lt IE 9] + = javascript_include_tag "http://html5shiv.googlecode.com/svn/trunk/html5.js" + + - # Fav Icon and CSRF meta tag + = favicon_link_tag + = csrf_meta_tag + + %body#top{:class => get_background_for_user } + + - # Top navigation header + = render 'shared/header' + + .container + + - # Main container + .middle-container + = yield + + - # Lower navigation + - unless @page_has_infinite_scrolling + .lower-container + = render 'shared/lower_navigation' + + - # GetSatisfaction Feedback Widget & Google Analytics + = render :partial => 'shared/extras' + + - # Scroll back to top + %p#back-to-top + %a{:href => "#top"} + %i + %span BACK TO TOP \ No newline at end of file diff --git a/app/views/layouts/empty.html.haml b/app/views/layouts/empty.html.haml new file mode 100755 index 0000000..845876e --- /dev/null +++ b/app/views/layouts/empty.html.haml @@ -0,0 +1,61 @@ +!!! +%html{:lang => "en", "xml:lang" => "en", :xmlns => "http://www.w3.org/1999/xhtml"} + %head + %meta{:content => "chrome=1", "http-equiv" => "X-UA-Compatible"}/ + %meta{:content => "text/html; charset=utf-8", "http-equiv" => "Content-Type"} + %meta{:content => "en", "http-equiv" => "Content-Language"} + %meta{:content => "noodp,noydir", :name => "robots"}/ + %meta{:name => "description", :content => "Brevidy allows you to capture the fleeting moments of your life on video and share them with those around you."}/ + %meta{:name => "keywords", :content => "brevidy,brevity,video,social,social network,networking,life,capture,camera,video camera,mobile camera,mobile,videography,people,connect,relationships"}/ + %link{:rel => "image_src", :href => "#{image_path('meta_tag_logo.png')}"}/ + + - # Facebook OpenGraph protocol for embedding our video link back to Brevidy + %meta{:property => "fb:app_id", :content => "178344348908217" }/ + %meta{:property => "og:url", :content => public_video_url(:public_token => @video.public_token) }/ + %meta{:property => "og:title", :content => @video.title }/ + %meta{:property => "og:description", :content => @video.description }/ + %meta{:property => "og:type", :content => "video" }/ + %meta{:property => "og:image", :content => @video.get_thumbnail_url(@video.selected_thumbnail) }/ + %meta{:property => "og:video", :content => public_video_url(:public_token => @video.public_token) }/ + %meta{:property => "og:video:type", :content => "application/x-shockwave-flash" }/ + %meta{:property => "og:video:width", :content => "398" }/ + %meta{:property => "og:video:height", :content => "224" }/ + %meta{:property => "og:site_name", :content => "Brevidy"}/ + + + / + \.b. + \.b. .e.e.e. .v v. . .d.d. .y. .y. + \.b.b.b. .r.r.r. .e .v v. .d. .d y y + \.b. .b. .r. .e.e.e. .v v. .i. .d. d .y. + \.b. .b .r. .e .v v. .i. .d. .d .y. + \.b.b.b. .r. .e.e.e. .v. .i. .d.d. .y. + \ + come help us create something amazing: jobs@brevidy.com + + %title + = browser_title + + :css + html, body { + line-height: 0px; + margin: 0px; + padding: 0px; + height: 100%; + width: 100%; + overflow: hidden; + } + body { + background-color: #000000; + } + + - # Site-wide JS + = javascript_include_tag "player/player.js" + + - # Fav Icon and CSRF meta tag + = favicon_link_tag + = csrf_meta_tag + + %body + + = yield \ No newline at end of file diff --git a/app/views/layouts/signed_out.html.haml b/app/views/layouts/signed_out.html.haml new file mode 100755 index 0000000..6ae288c --- /dev/null +++ b/app/views/layouts/signed_out.html.haml @@ -0,0 +1,69 @@ +!!! +%html{:lang => "en", "xml:lang" => "en", :xmlns => "http://www.w3.org/1999/xhtml"} + %head + %meta{:content => "chrome=1", "http-equiv" => "X-UA-Compatible"}/ + %meta{:content => "text/html; charset=utf-8", "http-equiv" => "Content-Type"} + %meta{:content => "en", "http-equiv" => "Content-Language"} + %meta{:content => "noodp,noydir", :name => "robots"}/ + + %meta{:property => "fb:page_id", :content => "247345075276137"}/ + %meta{:name => "description", :content => "Brevidy allows you to capture the fleeting moments of your life on video and share them with those around you."}/ + %meta{:name => "keywords", :content => "brevidy,brevity,video,social,social network,networking,life,capture,camera,video camera,mobile camera,mobile,videography,people,connect,relationships"}/ + %link{:rel => "image_src", :href => "#{image_path('meta_tag_logo.png')}"}/ + + / + \.b. + \.b. .e.e.e. .v v. . .d.d. .y. .y. + \.b.b.b. .r.r.r. .e .v v. .d. .d y y + \.b. .b. .r. .e.e.e. .v v. .i. .d. d .y. + \.b. .b .r. .e .v v. .i. .d. .d .y. + \.b.b.b. .r. .e.e.e. .v. .i. .d.d. .y. + \ + come help us create something amazing: jobs@brevidy.com + + %title + = browser_title + + - # Site-wide CSS + %link{:href => "#{cache_buster_path('/stylesheets/gotsoul_landing.css')}", :media => "screen", :rel => "stylesheet", :type => "text/css"}/ + + - # Site-wide JS + = javascript_include_tag "https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" + %script{:src => "#{cache_buster_path('/javascripts/functions.min.js')}", :type => "text/javascript"} + %script{:src => "#{cache_buster_path('/javascripts/i_love_lamp-1.0.3.min.js')}", :type => "text/javascript"} + + /[if lt IE 9] + = javascript_include_tag "http://html5shiv.googlecode.com/svn/trunk/html5.js" + + - # Fav Icon and CSRF meta tag + = favicon_link_tag + = csrf_meta_tag + + %body + .page_container + .center_container.rbxl.mhauto.clearfix + / banner + .banner_container + = link_to(image_tag("rgb_logo.png", :size => "184x90"), root_path, :class => "no_border rgb_logo") + + = link_to(explore_path, :class => "no_border") do + .go_explore.ram.right + %h3 + Go Explore! + + - unless current_page?(:login) || signed_in? + = link_to(login_path, :class => "no_border") do + .login_form.ram.right + %h3 + Already a member? Login! + + .banner_corners + = image_tag("banner_corners.png", :size => "880x15") + + = yield + + .lower_container + = render 'shared/lower_navigation' + + - # GetSatisfaction Feedback Widget & Google Analytics + = render :partial => 'shared/extras' \ No newline at end of file diff --git a/app/views/profile/base.json.rabl b/app/views/profile/base.json.rabl new file mode 100755 index 0000000..3a6f218 --- /dev/null +++ b/app/views/profile/base.json.rabl @@ -0,0 +1,3 @@ +attributes :bio, :interests, :favorite_music, :favorite_movies, :favorite_books, + :favorite_foods, :favorite_people, :things_i_could_live_without, + :one_thing_i_would_change_in_the_world, :quotes_to_live_by \ No newline at end of file diff --git a/app/views/profile/index.html.haml b/app/views/profile/index.html.haml new file mode 100755 index 0000000..ad974f1 --- /dev/null +++ b/app/views/profile/index.html.haml @@ -0,0 +1,65 @@ +- # Initialize a constant telling the My Stuff header what page it's on +- @profile_page = true +- # Initialize the categories +- @categories = { :website => { :name => "Website", :maxlength => "250" }, + :bio => { :name => "About yourself in 140 characters or less", :maxlength => "140" }, + :interests => { :name => "Interests", :maxlength => "1000" }, + :favorite_music => { :name => "Favorite Music", :maxlength => "1000" }, + :favorite_movies => { :name => "Favorite Movies", :maxlength => "1000" }, + :favorite_books => { :name => "Favorite Books", :maxlength => "1000" }, + :favorite_foods => { :name => "Favorite Foods", :maxlength => "1000" }, + :favorite_people => { :name => "Favorite People", :maxlength => "1000" }, + :things_i_could_live_without => { :name => "Things I don't like", :maxlength => "1000" }, + :one_thing_i_would_change_in_the_world => { :name => "One thing I would change in the world", :maxlength => "1000" }, + :quotes_to_live_by => { :name => "Quotes to live by", :maxlength => "3000" } } + +- content_for :profile_edit_buttons do + - unless current_user.blank? + - if current_user?(@user) + .profile-buttons.pull-right + = image_tag("ajax.gif", :class=>"ajax-animation", :size => "16x16") + %a#edit-profile.small.btn{:href => "#"} + = image_tag("edit_icon.png", :size => "11x11") + Edit Profile + = link_to("Save Profile", user_update_about_path(@user, @profile), :id => "save-profile", :class => "small primary btn") + %a#cancel-profile.small.btn{:href => "#"} Cancel + +- # User Banner Image += render 'users/top_banner' + +- # Middle User Info Section += render 'users/middle_user_banner' + +- # Dark Content Wrapper +.lower-wrapper + = render 'users/featured_videos' + + - # Main (White) Content Wrapper + .content-wrapper + = render 'shared/content_titles' + + .profile-content + - @categories.each_with_index do |value, index| + %h3{:class => "#{value[0]}"} + #{value[1][:name]} + + -if index == 0 + = content_for(:profile_edit_buttons) + + %div{:class => "#{value[0]}"} + - category ||= @user.profile.attributes["#{value[0]}"] + - if category.blank? + %p None + - else + = simple_format(auto_link(h(category), :html => { :target => "_blank" }), {}, :sanitize => false) + + - # Hide an editing textarea if the user can edit the about page + - unless current_user.blank? + - if current_user?(@user) + %textarea{:maxlength => "#{value[1][:maxlength]}", :placeholder => "", :class => "#{value[0]}"} + - if category.blank? + None + - else + - # use the preserve filter to preserve whitespace and newlines of original text + :preserve + #{category} \ No newline at end of file diff --git a/app/views/profile/index.json.rabl b/app/views/profile/index.json.rabl new file mode 100755 index 0000000..39d63d0 --- /dev/null +++ b/app/views/profile/index.json.rabl @@ -0,0 +1,6 @@ +object @user +# show the user data for whoever's page we're on +extends "users/base" + +# show profile data for the page +code(:profile) { partial("profile/base", :object => @profile) } \ No newline at end of file diff --git a/app/views/public/contact.html.haml b/app/views/public/contact.html.haml new file mode 100755 index 0000000..6a54d50 --- /dev/null +++ b/app/views/public/contact.html.haml @@ -0,0 +1,42 @@ +- # Dark Content Wrapper +.lower-wrapper.no-header + + - # Main (White) Content Wrapper + .content-wrapper.standard-padding + + = cache("contact_page") do + %ul.unstyled.mtl + %li + %h4 For all contact requests, please allow up to 24 hours for a reply. + + %ul.unstyled + %li + %h4 Do you need help? + %li + Please visit our #{link_to("community page", "http://getsatisfaction.com/brevidy", :target => "_blank")} + for help topics. If you need additional help, e-mail us at + #{mail_to("support@brevidy.com", nil, {:encode => "hex"} )} + + %ul.unstyled + %li + %h4 Are you interested in investing in us? + %li + We are open to discussions with professional investors. If you are interested, please send us a message at + #{mail_to("investors@brevidy.com", nil, {:encode => "hex"} )} for more info. + + %ul.unstyled + %li + %h4 Are you writing a story about us? + %li + For all media inquiries, please send us a message at + #{mail_to("media@brevidy.com", nil, {:encode => "hex"} )}. + + %ul.unstyled + %li + %h4 Are you interested in advertising with us? + %li + Please send an e-mail to #{mail_to("advertising@brevidy.com", nil, {:encode => "hex"} )} + + + + diff --git a/app/views/public/faq.html.haml b/app/views/public/faq.html.haml new file mode 100755 index 0000000..400463c --- /dev/null +++ b/app/views/public/faq.html.haml @@ -0,0 +1,99 @@ +- # Dark Content Wrapper +.lower-wrapper.no-header + + - # Main (White) Content Wrapper + .content-wrapper.standard-padding + + = cache("faq_page") do + %ul.unstyled.mtl + %li + %h4 What’s wrong with video sharing websites today? + %li + Simply put: they care too much about the video itself and not enough about the people in the video.   + %br/ + %br/ + Videos inherently connect with us on a deeper, more emotional level.  They have the power to move + you to tears, to inspire you, to make you laugh, and to bring people together. + However, up until this point, videos on the Internet have been distributed as a "consumable product"  + where you view the video, and then you move on without giving a second thought to the person + behind the video. Brevidy aims to change the status quo by connecting the people who are sharing + the video with the people who are watching it. + + %ul.unstyled + %li + %h4 What is Brevidy? + %li + Brevidy is a video social network. We want to use videos to connect people. We want to allow you + to share an intimate moment with your family, a laugh with your friends, or a window into your life + with someone halfway around the world. We want to put the soul back into video: You! + + %ul.unstyled + %li + %h4 What's with the name? + %li + The word "brevity" is defined as + %span.tb the quality of being brief in duration. + Brevidy provides you a platform to capture the fleeting moments of your life and share them with + those around you. + %br/ + %span{:style => "font-size: 11px; color: gray"} + Psssst... We spelled it wrong on purpose ;) + + %ul.unstyled + %li + %h4 Where can I get some general help, leave feedback, or submit an idea? + %li + Please visit our #{link_to("community page", "http://getsatisfaction.com/brevidy", :target => "_blank")} + to ask a question, leave feedback, submit an idea, or view other help topics. Alternatively, you can click the black "Feedback" + tab that is on the left side of this window. + + %ul.unstyled + %li + %h4 Where can I find more help specific to videos? + %li + Please refer to the #{link_to("frequently asked questions for videos", video_faq_path)}. + + %ul.unstyled + %li + %h4 Who can see my videos? + %li + It depends on what kind of channel you put the video in: + %ul + %li + %strong Featured Channel - + All videos are public (anyone visiting Brevidy can see them) + %li + %strong Public Channel - + All videos are public (anyone visiting Brevidy can see them) + %li + %strong Private Channel - + All videos are private (only people you approve can see them) + + %ul.unstyled + %li + %h4 When are you going to make an iPhone/Android app? + %li + Creating a mobile app to support Brevidy is currently on our roadmap. However, our top priority + is ensuring that our web-based interface provides a polished user-experience first. + + %ul.unstyled + %li + %h4 Are you hiring? + %li + We are + %strong always + looking for talented, driven, and focused Ruby on Rails, jQuery, iPhone, and Android developers + who want startup experience. If you think you're up for the challenge, shoot us a message at + #{mail_to("jobs@brevidy.com", nil, {:encode => "hex"} )} + %br/ + %br/ + %i + "Wanting something is not enough. You must hunger for it. Your motivation must be absolutely + compelling in order to overcome the obstacles that will invariably come your way." -- Les Brown + + %ul.unstyled + %li + %strong Note to Investors: + We are open to discussions with professional investors or business partners. If you are interested, + please contact us at #{mail_to("investors@brevidy.com", nil, {:encode => "hex"} )} + for more information. \ No newline at end of file diff --git a/app/views/public/privacy.html.haml b/app/views/public/privacy.html.haml new file mode 100755 index 0000000..f6f10f6 --- /dev/null +++ b/app/views/public/privacy.html.haml @@ -0,0 +1,151 @@ +- # Dark Content Wrapper +.lower-wrapper.no-header + + - # Main (White) Content Wrapper + .content-wrapper.standard-padding + + = cache("privacy_page") do + + %p.gray.mtl Last Updated: January 24, 2012 + + %p + %strong + This privacy policy describes how we collect and use personal data provided by our users. If you have any questions or comments regarding our privacy policy, please contact us at + #{mail_to("support@brevidy.com", nil, {:encode => "hex"})} + + %ul.unstyled + %li + %h4 Changes to the Terms of Service + %li + %p + If we make a material change in the way we use the personal data of our users, we will notify you by making the revised version available on this page. You should revisit this Privacy Policy on a regular basis as revised versions will be binding on you.. Your use of the website after any published change signifies your consent to the application of the revised policy to all information contributed before or after the date of those changes. + + + %ul.unstyled + %li + %h4 User Account Data + %li + %p + Anyone over the age of 13 may create a user account. You need to have a user account to post videos, comment on videos, badge videos, and perform other actions on the site. When you sign up for a user account, you are required to provide: + %br + %ul + %li + %strong + username - + we use this to identify you and the content you create + %li + %strong + full name - + we use this to identify you by name in communications and to allow other users to search for you on Brevidy by name + %li + %strong + email address - + we use this address to communicate service announcements and important updates about the site, which you consent to receive, and to allow others to search for you on Brevidy by this address + %li + %strong + password - + we use this for authentication of your account and recommend you create a strong password that is unique only to Brevidy + %li + %strong + birthday - + we use this for age authentication only in order to verify you are over 13 years of age + %p.mtm + If you are logged in, any content you post will be identified under your username. + + %ul.unstyled + %li + %h4 Community Features + %li + %strong User Profiles + %br/ + Once you have a user account, you may voluntarily provide other information to enhance your profile, including a photo or image, interests, etc. This information will be disclosed publicly on the site under your profile. You are not required to provide this information. We offer users the opportunity to provide this profile information in order to enhance the richness of our online community. We may use the information you provide us in your profile to customize your experience by showing you content relevant to your areas of interest or location. + %br/ + %br/ + %strong Your Videos or Comments + %br + It goes without saying that any personally identifiable information that you submit on any video, comment or elsewhere on the site can be read, collected, or used by other users of the site and could be used to send you unsolicited messages. We are not responsible for the personally identifiable information you choose to submit to the website and we strongly recommend not giving out any additional information to others. + + %ul.unstyled + %li + %h4 Cookies and Web Logs + %li + When you log into the site, we may set a persistent cookie, which will allow us to recognize you as an existing registered user and avoid the need to log into the site again. These cookies may last for over a year. You may clear these cookies in your browser after a session in which you have logged in or they are automatically deleted when you explicitly Logout from Brevidy. + %br/ + %br/ + We may also set additional cookies to record certain preferences or actions on the site. You may clear these cookies in your browser at any time. + %br/ + %br/ + Our servers automatically record information that your browser sends whenever you visit a website. We store this data in files called web logs. These web logs may include information such as your Internet Protocol address, browser type, browser language, the site you came from, the next site you visit and cookies that may uniquely identify your browser. + + %ul.unstyled + %li + %h4 Advertisers' Cookies + %li + We may use third-party advertising companies to serve ads when you visit our site. When you view ads on our site, a unique third-party cookie or cookies may be placed on your browser by the ad-serving company. In addition, the ad-serving companies may use web beacons to help manage and optimize our online advertising. The information collected by the ad-serving companies may be used to target ads to you on this site and on other sites that you visit. To "opt-out" of these ad-serving cookies and web beacons, please click here: #{link_to(nil, "http://www.networkadvertising.org/consumer/opt_out.asp", :class => "inlinelink")} + + %ul.unstyled + %li + %h4 Information for Contests and Sweepstakes + %li + If we offer a contest or sweepstakes, we may ask for additional information such as name, address, and phone number in order to provide the winner with their prize. We would not use that information for any other purpose unless we receive consent at the time you provide the information. + + %ul.unstyled + %li + %h4 Special Rules for Children + %li + Children under the age of 13 are not eligible to become registered users. If you are under 18, we also ask that you not include any personally identifiable information in your posts or communication on the site. + + %ul.unstyled + %li + %h4 Information Sharing + %li + %strong Third Parties + %br/ + We will never provide your personally identifiable information to any third parties without your consent, except as described under the "Our Service Providers" and "Investigating Potential Abuse" sections. We may provide third parties with aggregated or anonymous data about our users and their usage of our services that do not contain any individual's personally identifiable information. + %br/ + %br/ + %strong Our Service Providers + %br + We may hire third-parties to help us provide our services to you. These services may include technical services, customer support, email message deployment, data processing and other services. It may be necessary for us to provide these third parties with some of your personal information in order for them to fulfill their services. For example, we use a third party (SendGrid) to send our e-mails, so we will provide them your email address and your first and last name as necessary to provide you with communications from Brevidy. These third parties do not retain, share or store any personally identifiable information except to provide these services and are bound by strict confidentiality agreements which limit their use of such information to providing services to us. + + %ul.unstyled + %li + %h4 Links to Other Sites + %li + This web site contains links to other sites that are not owned or controlled by us, including links in advertisements. Please be aware that we are not responsible for the privacy practices of these sites. We encourage you to be aware when you leave our site and to read the privacy statements of each and every web site to which you provide information. + + %ul.unstyled + %li + %h4 Investigating Potential Abuse + %li + We may use any information provided to us by a user, including personally identifiable information, along with any data gathered from our web logs and from third parties to conduct investigations, and we may disclose this information to third parties, including law enforcement officials, investigators, or private parties seeking to protect and enforce their rights; if + %br/ + %br/ + %ul + %li + we have a good faith belief that it is necessary to comply with a court order, judicial proceeding, legal process, subpoena or warrant; + %li + we have a good faith belief that it is necessary to exercise our legal rights or defend against legal claims; or + %li + we believe that your actions violate the law or our Terms of Service or threaten the rights, property, or safety of our company, our users, or others. + + %ul.unstyled + %li + %h4 Business Transfer + %li + If we sell all or a portion of our business, we may transfer some or all of your information as a part of the sale. + + %ul.unstyled + %li + %h4 Accessing and Changing Your Account Information + %li + You can review your personal information and make any desired changes at any time by logging in to your account and editing the information on your profile or account page. We may retain certain data contributed by you if it may be necessary to prevent fraud or future abuse, or for legitimate business purposes, such as analysis of aggregated, non-personally-identifiable data, account recovery, or if required by law. All retained data will continue to be subject to the terms of our Privacy Policy. + + %ul.unstyled + %li + %h4 Security + %li + In order to secure your personal information, access to your user account from the website is password-protected. While we take security very seriously, no data transmission over the Internet or information storage technology is 100% secure. If you suspect that your Brevidy account has been compromised, please email us immediately at #{mail_to("support@brevidy.com", nil, {:encode => "hex", :class => "inlinelink"} )}. + %br/ + %br/ + We disclaim any responsibility for inaccuracies on the site. We are not responsible for any loss, harm, error or omission that may result from the use of the site or the services we provide or for any service failure, interruption, or security breach, regardless of the cause. \ No newline at end of file diff --git a/app/views/public/tos.html.haml b/app/views/public/tos.html.haml new file mode 100755 index 0000000..9aa30af --- /dev/null +++ b/app/views/public/tos.html.haml @@ -0,0 +1,258 @@ +- # Content Wrapper +.lower-wrapper.no-header + + - # Main (White) Content Wrapper + .content-wrapper.standard-padding + + = cache("tos_page") do + + %p.gray.mtl Last Updated: January 24, 2012 + + %p + %strong + This is an important document which you must consider carefully when choosing whether to use the Brevidy website at any time. In order to make sure that no one's use of the site harms or violates the rights of others, your use of the site is governed by these Terms and Conditions (the "Terms"), as well as by Brevidy's Privacy Policy: + + %ul.unstyled + %li + %h4 Changes to the Terms of Service + %li + %p + We may modify the Terms of Service from time to time. When changes are made, we will notify you by making the revised version available on this page. You should revisit these Terms of Service on a regular basis as revised versions will be binding on you. + + %ul.unstyled + %li + %h4 User Accounts + %li + %p + Anyone over the age of 13 may create a user account. You need to have a user account to post videos, comment on videos, badge videos, and perform other actions on the site. + %br + %br + All of the information that you supply to us in creating your user account must be accurate. + You are responsible for maintaining the confidentiality of your account and password. + %br + %br + We reserve the right at all times to terminate users or reclaim usernames. We may restrict and / or reclaim, without warning, any username that violates these Terms and Conditions, including any username that uses + another person's identity, violates a trademark, or violates our content guidelines. + %br + %br + We may use the email you provide to us in your user account to provide you with service messages and updates. By becoming a site member you are consenting to the receipt of these communications. + + %ul.unstyled + %li + %h4 Content You Post on Brevidy + %li + %p + You are responsible for all content that you post on or transmit through the site. You may not post content that: + %br + %br + %ul + %li + %p + infringes the copyright, trademark, patent right or other proprietary right of any person or that is used without the permission of the owner + %li + %p + you know to be inaccurate + %li + %p + is pornographic, sexually explicit or obscene + %li + %p + exploits children or minors + %li + %p + violates the rights of privacy or publicity of any person + %li + %p + is libelous, slanderous or defamatory + %li + %p + contains any personally identifying information about any person without their consent or about any person who is a minor + %li + %p + may be deemed generally offensive to the site community, including blatant expressions of bigotry, prejudice, racism, hatred or profanity + %li + %p + promotes or provides instructional information about illegal or illicit activities or + %li + %p + contains software viruses or any other computer code, files or programs designed to destroy, interrupt or otherwise limit the functionality of any computer software, computer hardware or other equipment + %br + %p + We may remove any content that violates these Terms and Conditions or that we determine is otherwise not appropriate for the site. + %br + %br + When you post or transmit content on or through the site, you grant Brevidy and our affiliates and partners a nonexclusive, perpetual, irrevocable, worldwide, sub-licensable, royalty-free license to use, store, display, publish, transmit, transfer, distribute, reproduce, rearrange, edit, modify, aggregate, create derivative works of and publicly perform the content that you submit to the site for any purpose, in any form, medium, or technology now known or later developed. You also grant us a license to use your name, city and state in connection with our use of any public content you provide to us. You also consent to the display of advertising within or adjacent to any of your content. + + %ul.unstyled + %li + %h4 Your Use of Brevidy + %li + %p + All of the content available through the site is protected by our copyrights or trademarks and the copyrights or trademarks of our partners and/or users. You may not use, store, display, publish, transmit, distribute, modify, reproduce, create derivative works of or in any way exploit any of this content, in whole or in part, outside of the specific usage rights granted to you by Brevidy as part of the services we provide. + %br + %br + You may not use the site to do any of the following: + %ul + %li + %p + harass or advocate harassment of another person or entity + %li + %p + perform any activities that violate any state, local, federal US laws, or international laws or regulations + %li + %p + provide resources to or otherwise support any organization(s) designated by the United States government as a foreign terrorist organization under section 219 of the Immigration and Nationality Act + %li + %p + impersonate any person or entity or misrepresent in any way your affiliation with a person or entity + %li + %p + transmit unsolicited mass mailings or 'spam' + %li + %p + collect or store any information about other users or members, other than in the normal course of using the site for its intended purpose of facilitating voluntary communication among users + %li + %p + transmit any virus, worm, defect, Trojan horse or similar destructive or harmful item + %br + %p + You may not do any of the following to the site: + %ul + %li + %p + use it in any manner that could damage, disable, overburden, disrupt or impair the site or our network or interfere with any other party's use and enjoyment of the site + %li + %p + modify, adapt, translate or reverse engineer the site + %li + %p + attempt to circumvent privacy controls or any type of authentication system + %li + %p + use any robot, spider, site search/retrieval application, or other device to retrieve or index any portion of the site (though Brevidy grants the operators of public search engines permission to use spiders to copy materials from the site for the sole purpose of creating publicly available searchable indices of the content on the site that link back to the site for the full text of the content) + %li + %p + frame the site or reformat it in any way or + %li + %p + create user accounts using any automated means or under false pretenses. + %br + %p + All of the content available through the site is subject to the copyrights and trademarks of us, our partners or users. + %br + %br + Except as described in the previous paragraph, you may not store, display, publish, transmit, distribute, modify, reproduce, create derivative works of or in any way exploit any of the site content, in whole or in part. + + %ul.unstyled + %li + %h4 Third-Party Sites, Products, and Services + %li + %p + The site may provide access and contain links to third party Internet sites and services. Your use of those sites and services is subject to the terms and conditions of those sites. We are not responsible for the activities of any third parties site. + + %ul.unstyled + %li + %h4 Embeddable Items + %li + %p + Brevidy may make available videos, graphics or other items that you can embed on your own website. If you use any of these embeddable items, you must include a prominent link back to the site on the pages containing the embeddable item and you may not modify, build upon, or block any portion of the embeddable item in anyway or present it in any manner that is false or misleading. + + %ul.unstyled + %li + %h4 Links + %li + %p + Brevidy has not reviewed all of the sites (both internally and externally) linked to its Internet web site and is not responsible for the contents of any such linked site. The inclusion of any link does not imply endorsement by Brevidy of the site. Use of any such linked web site is at the user's own risk. + + %ul.unstyled + %li + %h4 Intellectual Property Rights Policy + %li + %p + Brevidy respects the rights of intellectual property holders. If you believe that any content on the site violates these Terms and Conditions or your intellectual property rights, you can report such violation to us. In the case of an alleged infringement, please provide the following information: + %br + %br + %ul + %li + %p + A description of the copyrighted work or other intellectual property that you claim has been infringed + %li + %p + A description of where the material that you claim is infringing is located on the site (including the exact URL) + %li + %p + An address, a telephone number, and an e-mail address where we can contact you + %li + %p + A statement that you have a good-faith belief that the use is not authorized by the copyright or other intellectual property rights owner, by its agent, or by law + %li + %p + A statement by you under penalty of perjury that the information in your notice is accurate and that you are the copyright or intellectual property owner or are authorized to act on the owner's behalf + %li + %p + Your electronic or physical signature + %br + %p + We may request additional information before we remove allegedly infringing material. You may report a copyright violation by providing the above information to the Brevidy agent at + #{mail_to("support@brevidy.com", nil, {:encode => "hex"})} + %br + %br + We will terminate the user account of any user who repeatedly submits content that violates our intellectual property policies. + + %ul.unstyled + %li + %h4 Our Rights and Responsibilities + %li + %p + We maintain the right to do any of the following at any time, with or without prior notice: + %br + %br + %ul + %li + %p + restrict, suspend, or terminate your access to all or any part of our services; + %li + %p + change, suspend, or discontinue all or any part of our services; + %li + %p + refuse, move, or remove any content; + %li + %p + refuse to register any username that may be deemed offensive or restricted and + %li + %p + establish general practices, fees and policies concerning the site and the services we provide + %br + %p + We do not make any representations about the accuracy of any content posted on the site, whether posted by us, our users, or third parties. All information and services provided on the site are offered "as-is" without any express or implied warranty, and you should not rely on the information presented on the site. Your use of the site is at your own risk. + %br + %br + You understand that by using the site, you may be exposed to content that you find offensive or objectionable. + %br + %br + We disclaim any responsibility for inaccuracies on the site. We are not responsible for any loss, harm, error or omission that may result from the use of the site or the services we provide or for any service failure or interruption, regardless of the cause. + + %ul.unstyled + %li + %h4 Availability + %li + %p + Brevidy does not warrant that the service from this site will be uninterrupted, timely or error free, although it is provided to the best of our ability. By using this service you thereby indemnify Brevidy, its employees, agents and affiliates against any loss or damage, in whatever manner, howsoever caused. + + %ul.unstyled + %li + %h4 Applicable Law + %li + %p + These Terms shall be construed in accordance with the laws of the State of Delaware, and the parties irrevocably consent to bring any action to enforce these Terms and Conditions before an arbitration panel or before a court of competent jurisdiction in Delaware if seeking interim or preliminary relief or enforcement of an arbitration award. + + %ul.unstyled + %li + %h4 Consequences of Violation of these Terms + %li + %p + By utilizing the site you agree to indemnify, defend and hold Brevidy and its officers, directors, employees, agents, and affiliates harmless from and against any and all liability, losses, costs, and expenses (including attorneys' fees) incurred by Brevidy through your use of the site or your posting or transmission of content in violation of these Terms. You also agree to take sole responsibility for any royalties, fees or other monies owed to any person by reason of any content you post or transmit through the site or the services we provide. + %br + %br + We reserve the right to terminate your access to the site if you fail to abide by these Terms and Conditions. We also reserve the right to inform law enforcement authorities of any potential illegal activities and to provide them with all information about the account through which such activities occurred, as further described in our Privacy Policy. \ No newline at end of file diff --git a/app/views/public/video_faq.html.haml b/app/views/public/video_faq.html.haml new file mode 100755 index 0000000..295b934 --- /dev/null +++ b/app/views/public/video_faq.html.haml @@ -0,0 +1,91 @@ +- # Dark Content Wrapper +.lower-wrapper.no-header + + - # Main (White) Content Wrapper + .content-wrapper.standard-padding + + = cache("video_faq_page") do + %h3.mtl Frequently Asked Questions about Uploading Videos + + %ul.unstyled + %li + %h4 How large can my video file be? + %li + The size of uploaded videos is limited to 750 MB. You will not be able to upload anything larger in size, + and will have to shorten your video or lower its quality using + #{link_to("video editing software", "http://en.wikipedia.org/wiki/List_of_video_editing_software", :target => "_blank")} + before retrying the upload. + + %ul.unstyled + %li + %h4 How long can my video be? + %li + We don’t care! As long as it fits within the 750 MB limit, it can be uploaded. However, all videos on Brevidy + will be trimmed to a maximum of 10 minutes in length. If your video is longer than this, we will only display + the first 10 minutes... so make them good! + + %ul.unstyled + %li + %h4 What video formats do you support? + %li + We support the majority of containers/codecs out there! We’ve got elves in the backroom capable + of handling pretty much whatever you throw at us. However if they have an issue with your particular video, we’ll + be in touch to let you know. + %br + %br + FYI: Videos are encoded using a codec (one for the video and a different one for the audio), and then packaged up in a + container format (usually denoted by the file extension). For instance, if you take a video with your cell phone, + it may be an MP4 container (filename.mp4), and be encoded using the H.264 video codec and the AAC audio codec. + If your head’s not spinning yet, take a look at + #{link_to("this chart", "http://en.wikipedia.org/wiki/Comparison_of_container_formats", :target => "_blank")} + for more details. + + %ul.unstyled + %li + %h4 What video resolutions do you support? + %li + We will accept videos of any resolution, however the maximum resolution displayed is 1280 X 720. If the video is + larger than this resolution, we will scale it down while maintaining the aspect ratio. If your video is smaller + than this, then its resolution will not be changed. + %br + %br + FYI: Video resolution is denoted by number of horizontal pixels X the number of vertical pixels (pixels are the + tiny dots in a video). HD video resolutions are denoted by just their vertical resolution. For instance 720p = + 1280 horizontal pixels by 720 vertical pixels. + + + %ul.unstyled + %li + %h4 What will my output video look like? + %li + Your video will be encoded in the MP4 format using the H.264 video codec and AAC audio codec for maximum + compatibility across desktop and mobile devices along with the most popular browsers available with each platform. + It will have a maximum resolution of 1280 X 720. The maximum length will be 10 minutes. + + %ul.unstyled + %li + %h4 What if there’s something wrong with my video? + %li + %ul + %li + If your video shows the processing thumbnail for an unreasonable amount of time, first try a refresh of the + webpage. If the thumbnail doesn’t change to one that belongs to the video, then there may be an issue with + the video, in which case we’re actively working the issue and you’ll receive an email from us regarding the status. + %li + If your video plays, but the quality isn’t what you would expect, you can flag the video to have us look at + it. To do so, click on the “Flag” link to the right of the video, select the first option “There is something + wrong with my video.” leave an optional description of the issue, and click on the Report Video button. Once + we’ve taken a look at the video, we’ll get back in touch with you regarding its status. + %li + If none of the videos play on your PC/Mac, ensure that you have JavaScript enabled and you have the latest + version of + #{link_to("Adobe Flash Player", "http://get.adobe.com/flashplayer/", :target => "_blank")} + installed or have a browser that supports HTML 5. + %li + If videos play on your PC/Mac, but take a long time to buffer, it may be that your Internet connection is too + slow for the quality of the video. We recommend a connection speed of at least 1Mbps. You can test your + connection speed + #{link_to("here", "http://www.speedtest.net/", :target => "_blank")}. + %li + If videos don’t play on your mobile device, it might be because that particular video has too high of a + resolution for your device. diff --git a/app/views/public/video_guidelines.html.haml b/app/views/public/video_guidelines.html.haml new file mode 100755 index 0000000..6cf4eae --- /dev/null +++ b/app/views/public/video_guidelines.html.haml @@ -0,0 +1,33 @@ +- # Dark Content Wrapper +.lower-wrapper.no-header + + - # Main (White) Content Wrapper + .content-wrapper.standard-padding + + = cache("video_guidelines_page") do + %h3.mtl Brevidy Video Guidelines + + %ul + %li + No nudity or pornographic content allowed. + %li + No racism, hate speech, or violent acts (i.e. street fights). We understand that + everyone has their own opinion, but we firmly believe everyone should be respected. + Do your part to build up this great community! + %li + No using audio/video/photos/etc that are copyrighted by someone other than you + in your videos unless you can prove you have received prior approval by the copyright holder. + We rely on the user community to report instances of copyright infringement. If your video is + flagged for review and we find that you have used copyrighted material in it, we will remove + the video and send you an e-mail asking that you upload the video again with only your own + original content. Repeated violations of this guideline can result in deactivation or termination + of your account. + %li + Be kind to others. If we feel you are hurting another person's experience on Brevidy + with rude, obscene, or otherwise inappropriate comments, we will send you an e-mail + warning of possible account deactivation. If it is serious enough, we will skip the e-mail + and terminate your account. + %li + If you feel there is a concern with a video that needs to be brought to our attention, please + flag the video for review. That being said, please do not abuse the flagging feature. If we + feel that you are inappropriately flagging videos, we may deactivate or terminate your account. \ No newline at end of file diff --git a/app/views/public_videos/embed.html.haml b/app/views/public_videos/embed.html.haml new file mode 100755 index 0000000..ca8b1e0 --- /dev/null +++ b/app/views/public_videos/embed.html.haml @@ -0,0 +1,27 @@ +- # Checks if the video is a YouTube/Vimeo player +- video_is_not_remote ||= @video.remote_video_id.blank? +%div{:id => "videoPlayerContainer#{@video.id}"} + - if video_is_not_remote + :javascript + jwplayer("videoPlayerContainer#{@video.id}").setup({ + aboutlink: "http://www.brevidy.com", + abouttext: "Brevidy", + allowfullscreen: true, + autostart: false, + controlbar: "over", + flashplayer: "#{javascript_path('player/player.swf')}", + file: "#{@video.generate_secure_cf_url}", + height: "100%", + image: "#{@video.get_thumbnail_url(@video.selected_thumbnail)}", + screencolor: '#000000', + skin: "#{javascript_path('player/skins/beelden.zip')}", + stretching: "uniform", + volume: 100, + width: "100%", + wmode: "opaque" + }); + + - else + - # Video is remote (YouTube, Vimeo, etc.) so let's embed it + :plain + #{@video.get_html5_iframe_code(page_type = "embed")} \ No newline at end of file diff --git a/app/views/search/users.html.haml b/app/views/search/users.html.haml new file mode 100755 index 0000000..f079b24 --- /dev/null +++ b/app/views/search/users.html.haml @@ -0,0 +1,30 @@ +- # Dark Content Wrapper +.lower-wrapper.no-header + + - # Main (White) Content Wrapper + .content-wrapper + + .search-area + .info-message + %span.results-message + - if @term.blank? + Looking for something interesting? Try using the search box at the top of the page. + - else + Found #{pluralize(@results_count, @search_type)} for "#{@term}" + + - # Search form + = form_tag user_search_path, :method => :get, :class => "user-search" do + = text_field_tag :q, @term, :placeholder => "Search for a name or email" + %span.search + %button{:type => "submit", :value => "Go"} + + - if @users.blank? + .okayguy + = image_tag("okayguy.png", :alt => "Okay", :size => "256x275") + - else + %ul.user-listing + = render @users + + = will_paginate @users + + = render :partial => "shared/infinite_scrolling", :locals => { :item_selector => ".zebra-striped" } \ No newline at end of file diff --git a/app/views/search/users.js.haml b/app/views/search/users.js.haml new file mode 100755 index 0000000..5ca8ca9 --- /dev/null +++ b/app/views/search/users.js.haml @@ -0,0 +1,2 @@ +- unless @users.blank? + = render @users \ No newline at end of file diff --git a/app/views/search/videos.html.haml b/app/views/search/videos.html.haml new file mode 100755 index 0000000..c937732 --- /dev/null +++ b/app/views/search/videos.html.haml @@ -0,0 +1,22 @@ +- # Dark Content Wrapper +.lower-wrapper.no-header + + - # Main (White) Content Wrapper + .content-wrapper + + .search-area + .info-message + - if @term.blank? + Looking for something interesting? Try using the search box at the top of the page. + - else + Found #{pluralize(@results_count, @search_type)} for "#{@term}" + + - if @results.blank? + .okayguy + = image_tag("okayguy.png", :alt => "Okay", :size => "256x275") + - else + = render @results + + = will_paginate @results + + = render :partial => "shared/infinite_scrolling", :locals => { :item_selector => ".video-post" } \ No newline at end of file diff --git a/app/views/search/videos.js.haml b/app/views/search/videos.js.haml new file mode 100755 index 0000000..33f070a --- /dev/null +++ b/app/views/search/videos.js.haml @@ -0,0 +1,2 @@ +- unless @results.blank? + = render @results \ No newline at end of file diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml new file mode 100755 index 0000000..a21221e --- /dev/null +++ b/app/views/sessions/new.html.haml @@ -0,0 +1,33 @@ +.landing_login + / flash notices + - [:notice, :error, :message].each do |key| + - unless flash[key].blank? + %h3.mbm{ :class => "flash flash_#{key}" }= flash[key] + + %h2 Login to Brevidy + + .login_options_container + .social_login_buttons.left + = link_to(image_tag("social_login_facebook.png", :size => "225x43", :class => "left mtl"), "#{root_url}auth/facebook") + = link_to(image_tag("social_login_twitter.png", :size => "225x43", :class => "left mtl"), "#{root_url}auth/twitter") + + .old_fashioned_login.right + %form{:action => sessions_path, :method => "post", :id => "login_form"} + %input{:name => "#{request_forgery_protection_token}", :type => "hidden", :value => "#{form_authenticity_token}"} + .landing_login_form + %label.mas{:for => "email", :class => "login_label"} E-mail: + = text_field_tag :email, nil, :placeholder => "e-mail address", :class => "mls ras" + %label.mas{:for => "password", :class => "login_label"} Password: + = password_field_tag :password, nil, :placeholder => "password", :class => "mls ras" + .tcenter + %input.mls.mtm{:name => "remember", :type => "checkbox", :value => "permanent", :checked => "checked"} + %span.remember_me_label Keep me logged in + %input.ram.medium.silver.button{:type => "submit", :name => "submit", :value => "Login"} + = link_to :forgotten_password do + %h4.light Forget your password? + += render :partial => 'users/signup_box' + +%h4.light.mtl.ptm.borderTopDotted + Note: If you already have a Brevidy account, login using your e-mail / password and you can connect your Facebook or Twitter + account within your Account settings. This will allow you to login with Facebook or Twitter on future visits. \ No newline at end of file diff --git a/app/views/shared/_content_titles.html.haml b/app/views/shared/_content_titles.html.haml new file mode 100755 index 0000000..797d234 --- /dev/null +++ b/app/views/shared/_content_titles.html.haml @@ -0,0 +1,43 @@ +.content-titles + - viewing_someone_else = (current_user.blank? || !current_user?(@user)) + + %ul + - unless viewing_someone_else + %li + = link_to("Stream", user_stream_path(@user), :class => "lighten-to-blue") + + - unless viewing_someone_else + %li.latest-activity + - unseen_notifications_count ||= @user.notifications_count + - if unseen_notifications_count != 0 + %span.notifications-dot>< + #{unseen_notifications_count > 100 ? "100+" : unseen_notifications_count} + + = link_to("Latest Activity", user_latest_activity_path(@user), :class => "lighten-to-blue") + + + + %li.dropdown{"data-dropdown" => "dropdown"} + = link_to(viewing_someone_else ? "Channels" : "My Channels", user_channels_path(@user), :class => "dropdown-toggle lighten-to-blue") + %ul.dropdown-menu + - @user.channels.limit(5).each do |c| + %li + = link_to(user_channel_path(@user, c)) do + - if c.featured? + = image_tag("featured_star.png", :alt => "Featured Channel", :size => "13x13") + - elsif c.private? + = image_tag("private_small.png", :alt => "Private Channel", :size => "13x13") + - else + = image_tag("public.png", :alt => "Public Channel", :size => "13x13") + + = c.title + + %li.divider + %li.view-all-channels + = link_to("View All (#{@user.channels.count}) Channels", user_channels_path(@user)) + + %li + = link_to(viewing_someone_else ? "Videos" : "My Videos", user_path(@user), :class => "lighten-to-blue") + + %li + = link_to("About", user_about_path(@user), :class => "lighten-to-blue") \ No newline at end of file diff --git a/app/views/shared/_error_messages.html.haml b/app/views/shared/_error_messages.html.haml new file mode 100755 index 0000000..becb899 --- /dev/null +++ b/app/views/shared/_error_messages.html.haml @@ -0,0 +1,3 @@ +- if klass.errors.any? + - klass.errors.full_messages.each do |msg| + %div{:data => {:error => "#{msg}"}}= msg \ No newline at end of file diff --git a/app/views/shared/_extras.html.haml b/app/views/shared/_extras.html.haml new file mode 100755 index 0000000..eeed9de --- /dev/null +++ b/app/views/shared/_extras.html.haml @@ -0,0 +1,39 @@ +- # Google Analytics +:javascript + var _gaq = _gaq || []; + _gaq.push(['_setAccount', 'UA-23816026-1']); + _gaq.push(['_trackPageview']); + + (function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); + })(); + +- # GetSatisfaction Code +:javascript + var is_ssl = ("https:" == document.location.protocol); + var asset_host = is_ssl ? "https://s3.amazonaws.com/getsatisfaction.com/" : "http://s3.amazonaws.com/getsatisfaction.com/"; + document.write(unescape("%3Cscript src='" + asset_host + "javascripts/feedback-v2.js' type='text/javascript'%3E%3C/script%3E")); +:javascript + var feedback_widget_options = {}; + + feedback_widget_options.display = "overlay"; + feedback_widget_options.company = "brevidy"; + feedback_widget_options.placement = "left"; + feedback_widget_options.color = "#202020"; + feedback_widget_options.style = "idea"; + + var feedback_widget = new GSFN.feedback_widget(feedback_widget_options); + +- # AddThis script + +// Template for AddThis sharing +:javascript + var addthis_share = { + templates: { + twitter: "{{title}} {{url}} (via @brevidy)" + } + } + +%script{:src => "http://s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4e6fbcbf4b963006#domready=1", :type => "text/javascript"} \ No newline at end of file diff --git a/app/views/shared/_featured_video.html.haml b/app/views/shared/_featured_video.html.haml new file mode 100755 index 0000000..615ee53 --- /dev/null +++ b/app/views/shared/_featured_video.html.haml @@ -0,0 +1,5 @@ +.featured-video-object{"data-video-id" => featured_video.id} + = link_to(user_video_path(get_object_owner(featured_video), featured_video)) do + = image_tag(featured_video.get_thumbnail_url(featured_video.selected_thumbnail), :size => "190x102", :alt => featured_video.title, :class => "featured-popover", :title => featured_video.title, "data-content" => featured_video.description.blank? ? "No description given" : truncate(featured_video.description, :length => 150)) + %i.play-icon + = image_tag("play_small.png", :alt => "Play", :size => "45x34") \ No newline at end of file diff --git a/app/views/shared/_header.html.haml b/app/views/shared/_header.html.haml new file mode 100755 index 0000000..c7af705 --- /dev/null +++ b/app/views/shared/_header.html.haml @@ -0,0 +1,54 @@ +.topbar + .topbar-inner + .container + .static-links + = link_to(image_tag("brevidy_rgb_white.png", :size => "135x35", :alt => "Brevidy - The Soul of Video"), signed_in? ? user_stream_path(current_user) : :root, :class => "brand") + + %form.pull-left{:action => video_search_path, :method => "get"} + %input{:name => "q", :placeholder => "Search public videos", :type => "text"}/ + %span.search + %button{:type => "submit", :value => "Go"} + + - if signed_in? + %ul.nav + %li + = link_to("Explore", explore_path) + %li + = link_to("Upload", new_user_video_path(current_user)) + %li + = link_to("Share a Link", user_share_dialog_path(current_user), :remote => true, "data-method" => "GET") + %li + = link_to("Invite", user_invitations_path(current_user)) + + .dynamic-links + - if signed_in? + %ul.nav.secondary-nav + %li.dropdown{"data-dropdown" => "dropdown"} + = link_to(user_path(current_user), :class => "dropdown-toggle") do + %span#user-image + = image_tag("#{current_user.image.blank? ? 'default_user_35px.jpg' : current_user.image_url(:small_profile) }", :alt => "#{current_user.name}", :size => "35x35") + %span#username + = current_user.username + %ul.dropdown-menu + %li + = link_to("My Channels", user_channels_path(current_user)) + %li + = link_to("Account Settings", user_account_path(current_user)) + %li + = link_to("Find People", find_people_path) + %li + = link_to("Help", "http://getsatisfaction.com/brevidy", :target => "_blank") + %li.divider + %li + = link_to("Logout", logout_path, :remote => true, "data-method" => "DELETE") + - else + %ul.nav.secondary-nav + %li + = link_to("Explore", explore_path) + %li.vertical-divider + %li + = link_to("Sign up", signup_path) + %li.vertical-divider + %li + = link_to("Login", login_path) + \ No newline at end of file diff --git a/app/views/shared/_infinite_scrolling.html.haml b/app/views/shared/_infinite_scrolling.html.haml new file mode 100755 index 0000000..2aee4ac --- /dev/null +++ b/app/views/shared/_infinite_scrolling.html.haml @@ -0,0 +1,22 @@ +- @page_has_infinite_scrolling = true +:javascript + // Wait until all other JS in the page has loaded to run this + $(window).load(function(){ + + // Hides pagination controls + $('.pagination').hide(); + + // Adds infinite scrolling to a page based on 'item_selector' that gets passed in + $('.content-wrapper').infinitescroll({ + appendCallback: false, // tells the script not to scrape the HTML using the .load() method + dataType: 'html', // sets the data response type + navSelector : ".pagination", // selector for the paged navigation (it will be hidden) + nextSelector : ".pagination a.next_page", // selector for the NEXT link (to page 2) + itemSelector : "#{item_selector}", // selector for all items you'll retrieve + loading: { + msgText: "LOADING MORE...", + img: "#{image_path('ajax.gif')}" + } + }); + + }); \ No newline at end of file diff --git a/app/views/shared/_lower_navigation.haml b/app/views/shared/_lower_navigation.haml new file mode 100755 index 0000000..7835c4b --- /dev/null +++ b/app/views/shared/_lower_navigation.haml @@ -0,0 +1,15 @@ +.footerWrapper + %p.dark.pull-right + = succeed " · " do + = link_to("FAQ", faq_path, :class => "inlinelink") + = succeed " · " do + = link_to("Blog", "http://blog.brevidy.com", :class => "inlinelink") + = succeed " · " do + = link_to("Status", "http://status.brevidy.com", :class => "inlinelink") + = succeed " · " do + = link_to("Contact", contact_path, :class => "inlinelink") + = succeed " · " do + = link_to("Terms", tos_path, :class => "inlinelink") + = succeed " · " do + = link_to("Privacy", privacy_path, :class => "inlinelink") + Brevidy LLC, © 2012 \ No newline at end of file diff --git a/app/views/shared/_scroll_to_content_wrapper.html.haml b/app/views/shared/_scroll_to_content_wrapper.html.haml new file mode 100755 index 0000000..96399c5 --- /dev/null +++ b/app/views/shared/_scroll_to_content_wrapper.html.haml @@ -0,0 +1,8 @@ +:javascript + $(window).load(function() { + $.smoothScroll({ + offset: -50, + scrollTarget: '.content-wrapper', + speed: 500 + }); + }); \ No newline at end of file diff --git a/app/views/shared/_select_birthday.html.haml b/app/views/shared/_select_birthday.html.haml new file mode 100755 index 0000000..b481c05 --- /dev/null +++ b/app/views/shared/_select_birthday.html.haml @@ -0,0 +1,13 @@ +%select.medium.birthday_month{:name => "birthday_month"} + = options_for_select((1..12).map {|m| [Date::MONTHNAMES[m], m]}) +%select.medium.birthday_day{:name => "birthday_day"} + = options_for_select((1..31)) +%select.medium.birthday_year{:name => "birthday_year"} + = options_for_select((1901..Date.today.year).to_a.reverse) + +- # Set the select values if they were passed in +- if defined?(the_month) + :javascript + $('select.birthday_month').val("#{the_month}"); + $('select.birthday_day').val("#{the_day}"); + $('select.birthday_year').val("#{the_year}"); \ No newline at end of file diff --git a/app/views/subscriptions/handle_access_request.html.haml b/app/views/subscriptions/handle_access_request.html.haml new file mode 100755 index 0000000..9dc3943 --- /dev/null +++ b/app/views/subscriptions/handle_access_request.html.haml @@ -0,0 +1,19 @@ +- # User Banner Image += render 'users/top_banner' + +- # Middle User Info Section += render 'users/middle_user_banner' + +- # Dark Content Wrapper +.lower-wrapper + + - # Main (White) Content Wrapper + .content-wrapper + = render 'shared/content_titles' + + - unless flash.empty? + = compact_flash_messages + %div.mhl.mtm{:class => "access-request-msg #{@flash_key}-message"} + %h4 + = @flash_msg + \ No newline at end of file diff --git a/app/views/subscriptions/subscribers.html.haml b/app/views/subscriptions/subscribers.html.haml new file mode 100755 index 0000000..b7ceddf --- /dev/null +++ b/app/views/subscriptions/subscribers.html.haml @@ -0,0 +1,20 @@ +- # User Banner Image += render 'users/top_banner' + +- # Middle User Info Section += render 'users/middle_user_banner' + +- # Dark Content Wrapper +.lower-wrapper + = render 'users/featured_videos' + + - # Main (White) Content Wrapper + .content-wrapper + = render 'shared/content_titles' + + %ul.user-listing + = render @users + + = will_paginate @users + + = render :partial => "shared/infinite_scrolling", :locals => { :item_selector => ".zebra-striped" } \ No newline at end of file diff --git a/app/views/subscriptions/subscribers.js.haml b/app/views/subscriptions/subscribers.js.haml new file mode 100755 index 0000000..524e0a4 --- /dev/null +++ b/app/views/subscriptions/subscribers.js.haml @@ -0,0 +1 @@ += render @users \ No newline at end of file diff --git a/app/views/subscriptions/subscriptions.html.haml b/app/views/subscriptions/subscriptions.html.haml new file mode 100755 index 0000000..3331ada --- /dev/null +++ b/app/views/subscriptions/subscriptions.html.haml @@ -0,0 +1,19 @@ +- # User Banner Image += render 'users/top_banner' + +- # Middle User Info Section += render 'users/middle_user_banner' + +- # Dark Content Wrapper +.lower-wrapper + = render 'users/featured_videos' + + - # Main (White) Content Wrapper + .content-wrapper + = render 'shared/content_titles' + + = render @subscriptions + + = will_paginate @subscriptions + + = render :partial => "shared/infinite_scrolling", :locals => { :item_selector => ".channel-wrapper" } \ No newline at end of file diff --git a/app/views/subscriptions/subscriptions.js.haml b/app/views/subscriptions/subscriptions.js.haml new file mode 100755 index 0000000..fa44662 --- /dev/null +++ b/app/views/subscriptions/subscriptions.js.haml @@ -0,0 +1 @@ += render @subscriptions \ No newline at end of file diff --git a/app/views/user_events/_badge_event.html.haml b/app/views/user_events/_badge_event.html.haml new file mode 100755 index 0000000..9743bde --- /dev/null +++ b/app/views/user_events/_badge_event.html.haml @@ -0,0 +1,4 @@ +gave you a +%i{:class => "tooltip #{object.css_class}", :title => "#{object.name}"} +badge for += render :partial => 'user_events/video_link.html', :locals => { :video => video } \ No newline at end of file diff --git a/app/views/user_events/_channel_request_event.html.haml b/app/views/user_events/_channel_request_event.html.haml new file mode 100755 index 0000000..39aaeb3 --- /dev/null +++ b/app/views/user_events/_channel_request_event.html.haml @@ -0,0 +1,16 @@ +requested access to your private channel, +%strong #{object.channel.title} +%span.gray + #{time_ago_in_words(object.created_at)} ago + +.approval-options + - if object.ignored? + You have ignored this request. Would you like to + = link_to("Approve", user_channel_request_access_path(current_user, object.channel, :approved => true, :token => object.token)) + it, instead? + - else + Would you like to + = link_to("Approve", user_channel_request_access_path(current_user, object.channel, :approved => true, :token => object.token)) + or + = link_to("Ignore", user_channel_request_access_path(current_user, object.channel, :ignored => true, :token => object.token)) + this request? \ No newline at end of file diff --git a/app/views/user_events/_comment.html.haml b/app/views/user_events/_comment.html.haml new file mode 100755 index 0000000..5575c0a --- /dev/null +++ b/app/views/user_events/_comment.html.haml @@ -0,0 +1,9 @@ += render :partial => 'user_events/video_link.html', :locals => { :video => video } + +- # Show how long ago it occured +%span.gray + #{time_ago_in_words(object.created_at)} ago + +%p.comment + - # Sanitizes the comment output and auto-generates links + #{auto_link(h(object.content), :html => { :target => "_blank" })} \ No newline at end of file diff --git a/app/views/user_events/_comment_event.html.haml b/app/views/user_events/_comment_event.html.haml new file mode 100755 index 0000000..f28ec3a --- /dev/null +++ b/app/views/user_events/_comment_event.html.haml @@ -0,0 +1,4 @@ +commented on +- unless video.title.blank? + your video, += render :partial => 'user_events/comment.html', :locals => { :video => video, :object => object } \ No newline at end of file diff --git a/app/views/user_events/_comment_response_event.html.haml b/app/views/user_events/_comment_response_event.html.haml new file mode 100755 index 0000000..136d361 --- /dev/null +++ b/app/views/user_events/_comment_response_event.html.haml @@ -0,0 +1,4 @@ +also commented on +- unless video.title.blank? + #{get_object_owner(video).name}'s video, += render :partial => 'user_events/comment.html', :locals => { :video => video, :object => object } \ No newline at end of file diff --git a/app/views/user_events/_featured_video_event.html.haml b/app/views/user_events/_featured_video_event.html.haml new file mode 100755 index 0000000..7f2d3e1 --- /dev/null +++ b/app/views/user_events/_featured_video_event.html.haml @@ -0,0 +1,4 @@ +just featured += render :partial => 'user_events/video_link.html', :locals => { :video => video } +%p + Keep posting more great videos for more chances to be featured! Thanks for being awesome :) \ No newline at end of file diff --git a/app/views/user_events/_subscription_event.html.haml b/app/views/user_events/_subscription_event.html.haml new file mode 100755 index 0000000..0ec5786 --- /dev/null +++ b/app/views/user_events/_subscription_event.html.haml @@ -0,0 +1,3 @@ +subscribed to your channel, +%strong #{object.channel.title} + \ No newline at end of file diff --git a/app/views/user_events/_user_event.html.haml b/app/views/user_events/_user_event.html.haml new file mode 100755 index 0000000..876b370 --- /dev/null +++ b/app/views/user_events/_user_event.html.haml @@ -0,0 +1,16 @@ +%li.zebra-striped{:class => seen_by_user ? "" : "new-event-marker"} + + = link_to(user_path(user), :class => "user-image") do + = image_tag("#{user.image.blank? ? 'default_user_35px.jpg' : user.image_url(:small_profile) }", + :alt => "#{user.name}", :size => "35x35") + %i{:class => "event#{object_class}"} + + .user-meta + = link_to(user.name, user_path(user)) + + = render :partial => "user_events/#{event_type}_event.html", :locals => { :object => object, :video => video } + + - unless ['comment', 'comment_response', 'channel_request'].include?(event_type) + - # Show how long ago it occured + %span.gray + #{time_ago_in_words(object.created_at)} ago \ No newline at end of file diff --git a/app/views/user_events/_video_link.html.haml b/app/views/user_events/_video_link.html.haml new file mode 100755 index 0000000..a566033 --- /dev/null +++ b/app/views/user_events/_video_link.html.haml @@ -0,0 +1,3 @@ +- current_user_owns?(video) ? personalized_title = "your" : personalized_title = "#{get_object_owner(video).name}'s" +- video_title ||= video.title.blank? ? "#{personalized_title} video" : video.title += link_to("#{video_title}", user_video_path(get_object_owner(video), video)) \ No newline at end of file diff --git a/app/views/user_events/show.html.haml b/app/views/user_events/show.html.haml new file mode 100755 index 0000000..32f7bd8 --- /dev/null +++ b/app/views/user_events/show.html.haml @@ -0,0 +1,38 @@ +- # User Banner Image += render 'users/top_banner' + +- # Middle User Info Section += render 'users/middle_user_banner' + +- # Dark Content Wrapper +.lower-wrapper + = render 'users/featured_videos' + + - # Main (White) Content Wrapper + .content-wrapper + = render 'shared/content_titles' + + - if @user_events.empty? + .info-message.mhl + There is currently no activity to show you. + %br/ + %br/ + %strong + What kind of activity do we show here? + %ul + %li When someone new subscribes to one of your channels + %li When someone gives you a badge for one of your videos + %li When someone comments on one of your videos + %li When someone comments on a video that you have also commented on + + - else + %ul.latest-activity + + - @user_events.each do |user_event| + - # Render the content based on event type (badge, comment, etc) + = render_content_for_event(user_event) + + = will_paginate @user_events + + = render :partial => "shared/infinite_scrolling", :locals => { :item_selector => ".zebra-striped" } + \ No newline at end of file diff --git a/app/views/user_events/show.js.haml b/app/views/user_events/show.js.haml new file mode 100755 index 0000000..ddfd4ca --- /dev/null +++ b/app/views/user_events/show.js.haml @@ -0,0 +1,4 @@ +- @user_events.each do |user_event| + - unless user_event.blank? + - # Render the content based on event type (badge, comment, etc) + = render_content_for_event(user_event) \ No newline at end of file diff --git a/app/views/user_mailer/banned.html.haml b/app/views/user_mailer/banned.html.haml new file mode 100755 index 0000000..7a0cf82 --- /dev/null +++ b/app/views/user_mailer/banned.html.haml @@ -0,0 +1,23 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + Hi there, + %p + You've received this email because your account on Brevidy has been permanently banned. + %br/ + %br/ + Reason: #{@reason} + %p + If you feel this has been done in error or you have a question about this, please contact + #{mail_to "support@brevidy.com", "Support", :encode => "hex", :subject => "Account Deactivation for #{@user_email}"} + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;line-height:18px;border-bottom: 1px dotted #808080;padding-bottom:10px;margin-bottom:5px"} + Sincerely, + %br/ + The Brevidy Team + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:10px;color:#808080;margin-top:5px;"} + Please do not reply to this message; it was sent from an unmonitored email address. \ No newline at end of file diff --git a/app/views/user_mailer/celebrate_new_user.html.haml b/app/views/user_mailer/celebrate_new_user.html.haml new file mode 100755 index 0000000..0fa7211 --- /dev/null +++ b/app/views/user_mailer/celebrate_new_user.html.haml @@ -0,0 +1,15 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + Dear Brevidy Team, + %p + Yay!!!!!! A new user (#{@user.name}) just signed up! We now have #{@user_count} users. Keep up the great work!!! + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;line-height:18px;border-bottom: 1px dotted #808080;padding-bottom:10px;margin-bottom:5px"} + Sincerely, + %br/ + The Brevidy Robot \ No newline at end of file diff --git a/app/views/user_mailer/confirmation_instructions.html.haml b/app/views/user_mailer/confirmation_instructions.html.haml new file mode 100755 index 0000000..7fa6543 --- /dev/null +++ b/app/views/user_mailer/confirmation_instructions.html.haml @@ -0,0 +1,21 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + Hi #{@user.name}, + %p + Welcome to Brevidy! Please confirm your account by clicking this link: + %br + %a{:href => "#{@url}"} #{@url} + %p + Once you confirm, all future notifications will be sent to this email address. + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;line-height:18px;border-bottom: 1px dotted #808080;padding-bottom:10px;margin-bottom:5px"} + Sincerely, + %br/ + The Brevidy Team + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:10px;color:#808080;margin-top:5px;"} + Please do not reply to this message; it was sent from an unmonitored email address. \ No newline at end of file diff --git a/app/views/user_mailer/deactivated.html.haml b/app/views/user_mailer/deactivated.html.haml new file mode 100755 index 0000000..4c14bcb --- /dev/null +++ b/app/views/user_mailer/deactivated.html.haml @@ -0,0 +1,23 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + Hi #{@user.name}, + %p + You've received this email because your account on Brevidy has been deactivated. + %br/ + %br/ + Reason: #{@reason} + %p + If you feel this has been done in error or you have a question about this, please contact + #{mail_to "support@brevidy.com", "Support", :encode => "hex", :subject => "Account Deactivation for #{@user.email}"} + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;line-height:18px;border-bottom: 1px dotted #808080;padding-bottom:10px;margin-bottom:5px"} + Sincerely, + %br/ + The Brevidy Team + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:10px;color:#808080;margin-top:5px;"} + Please do not reply to this message; it was sent from an unmonitored email address. \ No newline at end of file diff --git a/app/views/user_mailer/fatal_error_on_video.html.haml b/app/views/user_mailer/fatal_error_on_video.html.haml new file mode 100755 index 0000000..4dfeb77 --- /dev/null +++ b/app/views/user_mailer/fatal_error_on_video.html.haml @@ -0,0 +1,23 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + Hi #{@user.name}, + %p + Unfortunately we were unable to process the video you recently uploaded. This happens sometimes due to uploading a + video that was blank, using incompatible codecs, or general errors with the transcoding process. Please check your + video file and try again if you'd like. + %p + If you continue to have problems with the file, please read the Video FAQ or contact us at + %a{:href=>"mailto:support@brevidy.com"} + support@brevidy.com + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;line-height:18px;border-bottom: 1px dotted #808080;padding-bottom:10px;margin-bottom:5px"} + Sincerely, + %br/ + The Brevidy Team + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:10px;color:#808080;margin-top:5px;"} + Please do not reply to this message; it was sent from an unmonitored email address. \ No newline at end of file diff --git a/app/views/user_mailer/featured_video.html.haml b/app/views/user_mailer/featured_video.html.haml new file mode 100755 index 0000000..aad1397 --- /dev/null +++ b/app/views/user_mailer/featured_video.html.haml @@ -0,0 +1,21 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + Hi #{@user.name}, + %p + Hooray! Your video was featured by the Brevidy Staff! :) + %p + To check it out, goto the + %a{:href => "#{@url}"} Explore + page. Keep posting more great videos for more chances to be featured! Thanks for being awesome! + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;line-height:18px;border-bottom: 1px dotted #808080;padding-bottom:10px;margin-bottom:5px"} + Sincerely, + %br/ + The Brevidy Team + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:10px;color:#808080;margin-top:5px;"} + Please do not reply to this message; it was sent from an unmonitored email address. \ No newline at end of file diff --git a/app/views/user_mailer/flagged_video.html.haml b/app/views/user_mailer/flagged_video.html.haml new file mode 100755 index 0000000..9b78bbe --- /dev/null +++ b/app/views/user_mailer/flagged_video.html.haml @@ -0,0 +1,32 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + World's Best Support Staff, + %p + You've received this email because a video has been flagged for your thorough, professional review. + %br/ + %br/ + Video URL: #{@url_to_video} + %br/ + Video Owner: #{@url_to_owner} + %br/ + Video Owner Name: #{h(@video_owner.name)} + %br/ + Video Owner E-mail: #{@video_owner.email} + %br/ + Flagged By: #{@url_to_flagged_by.blank? ? "Signed out user" : @url_to_flagged_by} + %br/ + Flag Type Description: #{@flag_type_description} + %br/ + Detailed Reason Given: #{@reason.blank? ? "No" : h(@reason) } + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;line-height:18px;border-bottom: 1px dotted #808080;padding-bottom:10px;margin-bottom:5px"} + Sincerely, + %br/ + The Brevidy Robot + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:10px;color:#808080;margin-top:5px;"} + Please do not reply to this message; it was sent from an unmonitored email address. \ No newline at end of file diff --git a/app/views/user_mailer/invitation.html.haml b/app/views/user_mailer/invitation.html.haml new file mode 100755 index 0000000..41a3c54 --- /dev/null +++ b/app/views/user_mailer/invitation.html.haml @@ -0,0 +1,40 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + Hi there! + %p + - if !@sender.blank? + You've received an exclusive invitation from + %strong + #{h(@sender.name)} + to join Brevidy, a video social network where you can share your moments, thoughts, and ideas with those around you! + - else + Thanks for signing up for early access to Brevidy, a video social network where you can share your moments, + thoughts, and ideas with those around you! Today, we are excited to announce that we are sending out invitations + to start using the site! + %br + - if @personal_message.blank? + %br + - unless @personal_message.blank? + %p + A personal message from #{h(@sender.name)}: + %p + %i{:style => "margin-left: 20px;"} + = h(@personal_message) + %br + To create an account (it's free!), just click this link: #{link_to(@url, @url)}. Alternatively, you can visit + #{link_to(@site_url, @site_url)} and copy/paste this invitation code: + %strong + #{@token} + + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;line-height:18px;border-bottom: 1px dotted #808080;padding-bottom:10px;margin-bottom:5px"} + Sincerely, + %br/ + The Brevidy Team + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:10px;color:#808080;margin-top:5px;"} + Please do not reply to this message; it was sent from an unmonitored email address. \ No newline at end of file diff --git a/app/views/user_mailer/new_badge.html.haml b/app/views/user_mailer/new_badge.html.haml new file mode 100755 index 0000000..fd5d53a --- /dev/null +++ b/app/views/user_mailer/new_badge.html.haml @@ -0,0 +1,43 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + Hi #{@video_owner.name}! + %p + %strong + #{h(@badge_from.name)} + has just given you the + %strong + #{@icon.name} + badge for your video. + %br + %div{:style => "background: #e0e0e0;padding: 5px;width: 98%;word-wrap: break-word;display:inline-block;-webkit-box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 1px 1px;box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 1px 1px;-moz-box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 1px 1px;-webkit-border-radius: 10px;-moz-border-radius: 10px;border-radius: 10px;"} + = image_tag("#{@badge_from.image.blank? ? @default_image : @badge_from.image_url(:medium_profile) }", + :alt => "#{@badge_from.name}", :size => "50x50", :style=>"margin-right: 7px;-webkit-border-radius: 5px;-moz-border-radius: 5px;border-radius: 5px;float:left") + + %img{:alt => "From", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/badges/badge_email_arrow.png", :style => "margin: 10px 10px;float:left"}/ + + %img{:alt => "Badge", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/badges/#{@icon.css_class}_Preview.png", :style => "margin: 0px;float:left"}/ + + %br + %br + %div{:style=>"clear:both"} + To view the video and the latest badges, click the link below: + %br + %br + %a{:href => "#{@url}"} #{@url} + + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;line-height:18px;border-bottom: 1px dotted #808080;padding-bottom:10px;margin-bottom:5px"} + Sincerely, + %br/ + The Brevidy Team + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:12px;color:#808080;margin-top:5px;"} + If you'd like to disable these notifications, + %a{:href => "#{@account_url}"} + click here to change your settings + %br + Please do not reply to this message; it was sent from an unmonitored email address. \ No newline at end of file diff --git a/app/views/user_mailer/new_comment.html.haml b/app/views/user_mailer/new_comment.html.haml new file mode 100755 index 0000000..15fb906 --- /dev/null +++ b/app/views/user_mailer/new_comment.html.haml @@ -0,0 +1,45 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + Hi #{@person_we_are_emailing.name}! + %p + %strong + #{h(@commenter.name)} + - if @the_comment_is_a_reply + also commented on #{h(@video_owner.name)}'s video. + - else + just commented on your video. + %br + %div{:style => "background: #e0e0e0;padding: 5px;width: 95%;word-wrap: break-word;display:inline-block;-webkit-box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 1px 1px;box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 1px 1px;-moz-box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 1px 1px;-webkit-border-radius: 10px;-moz-border-radius: 10px;border-radius: 10px;"} + = image_tag("#{@commenter.image.blank? ? @default_image : @commenter.image_url(:medium_profile) }", + :alt => "#{@commenter.name}", :size => "50x50", :style=>"margin-right: 7px;-webkit-border-radius: 5px;-moz-border-radius: 5px;border-radius: 5px;float:left") + + %div{:style => "float:left;width:85%"} + %div{:style => "font-weight: bolder;"} #{@commenter.name} + %div{:style => "padding-right: 5px;"} + %p{:style => "margin: 2px 0px"} + #{auto_link(h(@comment.content), :html => { :style => "text-decoration: none;border-bottom: dotted;border-bottom-width: 1px;", :target => "_blank" })} + + %br + %br + %div{:style=>"clear:both"} + To view the video and the latest comments, click the link below: + %br + %br + %a{:href => "#{@url}"} #{@url} + + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;line-height:18px;border-bottom: 1px dotted #808080;padding-bottom:10px;margin-bottom:5px"} + Sincerely, + %br/ + The Brevidy Team + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:12px;color:#808080;margin-top:5px;"} + If you'd like to disable these notifications, + %a{:href => "#{@account_url}"} + click here to change your settings + %br + Please do not reply to this message; it was sent from an unmonitored email address. \ No newline at end of file diff --git a/app/views/user_mailer/new_subscriber.html.haml b/app/views/user_mailer/new_subscriber.html.haml new file mode 100755 index 0000000..56f787f --- /dev/null +++ b/app/views/user_mailer/new_subscriber.html.haml @@ -0,0 +1,30 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + Hi #{@publisher.name}! + %p + %strong + #{h(@subscriber.name)} + just subscribed to your + %strong #{@channel.title} + channel on Brevidy! + %br/ + %p{:style=>"clear:both;"} + To view this person's page, click this link: + %a{:href => "#{@url}"} #{@url} + + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;line-height:18px;border-bottom: 1px dotted #808080;padding-bottom:10px;margin:5px 0px"} + Sincerely, + %br/ + The Brevidy Team + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:12px;color:#808080;margin-top:5px;"} + If you'd like to disable these notifications, + %a{:href => "#{@account_url}"} + click here to change your settings + %br + Please do not reply to this message; it was sent from an unmonitored email address. \ No newline at end of file diff --git a/app/views/user_mailer/private_channel_request_approved.html.haml b/app/views/user_mailer/private_channel_request_approved.html.haml new file mode 100755 index 0000000..310f89c --- /dev/null +++ b/app/views/user_mailer/private_channel_request_approved.html.haml @@ -0,0 +1,21 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + Hi #{@requesting_user.name}! + %p + %strong + #{h(@channel_owner.name)} + has approved your request to access their private channel: + %a{:href => "#{@channel_url}"} #{@channel_url} + + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;line-height:18px;border-bottom: 1px dotted #808080;padding-bottom:10px;margin:5px 0px"} + Sincerely, + %br/ + The Brevidy Team + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:12px;color:#808080;margin-top:5px;"} + Please do not reply to this message; it was sent from an unmonitored email address. \ No newline at end of file diff --git a/app/views/user_mailer/reactivated.html.haml b/app/views/user_mailer/reactivated.html.haml new file mode 100755 index 0000000..8df44ea --- /dev/null +++ b/app/views/user_mailer/reactivated.html.haml @@ -0,0 +1,18 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + Hi #{@user.name}, + %p + You've received this email because we have completed reviewing your account on Brevidy and determined that it should be reactivated. + You should now be able to access Brevidy like normal. Thank you for your patience throughout the review process. + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;line-height:18px;border-bottom: 1px dotted #808080;padding-bottom:10px;margin-bottom:5px"} + Sincerely, + %br/ + The Brevidy Team + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:10px;color:#808080;margin-top:5px;"} + Please do not reply to this message; it was sent from an unmonitored email address. \ No newline at end of file diff --git a/app/views/user_mailer/redesign_feedback.text.haml b/app/views/user_mailer/redesign_feedback.text.haml new file mode 100755 index 0000000..1790cfd --- /dev/null +++ b/app/views/user_mailer/redesign_feedback.text.haml @@ -0,0 +1,11 @@ +Hey #{@user_first_name}, +\ +I wanted to take a minute to personally e-mail you about the new Brevidy re-design (new feature highlights here: http://tumblr.com/Zcq4zwF_tgBB). If you have a minute, go check it out and let me know your thoughts: #{@user_url} +\ +Keep in mind this is just the first step on our roadmap to make Brevidy into something amazing. If you have any questions, concerns, ideas, etc. we would love to hear them! +\ +Thank you again for your support! We couldn't do this without you! +\ +Sincerely, +\ +Rob Phillips, Founder (http://brevidy.com/rob) \ No newline at end of file diff --git a/app/views/user_mailer/request_channel_approval.html.haml b/app/views/user_mailer/request_channel_approval.html.haml new file mode 100755 index 0000000..294e3e9 --- /dev/null +++ b/app/views/user_mailer/request_channel_approval.html.haml @@ -0,0 +1,37 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + Hi #{@channel_owner.name}! + %p + %strong + #{h(@requesting_user.name)} + has requested access to your private channel, + %strong + #{@channel.title} + %br + %br + Would you like to approve or ignore this request? + %a{:href => "#{@approve_url}"} Approve + or + %a{:href => "#{@ignore_url}"} Ignore + %br/ + %p{:style=>"clear:both;"} + %strong Note: + We will not notify #{@requesting_user.name} if you ignore the request. To view #{@requesting_user.name}'s page, click this link: + %a{:href => "#{@requesting_user_url}"} #{@requesting_user_url} + + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;line-height:18px;border-bottom: 1px dotted #808080;padding-bottom:10px;margin:5px 0px"} + Sincerely, + %br/ + The Brevidy Team + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:12px;color:#808080;margin-top:5px;"} + If you'd like to disable these notifications, + %a{:href => "#{@account_url}"} + click here to change your settings + %br + Please do not reply to this message; it was sent from an unmonitored email address. \ No newline at end of file diff --git a/app/views/user_mailer/reset_password_instructions.html.haml b/app/views/user_mailer/reset_password_instructions.html.haml new file mode 100755 index 0000000..f15ce8e --- /dev/null +++ b/app/views/user_mailer/reset_password_instructions.html.haml @@ -0,0 +1,26 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + Hi #{@user.name}, + %p + You've received this email because a request to reset your password was generated. To reset your password, click this link: + %br + %br + %a{:href => "#{@url}"} #{@url} + %p + Please be sure to reset your password to something secure that you'll remember. If you don't want to reset your password, please ignore this message and your password will not be reset. + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;line-height:18px;border-bottom: 1px dotted #808080;padding-bottom:10px;margin-bottom:5px"} + Sincerely, + %br/ + The Brevidy Team + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:10px;color:#808080;margin-top:5px;"} + If you did not initiate this request to reset your password, please contact us at + %a{:href=>"mailto:support@brevidy.com"} + support@brevidy.com + %br + Please do not reply to this message; it was sent from an unmonitored email address. \ No newline at end of file diff --git a/app/views/user_mailer/september_2011_newsletter.html.haml b/app/views/user_mailer/september_2011_newsletter.html.haml new file mode 100755 index 0000000..dc71666 --- /dev/null +++ b/app/views/user_mailer/september_2011_newsletter.html.haml @@ -0,0 +1,65 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + Hi #{@user.name}! + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:13px;font-weight:normal"} + It's been a crazy past two months at Brevidy and we wanted to update you about some of our new features! + %br/ + %ul{:style => "margin-top:10px;margin-left:5px"} + %li{:style => "margin-bottom: 5px"} + %strong Now it's easier than ever to invite others to join Brevidy! + %h3{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:13px;font-weight:normal"} + All you have to do is copy and paste this invitation link to Facebook, Twitter, Google Plus, or send it out in an e-mail! + Up to 100 people can join using your link, so please do us a BIG favor and share it with people you think might use Brevidy as well! :) + %br/ + %br/ + %a{:href => "#{@invitation_link_url}"} + #{@invitation_link_url} + %li{:style => "margin-bottom: 5px"} + %strong Brevidy now has an activity feed! + %h3{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:13px;font-weight:normal"} + Find out what's been happening to your videos while you've been away by looking at your + %a{:href => "#{@latest_activity_url}"} + Latest Activity Feed! + We'll let you know anytime you get a new follower, comment, badge, or someone adds your video to their favorites all in one place! + %br/ + %br/ + If you'd like to disable e-mail notifications for activity on Brevidy, you can do so within + %a{:href => "#{@account_url}"} + your account settings. + %li{:style => "margin-bottom: 5px"} + %strong Remember, you can share videos from YouTube and Vimeo as well! + %h3{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:13px;font-weight:normal"} + You may not know this, but you don't have to have any videos of your own to use Brevidy! In fact, most people on Brevidy + just find cool videos on YouTube or Vimeo and share them using the "Share a Link" link that's on the left side of + every page! + %br/ + %br/ + Already have videos that you've uploaded to YouTube or Vimeo? Cool! Share those too! If you DO have videos of your own, + you can upload them directly to Brevidy via the + %a{:href => "#{@new_video_url}"} Upload a Video + page! + %li{:style => "margin-bottom: 15px"} + %strong Watch what's happening on Brevidy! + %h3{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:13px;font-weight:normal"} + You can watch all the newest videos as they're posted to Brevidy by checking out the + %a{:href => "#{@whats_happening_url}"} What's Happening + page! + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 5px;font-size:13px;font-weight:normal"} + Thanks again for your support for Brevidy! We are really passionate about what we do, and we know that we could not do + this without you! Keep up the great work and keep posting videos!!! :) + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 5px;font-size:13px;font-weight:normal;border-bottom: 1px dotted #808080;padding-bottom:10px;margin-bottom:5px"} + Sincerely, + %br/ + The Brevidy Team + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:12px;color:#808080;margin-top:5px;"} + If you have questions about anything, feel free to reply back to this e-mail! + %br/ + If you forgot your password, you can reset it on the + %a{:href => "#{@forgotten_password_url}"} Forgotten Password + page. \ No newline at end of file diff --git a/app/views/user_mailer/share_video.html.haml b/app/views/user_mailer/share_video.html.haml new file mode 100755 index 0000000..bd66bce --- /dev/null +++ b/app/views/user_mailer/share_video.html.haml @@ -0,0 +1,34 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + Hi there, + %p + - if @user.blank? + Someone + - else + = h(@user.name) + has shared a video on Brevidy with you! + + - unless @personal_message.blank? + %p + They also included a personal message: + %p + %i{:style => "margin-left: 20px;"} + = h(@personal_message) + + %p + To check it out, click here: + %a{:href => "#{@link_to_video_url}"} + = @link_to_video_url + + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;line-height:18px;border-bottom: 1px dotted #808080;padding-bottom:10px;margin-bottom:5px"} + Sincerely, + %br/ + The Brevidy Team + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:10px;color:#808080;margin-top:5px;"} + Please do not reply to this message; it was sent from an unmonitored email address. \ No newline at end of file diff --git a/app/views/user_mailer/video_is_done_encoding.html.haml b/app/views/user_mailer/video_is_done_encoding.html.haml new file mode 100755 index 0000000..24a1066 --- /dev/null +++ b/app/views/user_mailer/video_is_done_encoding.html.haml @@ -0,0 +1,26 @@ +%div{:lang => "en", :style => "color:#202020;width:90%"} + %div{:style => "padding:4px;padding-left:10px;background-color:#202020;-webkit-border-top-left-radius: 6px;-webkit-border-top-right-radius: 6px;-moz-border-radius-topleft: 6px;-moz-border-radius-topright: 6px;border-top-left-radius: 6px;border-top-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %a{:href => "http://brevidy.com", :style => "color:#FFF", :target => "_blank"} + %img{:alt => "Brevidy - The soul of video", :src => "#{Brevidy::Application::S3_BASE_URL}/images/emails/brevidylogo.png", :style => "display:block;border:0"}/ + %div{:style => "background-color:#505050;height:4px;"} + / placeholder + %div{:style => "padding:14px;padding-bottom:2px;background-color:#f2f2f2;font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;-webkit-border-bottom-left-radius: 6px;-webkit-border-bottom-right-radius: 6px;-moz-border-radius-bottomleft: 6px;-moz-border-radius-bottomright: 6px;border-bottom-left-radius: 6px;border-bottom-right-radius: 6px;box-shadow: #555 0px 1px 8px;-moz-box-shadow: #555 0px 1px 8px;-webkit-box-shadow: #555 0px 1px 8px;"} + %h2{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;margin:0 0 16px;font-size:16px;font-weight:normal"} + Hi #{@user.name}, + %p + Your video is ready to be watched on Brevidy! + %p + To check it out, click here: + %a{:href => "#{@link_to_video_url}"} + = @link_to_video_url + + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:13px;line-height:18px;border-bottom: 1px dotted #808080;padding-bottom:10px;margin-bottom:5px"} + Sincerely, + %br/ + The Brevidy Team + %p{:style => "font-family:'Helvetica Neue', Arial, Helvetica, sans-serif;font-size:10px;color:#808080;margin-top:5px;"} + If you'd like to disable these notifications, + %a{:href => "#{@account_url}"} + click here to change your settings + %br + Please do not reply to this message; it was sent from an unmonitored email address. \ No newline at end of file diff --git a/app/views/users/_featured_videos.html.haml b/app/views/users/_featured_videos.html.haml new file mode 100755 index 0000000..527072f --- /dev/null +++ b/app/views/users/_featured_videos.html.haml @@ -0,0 +1,23 @@ +#featured-videos + .title + = image_tag("featured_star.png", :alt => "Featured", :size => "16x16") + %span + - if @user.blank? + Featured Videos + - else + = link_to("Featured Videos", user_channel_path(@user, @user.featured_channel), :class => "white-to-blue") + - if current_user?(@user) + %span.edit-order>< + = link_to(user_edit_featured_videos_path(@user)) do + = image_tag("edit_order.png", :alt => "Edit Order", :size => "13x13") + %span>< edit order + + .videos + - if @latest_featured_videos.empty? + %p + - if @user.blank? + There aren't any featured videos to show :( + - else + #{@user.name} does not have any featured videos to show :( + - else + = render :partial => 'shared/featured_video', :collection => @latest_featured_videos \ No newline at end of file diff --git a/app/views/users/_middle_user_banner.html.haml b/app/views/users/_middle_user_banner.html.haml new file mode 100755 index 0000000..0f23761 --- /dev/null +++ b/app/views/users/_middle_user_banner.html.haml @@ -0,0 +1,34 @@ +.middle-user-banner-container + = link_to(user_path(@user)) do + = image_tag("#{ @user.image.blank? ? 'default_user_150px.jpg' : @user.image_url(:large_profile) }", + :alt => "#{@user.name}", + :class => "thumbnail", + :size => "150x150") + + .user-info-section + .user-name-and-location + %span.user-name + = @user.name + %span.location + = @user.location + %p + %i + = @user.bio + %p.website + - unless @user.website.blank? + = link_to(@user.website, "#{@user.website}", :target => "_blank") + %p + #{link_to("Videos (#{@user.videos_count})", user_path(@user))} · + #{link_to("Channels (#{@user.channels_count})", user_channels_path(@user))} · + #{link_to("Badges (#{@user.badges_count})", user_badges_path(@user))} · + #{link_to("Subscribers (#{@user.subscribers_count})", user_subscribers_path(@user))} · + #{link_to("Subscriptions (#{@user.subscriptions_count})", user_subscriptions_path(@user))} + + - if !current_user?(@user) + %p + = link_to(user_channels_path(@user), :class => "small subscribe-with-icon btn") do + = image_tag("add_icon.png", :size => "16x16") + %span Subscribe + +.banner-corners + = image_tag("banner_corners.png", :size => "875x15", :alt => "") \ No newline at end of file diff --git a/app/views/users/_signup_box.html.haml b/app/views/users/_signup_box.html.haml new file mode 100755 index 0000000..5accbd3 --- /dev/null +++ b/app/views/users/_signup_box.html.haml @@ -0,0 +1,5 @@ +/ sign up footer +.landing_signup + %h3.ras.informationField.signup_box + Not a member? Sign up and start sharing videos with other people today! + = link_to "Sign Up", signup_path, :class => "mls ram medium green button" \ No newline at end of file diff --git a/app/views/users/_signup_form.html.haml b/app/views/users/_signup_form.html.haml new file mode 100755 index 0000000..b1fbb38 --- /dev/null +++ b/app/views/users/_signup_form.html.haml @@ -0,0 +1,64 @@ += form_for(@user, :html => { :class => "ptxs"}, :remote => true) do |f| + .landing_login_form + %input{:type => "hidden", :name => "uid", :value => @uid} + %input{:type => "hidden", :name => "provider", :value => @provider} + %input{:type => "hidden", :name => "oauth_token", :value => @oauth_token} + %input{:type => "hidden", :name => "oauth_token_secret", :value => @oauth_token_secret} + %input{:type => "hidden", :name => "social_signup", :value => @social_signup} + %input{:type => "hidden", :name => "user[location]", :value => @user.location} + + - unless @invitation.blank? + %input{:type => "hidden", :name => "invitation_token", :value => @invitation.token} + + %fieldset.ml25 + = f.label(:username, "Username", :class => "login_label") + = f.text_field(:username, :autocomplete => :off, :id => "signupUsername", :placeholder => "username", :class => "mls ras toolTipSignup", :title => "", :tabindex => 1, "data-path" => username_availability_path) + %span.username-availability + / placeholder + + %fieldset.ml25 + = f.label(:name, "Your Name (First & Last)", :class => "login_label") + = f.text_field(:name, :autocomplete => :off, :id => "signupName", :placeholder => "your name", :class => "mls ras toolTipSignup", :title => "We recommend using your real name so people you know can find you.", :tabindex => 2) + + %fieldset.ml25 + = f.label(:email, "E-mail", :class => "login_label") + = f.text_field(:email, :autocomplete => :off, :id => "signupEmail", :placeholder => "email address", :class => "mls ras toolTipSignup", :title => "We will not share your e-mail address with ANYONE. Period.", :tabindex => 3) + + %fieldset.ml25 + = f.label(:password, "Password", :class => "login_label") + = f.password_field(:password, :autocomplete => :off, :id => "signupPassword", :placeholder => "password", :class => "mls ras toolTipSignup", :title => "Passwords should be longer than 6 characters.", :tabindex => 4) + + %fieldset + = f.label(:birthday, "Date of Birth (age verification only)", :class => "ml30 login_label") + .dateOfBirth.mlxl + %select#signupBirthdayMonth.birthday_month.ras{:name => "user[birthday(2i)]", :tabindex => 5} + %option#signupChooseMonth{:value => ""} Month: + = options_for_select((1..12).map {|m| [Date::MONTHNAMES[m], m]}) + %select#signupBirthdayDay.birthday_day.ras{:name => "user[birthday(3i)]", :tabindex => 6} + %option#signupChooseDay{:value => ""} Day: + = options_for_select((1..31)) + %select#signupBirthdayYear.birthday_year.ras{:name => "user[birthday(1i)]", :tabindex => 7} + %option#signupChooseYear{:value => ""} Year: + = options_for_select((1901..Date.today.year).to_a.reverse) + + .tcenter + %fieldset.mtm + %label{:for => "Agree"} + %h4.light + By clicking Sign up, you are indicating that you have read and agree to the + %a.inlinelink.light{:href => "/tos"} + terms of service + + %fieldset.signupbutton + = image_tag("ajax_white.gif", :size => "16x16", :class => "soft_hidden signup_ajax_animation") + %input.ram.green.signup.button{:name => "signup", :type => "submit", :value => "Sign up", :tabindex => 8} + +- unless @user.birthday.blank? + - begin + - parsed_bday = Date.parse @user.birthday.to_s + + :javascript + $('#signupBirthdayMonth').val(#{parsed_bday.month}); + $('#signupBirthdayDay').val(#{parsed_bday.day}); + $('#signupBirthdayYear').val(#{parsed_bday.year}); + - rescue \ No newline at end of file diff --git a/app/views/users/_top_banner.html.haml b/app/views/users/_top_banner.html.haml new file mode 100755 index 0000000..6196bf2 --- /dev/null +++ b/app/views/users/_top_banner.html.haml @@ -0,0 +1,10 @@ +.top-banner + - if @user.banner_image_id == 0 + - # The user has uploaded a banner or they are using the default + = image_tag("#{ @user.banner_image.blank? ? @user.get_banner_image_url(nil) : @user.banner_image_url(:resized) }", :alt => "", :size => "850x315") + - else + - # The user has chosen a banner from the gallery + = image_tag("#{@user.get_banner_image_url(@user.banner_image_id)}", :alt => "", :size => "850x315") + + - if current_user?(@user) + = link_to("Change Banner Image", user_edit_banner_path(current_user), :class => "small primary btn") \ No newline at end of file diff --git a/app/views/users/_user.html.haml b/app/views/users/_user.html.haml new file mode 100755 index 0000000..ccfe1b5 --- /dev/null +++ b/app/views/users/_user.html.haml @@ -0,0 +1,16 @@ +%li.zebra-striped + = link_to(user_path(user), :class => "user-image") do + = image_tag("#{user.image.blank? ? 'default_user_50px.jpg' : user.image_url(:medium_profile) }", + :alt => "#{user.name}", + :size => "50x50") + + .user-meta + %h5 + = link_to(user.name, user_path(user)) + %span #{pluralize(user.videos_count, 'Video')} · #{pluralize(user.badges_count, 'Badge')} · #{pluralize(user.subscribers_count, 'Subscriber')} + - unless user.bio.blank? + %p + %i + = auto_link(h(user.bio), :html => { :target => "_blank" }) + + = link_to("View Channels", user_channels_path(user), :class => "small btn") \ No newline at end of file diff --git a/app/views/users/create.js.haml b/app/views/users/create.js.haml new file mode 100755 index 0000000..a4e38dc --- /dev/null +++ b/app/views/users/create.js.haml @@ -0,0 +1,5 @@ +- if @user.errors.any? + :plain + $('.formErrorField.js-validate').slideUp("fast", function () { + $(this).html('#{escape_javascript(render('shared/error_messages', :klass => @user))}').slideDown('fast'); + }); \ No newline at end of file diff --git a/app/views/users/edit.html.haml b/app/views/users/edit.html.haml new file mode 100755 index 0000000..ff528da --- /dev/null +++ b/app/views/users/edit.html.haml @@ -0,0 +1,229 @@ +- # User Banner Image += render 'users/top_banner' + +- # Middle User Info Section += render 'users/middle_user_banner' + +- # Dark Content Wrapper +.lower-wrapper + + - # Main (White) Content Wrapper + .content-wrapper.standard-padding + + - # Set up the tabs + %ul.tabs + %li.active + %a{:href => "#upload-image"} Profile Picture + %li + %a{:href => "#background-image"} Background + %li + %a{:href => "#user-info"} User Info + %li + %a{:href => "#notifications"} Notifications + %li + %a{:href => "#connected-services"} Connected Services + %li + %a{:href => "#password"} Password + %li + %a{:href => "#blocked-people"} Blocked People + + + - # Set up the content areas for the tabs + .pill-content + #upload-image.active + .info-message + %strong Note: + Maximum file size of 2 MB. Please keep it clean and only upload your own images. + + #progress-bar + .progress{:style => "width: 0%;"} + %span{:style => "display:none;"} + Uploading + + :plain + #{s3_uploader('image')} + %a{:id => "select-image", :href => "#", :class => "medium primary btn"} Select Image + + #background-image + .info-message + Choose whether you want Brevidy to have a light or dark background. + + .background-image-area + = link_to(user_update_background_image_path(current_user, :background_image_id => 0), :class => "set-new-background-image thumbnail light-bg", :remote => true, :method => :put) do + = image_tag("backgrounds/small/light.jpg", :size => "200x200") + .mts.centered Light + = link_to(user_update_background_image_path(current_user, :background_image_id => 1), :class => "set-new-background-image thumbnail dark-bg", :remote => true, :method => :put) do + = image_tag("backgrounds/small/dark.jpg", :size => "200x200") + .mts.centered Dark + + #user-info + .info-message + %strong Note: + Only your + %strong Username, + %strong Name + and + %strong Location + are publicly viewable; everything else is kept private. Usernames can only be changed + %strong once a month. + + = form_tag(user_account_update_path(@user), :id => "update-user-info", :class => "prl", :remote => true, :method => :put) do |f| + %label + Username + .input + = text_field(:user, :username, :maxlength => "30", 'data-path' => username_availability_path) + %span http://brevidy.com/#{@user.username} + + %label + Name + .input + = text_field(:user, :name, :maxlength => "30") + + %label + Location + .input + = text_field(:user, :location, :maxlength => "50") + + %label.align-to-top + = image_tag("private_small.png", :alt => "Private Information", :title => "Private Information", :size => "14x14", :class => "semi-transparent") + E-mail Address + .input + = text_field(:user, :email, :maxlength => "200") + + .mvl + %label.align-to-top + = image_tag("private_small.png", :alt => "Private Information", :title => "Private Information", :size => "14x14", :class => "semi-transparent") + Birthday + .input + = render :partial => 'shared/select_birthday', :locals => { :the_month => @user.birthday.strftime("%-m"), + :the_day => @user.birthday.strftime("%d"), + :the_year => @user.birthday.strftime("%Y")} + + .mvl + %label.align-to-top + = image_tag("private_small.png", :alt => "Private Information", :title => "Private Information", :size => "14x14", :class => "semi-transparent") + Gender + .input + %select.medium{:name => "user[gender]"} + %option{:value => ""} Choose your gender: + %option{:value => "Male"} Male + %option{:value => "Female"} Female + + - # Set the gender if it's defined + - unless @user.gender.blank? + :javascript + $('select[name="user[gender]"]').val("#{@user.gender}"); + + %br/ + .button-area + %input.medium.primary.btn{:type => "submit", :value => "Update Information"} + = image_tag("ajax.gif", :class=>"ajax-animation", :size => "16x16") + = image_tag("green_checkmark.png", :class=>"green-checkmark", :size => "16x16") + + + #notifications + .info-message + %strong Note: + All events, other than those marked with an asterisk + = succeed "," do + %strong * + are shown on your + = link_to("Latest Activity", user_latest_activity_path(current_user)) + feed. + + = form_tag(user_account_notifications_path(@user), :method => :put, :remote => true, :id => "update-notifications") do + = check_box("user", "send_email_for_private_channel_request", { }, "true", "false") + Send me an e-mail when someone requests access to one of my private channels + %br/ + = check_box("user", "send_email_for_new_subscriber", { }, "true", "false") + Send me an e-mail when someone subscribes to one of my channels + %br/ + = check_box("user", "send_email_for_new_badges", { }, "true", "false") + Send me an e-mail when I receive a new badge on my videos + %br/ + = check_box("user", "send_email_for_new_comments", { }, "true", "false") + Send me an e-mail when I receive a new comment on my videos + %br/ + = check_box("user", "send_email_for_replies_to_a_prior_comment", { }, "true", "false") + Send me an e-mail when someone comments on a video that I have also commented on + -# %br/ + -# = check_box("user", "send_email_for_featured_video", { }, "true", "false") + -# Send me an e-mail when one of my videos is featured + %br/ + = check_box("user", "send_email_for_encoding_completion", { }, "true", "false") + * Send me an e-mail when my uploaded video is done encoding and is ready to be watched + %br/ + + %br/ + .button-area + %input.medium.primary.btn{:type => "submit", :value => "Update Notifications"} + = image_tag("ajax.gif", :class=>"ajax-animation", :size => "16x16") + = image_tag("green_checkmark.png", :class=>"green-checkmark", :size => "16x16") + + + #connected-services + .info-message + This allows you to login with one click versus remembering your email/password. + It will also give you the option of easily posting videos to your social networks. + %br/ + %br/ + %strong + Note: + We will NEVER post to your Facebook or Twitter timeline without your consent. + + %ul.unstyled + %li.mvm + - if @facebook_connected + = image_tag("green_checkmark.png", :size => "16x16") + %strong Your Facebook account is currently connected. + (#{link_to("Disconnect", user_social_network_path(current_user, @facebook_connected, :provider => "facebook"), :remote => true, "data-method" => "DELETE")}) + - else + = link_to(image_tag("social_connect_facebook.png", :size => "200x38"), "#{root_url}auth/facebook") + %li.mvm + - if @twitter_connected + = image_tag("green_checkmark.png", :size => "16x16") + %strong Your Twitter account is currently connected. + (#{link_to("Disconnect", user_social_network_path(current_user, @twitter_connected, :provider => "twitter"), :remote => true, "data-method" => "DELETE")}) + - else + = link_to(image_tag("social_connect_twitter.png", :size => "200x38"), "#{root_url}auth/twitter") + + + #password + .info-message + %strong + Note: + Your password must be at least 6 characters long. Try to choose one that someone else could not easily guess. + + = form_tag(user_account_password_path(@user), :method => "PUT", :remote => true, :id => "update-password") do + %label Old Password + .input + = password_field_tag :old_password, nil, :placeholder => "old password" + %label New password: + .input + = password_field_tag :new_password, nil, :placeholder => "new password" + %label Confirm password: + .input + = password_field_tag :confirm_new_password, nil, :placeholder => "confirm new password" + + %br/ + .button-area + %input.medium.primary.btn{:type => "submit", :value => "Update"} + = image_tag("ajax.gif", :class=>"ajax-animation", :size => "16x16") + = image_tag("green_checkmark.png", :class=>"green-checkmark", :size => "16x16") + + + #blocked-people + - ze_blocked_users ||= @user.people_they_are_blocking + - if ze_blocked_users.empty? + .info-message + You are not currently blocking any people. + - else + %ul + - ze_blocked_users.each do |u| + %li + %strong #{u.name} + = link_to("(unblock)", user_unblock_path(u), :class => "unblock-user", :remote => true, :method => :delete) + + +- # Tell the page to scroll down to the content wrapper upon page load += render 'shared/scroll_to_content_wrapper' \ No newline at end of file diff --git a/app/views/users/edit_banner.html.haml b/app/views/users/edit_banner.html.haml new file mode 100755 index 0000000..0db31c4 --- /dev/null +++ b/app/views/users/edit_banner.html.haml @@ -0,0 +1,53 @@ +- # User Banner Image += render 'users/top_banner' + +- # Middle User Info Section += render 'users/middle_user_banner' + +- # Dark Content Wrapper +.lower-wrapper + + - # Main (White) Content Wrapper + .content-wrapper.standard-padding + + - # Set up the tabs + %ul.tabs + %li.active + %a{:href => "#upload-image"} Upload a Banner + %li + %a{:href => "#choose-from-gallery"} Choose one from the Gallery + + + - # Set up the content areas for the tabs + .pill-content + #upload-image.active + .info-message + %strong Note: + Maximum file size of 5 MB. Please keep it clean and only upload your own images. + + #progress-bar + .progress{:style => "width: 0%;"} + %span{:style => "display:none;"} + Uploading + + :plain + #{s3_uploader('banner')} + %a{:id => "select-banner", :href => "#", :class => "medium primary btn"} Select Image + + #choose-from-gallery + .info-message + %strong Note: + The images shown below are protected by copyright laws and are made available to Brevidy by Rob Phillips and + = succeed "." do + = link_to "Court Whelan", "http://cwhelanphotography.com", :target => "_blank" + You may + %strong + only + use them on Brevidy or within screenshots of Brevidy. + + .banner-area + - @banner_images.each do |b| + = link_to(image_tag("banners/small/#{b.filename}", :size => "250x93"), user_update_banner_from_gallery_path(current_user, :banner_image_id => b.id), :class => "set-new-banner-image thumbnail", :remote => true, :method => :put) + +- # Tell the page to scroll down to the content wrapper upon page load += render 'shared/scroll_to_content_wrapper' \ No newline at end of file diff --git a/app/views/users/forgotten_password.html.haml b/app/views/users/forgotten_password.html.haml new file mode 100755 index 0000000..edab77c --- /dev/null +++ b/app/views/users/forgotten_password.html.haml @@ -0,0 +1,34 @@ +.forgotten_password + = compact_flash_messages + - unless flash.empty? + %h3{ :class => "mbm flash_#{@flash_key}" } + = @flash_msg + + %h2.mbm + Forgotten Password: + - if @show_password_reset + Please reset your password below + - else + Please enter your email address below + + - if @show_password_reset + - # Show the reset password form + %form.mtl{:action => user_reset_password_path, :method => "post", :id => "forgotten_password_form"} + %input{:name => "#{request_forgery_protection_token}", :type => "hidden", :value => "#{form_authenticity_token}"} + %input{:name => "token", :type => "hidden", :value => "#{@token}"} + .forgotten_password_form + = password_field_tag :password, nil, :placeholder => "password", :class => "ras" + = password_field_tag :password_confirmation, nil, :placeholder => "confirm password", :class => "ras" + %input.ram.medium.silver.button{:type => "submit", :name => "submit", :value => "Reset Password"} + + = render :partial => 'users/signup_box' + + - else + - # Show the forgotten password form + %form.mtl{:action => validate_forgotten_password_path, :method => "post", :id => "forgotten_password_form"} + %input{:name => "#{request_forgery_protection_token}", :type => "hidden", :value => "#{form_authenticity_token}"} + .forgotten_password_form + = text_field_tag :email, nil, :placeholder => "e-mail address", :class => "ras" + %input.mls.mvm.ram.medium.silver.button{:type => "submit", :name => "submit", :value => "Submit"} + + = render :partial => 'users/signup_box' \ No newline at end of file diff --git a/app/views/users/index.html.haml b/app/views/users/index.html.haml new file mode 100755 index 0000000..39bea1f --- /dev/null +++ b/app/views/users/index.html.haml @@ -0,0 +1,24 @@ +- # Dark Content Wrapper +.lower-wrapper + = render 'users/featured_videos' + + - # Main (White) Content Wrapper + .content-wrapper + = render 'videos/explore_content_titles' + + - if signed_in? + - # Invitation sell + - if more_people_can_be_invited? + .info-message.mhl.centered + = link_to("Invite your friends and family", user_invitations_path(current_user)) + to join Brevidy and start sharing videos with them today! + + = cache("find_people_page", :expires_in => 15.minutes) do + - # Search form + = form_tag user_search_path, :method => :get, :class => "user-search" do + = text_field_tag :q, @term, :placeholder => "Search for a name or email" + %span.search + %button{:type => "submit", :value => "Go"} + + %ul.user-listing + = render User.show_random_people \ No newline at end of file diff --git a/app/views/users/new.html.haml b/app/views/users/new.html.haml new file mode 100755 index 0000000..8eaa25b --- /dev/null +++ b/app/views/users/new.html.haml @@ -0,0 +1,44 @@ +/ what is brevidy +.pitch_container + .pitch.left + .pitch_img.left + = image_tag("fleeting_moments.png", :size => "175x88") + %ul.right.mts.mll + %li + %h1 Share the fleeting moments of your life on video + %li.mlm.mtm + %h2 + Upload videos from your computer or share them from YouTube or Vimeo! + .mtm + Make videos public for the world to see, or make them private to share only with your family and friends. + The choice is yours. + + .pitch.left.mtl + .pitch_img.left + = image_tag("connect_people.png", :size => "96x125", :class => "connect_people") + %ul.right.mll.connect_people + %li + %h1 Connect with people around you + %li.mlm.mtm + %h2 + Subscribe to channels that interest you! Give people badges to express exactly how you feel about their videos! + Share other people's videos that you find interesting! + + .pitch.left.mtl + .pitch_img.left + = image_tag("social_sharing.png", :size => "129x115", :class => "social_sharing") + %ul.right.mll.social_sharing + %li + %h1 Let others join in the fun + %li.mlm.mtm + %h2 + Easily share any video to your social networks! Send your family and friends a link so they can + watch your private videos without signing up! + + .signup.left + .social_buttons + = link_to(image_tag("social_signup_facebook.png", :size => "225x43", :class => "left"), "#{root_url}auth/facebook") + = link_to(image_tag("social_signup_twitter.png", :size => "225x43", :class => "right"), "#{root_url}auth/twitter") + + .old_fashioned.left.tcenter + = link_to("...or sign up the old fashioned way", signup_path) \ No newline at end of file diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml new file mode 100755 index 0000000..800ec21 --- /dev/null +++ b/app/views/users/show.html.haml @@ -0,0 +1,23 @@ +- # User Banner Image += render 'users/top_banner' + +- # Middle User Info Section += render 'users/middle_user_banner' + +- # Dark Content Wrapper +.lower-wrapper + = render 'users/featured_videos' + + - # Main (White) Content Wrapper + .content-wrapper + = render 'shared/content_titles' + + - if @videos.blank? + .info-message.mhl + There are no videos to show. + - else + = render @videos + + = will_paginate @videos + + = render :partial => "shared/infinite_scrolling", :locals => { :item_selector => ".video-post" } \ No newline at end of file diff --git a/app/views/users/show.js.haml b/app/views/users/show.js.haml new file mode 100755 index 0000000..9ea505c --- /dev/null +++ b/app/views/users/show.js.haml @@ -0,0 +1 @@ += render @videos \ No newline at end of file diff --git a/app/views/users/signup.html.haml b/app/views/users/signup.html.haml new file mode 100755 index 0000000..4bf6c00 --- /dev/null +++ b/app/views/users/signup.html.haml @@ -0,0 +1,53 @@ +.old_fashioned_signup_container + + / flash notices + = compact_flash_messages + - unless flash.empty? + %h3{ :class => "mbm flash_#{@flash_key}" } + = @flash_msg + + .formErrorField.js-validate.mbm + - # validation error field placeholder + - # do not remove + + - if defined?(social_signup) + - # these instance vars get passed into the form + - @user = user + - @provider = provider + - @uid = uid + - @oauth_token = oauth_token + - @oauth_token_secret = oauth_token_secret + - @social_signup = social_signup + + %h2.tcenter + Hooray!!! You're almost done signing up! :) + + .old_fashioned_signup + = render :partial => 'users/signup_form' + + %h4.light.mtl.ptm.borderTopDotted + Note: If you already have a Brevidy account, login using your e-mail / password and you can connect your Facebook or Twitter + account within your Account settings. This will allow you to login with Facebook or Twitter on future visits. + + - else + .social_signup_buttons.left + %h2.tcenter + Quick Sign up + + = link_to(image_tag("social_signup_facebook.png", :size => "225x43", :class => "left mtl"), "#{root_url}auth/facebook") + = link_to(image_tag("social_signup_twitter.png", :size => "225x43", :class => "left mtl"), "#{root_url}auth/twitter") + + .old_fashioned_signup.right + %h2.tcenter + Sign up the old fashioned way + + = render :partial => 'users/signup_form' + +:javascript + // Description: Shows & hides AJAX loading GIF when necessary + $(document).bind('ajaxSend', function(e, request, options) { + $(".signup_ajax_animation").show(); + }); + $(document).bind('ajaxComplete', function(e, request, options) { + $(".signup_ajax_animation").hide(); + }); \ No newline at end of file diff --git a/app/views/users/subscriptions_stream.html.haml b/app/views/users/subscriptions_stream.html.haml new file mode 100755 index 0000000..d054de4 --- /dev/null +++ b/app/views/users/subscriptions_stream.html.haml @@ -0,0 +1,23 @@ +- # User Banner Image += render 'users/top_banner' + +- # Middle User Info Section += render 'users/middle_user_banner' + +- # Dark Content Wrapper +.lower-wrapper + = render 'users/featured_videos' + + - # Main (White) Content Wrapper + .content-wrapper + = render 'shared/content_titles' + + - if @videos.blank? + .info-message.mhl + There are no videos in your stream to show. + - else + = render @videos + + = will_paginate @videos + + = render :partial => "shared/infinite_scrolling", :locals => { :item_selector => ".video-post" } \ No newline at end of file diff --git a/app/views/users/subscriptions_stream.js.haml b/app/views/users/subscriptions_stream.js.haml new file mode 100755 index 0000000..9ea505c --- /dev/null +++ b/app/views/users/subscriptions_stream.js.haml @@ -0,0 +1 @@ += render @videos \ No newline at end of file diff --git a/app/views/videos/_add_to_channel_dialog.html.haml b/app/views/videos/_add_to_channel_dialog.html.haml new file mode 100755 index 0000000..791d247 --- /dev/null +++ b/app/views/videos/_add_to_channel_dialog.html.haml @@ -0,0 +1,35 @@ +#brevidy-modal.modal.hide + = form_tag(user_add_to_channel_path(current_user), :id => "add-to-channel-form", :method => "POST", :remote => true) do + .modal-header + %h3 Add this video to one of your channels + .modal-body + - if current_user_owns?(video) + .add-to-channel-warning + .p + %strong Note: + You are about to create a copy of a video you already posted. Did you want to + = link_to("organize this video into a different channel", edit_user_video_path(current_user, video)) + instead? + .p.mtxl + = link_to("Click here if you still want to create a copy of this video", "#", :class => "show-add-video-meta-area") + + .add-video-meta-area{:style => current_user_owns?(video) ? "display:none" : ""} + .video-preview-area + %input{:name => "video_id", :value => video.id, :type => "hidden"} + = image_tag(video.get_thumbnail_url(video.selected_thumbnail), :alt => video.title, :size => "100x54") + %p.title + %strong #{video.title} + %p.description #{truncate(video.description, :length => 100, :omission => '...')} + + .channel-selection-area + %p + %strong Which channel should it go in? + = render 'videos/channel_options' + + .modal-footer + .ajax-errors + - # placeholder for ajax errors to be shown + + = submit_tag("Add to Channel", :id => "add-to-channel", :class => "modal-confirm-btn btn primary", :style => current_user_owns?(video) ? "display:none" : "") + %a.modal-cancel-btn.btn.secondary{:href => "#"} Cancel + = image_tag("ajax.gif", :class=>"ajax-animation", :size => "16x16") \ No newline at end of file diff --git a/app/views/videos/_channel_options.html.haml b/app/views/videos/_channel_options.html.haml new file mode 100755 index 0000000..d15fd86 --- /dev/null +++ b/app/views/videos/_channel_options.html.haml @@ -0,0 +1,30 @@ +.channel-select-elements + %select#select-a-channel-via-upload.medium{:name => "channel_id"} + - current_user.channels.each do |c| + %option{:value => "#{c.id}"} #{c.title} + %option.dash-divider{:value => "divider", :disabled => "disabled"} ----------------- + %option{:value => "add_to_new_channel"} Add to new channel + + .add-to-new-channel-via-upload-area + = image_tag("right_arrow_icon.png", :alt => "", :size => "25x25") + = text_field_tag(:channel_name, nil, :placeholder => "Name your new channel") + %label + %input{:type => "checkbox", :name => "channel_is_private", :value => "true"} + %span Private? + + :javascript + // Check if we are adding a new channel + $('#select-a-channel-via-upload').live('change', function () { + var selected_val = $('#select-a-channel-via-upload option:selected').val(); + + if (selected_val == "add_to_new_channel") { + $('.add-to-new-channel-via-upload-area').css('display', 'inline'); + } else { + $('.add-to-new-channel-via-upload-area').fadeOut('fast'); + } + }); + + - # Set the video channel value if need be + - if defined?(@video) + :javascript + $('#select-a-channel-via-upload').val('#{@video.channel.id}'); \ No newline at end of file diff --git a/app/views/videos/_embed_code.html.haml b/app/views/videos/_embed_code.html.haml new file mode 100755 index 0000000..0de824b --- /dev/null +++ b/app/views/videos/_embed_code.html.haml @@ -0,0 +1,32 @@ +- if defined?(individual_player) + - ze_autostart = false + - ze_page_type = "individual" +- else + - ze_autostart = true + - ze_page_type = "regular" + +- # Checks if the video is a YouTube/Vimeo player +- video_is_not_remote ||= video.remote_video_id.blank? + +- if video_is_not_remote + :javascript + jwplayer("brevidy-player-#{video.id}").setup({ + aboutlink: "http://www.brevidy.com", + abouttext: "Brevidy", + allowfullscreen: true, + autostart: #{ze_autostart}, + controlbar: "over", + flashplayer: "#{javascript_path('player/player.swf')}", + file: "#{video.generate_secure_cf_url}", + height: #{Video::PLAYER_HEIGHT}, + skin: "#{javascript_path('player/skins/beelden.zip')}", + stretching: "uniform", + volume: 100, + width: #{Video::PLAYER_WIDTH}, + wmode: "opaque" + }); + +- else + - # Video is remote (YouTube, Vimeo, etc.) so let's embed it + :plain + #{video.get_html5_iframe_code(page_type = ze_page_type)} \ No newline at end of file diff --git a/app/views/videos/_explore_content_titles.html.haml b/app/views/videos/_explore_content_titles.html.haml new file mode 100755 index 0000000..297b5f6 --- /dev/null +++ b/app/views/videos/_explore_content_titles.html.haml @@ -0,0 +1,13 @@ +.content-titles + %ul + -#%li + -# = link_to("Popular Videos", explore_path, :class => "lighten-to-blue") + + -#%li + -# = link_to("Suggested Channels", suggested_channels_path, :class => "lighten-to-blue") + + %li + = link_to("Recent Videos", explore_path, :class => "lighten-to-blue") + + %li + = link_to("Find People", find_people_path, :class => "lighten-to-blue") \ No newline at end of file diff --git a/app/views/videos/_flag_video_dialog.html.haml b/app/views/videos/_flag_video_dialog.html.haml new file mode 100755 index 0000000..753f022 --- /dev/null +++ b/app/views/videos/_flag_video_dialog.html.haml @@ -0,0 +1,24 @@ +#brevidy-modal.modal.hide + = form_tag(user_video_flag_path(@user, @video), :id => "flag-video-form", :method => "POST", :remote => true) do + .modal-header + %h3 Report an error or a concern you may have + .modal-body + .flagging-area + %ul + - Video.get_all_flags.each do |f| + %li + %input{:name => "flag_id", :type => "radio", :value => "#{f.id}"}/ + = f.reason + + %li + %label + Provide more details (optional): + = text_area_tag(:detailed_reason, nil, :placeholder => "Optional") + + .modal-footer + .ajax-errors + - # placeholder for ajax errors to be shown + + = submit_tag("Flag Video", :id => "flag-video", :class => "modal-confirm-btn btn primary") + %a.modal-cancel-btn.btn.secondary{:href => "#"} Cancel + = image_tag("ajax.gif", :class=>"ajax-animation", :size => "16x16") \ No newline at end of file diff --git a/app/views/videos/_share_dialog.html.haml b/app/views/videos/_share_dialog.html.haml new file mode 100755 index 0000000..b15b113 --- /dev/null +++ b/app/views/videos/_share_dialog.html.haml @@ -0,0 +1,45 @@ +#brevidy-modal.modal.hide + = form_tag(user_create_shared_video_path(current_user), :id => "share-a-link-form", :method => "POST", :remote => true) do + .modal-header + %h3 Share a Link + .modal-body + %p.link-title + %strong Paste a YouTube or Vimeo link to share + = text_field_tag(:shared_video_link, nil, :placeholder => "http://www.youtube.com/watch?v=0FwOtHso5Wg") + + %p.channel-title + %strong Choose a channel to share it into + + .channel-select-elements + %select#select-a-channel-via-share.medium{:name => "channel_id"} + - current_user.channels.each do |c| + %option{:value => "#{c.id}"} #{c.title} + %option.dash-divider{:value => "divider", :disabled => "disabled"} ----------------- + %option{:value => "add_to_new_channel"} Add to new channel + + .add-to-new-channel-area + = image_tag("right_arrow_icon.png", :alt => "", :size => "25x25") + = text_field_tag(:channel_name, nil, :placeholder => "Name your new channel") + %label + %input{:type => "checkbox", :name => "channel_is_private", :value => "true"} + %span Private? + + :javascript + // Check if we are adding a new channel + $('#select-a-channel-via-share').live('change', function () { + var selected_val = $('#select-a-channel-via-share option:selected').val(); + + if (selected_val == "add_to_new_channel") { + $('.add-to-new-channel-area').css('display', 'inline'); + } else { + $('.add-to-new-channel-area').fadeOut('fast'); + } + }); + + .modal-footer + .ajax-errors + - # placeholder for ajax errors to be shown + + = submit_tag("Share", :id => "share-a-link", :class => "modal-confirm-btn btn primary") + %a.modal-cancel-btn.btn.secondary{:href => "#"} Cancel + = image_tag("ajax.gif", :class=>"ajax-animation", :size => "16x16") \ No newline at end of file diff --git a/app/views/videos/_video.html.haml b/app/views/videos/_video.html.haml new file mode 100755 index 0000000..1c04a78 --- /dev/null +++ b/app/views/videos/_video.html.haml @@ -0,0 +1,249 @@ +- # Initialize some objects for each video +- # such as badges, comments, tags, etc. +- video_owner ||= get_object_owner(video) +- comments ||= video.comments +- tags ||= video.tags +- badges_count ||= video.badges.count +- unique_badge_types_and_counts ||= video.unique_badge_types_and_counts + +- # checks that the video isn't processing so we will +- # allow the user to interact with it (play it, badge it, etc.) +- video_is_clickable ||= video.is_status?(VideoGraph::READY) + +- # checks whether we should expand the video or not +- expand_the_video = defined?(expand_video) ? expand_video : false + +- # Checks if the video is a YouTube/Vimeo player +- video_is_not_remote ||= video.remote_video_id.blank? + +.video-post{"data-video-id"=>"#{video.id}"} + .row.heading + = link_to(user_path(video_owner)) do + = image_tag("#{video_owner.image.blank? ? 'default_user_50px.jpg' : video_owner.image_url(:medium_profile) }", :alt => "#{video_owner.name}", :size => "50x50") + .video-title-and-info + %h3 + = video.title + %p + Shared by + = link_to(video_owner.name, user_path(video_owner)) + into their + = link_to(video.channel.title, user_channel_path(video_owner, video.channel)) + channel + %span.gray + - begin + - if video_is_not_remote + - via_user = video.video_graph.get_user_who_uploaded_this + - unless via_user.blank? || (video_owner.id == via_user.id) + (via #{link_to(via_user.name, user_path(via_user))}) + - rescue + + #{time_ago_in_words(video.created_at)} ago + + - # Settings drop down menu + %ul.settings-menu + %li.dropdown{"data-dropdown" => "dropdown"} + %a.settings-icon.dropdown-toggle.prevent-jump{:href => "#"} + %i + %ul.dropdown-menu + %li + = link_to("Flag Video", user_video_flag_dialog_path(video_owner, video), :remote => true, "data-method" => "GET") + - if current_user_owns?(video) + %li + = link_to("Edit Video", edit_user_video_path(video_owner, video)) + %li + = link_to("Delete Video", user_video_path(video_owner, video), :class => "delete-video", "data-video-id" => "#{video.id}") + + .row.video-meta + .player-area{"data-video-id"=>"#{video.id}", :style => expand_the_video ? "display:block" : ""} + .collapse-player-controls + %a.pull-right{:href => "#", "data-video-id" => "#{video.id}"} Close video + + .player-object{:id => "brevidy-player-#{video.id}"} + - if expand_the_video + = render :partial => 'videos/embed_code', :locals => { :video => video, :individual_player => true } + - else + - # Player code will be AJAX'ed in here + + .thumbnail-and-badges + %a{:href => "#"} + - path_to_this = @viewing_via_token_access ? user_video_embed_code_path(video_owner, video, :channel_token => video.channel.public_token) : user_video_embed_code_path(video_owner, video) + .thumbnail{:class => video_is_clickable ? "" : "processing", "data-video-id"=>"#{video.id}", "data-video-path" => video_is_clickable ? "#{path_to_this}" : "", :style => expand_the_video ? "display:none" : ""} + = image_tag(video_is_clickable ? video.get_thumbnail_url(video.selected_thumbnail) : "processing.png", :alt => video.title, :size => "250x134") + - if video_is_clickable + %i.play-icon + = image_tag("play.png", :alt => "Play", :size => "60x45") + + - if video_is_clickable + .show-badges{"data-video-id"=>"#{video.id}"} + - if badges_count == 0 + %p.no-badges-given-yet{:class => (badges_count != 0) ? "mtm" : ""} + No badges given yet. + + - if signed_in? + %a.give-first-badge{:href=>"#", "data-video-id"=>"#{video.id}"} Give one! + - else + .badges + %ul + - unique_badge_types_and_counts[:badge_sets].first(5).each do |icon, count| + = render :partial => "badges/badge", :locals => { :icon => icon, :count => count } + + %p.view-all-badges{"data-video-id"=>"#{video.id}"} + = render :partial => "badges/view_all_badges.html", + :locals => { :badges_count => badges_count, :video_owner => video_owner, :video => video } + + .description-and-controls + .description.expandable + - if video.description.blank? + %i No description given for this video + - else + = simple_format(auto_link(h(video.description), :html => { :target => "_blank" }), {}, :sanitize => false) + + - if video_is_clickable + .controls + %ul + - if current_user_owns?(video) || !video.channel.private? + %li.video-control-link + - if current_user.blank? + = link_to(login_path, :class => "show-msg-modal", "data-modal-title" => "Please login or sign up", "data-modal-message" => "You must Login or Sign up before you can add a video to a channel.") do + = image_tag("add_to_channel.png", :alt => "Channel Icon", :size => "13x13", :class => "control-icon") + %span>< Add To Channel + - else + = link_to(user_add_to_channel_dialog_path(current_user, :video_id => video.id), :class => "add-to-channel-link", "data-video-id" => "#{video.id}", :remote => true, "data-method" => "GET") do + = image_tag("add_to_channel.png", :alt => "Channel Icon", :size => "13x13", :class => "control-icon") + %span>< Add To Channel + + %li.video-control-link + %a.badge-link{:href => "#", "data-video-id" => "#{video.id}"} + = image_tag("badge_it.png", :alt => "Badge Icon", :size => "13x13", :class => "control-icon") + %span>< Badge It + %span.badge-arrow{"data-video-id" => "#{video.id}"} + + %li.video-control-link + %a.comment-link{:href => "#", "data-video-id" => "#{video.id}"} + = image_tag("comment.png", :alt => "Comment Icon", :size => "13x13", :class => "control-icon") + %span>< Comment + %span.comment-arrow{"data-video-id" => "#{video.id}", :style => comments.blank? ? "display: none" : ""} + + - if current_user_owns?(video) || !video.channel.private? + %li.video-control-link + %a.share-link{:href => "#", "data-video-id" => "#{video.id}"} + = image_tag("share.png", :alt => "Share Icon", :size => "13x13", :class => "control-icon") + %span>< Share + %span.share-arrow{"data-video-id" => "#{video.id}"} + + %li.video-control-link + %a.tag-link{:href => "#", "data-video-id" => "#{video.id}"} + = image_tag("tags.png", :alt => "Tags Icon", :size => "13x13", :class => "control-icon") + %span>< Tags + %span.tag-arrow{"data-video-id" => "#{video.id}"} + + %li.ajax-animation{"data-video-id" => "#{video.id}"} + = image_tag("ajax.gif", :size => "16x16") + + - # ----------- + - # BADGES AREA + - # ----------- + .badges-area{"data-video-id" => "#{video.id}"} + - # Render all of the active badges to choose from + = render :partial => "badges/give_badges.html", :collection => get_all_active_badges, + :locals => { :video => video, + :video_owner => video_owner, + :all_badge_types => unique_badge_types_and_counts[:all_badge_types] } + + - # ------------- + - # COMMENTS AREA + - # ------------- + .comments-area{"data-video-id" => "#{video.id}", :style => comments.blank? ? "display: none" : ""} + - if current_user.blank? && comments.blank? + %p.no-content-msg + %i No one has commented on this video yet. + + - else + %ul.comments-list + - if comments.size > 3 + %li.show-all-comments + %a.show-all-comments{:href => "#", "data-video-id" => "#{video.id}"} + Show all #{comments.size} comments + + - # Hide everything except the latest 3 comments + = render :partial => "comments/comment.html", + :collection => comments[-comments.size..-4], + :locals => { :video => video, :video_owner => video_owner, :hidden_comment => true } + + - # Show only the latest 3 comments in ASC order + = render :partial => "comments/comment.html", :collection => comments[-3..-1], + :locals => { :video => video, :video_owner => video_owner, :hidden_comment => false } + + - else + = render :partial => "comments/comment.html", :collection => comments, + :locals => { :video => video, :video_owner => video_owner, :hidden_comment => false } + + - unless current_user.blank? + .comment-box + .comment-form-wrapper + = form_tag(user_video_comments_path(video_owner, video), :method => "POST", :remote => true, :class => "post-new-comment", "data-video-id" => "#{video.id}") do + - if @viewing_via_token_access + %input{:name => "channel_token", :type => "hidden", :value => "#{video.channel.public_token}"}/ + + = text_area_tag(:content, nil, :autocomplete => :off, "data-video-id" => "#{video.id}", :placeholder => "Write a comment") + .button-wrapper{"data-video-id" => "#{video.id}"} + %input.btn.primary.small{:type => "submit", :value => "Comment", "data-video-id" => "#{video.id}"} + + - # ---------- + - # SHARE AREA + - # ---------- + - if current_user_owns?(video) || !video.channel.private? + .share-area{"data-video-id" => "#{video.id}"} + .share-content + - # the user owns the video or the channel holding the video is public + #{link_to("Social sharing", "#", :class => "share-socially lighten-to-blue")} · #{link_to("Link to this video", "#", :class => "link-to-video")} · #{link_to("Embed this video", "#", :class => "embed-video")} · #{link_to("Email this video", "#", :class => "email-video")} + + - public_url_to_this_video = public_video_url(:public_token => video.public_token) + .social-sharing + / AddThis Button BEGIN + .addthis_toolbox.addthis_default_style.addthis_32x32_style{"addthis:url" => public_url_to_this_video, "addthis:title" => "Check out this video! '#{truncate(video.title, :length => 70, :omission => '...')}'" } + %a.addthis_button_facebook + %a.addthis_button_twitter + %a.addthis_button_stumbleupon + %a.addthis_button_reddit + %a.addthis_button_digg + %a.addthis_button_gmail + %a.addthis_button_compact + / AddThis Button END + + .public-link + %input{:type => "text", :readonly => true, :value => public_url_to_this_video} + + .embed-code + %textarea{:readonly => true} + :plain + + + .email-form + = form_tag(user_video_share_via_email_path(video_owner, video), :class => "share-via-email", :remote => true, :method => "POST") do + %label{:for => "recipient_email"} Email Address + %input{:name => "recipient_email", :type => "text", :placeholder => "email address"} + %label{:for => "personal_message"} Personal Message (optional) + %textarea{:name => "personal_message", :placeholder => "Personalize your email!"} + %input.btn.primary.small{:type => "submit", :value => "Send"} + + + - # --------- + - # TAGS AREA + - # --------- + .tags-area{"data-video-id" => "#{video.id}"} + - if tags.blank? + %p.no-content-msg + %i + There are currently no tags organizing this video. + - if current_user_owns?(video) + = link_to("Give it some", edit_user_video_path(video_owner, video)) + + - else + %ul + - tags.each do |t| + %li + = link_to(t.content, video_search_path(:tag => t.content)) + - if current_user_owns?(video) + = link_to("x", user_video_tag_path(video_owner, video, t), :class => "tooltip remove-tag", + :method => "DELETE", :remote => true, :title => "Remove tag", "data-video-id" => "#{video.id}") diff --git a/app/views/videos/_video_form.html.haml b/app/views/videos/_video_form.html.haml new file mode 100755 index 0000000..771448a --- /dev/null +++ b/app/views/videos/_video_form.html.haml @@ -0,0 +1,80 @@ +.channel + %h3 Which channel should it go in? + .channel-select-elements + %select#select-a-channel-via-upload.medium{:name => "channel_id"} + - current_user.channels.each do |c| + %option{:value => "#{c.id}"} #{c.title} + %option.dash-divider{:value => "divider", :disabled => "disabled"} ----------------- + %option{:value => "add_to_new_channel"} Add to new channel + + .add-to-new-channel-via-upload-area + = image_tag("right_arrow_icon.png", :alt => "", :size => "25x25") + = text_field_tag(:channel_name, nil, :placeholder => "Name your new channel") + %label + %input{:type => "checkbox", :name => "channel_is_private", :value => "true"} + %span Private? + + :javascript + // Check if we are adding a new channel + $('#select-a-channel-via-upload').live('change', function () { + var selected_val = $('#select-a-channel-via-upload option:selected').val(); + + if (selected_val == "add_to_new_channel") { + $('.add-to-new-channel-via-upload-area').css('display', 'inline'); + } else { + $('.add-to-new-channel-via-upload-area').fadeOut('fast'); + } + }); + + - # Set the video channel value if need be + - if defined?(@video) + :javascript + $('#select-a-channel-via-upload').val('#{@video.channel.id}'); + +.title + %h3 Title it + = text_field(:video, :title, :maxlength => "75", :placeholder => "Give your video a title") + + %span.chars-remaining 75 characters left + +.description + %h3 Describe it (optional) + = text_area(:video, :description, :maxlength => "1000", :size => "40x5", :placeholder => "(Optional) Describe your video here so everyone knows what it is about") + + %span.chars-remaining 1000 characters left + +.tags + %h3 Organize it with Tags (separated by commas) + + - # show current tags if we're editing + - unless uploading_a_video + - tags ||= @video.tags + - unless tags.blank? + .tags-area + %ul + - tags.each do |t| + %li + = link_to(t.content, video_search_path(:tag => t.content)) + = link_to("x", user_video_tag_path(get_object_owner(@video), @video, t), :class => "tooltip remove-tag", + :method => "DELETE", :remote => true, :title => "Remove tag", "data-video-id" => "#{@video.id}") + + %textarea{:name => "video_tags", :placeholder => "please, separate, all, of, your, tags, with, commas"} + + +- if uploading_a_video + .social-options + %span + = check_box("video", "send_to_facebook", { :disabled => @facebook_connected.blank? }, true, false) + Post this video to Facebook + %span + = check_box("video", "send_to_twitter", { :disabled => @twitter_connected.blank? }, true, false) + Post this video to Twitter + +.buttons-area + = submit_tag("Save Video", :class => "medium primary btn" ) + +- # Tell them about social options +- if uploading_a_video + - if @facebook_connected.blank? or @twitter_connected.blank? + You can post your videos to your Facebook or Twitter account by connecting those accounts to Brevidy + within your #{link_to("Account Settings", user_account_path(current_user))} page. \ No newline at end of file diff --git a/app/views/videos/_video_sidebar.html.haml b/app/views/videos/_video_sidebar.html.haml new file mode 100755 index 0000000..33e20d5 --- /dev/null +++ b/app/views/videos/_video_sidebar.html.haml @@ -0,0 +1,9 @@ +.info-message.mbl + %p + %strong Note: + Videos will be trimmed to 10 minutes in length and can only be a maximum of 750 MB in size. + #{link_to("Read more of the frequently asked questions about videos", video_faq_path)} + + %p + %strong General guidelines: + No nudity, pornography, racism, hate speech, violence, or copyrighted material. #{link_to("View all community guidelines", video_guidelines_path)} \ No newline at end of file diff --git a/app/views/videos/add_to_channel_dialog.js.haml b/app/views/videos/add_to_channel_dialog.js.haml new file mode 100755 index 0000000..d4ebc92 --- /dev/null +++ b/app/views/videos/add_to_channel_dialog.js.haml @@ -0,0 +1,39 @@ +:plain + // Setup a custom template to use + var html_template = '#{escape_javascript(render(:partial => 'videos/add_to_channel_dialog.html', :locals => { :video => @video }))}'; + + // Show the custom dialog + brevidy.custom_dialog(html_template); + + // Bind the ajax animation + $(document).bind('ajaxStart', function(e, request, options) { + $('#add-to-channel-form .ajax-animation').show(); + $('#add-to-channel-form .ajax-errors').fadeOut("fast"); + }); + $(document).bind('ajaxComplete', function(e, request, options) { + $('#add-to-channel-form .ajax-animation').hide(); + }); + + // Handle success message and removal of dialog + $('#add-to-channel-form').live('ajax:success', function(data, json, response) { + $('.modal-header h3').text('Yay!'); + $('.video-preview-area, .channel-selection-area, .modal-footer .btn').fadeOut('fast', function() { + $('.video-preview-area').html('

Video was successfully shared!

').fadeIn('fast'); + setTimeout(function() { $('#brevidy-modal').modal('hide').remove(); }, 1500) + }); + }); + + // Description: Handles AJAX error + $('#add-to-channel-form').live('ajax:error', function(data, xhr, response) { + // get error message + var responseText = jQuery.parseJSON(xhr.responseText); + if (responseText !== null) { var responseMsg = responseText.error; } + if (typeof responseMsg == 'undefined') { responseMsg = 'There was an unknown error or the request timed out. Please try again later'; } + + // set message + $('#add-to-channel-form .ajax-errors').fadeOut("fast", function() { + $(this).text(responseMsg).fadeIn('fast'); + }); + + return false; + }); \ No newline at end of file diff --git a/app/views/videos/edit.html.haml b/app/views/videos/edit.html.haml new file mode 100755 index 0000000..7dfb0ad --- /dev/null +++ b/app/views/videos/edit.html.haml @@ -0,0 +1,38 @@ +- # Dark Content Wrapper +.lower-wrapper.no-header + + - # Main (White) Content Wrapper + .content-wrapper + + .video-meta-area + + %h3.video-meta-title Edit this video + + = form_tag(user_video_path(get_object_owner(@video), @video, :redirect => true), :id => "update-video-form", :method => "PUT", :remote => true) do + + - # Show the thumbnail selection area if the video is done processing and if it isn't a remote video (youtube or vimeo) + - if @video.is_status?(VideoGraph::READY) && @video.remote_video_id.blank? + .choose-a-thumbnail + %h3 Choose a Thumbnail + %input{:name => "video[selected_thumbnail]", :class => "selected-thumbnail", :value => @video.selected_thumbnail, :type => "hidden"} + %ul + - for thumb_number in (0..3) + - is_selected = (@video.selected_thumbnail == thumb_number) + %li{"data-thumb-number" => "#{thumb_number}", :class => is_selected ? "selected-thumbnail" : ""} + = image_tag(@video.get_thumbnail_url(thumb_number), :size => "180x96", :alt => "Choose a thumbnail") + + - # Show a static thumbnail for a remote, ready video + - if @video.is_status?(VideoGraph::READY) && !@video.remote_video_id.blank? + .static-thumbnail + %h3 Thumbnail + %ul + %li + = image_tag(@video.get_thumbnail_url(@video.selected_thumbnail), :size => "180x96", :alt => "Thumbnail") + + + = render :partial => 'videos/video_form', :locals => {:uploading_a_video => false} + + :javascript + $('#update-video-form').live('ajax:error', function(data, xhr, response){ + brevidy.handle_ajax_error("video", xhr); + }); \ No newline at end of file diff --git a/app/views/videos/explore.html.haml b/app/views/videos/explore.html.haml new file mode 100755 index 0000000..8b92f07 --- /dev/null +++ b/app/views/videos/explore.html.haml @@ -0,0 +1,14 @@ +- # Dark Content Wrapper +.lower-wrapper + = render 'users/featured_videos' + + - # Main (White) Content Wrapper + .content-wrapper + = render 'videos/explore_content_titles' + + - unless @videos.blank? + = render @videos + + = will_paginate @videos + + = render :partial => "shared/infinite_scrolling", :locals => { :item_selector => ".video-post" } \ No newline at end of file diff --git a/app/views/videos/explore.js.haml b/app/views/videos/explore.js.haml new file mode 100755 index 0000000..9ea505c --- /dev/null +++ b/app/views/videos/explore.js.haml @@ -0,0 +1 @@ += render @videos \ No newline at end of file diff --git a/app/views/videos/flag_video_dialog.js.haml b/app/views/videos/flag_video_dialog.js.haml new file mode 100755 index 0000000..0d50dbe --- /dev/null +++ b/app/views/videos/flag_video_dialog.js.haml @@ -0,0 +1,39 @@ +:plain + // Setup a custom template to use + var html_template = '#{escape_javascript(render(:partial => 'videos/flag_video_dialog.html'))}'; + + // Show the custom dialog + brevidy.custom_dialog(html_template); + + // Bind the ajax animation + $(document).bind('ajaxStart', function(e, request, options) { + $('#flag-video-form .ajax-animation').show(); + $('#flag-video-form .ajax-errors').fadeOut("fast"); + }); + $(document).bind('ajaxComplete', function(e, request, options) { + $('#flag-video-form .ajax-animation').hide(); + }); + + // Handle success message and removal of dialog + $('#flag-video-form').live('ajax:success', function(data, json, response) { + $('.modal-header h3').text('Thank you'); + $('.flagging-area, .modal-footer .btn').fadeOut('fast', function() { + $('.flagging-area').html('

We will look into this and determine the best course of action.

').fadeIn('fast'); + setTimeout(function() { $('#brevidy-modal').modal('hide').remove(); }, 3000) + }); + }); + + // Description: Handles AJAX error + $('#flag-video-form').live('ajax:error', function(data, xhr, response) { + // get error message + var responseText = jQuery.parseJSON(xhr.responseText); + if (responseText !== null) { var responseMsg = responseText.error; } + if (typeof responseMsg == 'undefined') { responseMsg = 'There was an unknown error or the request timed out. Please try again later'; } + + // set message + $('#flag-video-form .ajax-errors').fadeOut("fast", function() { + $(this).text(responseMsg).fadeIn('fast'); + }); + + return false; + }); \ No newline at end of file diff --git a/app/views/videos/new.html.haml b/app/views/videos/new.html.haml new file mode 100755 index 0000000..dc52a5f --- /dev/null +++ b/app/views/videos/new.html.haml @@ -0,0 +1,64 @@ +- # Dark Content Wrapper +.lower-wrapper.no-header + + - # Main (White) Content Wrapper + .content-wrapper + + .video-meta-area + + %h3.video-meta-title Upload a Video + + - if @error_creating_video_object + .info-message + Video uploads are unavailable at this time; please try again later. We have been notified about this issue. + - else + = render :partial => 'videos/video_sidebar' + + .success-message.video-saved + %p + Video information saved. + + #progress-bar + .progress{:style => "width: 0%;"} + %span{:style => "display:none;"} + Uploading + + .uploader-area + :plain + #{s3_swf_uploader('video')} + %a{:id => "select-video", :href => "#", :class => "medium primary btn"} Select Video + + + - # Meta data form + = form_tag(user_video_path(current_user, @video), :id => "new-video-form", :method => "PUT", :remote => true) do + + = render :partial => 'videos/video_form', :locals => {:uploading_a_video => true} + + :javascript + // Enable the save button upon a form change + $('#select-a-channel-via-upload').live('change', function() { + $('#new-video-form input[type=submit]').removeClass('disabled').addClass('primary').val('Save Video'); + }); + $('#video_title, #video_description, .tags textarea').keyup(function() { + $('#new-video-form input[type=submit]').removeClass('disabled').addClass('primary').val('Save Video'); + }); + $('.disabled.btn').live('click', function () { + return false; + }); + + // Handle AJAX responses + $('#new-video-form').live('ajax:success', function(data, json, response) { + // Replace select field + $('.channel-select-elements').remove(); + $('#new-video-form .channel').append(json.channel_select); + + // Disable save button until something is changed + $('#new-video-form input[type=submit]').removeClass('primary').addClass('disabled').val('Saved'); + $('.social-options').remove(); + $('.success-message.video-saved').fadeOut('fast', function() { + $(this).fadeIn('slow'); + }); + }); + $('#new-video-form').live('ajax:error', function(data, xhr, response){ + brevidy.handle_ajax_error("video", xhr); + }); diff --git a/app/views/videos/share_dialog.js.haml b/app/views/videos/share_dialog.js.haml new file mode 100755 index 0000000..c7c48d4 --- /dev/null +++ b/app/views/videos/share_dialog.js.haml @@ -0,0 +1,33 @@ +:plain + // Setup a custom template to use + var html_template = '#{escape_javascript(render(:partial => 'videos/share_dialog.html'))}'; + + // Show the custom dialog + brevidy.custom_dialog(html_template); + + // Set focus to the input field + $('input#shared_video_link').select(); + + // Bind the ajax animation + $(document).bind('ajaxStart', function(e, request, options) { + $('#share-a-link-form .ajax-animation').show(); + $('#share-a-link-form .ajax-errors').fadeOut('fast'); + }); + $(document).bind('ajaxComplete', function(e, request, options) { + $('#share-a-link-form .ajax-animation').hide(); + }); + + // Description: Handles AJAX error + $('#share-a-link-form').live('ajax:error', function(data, xhr, response) { + // get error message + var responseText = jQuery.parseJSON(xhr.responseText); + if (responseText !== null) { var responseMsg = responseText.error; } + if (typeof responseMsg == 'undefined') { responseMsg = 'There was an unknown error or the request timed out. Please try again later'; } + + // set message + $('#share-a-link-form .ajax-errors').fadeOut('fast', function() { + $(this).text(responseMsg).fadeIn('fast'); + }); + + return false; + }); \ No newline at end of file diff --git a/app/views/videos/show.html.haml b/app/views/videos/show.html.haml new file mode 100755 index 0000000..c48fd0a --- /dev/null +++ b/app/views/videos/show.html.haml @@ -0,0 +1,18 @@ +- # User Banner Image += render 'users/top_banner' + +- # Middle User Info Section += render 'users/middle_user_banner' + +- # Dark Content Wrapper +.lower-wrapper + = render 'users/featured_videos' + + - # Main (White) Content Wrapper + .content-wrapper + = render 'shared/content_titles' + + = render :partial => 'videos/video', :locals => { :video => @video, :expand_video => @video.is_status?(VideoGraph::READY) } + +- # Tell the page to scroll down to the content wrapper upon page load += render 'shared/scroll_to_content_wrapper' \ No newline at end of file diff --git a/assets/javascripts/functions.js b/assets/javascripts/functions.js new file mode 100755 index 0000000..990f098 --- /dev/null +++ b/assets/javascripts/functions.js @@ -0,0 +1,1975 @@ +/**********************/ +/* +* Site wide js and jquery functions +*/ +/**********************/ + +// Create a namespace for Brevidy functions and variables +var brevidy = {}; + +// Set a global AJAX timeout to 90 seconds +$.ajaxSetup({ timeout: 90000 }); + + +/*! + * ========================================================== + * bootstrap-twipsy.js v1.4.0 + * http://twitter.github.com/bootstrap/javascript.html#twipsy + * Adapted from the original jQuery.tipsy by Jason Frame + * ========================================================== + * Copyright 2011 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function( $ ) { + + "use strict" + + /* CSS TRANSITION SUPPORT (https://gist.github.com/373874) + * ======================================================= */ + + var transitionEnd + + $(document).ready(function () { + + $.support.transition = (function () { + var thisBody = document.body || document.documentElement + , thisStyle = thisBody.style + , support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined + return support + })() + + // set CSS transition event type + if ( $.support.transition ) { + transitionEnd = "TransitionEnd" + if ( $.browser.webkit ) { + transitionEnd = "webkitTransitionEnd" + } else if ( $.browser.mozilla ) { + transitionEnd = "transitionend" + } else if ( $.browser.opera ) { + transitionEnd = "oTransitionEnd" + } + } + + }) + + + /* TWIPSY PUBLIC CLASS DEFINITION + * ============================== */ + + var Twipsy = function ( element, options ) { + this.$element = $(element) + this.options = options + this.enabled = true + this.fixTitle() + } + + Twipsy.prototype = { + + show: function() { + var pos + , actualWidth + , actualHeight + , placement + , $tip + , tp + + if (this.hasContent() && this.enabled) { + $tip = this.tip() + this.setContent() + + if (this.options.animate) { + $tip.addClass('fade') + } + + $tip + .remove() + .css({ top: 0, left: 0, display: 'block' }) + .prependTo(document.body) + + pos = $.extend({}, this.$element.offset(), { + width: this.$element[0].offsetWidth + , height: this.$element[0].offsetHeight + }) + + actualWidth = $tip[0].offsetWidth + actualHeight = $tip[0].offsetHeight + + placement = maybeCall(this.options.placement, this, [ $tip[0], this.$element[0] ]) + + switch (placement) { + case 'below': + tp = {top: pos.top + pos.height + this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2} + break + case 'above': + tp = {top: pos.top - actualHeight - this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2} + break + case 'left': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth - this.options.offset} + break + case 'right': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width + this.options.offset} + break + } + + $tip + .css(tp) + .addClass(placement) + .addClass('in') + } + } + + , setContent: function () { + var $tip = this.tip() + $tip.find('.twipsy-inner')[this.options.html ? 'html' : 'text'](this.getTitle()) + $tip[0].className = 'twipsy' + } + + , hide: function() { + var that = this + , $tip = this.tip() + + $tip.removeClass('in') + + function removeElement () { + $tip.remove() + } + + $.support.transition && this.$tip.hasClass('fade') ? + $tip.bind(transitionEnd, removeElement) : + removeElement() + } + + , fixTitle: function() { + var $e = this.$element + if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { + $e.attr('data-original-title', $e.attr('title') || '').removeAttr('title') + } + } + + , hasContent: function () { + return this.getTitle() + } + + , getTitle: function() { + var title + , $e = this.$element + , o = this.options + + this.fixTitle() + + if (typeof o.title == 'string') { + title = $e.attr(o.title == 'title' ? 'data-original-title' : o.title) + } else if (typeof o.title == 'function') { + title = o.title.call($e[0]) + } + + title = ('' + title).replace(/(^\s*|\s*$)/, "") + + return title || o.fallback + } + + , tip: function() { + return this.$tip = this.$tip || $('
').html(this.options.template) + } + + , validate: function() { + if (!this.$element[0].parentNode) { + this.hide() + this.$element = null + this.options = null + } + } + + , enable: function() { + this.enabled = true + } + + , disable: function() { + this.enabled = false + } + + , toggleEnabled: function() { + this.enabled = !this.enabled + } + + , toggle: function () { + this[this.tip().hasClass('in') ? 'hide' : 'show']() + } + + } + + + /* TWIPSY PRIVATE METHODS + * ====================== */ + + function maybeCall ( thing, ctx, args ) { + return typeof thing == 'function' ? thing.apply(ctx, args) : thing + } + + /* TWIPSY PLUGIN DEFINITION + * ======================== */ + + $.fn.twipsy = function (options) { + $.fn.twipsy.initWith.call(this, options, Twipsy, 'twipsy') + return this + } + + $.fn.twipsy.initWith = function (options, Constructor, name) { + var twipsy + , binder + , eventIn + , eventOut + + if (options === true) { + return this.data(name) + } else if (typeof options == 'string') { + twipsy = this.data(name) + if (twipsy) { + twipsy[options]() + } + return this + } + + options = $.extend({}, $.fn[name].defaults, options) + + function get(ele) { + var twipsy = $.data(ele, name) + + if (!twipsy) { + twipsy = new Constructor(ele, $.fn.twipsy.elementOptions(ele, options)) + $.data(ele, name, twipsy) + } + + return twipsy + } + + function enter() { + var twipsy = get(this) + twipsy.hoverState = 'in' + + if (options.delayIn == 0) { + twipsy.show() + } else { + twipsy.fixTitle() + setTimeout(function() { + if (twipsy.hoverState == 'in') { + twipsy.show() + } + }, options.delayIn) + } + } + + function leave() { + var twipsy = get(this) + twipsy.hoverState = 'out' + if (options.delayOut == 0) { + twipsy.hide() + } else { + setTimeout(function() { + if (twipsy.hoverState == 'out') { + twipsy.hide() + } + }, options.delayOut) + } + } + + if (!options.live) { + this.each(function() { + get(this) + }) + } + + if (options.trigger != 'manual') { + binder = options.live ? 'live' : 'bind' + eventIn = options.trigger == 'hover' ? 'mouseenter' : 'focus' + eventOut = options.trigger == 'hover' ? 'mouseleave' : 'blur' + this[binder](eventIn, enter)[binder](eventOut, leave) + } + + return this + } + + $.fn.twipsy.Twipsy = Twipsy + + $.fn.twipsy.defaults = { + animate: true + , delayIn: 0 + , delayOut: 0 + , fallback: '' + , placement: 'above' + , html: false + , live: true + , offset: 0 + , title: 'title' + , trigger: 'hover' + , template: '
' + } + + $.fn.twipsy.rejectAttrOptions = [ 'title' ] + + $.fn.twipsy.elementOptions = function(ele, options) { + var data = $(ele).data() + , rejects = $.fn.twipsy.rejectAttrOptions + , i = rejects.length + + while (i--) { + delete data[rejects[i]] + } + + return $.extend({}, options, data) + } + +}( window.jQuery || window.ender ); // end twitter twipsy + + +/*! + * ========================================================= + * bootstrap-modal.js v1.4.0 + * http://twitter.github.com/bootstrap/javascript.html#modal + * ========================================================= + * Copyright 2011 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + + +!function( $ ){ + + "use strict" + + /* CSS TRANSITION SUPPORT (https://gist.github.com/373874) + * ======================================================= */ + + var transitionEnd + + $(document).ready(function () { + + $.support.transition = (function () { + var thisBody = document.body || document.documentElement + , thisStyle = thisBody.style + , support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined + return support + })() + + // set CSS transition event type + if ( $.support.transition ) { + transitionEnd = "TransitionEnd" + if ( $.browser.webkit ) { + transitionEnd = "webkitTransitionEnd" + } else if ( $.browser.mozilla ) { + transitionEnd = "transitionend" + } else if ( $.browser.opera ) { + transitionEnd = "oTransitionEnd" + } + } + + }) + + + /* MODAL PUBLIC CLASS DEFINITION + * ============================= */ + + var Modal = function ( content, options ) { + this.settings = $.extend({}, $.fn.modal.defaults, options) + this.$element = $(content) + .delegate('.close', 'click.modal', $.proxy(this.hide, this)) + + if ( this.settings.show ) { + this.show() + } + + return this + } + + Modal.prototype = { + + toggle: function () { + return this[!this.isShown ? 'show' : 'hide']() + } + + , show: function () { + var that = this + this.isShown = true + this.$element.trigger('show') + + escape.call(this) + backdrop.call(this, function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + that.$element + .appendTo(document.body) + .show() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element.addClass('in') + + transition ? + that.$element.one(transitionEnd, function () { that.$element.trigger('shown') }) : + that.$element.trigger('shown') + + }) + + return this + } + + , hide: function (e) { + e && e.preventDefault() + + if ( !this.isShown ) { + return this + } + + var that = this + this.isShown = false + + escape.call(this) + + this.$element + .trigger('hide') + .removeClass('in') + + $.support.transition && this.$element.hasClass('fade') ? + hideWithTransition.call(this) : + hideModal.call(this) + + return this + } + + } + + + /* MODAL PRIVATE METHODS + * ===================== */ + + function hideWithTransition() { + // firefox drops transitionEnd events :{o + var that = this + , timeout = setTimeout(function () { + that.$element.unbind(transitionEnd) + hideModal.call(that) + }, 500) + + this.$element.one(transitionEnd, function () { + clearTimeout(timeout) + hideModal.call(that) + }) + } + + function hideModal (that) { + this.$element + .hide() + .trigger('hidden') + + backdrop.call(this) + } + + function backdrop ( callback ) { + var that = this + , animate = this.$element.hasClass('fade') ? 'fade' : '' + if ( this.isShown && this.settings.backdrop ) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $('