diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..1dee305e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle +/.env + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore Byebug command history file. +.byebug_history +/config/aws.yml + +# Ignore application configuration +/config/application.yml diff --git a/.rspec b/.rspec new file mode 100644 index 000000000..e3dd8285e --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--color +--require spec_helper +--format d diff --git a/Gemfile b/Gemfile new file mode 100644 index 000000000..8d5063c35 --- /dev/null +++ b/Gemfile @@ -0,0 +1,80 @@ +source 'https://rubygems.org' + + +# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' +gem 'rails', '~> 5.0.0', '>= 5.0.0.1' +# Use postgresql as the database for Active Record +gem 'pg', '~> 0.18' +# Use Puma as the app server +gem 'puma', '~> 3.0' +# Use SCSS for stylesheets +gem 'sass-rails', '~> 5.0' +# Use Uglifier as compressor for JavaScript assets +gem 'uglifier', '>= 1.3.0' +# Use CoffeeScript for .coffee assets and views +gem 'coffee-rails', '~> 4.2' +# See https://github.com/rails/execjs#readme for more supported runtimes +# gem 'therubyracer', platforms: :ruby +# Use jquery as the JavaScript library +gem 'jquery-rails' +gem 'jquery-fileupload-rails' + +# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks +gem 'turbolinks', '~> 5' +# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder +gem 'jbuilder', '~> 2.5' +# Use Redis adapter to run Action Cable in production +# gem 'redis', '~> 3.0' +# Use ActiveModel has_secure_password +gem 'bootstrap-sass' +gem 'bcrypt', '~> 3.1.7' +gem 'conversation_forms', '~> 2.0.1' +gem "jwt" +gem 'figaro' +gem 'delayed_job' +gem 'delayed_job_active_record' +gem 'daemons' + +gem 'aws-sdk' +gem 'paperclip', :git=> 'https://github.com/thoughtbot/paperclip', :ref => '523bd46c768226893f23889079a7aa9c73b57d68' +gem "delayed_paperclip" +gem 'pull_tempfile' +# direct AWS bucket file uploads +gem 'aws-sdk-rails' +gem 'faker' +# Use Capistrano for deployment +# gem 'capistrano-rails', group: :development + +group :test do + gem 'capybara' + gem 'launchy' +end + +group :development, :test do + # Call 'byebug' anywhere in the code to stop execution and get a debugger console + gem 'byebug', platform: :mri + gem 'jazz_fingers' + gem 'pry-rails' + gem 'bullet' + gem 'rack-mini-profiler' + gem 'factory_girl_rails', '~> 4.0' + gem 'rspec' + gem 'rspec-rails' + gem 'guard' + gem 'guard-rspec' + gem 'shoulda' + gem 'brakeman' +end + +group :development do + gem 'letter_opener' + # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. + gem 'web-console' + gem 'listen', '~> 3.0.5' + # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring + gem 'spring' + gem 'spring-watcher-listen', '~> 2.0.0' +end + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 000000000..78baa09ca --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,368 @@ +GIT + remote: https://github.com/thoughtbot/paperclip + revision: 523bd46c768226893f23889079a7aa9c73b57d68 + ref: 523bd46c768226893f23889079a7aa9c73b57d68 + specs: + paperclip (4.3.1) + activemodel (>= 3.2.0) + activesupport (>= 3.2.0) + cocaine (~> 0.5.5) + mime-types + mimemagic (= 0.3.0) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (5.0.0.1) + actionpack (= 5.0.0.1) + nio4r (~> 1.2) + websocket-driver (~> 0.6.1) + actionmailer (5.0.0.1) + actionpack (= 5.0.0.1) + actionview (= 5.0.0.1) + activejob (= 5.0.0.1) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (5.0.0.1) + actionview (= 5.0.0.1) + activesupport (= 5.0.0.1) + rack (~> 2.0) + rack-test (~> 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.0.0.1) + activesupport (= 5.0.0.1) + builder (~> 3.1) + erubis (~> 2.7.0) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + activejob (5.0.0.1) + activesupport (= 5.0.0.1) + globalid (>= 0.3.6) + activemodel (5.0.0.1) + activesupport (= 5.0.0.1) + activerecord (5.0.0.1) + activemodel (= 5.0.0.1) + activesupport (= 5.0.0.1) + arel (~> 7.0) + activesupport (5.0.0.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (~> 0.7) + minitest (~> 5.1) + tzinfo (~> 1.1) + addressable (2.5.0) + public_suffix (~> 2.0, >= 2.0.2) + arel (7.1.4) + autoprefixer-rails (6.5.4) + execjs + awesome_print (1.7.0) + aws-sdk (2.6.35) + aws-sdk-resources (= 2.6.35) + aws-sdk-core (2.6.35) + aws-sigv4 (~> 1.0) + jmespath (~> 1.0) + aws-sdk-rails (1.0.1) + aws-sdk-resources (~> 2) + railties (>= 3) + aws-sdk-resources (2.6.35) + aws-sdk-core (= 2.6.35) + aws-sigv4 (1.0.0) + bcrypt (3.1.11) + bootstrap-sass (3.3.7) + autoprefixer-rails (>= 5.2.1) + sass (>= 3.3.4) + brakeman (3.4.1) + builder (3.2.2) + bullet (5.4.2) + activesupport (>= 3.0.0) + uniform_notifier (~> 1.10.0) + byebug (9.0.6) + capybara (2.11.0) + addressable + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) + climate_control (0.0.3) + activesupport (>= 3.0) + cocaine (0.5.8) + climate_control (>= 0.0.3, < 1.0) + coderay (1.1.1) + coffee-rails (4.2.1) + coffee-script (>= 2.2.0) + railties (>= 4.0.0, < 5.2.x) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.11.1) + concurrent-ruby (1.0.2) + conversation_forms (2.0.2) + autoprefixer-rails (>= 5.2.1) + rails (~> 5.0.0, >= 5.0.0.1) + railties + sass (>= 3.3.4) + coolline (0.5.0) + unicode_utils (~> 1.4) + daemons (1.2.4) + debug_inspector (0.0.2) + delayed_job (4.1.2) + activesupport (>= 3.0, < 5.1) + delayed_job_active_record (4.1.1) + activerecord (>= 3.0, < 5.1) + delayed_job (>= 3.0, < 5) + delayed_paperclip (3.0.1) + activejob (>= 4.2) + paperclip (>= 3.3) + diff-lcs (1.2.5) + erubis (2.7.0) + execjs (2.7.0) + factory_girl (4.7.0) + activesupport (>= 3.0.0) + factory_girl_rails (4.7.0) + factory_girl (~> 4.7.0) + railties (>= 3.0.0) + faker (1.6.6) + i18n (~> 0.5) + ffi (1.9.14) + figaro (1.1.1) + thor (~> 0.14) + formatador (0.2.5) + globalid (0.3.7) + activesupport (>= 4.1.0) + guard (2.14.0) + formatador (>= 0.2.4) + listen (>= 2.7, < 4.0) + lumberjack (~> 1.0) + nenv (~> 0.1) + notiffany (~> 0.0) + pry (>= 0.9.12) + shellany (~> 0.0) + thor (>= 0.18.1) + guard-compat (1.2.1) + guard-rspec (4.7.3) + guard (~> 2.1) + guard-compat (~> 1.1) + rspec (>= 2.99.0, < 4.0) + hirb (0.7.3) + i18n (0.7.0) + jazz_fingers (4.0.1) + awesome_print (~> 1.6) + hirb (~> 0.7) + pry (~> 0.10) + pry-byebug (~> 3.4) + pry-coolline (~> 0.2) + pry-doc (~> 0.6) + jbuilder (2.6.1) + activesupport (>= 3.0.0, < 5.1) + multi_json (~> 1.2) + jmespath (1.3.1) + jquery-fileupload-rails (0.4.7) + actionpack (>= 3.1) + railties (>= 3.1) + sass (>= 3.2) + jquery-rails (4.2.1) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) + jwt (1.5.6) + launchy (2.4.3) + addressable (~> 2.3) + letter_opener (1.4.1) + launchy (~> 2.2) + listen (3.0.8) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + loofah (2.0.3) + nokogiri (>= 1.5.9) + lumberjack (1.0.10) + mail (2.6.4) + mime-types (>= 1.16, < 4) + method_source (0.8.2) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + mimemagic (0.3.0) + mini_portile2 (2.1.0) + minitest (5.10.1) + multi_json (1.12.1) + nenv (0.3.0) + nio4r (1.2.1) + nokogiri (1.6.8.1) + mini_portile2 (~> 2.1.0) + notiffany (0.1.1) + nenv (~> 0.1) + shellany (~> 0.0) + pg (0.19.0) + pry (0.10.4) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) + pry-byebug (3.4.2) + byebug (~> 9.0) + pry (~> 0.10) + pry-coolline (0.2.5) + coolline (~> 0.5) + pry-doc (0.9.0) + pry (~> 0.9) + yard (~> 0.8) + pry-rails (0.3.4) + pry (>= 0.9.10) + public_suffix (2.0.4) + pull_tempfile (0.2.0) + puma (3.6.2) + rack (2.0.1) + rack-mini-profiler (0.10.1) + rack (>= 1.2.0) + rack-test (0.6.3) + rack (>= 1.0) + rails (5.0.0.1) + actioncable (= 5.0.0.1) + actionmailer (= 5.0.0.1) + actionpack (= 5.0.0.1) + actionview (= 5.0.0.1) + activejob (= 5.0.0.1) + activemodel (= 5.0.0.1) + activerecord (= 5.0.0.1) + activesupport (= 5.0.0.1) + bundler (>= 1.3.0, < 2.0) + railties (= 5.0.0.1) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.1) + activesupport (>= 4.2.0, < 6.0) + nokogiri (~> 1.6.0) + rails-html-sanitizer (1.0.3) + loofah (~> 2.0) + railties (5.0.0.1) + actionpack (= 5.0.0.1) + activesupport (= 5.0.0.1) + method_source + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + rake (12.0.0) + rb-fsevent (0.9.8) + rb-inotify (0.9.7) + ffi (>= 0.5.0) + rspec (3.5.0) + rspec-core (~> 3.5.0) + rspec-expectations (~> 3.5.0) + rspec-mocks (~> 3.5.0) + rspec-core (3.5.4) + rspec-support (~> 3.5.0) + rspec-expectations (3.5.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.5.0) + rspec-mocks (3.5.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.5.0) + rspec-rails (3.5.2) + actionpack (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec-core (~> 3.5.0) + rspec-expectations (~> 3.5.0) + rspec-mocks (~> 3.5.0) + rspec-support (~> 3.5.0) + rspec-support (3.5.0) + sass (3.4.22) + sass-rails (5.0.6) + railties (>= 4.0.0, < 6) + sass (~> 3.1) + sprockets (>= 2.8, < 4.0) + sprockets-rails (>= 2.0, < 4.0) + tilt (>= 1.1, < 3) + shellany (0.0.1) + shoulda (3.5.0) + shoulda-context (~> 1.0, >= 1.0.1) + shoulda-matchers (>= 1.4.1, < 3.0) + shoulda-context (1.2.2) + shoulda-matchers (2.8.0) + activesupport (>= 3.0.0) + slop (3.6.0) + spring (2.0.0) + activesupport (>= 4.2) + spring-watcher-listen (2.0.1) + listen (>= 2.7, < 4.0) + spring (>= 1.2, < 3.0) + sprockets (3.7.0) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.0) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + thor (0.19.4) + thread_safe (0.3.5) + tilt (2.0.5) + turbolinks (5.0.1) + turbolinks-source (~> 5) + turbolinks-source (5.0.0) + tzinfo (1.2.2) + thread_safe (~> 0.1) + uglifier (3.0.4) + execjs (>= 0.3.0, < 3) + unicode_utils (1.4.0) + uniform_notifier (1.10.0) + web-console (3.4.0) + actionview (>= 5.0) + activemodel (>= 5.0) + debug_inspector + railties (>= 5.0) + websocket-driver (0.6.4) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) + xpath (2.0.0) + nokogiri (~> 1.3) + yard (0.9.5) + +PLATFORMS + ruby + +DEPENDENCIES + aws-sdk + aws-sdk-rails + bcrypt (~> 3.1.7) + bootstrap-sass + brakeman + bullet + byebug + capybara + coffee-rails (~> 4.2) + conversation_forms (~> 2.0.1) + daemons + delayed_job + delayed_job_active_record + delayed_paperclip + factory_girl_rails (~> 4.0) + faker + figaro + guard + guard-rspec + jazz_fingers + jbuilder (~> 2.5) + jquery-fileupload-rails + jquery-rails + jwt + launchy + letter_opener + listen (~> 3.0.5) + paperclip! + pg (~> 0.18) + pry-rails + pull_tempfile + puma (~> 3.0) + rack-mini-profiler + rails (~> 5.0.0, >= 5.0.0.1) + rspec + rspec-rails + sass-rails (~> 5.0) + shoulda + spring + spring-watcher-listen (~> 2.0.0) + turbolinks (~> 5) + tzinfo-data + uglifier (>= 1.3.0) + web-console + +BUNDLED WITH + 1.13.6 diff --git a/Guardfile b/Guardfile new file mode 100644 index 000000000..3215f0137 --- /dev/null +++ b/Guardfile @@ -0,0 +1,70 @@ +# A sample Guardfile +# More info at https://github.com/guard/guard#readme + +## Uncomment and set this to only include directories you want to watch +# directories %w(app lib config test spec features) \ +# .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")} + +## Note: if you are using the `directories` clause above and you are not +## watching the project directory ('.'), then you will want to move +## the Guardfile to a watched dir and symlink it back, e.g. +# +# $ mkdir config +# $ mv Guardfile config/ +# $ ln -s config/Guardfile . +# +# and, you'll have to watch "config/Guardfile" instead of "Guardfile" + +# Note: The cmd option is now required due to the increasing number of ways +# rspec may be run, below are examples of the most common uses. +# * bundler: 'bundle exec rspec' +# * bundler binstubs: 'bin/rspec' +# * spring: 'bin/rspec' (This will use spring if running and you have +# installed the spring binstubs per the docs) +# * zeus: 'zeus rspec' (requires the server to be started separately) +# * 'just' rspec: 'rspec' + +guard :rspec, cmd: "bundle exec rspec" do + require "guard/rspec/dsl" + dsl = Guard::RSpec::Dsl.new(self) + + # Feel free to open issues for suggestions and improvements + + # RSpec files + rspec = dsl.rspec + watch(rspec.spec_helper) { rspec.spec_dir } + watch(rspec.spec_support) { rspec.spec_dir } + watch(rspec.spec_files) + + # Ruby files + ruby = dsl.ruby + dsl.watch_spec_files_for(ruby.lib_files) + + # Rails files + rails = dsl.rails(view_extensions: %w(erb haml slim)) + dsl.watch_spec_files_for(rails.app_files) + dsl.watch_spec_files_for(rails.views) + + watch(rails.controllers) do |m| + [ + rspec.spec.call("routing/#{m[1]}_routing"), + rspec.spec.call("controllers/#{m[1]}_controller"), + rspec.spec.call("acceptance/#{m[1]}") + ] + end + + # Rails config changes + watch(rails.spec_helper) { rspec.spec_dir } + watch(rails.routes) { "#{rspec.spec_dir}/routing" } + watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" } + + # Capybara features specs + watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") } + watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") } + + # Turnip features and steps + watch(%r{^spec/acceptance/(.+)\.feature$}) + watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m| + Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance" + end +end diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..0dca8010b --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +worker: bundle exec rake jobs:work +web: bundle exec rails s -p $PORT -e $RACK_ENV diff --git a/README.md b/README.md index 24602872a..bf6bb647e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,39 @@ danebook ======== +A localized social network with an ad free experience -This is the Real Dane Deal. +## Written by + +[Sampson Crowley](https://github.com/SampsonCrowley) + +## [View on Heroku](https://danebook-sampson-crowley.herokuapp.com/) + + +## User Experience Perks + +### Posting + +Users can post what they are feeling on their timeline + +A New Post form is present at the top of their newsfeed and timeline + +Any Post can be commented on and liked by Users and their Friends + +### Images + +Users can create galleries and post images to share with their friends + +Images can have likes and comments by the user and their Friends + +Any Image can be set as a profile image or cover image + +A running history of profile photos is kept in the Profile Images gallery + + +### Friending + +Users can request to be friends with other users. + +An accepted friend request creates a friendship + +Users can comment on their friends posts and images diff --git a/Rakefile b/Rakefile new file mode 100644 index 000000000..e85f91391 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative 'config/application' + +Rails.application.load_tasks diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 000000000..b16e53d6d --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link_directory ../javascripts .js +//= link_directory ../stylesheets .css diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js new file mode 100644 index 000000000..29be5aebc --- /dev/null +++ b/app/assets/javascripts/application.js @@ -0,0 +1,40 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// compiled file. JavaScript code in this file should be added after the last require_* statement. +// +// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details +// about supported directives. +// +//= require jquery +//= require jquery_ujs +//= require jquery-fileupload/basic +//= require bootstrap +//= require conversation_forms +//= require turbolinks +//= require_tree . + + +$(document).on('turbolinks:load', function(){ + if ($(".cf-main-form")[0]){ + $(".cf-main-form").conversationalForm({formEl: '.cf-main-form'}); + $('.remove-cf').show(); + $('.remove-cf').on('click', function(){ + $('#conversational-form').hide(); + $(this).hide("slide", function() { + $('.show-cf').show(); + }) + }); + $('.show-cf').on('click', function(){ + $('#conversational-form').show(); + $(this).hide("slide", function() { + $('.remove-cf').show(); + }) + }); + } + +}) diff --git a/app/assets/javascripts/cable.js b/app/assets/javascripts/cable.js new file mode 100644 index 000000000..71ee1e66d --- /dev/null +++ b/app/assets/javascripts/cable.js @@ -0,0 +1,13 @@ +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the rails generate channel command. +// +//= require action_cable +//= require_self +//= require_tree ./channels + +(function() { + this.App || (this.App = {}); + + App.cable = ActionCable.createConsumer(); + +}).call(this); diff --git a/app/assets/javascripts/channels/.keep b/app/assets/javascripts/channels/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js new file mode 100644 index 000000000..b2a89941e --- /dev/null +++ b/app/assets/javascripts/flash.js @@ -0,0 +1,44 @@ +var DANEBOOK = DANEBOOK || {} + +DANEBOOK.Flashes = (function($){ + var createFlashWrapper = function createFlashWrapper(){ + var el = document.createElement('DIV'); + el.style.display = "none"; + return el + } + + createFlash = function createFlash(status, msgs){ + var htmlStr = '
' + for(var i = 0; i < msgs.length; i++ ){ + htmlStr +='

'+ msgs[i] +'

'; + } + return htmlStr += '
' + } + + var removeFlash = function removeFlash(target, flash){ + setTimeout(function(){ + $(flash).slideUp(500, $(flash).remove) + },2000); + } + + var setFlash = function setFlash(target, msgs, status){ + var wrapper = createFlashWrapper(); + wrapper.innerHTML = createFlash(status, msgs); + target.appendChild(wrapper); + $(wrapper).slideDown(500, removeFlash(target, wrapper)); + } + + var flashAboveTarget = function flashAboveTarget(id, type){ + var target = document.querySelector('[data-'+ type + '-id="'+id+'"]'); + var el = document.createElement("DIV"); + target.parentNode.insertBefore(el, target); + return el; + } + + var addFlash = function(status, msgs, target, targetType){ + target = target ? flashAboveTarget(target, targetType) : document.getElementById('flash-box'); + setFlash(target, msgs, status); + } + + return addFlash; +})($) diff --git a/app/assets/javascripts/galleries.js b/app/assets/javascripts/galleries.js new file mode 100644 index 000000000..e046f6d8b --- /dev/null +++ b/app/assets/javascripts/galleries.js @@ -0,0 +1,89 @@ +$(document).on('turbolinks:load', function(){ + var validFile = true; + $('.syncronousUpload').find("input:file").each(function(i, elem) { + var fileInput = $(elem); + var form = $(fileInput.parents('form:first')); + var submitButton = $(document).find('input[type="submit"]'); + var imgPreview = $("your image") + var fileList = $("
"); + var errorDiv = $('#upload-errors'); + var max_size = 2 * 1024 * 1024; + + fileInput.after(fileList); + fileInput.fileupload({ + fileInput: fileInput, + url: form.data('url'), + type: 'POST', + autoUpload: false, + formData: form.data('form-data'), + paramName: 'file', // S3 does not like nested name fields i.e. name="user[avatar_url]" + dataType: 'XML', // S3 returns XML if success_action_status is set to 201 + replaceFileInput: false, + + add: function (e, data) { + validFile = true; + var uploadFile = data.files[0]; + if (!(/\.(png)|(jpe?g)|(gif)$/i).test(uploadFile.name)) { + errorDiv.removeClass("hidden"); + errorDiv.append("

Only web safe images allowed.

Valid formats are png, jpg/jpeg, and gif

"); + validFile = false; + } + if (uploadFile.size > (1024 * 1024)) { // 2mb + errorDiv.removeClass("hidden"); + errorDiv.append("

File too large.

Max Image size is 1MB

"); + validFile = false; + } + + if(validFile){ + fileName = "

"+uploadFile.name+"

" + fileList.append(fileName); + var reader = new FileReader(); + reader.onload = function (e) { + $(imgPreview).attr('src', e.target.result); + } + reader.readAsDataURL(uploadFile); + fileList.append(imgPreview); + }else{ + fileInput.replaceWith( fileInput = fileInput.clone( true ) ); + $('.image-upload-description').each(function(){ + $(this).replaceWith( $(this) = $(this).clone( true ) ); + }) + form.off().submit(); + } + }, + progressall: function (e, data) { + var progress = parseInt(data.loaded / data.total * 100, 10); + progressBar.css('width', progress + '%') + }, + start: function (e) { + submitButton.prop('disabled', true); + + progressBar. + css('background', 'green'). + css('display', 'block'). + css('width', '0%'). + text("Loading..."); + }, + done: function(e, data) { + submitButton.prop('disabled', false); + progressBar.text("Uploading done"); + + // extract key and generate URL from response + var key = $(data.jqXHR.responseXML).find("Key").text(); + var url = 'https://' + form.data('host') + '/' + key; + + // create hidden field + var input = $("", { type:'hidden', name: fileInput.attr('name'), value: url }) + form.append(input); + form.off().submit(); + }, + fail: function(e, data) { + submitButton.prop('disabled', false); + + progressBar. + css("background", "red"). + text("Failed"); + } + }); + }); +}); diff --git a/app/assets/javascripts/init.js b/app/assets/javascripts/init.js new file mode 100644 index 000000000..5a74e8a39 --- /dev/null +++ b/app/assets/javascripts/init.js @@ -0,0 +1,14 @@ +// var already = false; +// $(function(){ +// already = true +// var timeline = document.getElementById('timeline'); +// if(timeline){ +// DANEBOOK.Posts.getPosts(timeline.getAttribute("data-user-id")) +// } +// }); +$(document).on('turbolinks:load', function(){ + var timeline = document.getElementById('timeline'); + if(timeline){ + DANEBOOK.Posts.init(timeline) + } +}); diff --git a/app/assets/javascripts/posts.js b/app/assets/javascripts/posts.js new file mode 100644 index 000000000..aa83abb92 --- /dev/null +++ b/app/assets/javascripts/posts.js @@ -0,0 +1,126 @@ +var DANEBOOK = DANEBOOK || {} + +DANEBOOK.Posts = (function($){ + var lastId = "last", userId, checking; + + var createElFromHTML = function createElFromHTML(html){ + var el = document.createElement("DIV"); + el.innerHTML = html; + return el.firstChild + } + + var checkNoPost = function checkNoPost(){ + var noPost = document.getElementById("no-posts"); + if(noPost) + $(noPost).slideUp(); + } + + var insertPost = function(html){ + var el = createElFromHTML(html); + el.style.display = "none" + var timelineToInsert = document.getElementById("timeline"), + newPostWrapper = document.getElementById("new_post_wrapper"); + checkNoPost(); + timelineToInsert.insertBefore(el, newPostWrapper.nextSibling); + show(el) + clearPostInput(newPostWrapper); + } + + var appendPost = function(html){ + var el = createElFromHTML(html), + timelineToInsert = document.getElementById("timeline"); + checkNoPost(); + el.style.display = "none" + timelineToInsert.appendChild(el); + show(el) + } + + var show = function show(el){ + $(el).slideDown(250); + } + + function contains(selector, text, wrapper) { + var wrapper = wrapper || document, + elements = wrapper.querySelectorAll(selector); + return Array.prototype.filter.call(elements, function(element){ + return RegExp(text).test(element.textContent); + }); + } + + var clearPostInput = function(wrapper){ + var inp = wrapper.getElementsByTagName("input")[0] + ta = wrapper.getElementsByTagName("textarea")[0] + inp.value = null; + ta.value = null; + } + + var commentForm = function commentForm(postId, form){ + form.style.display = "none" + var post = document.querySelector('.post[data-post-id="'+ postId +'"]'), + footer = post.getElementsByClassName('post-footer')[0]; + commentLink = contains('a', 'Comment', post)[0]; + commentLink.style.pointerEvents = "none"; + footer.appendChild(form); + show(form); + } + + var showComments = function showComments(link){ + $(link).slideUp(0); + var node = link.parentNode; + while(!node.classList.contains("post")){ + node = node.parentNode + } + var comments = node.querySelector('.post-footer .comments') + show(comments); + } + + var getPosts = function getPosts(userId){ + $.ajax({ + url: '/users/'+ userId + '/posts?start_id=' + lastId + }).then(function(){ + checking = false + }) + } + + var setLastIndex = function setLastIndex(id){ + lastId = id; + } + + var checkForTimeLine = function checkForTimeLine(){ + return document.getElementById('timeline'); + } + + var scrollListener = function scrollListener(){ + $(window).scroll(function(){ + if(!checkForTimeLine() || lastId === "end" || checking){ + return + } + checkScrollHeight(); + }); + } + + var checkScrollHeight = function checkScrollHeight() { + if ($(window).scrollTop() >= $(document).height() - $(window).height() - 200){ + checking = true; + getPosts(userId) + } + } + + var init = function init(timeline){ + lastId = "last" + userId = timeline.getAttribute("data-user-id") + getPosts(userId); + scrollListener(); + }; + + + return { + init: init, + insert: insertPost, + append: appendPost, + showCommentForm: commentForm, + showComments: showComments, + getPosts: getPosts, + setLastIndex: setLastIndex + } +})($) diff --git a/app/assets/javascripts/profiles.js b/app/assets/javascripts/profiles.js new file mode 100644 index 000000000..fb46b4935 --- /dev/null +++ b/app/assets/javascripts/profiles.js @@ -0,0 +1,101 @@ +// $(document).on('turbolinks:load', function(){ +// var validFile = true; +// $('.directUpload').find("input:file").each(function(i, elem) { +// console.log($(this)); +// var fileInput = $(elem); +// var form = $(fileInput.parents('form:first')); +// var submitButton = $(document).find('input[type="submit"]'); +// var imgPreview = $("your image") +// var fileList = $("
"); +// var progressBar = $("
"); +// var barContainer = $("
").append(progressBar); +// var errorDiv = $('#upload-errors'); +// var max_size = 2 * 1024 * 1024; +// +// fileInput.after(fileList).after(barContainer); +// fileInput.fileupload({ +// fileInput: fileInput, +// url: form.data('url'), +// type: 'POST', +// autoUpload: false, +// formData: form.data('form-data'), +// paramName: 'file', // S3 does not like nested name fields i.e. name="user[avatar_url]" +// dataType: 'XML', // S3 returns XML if success_action_status is set to 201 +// add: function (e, data) { +// validFile = true; +// var uploadFile = data.files[0]; +// if (!(/\.(png)|(jpe?g)|(gif)$/i).test(uploadFile.name)) { +// errorDiv.removeClass("hidden"); +// errorDiv.append("

Only web safe images allowed.

Valid formats are png, jpg/jpeg, and gif

"); +// validFile = false; +// } +// if (uploadFile.size > (1024 * 1024)) { // 2mb +// errorDiv.removeClass("hidden"); +// errorDiv.append("

File too large.

Max Image size is 1MB

"); +// validFile = false; +// } +// +// if(validFile){ +// fileName = "

"+uploadFile.name+"

