diff --git a/.capistrano/metrics b/.capistrano/metrics new file mode 100644 index 00000000..94a4335e --- /dev/null +++ b/.capistrano/metrics @@ -0,0 +1 @@ +full \ No newline at end of file diff --git a/Capfile b/Capfile new file mode 100644 index 00000000..6e64843e --- /dev/null +++ b/Capfile @@ -0,0 +1,24 @@ +# Load DSL and set up stages +require 'capistrano/setup' + +# Include default deployment tasks +require 'capistrano/deploy' + +# Include tasks from other gems included in your Gemfile +# +# For documentation on these, see for example: +# +# https://github.com/capistrano/rvm +# https://github.com/capistrano/rbenv +# https://github.com/capistrano/chruby +# https://github.com/capistrano/bundler +# https://github.com/capistrano/rails +# https://github.com/capistrano/passenger +# +require 'capistrano/rvm' +require 'capistrano/bundler' +require 'capistrano/rails' +require 'capistrano/console' + +# Load custom tasks from `lib/capistrano/tasks' if you have any defined +Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } diff --git a/Gemfile b/Gemfile index 3db7a191..af1d4a74 100644 --- a/Gemfile +++ b/Gemfile @@ -48,6 +48,11 @@ gem 'foreman' group :development do gem 'spring' + + # Capistrano for deployment + gem 'capistrano', '~> 3.1' + gem 'capistrano-rvm', group: :rvm + gem 'capistrano-rails', '~> 1.1' end group :production do diff --git a/Gemfile.lock b/Gemfile.lock index 438e7b2f..c19e111c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -44,6 +44,21 @@ GEM railties (>= 3.1) buftok (0.2.0) builder (3.2.2) + capistrano (3.3.5) + capistrano-stats (~> 1.1.0) + i18n + rake (>= 10.0.0) + sshkit (~> 1.3) + capistrano-bundler (1.1.3) + capistrano (~> 3.1) + sshkit (~> 1.2) + capistrano-rails (1.1.2) + capistrano (~> 3.1) + capistrano-bundler (~> 1.1) + capistrano-rvm (0.1.2) + capistrano (~> 3.0) + sshkit (~> 1.2) + capistrano-stats (1.1.1) capybara (2.4.4) mime-types (>= 1.16) nokogiri (>= 1.3.3) @@ -60,6 +75,7 @@ GEM coffee-script-source execjs coffee-script-source (1.8.0) + colorize (0.7.5) connection_pool (2.1.0) daemons (1.1.9) database_cleaner (1.3.0) @@ -131,6 +147,9 @@ GEM mysql2 (0.3.17) naught (1.0.0) nested_form (0.3.2) + net-scp (1.2.1) + net-ssh (>= 2.6.5) + net-ssh (2.9.1) nokogiri (1.6.5) mini_portile (~> 0.6.0) nprogress-rails (0.1.6.3) @@ -254,6 +273,10 @@ GEM actionpack (>= 3.0) activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) + sshkit (1.6.1) + colorize (>= 0.7.0) + net-scp (>= 1.1.2) + net-ssh (>= 2.8.0) thin (1.6.3) daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0) @@ -302,6 +325,9 @@ DEPENDENCIES bootstrap-sass (~> 3.2.0.1) bootstrap_form bootswatch-rails + capistrano (~> 3.1) + capistrano-rails (~> 1.1) + capistrano-rvm capybara coffee-rails (~> 4.1.0) database_cleaner diff --git a/Rakefile b/Rakefile index 83db07a0..7f7ed899 100644 --- a/Rakefile +++ b/Rakefile @@ -79,4 +79,22 @@ namespace :justask do puts "#{sprintf "%3d", u.id}. #{u.screen_name}" end end + + desc "Fixes the notifications" + task fix_notifications: :environment do + format = '%t (%c/%C) [%b>%i] %e' + total = Notification.count + progress = ProgressBar.create title: 'Processing notifications', format: format, starting_at: 0, total: total + destroyed_count = 0 + + Notification.all.each do |n| + if n.target.nil? + n.destroy + destroyed_count += 1 + end + progress.increment + end + + puts "Purged #{destroyed_count} dead notifications." + end end diff --git a/app/assets/javascripts/application.js.erb.coffee b/app/assets/javascripts/application.js.erb.coffee index 3693840e..f37ea0b7 100644 --- a/app/assets/javascripts/application.js.erb.coffee +++ b/app/assets/javascripts/application.js.erb.coffee @@ -6,6 +6,7 @@ #= require nprogress #= require nprogress-turbolinks #= require growl +#= require cheet #= require_tree . NProgress.configure diff --git a/app/assets/javascripts/inbox.coffee b/app/assets/javascripts/inbox.coffee index 87bd87e9..32abdec6 100644 --- a/app/assets/javascripts/inbox.coffee +++ b/app/assets/javascripts/inbox.coffee @@ -8,18 +8,51 @@ success: (data, status, jqxhr) -> if data.success ($ "div#entries").prepend(data.render) # TODO: slideDown or something + # GitHub issue #26: + del_all_btn = ($ "button#ib-delete-all") + del_all_btn.removeAttr 'disabled' + del_all_btn[0].dataset.ibCount = 1 error: (jqxhr, status, error) -> console.log jqxhr, status, error showNotification "An error occurred, a developer should check the console for details", false complete: (jqxhr, status) -> btn.button "reset" + +($ document).on "click", "button#ib-delete-all", -> + btn = ($ this) + count = btn[0].dataset.ibCount + if confirm "Really delete #{count} questions?" + btn.button "loading" + succ = no + $.ajax + url: '/ajax/delete_all_inbox' + type: 'POST' + dataType: 'json' + success: (data, status, jqxhr) -> + if data.success + succ = yes + entries = ($ "div#entries") + entries.slideUp 400, -> + entries.html("Nothing to see here.") + entries.fadeIn() + error: (jqxhr, status, error) -> + console.log jqxhr, status, error + showNotification "An error occurred, a developer should check the console for details", false + complete: (jqxhr, status) -> + btn.button "reset" + if succ + btn.attr "disabled", "disabled" # this doesn't really work like I wanted it to… + btn[0].dataset.ibCount = 0 + + $(document).on "keydown", "textarea[name=ib-answer]", (evt) -> iid = $(this)[0].dataset.id if evt.keyCode == 13 and evt.ctrlKey # trigger warning: $("button[name=ib-answer][data-ib-id=#{iid}]").trigger 'click' + $(document).on "click", "button[name=ib-answer]", -> btn = $(this) btn.button "loading" @@ -48,6 +81,7 @@ $(document).on "click", "button[name=ib-answer]", -> btn.button "reset" $("textarea[name=ib-answer][data-id=#{iid}]").removeAttr "readonly" + $(document).on "click", "button[name=ib-destroy]", -> if confirm 'Are you sure?' btn = $(this) diff --git a/app/assets/javascripts/memes.coffee b/app/assets/javascripts/memes.coffee new file mode 100644 index 00000000..873d34d0 --- /dev/null +++ b/app/assets/javascripts/memes.coffee @@ -0,0 +1,3 @@ +cheet 'up up down down left right left right b a', -> + ($ "body").addClass 'fa-spin' + ($ "p.answerbox--question-text").each (i) -> ($ this).html ":^)" \ No newline at end of file diff --git a/app/assets/stylesheets/base.css.scss b/app/assets/stylesheets/base.css.scss index c2f74b76..8bb46eb2 100644 --- a/app/assets/stylesheets/base.css.scss +++ b/app/assets/stylesheets/base.css.scss @@ -53,4 +53,8 @@ body { .smiles { margin-bottom: 7px; +} + +.j2-lh { + color: #fff; } \ No newline at end of file diff --git a/app/assets/stylesheets/scss/answerbox.scss b/app/assets/stylesheets/scss/answerbox.scss index 45524a49..af1595fe 100644 --- a/app/assets/stylesheets/scss/answerbox.scss +++ b/app/assets/stylesheets/scss/answerbox.scss @@ -1,4 +1,4 @@ -.answerbox .text-muted a, .answerbox .text-muted a:hover { +.text-muted a, .answerbox .text-muted a:hover { color: $gray-dark; text-decoration: none; } diff --git a/app/assets/stylesheets/scss/user.scss b/app/assets/stylesheets/scss/user.scss index 8e51907e..1c04aa9b 100644 --- a/app/assets/stylesheets/scss/user.scss +++ b/app/assets/stylesheets/scss/user.scss @@ -33,4 +33,33 @@ height: 40vh; background-color: darken($navbar-inverse-bg, 10%); background-size: cover; +} + +.profile--panel .panel-heading { + color: $brand-primary; + border-bottom: 2px solid $brand-primary; + background-color: #fff; + text-transform: uppercase; +} + +.profile--panel .panel-body { + padding-top: 0px; +} + +.inbox--panel .panel-heading { + color: $brand-info; + border-bottom: 2px solid $brand-info; + background-color: #fff; + text-transform: uppercase; +} + +.warning--panel .panel-heading { + color: $brand-danger; + border-bottom: 2px solid $brand-danger; + background-color: #fff; + text-transform: uppercase; +} + +.profile--follow-btn { + margin-top: 5px; } \ No newline at end of file diff --git a/app/controllers/ajax/inbox_controller.rb b/app/controllers/ajax/inbox_controller.rb index 88393659..21ff83dc 100644 --- a/app/controllers/ajax/inbox_controller.rb +++ b/app/controllers/ajax/inbox_controller.rb @@ -85,4 +85,20 @@ class Ajax::InboxController < ApplicationController @message = "Successfully deleted question." @success = true end + + def remove_all + begin + Inbox.where(user: current_user).each { |i| i.remove } + rescue + @status = :err + @message = "An error occurred" + @success = false + return + end + + @status = :okay + @message = "Successfully deleted questions." + @success = true + render 'ajax/inbox/remove' + end end diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb index 32d0229f..cc560d54 100644 --- a/app/controllers/user_controller.rb +++ b/app/controllers/user_controller.rb @@ -1,6 +1,6 @@ class UserController < ApplicationController def show - @user = User.where('LOWER(screen_name) = ?', params[:username].downcase).first + @user = User.where('LOWER(screen_name) = ?', params[:username].downcase).first! @answers = @user.answers.reverse_order.paginate(page: params[:page]) respond_to do |format| format.html @@ -23,7 +23,7 @@ class UserController < ApplicationController def followers @title = 'Followers' - @user = User.where('LOWER(screen_name) = ?', params[:username].downcase).first + @user = User.where('LOWER(screen_name) = ?', params[:username].downcase).first! @users = @user.followers.reverse_order.paginate(page: params[:page]) @type = :friend render 'show_follow' @@ -31,9 +31,15 @@ class UserController < ApplicationController def friends @title = 'Following' - @user = User.where('LOWER(screen_name) = ?', params[:username].downcase).first + @user = User.where('LOWER(screen_name) = ?', params[:username].downcase).first! @users = @user.friends.reverse_order.paginate(page: params[:page]) @type = :friend render 'show_follow' end + + def questions + @title = 'Questions' + @user = User.where('LOWER(screen_name) = ?', params[:username].downcase).first! + @questions = @user.questions.where(author_is_anonymous: false).reverse_order.paginate(page: params[:page]) + end end diff --git a/app/models/user.rb b/app/models/user.rb index b89dddcd..45b3626a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -33,6 +33,15 @@ class User < ActiveRecord::Base # validates :website, format: { with: WEBSITE_REGEX } + before_save do + self.display_name = 'WRYYYYYYYY' if display_name == 'Dio Brando' + self.website = if website.match %r{\Ahttps?://} + website + else + "http://#{website}" + end unless website.blank? + end + def login=(login) @login = login end diff --git a/app/views/inbox/show.html.haml b/app/views/inbox/show.html.haml index 7506b491..2838789d 100644 --- a/app/views/inbox/show.html.haml +++ b/app/views/inbox/show.html.haml @@ -1,19 +1,26 @@ .container.j2-page - = render 'layouts/messages' - .alert.alert-info - .row - .col-md-9.col-sm-8.col-xs-12 - Out of questions? - .col-md-3.col-sm-5.col-xs-12 - %button.btn.btn-block.btn-info{type: :button, id: 'ib-generate-question'} Get new question + .row + .col-md-3.col-xs-12.col-sm-3 + .panel.panel-default.inbox--panel + .panel-heading + %h3.panel-title Out of questions? + .panel-body + %button.btn.btn-block.btn-info{type: :button, id: 'ib-generate-question'} Get new question + %a.btn.btn-block.btn-primary{target: '_blank', href: "https://twitter.com/intent/tweet?text=Ask%20me%20anything%21&url=#{show_user_profile_url(current_user.screen_name)}"} Share on Twitter + .panel.panel-default.warning--panel + .panel-heading + %h3.panel-title Actions + .panel-body + %button.btn.btn-block.btn-danger{type: :button, id: 'ib-delete-all', disabled: (@inbox.empty? ? 'disabled' : nil), data: { ib_count: @inbox.count }} Delete all questions + .col-md-9.col-xs-12.col-sm-9 + = render 'layouts/messages' + #entries + - @inbox.each do |i| + = render 'inbox/entry', i: i - #entries - - @inbox.each do |i| - = render 'inbox/entry', i: i + - if @inbox.empty? - - if @inbox.empty? - - Nothing to see here. + Nothing to see here. = render "shared/links" - @inbox.update_all(new: false) \ No newline at end of file diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index 977f37c9..32bab7a8 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -4,7 +4,7 @@ %button.navbar-toggle{"data-target" => "#j2-main-navbar-collapse", "data-toggle" => "collapse", type: "button"} %span.sr-only Toggle navigation - if user_signed_in? - - unless inbox_count.nil? + - unless inbox_count.nil? or notification_count.nil? %span.icon-bar.navbar--inbox-animation %span.icon-bar.navbar--inbox-animation %span.icon-bar.navbar--inbox-animation diff --git a/app/views/shared/_answerbox.html.haml b/app/views/shared/_answerbox.html.haml index 791f7e7f..8a53cdaa 100644 --- a/app/views/shared/_answerbox.html.haml +++ b/app/views/shared/_answerbox.html.haml @@ -9,7 +9,11 @@ %h6.text-muted.media-heading.answerbox--question-user = user_screen_name a.question.user, a.question.author_is_anonymous asked - = time_ago_in_words(a.question.created_at) + - if @user.nil? or a.question.author_is_anonymous + = time_ago_in_words(a.question.created_at) + - else + %a{href: show_user_question_path(a.question.user.screen_name, a.question.id)} + = time_ago_in_words(a.question.created_at) ago - unless a.question.author_is_anonymous - if a.question.answer_count > 1 @@ -45,6 +49,6 @@ .row .col-md-6.col-md-offset-6.col-sm-8.col-sm-offset-4.col-xs-6.col-xs-offset-6.text-right = render 'shared/answerbox_buttons', a: a - .panel-footer{id: "ab-comments-section-#{a.id}", style: 'display: none'} + .panel-footer{id: "ab-comments-section-#{a.id}", style: @display_all.nil? ? 'display: none' : nil } %div{id: "ab-smiles-#{a.id}"}= render 'shared/smiles', a: a - %div{id: "ab-comments-#{a.id}"}= render 'shared/comments', a: a + %div{id: "ab-comments-#{a.id}"}= render 'shared/comments', a: a \ No newline at end of file diff --git a/app/views/shared/_answerbox_buttons.html.haml b/app/views/shared/_answerbox_buttons.html.haml index d2b05016..924036ae 100644 --- a/app/views/shared/_answerbox_buttons.html.haml +++ b/app/views/shared/_answerbox_buttons.html.haml @@ -12,9 +12,10 @@ %button.btn.btn-info.btn-sm{type: :button, name: 'ab-smile', data: { a_id: a.id, action: :smile }} %i.fa.fa-smile-o %span{id: "ab-smile-count-#{a.id}"}= a.smile_count -%button.btn.btn-primary.btn-sm{type: :button, name: 'ab-comments', data: { a_id: a.id, state: :hidden }} - %i.fa.fa-comments - %span{id: "ab-comment-count-#{a.id}"}= a.comment_count +- unless @display_all + %button.btn.btn-primary.btn-sm{type: :button, name: 'ab-comments', data: { a_id: a.id, state: :hidden }} + %i.fa.fa-comments + %span{id: "ab-comment-count-#{a.id}"}= a.comment_count - if privileged? a.user %button.btn.btn-danger.btn-sm{name: 'ab-destroy', data: { a_id: a.id }} %i.fa.fa-trash-o \ No newline at end of file diff --git a/app/views/shared/_question.html.haml b/app/views/shared/_question.html.haml new file mode 100644 index 00000000..d2b79ee3 --- /dev/null +++ b/app/views/shared/_question.html.haml @@ -0,0 +1,16 @@ +.panel.panel-default + .panel-body + .media + .media-body + %h6.media-heading.text-muted.answerbox--question-user + = user_screen_name q.user + asked + %a{href: show_user_question_path(q.user.screen_name, q.id)} + = time_ago_in_words(q.created_at) + ago + - if q.answer_count > 1 + · + %a{href: show_user_question_path(q.user.screen_name, q.id)} + #{q.answer_count} answers + %p.answerbox--question-text + = q.content \ No newline at end of file diff --git a/app/views/shared/_questionbox.html.haml b/app/views/shared/_questionbox.html.haml index dfaff539..e9d066b7 100644 --- a/app/views/shared/_questionbox.html.haml +++ b/app/views/shared/_questionbox.html.haml @@ -23,7 +23,7 @@ - unless user_signed_in? #question-box-promote.row{:style => "display: none;"} .row - .col-xs-12 + .col-xs-12.text-center %strong Your question has been sent. .row .col-sm-1 diff --git a/app/views/user/_actions.html.haml b/app/views/user/_actions.html.haml index 7a6b277d..730c1e1d 100644 --- a/app/views/user/_actions.html.haml +++ b/app/views/user/_actions.html.haml @@ -1,11 +1,11 @@ - if user_signed_in? - type ||= :nil - if user == current_user - %a.btn.btn-default.btn-block{href: edit_user_profile_path} Edit profile + %a.btn.btn-default.btn-block.profile--follow-btn{href: edit_user_profile_path} Edit profile - else - if current_user.following? user - %button#editprofile.btn.btn-default.btn-block{type: :button, name: 'user-action', data: { action: :unfollow, type: type, target: user.screen_name }} + %button#editprofile.btn.btn-default.btn-block.profile--follow-btn{type: :button, name: 'user-action', data: { action: :unfollow, type: type, target: user.screen_name }} Unfollow - else - %button#editprofile.btn.btn-primary.btn-block{type: :button, name: 'user-action', data: { action: :follow, type: type, target: user.screen_name }} + %button#editprofile.btn.btn-primary.btn-block.profile--follow-btn{type: :button, name: 'user-action', data: { action: :follow, type: type, target: user.screen_name }} Follow \ No newline at end of file diff --git a/app/views/user/_profile_info.html.haml b/app/views/user/_profile_info.html.haml index de1376a0..a9c59c48 100644 --- a/app/views/user/_profile_info.html.haml +++ b/app/views/user/_profile_info.html.haml @@ -23,20 +23,5 @@ %p.profile--text %i.fa.fa-location-arrow = @user.location - .row - %a{href: show_user_followers_path(@user.screen_name)} - .col-md-6.col-sm-6.col-xs-6 - %h4.entry-text#follower-count= @user.follower_count - %h6.entry-subtext Followers - %a{href: show_user_friends_path(@user.screen_name)} - .col-md-6.col-sm-6.col-xs-6 - %h4.entry-text#friend-count= @user.friend_count - %h6.entry-subtext Following - .row - .col-md-6.col-sm-6.col-xs-6 - %h4.entry-text#asked-count= @user.asked_count - %h6.entry-subtext Questions - .col-md-6.col-sm-6.col-xs-6 - %h4.entry-text#answered-count= @user.answered_count - %h6.entry-subtext Answers - = render 'user/actions', user: @user, type: :follower \ No newline at end of file + = render 'user/actions', user: @user, type: :follower += render 'user/stats', user: @user \ No newline at end of file diff --git a/app/views/user/_stats.html.haml b/app/views/user/_stats.html.haml new file mode 100644 index 00000000..b6ff4d5d --- /dev/null +++ b/app/views/user/_stats.html.haml @@ -0,0 +1,22 @@ +.panel.panel-default.profile--panel + .panel-heading + %h3.panel-title Stats + .panel-body + .row + %a{href: show_user_followers_path(@user.screen_name)} + .col-md-6.col-sm-6.col-xs-6 + %h4.entry-text#follower-count= @user.follower_count + %h6.entry-subtext Followers + %a{href: show_user_friends_path(@user.screen_name)} + .col-md-6.col-sm-6.col-xs-6 + %h4.entry-text#friend-count= @user.friend_count + %h6.entry-subtext Following + .row + %a{href: show_user_questions_path(@user.screen_name)} + .col-md-6.col-sm-6.col-xs-6 + %h4.entry-text#asked-count= @user.asked_count + %h6.entry-subtext Questions + %a{href: show_user_profile_path(@user.screen_name)} + .col-md-6.col-sm-6.col-xs-6 + %h4.entry-text#answered-count= @user.answered_count + %h6.entry-subtext Answers \ No newline at end of file diff --git a/app/views/user/questions.html.haml b/app/views/user/questions.html.haml new file mode 100644 index 00000000..b271b69a --- /dev/null +++ b/app/views/user/questions.html.haml @@ -0,0 +1,18 @@ +.profile--header +.container.j2-page + .col-md-4.col-xs-12.col-sm-4 + = render 'user/profile_info' + .hidden-xs= render 'shared/links' + .col-md-8.col-xs-12.col-sm-8 + %h1.j2-lh.hidden-xs= @title + %h1.visible-xs= @title + #questions + - @questions.each do |q| + = render 'shared/question', q: q + + #pagination= will_paginate @questions, renderer: BootstrapPagination::Rails, page_links: false + + - if @questions.next_page + %button#load-more-btn.btn.btn-default{type: :button, data: { current_page: @questions.current_page }} + Load more + .visible-xs= render 'shared/links' diff --git a/app/views/user/questions.js.erb b/app/views/user/questions.js.erb new file mode 100644 index 00000000..01344836 --- /dev/null +++ b/app/views/user/questions.js.erb @@ -0,0 +1,8 @@ +$('#questions').append('<% @questions.each do |q| + %><%= j render 'shared/question', q: q +%><% end %>'); +<% if @questions.next_page %> +$('#pagination').html('<%= j will_paginate @questions, renderer: BootstrapPagination::Rails, page_links: false %>'); +<% else %> +$('#pagination, #load-more-btn').remove(); +<% end %> \ No newline at end of file diff --git a/app/views/user/show.html.haml b/app/views/user/show.html.haml index 1e6e72eb..ff2c55be 100644 --- a/app/views/user/show.html.haml +++ b/app/views/user/show.html.haml @@ -1,9 +1,9 @@ .profile--header .container.j2-page - .col-md-3.col-xs-12.col-sm-3 + .col-md-4.col-xs-12.col-sm-4 = render 'user/profile_info' .hidden-xs= render 'shared/links' - .col-md-9.col-xs-12.col-sm-9 + .col-md-8.col-xs-12.col-sm-8 = render 'shared/questionbox' #answers - @answers.each do |a| diff --git a/app/views/user/show_follow.html.haml b/app/views/user/show_follow.html.haml index a93a0f61..04bd7f2d 100644 --- a/app/views/user/show_follow.html.haml +++ b/app/views/user/show_follow.html.haml @@ -1,9 +1,11 @@ +.profile--header .container.j2-page - .col-md-3.col-xs-12.col-sm-3 + .col-md-4.col-xs-12.col-sm-4 = render 'user/profile_info' .hidden-xs= render 'shared/links' - .col-md-9.col-xs-12.col-sm-9 - %h1= @title + .col-md-8.col-xs-12.col-sm-8 + %h1.j2-lh.hidden-xs= @title + %h1.visible-xs= @title #users - @users.each do |user| .col-sm-6 diff --git a/bin/cap b/bin/cap new file mode 100755 index 00000000..30352d4d --- /dev/null +++ b/bin/cap @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# +# This file was generated by Bundler. +# +# The application 'cap' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'pathname' +ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('capistrano', 'cap') diff --git a/bin/capify b/bin/capify new file mode 100755 index 00000000..0f486e81 --- /dev/null +++ b/bin/capify @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# +# This file was generated by Bundler. +# +# The application 'capify' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'pathname' +ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('capistrano', 'capify') diff --git a/config/deploy.rb b/config/deploy.rb new file mode 100644 index 00000000..01f44e6d --- /dev/null +++ b/config/deploy.rb @@ -0,0 +1,31 @@ +# config valid only for current version of Capistrano +lock '3.3.5' + +set :application, 'justask' +set :repo_url, 'git@git.rrerr.net:justask/justask.git' +ask :branch, :master +set :deploy_to, '/home/justask/cap/' +set :scm, :git +set :format, :pretty +set :log_level, :debug + +# RVM +set :rvm_type, :user +set :rvm_ruby_version, '2.0.0' + +# Rails +set :conditionally_migrate, true + +namespace :deploy do + + after :updated do + + end + + after :restart, :clear_cache do + on roles(:web), in: :groups, limit: 3, wait: 10 do + + end + end + +end diff --git a/config/deploy/production.rb b/config/deploy/production.rb new file mode 100644 index 00000000..d79adf18 --- /dev/null +++ b/config/deploy/production.rb @@ -0,0 +1,7 @@ +server 'rrerr.net', user: 'justask', roles: %w{web app} + +set :ssh_options, { + keys: %w(~/.ssh/id_rsa), + forward_agent: false, + auth_methods: %w(publickey) +} diff --git a/config/deploy/staging.rb b/config/deploy/staging.rb new file mode 100644 index 00000000..e664a6cd --- /dev/null +++ b/config/deploy/staging.rb @@ -0,0 +1,45 @@ +# Simple Role Syntax +# ================== +# Supports bulk-adding hosts to roles, the primary server in each group +# is considered to be the first unless any hosts have the primary +# property set. Don't declare `role :all`, it's a meta role. + +role :app, %w{deploy@example.com} +role :web, %w{deploy@example.com} +role :db, %w{deploy@example.com} + + +# Extended Server Syntax +# ====================== +# This can be used to drop a more detailed server definition into the +# server list. The second argument is a, or duck-types, Hash and is +# used to set extended properties on the server. + +server 'example.com', user: 'deploy', roles: %w{web app}, my_property: :my_value + + +# Custom SSH Options +# ================== +# You may pass any option but keep in mind that net/ssh understands a +# limited set of options, consult[net/ssh documentation](http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start). +# +# Global options +# -------------- +# set :ssh_options, { +# keys: %w(/home/rlisowski/.ssh/id_rsa), +# forward_agent: false, +# auth_methods: %w(password) +# } +# +# And/or per server (overrides global) +# ------------------------------------ +# server 'example.com', +# user: 'user_name', +# roles: %w{web app}, +# ssh_options: { +# user: 'user_name', # overrides user setting above +# keys: %w(/home/user_name/.ssh/id_rsa), +# forward_agent: false, +# auth_methods: %w(publickey password) +# # password: 'please use keys' +# } diff --git a/config/routes.rb b/config/routes.rb index 6e268567..cc6ccb8a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -46,6 +46,7 @@ Rails.application.routes.draw do match '/answer', to: 'inbox#destroy', via: :post, as: :answer match '/generate_question', to: 'inbox#create', via: :post, as: :generate_question match '/delete_inbox', to: 'inbox#remove', via: :post, as: :delete_inbox + match '/delete_all_inbox', to: 'inbox#remove_all', via: :post, as: :delete_all_inbox match '/destroy_answer', to: 'answer#destroy', via: :post, as: :destroy_answer match '/create_friend', to: 'friend#create', via: :post, as: :create_friend match '/destroy_friend', to: 'friend#destroy', via: :post, as: :destroy_friend @@ -71,4 +72,5 @@ Rails.application.routes.draw do match '/:username/q/:id', to: 'question#show', via: 'get', as: :show_user_question_alt match '/:username/followers(/p/:page)', to: 'user#followers', via: 'get', as: :show_user_followers_alt, defaults: {page: 1} match '/:username/friends(/p/:page)', to: 'user#friends', via: 'get', as: :show_user_friends_alt, defaults: {page: 1} + match '/:username/questions(/p/:page)', to: 'user#questions', via: 'get', as: :show_user_questions, defaults: {page: 1} end diff --git a/lib/assets/javascripts/cheet.js b/lib/assets/javascripts/cheet.js new file mode 100644 index 00000000..dd0806f3 --- /dev/null +++ b/lib/assets/javascripts/cheet.js @@ -0,0 +1,279 @@ +/* +The MIT License (MIT) + +Copyright (c) 2013 Louis Acresti + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + + +(function (global) { + 'use strict'; + + var cheet, + sequences = {}, + keys = { + backspace: 8, + tab: 9, + enter: 13, + 'return': 13, + shift: 16, + '⇧': 16, + control: 17, + ctrl: 17, + '⌃': 17, + alt: 18, + option: 18, + '⌥': 18, + pause: 19, + capslock: 20, + esc: 27, + space: 32, + pageup: 33, + pagedown: 34, + end: 35, + home: 36, + left: 37, + L: 37, + '←': 37, + up: 38, + U: 38, + '↑': 38, + right: 39, + R: 39, + '→': 39, + down: 40, + D: 40, + '↓': 40, + insert: 45, + 'delete': 46, + '0': 48, + '1': 49, + '2': 50, + '3': 51, + '4': 52, + '5': 53, + '6': 54, + '7': 55, + '8': 56, + '9': 57, + a: 65, + b: 66, + c: 67, + d: 68, + e: 69, + f: 70, + g: 71, + h: 72, + i: 73, + j: 74, + k: 75, + l: 76, + m: 77, + n: 78, + o: 79, + p: 80, + q: 81, + r: 82, + s: 83, + t: 84, + u: 85, + v: 86, + w: 87, + x: 88, + y: 89, + z: 90, + '⌘': 91, + command: 91, + kp_0: 96, + kp_1: 97, + kp_2: 98, + kp_3: 99, + kp_4: 100, + kp_5: 101, + kp_6: 102, + kp_7: 103, + kp_8: 104, + kp_9: 105, + kp_multiply: 106, + kp_plus: 107, + kp_minus: 109, + kp_decimal: 110, + kp_divide: 111, + f1: 112, + f2: 113, + f3: 114, + f4: 115, + f5: 116, + f6: 117, + f7: 118, + f8: 119, + f9: 120, + f10: 121, + f11: 122, + f12: 123, + equal: 187, + '=': 187, + comma: 188, + ',': 188, + minus: 189, + '-': 189, + period: 190, + '.': 190 + }, + Sequence, + NOOP = function NOOP() {}, + held = {}; + + Sequence = function Sequence (str, next, fail, done) { + var i; + + this.str = str; + this.next = next ? next : NOOP; + this.fail = fail ? fail : NOOP; + this.done = done ? done : NOOP; + + this.seq = str.split(' '); + this.keys = []; + + for (i=0; i 0) { + this.reset(); + this.fail(this.str); + cheet.__fail(this.str); + } + return; + } + + this.next(this.str, this.seq[i], i, this.seq); + cheet.__next(this.str, this.seq[i], i, this.seq); + + if (++this.idx === this.keys.length) { + this.done(this.str); + cheet.__done(this.str); + this.reset(); + } + }; + + Sequence.prototype.reset = function () { + this.idx = 0; + }; + + cheet = function cheet (str, handlers) { + var next, fail, done; + + if (typeof handlers === 'function') { + done = handlers; + } else if (handlers !== null && handlers !== undefined) { + next = handlers.next; + fail = handlers.fail; + done = handlers.done; + } + + sequences[str] = new Sequence(str, next, fail, done); + }; + + cheet.disable = function disable (str) { + delete sequences[str]; + }; + + function keydown (e) { + var id, + k = e ? e.keyCode : event.keyCode; + + if (held[k]) return; + held[k] = true; + + for (id in sequences) { + sequences[id].keydown(k); + } + } + + function keyup (e) { + var k = e ? e.keyCode : event.keyCode; + held[k] = false; + } + + function resetHeldKeys (e) { + var k; + for (k in held) { + held[k] = false; + } + } + + function on (obj, type, fn) { + if (obj.addEventListener) { + obj.addEventListener(type, fn, false); + } else if (obj.attachEvent) { + obj['e' + type + fn] = fn; + obj[type + fn] = function () { + obj['e' + type + fn](window.event); + }; + obj.attachEvent('on' + type, obj[type + fn]); + } + } + + on(window, 'keydown', keydown); + on(window, 'keyup', keyup); + on(window, 'blur', resetHeldKeys); + on(window, 'focus', resetHeldKeys); + + cheet.__next = NOOP; + cheet.next = function next (fn) { + cheet.__next = fn === null ? NOOP : fn; + }; + + cheet.__fail = NOOP; + cheet.fail = function fail (fn) { + cheet.__fail = fn === null ? NOOP : fn; + }; + + cheet.__done = NOOP; + cheet.done = function done (fn) { + cheet.__done = fn === null ? NOOP : fn; + }; + + cheet.reset = function reset (id) { + var seq = sequences[id]; + if (!(seq instanceof Sequence)) { + console.warn('cheet: Unknown sequence: ' + id); + return; + } + + seq.reset(); + }; + + global.cheet = cheet; + + if (typeof define === 'function' && define.amd) { + define([], function () { return cheet; }); + } else if (typeof module !== 'undefined' && module !== null) { + module.exports = cheet; + } + +})(this); diff --git a/lib/assets/.keep b/lib/capistrano/tasks/.keep similarity index 100% rename from lib/assets/.keep rename to lib/capistrano/tasks/.keep diff --git a/public/404.html b/public/404.html index 5503b7e7..220d07c4 100644 --- a/public/404.html +++ b/public/404.html @@ -1,59 +1,75 @@ - The page you were looking for doesn't exist (404) + Page not found!!1! (404) -
@@ -62,5 +78,9 @@

If you think you found a bug, please let us know.

+ diff --git a/public/422.html b/public/422.html index 1c971ab3..b364fad1 100644 --- a/public/422.html +++ b/public/422.html @@ -4,63 +4,83 @@ 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 think you found a bug, please let us know.

+

If you think you found a bug, please please let us know.

+
+ diff --git a/public/500.html b/public/500.html index eb240d94..c528dbb8 100644 --- a/public/500.html +++ b/public/500.html @@ -4,62 +4,82 @@ We're sorry, but something went wrong (500) -
-

We're sorry, but something went wrong.

+

Looks like something went wrong!

-

If you think you found a bug, please let us know.

+

This usually happens due to an error in our code. If you think you found a bug, please let us know.

+
+ diff --git a/public/502.html b/public/502.html new file mode 100644 index 00000000..f92daf76 --- /dev/null +++ b/public/502.html @@ -0,0 +1,86 @@ + + + + Unicorn! (502) + + + + +
+
+

This page takes way too long to load!

+ Unicorn +
+

Sorry about that. Please try refreshing and contact us if the problem persists.

+
+ + + diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 00000000..7d16acd5 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/favicon.ico b/public/favicon.ico index e69de29b..e2138895 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/images/angry_unicorn.png b/public/images/angry_unicorn.png new file mode 100644 index 00000000..86b2beb6 Binary files /dev/null and b/public/images/angry_unicorn.png differ diff --git a/spec/factories/notifications.rb b/spec/factories/notifications.rb new file mode 100644 index 00000000..79eaba4b --- /dev/null +++ b/spec/factories/notifications.rb @@ -0,0 +1,9 @@ +FactoryGirl.define do + factory :notification do + target_type "MyString" +target_id 1 +recipient_id 1 +new false + end + +end diff --git a/spec/features/users/follow_user_spec.rb b/spec/features/users/follow_user_spec.rb new file mode 100644 index 00000000..3d03347f --- /dev/null +++ b/spec/features/users/follow_user_spec.rb @@ -0,0 +1,28 @@ +include Warden::Test::Helpers +Warden.test_mode! + +feature "User profile page", :devise do + + after :each do + Warden.test_reset! + end + + scenario "user gets followed", js: true do + me = FactoryGirl.create(:user) + other = FactoryGirl.create(:user) + + login_as me, scope: :user + visit show_user_profile_path(other.screen_name) + page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_1.png"), full: true + + click_button "Follow" + wait_for_ajax + page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_2.png"), full: true + + expect(page).to have_text("FOLLOWING") + + click_link 'Followers' + page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_3.png"), full: true + expect(page).to have_text(me.screen_name) + end +end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb new file mode 100644 index 00000000..f79213f9 --- /dev/null +++ b/spec/models/notification_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Notification, :type => :model do + pending "add some examples to (or delete) #{__FILE__}" +end