" +// fileList.append(fileName); +// var reader = new FileReader(); +// reader.onload = function (e) { +// $(imgPreview).attr('src', e.target.result); +// } +// reader.readAsDataURL(uploadFile); +// fileList.append(imgPreview); +// console.log(form.data('form-data')) +// form.data('form-data')['Content Type'] = uploadFile.type +// console.log(form.data('form-data')) +// } +// +// $(form).off().on('submit', function(e){ +// // validation code here +// if(validFile) { +// e.preventDefault(); +// data.submit(); +// }else{ +// fileInput.replaceWith( fileInput = fileInput.clone( true ) ); +// $('.image-upload-description').each(function(){ +// $(this).replaceWith( $(this) = $(this).clone( true ) ); +// }) +// form.off().submit(); +// } +// }); +// }, +// progressall: function (e, data) { +// var progress = parseInt(data.loaded / data.total * 100, 10); +// progressBar.css('width', progress + '%') +// }, +// start: function (e) { +// submitButton.prop('disabled', true); +// +// progressBar. +// css('background', 'green'). +// css('display', 'block'). +// css('width', '0%'). +// text("Loading..."); +// }, +// done: function(e, data) { +// submitButton.prop('disabled', false); +// progressBar.text("Uploading done"); +// +// // extract key and generate URL from response +// var key = $(data.jqXHR.responseXML).find("Key").text(); +// var url = 'https://' + form.data('host') + '/' + key; +// +// // create hidden field +// var input = $("", { type:'hidden', name: fileInput.attr('name'), value: url }) +// form.append(input); +// form.off().submit(); +// }, +// fail: function(e, data) { +// submitButton.prop('disabled', false); +// +// progressBar. +// css("background", "red"). +// text("Failed"); +// } +// }); +// }); +// }); diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss new file mode 100644 index 000000000..13ca4cc6f --- /dev/null +++ b/app/assets/stylesheets/application.css.scss @@ -0,0 +1,18 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require conversation_forms + *= require jquery.fileupload + *= require jquery.fileupload-ui + *= require_self + */ + @import 'styles'; diff --git a/app/assets/stylesheets/partials/_animations.scss b/app/assets/stylesheets/partials/_animations.scss new file mode 100644 index 000000000..7475ca141 --- /dev/null +++ b/app/assets/stylesheets/partials/_animations.scss @@ -0,0 +1,28 @@ + .animated { + -webkit-animation-duration: 1s; + animation-duration: 1s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + } + + @keyframes shake { + from, to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 10%, 30%, 50%, 70%, 90% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + + 20%, 40%, 60%, 80% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } + } + + .shake { + -webkit-animation-name: shake; + animation-name: shake; + } diff --git a/app/assets/stylesheets/partials/_form_components.scss b/app/assets/stylesheets/partials/_form_components.scss new file mode 100644 index 000000000..0020d3a68 --- /dev/null +++ b/app/assets/stylesheets/partials/_form_components.scss @@ -0,0 +1,25 @@ +.progress { + .bar { + padding-left: 0.2em; + color: white; + display: none; + } +} +.btn-file { + position: relative; + overflow: hidden; +} +.btn-file input[type=file] { + text-align: right; + filter: alpha(opacity=0); + opacity: 0; + outline: none; + background: white; + cursor: inherit; + display: block; +} + +.image-upload-section{ + margin-top: -50px; + margin-bottom: 50px; +} diff --git a/app/assets/stylesheets/partials/_gallery.scss b/app/assets/stylesheets/partials/_gallery.scss new file mode 100644 index 000000000..f7e9bcf95 --- /dev/null +++ b/app/assets/stylesheets/partials/_gallery.scss @@ -0,0 +1,7 @@ +img{ + max-width: 100%; +} +.photo img { + border: 2px solid #333; + border-radius: 2px +} diff --git a/app/assets/stylesheets/partials/_header.scss b/app/assets/stylesheets/partials/_header.scss new file mode 100644 index 000000000..64dd207ba --- /dev/null +++ b/app/assets/stylesheets/partials/_header.scss @@ -0,0 +1,92 @@ +#profile-header { + position:relative; + + .cover-img { + position: relative; + height:0; + padding-bottom:30%; + background: #fff url(https://s3.amazonaws.com/viking_education/web_development/web_app_eng/hogwarts_small.jpg) no-repeat; + background-size: cover; + background-position: center; + border-radius:0; + + img{ + position: absolute; + top: 0; + left: 0; + width: 100%; + height:100%; + } + + } + + .profile-username { + position:absolute; + bottom:0; + left:calc(15% + 30px); + font-weight: bolder; + color: #fff; + text-shadow: -1px 0 1px black, 0 1px 1px black, 1px 0 1px black, 0 -1px 1px black; + } + + + .profile-img { + position:absolute; + border: $nav-border; + border-width: 4px; + border-radius: 4px; + width:15%; + z-index:2; + left:15px; + bottom:-35px; + background-color: #f0f8ff; + img { + width:100% + } + } + .profile-nav{ + background:#fff; + border:$nav-border; + border-radius: 0 0 2px 2px; + } + .navbar-nav{ + width:100%; + padding-left:calc(15% + 15px); + } + li, li:hover, li:active { + display:block; + border-left:$nav-border; + width:auto; + font-weight: bold; + a, a:hover, a:active{ + background:transparent; + color: #555; + } + + @media (min-width: 768px) { + &:nth-last-child(2) { + border-right:$nav-border; + } + &.edit{ + border:none; + float:right; + a { + color: $primary; + text-decoration: underline; + } + } + } + + .active { + background: #eee; + } + } + .navbar-toggle:hover + .collapse, .collapse:hover { + display:block; + } + @media (min-width: 768px) { + .collapse { + display:block; + } + } +} diff --git a/app/assets/stylesheets/partials/_navbar.scss b/app/assets/stylesheets/partials/_navbar.scss new file mode 100644 index 000000000..2087939cc --- /dev/null +++ b/app/assets/stylesheets/partials/_navbar.scss @@ -0,0 +1,92 @@ +#navbar { + color:$nav-font-color; + background: $primary; + border-color: rgb(8, 8, 8); + .icon-bar { + background-color:#fff; + } + .navbar-toggle:focus, .navbar-toggle:hover{ + background-color:#fff; + .icon-bar { + background-color: #000; + } + } + .navbar-nav > li > p { + padding: 15px; + margin:0; + line-height: 20px; + color: $nav-font-color; + } + li:hover > a, .navbar-brand:hover { + color:$nav-font-hover-color; + } + &, a, a:visited { + font-size:20px; + color:$nav-font-color; + font-weight:bold; + } + .navbar-brand { + padding:10px; + font-size: 30px; + line-height:1; + color:$nav-font-color; + + img { + height:30px; + width:30px; + border-radius:50%; + float:left; + margin-right:15px; + } + + } + #login-dp{ + min-width: 250px; + padding: 14px; + overflow:hidden; + background-color:rgba(240,248,255,.8); + &, a { + color: #333; + } + } + #login-dp .help-block{ + font-size:12px + } + #login-dp .bottom{ + background-color:rgba(240,248,255,.8); + border-top:1px solid #ddd; + clear:both; + padding:14px; + } + #login-dp .social-buttons{ + margin:12px 0 + } + #login-dp .social-buttons a{ + width: 49%; + } + #login-dp .form-group { + margin-bottom: 10px; + } + .navbar-form { + border:0; + @media (max-width: 767px) { + margin:0 -15px; + + } + } + + + .collapse { + border-bottom: 1px solid rgb(204, 204, 204); + @media (min-width: 768px) { + border:0; + display: block!important; + height: auto!important; + padding-bottom: 0; + overflow: visible!important; + } + } +} +.dropdown-menu { + padding: 0; +} diff --git a/app/assets/stylesheets/partials/_notices.scss b/app/assets/stylesheets/partials/_notices.scss new file mode 100644 index 000000000..c9edb12fe --- /dev/null +++ b/app/assets/stylesheets/partials/_notices.scss @@ -0,0 +1,55 @@ +// .notices{ +// background-color: #f0f8ff !important; +// li{ +// a { +// font-size: 16px !important; +// color: #333 !important; +// font-weight: 100 !important; +// +// &:hover { +// background-color: #efefef !important; +// } +// &.new-notice { +// color: #eee !important; +// background: #2b78e4 !important; +// } +// } +// +// } +// +// } +#navbar, ul.nav, .navbar-default { + + ul.dropdown-menu.notices{ + li { + border-bottom:1px solid #333; + &:last-child{ + border: none; + } + } + + a { + font-size: 16px; + color: #333; + font-weight: 100; + + &:hover { + background-color: #efefef; + } + &.new-notice { + color: #eee; + background: #2b78e4; + &:hover { + color: #333; + background-color: #f0f8ff; + } + } + } + } + +} + +ul.nav li.dropdown:hover > ul.dropdown-menu.notices { + display: block; + background-color: #f0f8ff; +} diff --git a/app/assets/stylesheets/partials/_post.scss b/app/assets/stylesheets/partials/_post.scss new file mode 100644 index 000000000..4a68cc815 --- /dev/null +++ b/app/assets/stylesheets/partials/_post.scss @@ -0,0 +1,151 @@ +.post { + margin-top:20px; + border:$content-border; + border-radius:2px; + font-size: 14px; + color: #555; + h1,h2,h3 { + font-weight: bold; + } + + .post-header { + border-bottom: $content-border; + background: #eee; + text-align:center; + font-size:24px; + padding:15px; + position:relative; + + + .edit-button { + position: absolute; + right: 15px; + top:15px; + } + + h1, h2, h3, h4, h5, h6, p { + margin-top: 0; + margin-bottom: 0; + } + } + .post-content { + h3 { + margin: 30px 0 20px 0; + } + } + + .post-footer { + padding:15px; + background: #eee; + + a.public { + padding-right:15px; + } + + .likes { + margin-top:15px; + } + + .comments { + + padding-top:15px; + background: #eee; + background-image: -webkit-linear-gradient(top, rgba(238,238,238,0) 0px, #999 2px, rgba(238,238,238,0) 0px); + background-image: -moz-linear-gradient(top, rgba(238,238,238,0) 0px, #999 2px, rgba(238,238,238,0) 0px); + background-image: -o-linear-gradient(top, rgba(238,238,238,0) 0px, #999 2px, rgba(238,238,238,0) 0px); + background-image: -ms-linear-gradient(top, rgba(238,238,238,0) 0px, #999 2px, rgba(238,238,238,0) 0px); + background-image: linear-gradient(top, rgba(238,238,238,0) 0px, #999 2px, rgba(238,238,238,0) 0px); + + textarea { + margin-bottom:1em; + } + } + } + + &.post-small { + + .post-header, .post-footer { + padding:4px 15px; + font-size:14px; + } + .post-header { + h1, h2, h3, h4, h5, h6, p { + font-weight: bold; + } + } + + .post-content { + padding:15px; + h3 { + margin: 30px 0 20px 0; + } + } + + } + + &.post-large { + + font-size: 16px; + + .post-header, .post-footer { + padding:4px 15px; + font-size:14px; + } + .post-header { + h1, h2, h3, h4, h5, h6, p { + font-weight: bold; + } + } + + .post-content { + padding:15px; + h3 { + margin: 30px 0 20px 0; + } + } + + } + + &.post-gutters { + .post-content { + padding:15px; + } + } + + .comment { + width: calc(100% - 60px); + padding-left: 15px; + + .muted { + padding-left:5px; + } + p { + margin:0; + padding-bottom: 1em; + } + } + + dl{ + dt, dd{ + float:left; + width: 30%; + } + dd { + width: 70%; + margin-bottom:20px; + } + &:before, &:after{ + display: table; + content: " "; + } + &:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; + } + } + +} diff --git a/app/assets/stylesheets/partials/_profile.scss b/app/assets/stylesheets/partials/_profile.scss new file mode 100644 index 000000000..29fe74c91 --- /dev/null +++ b/app/assets/stylesheets/partials/_profile.scss @@ -0,0 +1,27 @@ +.profile { + padding-bottom: 15px; + + .profile-img { + width:50px; + height:50px; + overflow:hidden; + border-radius:2px; + border: $content-border; + + img { + width:100%; + height:100%; + } + } + + .profile-info { + padding-left:15px; + + a, a:visited, a:hover { + text-decoration: underline; + } + p{ + color:#777; + } + } +} diff --git a/app/assets/stylesheets/styles.scss b/app/assets/stylesheets/styles.scss new file mode 100644 index 000000000..0150a45db --- /dev/null +++ b/app/assets/stylesheets/styles.scss @@ -0,0 +1,97 @@ +@import 'bootstrap-sprockets'; +@import 'bootstrap'; + +$nav-font-color: #eee; +$nav-font-hover-color: #fff; +$nav-border: 1px solid rgba(0, 0, 0, .8); +$content-border: 2px solid rgba(0, 0, 0, .8); +$primary: #2b78e4; +$primary-hover: #9fc5f8; +$font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + +.remove-cf, .show-cf { + margin-bottom:20px; +} +cf-chat{ + min-height: 300px; + padding-bottom:30px !important; +} +cf-input{ + position: relative !important; +} +#navbar, ul.nav, .navbar-default { + + .navbar-nav > .open > a, + li.dropdown:hover, + ul.dropdown-menu a:hover{ + + &, &:active, &:hover{ + background-color: $primary-hover; + color: #333; + } + + } + +} + +ul.nav li.dropdown:hover > ul.dropdown-menu { + display: block; + background-color: $primary; + +} + +body { + font-family: $font-family; + padding: 50px 0 20px 0; +} +.btn-primary { + background-color: $primary; +} +.muted { + color: #999; +} +.right-gutter { + padding-right:2em +} +.btn.unfriend { + background: #ccc; + color: #000; +} + +.bordered{ + border: 2px solid #333; + border-radius: 6px; + padding: 15px; +} +.spaced { + margin-top: 20px; + margin-bottom: 20px +} + +@import 'partials/navbar'; +@import 'partials/notices'; +@import 'partials/header'; +@import 'partials/post'; +@import 'partials/profile'; +@import 'partials/gallery'; +@import 'partials/animations'; +@import 'partials/form_components'; + + +input.invalid { + border-color: #DD2C00 !important; +} + +#about { + + .information, .personality, .submit-button { + margin-bottom:20px; + } + .submit-button { + + .btn{ + font-size:24px; + } + + } +} diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 000000000..d67269728 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 000000000..0ff5442f4 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 000000000..643dc26d7 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,75 @@ +class ApplicationController < ActionController::Base + before_action :authenticate + protect_from_forgery with: :exception + + private + + def decrypt(msg) + Crypt.decrypt(msg) + end + + def encrypt(msg) + Crypt.encrypt(msg) + end + + def authenticate + unless signed_in_user? + flash[:danger] = ["Please sign in"] + redirect_to login_path + else + incomplete_profile + end + end + + def create_session + user = User.find_by_token(cookies[:token]) if cookies[:token] + sign_in(user) if user + end + + def current_user + create_session unless session[:user_id] + @current_user ||= User.includes(:profile).includes(:notices).find_by_id(decrypt(session[:user_id])) if session[:user_id] + end + helper_method :current_user + + def incomplete_profile + unless current_user.profile.edited + flash.now[:info] = ["You have not completed your profile.", view_context.link_to("Please fill out your info here", edit_user_profile_path(current_user))] + end + end + + def sign_in(user, remember = false) + reset_session + if remember + set_token(user) + end + session[:user_id] = encrypt(user.id) + @current_user = user + end + + def set_token(user) + user.regenerate_auth_token + cookies[:token] = user.token + end + + def sign_out + current_user.destroy_token + @current_user = nil + session.delete(:user_id) + cookies.delete(:token) + reset_session + end + + + def signed_in_user? + !!current_user + end + helper_method :signed_in_user? + + def whitelisted_user_params + params.require(:user).permit(:email, + :password, + :password_confirmation) + end + +end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb new file mode 100644 index 000000000..b3409e8f8 --- /dev/null +++ b/app/controllers/comments_controller.rb @@ -0,0 +1,61 @@ +class CommentsController < ApplicationController + include UserCheck + + before_action :set_user + before_action :get_post + + def new + if @user == current_user || @user.friends.include?(current_user) + @comment = @post.comments.build + respond_to do |format| + format.js + format.html + end + else + respond_to do |format| + format.js do + @status = :danger + @msg = ["You can only comment on your friends' posts."] + @target = @post.id + @target_type = "post" + render template: 'shared/flashes' + end + format.html do + flash[:danger] = ["You can only comment on your friends' posts."] + redirect_to user_post_path(@user, @post) + end + end + end + end + + def create + if @user == current_user || @user.friends.include?(current_user) + @comment = @post.comments.build(whitelisted) + if @comment.save + p "made it" + flash[:success] = ["Comment Successfull"] + redirect_to user_post_path(@user, @post) + else + flash[:danger] = ["Something went wrong.."] + @comment.errors.full_messages.each do |error| + flash[:danger] << error + end + redirect_to user_post_path(@user, @post) + end + end + end + + + private + def get_post + @post = Post.includes(:comments).find_by_id(params[:post_id]) + unless @post + flash[:danger] = ["Post does not exist"] + redirect_to root_path + end + end + + def whitelisted + params.require(:post).permit(:body).merge(post_type: "Comment", user_id: current_user.id) + end +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/controllers/concerns/user_check.rb b/app/controllers/concerns/user_check.rb new file mode 100644 index 000000000..2cd659c05 --- /dev/null +++ b/app/controllers/concerns/user_check.rb @@ -0,0 +1,39 @@ +module UserCheck + extend ActiveSupport::Concern + + def correct_user + unless params[user_id_key] == current_user.id.to_s + flash[:danger] = ["You cannot mess with other users! Jerk.."] + redirect_to user_profile_path(current_user) + end + end + + def set_user + @user = User.find_by_id(params[user_id_key]) + unless @user + flash[:danger] = ["User does not exist"] + redirect_to root_path + end + end + + + def set_user_basic_profile + @user = User.includes(:profile).find_by_id(params[user_id_key]) + unless @user + flash[:danger] = ["User does not exist"] + redirect_to root_path + end + end + + def set_user_full_profile + @user = User.includes(profile: [:bio]).find_by_id(params[user_id_key]) + unless @user + flash[:danger] = ["User does not exist"] + redirect_to root_path + end + end + + def user_id_key + controller_name == 'users' ? :id : :user_id + end +end diff --git a/app/controllers/friends_controller.rb b/app/controllers/friends_controller.rb new file mode 100644 index 000000000..45bd627ca --- /dev/null +++ b/app/controllers/friends_controller.rb @@ -0,0 +1,27 @@ +class FriendsController < ApplicationController + + include UserCheck + before_action :set_user_basic_profile + + def show + @friends = @user.friends + end + + def create + @status, @msg = Friendify.friendship(current_user, @user) + respond_to do |format| + format.js { render template: 'shared/flashes' } + format.html do + flash[@status] = @msg + redirect_to user_profile_path(@user.profile) + end + end + end + + def destroy + Friendify.clear_friendship(@user, current_user) + + redirect_to root_path + end + +end diff --git a/app/controllers/galleries_controller.rb b/app/controllers/galleries_controller.rb new file mode 100644 index 000000000..60b76718e --- /dev/null +++ b/app/controllers/galleries_controller.rb @@ -0,0 +1,104 @@ +class GalleriesController < ApplicationController + include UserCheck + + before_action :set_user + before_action :correct_user, except: [:show, :index] + + def index + @galleries = @user.galleries + end + + def show + @gallery = Gallery.includes(images: [:post]).find_by(id: params[:id]) + end + + def new + @gallery = @user.galleries.build + 5.times do + @gallery.images.build + end + end + + def create + @gallery = @user.galleries.build(whitelisted) + if @gallery.save + flash[:success] = ["New Gallery Created.", "Your images will be viewable after processing"] + redirect_to user_gallery_path(current_user, @gallery) + else + flash.now[:danger] = ["something went, wrong.."] + @gallery.errors.full_messages.each do |msg| + flash.now[:danger] << msg + end + render :new + end + end + + def edit + @gallery = Gallery.find_by(id: params[:id]) + if @gallery.title == "Profile Images" + @gallery.images.build + else + 5.times do + @gallery.images.build + end + end + end + + def update + @gallery = Gallery.find_by(id: params[:id]) + size = @gallery.images.size + if @gallery.update(whitelisted) + flash[:success] = ["Gallery: #{@gallery.title} updated!"] + check_count_change(size) + profile_img_msg + redirect_to user_gallery_path(current_user, @gallery) + else + flash.now[:danger] = ["something went, wrong.."] + @gallery.errors.full_messages.each do |msg| + flash.now[:danger] << msg + end + render :edit + end + end + + def destroy + gallery = Gallery.find_by(id: params[:id]) + flash[:success] = ["#{gallery.title} gallery has been deleted."] + gallery.destroy + redirect_to user_galleries_path(@user) + end + + private + def check_count_change(size) + if @gallery.images.size > size + flash[:success] << "Your image has been uploaded." << "Your image will be viewable after processing" + elsif @gallery.images.size < size + flash[:success] << "Your image has been deleted." + end + end + + def profile_img_msg + if params[:gallery][:images_attributes].any? {|k, v| v[:set_profile_photo] == "1"} + flash[:success] << "Your New Profile Photo will be set after processing" + end + end + + def whitelisted + params.require(:gallery).permit( + :id, + :title, + :description, + :user_id, + { images_attributes: [ + :id, + :gallery_id, + :picture, + :url, + :description, + :set_profile_photo, + :_destroy + ] + } + ) + end +end diff --git a/app/controllers/likes_controller.rb b/app/controllers/likes_controller.rb new file mode 100644 index 000000000..2d019ad7c --- /dev/null +++ b/app/controllers/likes_controller.rb @@ -0,0 +1,26 @@ +class LikesController < ApplicationController + + before_action :get_post + + def update + if @post + unless @post.liked_user_ids.include?(current_user.id) + @post.liked_users << current_user + end + redirect_back(fallback_location: root_path) + else + flash[:danger] = ["Post Does Not Exist"] + redirect_back(fallback_location: root_path) + end + end + + def destroy + Like.where(post_id: params[:id], user_id: current_user.id).destroy_all + redirect_back(fallback_location: root_path) + end + + private + def get_post + @post = Post.find_by(id: params[:id]) + end +end diff --git a/app/controllers/notices_controller.rb b/app/controllers/notices_controller.rb new file mode 100644 index 000000000..a43da3e1a --- /dev/null +++ b/app/controllers/notices_controller.rb @@ -0,0 +1,24 @@ +class NoticesController < ApplicationController + + def index + @notices = current_user.notices + end + + def show + @notice = Notice.find_by(user_id: current_user.id, id: params[:id]) + set_viewed if @notice + end + + def destroy + Notice.find_by(user_id: current_user.id, id: params[:id]).destroy + flash[:info] = ["Notice deleted."] + redirect_to root_path + end + + private + def set_viewed + unless @notice.viewed + @notice.update_attribute(:viewed, true) + end + end +end diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb new file mode 100644 index 000000000..0493506e7 --- /dev/null +++ b/app/controllers/posts_controller.rb @@ -0,0 +1,70 @@ +class PostsController < ApplicationController + + include UserCheck + + before_action :set_user + before_action :correct_user, except: [:show, :index] + + def index + if(params[:start_id] != "last") + @posts = @user.posts.order(created_at: :desc).where("post_type='Post' AND id < ?", params[:start_id]).limit(10) + else + @posts = @user.posts.order(created_at: :desc).where(post_type: "Post").limit(10) + end + end + + def create + @post = @user.posts.build(whitelisted) + if @post.save + flash[:success] = ["Profile Created"] + respond_to do |format| + format.js + format.html {redirect_to user_post_path(@user, @post)} + end + else + respond_to do |format| + format.js do + @status = :danger + @msg = ["Something went wrong.."] + @post.errors.full_messages.each do |error| + @msg << error + end + render template: 'shared/flashes' + end + format.html do + flash.now[:danger] = ["Something went wrong.."] + @post.errors.full_messages.each do |error| + flash.now[:danger] << error + end + render :new + end + end + end + end + + def show + @post = Post.find_by(id: params[:id]) + end + + def destroy + @post = Post.find_by(id: params[:id]) + if @post + if(@post.post) + flash[:success] = ["Comment deleted."] + else + flash[:success] = ["Post deleted."] + end + @post.destroy + else + flash[:danger] = ["Post not found."] + end + redirect_to root_path + end + + private + + def whitelisted + params.require(:post).permit(:body).merge(post_type: "Post") + end + +end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb new file mode 100644 index 000000000..60963ee74 --- /dev/null +++ b/app/controllers/profiles_controller.rb @@ -0,0 +1,69 @@ +class ProfilesController < ApplicationController + + include UserCheck + + before_action :set_user, except: [:show] + before_action :correct_user, except: [:index, :show] + + def show + set_user_full_profile + end + + def edit + current_user.profile.build_bio unless current_user.profile.bio + end + + def update + if @user.profile.update(whitelisted) + @user.profile.update_attribute(:edited, true) unless @user.profile.edited + flash[:success] = ["Profile Successfully Edited"] + set_profile_img if params[:profile][:profile_gallery_attributes] + redirect_to user_profile_path(@user) + else + flash.now[:danger] = ["Something went wrong.."] + @user.profile.errors.full_messages.each do |error| + flash.now[:danger] << error + end + render :edit + end + end + + private + def whitelisted + params.require(:profile).permit(:user_id, + :birthday, + :college, + :hometown, + :current_home, + :phone, + { bio_attributes: [ + :id, + :profile_id, + :slogan, + :about + ] + }, + { profile_gallery_attributes: [ + :id, + :user_id, + { images_attributes: [ + :id, + :gallery_id, + :url, + :picture, + :description + ] + } + ] + } + ) + end + + def set_profile_img + attributes = params[:profile][:profile_gallery_attributes][:images_attributes] + if attributes["0"][:url] || attributes["0"][:picture] + flash[:success] << "Your image has been uploaded." << "Your image will be viewable after processing" + @user.profile.fix_profile_image + end + end +end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb new file mode 100644 index 000000000..6a1f6deff --- /dev/null +++ b/app/controllers/search_controller.rb @@ -0,0 +1,9 @@ +class SearchController < ApplicationController + def show + if params[:search] == "" + @profiles = Profile.all + else + @profiles = Profile.where('UPPER(first_name) like ? OR UPPER(last_name) like ?', "%#{params[:search].upcase}%", "%#{params[:search].upcase}%") + end + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 000000000..a1487be27 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,52 @@ +class SessionsController < ApplicationController + skip_before_action :authenticate, only: [:new, :create] + + def new + create_session unless session[:user_id] + if signed_in_user? + redirect_to users_path + end + end + + def create + @user = User.find_by_email(params[:email].downcase) + if @user + validate_credentials + else + flash.now[:danger] = ["Incorrect Credentials"] + render :new + end + end + + def destroy + sign_out + flash[:success] = ["Signed Out"] + redirect_to signup_path + end + + def show + redirect_to new_session_path + end + + private + + def validate_credentials + case Sessionizer.validate_credentials(@user, params[:password]) + when "locked" + flash[:danger] = [ + "Your account has been locked for due to too many incorrect login attempts.", + "You may try again in #{(Time.now - @user.last_attempt).strftime("%M")} minutes" + ] + redirect_to root_path + when "valid" + sign_in(@user, params[:remember] == "true") + redirect_to users_path + else + flash.now[:danger] = [ + "Incorrect Credentials", + "Warning: #{5 - @user.failed} attempts remaining before your account will be put into lockdown mode" + ] + render :new + end + end + end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 000000000..abc90cc91 --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,88 @@ +class UsersController < ApplicationController + include UserCheck + + before_action :set_user, only: [:show, :edit, :update, :destroy] + before_action :correct_user, except: [:index, :show, :new, :create] + + skip_before_action :authenticate, only: [:new, :create] + + def create + @user = User.new(whitelisted) + if @user.save + sign_in(@user) + flash[:success] = ["Successfully signed up!"] + redirect_to users_path + else + flash.now[:danger] = ["Something went wrong signing up"] + @user.errors.full_messages.each do |error| + flash.now[:danger] << error + end + render :new + end + end + + def edit + end + + def index + @user = current_user + @post = current_user.posts.build + allowed_ids = [current_user.id, *current_user.friend_ids] + @posts = Post.includes(:comments).where(post_type: "Post", user_id: allowed_ids).order(created_at: :desc) + end + + def new + create_session unless session[:user_id] + if signed_in_user? + redirect_to users_path + end + @user = User.new + @user.build_profile + end + + def show + @post = current_user.posts.build + @posts = Post.includes(:comments).where(post_type: "Post", user_id: @user.id).order(created_at: :desc) + render :index + end + + def update + if @user.authenticate(params[:user][:current_password]) + if @user.update(whitelisted) + flash[:success] = ["Account Edited!"] + redirect_to @user + else + flash.now[:danger] = ["Something went wrong"] + @user.errors.full_messages.each do |error| + flash.now[:danger] << error + end + render :edit + end + else + flash.now[:danger] = ["Something went wrong", + "Current Password Required to change account credentials"] + render :edit + end + end + + private + + def whitelisted + + params.require(:user).permit( + :email, + :password, + :password_confirmation, + { + profile_attributes:[ + :id, + :first_name, + :last_name, + :birthday, + :gender + ] + } + ) + end + +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 000000000..a310bd965 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,25 @@ +module ApplicationHelper + def render_nav_content + if signed_in_user? + render_user_nav + else + render_login_nav + end + end + + def render_user_nav + render partial: 'shared/navbar/user' + end + + def render_notice_badge + " #{current_user.notice_count}".html_safe if current_user.notice_count > 0 + end + + def render_login_nav + render partial: 'shared/navbar/anon' + end + + def format_user_name(user) + "#{user.profile.first_name} #{user.profile.last_name}" + end +end diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb new file mode 100644 index 000000000..0ec9ca5f2 --- /dev/null +++ b/app/helpers/comments_helper.rb @@ -0,0 +1,2 @@ +module CommentsHelper +end diff --git a/app/helpers/friends_helper.rb b/app/helpers/friends_helper.rb new file mode 100644 index 000000000..0b69e9bce --- /dev/null +++ b/app/helpers/friends_helper.rb @@ -0,0 +1,2 @@ +module FriendsHelper +end diff --git a/app/helpers/galleries_helper.rb b/app/helpers/galleries_helper.rb new file mode 100644 index 000000000..3a98ed09d --- /dev/null +++ b/app/helpers/galleries_helper.rb @@ -0,0 +1,52 @@ +module GalleriesHelper + def gallery_cover(gallery) + gallery.images.size == 0 ? (image_tag "//s3.amazonaws.com/viking_education/web_development/web_app_eng/user_silhouette_generic.gif") : (image_tag gallery.images.first.picture.url(:medium)) + end + + def render_gallery_image(photo) + if Rails.configuration.aws_images + if photo.post + link_to user_post_path(photo.gallery.user, photo.post) do + image_tag photo.picture.url(:medium) + end + else + image_tag photo.picture.url(:medium) + end + else + "//s3.amazonaws.com/viking_education/web_development/web_app_eng/user_silhouette_generic.gif" + end + end + + def image_upload_form(form, gallery, build = nil) + if Rails.configuration.aws_images + " +
+

(If you both choose an image and enter a Url, the Url will take precedence and selected image will be discarded)

+
+ #{render_image_upload(form, gallery, build)} + ".html_safe + else + "

Image Uploading disabled by admin

".html_safe + end + end + + def render_image_upload(form, gallery, build) + form.fields_for gallery do |gf| + if defined?(build) + gf.fields_for :images, build do |gif| + render partial: 'shared/forms/img_upload_form' , locals: { form: gif } + end + else + gf.fields_for :images do |gif| + render partial: 'shared/forms/img_upload_form' , locals: { form: gif } + end + end + end + end + # def set_aws_options + # if Rails.configuration.aws_images + # s3_direct_post = S3_BUCKET.presigned_post(key: "uploads/#{current_user.id}/images/original/#{SecureRandom.uuid}-${filename}", success_action_status: '201', acl: 'public-read') + # {'form-data' => (s3_direct_post.fields), 'url' => s3_direct_post.url, 'host' => URI.parse(s3_direct_post.url).host } + # end + # end +end diff --git a/app/helpers/likes_helper.rb b/app/helpers/likes_helper.rb new file mode 100644 index 000000000..c705f1959 --- /dev/null +++ b/app/helpers/likes_helper.rb @@ -0,0 +1,9 @@ +module LikesHelper + def render_like(post) + if post.liked_user_ids.include?(current_user.id) + link_to "Unlike", like_path(post), method: :delete + else + link_to "Like", like_path(post), method: :put + end + end +end diff --git a/app/helpers/notices_helper.rb b/app/helpers/notices_helper.rb new file mode 100644 index 000000000..7adf7a648 --- /dev/null +++ b/app/helpers/notices_helper.rb @@ -0,0 +1,2 @@ +module NoticesHelper +end diff --git a/app/helpers/posts_helper.rb b/app/helpers/posts_helper.rb new file mode 100644 index 000000000..d788a5f20 --- /dev/null +++ b/app/helpers/posts_helper.rb @@ -0,0 +1,13 @@ +module PostsHelper + def render_post_comments(post, limit = nil) + if limit + comments = post.comments.limit(limit).select{|com| com.user } + else + p post.comments + comments = post.comments.select{|com| com.user } + end + if comments.length > 0 + render partial: 'shared/posts/comments', locals: { comments: comments} + end + end +end diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb new file mode 100644 index 000000000..200f2af29 --- /dev/null +++ b/app/helpers/profiles_helper.rb @@ -0,0 +1,21 @@ +module ProfilesHelper + + def profile_upload_form(form) + image_upload_form(form, :profile_gallery, current_user.profile.profile_gallery.images.build) + end + + def render_profile_img(profile) + if Rails.configuration.aws_images && profile.profile_img + profile.profile_img.picture.url(:medium) || "//s3.amazonaws.com/viking_education/web_development/web_app_eng/user_silhouette_generic.gif" + else + "//s3.amazonaws.com/viking_education/web_development/web_app_eng/user_silhouette_generic.gif" + end + end + + # def set_aws_options + # if Rails.configuration.aws_images + # s3_direct_post = S3_BUCKET.presigned_post(key: "uploads/#{current_user.id}/images/original/#{SecureRandom.uuid}-${filename}", success_action_status: '201', acl: 'public-read') + # {'form-data' => (s3_direct_post.fields), 'url' => s3_direct_post.url, 'host' => URI.parse(s3_direct_post.url).host } + # end + # end +end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb new file mode 100644 index 000000000..b3ce20acb --- /dev/null +++ b/app/helpers/search_helper.rb @@ -0,0 +1,2 @@ +module SearchHelper +end diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb new file mode 100644 index 000000000..309f8b2eb --- /dev/null +++ b/app/helpers/sessions_helper.rb @@ -0,0 +1,2 @@ +module SessionsHelper +end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 000000000..9ef5d7db6 --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,15 @@ +module UsersHelper + def render_new_post(user, post) + if user == current_user || !user + render partial: 'shared/forms/new_post', locals: {user: current_user, post: post} + end + end + + def render_posts(posts) + if posts.length > 0 + render partial: 'shared/posts/post', collection: posts, as: :post, locals: { size: "small", limit: 4} + else + '

No posts to show

'.html_safe + end + end +end diff --git a/app/jobs/add_to_profile_photos_job.rb b/app/jobs/add_to_profile_photos_job.rb new file mode 100644 index 000000000..344c41050 --- /dev/null +++ b/app/jobs/add_to_profile_photos_job.rb @@ -0,0 +1,8 @@ +class AddToProfilePhotosJob < ApplicationJob + queue_as :default + + def perform(image_id) + # Do something later + Image.find(image_id).copy_to_profile_photos + end +end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 000000000..a009ace51 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/app/jobs/fix_counters_job.rb b/app/jobs/fix_counters_job.rb new file mode 100644 index 000000000..24f2e4caf --- /dev/null +++ b/app/jobs/fix_counters_job.rb @@ -0,0 +1,29 @@ +class FixCountersJob < ApplicationJob + queue_as :low_priority + + def perform(*args) + # Do something later + # User.all.each do |user| + # + # end + ActiveRecord::Base.connection.execute <<-SQL.squish + UPDATE users + SET notice_count = (SELECT count(1) + FROM notices + WHERE (notices.user_id = users.id) AND notices.viewed = false) + SQL + ActiveRecord::Base.connection.execute <<-SQL.squish + UPDATE posts + SET likes_count = (SELECT count(1) + FROM likes + WHERE likes.post_id = posts.id), + comments_count = (SELECT count(1) + FROM posts + WHERE posts.post_id = posts.id) + SQL + end + + after_perform do + FixCountersJob.set(wait: 60.minutes).perform_later + end +end diff --git a/app/jobs/process_image_job.rb b/app/jobs/process_image_job.rb new file mode 100644 index 000000000..40375218a --- /dev/null +++ b/app/jobs/process_image_job.rb @@ -0,0 +1,9 @@ +class ProcessImageJob < ApplicationJob + queue_as :images + + def perform(id) + # Do something later + img = Image.find_by(id: id) + img.process_img + end +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 000000000..286b2239d --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' +end diff --git a/app/mailers/profile_mailer.rb b/app/mailers/profile_mailer.rb new file mode 100644 index 000000000..9fac0e227 --- /dev/null +++ b/app/mailers/profile_mailer.rb @@ -0,0 +1,9 @@ +class ProfileMailer < ApplicationMailer + default from: "Sampson Crowley " + + def welcome(profile_id) + @profile = Profile.includes(:user).find(profile_id) + @title = "Velkommen to Danebook" + mail(to: @profile.user.email, subject: "Velkommen! Explore your new Danebook account.") + end +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 000000000..10a4cba84 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/app/models/authenticator.rb b/app/models/authenticator.rb new file mode 100644 index 000000000..ebee3642a --- /dev/null +++ b/app/models/authenticator.rb @@ -0,0 +1,15 @@ +class Authenticator + class << self + def crypt + @crypt ||= ActiveSupport::MessageEncryptor.new(Rails.application.secrets.secret_key_base) + end + + def decrypt(msg) + crypt.decrypt_and_verify(msg) + end + + def encrypt(msg) + crypt.encrypt_and_sign(msg) + end + end +end diff --git a/app/models/auto_poster.rb b/app/models/auto_poster.rb new file mode 100644 index 000000000..d0167017f --- /dev/null +++ b/app/models/auto_poster.rb @@ -0,0 +1,14 @@ +class AutoPoster + class << self + include ActionView::Helpers::UrlHelper + include ActionView::Helpers::AssetTagHelper + + def new_image(img) + body = "

#{image_tag(img.picture.url(:original))}

" + post = Post.create(post_type: "Image", body: body, user: img.user) + img.post = post + img.save + end + + end +end diff --git a/app/models/bio.rb b/app/models/bio.rb new file mode 100644 index 000000000..9564764f7 --- /dev/null +++ b/app/models/bio.rb @@ -0,0 +1,3 @@ +class Bio < ApplicationRecord + belongs_to :profile +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/models/crypt.rb b/app/models/crypt.rb new file mode 100644 index 000000000..08fff1da0 --- /dev/null +++ b/app/models/crypt.rb @@ -0,0 +1,16 @@ +class Crypt + class << self + def crypt + @crypt ||= ActiveSupport::MessageEncryptor.new(Rails.application.secrets.secret_key_base) + end + + def decrypt(msg) + crypt.decrypt_and_verify(msg) + end + + def encrypt(msg) + crypt.encrypt_and_sign(msg) + end + end + +end diff --git a/app/models/friend_request.rb b/app/models/friend_request.rb new file mode 100644 index 000000000..3c2530ef6 --- /dev/null +++ b/app/models/friend_request.rb @@ -0,0 +1,7 @@ +class FriendRequest < ApplicationRecord + belongs_to :user + belongs_to :request, class_name: "User" + + validates_uniqueness_of :request_id, scope: [:user_id] + +end diff --git a/app/models/friendify.rb b/app/models/friendify.rb new file mode 100644 index 000000000..c0afd7170 --- /dev/null +++ b/app/models/friendify.rb @@ -0,0 +1,133 @@ +class Friendify + class << self + include ActionView::Helpers::UrlHelper + + def friendship(user, friend) + + if user.id == friend.id + + [ + :danger, + [ + "You can't friend yourself!", + "Lonely Bastard..." + ] + ] + + elsif friends?(user, friend) + + [ + :danger, + ["You're Already Friends!"] + ] + + elsif accepting_request?(user, friend) + + accept_request(user, friend) + + else + + send_request(user, friend) + + end + end + + def clear_friendship(user, friend) + FriendsUser. + where(user_id: user.id, friend_id: friend.id). + or( + FriendsUser. + where(user_id: friend.id, friend_id: user.id) + ). + destroy_all + + FriendRequest. + where(user_id: user.id, request_id: friend.id). + or(FriendRequest. + where(user_id: friend.id, request_id: user.id) + ). + destroy_all + end + + private + + def accept_request(user, friend) + + clear_friendship(user, friend) + + set_friends(user, friend) + + [ + :success, + ["Friend Request Accepted!"] + ] + + end + + + def send_request(user, friend) + + request = FriendRequest.new(user_id: friend.id, request_id: user.id) + + if request.save + + messages = [ + "#{user.profile.first_name} #{user.profile.last_name} would like to be your friend. + #{link_to "Accept", Rails.application.routes.url_helpers.user_friends_path(user), method: :post, class: "btn btn-default pull-right friend"} + #{link_to "Reject", Rails.application.routes.url_helpers.user_friends_path(user), method: :delete, class: "btn btn-default pull-right unfriend"}" + ] + Notice.create(user: friend, title: "New Friend Request", messages: messages ) + + [ + :success, + ["Friend Request Sent!"] + ] + + else + + [ + :danger, + ["Friend Request Already Sent. Stop buggin them!"] + ] + + end + end + + def accepting_request?(user, friend) + friend.requested_friends.include?(user) + end + + def friends?(user, friend) + user.friends.include?(friend) + end + + + def set_friends(user, friend) + + user.friends << friend + + friend.friends << user + + friend.notices.each do |notice| + notice.destroy if notice.messages.any?{|msg| msg =~ /#{user.profile.first_name} #{user.profile.last_name} would like to be your friend./} + end + user.notices.each do |notice| + notice.destroy if notice.messages.any?{|msg| msg =~ /#{friend.profile.first_name} #{friend.profile.last_name} would like to be your friend./} + end + + + messages = [ + "You have a new friendship with #{friend.profile.first_name} #{friend.profile.last_name}.", + "#{link_to "Click Here to view their profile", Rails.application.routes.url_helpers.user_path(friend)}" + ] + Notice.create(user: user, title: "Friend request Accepted", messages: messages ) + + messages = [ + "You have a new friendship with #{user.profile.first_name} #{user.profile.last_name}.", + "#{link_to "Click Here to view their profile", Rails.application.routes.url_helpers.user_path(user)}" + ] + Notice.create(user: friend, title: "Friend request Accepted", messages: messages ) + + end + end +end diff --git a/app/models/friends_user.rb b/app/models/friends_user.rb new file mode 100644 index 000000000..ddf78065a --- /dev/null +++ b/app/models/friends_user.rb @@ -0,0 +1,9 @@ +class FriendsUser < ApplicationRecord + belongs_to :user + belongs_to :friend, class_name: "User" + + default_scope { + includes :user + } + +end diff --git a/app/models/gallery.rb b/app/models/gallery.rb new file mode 100644 index 000000000..4c98773af --- /dev/null +++ b/app/models/gallery.rb @@ -0,0 +1,20 @@ +class Gallery < ApplicationRecord + validate :protect_profile_gallery, on: :update + + belongs_to :user + has_many :images, -> { order(:created_at) }, inverse_of: :gallery, dependent: :destroy + accepts_nested_attributes_for :images, allow_destroy: true, reject_if: :all_blank + + validates_uniqueness_of :title, scope: [:user_id] + + def protect_profile_gallery + if self.changes[:title] && self.changes[:title].include?("Profile Images") + errors.add(:title, :invalid, message: "cannot be modified for profile gallery") + end + end + + def destroy + raise "Cannot delete profile gallery" unless user.nil? || self.title != "Profile Images" + super + end +end diff --git a/app/models/image.rb b/app/models/image.rb new file mode 100644 index 000000000..0791a29e7 --- /dev/null +++ b/app/models/image.rb @@ -0,0 +1,80 @@ +class Image < ApplicationRecord + before_save :file_or_url + after_create :set_job + + attr_reader :set_profile_photo + + belongs_to :gallery, inverse_of: :images + belongs_to :post, optional: true, dependent: :destroy + + has_one :user, through: :gallery + + has_attached_file :picture, styles: {large: '500x500>', medium: '300x300>', thumb: "100x100>"}, default_url: 'https://s3-us-west-1.amazonaws.com/danebook-sampson-crowley-dev/uploads/1/images/original/loading.svg' + + validates_attachment_content_type :picture, content_type: /\Aimage.*\Z/ + + process_in_background :picture + + has_one :profile, dependent: :nullify + + before_post_process :randomize_file_name + after_post_process :create_post + + def randomize_file_name + unless url && url != "" + extension = File.extname(picture_file_name).downcase + self.picture.instance_write(:file_name, "#{SecureRandom.hex}-#{picture_file_name.downcase}") + end + end + + def set_profile_photo=(bool) + if bool == "1" + AddToProfilePhotosJob.perform_later self.id + end + end + + def copy_to_profile_photos + if self.user.profile.profile_gallery.images.any? {|pgi| pgi.picture_file_name == self.picture_file_name } + self.user.profile.update_attribute(:profile_img, self) + else + new_copy = self.user.profile.profile_gallery.images.build(picture: self.picture) + new_copy.save + self.user.profile.fix_profile_image + end + end + # handle_asynchronously :set_profile_photo + + def create_post + self.url = nil + AutoPoster.new_image(self) + end + handle_asynchronously :create_post + + def file_or_url + url = nil if url == "" + picture_file_name = File.basename(URI.parse(self.url).path) if url + picture = nil if url + end + + def set_job + if self.url && self.url != "" + ProcessImageJob.perform_later self.id + end + end + + def process_img + PullTempfile.transaction(url: self.url, original_filename: "#{SecureRandom.hex}-#{File.basename(URI.parse(self.url).path).downcase}") do |tmp_image| + self.picture = tmp_image + self.url = nil + unless save + profile = self.profile + messages = self.errors.full_messages.select {|e| e !~ /Paperclip/} + messages << "Make sure you are using a web-safe image." + Notice.create(user: self.gallery.user, title: "Image Processing Failed", messages: messages ) + self.destroy + profile.fix_profile_image if profile + end + end + end + # handle_asynchronously :process_img +end diff --git a/app/models/like.rb b/app/models/like.rb new file mode 100644 index 000000000..4abcef626 --- /dev/null +++ b/app/models/like.rb @@ -0,0 +1,7 @@ +class Like < ApplicationRecord + belongs_to :user + belongs_to :post, counter_cache: true + + validates_uniqueness_of :user_id, scope: [:post_id] + +end diff --git a/app/models/notice.rb b/app/models/notice.rb new file mode 100644 index 000000000..fd479924d --- /dev/null +++ b/app/models/notice.rb @@ -0,0 +1,32 @@ +class Notice < ApplicationRecord + after_create :up_count + before_destroy :destroy_count + after_save :check_count + + belongs_to :user + + private + def up_count + count = user.notice_count + 1 + self.user.update_attribute(:notice_count, count) + end + + def down_count + count = user.notice_count - 1 + self.user.update_attribute(:notice_count, count) + end + + def destroy_count + unless self.viewed + down_count + end + end + + def check_count + if self.changes[:viewed] == [false, true] + down_count + elsif self.changes[:viewed] == [true, false] + up_count + end + end +end diff --git a/app/models/post.rb b/app/models/post.rb new file mode 100644 index 000000000..b3047e88f --- /dev/null +++ b/app/models/post.rb @@ -0,0 +1,23 @@ +class Post < ApplicationRecord + belongs_to :user + belongs_to :post, optional: true + + has_many :likes, foreign_key: :post_id, dependent: :destroy + has_many :liked_users, through: :likes, source: :user + + has_one :image, dependent: :destroy + + has_many :comments, -> { order(:created_at) }, class_name: "Post", foreign_key: :post_id, dependent: :destroy + + validate :posting_on_friend + + def posting_on_friend + unless post == nil || post.user.id == user_id || post.user.friend_ids.include?(user_id) + errors.add(:post, :invalid, message: "- You can only post to you own or your friends' posts") + end + end + + default_scope { + includes :user + } +end diff --git a/app/models/profile.rb b/app/models/profile.rb new file mode 100644 index 000000000..eabe40105 --- /dev/null +++ b/app/models/profile.rb @@ -0,0 +1,57 @@ +class Profile < ApplicationRecord + before_save :format_input + before_save :normalize_blank_values + + after_create :send_welcome_email + + enum gender_type: [:Male, :Female, :Other] + + belongs_to :user, inverse_of: :profile + + has_one :bio, dependent: :destroy + accepts_nested_attributes_for :bio, reject_if: :all_blank + + has_one :profile_gallery, -> {where(title: "Profile Images")}, through: :user, source: :galleries + accepts_nested_attributes_for :profile_gallery, reject_if: :all_blank + + has_many :images, through: :profile_gallery + accepts_nested_attributes_for :images, reject_if: :all_blank + + belongs_to :profile_img, class_name: "Image", foreign_key: :image_id, optional: :true + + belongs_to :cover, class_name: "Image", foreign_key: :cover_id, optional: :true + + validates_presence_of :first_name, :last_name, :birthday, :gender + validates_format_of :phone, with: /\A(?=.*\d)[0-9\- +]+\Z/, allow_nil: true, allow_blank: true + validates :phone, length: { minimum: 4, maximum: 30 }, allow_nil: true, allow_blank: true + + + def normalize_blank_values + attributes.each do |column, value| + self[column].present? || self[column] = nil + end + end + + default_scope { + includes :profile_img + } + + def self.genders + gender_types.keys.to_a.map{|v| [v.humanize, v.to_s.classify]} + end + + def fix_profile_image + self.update_attribute(:profile_img, profile_gallery.images.last) + end + + private + def format_input + first_name.capitalize! if first_name + last_name.capitalize! if last_name + end + + def send_welcome_email + ProfileMailer.welcome(self.id).deliver_later + end + +end diff --git a/app/models/sessionizer.rb b/app/models/sessionizer.rb new file mode 100644 index 000000000..db5d4ca3e --- /dev/null +++ b/app/models/sessionizer.rb @@ -0,0 +1,81 @@ +class Sessionizer + class << self + + def validate_credentials(user, password) + @user = user + @password = password + + if locked + + return "locked" + + elsif valid? + + reset_failed + + return "valid" + + else + + failed_attempt + + "invalid" + + end + end + + private + CHAR_MAP = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789!@#$%^&*' + + # TODO: Implement JWT tokens for multi browser login + # def create_jwt + # data = {auth: 0, id: @user.id, browser: ""} + # sql_key = Digest::SHA256.base64digest((0..32).map{ CHAR_MAP.chars.sample }.join) + # cookie_key = Digest::SHA256.base64digest((0..32).map{ CHAR_MAP.chars.sample }.join) + # sec = Digest::SHA256.base64digest("#{sql_key}#{cookie_key}#{Rails.application.secrets.secret_key_base}") + # + # cookies[:token_key] = cookie_key + # @user.token = sql_key + # @user.save + # + # exp = Time.now.to_i + 4 * 3600 + # iss = 'Project Danbook' + # iat = Time.now.to_i + # jti_raw = [sec, iat].join(':').to_s + # jti = Digest::MD5.hexdigest(jti_raw) + # nbf = Time.now.to_i + 10 + # payload = { data: data, exp: exp, iat: iat, jti: jti, nbf: nbf } + # + # cookies[:token] = JWT.encode payload, sec, 'HS256' + # end + + def failed_attempt + if time_limit + @user.failed += 1 + else + @user.failed = 1 + end + @user.last_attempt = Time.now + @user.save + end + + def locked + @user.failed && @user.failed >= 5 && time_limit + end + + def reset_failed + @user.failed = 0 + @user.last_attempt = nil + @user.save + end + + def time_limit + @user.last_attempt && @user.last_attempt > 1.hour.ago + end + + def valid? + @user && @user.authenticate(@password) + end + + end +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 000000000..7df71b4eb --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,70 @@ +class User < ApplicationRecord + attr_accessor :current_password + + after_create :set_up_profile_gallery + after_destroy :destroy_orphans + + before_save :format_input + + has_one :profile, inverse_of: :user, dependent: :destroy + accepts_nested_attributes_for :profile, reject_if: :all_blank + + has_many :f_requests, class_name: "FriendRequest", dependent: :destroy + has_many :friend_requests, through: :f_requests, source: :request + + has_many :r_friends, class_name: "FriendRequest", foreign_key: :request_id + has_many :requested_friends, through: :r_friends, source: :user + + has_many :friends_users, dependent: :destroy + has_many :friends, through: :friends_users + + has_many :posts, dependent: :destroy + + has_many :likes, dependent: :destroy + has_many :liked_posts, through: :likes, source: :post + + has_many :notices, -> { order(created_at: :desc) }, dependent: :destroy + + has_many :galleries, dependent: :nullify + + has_many :images, -> { order(created_at: :desc).limit(9) }, through: :galleries + + + has_secure_password + + validates_uniqueness_of :email + validates_format_of :email, with: /\A[^@]+@[^@]+\.[^@]+\Z/, allow_nil: true + validates_format_of :password, with: /\A(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{12,}\Z/, allow_nil: true + + def regenerate_auth_token + destroy_token + generate_token + save! + end + + def destroy_token + self.token = nil + end + +private + + def format_input + email.downcase! if email + end + + def generate_token + begin + self[:token] = SecureRandom.urlsafe_base64 + end while User.exists?(token: self[:token]) + end + + def set_up_profile_gallery + self.galleries.create(title: "Profile Images") + end + + def destroy_orphans + Gallery.where(user_id: nil).destroy_all + end + + +end diff --git a/app/views/comments/_comment_form.html.erb b/app/views/comments/_comment_form.html.erb new file mode 100644 index 000000000..b012818d4 --- /dev/null +++ b/app/views/comments/_comment_form.html.erb @@ -0,0 +1,6 @@ +<%= form_for [post, comment], url: user_post_comment_path do |com_f| %> + <%= com_f.label :body, "Write a Comment..", class: "sr-only" %> + <%= com_f.text_area :body, class: "form-control", rows: "3", placeholder: "Write a Comment.." %> + <%= link_to "Cancel", request.referer || root_path %> + <%= com_f.submit "Comment", class: "btn btn-primary pull-right" %> +<% end %> diff --git a/app/views/comments/new.html.erb b/app/views/comments/new.html.erb new file mode 100644 index 000000000..6b27cb5b0 --- /dev/null +++ b/app/views/comments/new.html.erb @@ -0,0 +1,5 @@ +<%= content_for :usernav, true %> +<%= content_for :post_footer do %> + <%= render partial: 'comment_form', locals: {post: @post, comment: @comment} %> +<% end %> +<%= render partial: 'shared/posts/post', locals: { post: @post, size: "large", limit:4} %> diff --git a/app/views/comments/new.js.erb b/app/views/comments/new.js.erb new file mode 100644 index 000000000..1e80fed2e --- /dev/null +++ b/app/views/comments/new.js.erb @@ -0,0 +1,3 @@ +var el = document.createElement('div'); +el.innerHTML = "<%= j(render partial: 'comment_form', locals: {post: @post, comment: @comment}) %>" +DANEBOOK.Posts.showCommentForm(<%= @post.id %>, el.firstChild) diff --git a/app/views/friends/show.html.erb b/app/views/friends/show.html.erb new file mode 100644 index 000000000..094d20621 --- /dev/null +++ b/app/views/friends/show.html.erb @@ -0,0 +1,18 @@ +<%= content_for :usernav, true %> +
+
+
+

Friends

+
+
+
+
+
+ <% @friends.each do |friend| %> + <%= render partial: 'shared/friends/friend', locals: {user: friend} %> + <% end %> +
+
+
+ +
diff --git a/app/views/galleries/edit.html.erb b/app/views/galleries/edit.html.erb new file mode 100644 index 000000000..2dd1a7a47 --- /dev/null +++ b/app/views/galleries/edit.html.erb @@ -0,0 +1,72 @@ +<%= content_for :usernav, true %> + +<%= content_for :usernav, true %> + +
+
+
+

Edit <%= @gallery.title %>

+
+
+
+
+ <%= form_for([current_user, @gallery], html: { class: 'syncronousUpload' }) do |f| %> +
+
+
+ <% unless @gallery.title == "Profile Images" %> +
+ <%= f.label :title %>
+ <%= f.text_field :title, class: "form-control", placeholder: "Enter a title (required)", required: true %> +
+
+ <%= f.label :description, class: 'sr-only image-upload-description' %>
+ <%= f.text_field :description, class: "form-control", placeholder: "Enter a description (optional)"%> +
+ <% end %> +
+
+
+ +
+
+

Edit Images

+
+ <%= f.fields_for :images do |gif| %> + <% if gif.object.persisted? %> +
+ <%= render partial: 'shared/forms/img_edit_form' , locals: { form: gif } %> +
+ <% end %> + <% end %> +
+
+
+ <% unless @gallery.title == "Profile Images" %> +
+
+

Upload Images (up to 5 new images at once)

+
+

(If you both choose an image and enter a Url, the Url will take precedence and selected image will be discarded)

+
+
+ <%= f.fields_for :images do |gif| %> + <% unless gif.object.persisted? %> +
+ <%= render partial: 'shared/forms/img_upload_form' , locals: { form: gif } %> +
+ <% end %> + <% end %> +
+
+
+ <% end %> +
+
+ <%= f.submit "Save Changes", class: "btn btn-block btn-primary" %> +
+
+ <% end %> +
+
+
diff --git a/app/views/galleries/index.html.erb b/app/views/galleries/index.html.erb new file mode 100644 index 000000000..d550d3723 --- /dev/null +++ b/app/views/galleries/index.html.erb @@ -0,0 +1,28 @@ +<%= content_for :usernav, true %> +
+
+
+ <%= link_to "Add Gallery!", new_user_gallery_path(@user), class: "edit-button btn btn-primary" if @user == current_user %> +

Galleries

+
+
+ +
+
+
+ <% @galleries.each do |gallery| %> +
+ <%= link_to user_gallery_path(@user, gallery) do %> + <%= gallery_cover(gallery) %> + <% end %> + +
+ <%= gallery.title %> +
+
+ <% end %> +
+
+
+ +
diff --git a/app/views/galleries/new.html.erb b/app/views/galleries/new.html.erb new file mode 100644 index 000000000..019404a54 --- /dev/null +++ b/app/views/galleries/new.html.erb @@ -0,0 +1,52 @@ +<%= content_for :usernav, true %> + +
+
+
+

New Gallery

+
+
+
+
+ <%= form_for([current_user, @gallery], html: { class: 'syncronousUpload' }) do |f| %> +
+
+
+
+ <%= f.label :title %>
+ <%= f.text_field :title, class: "form-control", placeholder: "Enter a title (required)", required: true %> +
+
+ <%= f.label :description, class: 'sr-only image-upload-description' %>
+ <%= f.text_field :description, class: "form-control", placeholder: "Enter a description (optional)"%> +
+
+
+
+ +
+
+

Upload Images (up to 5 new images at once)

+
+

(If you both choose an image and enter a Url, the Url will take precedence and selected image will be discarded)

+
+
+ <%= f.fields_for :images do |gif| %> + <% unless gif.object.persisted? %> +
+ <%= render partial: 'shared/forms/img_upload_form' , locals: { form: gif } %> +
+ <% end %> + <% end %> +
+
+
+
+
+ <%= f.submit "Save Changes", class: "btn btn-block btn-primary" %> +
+
+ <% end %> +
+
+
diff --git a/app/views/galleries/show.html.erb b/app/views/galleries/show.html.erb new file mode 100644 index 000000000..2c375b554 --- /dev/null +++ b/app/views/galleries/show.html.erb @@ -0,0 +1,29 @@ +<%= content_for :usernav, true %> +
+
+
+ <% if current_user == @user && @gallery.title != "Profile Images" %> + <%= link_to "Delete Gallery", user_gallery_path(current_user, @gallery), class: "pull-right btn btn-danger", method: :delete, data: {confirm: "Are you sure? All images in the gallery will be deleted"} %> + <% end %> + <% if current_user == @user %> + <%= link_to "Edit and Add Photos", edit_user_gallery_path(current_user, @gallery), class: "pull-right btn btn-primary" %> + <% end %> + <%= link_to "Back", user_galleries_path(@user), class: "pull-left btn btn-info" %> +

<%= @gallery.title %>

+
+
+ +
+
+
+ <% @gallery.images.each_with_index do |img, i| %> + <%= render partial: 'shared/photos/photo', locals: {photo: img } %> + <% if i % 4 == 3 %> +
+ <% end %> + <% end %> +
+
+
+ +
diff --git a/app/views/layouts/about.html b/app/views/layouts/about.html new file mode 100644 index 000000000..a6157dc36 --- /dev/null +++ b/app/views/layouts/about.html @@ -0,0 +1,136 @@ + + + + + + + + + Danebook About Me + + + + + + + + + + +
+ + +
+
+
+ Cover Image +
+

Harry Potter

+
+ profile +
+
+ + +
+
+
+ +
+
+
+
+ Edit your Profile +

About

+
+
+
+
+
+
+

Basic Information

+
+
Birthday:
+
July 31st, 1980
+
College:
+
Hogwarts College
+
Hometown:
+
Godrick's Hollow, England
+
Currently Lives:
+
Godrick's Hollow, England
+
+ +

Contact Information

+
+

Email:

+

harry_potter@hogwarts.edu

+

Telephone:

+

555-123-4567

+
+
+ +
+

Words To Live By

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Debitis minima natus assumenda, iste nesciunt odio dolorum. Eveniet accusamus, quis, esse dignissimos ex fuga est, excepturi ab non itaque nostrum quae. +

+

About Me

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas viverra tellus nisi, id condimentum dui tristique sed. Donec luctus eros quis ante consequat sagittis. Phasellus consectetur lectus ac lectus mattis vulputate. Maecenas non risus tellus. Proin ullamcorper, ex id iaculis scelerisque, odio dui posuere purus, sit amet viverra mi nibh sit amet risus. Duis tempor pretium arcu, ut dictum justo interdum pharetra. Suspendisse vulputate pellentesque massa, eu gravida diam semper ut. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pharetra erat eget vulputate lacinia. Aenean in porta elit. Aenean egestas in urna id bibendum. Integer nec placerat metus. +

+
+
+
+
+
+
+ +
+ + + + + + \ No newline at end of file diff --git a/app/views/layouts/about_edit.html b/app/views/layouts/about_edit.html new file mode 100644 index 000000000..4ad4f9907 --- /dev/null +++ b/app/views/layouts/about_edit.html @@ -0,0 +1,148 @@ + + + + + + + + + + Danebook About Me + + + + + + + + + + +
+ + +
+
+
+ Cover Image +
+

Harry Potter

+
+ profile +
+
+ + +
+
+
+ +
+
+
+
+

About

+
+
+
+
+
+
+ +
+

Basic Information

+
+
+
+ +
+
+ +
+
+ +
+
+
+ +

Contact Information

+
+
+

+ +
+ +

+
+
+ +
+ +
+

+ +

+ +
+
+ +
+
+
+
+
+
+ +
+ + + + + + \ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 000000000..1161278a8 --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,24 @@ + + + + ProjectDanebook + <%= csrf_meta_tags %> + + <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> + <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> + + + + + <%= render partial: 'shared/navbar' %> +
+ <% if content_for?(:usernav) %> + <%= render partial: 'shared/user_navbar', locals: {user: @user || current_user} %> + <% end %> + <%= render partial: 'shared/flashes' %> +
+ <%= yield %> +
+
+ + diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 000000000..271d4d995 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,132 @@ + + + + + + + <%= @title %> + + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+

+ <%= @title %> +

+
+ + + <%= yield %> + +
+
+ + + + + + + +
+ Visit The Danebook + + Danebook +
+
+
+ + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 000000000..ec5a14199 --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1,3 @@ +<%= @title %> + +<%= yield %> diff --git a/app/views/layouts/photos.html b/app/views/layouts/photos.html new file mode 100644 index 000000000..97de74c7c --- /dev/null +++ b/app/views/layouts/photos.html @@ -0,0 +1,230 @@ + + + + + + + + + Danebook About Me + + + + + + + + + + +
+ +
+
+
+ Cover Image +
+

Harry Potter

+
+ profile +
+
+ + +
+
+
+ +
+
+
+
+ + Add Photo! +

Photos

+
+
+ +
+
+
+ +
+ + profile photo + + +
+ +
+
+ +
+ + profile photo + + +
+ +
+
+ +
+ + profile photo + + +
+ +
+
+ +
+ + profile photo + + +
+ +
+
+ +
+ + profile photo + + +
+ +
+
+ +
+ + profile photo + + +
+ +
+
+ +
+ + profile photo + + +
+ +
+
+ +
+ + profile photo + + +
+ +
+
+ +
+ + profile photo + + +
+ +
+
+ +
+ + profile photo + + +
+ +
+
+ +
+ + profile photo + + +
+ +
+
+ +
+ + profile photo + + +
+ +
+
+ + +
+
+
+ +
+
+ +
+ + + + + \ No newline at end of file diff --git a/app/views/layouts/timeline.html b/app/views/layouts/timeline.html new file mode 100644 index 000000000..717ed836b --- /dev/null +++ b/app/views/layouts/timeline.html @@ -0,0 +1,496 @@ + + + + + + + + + Danebook About Me + + + + + + + + + + +
+ +
+
+
+ Cover Image +
+

Harry Potter

+
+ profile +
+
+ + +
+
+
+ +
+ + + + + + + + +
+ +
+ +
+
+
+

Post

+
+
+ +
+
+
+ + +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+
+
+ +
+
+ Harry Potter +

Posted on Thursday 6/1/2014

+
+
+
+
+
+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Asperiores harum quisquam nostrum veritatis, provident maiores deleniti vero earum sunt ex quam modi magni, sed hic atque rerum eveniet? Nam, voluptatum.

+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
+
+ +
+
+
+ +
+
+ Harry Potter +

Posted on Thursday 5/31/2014

+
+
+
+
+
+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Asperiores harum quisquam nostrum veritatis, provident maiores deleniti vero earum sunt ex quam modi magni, sed hic atque rerum eveniet? Nam, voluptatum.

+
+
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+
+
+ +
+
+ Harry Potter +

Posted on Thursday 5/31/2014

+
+
+
+
+
+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Asperiores harum quisquam nostrum veritatis, provident maiores deleniti vero earum sunt ex quam modi magni, sed hic atque rerum eveniet? Nam, voluptatum.

+
+
+ +
+
+ +
+ +
+ +
+ +
+
+ +
+ + +
+ + + + + \ No newline at end of file diff --git a/app/views/notices/show.html.erb b/app/views/notices/show.html.erb new file mode 100644 index 000000000..07bdf73f8 --- /dev/null +++ b/app/views/notices/show.html.erb @@ -0,0 +1,22 @@ +<%= content_for :usernav, true %> + +
+
+
+ <%= link_to "Delete Notice", notice_path(@notice), method: :delete, class: "edit-button btn btn-primary" %> +

Notice

+
+
+
+
+
+
+

<%= @notice.title %>

+ <% @notice.messages.each do |msg| %> +

<%= msg.html_safe %>

+ <% end %> +
+
+
+ +
diff --git a/app/views/posts/create.js.erb b/app/views/posts/create.js.erb new file mode 100644 index 000000000..c8d9563a6 --- /dev/null +++ b/app/views/posts/create.js.erb @@ -0,0 +1 @@ +DANEBOOK.Posts.insert("<%= j(render partial: 'shared/posts/post', locals: {post: @post, size: "small", limit: 4 }) %>"); diff --git a/app/views/posts/index.js.erb b/app/views/posts/index.js.erb new file mode 100644 index 000000000..798cbe8e3 --- /dev/null +++ b/app/views/posts/index.js.erb @@ -0,0 +1,16 @@ +<% if(@posts.length > 0) %> +(function(){ + var posts = [ + <% @posts.each do |post| %> + "<%= j(render partial: 'shared/posts/post', locals: {post: post, size: 'small', limit: 4 }) %>", + <% end %> + ]; + + for(var i = 0; i < posts.length; i++){ + DANEBOOK.Posts.append(posts[i]); + } + DANEBOOK.Posts.setLastIndex(<%= @posts[-1].id %>) +})() +<% else %> + DANEBOOK.Posts.setLastIndex("end") +<% end %> diff --git a/app/views/posts/new.html.erb b/app/views/posts/new.html.erb new file mode 100644 index 000000000..ff794bc9c --- /dev/null +++ b/app/views/posts/new.html.erb @@ -0,0 +1 @@ +<%= content_for :usernav, true %> diff --git a/app/views/posts/show.html.erb b/app/views/posts/show.html.erb new file mode 100644 index 000000000..30d49bca7 --- /dev/null +++ b/app/views/posts/show.html.erb @@ -0,0 +1,4 @@ +<%= content_for :usernav, true %> +
+ <%= render partial: 'shared/posts/post', locals: { user: @user, post: @post, size: "large", limit: nil} %> +
diff --git a/app/views/profile_mailer/welcome.html.erb b/app/views/profile_mailer/welcome.html.erb new file mode 100644 index 000000000..9f0d7bbb7 --- /dev/null +++ b/app/views/profile_mailer/welcome.html.erb @@ -0,0 +1,67 @@ + + Hi <%= @profile.first_name %>! + + + Thank you for your interest in Danebook, the number 1 nordic social network. To get the most from your new account, here's what to do: + + + The first item on your list should be to finish filling out your profile. + + + + + + +
1. <%= link_to "Log in", login_url %> to the Danebook
+ + + + + + + +
2. Click the Edit Profile Link
+ + + + + + + +
3. Fill out any additional info you want your friends to see, and add a slogan
+ + + + + + + +
4. Select a profile image to use with your account
+ + + + + + + +
5. Save!
+ + + + Once you've finished editing your profile, you'll want to find some friends. You can search for users to friend using the search bar at the top of any page. You can browse through all of Danebooks vikings by leaving the search bar blank. "enter" + + + Press the "enter" key on your keyboard while focused in the search bar to start your search. + + + Once you're done adding friends, make sure to add some posts so your friends can see what's on your mind! + + + That's it! you now have a solid start on your new social network. We hope you enjoy sharing with your fellow plunderers. + + + Cheers, + + + Sampson Crowley + diff --git a/app/views/profile_mailer/welcome.text.erb b/app/views/profile_mailer/welcome.text.erb new file mode 100644 index 000000000..e69de29bb diff --git a/app/views/profiles/edit.html.erb b/app/views/profiles/edit.html.erb new file mode 100644 index 000000000..1c39a9659 --- /dev/null +++ b/app/views/profiles/edit.html.erb @@ -0,0 +1,75 @@ +<%= content_for :usernav, true %> + +
+
+
+

About

+
+
+
+
+ <%= form_for([current_user, current_user.profile], html: { class: 'syncronousUpload' }) do |f| %> +
+ +
+ +

Basic Information

+
+
<%= f.label :birthday, "Birthday:" %>
+
+
+ <%= f.date_select :birthday, + { start_year: Date.current.year, + end_year: 100.years.ago.year, + prompt: { day: 'Birth Day', month: 'Birth Month', year: 'Birth Year' }, + }, + {class: 'form-control', required: "true"} %> +
+
+ +
<%= f.label :college, "College:" %>
+
<%= f.text_field :college, class: 'form-control' %>
+ +
<%= f.label :hometown, "Hometown:" %>
+
<%= f.text_field :hometown, class: 'form-control' %>
+ +
<%= f.label :current_home, "Currently Lives:" %>
+
<%= f.text_field :current_home, class: 'form-control' %>
+ +
+ +

Contact Information

+
+
Email:
+
<%= current_user.email %> <%= link_to "edit", edit_user_path(current_user) %>
+ +
<%= f.label :phone, "Telephone:" %>
+
<%= f.text_field :phone, class: 'form-control' %>
+
+ +
+ +
+ <%= f.fields_for :bio do |bf| %> +

<%= bf.label :slogan, "Words To Live By" %>

+ <%= bf.text_area :slogan, class: 'form-control', rows: 5 %> +

<%= bf.label :about, "Words To Live By" %>

+ <%= bf.text_area :about, class: 'form-control', rows: 9 %> + <% end %> +
+
+
+
+

Upload New Profile Image

+ <%= profile_upload_form(f) %> +
+
+
+
+ <%= f.submit "Save Changes", class: "btn btn-block btn-primary" %> +
+
+ <% end %> +
+
+
diff --git a/app/views/profiles/show.html.erb b/app/views/profiles/show.html.erb new file mode 100644 index 000000000..ad6fd6b7a --- /dev/null +++ b/app/views/profiles/show.html.erb @@ -0,0 +1,34 @@ +<%= content_for :usernav, true %> +
+
+
+ <% if @user == current_user %> + <%= link_to "Edit Profile", edit_user_profile_path(current_user), class: "edit-button btn btn-primary" %> + <% end %> +

About

+
+
+
+
+
+
+

Basic Information

+ <%= render partial: 'shared/info/basic', locals: {user: @user} %> + +

Contact Information

+ <%= render partial: 'shared/info/contact', locals: {user: @user} %> + +
+ +
+ <% if @user.profile.bio %> +

Words To Live By

+

<%= @user.profile.bio.slogan %>

+

About Me

+

<%= @user.profile.bio.about%>

+ <% end %> +
+
+
+
+
diff --git a/app/views/search/show.html.erb b/app/views/search/show.html.erb new file mode 100644 index 000000000..fbd84d3b0 --- /dev/null +++ b/app/views/search/show.html.erb @@ -0,0 +1,18 @@ +<%= content_for :usernav, true %> +
+
+
+

Search Results

+
+
+
+
+
+ <% @profiles.each do |profile| %> + <%= render partial: 'shared/friends/friend', locals: {user: profile.user} %> + <% end %> +
+
+
+ +
diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb new file mode 100644 index 000000000..9c77e7237 --- /dev/null +++ b/app/views/sessions/new.html.erb @@ -0,0 +1,8 @@ +
+

Login

+
+
+ <%= render partial: '/shared/login' %> +
+
+
diff --git a/app/views/shared/_flashes.html.erb b/app/views/shared/_flashes.html.erb new file mode 100644 index 000000000..7c2825b6f --- /dev/null +++ b/app/views/shared/_flashes.html.erb @@ -0,0 +1,9 @@ +
+ <% flash.each do |type, msg| %> +
+ <% msg.each do |m| %> +

<%= m %>

+ <% end %> +
+ <% end %> +
diff --git a/app/views/shared/_login.html.erb b/app/views/shared/_login.html.erb new file mode 100644 index 000000000..347eafd0e --- /dev/null +++ b/app/views/shared/_login.html.erb @@ -0,0 +1,26 @@ +<%= form_tag session_path do %> +
+
+
+ <%= label_tag :email %> + <%= text_field_tag :email, nil, class: 'form-control' %> +
+
+
+
+
+
+ <%= label_tag :password %> + <%= password_field_tag :password, nil, class: 'form-control' %> +
+
+
+
+
+
+ <%= submit_tag "Log In", class: "btn btn-success btn-block btn-large" %> +
+
+
+ +<% end %> diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb new file mode 100644 index 000000000..2cf097ebf --- /dev/null +++ b/app/views/shared/_navbar.html.erb @@ -0,0 +1,22 @@ + diff --git a/app/views/shared/_timeline_sidebar.html.erb b/app/views/shared/_timeline_sidebar.html.erb new file mode 100644 index 000000000..6d51ecceb --- /dev/null +++ b/app/views/shared/_timeline_sidebar.html.erb @@ -0,0 +1,48 @@ + +
+
+
+
+

About

+
+
+ +
+
+ <%= render partial: 'shared/info/basic', locals: { user: user } %> +
+
+ +
+
+ +
+
+
+
+

Photos

+
+
+ +
+ <%= render partial: 'shared/sidebar/photos', locals: { user: user } %> +
+ +
+
+ + +
+
+
+
+

Friends

+
+
+ +
+ <%= render partial: 'shared/sidebar/friends', locals: { user: user } %> +
+ +
+
diff --git a/app/views/shared/_user_navbar.html.erb b/app/views/shared/_user_navbar.html.erb new file mode 100644 index 000000000..d5447b8bb --- /dev/null +++ b/app/views/shared/_user_navbar.html.erb @@ -0,0 +1,28 @@ +
+
+
+ Cover Image +
+

<%= format_user_name(user) %>

+
+ profile +
+
+ + +
+
+
diff --git a/app/views/shared/flashes.js.erb b/app/views/shared/flashes.js.erb new file mode 100644 index 000000000..6b48be549 --- /dev/null +++ b/app/views/shared/flashes.js.erb @@ -0,0 +1,12 @@ +(function(){ + var target, type, msg = [ + <% @msg.each do |msg| %> + "<%= j(msg) %>", + <% end %> + ]; + <% if defined?(@target) %> + target = <%= @target %>; + type = "<%= @target_type %>"; + <% end %> + DANEBOOK.Flashes("<%= j(@status.to_s) %>", msg, target, type) +})() diff --git a/app/views/shared/forms/_img_edit_form.html.erb b/app/views/shared/forms/_img_edit_form.html.erb new file mode 100644 index 000000000..585e64a51 --- /dev/null +++ b/app/views/shared/forms/_img_edit_form.html.erb @@ -0,0 +1,18 @@ +
+

+ <%= image_tag form.object.picture.url(:thumb) %> +

+ +
+ <%= form.label :description, "Image Description" %>
+ <%= form.text_field :description, class: "form-control", placeholder: "Enter a description (optional)"%> +
+
+ <%= form.label "_destroy" do %> + <%= form.check_box '_destroy' %> Destroy? + <% end %> + <%= form.label :set_profile_photo do %> + <%= form.check_box :set_profile_photo %> Set as Profile? + <% end %> +
+
diff --git a/app/views/shared/forms/_img_upload_form.html.erb b/app/views/shared/forms/_img_upload_form.html.erb new file mode 100644 index 000000000..6261743e4 --- /dev/null +++ b/app/views/shared/forms/_img_upload_form.html.erb @@ -0,0 +1,18 @@ +
+ +
+ +
+

OR

+
+ <%= form.label :url, "Enter Url" %> + <%= form.text_field :url, class: 'form-control', placeholder: 'https://cdn.keycdn.com/img/cdn-fast.svg' %> +
+
+ <%= form.label :description, "Image Description" %>
+ <%= form.text_field :description, class: "form-control", placeholder: "Enter a description (optional)"%> +
+
diff --git a/app/views/shared/forms/_new_post.html.erb b/app/views/shared/forms/_new_post.html.erb new file mode 100644 index 000000000..c6a5062bb --- /dev/null +++ b/app/views/shared/forms/_new_post.html.erb @@ -0,0 +1,28 @@ +
+ +
+
+
+

Post

+
+
+ + <%= form_for [user, post], remote: true, html: {class: "form-horizontal"} do |f| %> +
+
+ <%= f.label :body, "Tell the world something", class: "sr-only" %> + <%= f.text_area :body, class: "form-control", rows: 3, placeholder: "Tell the world something..." %> +
+
+ +
+ +
+ <% end %> + +
+ +
diff --git a/app/views/shared/friends/_friend.html.erb b/app/views/shared/friends/_friend.html.erb new file mode 100644 index 000000000..adb28b650 --- /dev/null +++ b/app/views/shared/friends/_friend.html.erb @@ -0,0 +1,28 @@ +
+
+
+ +
+ <%= link_to user_path(user, user.profile) do %> + <%= image_tag render_profile_img(user.profile) %> + <% end %> +
+ + +
+ + <%= link_to format_user_name(user), user_path(user, user.profile) %> + +

432 Friends

+
+ <% if current_user.friend_ids.include?(user.id) %> + <%= link_to "Unfriend", user_friends_path(user), remote: true, method: :delete, class: "btn btn-default pull-right unfriend" %> + <% elsif current_user.id != user.id %> + <%= link_to "+Friend", user_friends_path(user), remote: true, method: :post, class: "btn btn-default pull-right friend" %> + <% end %> +
+
+ + +
+
diff --git a/app/views/shared/info/_basic.html.erb b/app/views/shared/info/_basic.html.erb new file mode 100644 index 000000000..38557b54a --- /dev/null +++ b/app/views/shared/info/_basic.html.erb @@ -0,0 +1,16 @@ +
+
Birthday:
+
<%= user.profile.birthday.to_s(:long) %>
+ <% if user.profile.college && !user.profile.college.empty? %> +
College:
+
<%= user.profile.college %>
+ <% end %> + <% if user.profile.hometown && !user.profile.hometown.empty? %> +
Hometown:
+
<%= user.profile.hometown %>
+ <% end %> + <% if user.profile.current_home && !user.profile.current_home.empty? %> +
Currently Lives:
+
<%= user.profile.current_home %>
+ <% end %> +
diff --git a/app/views/shared/info/_contact.html.erb b/app/views/shared/info/_contact.html.erb new file mode 100644 index 000000000..f9afc736b --- /dev/null +++ b/app/views/shared/info/_contact.html.erb @@ -0,0 +1,8 @@ +
+
Email:
+
<%= user.email %>
+ <% if user.profile.phone %> +
Telephone:
+
<%= user.profile.phone %>
+ <% end %> +
diff --git a/app/views/shared/navbar/_anon.html.erb b/app/views/shared/navbar/_anon.html.erb new file mode 100644 index 000000000..956d4fd56 --- /dev/null +++ b/app/views/shared/navbar/_anon.html.erb @@ -0,0 +1,36 @@ + diff --git a/app/views/shared/navbar/_search_bar.html.erb b/app/views/shared/navbar/_search_bar.html.erb new file mode 100644 index 000000000..985111ffb --- /dev/null +++ b/app/views/shared/navbar/_search_bar.html.erb @@ -0,0 +1,9 @@ +
+ +
diff --git a/app/views/shared/navbar/_user.html.erb b/app/views/shared/navbar/_user.html.erb new file mode 100644 index 000000000..4f85fc3f1 --- /dev/null +++ b/app/views/shared/navbar/_user.html.erb @@ -0,0 +1,18 @@ +<%= render partial: 'shared/navbar/search_bar' %> + + diff --git a/app/views/shared/notices/_notice.html.erb b/app/views/shared/notices/_notice.html.erb new file mode 100644 index 000000000..9b1f2b533 --- /dev/null +++ b/app/views/shared/notices/_notice.html.erb @@ -0,0 +1,7 @@ +
  • + <% if notice.viewed %> + <%= link_to notice.title, notice_path(notice) %> + <% else %> + <%= link_to notice.title, notice_path(notice), class: "new-notice" %> + <% end %> +
  • diff --git a/app/views/shared/notices/_notice_list.html.erb b/app/views/shared/notices/_notice_list.html.erb new file mode 100644 index 000000000..fb4b3419b --- /dev/null +++ b/app/views/shared/notices/_notice_list.html.erb @@ -0,0 +1,3 @@ +<% notices.each do |notice| %> + <%= render partial: 'shared/notices/notice', locals: {notice: notice} %> +<% end %> diff --git a/app/views/shared/photos/_photo.html.erb b/app/views/shared/photos/_photo.html.erb new file mode 100644 index 000000000..80bbdc3eb --- /dev/null +++ b/app/views/shared/photos/_photo.html.erb @@ -0,0 +1,6 @@ +
    + <%= render_gallery_image(photo) %> +
    + <%= photo.description %> +
    +
    diff --git a/app/views/shared/photos/_photo_mini.html.erb b/app/views/shared/photos/_photo_mini.html.erb new file mode 100644 index 000000000..f42b36fb8 --- /dev/null +++ b/app/views/shared/photos/_photo_mini.html.erb @@ -0,0 +1,5 @@ +
    + + <%= image_tag photo.picture.url(:medium) %> + +
    diff --git a/app/views/shared/posts/_comment.html.erb b/app/views/shared/posts/_comment.html.erb new file mode 100644 index 000000000..4801ee8c0 --- /dev/null +++ b/app/views/shared/posts/_comment.html.erb @@ -0,0 +1,23 @@ +
    +
    + +
    +
    +

    + <%= link_to format_user_name(comment.user), user_path(comment.user) %> + + <%= comment.created_at.strftime("Said on %A, %d/%m/%Y") %> + +

    +

    <%= comment.body %>

    +

    + <%= render_like(comment) %> + <% unless comment.likes.size == 0 %> + <%= comment.likes_count %> people like this + <% end %> + <% if comment.user == current_user %> + <%= link_to "Delete", user_post_path(comment.user, comment), method: :delete, class: "private pull-right" %> + <% end %> +

    +
    +
    diff --git a/app/views/shared/posts/_comments.html.erb b/app/views/shared/posts/_comments.html.erb new file mode 100644 index 000000000..261e5e943 --- /dev/null +++ b/app/views/shared/posts/_comments.html.erb @@ -0,0 +1,9 @@ +<% if(comments.length > 0) %> + +<% end %> diff --git a/app/views/shared/posts/_post.html.erb b/app/views/shared/posts/_post.html.erb new file mode 100644 index 000000000..d11a9fbbe --- /dev/null +++ b/app/views/shared/posts/_post.html.erb @@ -0,0 +1,60 @@ +
    +
    + +
    +
    + +
    +
    +
    + <%= link_to user_path(post.user) do %><% end %> +
    +
    + <%= link_to format_user_name(post.user), user_path(post.user) %> +

    <%= post.created_at.strftime("Posted on %A, %d/%m/%Y") %>

    +
    +
    +
    +
    +
    +

    <%= post.body.html_safe %>

    +
    +
    + +
    +
    + +
    + +
    + +
    +
    diff --git a/app/views/shared/sidebar/_friends.html.erb b/app/views/shared/sidebar/_friends.html.erb new file mode 100644 index 000000000..2543490f1 --- /dev/null +++ b/app/views/shared/sidebar/_friends.html.erb @@ -0,0 +1,19 @@ +
    +
    + <% user.friends.first(9).each_with_index do |friend, i| %> +
    + <%= link_to user_path(friend) do %> + <%= image_tag render_profile_img(friend.profile) %> +

    <%= format_user_name(friend) %>

    + <% end %> +
    + <% if i % 3 == 2 %> +
    + <% end %> + <% end %> + +
    +

    <%= link_to "See More Friends", user_friends_path(user) %>

    +
    +
    +
    diff --git a/app/views/shared/sidebar/_photos.html.erb b/app/views/shared/sidebar/_photos.html.erb new file mode 100644 index 000000000..3b298d973 --- /dev/null +++ b/app/views/shared/sidebar/_photos.html.erb @@ -0,0 +1,15 @@ +
    +
    + + <% user.images.each_with_index do |image, i| %> + <%= render partial: 'shared/photos/photo_mini', locals: {photo: image} %> + <% if i % 3 == 2 %> +
    + <% end %> + <% end %> + +
    +

    <%= link_to "See More Photos", user_galleries_path(user) %>

    +
    +
    +
    diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb new file mode 100644 index 000000000..c0f1727a4 --- /dev/null +++ b/app/views/users/edit.html.erb @@ -0,0 +1,101 @@ +<%= content_for :usernav, true %> +
    +

    Edit Account

    +
    +
    + <%= form_for @user do |f| %> + <%= f.fields_for :profile do |pf| %> +
    +
    +
    + <%= f.label :current_password, "Enter your current password to edit details" %> + <%= f.password_field :current_password, class: "form-control", placeholder: "Your Current Password", required: true %> +
    +
    +
    +
    + <%= pf.label :first_name, "First Name:", class:'sr-only' %> + <%= pf.text_field :first_name, class: "form-control", placeholder: "First Name" %> +
    +
    +
    +
    + <%= pf.label :last_name, "Last Name:", class:'sr-only' %> + <%= pf.text_field :last_name, class: "form-control", placeholder: "Last Name" %> +
    +
    +
    +
    +
    +
    + <%= f.label :email, "Your Email:", class:'sr-only' %> + <%= f.email_field :email, class: "form-control", placeholder: "Your Email" %> +
    +
    + <%= f.label :password, "Change Password:", class:'sr-only' %> + <%= f.password_field :password, class: "form-control", + placeholder: "Change Password", + pattern: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{12,}' + %> +
    +
    + <%= f.label :password_confirmation, "Confirm Your Password:", class:'sr-only' %> + <%= f.password_field :password_confirmation, class: "form-control", placeholder: "Confirm New Password" %> +
    +
    +
    +
    + <% Profile.genders.each do |g| %> +
    +
    + +
    +
    + <% end %> +
    + <% end %> +
    +
    +
    + <%= f.submit "Edit Account Details", class: "btn btn-success btn-block btn-large" %> +
    +
    +
    +
    +
    + <% end %> +
    +
    +
    + diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb new file mode 100644 index 000000000..ba8995078 --- /dev/null +++ b/app/views/users/index.html.erb @@ -0,0 +1,15 @@ +<%= content_for :usernav, true %> +
    + +
    + + +
    + <%= render_new_post(@user, @post) %> + <%#= render_posts(@posts) %> +
    + +
    +
    diff --git a/app/views/users/new.html.erb b/app/views/users/new.html.erb new file mode 100644 index 000000000..3d4fd76ad --- /dev/null +++ b/app/views/users/new.html.erb @@ -0,0 +1,126 @@ +
    +

    Connect with all your friends!

    + +
    +
    +

    Sign Up

    +
    +
    + + +
    +
    +
    +
    + <%= form_for @user, html: { class: 'cf-main-form' } do |f| %> + <%= f.fields_for :profile do |pf| %> +
    +
    +
    +
    +
    + <%= pf.label :first_name, "First Name:", class:'sr-only' %> + <%= pf.text_field :first_name, class: "form-control", placeholder: "First Name", required: "true", + "cf-questions" => "Please tell me your First Name." %> +
    +
    +
    +
    + <%= pf.label :last_name, "Last Name:", class:'sr-only' %> + <%= pf.text_field :last_name, class: "form-control", placeholder: "Last Name", required: "true", + "cf-questions" => "Thanks {previous-answer}! Now please tell me your Last Name." %> +
    +
    +
    +
    +
    +
    + <%= f.label :email, "Your Email:", class:'sr-only' %> + <%= f.email_field :email, class: "form-control", placeholder: "Your Email", required: "true", "cf-validation-email" => "", + "cf-questions" => "Thanks! What is the best email to reach you at?" %> +
    +
    + <%= f.label :password, "Your New Password:", class:'sr-only' %> + <%= f.password_field :password, class: "form-control", + placeholder: "Your New Password", + required: "true", + pattern: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{12,}', + :"cf-error" => "Min strength: 12 characters, 1 Uppercase, 1 Lowercase, 1 Digit", + "cf-questions" => "Now enter the password you would like to use for your new account. Don't worry, it won't be displayed and I can't see it." %> +
    +
    + <%= f.label :password_confirmation, "Confirm Your Password:", class:'sr-only' %> + <%= f.password_field :password_confirmation, class: "form-control", placeholder: "Confirm Your Password", required: "true", + "cf-questions" => "Please enter your password again to make sure you typed it correctly. If you need to change what you entered, click the \"Normal Form\" Button" %> +
    +
    +
    +
    + <%= pf.label :birthday, "Your Birthday:", class:'sr-only' %> + <%= pf.date_select :birthday, + { start_year: Date.current.year, + end_year: 100.years.ago.year, + prompt: { day: 'Birth Day', month: 'Birth Month', year: 'Birth Year' }, + }, + {class: 'form-control', required: "true", "cf-questions": "Please Select your Birthday:"} %> +
    +
    + <% Profile.genders.each do |g| %> +
    +
    + +
    +
    + <% end %> +
    + <% end %> +
    +
    +
    + <%= f.submit "Sign Up!", class: "btn btn-success btn-block btn-large" %> +
    +
    +
    +
    +
    + <% end %> +
    +
    +
    + diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 000000000..66e9889e8 --- /dev/null +++ b/bin/bundle @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +load Gem.bin_path('bundler', 'bundle') diff --git a/bin/delayed_job b/bin/delayed_job new file mode 100755 index 000000000..edf195985 --- /dev/null +++ b/bin/delayed_job @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby + +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) +require 'delayed/command' +Delayed::Command.new(ARGV).daemonize diff --git a/bin/rails b/bin/rails new file mode 100755 index 000000000..5badb2fde --- /dev/null +++ b/bin/rails @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/bin/rake b/bin/rake new file mode 100755 index 000000000..d87d5f578 --- /dev/null +++ b/bin/rake @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/bin/setup b/bin/setup new file mode 100755 index 000000000..e620b4dad --- /dev/null +++ b/bin/setup @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby +require 'pathname' +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a starting point to setup your application. + # Add necessary setup steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # puts "\n== Copying sample files ==" + # unless File.exist?('config/database.yml') + # cp 'config/database.yml.sample', 'config/database.yml' + # end + + puts "\n== Preparing database ==" + system! 'bin/rails db:setup' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/bin/spring b/bin/spring new file mode 100755 index 000000000..9bc076b9e --- /dev/null +++ b/bin/spring @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby + +# This file loads spring without using Bundler, in order to be fast. +# It gets overwritten when you run the `spring binstub` command. + +unless defined?(Spring) + require 'rubygems' + require 'bundler' + + lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) + if spring = lockfile.specs.detect { |spec| spec.name == "spring" } + Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path + gem 'spring', spring.version + require 'spring/binstub' + end +end diff --git a/bin/update b/bin/update new file mode 100755 index 000000000..a8e4462f2 --- /dev/null +++ b/bin/update @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +require 'pathname' +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a way to update your development environment automatically. + # Add necessary update steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + puts "\n== Updating database ==" + system! 'bin/rails db:migrate' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/config.ru b/config.ru new file mode 100644 index 000000000..f7ba0b527 --- /dev/null +++ b/config.ru @@ -0,0 +1,5 @@ +# This file is used by Rack-based servers to start the application. + +require_relative 'config/environment' + +run Rails.application diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 000000000..98b6bae70 --- /dev/null +++ b/config/application.rb @@ -0,0 +1,23 @@ +require_relative 'boot' + +require 'rails/all' + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module ProjectDanebook + class Application < Rails::Application + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + # Do not swallow errors in after_commit/after_rollback callbacks. + config.active_record.raise_in_transactional_callbacks = true + config.active_job.queue_adapter = :delayed_job + + config.after_initialize do + FixCountersJob.perform_later if Delayed::Job.all.none? {|job| !!(job.handler =~ /FixCountersJob/) } + end + + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 000000000..30f5120df --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,3 @@ +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'bundler/setup' # Set up gems listed in the Gemfile. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 000000000..0bbde6f74 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,9 @@ +development: + adapter: async + +test: + adapter: async + +production: + adapter: redis + url: redis://localhost:6379/1 diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 000000000..e7c6a08cf --- /dev/null +++ b/config/database.yml @@ -0,0 +1,85 @@ +# PostgreSQL. Versions 9.1 and up are supported. +# +# Install the pg driver: +# gem install pg +# On OS X with Homebrew: +# gem install pg -- --with-pg-config=/usr/local/bin/pg_config +# On OS X with MacPorts: +# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config +# On Windows: +# gem install pg +# Choose the win32 build. +# Install PostgreSQL and put its /bin directory on your path. +# +# Configure Using Gemfile +# gem 'pg' +# +default: &default + adapter: postgresql + encoding: unicode + # For details on connection pooling, see rails configuration guide + # http://guides.rubyonrails.org/configuring.html#database-pooling + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + +development: + <<: *default + database: project_danebook_development + + # The specified database role being used to connect to postgres. + # To create additional roles in postgres see `$ createuser --help`. + # When left blank, postgres will use the default role. This is + # the same name as the operating system user that initialized the database. + #username: project_danebook + + # The password associated with the postgres role (username). + #password: + + # Connect on a TCP socket. Omitted by default since the client uses a + # domain socket that doesn't need configuration. Windows does not have + # domain sockets, so uncomment these lines. + #host: localhost + + # The TCP port the server listens on. Defaults to 5432. + # If your server runs on a different port number, change accordingly. + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # log, notice, warning, error, fatal, and panic + # Defaults to warning. + #min_messages: notice + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: project_danebook_test + +# As with config/secrets.yml, you never want to store sensitive information, +# like your database password, in your source code. If your source code is +# ever seen by anyone, they now have access to your database. +# +# Instead, provide the password as a unix environment variable when you boot +# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database +# for a full rundown on how to provide these environment variables in a +# production deployment. +# +# On Heroku and other platform providers, you may have a full connection URL +# available as an environment variable. For example: +# +# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" +# +# You can use this database configuration with: +# +# production: +# url: <%= ENV['DATABASE_URL'] %> +# +production: + <<: *default + database: project_danebook_production + username: project_danebook + password: <%= ENV['PROJECT_DANEBOOK_DATABASE_PASSWORD'] %> diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 000000000..426333bb4 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative 'application' + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 000000000..9c801f95b --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,92 @@ +Rails.configuration.aws_images = true + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + if Rails.root.join('tmp/caching-dev.txt').exist? + config.action_controller.perform_caching = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => 'public, max-age=172800' + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + config.action_mailer.default_url_options = { :host => 'localhost:3000' } + config.action_mailer.delivery_method = :letter_opener + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. + config.assets.debug = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + config.file_watcher = ActiveSupport::EventedFileUpdateChecker + + config.after_initialize do + Bullet.enable = true + Bullet.alert = false + Bullet.bullet_logger = true + Bullet.console = true + Bullet.rails_logger = false + Bullet.add_footer = true + end + + + config.paperclip_defaults = { + + # Don't forget to make S3 your storage option! + storage: :s3, + + s3_region: ENV['AWS_REGION'], + + s3_credentials: { + + # # put your host name here if needed + # # see the reading below for more details + # # NOTE: This must be the correct region for YOU + :s3_host_name => "s3-#{Rails.application.secrets.aws_region}.amazonaws.com", + + # NOTE: these lines are changed to use secrets.yml + # from the examples (which use ENV vars instead) + bucket: Rails.application.secrets.s3_bucket_name, + access_key_id: Rails.application.secrets.aws_access_key_id, + secret_access_key: Rails.application.secrets.aws_secret_access_key + } + } + +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 000000000..670728b10 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,126 @@ +Rails.configuration.aws_images = ENV["AWS_ENABLE"] == "enable" +Rails.application.configure do + config.force_ssl = true + + config.action_mailer.smtp_settings = { + :address => 'smtp.sendgrid.net', + :port => '587', + :authentication => :plain, + :user_name => ENV['SENDGRID_USERNAME'], + :password => ENV['SENDGRID_PASSWORD'], + :domain => 'heroku.com', + :enable_starttls_auto => true + } + config.action_mailer.delivery_method ||= :smtp + + # Rails also needs to know where your app is + # located to properly configure sending of emails + config.action_mailer.default_url_options = { + :host => 'https://danebook-sampson-crowley.herokuapp.com', + } + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? + + # Compress JavaScripts and CSS. + config.assets.js_compressor = :uglifier + # config.assets.css_compressor = :sass + + # Do not fallback to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = 'http://assets.example.com' + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + + # Mount Action Cable outside main process or domain + # config.action_cable.mount_path = nil + # config.action_cable.url = 'wss://example.com/cable' + # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Use the lowest log level to ensure availability of diagnostic information + # when problems arise. + config.log_level = :debug + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment) + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "project_danebook_#{Rails.env}" + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners. + config.active_support.deprecation = :notify + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require 'syslog/logger' + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + config.paperclip_defaults = { + + # Don't forget to make S3 your storage option! + storage: :s3, + + s3_credentials: { + + # # put your host name here if needed + # # see the reading below for more details + # # NOTE: This must be the correct region for YOU + :s3_host_name => "s3-#{Rails.application.secrets.aws_region}.amazonaws.com", + + # NOTE: these lines are changed to use secrets.yml + # from the examples (which use ENV vars instead) + s3_region: Rails.application.secrets.aws_region, + bucket: Rails.application.secrets.s3_bucket_name, + access_key_id: Rails.application.secrets.aws_access_key_id, + secret_access_key: Rails.application.secrets.aws_secret_access_key + } + } +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 000000000..d5c39235b --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,44 @@ +Rails.configuration.aws_images = false + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true + + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => 'public, max-age=3600' + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true +end diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb new file mode 100644 index 000000000..51639b67a --- /dev/null +++ b/config/initializers/application_controller_renderer.rb @@ -0,0 +1,6 @@ +# Be sure to restart your server when you modify this file. + +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 000000000..01ef3e663 --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,11 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Add additional assets to the asset load path +# Rails.application.config.assets.paths << Emoji.images_path + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. +# Rails.application.config.assets.precompile += %w( search.js ) diff --git a/config/initializers/aws_sdk.rb b/config/initializers/aws_sdk.rb new file mode 100644 index 000000000..6d1d15eff --- /dev/null +++ b/config/initializers/aws_sdk.rb @@ -0,0 +1,15 @@ +unless Rails.env.production? + AWS_Config = YAML.load_file("config/aws.yml")[Rails.env] + ENV['AWS_BUCKET_NAME'] = AWS_Config['bucket'] + ENV['AWS_REGION'] = AWS_Config['region'] + ENV['AWS_ACCESS_KEY_ID'] = AWS_Config['access_key_id'] + ENV['AWS_SECRET_ACCESS_KEY'] = AWS_Config['secret_access_key'] +end + +Aws.config.update({ + region: ENV['AWS_REGION'], + credentials: Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY']), + }) + + +S3_BUCKET = Aws::S3::Resource.new.bucket(ENV['AWS_BUCKET_NAME']) diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb new file mode 100644 index 000000000..59385cdf3 --- /dev/null +++ b/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers! diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb new file mode 100644 index 000000000..5a6a32d37 --- /dev/null +++ b/config/initializers/cookies_serializer.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/config/initializers/delayed_job_config.rb b/config/initializers/delayed_job_config.rb new file mode 100644 index 000000000..a6f45ef56 --- /dev/null +++ b/config/initializers/delayed_job_config.rb @@ -0,0 +1,10 @@ +Rails.logger.level = Logger::INFO +Delayed::Worker.destroy_failed_jobs = true +Delayed::Worker.sleep_delay = 1 +Delayed::Worker.max_attempts = 3 +Delayed::Worker.max_run_time = 5.minutes +Delayed::Worker.read_ahead = 10 +Delayed::Worker.default_queue_name = 'default' +Delayed::Worker.delay_jobs = !Rails.env.test? +Delayed::Worker.raise_signal_exceptions = :term +Delayed::Worker.logger = Logger.new(File.join(Rails.root, 'log', 'delayed_job.log')) diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 000000000..4a994e1e7 --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [:password] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 000000000..ac033bf9d --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym 'RESTful' +# end diff --git a/config/initializers/jazz_fingers.rb b/config/initializers/jazz_fingers.rb new file mode 100644 index 000000000..399b15033 --- /dev/null +++ b/config/initializers/jazz_fingers.rb @@ -0,0 +1,9 @@ +if defined?(JazzFingers) + JazzFingers.configure do |config| + config.colored_prompt = true + config.awesome_print = true + config.coolline = false + end + + require 'jazz_fingers/setup' +end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb new file mode 100644 index 000000000..dc1899682 --- /dev/null +++ b/config/initializers/mime_types.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb new file mode 100644 index 000000000..0706cafd4 --- /dev/null +++ b/config/initializers/new_framework_defaults.rb @@ -0,0 +1,24 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains migration options to ease your Rails 5.0 upgrade. +# +# Read the Rails 5.0 release notes for more info on each option. + +# Enable per-form CSRF tokens. Previous versions had false. +Rails.application.config.action_controller.per_form_csrf_tokens = true + +# Enable origin-checking CSRF mitigation. Previous versions had false. +Rails.application.config.action_controller.forgery_protection_origin_check = true + +# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. +# Previous versions had false. +ActiveSupport.to_time_preserves_timezone = true + +# Require `belongs_to` associations by default. Previous versions had false. +Rails.application.config.active_record.belongs_to_required_by_default = true + +# Do not halt callback chains when a callback returns false. Previous versions had true. +ActiveSupport.halt_callback_chains_on_return_false = false + +# Configure SSL options to enable HSTS with subdomains. Previous versions had false. +Rails.application.config.ssl_options = { hsts: { subdomains: true } } diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb new file mode 100644 index 000000000..f2d118131 --- /dev/null +++ b/config/initializers/session_store.rb @@ -0,0 +1,3 @@ +# Be sure to restart your server when you modify this file. + +Rails.application.config.session_store :cookie_store, key: '_project_danebook_session' diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb new file mode 100644 index 000000000..bbfc3961b --- /dev/null +++ b/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] +end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 000000000..065395716 --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,23 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more, please read the Rails Internationalization guide +# available at http://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 000000000..c7f311f81 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,47 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum, this matches the default thread size of Active Record. +# +threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests, default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked webserver processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. If you use this option +# you need to make sure to reconnect any threads in the `on_worker_boot` +# block. +# +# preload_app! + +# The code in the `on_worker_boot` will be called if you are using +# clustered mode by specifying a number of `workers`. After each worker +# process is booted this block will be run, if you are using `preload_app!` +# option you will want to use this block to reconnect to any threads +# or connections that may have been created at application boot, Ruby +# cannot share connections between processes. +# +# on_worker_boot do +# ActiveRecord::Base.establish_connection if defined?(ActiveRecord) +# end + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 000000000..85702a19a --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,21 @@ +Rails.application.routes.draw do + # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html + root to: 'users#new' + resource :session + resources :notices + resources :users do + resource :profile + resources :galleries + resource :friends, only: [:show, :create, :destroy] + resources :posts do + resource :comment, only: [:create, :new] + end + end + resources :likes, only: [:update, :destroy] + + get '/search' => 'search#show' + get '/signup' => 'users#new' + get '/logout' => 'sessions#destroy' + get '/login' => 'sessions#new' + match via: [:get, :post], "*path" => redirect("/") +end diff --git a/config/secrets.yml b/config/secrets.yml new file mode 100644 index 000000000..c49229fbd --- /dev/null +++ b/config/secrets.yml @@ -0,0 +1,32 @@ +# Be sure to restart your server when you modify this file. + +# Your secret key is used for verifying the integrity of signed cookies. +# If you change this key, all old signed cookies will become invalid! + +# Make sure the secret is at least 30 characters and all random, +# no regular words or you'll be exposed to dictionary attacks. +# You can use `rails secret` to generate a secure secret key. + +# Make sure the secrets in this file are kept private +# if you're sharing your code publicly. + +development: + secret_key_base: 2cca00c02db4d5f66ac00c16bedb1b590086df57ab19f1562085c53ccd136fa3cd3fc7db7200cc7506cba2c0c0f6813894918397d5d87fdd8cc10625b6dded78 + s3_bucket_name: <%= ENV["S3_BUCKET_NAME"] %> + aws_region: <%= ENV['AWS_REGION'] %> + aws_access_key_id: <%= ENV["AWS_ACCESS_KEY_ID"] %> + aws_secret_access_key: <%= ENV["AWS_SECRET_ACCESS_KEY"] %> + +test: + secret_key_base: 5f3fd4a2322dbfe790f95cba98d8100e76d646f16fbc1019026895142ad7916dde92ced039b45d5185b645ccdf3c7f3732fa8a80fb7a570e33b12fc92fba3bea + +# Do not keep production secrets in the repository, +# instead read values from the environment. +production: + secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> + s3_bucket_name: <%= ENV["S3_BUCKET_NAME"] %> + aws_region: <%= ENV['AWS_REGION'] %> + aws_access_key_id: <%= ENV["AWS_ACCESS_KEY_ID"] %> + aws_secret_access_key: <%= ENV["AWS_SECRET_ACCESS_KEY"] %> + sendgrid_username: <%= ENV["SENDGRID_USERNAME"] %> + sendgrid_password: <%= ENV["SENDGRID_PASSWORD"] %> diff --git a/config/spring.rb b/config/spring.rb new file mode 100644 index 000000000..c9119b40c --- /dev/null +++ b/config/spring.rb @@ -0,0 +1,6 @@ +%w( + .ruby-version + .rbenv-vars + tmp/restart.txt + tmp/caching-dev.txt +).each { |path| Spring.watch(path) } diff --git a/db/migrate/20161207193754_create_users.rb b/db/migrate/20161207193754_create_users.rb new file mode 100644 index 000000000..d501dcad6 --- /dev/null +++ b/db/migrate/20161207193754_create_users.rb @@ -0,0 +1,13 @@ +class CreateUsers < ActiveRecord::Migration[5.0] + def change + create_table :users do |t| + t.string :email, unique:true, null: false + t.string :password_digest, null: false + t.string :token + t.integer :failed + t.datetime :last_attempt + t.timestamps + end + add_index :users, :email, :unique => true + end +end diff --git a/db/migrate/20161207193850_create_profiles.rb b/db/migrate/20161207193850_create_profiles.rb new file mode 100644 index 000000000..de3420933 --- /dev/null +++ b/db/migrate/20161207193850_create_profiles.rb @@ -0,0 +1,20 @@ +class CreateProfiles < ActiveRecord::Migration[5.0] + def change + create_table :profiles do |t| + t.references :user + t.string :first_name + t.string :last_name + t.date :birthday + t.string :gender + t.string :college + t.string :hometown + t.string :current_home + t.string :phone + t.integer :image_id + t.integer :cover_id + t.boolean :edited, default: false + + t.timestamps + end + end +end diff --git a/db/migrate/20161209170807_create_posts.rb b/db/migrate/20161209170807_create_posts.rb new file mode 100644 index 000000000..38fe03e53 --- /dev/null +++ b/db/migrate/20161209170807_create_posts.rb @@ -0,0 +1,14 @@ +class CreatePosts < ActiveRecord::Migration[5.0] + def change + create_table :posts do |t| + t.references :user, foreign_key: true + t.string :post_type, default: "Post" + t.integer :post_id + t.text :body + t.integer :likes_count, default: 0 + t.integer :comments_count, default: 0 + + t.timestamps + end + end +end diff --git a/db/migrate/20161209195940_create_bios.rb b/db/migrate/20161209195940_create_bios.rb new file mode 100644 index 000000000..c868fa8cb --- /dev/null +++ b/db/migrate/20161209195940_create_bios.rb @@ -0,0 +1,11 @@ +class CreateBios < ActiveRecord::Migration[5.0] + def change + create_table :bios do |t| + t.references :profile + t.string :slogan + t.text :about + + t.timestamps + end + end +end diff --git a/db/migrate/20161209221424_create_likes.rb b/db/migrate/20161209221424_create_likes.rb new file mode 100644 index 000000000..2884b210f --- /dev/null +++ b/db/migrate/20161209221424_create_likes.rb @@ -0,0 +1,11 @@ +class CreateLikes < ActiveRecord::Migration[5.0] + def change + create_table :likes do |t| + t.references :user, foreign_key: true + t.references :post, foreign_key: true + + t.timestamps + end + add_index :likes, [:post_id, :user_id], :unique => true + end +end diff --git a/db/migrate/20161209235756_create_friends_users.rb b/db/migrate/20161209235756_create_friends_users.rb new file mode 100644 index 000000000..68543f19b --- /dev/null +++ b/db/migrate/20161209235756_create_friends_users.rb @@ -0,0 +1,10 @@ +class CreateFriendsUsers < ActiveRecord::Migration[5.0] + def change + create_table :friends_users do |t| + t.references :user, foreign_key: true + t.references :friend, references: :users + + t.timestamps + end + end +end diff --git a/db/migrate/20161210212836_create_galleries.rb b/db/migrate/20161210212836_create_galleries.rb new file mode 100644 index 000000000..ccb76cf34 --- /dev/null +++ b/db/migrate/20161210212836_create_galleries.rb @@ -0,0 +1,11 @@ +class CreateGalleries < ActiveRecord::Migration[5.0] + def change + create_table :galleries do |t| + t.references :user, foreign_key: true + t.string :title + t.string :description + + t.timestamps + end + end +end diff --git a/db/migrate/20161210212954_create_images.rb b/db/migrate/20161210212954_create_images.rb new file mode 100644 index 000000000..8b6ece504 --- /dev/null +++ b/db/migrate/20161210212954_create_images.rb @@ -0,0 +1,11 @@ +class CreateImages < ActiveRecord::Migration[5.0] + def change + create_table :images do |t| + t.references :gallery, foreign_key: true + t.string :url + t.string :description + + t.timestamps + end + end +end diff --git a/db/migrate/20161211173051_create_friend_requests.rb b/db/migrate/20161211173051_create_friend_requests.rb new file mode 100644 index 000000000..74d672cda --- /dev/null +++ b/db/migrate/20161211173051_create_friend_requests.rb @@ -0,0 +1,10 @@ +class CreateFriendRequests < ActiveRecord::Migration[5.0] + def change + create_table :friend_requests do |t| + t.references :user, foreign_key: true + t.integer :request_id + + t.timestamps + end + end +end diff --git a/db/migrate/20161215233239_add_picture_to_images.rb b/db/migrate/20161215233239_add_picture_to_images.rb new file mode 100644 index 000000000..c761bfc47 --- /dev/null +++ b/db/migrate/20161215233239_add_picture_to_images.rb @@ -0,0 +1,5 @@ +class AddPictureToImages < ActiveRecord::Migration[5.0] + def change + add_attachment :images, :picture + end +end diff --git a/db/migrate/20161216035452_create_delayed_jobs.rb b/db/migrate/20161216035452_create_delayed_jobs.rb new file mode 100644 index 000000000..27fdcf6cc --- /dev/null +++ b/db/migrate/20161216035452_create_delayed_jobs.rb @@ -0,0 +1,22 @@ +class CreateDelayedJobs < ActiveRecord::Migration + def self.up + create_table :delayed_jobs, force: true do |table| + table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue + table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually. + table.text :handler, null: false # YAML-encoded string of the object that will do work + table.text :last_error # reason for last failure (See Note below) + table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. + table.datetime :locked_at # Set when a client is working on this object + table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) + table.string :locked_by # Who is working on this object (if locked) + table.string :queue # The name of the queue this job is in + table.timestamps null: true + end + + add_index :delayed_jobs, [:priority, :run_at], name: "delayed_jobs_priority" + end + + def self.down + drop_table :delayed_jobs + end +end diff --git a/db/migrate/20161216045136_create_notices.rb b/db/migrate/20161216045136_create_notices.rb new file mode 100644 index 000000000..e857ac44c --- /dev/null +++ b/db/migrate/20161216045136_create_notices.rb @@ -0,0 +1,12 @@ +class CreateNotices < ActiveRecord::Migration[5.0] + def change + create_table :notices do |t| + t.references :user, foreign_key: true + t.boolean :viewed, default: false + t.string :title + t.string :messages, array: true, default: [] + + t.timestamps + end + end +end diff --git a/db/migrate/20161216214852_add_notice_counter_to_users.rb b/db/migrate/20161216214852_add_notice_counter_to_users.rb new file mode 100644 index 000000000..6eba417b4 --- /dev/null +++ b/db/migrate/20161216214852_add_notice_counter_to_users.rb @@ -0,0 +1,5 @@ +class AddNoticeCounterToUsers < ActiveRecord::Migration[5.0] + def change + add_column :users, :notice_count, :integer, default: 0 + end +end diff --git a/db/migrate/20161217172449_add_post_to_images.rb b/db/migrate/20161217172449_add_post_to_images.rb new file mode 100644 index 000000000..52fd93308 --- /dev/null +++ b/db/migrate/20161217172449_add_post_to_images.rb @@ -0,0 +1,5 @@ +class AddPostToImages < ActiveRecord::Migration[5.0] + def change + add_reference :images, :post, foreign_key: true + end +end diff --git a/db/migrate/20161217191858_add_picture_processing_to_image.rb b/db/migrate/20161217191858_add_picture_processing_to_image.rb new file mode 100644 index 000000000..d4dd66237 --- /dev/null +++ b/db/migrate/20161217191858_add_picture_processing_to_image.rb @@ -0,0 +1,9 @@ +class AddPictureProcessingToImage < ActiveRecord::Migration[5.0] + def self.up + add_column :images, :picture_processing, :boolean + end + + def self.down + remove_column :images, :picture_processing + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 000000000..6baa0b502 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,155 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 20161217191858) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "bios", force: :cascade do |t| + t.integer "profile_id" + t.string "slogan" + t.text "about" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["profile_id"], name: "index_bios_on_profile_id", using: :btree + end + + create_table "delayed_jobs", force: :cascade do |t| + t.integer "priority", default: 0, null: false + t.integer "attempts", default: 0, null: false + t.text "handler", null: false + t.text "last_error" + t.datetime "run_at" + t.datetime "locked_at" + t.datetime "failed_at" + t.string "locked_by" + t.string "queue" + t.datetime "created_at" + t.datetime "updated_at" + t.index ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree + end + + create_table "friend_requests", force: :cascade do |t| + t.integer "user_id" + t.integer "request_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_friend_requests_on_user_id", using: :btree + end + + create_table "friends_users", force: :cascade do |t| + t.integer "user_id" + t.integer "friend_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["friend_id"], name: "index_friends_users_on_friend_id", using: :btree + t.index ["user_id"], name: "index_friends_users_on_user_id", using: :btree + end + + create_table "galleries", force: :cascade do |t| + t.integer "user_id" + t.string "title" + t.string "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_galleries_on_user_id", using: :btree + end + + create_table "images", force: :cascade do |t| + t.integer "gallery_id" + t.string "url" + t.string "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "picture_file_name" + t.string "picture_content_type" + t.integer "picture_file_size" + t.datetime "picture_updated_at" + t.integer "post_id" + t.boolean "picture_processing" + t.index ["gallery_id"], name: "index_images_on_gallery_id", using: :btree + t.index ["post_id"], name: "index_images_on_post_id", using: :btree + end + + create_table "likes", force: :cascade do |t| + t.integer "user_id" + t.integer "post_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["post_id", "user_id"], name: "index_likes_on_post_id_and_user_id", unique: true, using: :btree + t.index ["post_id"], name: "index_likes_on_post_id", using: :btree + t.index ["user_id"], name: "index_likes_on_user_id", using: :btree + end + + create_table "notices", force: :cascade do |t| + t.integer "user_id" + t.boolean "viewed", default: false + t.string "title", null: false + t.string "messages", default: [], array: true + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_notices_on_user_id", using: :btree + end + + create_table "posts", force: :cascade do |t| + t.integer "user_id" + t.string "post_type", default: "Post" + t.integer "post_id" + t.text "body" + t.integer "likes_count", default: 0 + t.integer "comments_count", default: 0 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_posts_on_user_id", using: :btree + end + + create_table "profiles", force: :cascade do |t| + t.integer "user_id" + t.string "first_name" + t.string "last_name" + t.date "birthday" + t.string "gender" + t.string "college" + t.string "hometown" + t.string "current_home" + t.string "phone" + t.integer "image_id" + t.integer "cover_id" + t.boolean "edited", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_profiles_on_user_id", using: :btree + end + + create_table "users", force: :cascade do |t| + t.string "email", null: false + t.string "password_digest", null: false + t.string "token" + t.integer "failed" + t.datetime "last_attempt" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "notice_count", default: 0 + t.index ["email"], name: "index_users_on_email", unique: true, using: :btree + end + + add_foreign_key "friend_requests", "users" + add_foreign_key "friends_users", "users" + add_foreign_key "galleries", "users" + add_foreign_key "images", "galleries" + add_foreign_key "images", "posts" + add_foreign_key "likes", "posts" + add_foreign_key "likes", "users" + add_foreign_key "notices", "users" + add_foreign_key "posts", "users" +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 000000000..87cca2848 --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,105 @@ +# This file should contain all the record creation needed to seed the database with its default values. +# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). +# +# Examples: +# +# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) +# Character.create(name: 'Luke', movie: movies.first) +p "Destroying Users" + +User.destroy_all + +genders = {0 => "Male", 1 => "Female", 2 => "Other"} + +p "Creating Users" +def randomize? + rand(9) > 4 +end +20.times do |i| + hometown = randomize? ? nil : Faker::Hipster.word + p User.create( + email: "foo#{i}@bar.com", + password: "1234Qwerasdfzxcv", + password_confirmation: "1234Qwerasdfzxcv", + profile_attributes: + { + first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + birthday: rand(10000).days.ago.to_date, + gender: genders[rand(3)], + college: randomize? ? nil : Faker::University.name, + hometown: hometown, + current_home: randomize? ? hometown : Faker::Hipster.word, + phone: randomize? ? hometown : Faker::Hipster.word + } + + ) +end + +user_ids = User.pluck(:id) + +p "creating posts" +user_ids.each do |u_id| + rand(20).times do + Post.create( + user_id: u_id, + post_type: "Post", + body: randomize? ? Faker::Hipster.paragraph(2, false, 4) : Faker::Hacker.say_something_smart, + likes_count: 0, + comments_count: 0, + ) + end +end + +image_urls = ["https://s-media-cache-ak0.pinimg.com/736x/9b/79/52/9b795278d51497222d70722e3ab110ca.jpg", + "https://s-media-cache-ak0.pinimg.com/736x/01/0b/68/010b68214bf1eeb91060732aa58bed1e.jpg", + "http://www.funny-meme-pictures.com/wp-content/uploads/2013/11/11162013-funny-memes-105.jpg", + "https://s-media-cache-ak0.pinimg.com/originals/77/92/00/779200532083a9b899047e361e055658.jpg", + "http://i0.wp.com/pictures.jokofy.com/wp/wp-content/uploads/2015/12/Cute-and-funny-fat-child-meme.jpg?fit=600%2C384", + "http://www.dumpaday.com/wp-content/uploads/2016/04/funny-25.png" + + ] + +p "creating images and friends" +user_ids.each do |u_id| + rand(3).times do + gal = Gallery.create( + user_id: u_id, + title: Faker::Hipster.words(rand(1..5)).join(" "), + description: Faker::Hacker.say_something_smart + ) + image_urls.each do |url| + Image.create( + gallery_id: gal.id, + url: url, + description: Faker::Hacker.say_something_smart, + ) + end + img = gal.images.sample + img.set_profile_photo = "1" + end + 10.times do + Friendify.friendship(User.find_by(id: u_id), User.find_by(id: user_ids.sample)) + end +end + +p "SLEEPING UNTIL ALL JOBS COMPLETE" +sleep(1) until Delayed::Job.count < 2 + +post_ids = Post.pluck(:id) + +p "creating comments" +Post.all.each do |post| + if post.user.friends && post.user.friends.count > 0 + rand(2..10).times do + p Post.create( + user_id: post.user.friend_ids.sample, + post_id: post.id, + post_type: "Comment", + body: randomize? ? Faker::Hipster.paragraph(2, false, 4) : Faker::Hacker.say_something_smart, + likes_count: 0, + comments_count: 0, + ) + end + end +end diff --git a/jquery.fileupload.js b/jquery.fileupload.js new file mode 100644 index 000000000..4d41f1e95 --- /dev/null +++ b/jquery.fileupload.js @@ -0,0 +1,1482 @@ +/* + * jQuery File Upload Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window, document, location, Blob, FormData */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'jquery-ui/widget' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('./vendor/jquery.ui.widget') + ); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + // Detect file input support, based on + // http://viljamis.com/blog/2012/file-upload-support-on-mobile/ + $.support.fileInput = !(new RegExp( + // Handle devices which give false positives for the feature detection: + '(Android (1\\.[0156]|2\\.[01]))' + + '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' + + '|(w(eb)?OSBrowser)|(webOS)' + + '|(Kindle/(1\\.0|2\\.[05]|3\\.0))' + ).test(window.navigator.userAgent) || + // Feature detection for all other devices: + $('').prop('disabled')); + + // The FileReader API is not actually used, but works as feature detection, + // as some Safari versions (5?) support XHR file uploads via the FormData API, + // but not non-multipart XHR file uploads. + // window.XMLHttpRequestUpload is not available on IE10, so we check for + // window.ProgressEvent instead to detect XHR2 file upload capability: + $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader); + $.support.xhrFormDataFileUpload = !!window.FormData; + + // Detect support for Blob slicing (required for chunked uploads): + $.support.blobSlice = window.Blob && (Blob.prototype.slice || + Blob.prototype.webkitSlice || Blob.prototype.mozSlice); + + // Helper function to create drag handlers for dragover/dragenter/dragleave: + function getDragHandler(type) { + var isDragOver = type === 'dragover'; + return function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var dataTransfer = e.dataTransfer; + if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 && + this._trigger( + type, + $.Event(type, {delegatedEvent: e}) + ) !== false) { + e.preventDefault(); + if (isDragOver) { + dataTransfer.dropEffect = 'copy'; + } + } + }; + } + + // The fileupload widget listens for change events on file input fields defined + // via fileInput setting and paste or drop events of the given dropZone. + // In addition to the default jQuery Widget methods, the fileupload widget + // exposes the "add" and "send" methods, to add or directly send files using + // the fileupload API. + // By default, files added via file input selection, paste, drag & drop or + // "add" method are uploaded immediately, but it is possible to override + // the "add" callback option to queue file uploads. + $.widget('blueimp.fileupload', { + + options: { + // The drop target element(s), by the default the complete document. + // Set to null to disable drag & drop support: + dropZone: $(document), + // The paste target element(s), by the default undefined. + // Set to a DOM node or jQuery object to enable file pasting: + pasteZone: undefined, + // The file input field(s), that are listened to for change events. + // If undefined, it is set to the file input fields inside + // of the widget element on plugin initialization. + // Set to null to disable the change listener. + fileInput: undefined, + // By default, the file input field is replaced with a clone after + // each input field change event. This is required for iframe transport + // queues and allows change events to be fired for the same file + // selection, but can be disabled by setting the following option to false: + replaceFileInput: true, + // The parameter name for the file form data (the request argument name). + // If undefined or empty, the name property of the file input field is + // used, or "files[]" if the file input name property is also empty, + // can be a string or an array of strings: + paramName: undefined, + // By default, each file of a selection is uploaded using an individual + // request for XHR type uploads. Set to false to upload file + // selections in one request each: + singleFileUploads: true, + // To limit the number of files uploaded with one XHR request, + // set the following option to an integer greater than 0: + limitMultiFileUploads: undefined, + // The following option limits the number of files uploaded with one + // XHR request to keep the request size under or equal to the defined + // limit in bytes: + limitMultiFileUploadSize: undefined, + // Multipart file uploads add a number of bytes to each uploaded file, + // therefore the following option adds an overhead for each file used + // in the limitMultiFileUploadSize configuration: + limitMultiFileUploadSizeOverhead: 512, + // Set the following option to true to issue all file upload requests + // in a sequential order: + sequentialUploads: false, + // To limit the number of concurrent uploads, + // set the following option to an integer greater than 0: + limitConcurrentUploads: undefined, + // Set the following option to true to force iframe transport uploads: + forceIframeTransport: false, + // Set the following option to the location of a redirect url on the + // origin server, for cross-domain iframe transport uploads: + redirect: undefined, + // The parameter name for the redirect url, sent as part of the form + // data and set to 'redirect' if this option is empty: + redirectParamName: undefined, + // Set the following option to the location of a postMessage window, + // to enable postMessage transport uploads: + postMessage: undefined, + // By default, XHR file uploads are sent as multipart/form-data. + // The iframe transport is always using multipart/form-data. + // Set to false to enable non-multipart XHR uploads: + multipart: true, + // To upload large files in smaller chunks, set the following option + // to a preferred maximum chunk size. If set to 0, null or undefined, + // or the browser does not support the required Blob API, files will + // be uploaded as a whole. + maxChunkSize: undefined, + // When a non-multipart upload or a chunked multipart upload has been + // aborted, this option can be used to resume the upload by setting + // it to the size of the already uploaded bytes. This option is most + // useful when modifying the options object inside of the "add" or + // "send" callbacks, as the options are cloned for each file upload. + uploadedBytes: undefined, + // By default, failed (abort or error) file uploads are removed from the + // global progress calculation. Set the following option to false to + // prevent recalculating the global progress data: + recalculateProgress: true, + // Interval in milliseconds to calculate and trigger progress events: + progressInterval: 100, + // Interval in milliseconds to calculate progress bitrate: + bitrateInterval: 500, + // By default, uploads are started automatically when adding files: + autoUpload: true, + + // Error and info messages: + messages: { + uploadedBytes: 'Uploaded bytes exceed file size' + }, + + // Translation function, gets the message key to be translated + // and an object with context specific data as arguments: + i18n: function (message, context) { + message = this.messages[message] || message.toString(); + if (context) { + $.each(context, function (key, value) { + message = message.replace('{' + key + '}', value); + }); + } + return message; + }, + + // Additional form data to be sent along with the file uploads can be set + // using this option, which accepts an array of objects with name and + // value properties, a function returning such an array, a FormData + // object (for XHR file uploads), or a simple object. + // The form of the first fileInput is given as parameter to the function: + formData: function (form) { + return form.serializeArray(); + }, + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop, paste or add API call). + // If the singleFileUploads option is enabled, this callback will be + // called once for each file in the selection for XHR file uploads, else + // once for each file selection. + // + // The upload starts when the submit method is invoked on the data parameter. + // The data object contains a files property holding the added files + // and allows you to override plugin options as well as define ajax settings. + // + // Listeners for this callback can also be bound the following way: + // .bind('fileuploadadd', func); + // + // data.submit() returns a Promise object and allows to attach additional + // handlers using jQuery's Deferred callbacks: + // data.submit().done(func).fail(func).always(func); + add: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + if (data.autoUpload || (data.autoUpload !== false && + $(this).fileupload('option', 'autoUpload'))) { + data.process().done(function () { + data.submit(); + }); + } + }, + + // Other callbacks: + + // Callback for the submit event of each file upload: + // submit: function (e, data) {}, // .bind('fileuploadsubmit', func); + + // Callback for the start of each file upload request: + // send: function (e, data) {}, // .bind('fileuploadsend', func); + + // Callback for successful uploads: + // done: function (e, data) {}, // .bind('fileuploaddone', func); + + // Callback for failed (abort or error) uploads: + // fail: function (e, data) {}, // .bind('fileuploadfail', func); + + // Callback for completed (success, abort or error) requests: + // always: function (e, data) {}, // .bind('fileuploadalways', func); + + // Callback for upload progress events: + // progress: function (e, data) {}, // .bind('fileuploadprogress', func); + + // Callback for global upload progress events: + // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); + + // Callback for uploads start, equivalent to the global ajaxStart event: + // start: function (e) {}, // .bind('fileuploadstart', func); + + // Callback for uploads stop, equivalent to the global ajaxStop event: + // stop: function (e) {}, // .bind('fileuploadstop', func); + + // Callback for change events of the fileInput(s): + // change: function (e, data) {}, // .bind('fileuploadchange', func); + + // Callback for paste events to the pasteZone(s): + // paste: function (e, data) {}, // .bind('fileuploadpaste', func); + + // Callback for drop events of the dropZone(s): + // drop: function (e, data) {}, // .bind('fileuploaddrop', func); + + // Callback for dragover events of the dropZone(s): + // dragover: function (e) {}, // .bind('fileuploaddragover', func); + + // Callback for the start of each chunk upload request: + // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func); + + // Callback for successful chunk uploads: + // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func); + + // Callback for failed (abort or error) chunk uploads: + // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func); + + // Callback for completed (success, abort or error) chunk upload requests: + // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func); + + // The plugin options are used as settings object for the ajax calls. + // The following are jQuery ajax settings required for the file uploads: + processData: false, + contentType: false, + cache: false, + timeout: 0 + }, + + // A list of options that require reinitializing event listeners and/or + // special initialization code: + _specialOptions: [ + 'fileInput', + 'dropZone', + 'pasteZone', + 'multipart', + 'forceIframeTransport' + ], + + _blobSlice: $.support.blobSlice && function () { + var slice = this.slice || this.webkitSlice || this.mozSlice; + return slice.apply(this, arguments); + }, + + _BitrateTimer: function () { + this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime()); + this.loaded = 0; + this.bitrate = 0; + this.getBitrate = function (now, loaded, interval) { + var timeDiff = now - this.timestamp; + if (!this.bitrate || !interval || timeDiff > interval) { + this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; + this.loaded = loaded; + this.timestamp = now; + } + return this.bitrate; + }; + }, + + _isXHRUpload: function (options) { + return !options.forceIframeTransport && + ((!options.multipart && $.support.xhrFileUpload) || + $.support.xhrFormDataFileUpload); + }, + + _getFormData: function (options) { + var formData; + if ($.type(options.formData) === 'function') { + return options.formData(options.form); + } + if ($.isArray(options.formData)) { + return options.formData; + } + if ($.type(options.formData) === 'object') { + formData = []; + $.each(options.formData, function (name, value) { + formData.push({name: name, value: value}); + }); + return formData; + } + return []; + }, + + _getTotal: function (files) { + var total = 0; + $.each(files, function (index, file) { + total += file.size || 1; + }); + return total; + }, + + _initProgressObject: function (obj) { + var progress = { + loaded: 0, + total: 0, + bitrate: 0 + }; + if (obj._progress) { + $.extend(obj._progress, progress); + } else { + obj._progress = progress; + } + }, + + _initResponseObject: function (obj) { + var prop; + if (obj._response) { + for (prop in obj._response) { + if (obj._response.hasOwnProperty(prop)) { + delete obj._response[prop]; + } + } + } else { + obj._response = {}; + } + }, + + _onProgress: function (e, data) { + if (e.lengthComputable) { + var now = ((Date.now) ? Date.now() : (new Date()).getTime()), + loaded; + if (data._time && data.progressInterval && + (now - data._time < data.progressInterval) && + e.loaded !== e.total) { + return; + } + data._time = now; + loaded = Math.floor( + e.loaded / e.total * (data.chunkSize || data._progress.total) + ) + (data.uploadedBytes || 0); + // Add the difference from the previously loaded state + // to the global loaded counter: + this._progress.loaded += (loaded - data._progress.loaded); + this._progress.bitrate = this._bitrateTimer.getBitrate( + now, + this._progress.loaded, + data.bitrateInterval + ); + data._progress.loaded = data.loaded = loaded; + data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate( + now, + loaded, + data.bitrateInterval + ); + // Trigger a custom progress event with a total data property set + // to the file size(s) of the current upload and a loaded data + // property calculated accordingly: + this._trigger( + 'progress', + $.Event('progress', {delegatedEvent: e}), + data + ); + // Trigger a global progress event for all current file uploads, + // including ajax calls queued for sequential file uploads: + this._trigger( + 'progressall', + $.Event('progressall', {delegatedEvent: e}), + this._progress + ); + } + }, + + _initProgressListener: function (options) { + var that = this, + xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); + // Accesss to the native XHR object is required to add event listeners + // for the upload progress event: + if (xhr.upload) { + $(xhr.upload).bind('progress', function (e) { + var oe = e.originalEvent; + // Make sure the progress event properties get copied over: + e.lengthComputable = oe.lengthComputable; + e.loaded = oe.loaded; + e.total = oe.total; + that._onProgress(e, options); + }); + options.xhr = function () { + return xhr; + }; + } + }, + + _isInstanceOf: function (type, obj) { + // Cross-frame instanceof check + return Object.prototype.toString.call(obj) === '[object ' + type + ']'; + }, + + _initXHRData: function (options) { + var that = this, + formData, + file = options.files[0], + // Ignore non-multipart setting if not supported: + multipart = options.multipart || !$.support.xhrFileUpload, + paramName = $.type(options.paramName) === 'array' ? + options.paramName[0] : options.paramName; + options.headers = $.extend({}, options.headers); + if (options.contentRange) { + options.headers['Content-Range'] = options.contentRange; + } + if (!multipart || options.blob || !this._isInstanceOf('File', file)) { + options.headers['Content-Disposition'] = 'attachment; filename="' + + encodeURI(file.name) + '"'; + } + if (!multipart) { + options.contentType = file.type || 'application/octet-stream'; + options.data = options.blob || file; + } else if ($.support.xhrFormDataFileUpload) { + if (options.postMessage) { + // window.postMessage does not allow sending FormData + // objects, so we just add the File/Blob objects to + // the formData array and let the postMessage window + // create the FormData object out of this array: + formData = this._getFormData(options); + if (options.blob) { + formData.push({ + name: paramName, + value: options.blob + }); + } else { + $.each(options.files, function (index, file) { + formData.push({ + name: ($.type(options.paramName) === 'array' && + options.paramName[index]) || paramName, + value: file + }); + }); + } + } else { + if (that._isInstanceOf('FormData', options.formData)) { + formData = options.formData; + } else { + formData = new FormData(); + $.each(this._getFormData(options), function (index, field) { + formData.append(field.name, field.value); + }); + } + if (options.blob) { + formData.append(paramName, options.blob, file.name); + } else { + $.each(options.files, function (index, file) { + // This check allows the tests to run with + // dummy objects: + if (that._isInstanceOf('File', file) || + that._isInstanceOf('Blob', file)) { + formData.append( + ($.type(options.paramName) === 'array' && + options.paramName[index]) || paramName, + file, + file.uploadName || file.name + ); + } + }); + } + } + options.data = formData; + } + // Blob reference is not needed anymore, free memory: + options.blob = null; + }, + + _initIframeSettings: function (options) { + var targetHost = $('').prop('href', options.url).prop('host'); + // Setting the dataType to iframe enables the iframe transport: + options.dataType = 'iframe ' + (options.dataType || ''); + // The iframe transport accepts a serialized array as form data: + options.formData = this._getFormData(options); + // Add redirect url to form data on cross-domain uploads: + if (options.redirect && targetHost && targetHost !== location.host) { + options.formData.push({ + name: options.redirectParamName || 'redirect', + value: options.redirect + }); + } + }, + + _initDataSettings: function (options) { + if (this._isXHRUpload(options)) { + if (!this._chunkedUpload(options, true)) { + if (!options.data) { + this._initXHRData(options); + } + this._initProgressListener(options); + } + if (options.postMessage) { + // Setting the dataType to postmessage enables the + // postMessage transport: + options.dataType = 'postmessage ' + (options.dataType || ''); + } + } else { + this._initIframeSettings(options); + } + }, + + _getParamName: function (options) { + var fileInput = $(options.fileInput), + paramName = options.paramName; + if (!paramName) { + paramName = []; + fileInput.each(function () { + var input = $(this), + name = input.prop('name') || 'files[]', + i = (input.prop('files') || [1]).length; + while (i) { + paramName.push(name); + i -= 1; + } + }); + if (!paramName.length) { + paramName = [fileInput.prop('name') || 'files[]']; + } + } else if (!$.isArray(paramName)) { + paramName = [paramName]; + } + return paramName; + }, + + _initFormSettings: function (options) { + // Retrieve missing options from the input field and the + // associated form, if available: + if (!options.form || !options.form.length) { + options.form = $(options.fileInput.prop('form')); + // If the given file input doesn't have an associated form, + // use the default widget file input's form: + if (!options.form.length) { + options.form = $(this.options.fileInput.prop('form')); + } + } + options.paramName = this._getParamName(options); + if (!options.url) { + options.url = options.form.prop('action') || location.href; + } + // The HTTP request method must be "POST" or "PUT": + options.type = (options.type || + ($.type(options.form.prop('method')) === 'string' && + options.form.prop('method')) || '' + ).toUpperCase(); + if (options.type !== 'POST' && options.type !== 'PUT' && + options.type !== 'PATCH') { + options.type = 'POST'; + } + if (!options.formAcceptCharset) { + options.formAcceptCharset = options.form.attr('accept-charset'); + } + }, + + _getAJAXSettings: function (data) { + var options = $.extend({}, this.options, data); + this._initFormSettings(options); + this._initDataSettings(options); + return options; + }, + + // jQuery 1.6 doesn't provide .state(), + // while jQuery 1.8+ removed .isRejected() and .isResolved(): + _getDeferredState: function (deferred) { + if (deferred.state) { + return deferred.state(); + } + if (deferred.isResolved()) { + return 'resolved'; + } + if (deferred.isRejected()) { + return 'rejected'; + } + return 'pending'; + }, + + // Maps jqXHR callbacks to the equivalent + // methods of the given Promise object: + _enhancePromise: function (promise) { + promise.success = promise.done; + promise.error = promise.fail; + promise.complete = promise.always; + return promise; + }, + + // Creates and returns a Promise object enhanced with + // the jqXHR methods abort, success, error and complete: + _getXHRPromise: function (resolveOrReject, context, args) { + var dfd = $.Deferred(), + promise = dfd.promise(); + context = context || this.options.context || promise; + if (resolveOrReject === true) { + dfd.resolveWith(context, args); + } else if (resolveOrReject === false) { + dfd.rejectWith(context, args); + } + promise.abort = dfd.promise; + return this._enhancePromise(promise); + }, + + // Adds convenience methods to the data callback argument: + _addConvenienceMethods: function (e, data) { + var that = this, + getPromise = function (args) { + return $.Deferred().resolveWith(that, args).promise(); + }; + data.process = function (resolveFunc, rejectFunc) { + if (resolveFunc || rejectFunc) { + data._processQueue = this._processQueue = + (this._processQueue || getPromise([this])).then( + function () { + if (data.errorThrown) { + return $.Deferred() + .rejectWith(that, [data]).promise(); + } + return getPromise(arguments); + } + ).then(resolveFunc, rejectFunc); + } + return this._processQueue || getPromise([this]); + }; + data.submit = function () { + if (this.state() !== 'pending') { + data.jqXHR = this.jqXHR = + (that._trigger( + 'submit', + $.Event('submit', {delegatedEvent: e}), + this + ) !== false) && that._onSend(e, this); + } + return this.jqXHR || that._getXHRPromise(); + }; + data.abort = function () { + if (this.jqXHR) { + return this.jqXHR.abort(); + } + this.errorThrown = 'abort'; + that._trigger('fail', null, this); + return that._getXHRPromise(false); + }; + data.state = function () { + if (this.jqXHR) { + return that._getDeferredState(this.jqXHR); + } + if (this._processQueue) { + return that._getDeferredState(this._processQueue); + } + }; + data.processing = function () { + return !this.jqXHR && this._processQueue && that + ._getDeferredState(this._processQueue) === 'pending'; + }; + data.progress = function () { + return this._progress; + }; + data.response = function () { + return this._response; + }; + }, + + // Parses the Range header from the server response + // and returns the uploaded bytes: + _getUploadedBytes: function (jqXHR) { + var range = jqXHR.getResponseHeader('Range'), + parts = range && range.split('-'), + upperBytesPos = parts && parts.length > 1 && + parseInt(parts[1], 10); + return upperBytesPos && upperBytesPos + 1; + }, + + // Uploads a file in multiple, sequential requests + // by splitting the file up in multiple blob chunks. + // If the second parameter is true, only tests if the file + // should be uploaded in chunks, but does not invoke any + // upload requests: + _chunkedUpload: function (options, testOnly) { + options.uploadedBytes = options.uploadedBytes || 0; + var that = this, + file = options.files[0], + fs = file.size, + ub = options.uploadedBytes, + mcs = options.maxChunkSize || fs, + slice = this._blobSlice, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + upload; + if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || + options.data) { + return false; + } + if (testOnly) { + return true; + } + if (ub >= fs) { + file.error = options.i18n('uploadedBytes'); + return this._getXHRPromise( + false, + options.context, + [null, 'error', file.error] + ); + } + // The chunk upload method: + upload = function () { + // Clone the options object for each chunk upload: + var o = $.extend({}, options), + currentLoaded = o._progress.loaded; + o.blob = slice.call( + file, + ub, + ub + mcs, + file.type + ); + // Store the current chunk size, as the blob itself + // will be dereferenced after data processing: + o.chunkSize = o.blob.size; + // Expose the chunk bytes position range: + o.contentRange = 'bytes ' + ub + '-' + + (ub + o.chunkSize - 1) + '/' + fs; + // Process the upload data (the blob and potential form data): + that._initXHRData(o); + // Add progress listeners for this chunk upload: + that._initProgressListener(o); + jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) || + that._getXHRPromise(false, o.context)) + .done(function (result, textStatus, jqXHR) { + ub = that._getUploadedBytes(jqXHR) || + (ub + o.chunkSize); + // Create a progress event if no final progress event + // with loaded equaling total has been triggered + // for this chunk: + if (currentLoaded + o.chunkSize - o._progress.loaded) { + that._onProgress($.Event('progress', { + lengthComputable: true, + loaded: ub - o.uploadedBytes, + total: ub - o.uploadedBytes + }), o); + } + options.uploadedBytes = o.uploadedBytes = ub; + o.result = result; + o.textStatus = textStatus; + o.jqXHR = jqXHR; + that._trigger('chunkdone', null, o); + that._trigger('chunkalways', null, o); + if (ub < fs) { + // File upload not yet complete, + // continue with the next chunk: + upload(); + } else { + dfd.resolveWith( + o.context, + [result, textStatus, jqXHR] + ); + } + }) + .fail(function (jqXHR, textStatus, errorThrown) { + o.jqXHR = jqXHR; + o.textStatus = textStatus; + o.errorThrown = errorThrown; + that._trigger('chunkfail', null, o); + that._trigger('chunkalways', null, o); + dfd.rejectWith( + o.context, + [jqXHR, textStatus, errorThrown] + ); + }); + }; + this._enhancePromise(promise); + promise.abort = function () { + return jqXHR.abort(); + }; + upload(); + return promise; + }, + + _beforeSend: function (e, data) { + if (this._active === 0) { + // the start callback is triggered when an upload starts + // and no other uploads are currently running, + // equivalent to the global ajaxStart event: + this._trigger('start'); + // Set timer for global bitrate progress calculation: + this._bitrateTimer = new this._BitrateTimer(); + // Reset the global progress values: + this._progress.loaded = this._progress.total = 0; + this._progress.bitrate = 0; + } + // Make sure the container objects for the .response() and + // .progress() methods on the data object are available + // and reset to their initial state: + this._initResponseObject(data); + this._initProgressObject(data); + data._progress.loaded = data.loaded = data.uploadedBytes || 0; + data._progress.total = data.total = this._getTotal(data.files) || 1; + data._progress.bitrate = data.bitrate = 0; + this._active += 1; + // Initialize the global progress values: + this._progress.loaded += data.loaded; + this._progress.total += data.total; + }, + + _onDone: function (result, textStatus, jqXHR, options) { + var total = options._progress.total, + response = options._response; + if (options._progress.loaded < total) { + // Create a progress event if no final progress event + // with loaded equaling total has been triggered: + this._onProgress($.Event('progress', { + lengthComputable: true, + loaded: total, + total: total + }), options); + } + response.result = options.result = result; + response.textStatus = options.textStatus = textStatus; + response.jqXHR = options.jqXHR = jqXHR; + this._trigger('done', null, options); + }, + + _onFail: function (jqXHR, textStatus, errorThrown, options) { + var response = options._response; + if (options.recalculateProgress) { + // Remove the failed (error or abort) file upload from + // the global progress calculation: + this._progress.loaded -= options._progress.loaded; + this._progress.total -= options._progress.total; + } + response.jqXHR = options.jqXHR = jqXHR; + response.textStatus = options.textStatus = textStatus; + response.errorThrown = options.errorThrown = errorThrown; + this._trigger('fail', null, options); + }, + + _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { + // jqXHRorResult, textStatus and jqXHRorError are added to the + // options object via done and fail callbacks + this._trigger('always', null, options); + }, + + _onSend: function (e, data) { + if (!data.submit) { + this._addConvenienceMethods(e, data); + } + var that = this, + jqXHR, + aborted, + slot, + pipe, + options = that._getAJAXSettings(data), + send = function () { + that._sending += 1; + // Set timer for bitrate progress calculation: + options._bitrateTimer = new that._BitrateTimer(); + jqXHR = jqXHR || ( + ((aborted || that._trigger( + 'send', + $.Event('send', {delegatedEvent: e}), + options + ) === false) && + that._getXHRPromise(false, options.context, aborted)) || + that._chunkedUpload(options) || $.ajax(options) + ).done(function (result, textStatus, jqXHR) { + that._onDone(result, textStatus, jqXHR, options); + }).fail(function (jqXHR, textStatus, errorThrown) { + that._onFail(jqXHR, textStatus, errorThrown, options); + }).always(function (jqXHRorResult, textStatus, jqXHRorError) { + that._onAlways( + jqXHRorResult, + textStatus, + jqXHRorError, + options + ); + that._sending -= 1; + that._active -= 1; + if (options.limitConcurrentUploads && + options.limitConcurrentUploads > that._sending) { + // Start the next queued upload, + // that has not been aborted: + var nextSlot = that._slots.shift(); + while (nextSlot) { + if (that._getDeferredState(nextSlot) === 'pending') { + nextSlot.resolve(); + break; + } + nextSlot = that._slots.shift(); + } + } + if (that._active === 0) { + // The stop callback is triggered when all uploads have + // been completed, equivalent to the global ajaxStop event: + that._trigger('stop'); + } + }); + return jqXHR; + }; + this._beforeSend(e, options); + if (this.options.sequentialUploads || + (this.options.limitConcurrentUploads && + this.options.limitConcurrentUploads <= this._sending)) { + if (this.options.limitConcurrentUploads > 1) { + slot = $.Deferred(); + this._slots.push(slot); + pipe = slot.then(send); + } else { + this._sequence = this._sequence.then(send, send); + pipe = this._sequence; + } + // Return the piped Promise object, enhanced with an abort method, + // which is delegated to the jqXHR object of the current upload, + // and jqXHR callbacks mapped to the equivalent Promise methods: + pipe.abort = function () { + aborted = [undefined, 'abort', 'abort']; + if (!jqXHR) { + if (slot) { + slot.rejectWith(options.context, aborted); + } + return send(); + } + return jqXHR.abort(); + }; + return this._enhancePromise(pipe); + } + return send(); + }, + + _onAdd: function (e, data) { + var that = this, + result = true, + options = $.extend({}, this.options, data), + files = data.files, + filesLength = files.length, + limit = options.limitMultiFileUploads, + limitSize = options.limitMultiFileUploadSize, + overhead = options.limitMultiFileUploadSizeOverhead, + batchSize = 0, + paramName = this._getParamName(options), + paramNameSet, + paramNameSlice, + fileSet, + i, + j = 0; + if (!filesLength) { + return false; + } + if (limitSize && files[0].size === undefined) { + limitSize = undefined; + } + if (!(options.singleFileUploads || limit || limitSize) || + !this._isXHRUpload(options)) { + fileSet = [files]; + paramNameSet = [paramName]; + } else if (!(options.singleFileUploads || limitSize) && limit) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < filesLength; i += limit) { + fileSet.push(files.slice(i, i + limit)); + paramNameSlice = paramName.slice(i, i + limit); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); + } + } else if (!options.singleFileUploads && limitSize) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < filesLength; i = i + 1) { + batchSize += files[i].size + overhead; + if (i + 1 === filesLength || + ((batchSize + files[i + 1].size + overhead) > limitSize) || + (limit && i + 1 - j >= limit)) { + fileSet.push(files.slice(j, i + 1)); + paramNameSlice = paramName.slice(j, i + 1); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); + j = i + 1; + batchSize = 0; + } + } + } else { + paramNameSet = paramName; + } + data.originalFiles = files; + $.each(fileSet || files, function (index, element) { + var newData = $.extend({}, data); + newData.files = fileSet ? element : [element]; + newData.paramName = paramNameSet[index]; + that._initResponseObject(newData); + that._initProgressObject(newData); + that._addConvenienceMethods(e, newData); + result = that._trigger( + 'add', + $.Event('add', {delegatedEvent: e}), + newData + ); + return result; + }); + return result; + }, + + _replaceFileInput: function (data) { + var input = data.fileInput, + inputClone = input.clone(true), + restoreFocus = input.is(document.activeElement); + // Add a reference for the new cloned file input to the data argument: + data.fileInputClone = inputClone; + $('
    ').append(inputClone)[0].reset(); + // Detaching allows to insert the fileInput on another form + // without loosing the file input value: + input.after(inputClone).detach(); + // If the fileInput had focus before it was detached, + // restore focus to the inputClone. + if (restoreFocus) { + inputClone.focus(); + } + // Avoid memory leaks with the detached file input: + $.cleanData(input.unbind('remove')); + // Replace the original file input element in the fileInput + // elements set with the clone, which has been copied including + // event handlers: + this.options.fileInput = this.options.fileInput.map(function (i, el) { + if (el === input[0]) { + return inputClone[0]; + } + return el; + }); + // If the widget has been initialized on the file input itself, + // override this.element with the file input clone: + if (input[0] === this.element[0]) { + this.element = inputClone; + } + }, + + _handleFileTreeEntry: function (entry, path) { + var that = this, + dfd = $.Deferred(), + entries = [], + dirReader, + errorHandler = function (e) { + if (e && !e.entry) { + e.entry = entry; + } + // Since $.when returns immediately if one + // Deferred is rejected, we use resolve instead. + // This allows valid files and invalid items + // to be returned together in one set: + dfd.resolve([e]); + }, + successHandler = function (entries) { + that._handleFileTreeEntries( + entries, + path + entry.name + '/' + ).done(function (files) { + dfd.resolve(files); + }).fail(errorHandler); + }, + readEntries = function () { + dirReader.readEntries(function (results) { + if (!results.length) { + successHandler(entries); + } else { + entries = entries.concat(results); + readEntries(); + } + }, errorHandler); + }; + path = path || ''; + if (entry.isFile) { + if (entry._file) { + // Workaround for Chrome bug #149735 + entry._file.relativePath = path; + dfd.resolve(entry._file); + } else { + entry.file(function (file) { + file.relativePath = path; + dfd.resolve(file); + }, errorHandler); + } + } else if (entry.isDirectory) { + dirReader = entry.createReader(); + readEntries(); + } else { + // Return an empy list for file system items + // other than files or directories: + dfd.resolve([]); + } + return dfd.promise(); + }, + + _handleFileTreeEntries: function (entries, path) { + var that = this; + return $.when.apply( + $, + $.map(entries, function (entry) { + return that._handleFileTreeEntry(entry, path); + }) + ).then(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _getDroppedFiles: function (dataTransfer) { + dataTransfer = dataTransfer || {}; + var items = dataTransfer.items; + if (items && items.length && (items[0].webkitGetAsEntry || + items[0].getAsEntry)) { + return this._handleFileTreeEntries( + $.map(items, function (item) { + var entry; + if (item.webkitGetAsEntry) { + entry = item.webkitGetAsEntry(); + if (entry) { + // Workaround for Chrome bug #149735: + entry._file = item.getAsFile(); + } + return entry; + } + return item.getAsEntry(); + }) + ); + } + return $.Deferred().resolve( + $.makeArray(dataTransfer.files) + ).promise(); + }, + + _getSingleFileInputFiles: function (fileInput) { + fileInput = $(fileInput); + var entries = fileInput.prop('webkitEntries') || + fileInput.prop('entries'), + files, + value; + if (entries && entries.length) { + return this._handleFileTreeEntries(entries); + } + files = $.makeArray(fileInput.prop('files')); + if (!files.length) { + value = fileInput.prop('value'); + if (!value) { + return $.Deferred().resolve([]).promise(); + } + // If the files property is not available, the browser does not + // support the File API and we add a pseudo File object with + // the input value as name with path information removed: + files = [{name: value.replace(/^.*\\/, '')}]; + } else if (files[0].name === undefined && files[0].fileName) { + // File normalization for Safari 4 and Firefox 3: + $.each(files, function (index, file) { + file.name = file.fileName; + file.size = file.fileSize; + }); + } + return $.Deferred().resolve(files).promise(); + }, + + _getFileInputFiles: function (fileInput) { + if (!(fileInput instanceof $) || fileInput.length === 1) { + return this._getSingleFileInputFiles(fileInput); + } + return $.when.apply( + $, + $.map(fileInput, this._getSingleFileInputFiles) + ).then(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _onChange: function (e) { + var that = this, + data = { + fileInput: $(e.target), + form: $(e.target.form) + }; + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + if (that.options.replaceFileInput) { + that._replaceFileInput(data); + } + if (that._trigger( + 'change', + $.Event('change', {delegatedEvent: e}), + data + ) !== false) { + that._onAdd(e, data); + } + }); + }, + + _onPaste: function (e) { + var items = e.originalEvent && e.originalEvent.clipboardData && + e.originalEvent.clipboardData.items, + data = {files: []}; + if (items && items.length) { + $.each(items, function (index, item) { + var file = item.getAsFile && item.getAsFile(); + if (file) { + data.files.push(file); + } + }); + if (this._trigger( + 'paste', + $.Event('paste', {delegatedEvent: e}), + data + ) !== false) { + this._onAdd(e, data); + } + } + }, + + _onDrop: function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var that = this, + dataTransfer = e.dataTransfer, + data = {}; + if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { + e.preventDefault(); + this._getDroppedFiles(dataTransfer).always(function (files) { + data.files = files; + if (that._trigger( + 'drop', + $.Event('drop', {delegatedEvent: e}), + data + ) !== false) { + that._onAdd(e, data); + } + }); + } + }, + + _onDragOver: getDragHandler('dragover'), + + _onDragEnter: getDragHandler('dragenter'), + + _onDragLeave: getDragHandler('dragleave'), + + _initEventHandlers: function () { + if (this._isXHRUpload(this.options)) { + this._on(this.options.dropZone, { + dragover: this._onDragOver, + drop: this._onDrop, + // event.preventDefault() on dragenter is required for IE10+: + dragenter: this._onDragEnter, + // dragleave is not required, but added for completeness: + dragleave: this._onDragLeave + }); + this._on(this.options.pasteZone, { + paste: this._onPaste + }); + } + if ($.support.fileInput) { + this._on(this.options.fileInput, { + change: this._onChange + }); + } + }, + + _destroyEventHandlers: function () { + this._off(this.options.dropZone, 'dragenter dragleave dragover drop'); + this._off(this.options.pasteZone, 'paste'); + this._off(this.options.fileInput, 'change'); + }, + + _destroy: function () { + this._destroyEventHandlers(); + }, + + _setOption: function (key, value) { + var reinit = $.inArray(key, this._specialOptions) !== -1; + if (reinit) { + this._destroyEventHandlers(); + } + this._super(key, value); + if (reinit) { + this._initSpecialOptions(); + this._initEventHandlers(); + } + }, + + _initSpecialOptions: function () { + var options = this.options; + if (options.fileInput === undefined) { + options.fileInput = this.element.is('input[type="file"]') ? + this.element : this.element.find('input[type="file"]'); + } else if (!(options.fileInput instanceof $)) { + options.fileInput = $(options.fileInput); + } + if (!(options.dropZone instanceof $)) { + options.dropZone = $(options.dropZone); + } + if (!(options.pasteZone instanceof $)) { + options.pasteZone = $(options.pasteZone); + } + }, + + _getRegExp: function (str) { + var parts = str.split('/'), + modifiers = parts.pop(); + parts.shift(); + return new RegExp(parts.join('/'), modifiers); + }, + + _isRegExpOption: function (key, value) { + return key !== 'url' && $.type(value) === 'string' && + /^\/.*\/[igm]{0,3}$/.test(value); + }, + + _initDataAttributes: function () { + var that = this, + options = this.options, + data = this.element.data(); + // Initialize options set via HTML5 data-attributes: + $.each( + this.element[0].attributes, + function (index, attr) { + var key = attr.name.toLowerCase(), + value; + if (/^data-/.test(key)) { + // Convert hyphen-ated key to camelCase: + key = key.slice(5).replace(/-[a-z]/g, function (str) { + return str.charAt(1).toUpperCase(); + }); + value = data[key]; + if (that._isRegExpOption(key, value)) { + value = that._getRegExp(value); + } + options[key] = value; + } + } + ); + }, + + _create: function () { + this._initDataAttributes(); + this._initSpecialOptions(); + this._slots = []; + this._sequence = this._getXHRPromise(true); + this._sending = this._active = 0; + this._initProgressObject(this); + this._initEventHandlers(); + }, + + // This method is exposed to the widget API and allows to query + // the number of active uploads: + active: function () { + return this._active; + }, + + // This method is exposed to the widget API and allows to query + // the widget upload progress. + // It returns an object with loaded, total and bitrate properties + // for the running uploads: + progress: function () { + return this._progress; + }, + + // This method is exposed to the widget API and allows adding files + // using the fileupload API. The data parameter accepts an object which + // must have a files property and can contain additional options: + // .fileupload('add', {files: filesList}); + add: function (data) { + var that = this; + if (!data || this.options.disabled) { + return; + } + if (data.fileInput && !data.files) { + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + that._onAdd(null, data); + }); + } else { + data.files = $.makeArray(data.files); + this._onAdd(null, data); + } + }, + + // This method is exposed to the widget API and allows sending files + // using the fileupload API. The data parameter accepts an object which + // must have a files or fileInput property and can contain additional options: + // .fileupload('send', {files: filesList}); + // The method returns a Promise object for the file upload call. + send: function (data) { + if (data && !this.options.disabled) { + if (data.fileInput && !data.files) { + var that = this, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + aborted; + promise.abort = function () { + aborted = true; + if (jqXHR) { + return jqXHR.abort(); + } + dfd.reject(null, 'abort', 'abort'); + return promise; + }; + this._getFileInputFiles(data.fileInput).always( + function (files) { + if (aborted) { + return; + } + if (!files.length) { + dfd.reject(); + return; + } + data.files = files; + jqXHR = that._onSend(null, data); + jqXHR.then( + function (result, textStatus, jqXHR) { + dfd.resolve(result, textStatus, jqXHR); + }, + function (jqXHR, textStatus, errorThrown) { + dfd.reject(jqXHR, textStatus, errorThrown); + } + ); + } + ); + return this._enhancePromise(promise); + } + data.files = $.makeArray(data.files); + if (data.files.length) { + return this._onSend(null, data); + } + } + return this._getXHRPromise(false, data && data.context); + } + + }); + +})); diff --git a/lib/assets/.keep b/lib/assets/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/log/.keep b/log/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/public/404.html b/public/404.html new file mode 100644 index 000000000..b612547fc --- /dev/null +++ b/public/404.html @@ -0,0 +1,67 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
    +
    +

    The page you were looking for doesn't exist.

    +

    You may have mistyped the address or the page may have moved.

    +
    +

    If you are the application owner check the logs for more information.

    +
    + + diff --git a/public/422.html b/public/422.html new file mode 100644 index 000000000..a21f82b3b --- /dev/null +++ b/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
    +
    +

    The change you wanted was rejected.

    +

    Maybe you tried to change something you didn't have access to.

    +
    +

    If you are the application owner check the logs for more information.

    +
    + + diff --git a/public/500.html b/public/500.html new file mode 100644 index 000000000..061abc587 --- /dev/null +++ b/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
    +
    +

    We're sorry, but something went wrong.

    +
    +

    If you are the application owner check the logs for more information.

    +
    + + diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png new file mode 100644 index 000000000..e69de29bb diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 000000000..e69de29bb diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 000000000..e69de29bb diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 000000000..3c9c7c01f --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,5 @@ +# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-agent: * +# Disallow: / diff --git a/spec/controllers/.keep b/spec/controllers/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/spec/controllers/comments_controller_spec.rb b/spec/controllers/comments_controller_spec.rb new file mode 100644 index 000000000..63f198bcc --- /dev/null +++ b/spec/controllers/comments_controller_spec.rb @@ -0,0 +1 @@ +require 'rails_helper' diff --git a/spec/controllers/friends_controller_spec.rb b/spec/controllers/friends_controller_spec.rb new file mode 100644 index 000000000..63f198bcc --- /dev/null +++ b/spec/controllers/friends_controller_spec.rb @@ -0,0 +1 @@ +require 'rails_helper' diff --git a/spec/controllers/galleries_controller_spec.rb b/spec/controllers/galleries_controller_spec.rb new file mode 100644 index 000000000..63f198bcc --- /dev/null +++ b/spec/controllers/galleries_controller_spec.rb @@ -0,0 +1 @@ +require 'rails_helper' diff --git a/spec/controllers/likes_controller_spec.rb b/spec/controllers/likes_controller_spec.rb new file mode 100644 index 000000000..63f198bcc --- /dev/null +++ b/spec/controllers/likes_controller_spec.rb @@ -0,0 +1 @@ +require 'rails_helper' diff --git a/spec/controllers/notices_controller_spec.rb b/spec/controllers/notices_controller_spec.rb new file mode 100644 index 000000000..4bcfaae43 --- /dev/null +++ b/spec/controllers/notices_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe NoticesController, type: :controller do + +end diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb new file mode 100644 index 000000000..63f198bcc --- /dev/null +++ b/spec/controllers/posts_controller_spec.rb @@ -0,0 +1 @@ +require 'rails_helper' diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb new file mode 100644 index 000000000..63f198bcc --- /dev/null +++ b/spec/controllers/profiles_controller_spec.rb @@ -0,0 +1 @@ +require 'rails_helper' diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb new file mode 100644 index 000000000..63f198bcc --- /dev/null +++ b/spec/controllers/search_controller_spec.rb @@ -0,0 +1 @@ +require 'rails_helper' diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb new file mode 100644 index 000000000..215da05e2 --- /dev/null +++ b/spec/controllers/sessions_controller_spec.rb @@ -0,0 +1,89 @@ +require 'rails_helper' +# spec/controllers/users_controller_spec.rb + +describe SessionsController do + let(:profile){ create(:profile)} + let(:user){ profile.user } + + before do + user + end + + describe 'GET #new' do + context "has token" do + + before do + user.regenerate_auth_token + cookies[:token] = user.token + end + + + it "signs the user in and redirects" do + process :new + + expect(session[:user_id]).to_not be_nil + expect(response).to redirect_to(users_path) + end + + end + + + context "authenticated" do + + before do + create_session(user) + end + + + it "redirects to the user index" do + process :new + expect(response).to redirect_to(users_path) + end + + end + end + + describe 'POST #create' do + + it "logs in a user with valid credentials" do + process :create, params: {email: user.email, password: user.password} + expect(session[:user_id]).to_not be_nil + assert_response :redirect + end + + it "does not log in a user with invalid credentials" do + process :create, params: {email: user.email + "x", password: user.password} + expect(session[:user_id]).to be_nil + assert_response :success + end + + it "flashes a danger method on invalid credentials" do + process :create, params: {email: user.email + "x", password: user.password} + expect(flash[:danger]).to_not be_nil + end + + end + + describe 'POST #destroy' do + before do + create_session(user) + end + + it "logs out a user and redirects to the homepage" do + process :destroy + + expect(session[:user_id]).to be_nil + expect(response).to redirect_to(signup_path) + end + + it "flashes a message to inform the user" do + process :destroy + + expect(flash[:success]).to_not be_nil + end + + end + + + +end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb new file mode 100644 index 000000000..a9a0647e3 --- /dev/null +++ b/spec/controllers/users_controller_spec.rb @@ -0,0 +1,94 @@ +require 'rails_helper' +# spec/controllers/users_controller_spec.rb + +describe UsersController do + let(:profile){ create(:profile)} + let(:user){ profile.user } + let(:another){create(:user)} + + before do + another + user + end + + context "authenticated" do + describe 'user access' do + + before :each do + create_session(user) + end + + describe "#set_user" do + + end + + describe 'GET #index' do + + it "renders the :index template" do + process :index + assert_response :success + end + + end + + describe 'GET #new' do + + it "GET #new redirects to index" do + process :new + expect(response).to redirect_to users_path + end + + end + + describe 'GET #edit' do + it "allows viewing the edit page" do + process :edit, params: {id: user.id } + assert_response :success + end + + it "does not allow viewing another user's edit page" do + process :edit, params: {id: another.id } + assert_response :redirect + end + end + + describe 'POST #update' do + + it_has_behavior 'valid_update', :user, { current_password: "!23456Yuiopasdf", email: "foo@email.com" }, :user_path do + let(:checked) { user } + end + + it_has_behavior 'invalid_update', :user, { email: "new@email.com" } do + let(:checked) { user } + end + + it_has_behavior 'unauthorized_update', :user, { current_password: "!23456Yuiopasdf", email: "foo@email.com" } do + let(:checked) { another } + end + end + + end + + end + + describe 'GET #new' do + + it "does not require authentication" do + process :new + assert_response :success + end + + end + + describe 'POST #create' do + + it_has_behavior 'valid_create', :user, :users_path, 1 + + it_has_behavior 'invalid_create', :user, { user: { + email: "newemail.com", + password: "asdf" + } + } + + end +end diff --git a/spec/factories.rb b/spec/factories.rb new file mode 100644 index 000000000..9c3b2db89 --- /dev/null +++ b/spec/factories.rb @@ -0,0 +1,69 @@ +FactoryGirl.define do + factory :notice do + user nil + message "MyString" + end + factory :user do + sequence(:email) { |n| "foo#{n}@bar.com" } + password "!23456Yuiopasdf" + token nil + failed nil + last_attempt nil + end + + factory :bio do + association :profile + slogan "I'm a Prude" + about "Catch Me if you can" + end + + factory :friend_request do + association :user + association :request, factory: :user + end + + factory :friends_user do + association :user + association :friend, factory: :user + end + + factory :gallery do + association :user + title "gallery title" + description "graph" + end + + factory :image do + association :gallery + url "http://img.com" + description "a picture" + end + + factory :like do + association :user + association :post + end + + factory :post do + association :user + post_type "Post" + body "Post Body" + likes_count 0 + comments_count 0 + end + + factory :profile do + association :user + first_name "Hermione" + last_name "Granger" + birthday Date.today + gender "Female" + college "Hogwarts" + hometown "Heathgate, London" + current_home "Ron's Pad" + phone "555-555-5555" + association :profile_img, factory: :image + cover nil + edited false + end +end diff --git a/spec/features/.keep b/spec/features/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/spec/features/posts_spec.rb b/spec/features/posts_spec.rb new file mode 100644 index 000000000..59b5358ed --- /dev/null +++ b/spec/features/posts_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +describe "Timeline" do + let(:profile){ create(:profile)} + let(:second_profile){ create(:profile, first_name: "Ron", last_name: "Weasley")} + let(:user){ profile.user } + let(:second_user){ second_profile.user } + + feature "Create Post" do + context "Authenticated" do + before do + sign_in(user) + end + context "Current User" do + before do + visit user_path(user) + end + it "displays a new post form on the timeline" do + expect(page).to have_css("form#new_post textarea") + end + + it "creates a new post" do + within "form#new_post" do + fill_in "post_body", with: "Test New Post" + + expect{click_button "Post"}.to change(Post, :count).by(1) + end + end + + it "redirects you to view the newly created post" do + create_new_post + expect(page).to have_current_path(user_post_path(user, Post.last)) + end + end + context "Other User" do + before do + visit user_path(second_user) + end + + it "does not display the new post form" do + expect(page).to_not have_css("form#new_post textarea") + end + + end + end + context "Unauthenticated" do + it "redirects to the login path" do + visit user_path(user) + expect(page).to have_current_path(login_path) + end + end + end +end diff --git a/spec/features/profiles_spec.rb b/spec/features/profiles_spec.rb new file mode 100644 index 000000000..76a54cf4b --- /dev/null +++ b/spec/features/profiles_spec.rb @@ -0,0 +1,139 @@ +require 'rails_helper' + +describe "Profiles" do + let(:profile){ create(:profile)} + let(:second_profile){ create(:profile, first_name: "Ron", last_name: "Weasley")} + let(:user){ profile.user } + let(:second_user){ second_profile.user } + + + + feature "View Profile" do + + context "Authenticated" do + + before do + profile + sign_in(user) + visit root_path + click_link "About" + end + + it "renders the about page" do + expect(page).to have_current_path(user_profile_path(user)) + expect(page).to have_css "section#about" + end + + context "Current User" do + it "shows the edit profile link" do + expect(page).to have_css "li.edit > a", text: "Edit Profile" + expect(page).to have_link "Edit Profile" + end + end + + context "Other User" do + it "does not show the edit profile link" do + second_profile.save + visit user_profile_path(second_user) + expect(page).to have_css ".profile-username", text: "#{second_profile.first_name} #{second_profile.last_name}" + expect(page).to_not have_css "li.edit > a" + end + end + + end + context "Unauthenticated User" do + it "redirects to login path" do + visit user_profile_path(user) + expect(page).to have_current_path(login_path) + end + end + end + + feature "Edit Profile" do + + context "Authenticated" do + + before do + profile + sign_in(user) + visit root_path + click_link "Edit Profile" + end + + it "renders the Edit-About page" do + expect(page).to have_current_path(edit_user_profile_path(user)) + expect(page).to have_css "section#about-edit" + end + + context "Current User" do + it "shows the edit profile form" do + expect(page).to have_css "form#edit_profile_#{user.profile.id}" + end + + it "can update the current user's info" do + expect(page).to have_css "select#profile_birthday_1i" + expect(page).to have_css "form#edit_profile_#{user.profile.id}" + new_date = 10.years.ago + within "form#edit_profile_#{user.profile.id}" do + fill_in "profile_college", with: "New College" + fill_in "profile_hometown", with: "New Hometown" + fill_in "profile_current_home", with: "New Current" + fill_in "profile_phone", with: "555-555-5555" + select new_date.year, from: "profile_birthday_1i" + select new_date.strftime('%B'), from: "profile_birthday_2i" + select new_date.day, from: "profile_birthday_3i" + click_button "Save Changes" + end + + profile.reload + expect(profile.college).to eq("New College") + expect(profile.hometown).to eq("New Hometown") + expect(profile.birthday).to eq(new_date.to_date) + end + + it "creates a bio if a slogan is filled out for the first time" do + fill_in "profile_bio_attributes_slogan", with: "Words Words Words" + expect{click_button "Save Changes"}.to change(Bio, :count).by(1) + end + + it "creates a bio if the About Me section is filled out for the first time" do + fill_in "profile_bio_attributes_about", with: "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco" + expect{click_button "Save Changes"}.to change(Bio, :count).by(1) + end + + it "Updates an existing bio" do + bio = create(:bio, profile: profile) + slogan = bio.slogan + + visit edit_user_profile_path(user) + + fill_in "profile_bio_attributes_slogan", with: "#{slogan} Words" + + expect{click_button "Save Changes"}.to_not change(Bio, :count) + expect(bio.slogan).to_not eq("#{slogan} Words") + end + + end + + context "Other User" do + it "does not allow editing other user profiles" do + visit edit_user_profile_path(second_user) + expect(page).to_not have_current_path(edit_user_profile_path(second_user)) + end + + it "redirects back to the logged in user's profile" do + visit edit_user_profile_path(second_user) + expect(page).to have_current_path(user_profile_path(user)) + end + end + end + + context "Unauthenticated User" do + it "redirects to the login path" do + visit edit_user_profile_path(second_user) + expect(page).to have_current_path(login_path) + end + end + end + +end diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb new file mode 100644 index 000000000..e9e653b1c --- /dev/null +++ b/spec/features/users_spec.rb @@ -0,0 +1,99 @@ +require 'rails_helper' + +# `feature` is an alias for `describe` +describe "Users" do + let(:profile){ create(:profile)} + let(:user){ profile.user } + + feature 'Sign up' do + + before do + visit root_path + end + + # `scenario` is an alias for `it` + context "New user" do + it "creates a new user" do + # fill in the form for a new user + within "form#new_user" do + fill_in "user_profile_attributes_first_name", with: "Test" + fill_in "user_profile_attributes_last_name", with: "User" + fill_in "user_email", with: "foo@bar.com" + fill_in "user_password", with: "!23456Yuiopasdf" + fill_in "user_password_confirmation", with: "!23456Yuiopasdf" + select "2012", from: "user_profile_attributes_birthday_1i" + select "March", from: "user_profile_attributes_birthday_2i" + select "13", from: "user_profile_attributes_birthday_3i" + choose "Male" + end + + # submit the form and verify it created a user + expect{ click_button "Sign Up!" }.to change(User, :count).by(1) + + # verify that we've been logged in + expect(page).to have_css ".profile-username", text: "Test User" + + # verify the flash is working properly + expect(page).to have_content "Successfully signed up!" + end + + end + + context "Existing email" do + it "rejects the attempt and informs the user" do + user.save + # fill in the form for a new user + within "form#new_user" do + fill_in "user_profile_attributes_first_name", with: "Test" + fill_in "user_profile_attributes_last_name", with: "User" + fill_in "user_email", with: user.email + fill_in "user_password", with: "!23456Yuiopasdf" + fill_in "user_password_confirmation", with: "!23456Yuiopasdf" + select "2012", from: "user_profile_attributes_birthday_1i" + select "March", from: "user_profile_attributes_birthday_2i" + select "13", from: "user_profile_attributes_birthday_3i" + choose "Male" + end + + expect{ click_button "Sign Up!" }.to_not change(User, :count) + expect(page).to have_content "Email has already been taken" + end + end + end + + feature 'Login' do + before do + visit login_path + end + + context "with improper credentials" do + before do + user.email = user.email + "x" + sign_in(user) + end + + scenario "does not allow sign in" do + expect(page).to have_content "Incorrect Credentials" + end + end + + context "with proper credentials" do + before do + sign_in(user) + end + scenario "successfully signs in an existing user" do + # verify the user nav is shown + expect(page).to have_css ".profile-username", text: "Hermione Granger" + end + + context "after signing out" do + before do + sign_out + end + scenario "the user should be logged out" do + expect(page).to have_css "div.alert-success p", text: "Signed Out" + end + end + end + end +end diff --git a/spec/helpers/notices_helper_spec.rb b/spec/helpers/notices_helper_spec.rb new file mode 100644 index 000000000..79a616059 --- /dev/null +++ b/spec/helpers/notices_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the NoticesHelper. For example: +# +# describe NoticesHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe NoticesHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/jobs/add_to_profile_photos_job_spec.rb b/spec/jobs/add_to_profile_photos_job_spec.rb new file mode 100644 index 000000000..a677eac1d --- /dev/null +++ b/spec/jobs/add_to_profile_photos_job_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe AddToProfilePhotosJob, type: :job do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/jobs/fix_counters_job_spec.rb b/spec/jobs/fix_counters_job_spec.rb new file mode 100644 index 000000000..df9a66f82 --- /dev/null +++ b/spec/jobs/fix_counters_job_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe FixCountersJob, type: :job do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/jobs/process_image_job_spec.rb b/spec/jobs/process_image_job_spec.rb new file mode 100644 index 000000000..088b2e6d4 --- /dev/null +++ b/spec/jobs/process_image_job_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe ProcessImageJob, type: :job do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/mailers/.keep b/spec/mailers/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/spec/mailers/previews/profile_mailer_preview.rb b/spec/mailers/previews/profile_mailer_preview.rb new file mode 100644 index 000000000..eb6ebface --- /dev/null +++ b/spec/mailers/previews/profile_mailer_preview.rb @@ -0,0 +1,4 @@ +# Preview all emails at http://localhost:3000/rails/mailers/profile_mailer +class ProfileMailerPreview < ActionMailer::Preview + +end diff --git a/spec/mailers/profile_mailer_spec.rb b/spec/mailers/profile_mailer_spec.rb new file mode 100644 index 000000000..925e534eb --- /dev/null +++ b/spec/mailers/profile_mailer_spec.rb @@ -0,0 +1,5 @@ +require "rails_helper" + +RSpec.describe ProfileMailerMailer, type: :mailer do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/.keep b/spec/models/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/spec/models/bio_spec.rb b/spec/models/bio_spec.rb new file mode 100644 index 000000000..e2cd115f1 --- /dev/null +++ b/spec/models/bio_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +describe Bio do + it { is_expected.to belong_to(:profile) } +end diff --git a/spec/models/friend_request_spec.rb b/spec/models/friend_request_spec.rb new file mode 100644 index 000000000..dfbebbe72 --- /dev/null +++ b/spec/models/friend_request_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' +describe FriendRequest do + + let(:user){build(:user)} + let(:user2){build(:user)} + let(:user3){build(:user)} + let(:request){build(:friend_request, user: user, request: user2)} + let(:request_dup){build(:friend_request, user: user, request: user2)} + + it "validates users can only request the same friend once" do + request.save + expect(request_dup).to_not be_valid + request_dup.update_attributes(user: user3, request: user2) + expect(request_dup).to be_valid + request_dup.update_attributes(user: user2, request: user) + expect(request_dup).to be_valid + end + + it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:request) } +end diff --git a/spec/models/friendify_spec.rb b/spec/models/friendify_spec.rb new file mode 100644 index 000000000..e74867bd1 --- /dev/null +++ b/spec/models/friendify_spec.rb @@ -0,0 +1,64 @@ +require 'rails_helper' + +describe Friendify do + + let(:friendify){ Friendify } + let(:user){build(:user)} + let(:friend){build(:user)} + + before do + user.save + friend.save + end + + describe '.friendship' do + + it "processes a new friend request and returns a status message" do + expect(friendify.friendship(user, friend)[0]).to eq(:success) + expect(friend.friend_requests.length).to be > 0 + end + + it "rejects an already sent friend request and returns a status message" do + friendify.friendship(user, friend) + requests = friend.friend_requests.length + expect(friendify.friendship(user, friend)[0]).to eq(:danger) + expect(friend.friend_requests.length).to eq(requests) + end + + it "rejects a self friend request and returns a status message" do + requests = user.friend_requests.length + expect(friendify.friendship(user, user)[0]).to eq(:danger) + expect(user.friend_requests.length).to eq(requests) + end + + it "creates a friendship when a friend request is reciprocated and returns a status message" do + friends = user.friends.size + friendify.friendship(user, friend) + expect(friendify.friendship(friend, user)[0]).to eq(:success) + expect(user.friends.size).to be > friends + expect(friend.friends).to include(user) + end + + it "rejects friend request if users are already friends and returns a status message" do + friendify.friendship(user, friend) + friendify.friendship(friend, user) + friends = user.friends.size + expect(friendify.friendship(user, friend)[0]).to eq(:danger) + expect(user.friends.size).to eq(friends) + end + + end + + describe '.clear_request' do + + it "cancels a friend request and returns a status message" do + friendify.friendship(user, friend) + requests = friend.friend_requests.length + friendify.clear_request(user, friend) + friend.reload + expect(friend.friend_requests.length).to be < requests + end + + end + +end diff --git a/spec/models/friends_user_spec.rb b/spec/models/friends_user_spec.rb new file mode 100644 index 000000000..c25ad0369 --- /dev/null +++ b/spec/models/friends_user_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' +describe FriendsUser do + it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:friend) } +end diff --git a/spec/models/gallery_spec.rb b/spec/models/gallery_spec.rb new file mode 100644 index 000000000..eada10f0c --- /dev/null +++ b/spec/models/gallery_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +describe Gallery do + + let(:user){build(:user)} + let(:user2){build(:user)} + let(:gallery){build(:gallery, user: user, title: "test")} + let(:gallery_dup){build(:gallery, user: user, title: "test")} + + it "validates unique gallery titles per user" do + gallery.save + expect(gallery_dup).to_not be_valid + gallery_dup.update_attributes(user: user, title: "not dup") + expect(gallery_dup).to be_valid + gallery_dup.update_attributes(user: user2, title: "test") + expect(gallery_dup).to be_valid + end + + it { is_expected.to belong_to(:user) } + it { is_expected.to have_many(:images) } +end diff --git a/spec/models/image_spec.rb b/spec/models/image_spec.rb new file mode 100644 index 000000000..37ecebb90 --- /dev/null +++ b/spec/models/image_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' + +describe Image do + + it { is_expected.to belong_to(:gallery) } +end diff --git a/spec/models/like_spec.rb b/spec/models/like_spec.rb new file mode 100644 index 000000000..be3785dcf --- /dev/null +++ b/spec/models/like_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +describe Like do + let(:user){build(:user)} + let(:post){build(:post)} + let(:user2){build(:user)} + let(:post2){build(:post)} + let(:like){build(:like, user: user, post: post)} + let(:like_dup){build(:like, user: user, post: post)} + + it "validates unique users per post" do + like.save + expect(like_dup).to_not be_valid + like_dup.update_attributes(user: user, post: post2) + expect(like_dup).to be_valid + like_dup.update_attributes(user: user2, post: post) + expect(like_dup).to be_valid + end + + it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:post) } +end diff --git a/spec/models/notice_spec.rb b/spec/models/notice_spec.rb new file mode 100644 index 000000000..71510b2bb --- /dev/null +++ b/spec/models/notice_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Notice, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb new file mode 100644 index 000000000..a20344377 --- /dev/null +++ b/spec/models/post_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +describe Post do + + it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:post) } + it { is_expected.to have_many(:likes) } + it { is_expected.to have_many(:liked_users) } + it { is_expected.to have_many(:comments) } + + it "includes User with all posts to avoid N+1" do + create(:post) + expect { Post.first.user }.to_not exceed_query_limit(2) + end +end diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb new file mode 100644 index 000000000..0dac5213c --- /dev/null +++ b/spec/models/profile_spec.rb @@ -0,0 +1,71 @@ +require 'rails_helper' + +describe Profile do + + let(:profile){build(:profile)} + + it "requires a first_name" do + profile.update_attributes(first_name: nil) + expect(profile).to_not be_valid + end + + it "requires a last_name" do + profile.update_attributes(last_name: nil) + expect(profile).to_not be_valid + end + + it "requires a birthday" do + profile.update_attributes(birthday: nil) + expect(profile).to_not be_valid + end + + it "requires a gender" do + profile.update_attributes(gender: nil) + expect(profile).to_not be_valid + end + + it "does not discriminate gender roles" do + profile.update_attributes(gender: "Androgyne") + expect(profile).to be_valid + end + + it "validates a phone number min and max length are within international formating standards" do + profile.update_attributes(phone: "#{'1'*4}") + expect(profile).to be_valid + profile.update_attributes(phone: "#{'1'*30}") + expect(profile).to be_valid + profile.update_attributes(phone: "123") + expect(profile).to_not be_valid + profile.update_attributes(phone: "#{'1'*31}") + expect(profile).to_not be_valid + end + + it "validates a phone has at least one number" do + profile.update_attributes(phone: "asdf") + expect(profile).to_not be_valid + end + + it "allows your choice of formating your phone number" do + profile.update_attributes(phone: "+61 13 15 17") + expect(profile).to be_valid + end + + it "capitalizes First and Last Name before saving" do + profile.update_attributes(first_name: "foo", last_name: "bar") + expect(profile.first_name).to eq("Foo") + expect(profile.last_name).to eq("Bar") + end + + describe ".genders" do + it "returns an array of genders for select options" do + expect(Profile.genders).to be_a(Array) + expect(Profile.genders[0]).to eq(["Male", "Male"]) + end + end + + it { is_expected.to belong_to(:user) } + it { is_expected.to have_one(:bio) } + it { is_expected.to have_one(:profile_gallery) } + it { is_expected.to have_many(:images) } + it { is_expected.to belong_to(:profile_img) } +end diff --git a/spec/models/sessionizer_spec.rb b/spec/models/sessionizer_spec.rb new file mode 100644 index 000000000..1cda66245 --- /dev/null +++ b/spec/models/sessionizer_spec.rb @@ -0,0 +1,47 @@ +require 'rails_helper' + +describe Sessionizer do + let(:user){build(:user)} + let(:bad_params){{password: "23456Yuiopasdf"}} + let(:sess){ Sessionizer.validate_credentials(user, "!23456Yuiopasdf") } + let(:sess_bad){ Sessionizer.validate_credentials(user, "23456Yuiopasdf") } + + before do + user + end + + describe "#validate_credentials" do + it "logs in a user with valid credentials" do + expect(sess).to eq("valid") + end + + it "rejects a user with invalid credentials" do + expect(sess_bad).to eq("invalid") + end + + it "locks you out if you have 5 incorrect logins within an hour" do + 5.times do + Sessionizer.validate_credentials(user, "!23456Yuiopasdfa") + user.reload + end + + expect(sess_bad).to eq("locked") + end + + it "lockout count resets after an hour" do + 3.times do + Sessionizer.validate_credentials(user, "!23456Yuiopasdfa") + end + user.last_attempt = 1.hour.ago + expect(sess_bad).to eq("invalid") + expect(user.failed).to eq(1) + + 5.times do + Sessionizer.validate_credentials(user, "!23456Yuiopasdfa") + end + user.last_attempt = 1.hour.ago + expect(sess_bad).to eq("invalid") + end + end + +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb new file mode 100644 index 000000000..d6a78a437 --- /dev/null +++ b/spec/models/user_spec.rb @@ -0,0 +1,97 @@ +require 'rails_helper' + +describe User do + + let(:user){build(:user)} + let(:full_pass){build(:user, password: "#{'a'*10}1A")} + let(:short_pass){build(:user, password: "#{'a'*9}1A")} + let(:numberless_pass){build(:user, password: "#{'a'*10}A")} + let(:no_cap_pass){build(:user, password: "#{'a'*12}1")} + let(:crazy_pass){build(:user, password: "!@$%^&*()\##{'a'*12}A1")} + let(:dup_email){build(:user, email: user.email)} + let(:double_a_email){build(:user, email: "22@@a.com")} + let(:no_domain_email){build(:user, email: "22@.com")} + let(:no_u_email){build(:user, email: "@a.com")} + let(:no_tld_email){build(:user, email: "22@a.")} + + it "does not allow nil passwords on create" do + user.update_attributes(password: nil) + expect(user).to_not be_valid + end + + it "requires a strong password" do + expect(full_pass).to be_valid + expect(short_pass).to_not be_valid + expect(numberless_pass).to_not be_valid + expect(no_cap_pass).to_not be_valid + end + + it "allows any character in the password" do + expect(crazy_pass).to be_valid + end + + it "requires a unique email" do + user.save + expect(dup_email).to_not be_valid + end + + it "skips password validation if nil on update" do + user.save + user.update_attributes(email: "asdf@asdf.com") + expect(user).to be_valid + user.update_attributes(email: "asdf@asdf.com", password: "f") + expect(user).to_not be_valid + end + + it "requires emails to be emails" do + expect(double_a_email).to_not be_valid + expect(no_domain_email).to_not be_valid + expect(no_u_email).to_not be_valid + expect(no_tld_email).to_not be_valid + end + + it 'downcases all emails' do + user.update_attributes(email: "AsDf@ASDF.coM") + expect(user.email).to eq("asdf@asdf.com") + end + + it 'creates a profile images gallery for new users' do + expect(user.galleries.size).to eq(0) + user.save + expect(user.galleries.size).to eq(1) + user.update_attributes(email: "AsDf@ASDF.coM") + expect(user.galleries.size).to eq(1) + end + + describe "#regenerate_auth_token" do + it "destroys the old token and generates a new one" do + user.save + user.regenerate_auth_token + old_token = user.token + user.regenerate_auth_token + expect(user.token).to_not eq(old_token) + end + end + + describe "#destroy_token" do + it "destroys the old token" do + user.save + user.regenerate_auth_token + user.destroy_token + expect(user.token).to be_nil + end + end + + it { is_expected.to have_secure_password } + it { is_expected.to have_one(:profile) } + it { is_expected.to have_many(:f_requests) } + it { is_expected.to have_many(:friend_requests) } + it { is_expected.to have_many(:r_friends) } + it { is_expected.to have_many(:requested_friends) } + it { is_expected.to have_many(:friend_requests) } + it { is_expected.to have_many(:friends) } + it { is_expected.to have_many(:posts) } + it { is_expected.to have_many(:likes) } + it { is_expected.to have_many(:liked_posts) } + it { is_expected.to have_many(:galleries) } +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 000000000..487191989 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,68 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +ENV['RAILS_ENV'] ||= 'test' +require File.expand_path('../../config/environment', __FILE__) +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require 'spec_helper' +require 'rspec/rails' +require 'factory_girl_rails' +require 'capybara/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } + +# Checks for pending migration and applies them before tests are run. +# If you are not using ActiveRecord, you can remove this line. +ActiveRecord::Migration.maintain_test_schema! + +RSpec.configure do |config| + config.include Rails.application.routes.url_helpers + config.alias_it_should_behave_like_to :it_has_behavior, 'has behavior:' + + config.include FactoryGirl::Syntax::Methods + + config.include LoginMacros + config.include PostMacros + config.include UserMacros + + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, :type => :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://relishapp.com/rspec/rspec-rails/docs + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 000000000..8f698be46 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,99 @@ +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# The `.rspec` file also contains a few flags that are not defaults but that +# users commonly want. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = 'doc' + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/spec/support/examples/post_requests.rb b/spec/support/examples/post_requests.rb new file mode 100644 index 000000000..fb44c6ac2 --- /dev/null +++ b/spec/support/examples/post_requests.rb @@ -0,0 +1,97 @@ +shared_examples_for "valid_create" do |model, redirect, amount| + it "should be successful" do + expect{process :create, params: {model => attributes_for(model)} }.to change(model.to_s.classify.constantize, :count).by(amount) + end + + it "should set success flash messages" do + process :create, params: {model => attributes_for(model)} + + expect(flash[:success]).to_not be_nil + end + + it "should redirect to #{redirect}" do + process :create, params: {model => attributes_for(model)} + + expect(response).to redirect_to(send(redirect)) + end +end + +shared_examples_for "invalid_create" do |model, params| + it "should be rejected" do + expect{process :create, params: params}.to_not change(model.to_s.classify.constantize, :count) + end + + it "should set danger flash messages" do + process :create, params: params + + expect(flash[:danger]).to_not be_nil + end + + it "should not redirect" do + process :create, params: params + + assert_response :success + end +end + +shared_examples_for "valid_update" do |model, params, redirect| + + it "should be successful" do + process :update, params: { id: checked.id, model => params } + checked.reload + params.keys.each do |k| + next if k.to_s.match(/password/) + expect(checked.send(k)).to eq(params[k]) + end + end + + it "should set success flash messages" do + process :update, params: {id: checked.id, model => params} + + expect(flash[:success]).to_not be_nil + end + + it "should redirect to #{redirect}" do + process :update, params: { id: checked.id, model => params } + + expect(response).to redirect_to(send(redirect, checked.id)) + end +end + +shared_examples_for "invalid_update" do |model, params| + it "should be rejected" do + before = checked + process :update, params: { id: checked.id, model => params } + end + + it "should set danger flash messages" do + process :update, params: {id: checked.id, model => params } + + expect(flash[:danger]).to_not be_nil + end + + it "should not redirect" do + process :update, params: { id: checked.id, model => params } + + assert_response :success + end +end + +shared_examples_for "unauthorized_update" do |model, params| + it "should be rejected" do + before = checked + process :update, params: { id: checked.id, model => params } + end + + it "should set danger flash messages" do + process :update, params: {id: checked.id, model => params } + + expect(flash[:danger]).to_not be_nil + end + + it "should redirect" do + process :update, params: { id: checked.id, model => params } + + assert_response :redirect + end +end diff --git a/spec/support/login_macros.rb b/spec/support/login_macros.rb new file mode 100644 index 000000000..7e3cdd853 --- /dev/null +++ b/spec/support/login_macros.rb @@ -0,0 +1,14 @@ +module LoginMacros + def sign_in(user) + visit root_path + within "#login-dp" do + fill_in 'Email', with: user.email + fill_in 'Password', with: user.password + click_button 'Log In' + end + end + + def sign_out + visit logout_path + end +end diff --git a/spec/support/matchers/exceed_query_limit.rb b/spec/support/matchers/exceed_query_limit.rb new file mode 100644 index 000000000..0ea731c19 --- /dev/null +++ b/spec/support/matchers/exceed_query_limit.rb @@ -0,0 +1,17 @@ +RSpec::Matchers.define :exceed_query_limit do |expected| + supports_block_expectations + + match do |block| + query_count(&block) > expected + end + + failure_message_when_negated do |actual| + "Expected to run maximum #{expected} queries, got #{@counter.query_count}" + end + + def query_count(&block) + @counter = ActiveRecord::QueryCounter.new + ActiveSupport::Notifications.subscribed(@counter.to_proc, 'sql.active_record', &block) + @counter.query_count + end +end diff --git a/spec/support/post_macros.rb b/spec/support/post_macros.rb new file mode 100644 index 000000000..7c3ea095b --- /dev/null +++ b/spec/support/post_macros.rb @@ -0,0 +1,7 @@ +module PostMacros + def create_new_post + visit root_path + fill_in "post_body", with: "Test New Post" + click_button "Post" + end +end diff --git a/spec/support/query_counter.rb b/spec/support/query_counter.rb new file mode 100644 index 000000000..5bf273f7b --- /dev/null +++ b/spec/support/query_counter.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class QueryCounter + attr_reader :query_count + + def initialize + @query_count = 0 + end + + def to_proc + lambda(&method(:callback)) + end + + def callback(name, start, finish, message_id, values) + @query_count += 1 unless %w(CACHE SCHEMA).include?(values[:name]) + end + end +end diff --git a/spec/support/user_macros.rb b/spec/support/user_macros.rb new file mode 100644 index 000000000..0a31cc2b9 --- /dev/null +++ b/spec/support/user_macros.rb @@ -0,0 +1,20 @@ +module UserMacros + def create_session(user) + session[:user_id] = Crypt.encrypt(user.id) + end + + def create_friendship(user, friend) + create(:friends_user, user: user, friend: friend) + create(:friends_user, user: friend, friend: user) + end + + def destroy_friendship(user, friend) + FriendsUser. + where(user_id: user.id, friend_id: friend.id). + or( + FriendsUser. + where(user_id: friend.id, friend_id: user.id) + ). + destroy_all + end +end diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/assets/javascripts/.keep b/vendor/assets/javascripts/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/assets/stylesheets/.keep b/vendor/assets/stylesheets/.keep new file mode 100644 index 000000000..e69de29bb