diff --git a/Gemfile b/Gemfile index d7b5bfd7d..20f656985 100644 --- a/Gemfile +++ b/Gemfile @@ -15,7 +15,6 @@ gem 'rack-cors', '~> 0.2.4', :require => 'rack/cors' # authentication gem 'devise', '~> 1.3.1' -gem 'devise_invitable', '0.5.0' gem 'jwt' gem 'oauth2-provider', '0.0.19' diff --git a/Gemfile.lock b/Gemfile.lock index 42943b247..b4b489c8e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -138,9 +138,6 @@ GEM bcrypt-ruby (~> 2.1.2) orm_adapter (~> 0.0.3) warden (~> 1.0.3) - devise_invitable (0.5.0) - devise (~> 1.3.1) - rails (>= 3.0.0, <= 3.2) diff-lcs (1.1.3) em-http-request (1.0.2) addressable (>= 2.2.3) @@ -465,7 +462,6 @@ DEPENDENCIES cucumber-rails (= 1.2.1) database_cleaner (= 0.7.1) devise (~> 1.3.1) - devise_invitable (= 0.5.0) diaspora-client! em-synchrony (= 1.0.0) factory_girl_rails diff --git a/app/controllers/invitation_codes_controller.rb b/app/controllers/invitation_codes_controller.rb new file mode 100644 index 000000000..e79b4f868 --- /dev/null +++ b/app/controllers/invitation_codes_controller.rb @@ -0,0 +1,16 @@ +class InvitationCodesController < ApplicationController + before_filter :ensure_valid_invite_code + + rescue_from ActiveRecord::RecordNotFound do + redirect_to root_url, :notice => "That invite code is no longer valid" + end + + def show + sign_out(current_user) if user_signed_in? + redirect_to new_user_registration_path(:invite => {:token => params[:id]}) + end + + def ensure_valid_invite_code + InvitationCode.find_by_token!(params[:id]) + end +end diff --git a/app/controllers/invitations_controller.rb b/app/controllers/invitations_controller.rb index 1b4561972..9ca07d839 100644 --- a/app/controllers/invitations_controller.rb +++ b/app/controllers/invitations_controller.rb @@ -2,12 +2,12 @@ # licensed under the Affero General Public License version 3 or later. See # the COPYRIGHT file. -class InvitationsController < Devise::InvitationsController +require Rails.root.join('lib', 'email_inviter') - before_filter :check_token, :only => [:edit, :email] - before_filter :check_if_invites_open, :only =>[:create] +class InvitationsController < ApplicationController def new + @invite_code = current_user.invitation_code @sent_invitations = current_user.invitations_from_me.includes(:recipient) respond_to do |format| format.html do @@ -16,65 +16,18 @@ class InvitationsController < Devise::InvitationsController end end + # this is for legacy invites. We try to look the person who sent them the + # invite, and use their new invite code + def edit + user = User.find_by_invitation_token(params[:invitation_token]) + invitation_code = user.ugly_accept_invitation_code + redirect_to invite_code_path(invitation_code) + end + + def create - aspect_id = params[:user].delete(:aspects) - message = params[:user].delete(:invite_messages) - emails = params[:user][:email].to_s.gsub(/\s/, '').split(/, */) - #NOTE should we try and find users by email here? probs - aspect = current_user.aspects.find(aspect_id) - - language = params[:user][:language] - - invites = Invitation.batch_invite(emails, :message => message, :sender => current_user, :aspect => aspect, :service => 'email', :language => language) - - flash[:notice] = extract_messages(invites) - - redirect_to :back - end - - def update - invitation_token = params[:user][:invitation_token] - - if invitation_token.nil? || invitation_token.blank? - redirect_to :back, :error => I18n.t('invitations.check_token.not_found') - return - end - - user = User.find_by_invitation_token!(invitation_token) - - user.accept_invitation!(params[:user]) - - if user.persisted? && user.person && user.person.persisted? - user.seed_aspects - flash[:notice] = I18n.t 'registrations.create.success' - sign_in_and_redirect(:user, user) - else - user.errors.delete(:person) - flash[:error] = user.errors.full_messages.join(", ") - redirect_to accept_user_invitation_path(:invitation_token => params[:user][:invitation_token]) - end - end - - def resend - invitation = current_user.invitations_from_me.where(:id => params[:id]).first - if invitation - Resque.enqueue(Jobs::ResendInvitation, invitation.id) - flash[:notice] = I18n.t('invitations.create.sent') + invitation.recipient.email - end - redirect_to :back - end - - def email - @invs = [] - @resource = User.find_by_invitation_token(params[:invitation_token]) - render 'devise/mailer/invitation_instructions', :layout => false - end - - protected - def check_token - if User.find_by_invitation_token(params[:invitation_token]).nil? - render 'invitations/token_not_found' - end + inviter = EmailInviter.new(params[:email_inviter][:emails], params[:email_inviter]) + redirect_to :back, :notice => "Great! Invites were sent off to #{inviter.emails.join(', ')}" end def check_if_invites_open @@ -84,26 +37,4 @@ class InvitationsController < Devise::InvitationsController return end end - - # @param invites [Array] Invitations to be sent. - # @return [String] A full list of success and error messages. - def extract_messages(invites) - success_message = "Invites Successfully Sent to: " - failure_message = "There was a problem with: " - following_message = " already are on Diaspora, so you are now sharing with them." - successes, failures = invites.partition{|x| x.persisted? } - - followings, real_failures = failures.partition{|x| x.errors[:recipient].present? } - - success_message += successes.map{|k| k.identifier }.to_sentence - failure_message += real_failures.map{|k| k.identifier }.to_sentence - following_message += followings.map{|k| k.identifier}.to_sentence - - messages = [] - messages << success_message if successes.present? - messages << failure_message if failures.present? - messages << following_message if followings.present? - - messages.join('\n') - end -end +end \ No newline at end of file diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index bcb7cad5b..d55cbb2ec 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -3,10 +3,12 @@ # the COPYRIGHT file. class RegistrationsController < Devise::RegistrationsController - before_filter :check_registrations_open! + before_filter :check_registrations_open_or_vaild_invite! def create @user = User.build(params[:user]) + @user.process_invite_acceptence(invite) if invite.present? + if @user.save flash[:notice] = I18n.t 'registrations.create.success' @user.seed_aspects @@ -26,10 +28,19 @@ class RegistrationsController < Devise::RegistrationsController end private - def check_registrations_open! + def check_registrations_open_or_vaild_invite! + return true if invite.present? if AppConfig[:registrations_closed] flash[:error] = t('registrations.closed') redirect_to new_user_session_path end end + + def invite + if params[:invite].present? + @invite ||= InvitationCode.find_by_token(params[:invite][:token]) + end + end + + helper_method :invite end diff --git a/app/controllers/services_controller.rb b/app/controllers/services_controller.rb index 25df66366..c67f609bb 100644 --- a/app/controllers/services_controller.rb +++ b/app/controllers/services_controller.rb @@ -99,24 +99,8 @@ class ServicesController < ApplicationController #{t('services.inviter.click_link_to_accept_invitation')}: \n \n -#{accept_invitation_url(user, :invitation_token => user.invitation_token)} +#{invitation_code_url(user.invitation_code)} MSG "https://www.facebook.com/messages/#{facebook_uid}?msg_prefill=#{message}" end - - def invite_redirect_json(invite, user, service_user) - if invite.email_like_identifer - {:message => t("invitations.create.sent") + service_user.name } - else - {:url => facebook_message_url(user, service_user.uid)} - end - end - - def invite_redirect_url(invite, user, service_user) - if invite.email_like_identifer - redirect_to(friend_finder_path(:provider => 'facebook'), :notice => "you re-invited #{service_user.name}") - else - redirect_to(facebook_message_url(user, service_user.uid)) - end - end -end +end \ No newline at end of file diff --git a/app/helpers/invitation_codes_helper.rb b/app/helpers/invitation_codes_helper.rb new file mode 100644 index 000000000..83ac8ddd5 --- /dev/null +++ b/app/helpers/invitation_codes_helper.rb @@ -0,0 +1,28 @@ +module InvitationCodesHelper + def invite_welcome_message + if invite.present? + content_tag(:div) do + person_image_link(invite.user.person) + + I18n.translate('invitation_codes.excited', :name => invite.user.name) + end + end + end + + def invite_hidden_tag(invite) + if invite.present? + hidden_field_tag 'invite[token]', invite.token + end + end + + def invite_link(invite_code) + text_field_tag :invite_code, invite_code_url(@invite_code), :readonly => true + end + + def invited_by_message + inviter = current_user.invited_by + if inviter.present? + contact = current_user.contact_for(inviter.person) || Contact.new + render :partial => 'people/add_contact', :locals => {:inviter => inviter.person, :contact => contact} + end + end +end diff --git a/app/mailers/notifier.rb b/app/mailers/notifier.rb index f9c4ad044..5b5ea9ee6 100644 --- a/app/mailers/notifier.rb +++ b/app/mailers/notifier.rb @@ -33,6 +33,21 @@ class Notifier < ActionMailer::Base mail(default_opts) end + def invite(email, message, inviter, invitation_code, locale) + @inviter = inviter + @message = message + @locale = locale + @invitation_code = invitation_code + + mail_opts = {:to => email, :from => AppConfig[:smtp_sender_address], + :subject => I18n.t('notifier.invited!'), + :host => AppConfig[:pod_uri].host} + + I18n.with_locale(locale) do + mail(mail_opts) + end + end + def started_sharing(recipient_id, sender_id) send_notification(:started_sharing, recipient_id, sender_id) end diff --git a/app/models/invitation_code.rb b/app/models/invitation_code.rb new file mode 100644 index 000000000..ba31b74c9 --- /dev/null +++ b/app/models/invitation_code.rb @@ -0,0 +1,17 @@ +class InvitationCode < ActiveRecord::Base + belongs_to :user + + validates_presence_of :user + + before_create :generate_token + + def to_param + token + end + + def generate_token + begin + self.token = ActiveSupport::SecureRandom.hex(6) + end while InvitationCode.exists?(:token => self[:token]) + end +end diff --git a/app/models/user.rb b/app/models/user.rb index e35314d50..f03cf6ebf 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -8,12 +8,11 @@ require 'rest-client' class User < ActiveRecord::Base include Encryptor::Private - include Connecting include Querying include SocialActions - devise :invitable, :database_authenticatable, :registerable, + devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :timeoutable, :token_authenticatable, :lockable, :lock_strategy => :none, :unlock_strategy => :none @@ -40,7 +39,10 @@ class User < ActiveRecord::Base has_many :invitations_from_me, :class_name => 'Invitation', :foreign_key => :sender_id has_many :invitations_to_me, :class_name => 'Invitation', :foreign_key => :recipient_id has_many :aspects, :order => 'order_id ASC' + belongs_to :auto_follow_back_aspect, :class_name => 'Aspect' + belongs_to :invited_by, :class_name => 'User' + has_many :aspect_memberships, :through => :aspects has_many :contacts @@ -64,7 +66,6 @@ class User < ActiveRecord::Base before_save :guard_unconfirmed_email, :save_person! - before_create :infer_email_from_invitation_provider attr_accessible :getting_started, :password, @@ -82,6 +83,7 @@ class User < ActiveRecord::Base User.joins(:contacts).where(:contacts => {:person_id => person.id}) end +<<<<<<< HEAD def self.monthly_actives(start_day = Time.now) logged_in_since(start_day - 1.month) end @@ -115,23 +117,24 @@ class User < ActiveRecord::Base existing_user = User.where(:email => identifier).first else existing_user = User.joins(:services).where(:services => {:type => "Services::#{service.titleize}", :uid => identifier}).first +======= + #should be deprecated + def ugly_accept_invitation_code + begin + self.invitations_to_me.first.sender.invitation_code + rescue Exception => e + nil +>>>>>>> invite_link functionailty mostly works end - - if existing_user.nil? - i = Invitation.where(:service => service, :identifier => identifier).first - existing_user = i.recipient if i - end - - existing_user end - # @return [User] - def self.find_or_create_by_invitation(invitation) - if existing_user = self.find_by_invitation(invitation) - existing_user - else - self.create_from_invitation!(invitation) - end + def process_invite_acceptence(invite) + self.invited_by = invite.user + end + + + def invitation_code + InvitationCode.find_or_create_by_user_id(self.id) end def hidden_shareables @@ -389,39 +392,6 @@ class User < ActiveRecord::Base end end - # This method is called when an invited user accepts his invitation - # - # @param [Hash] opts the options to accept the invitation with - # @option opts [String] :username The username the invited user wants. - # @option opts [String] :password - # @option opts [String] :password_confirmation - def accept_invitation!(opts = {}) - log_hash = {:event => :invitation_accepted, :username => opts[:username], :uid => self.id} - log_hash[:inviter] = invitations_to_me.first.sender.diaspora_handle if invitations_to_me.first && invitations_to_me.first.sender - - if self.invited? - self.setup(opts) - self.invitation_token = nil - self.password = opts[:password] - self.password_confirmation = opts[:password_confirmation] - - self.save - return unless self.errors.empty? - - # moved old Invitation#share_with! logic into here, - # but i don't think we want to destroy the invitation - # anymore. we may want to just call self.share_with - invitations_to_me.each do |invitation| - if !invitation.admin? && invitation.sender.share_with(self.person, invitation.aspect) - invitation.destroy - end - end - - log_hash[:status] = "success" - Rails.logger.info(log_hash) - self - end - end ###Helpers############ def self.build(opts = {}) @@ -511,14 +481,6 @@ class User < ActiveRecord::Base self.person end - # Set the User's email to the one they've been invited at, if the user - # is being created via an invitation. - # - # @return [User] - def infer_email_from_invitation_provider - self.email = self.invitation_identifier if self.invitation_service == 'email' - self - end def no_person_with_same_username diaspora_id = "#{self.username}#{User.diaspora_id_host}" diff --git a/app/views/invitations/new.html.haml b/app/views/invitations/new.html.haml index 5bfa3e319..448876030 100644 --- a/app/views/invitations/new.html.haml +++ b/app/views/invitations/new.html.haml @@ -8,34 +8,35 @@ .description = t('.if_they_accept_info') %br - .span-7.append-1 + .span-7.append-1.last #email_invitation - = form_for User.new, :url => invitation_path(User) do |invite| + = form_tag new_user_invitation_path do %h4 = t('email') - = invite.text_field :email, :title => t('.comma_seperated_plz'), :placeholder => 'foo@bar.com, max@foo.com...' - %br - - %h4 - = t('.aspect') - = invite.select(:aspects, options_from_collection_for_select(all_aspects, 'id', 'name')) - + = text_field_tag 'email_inviter[emails]' ,nil, :title => t('.comma_seperated_plz'), :placeholder => 'foo@bar.com, max@foo.com...' %br %br %h4 = t('.language') - = invite.select(:language, available_language_options, :selected => current_user.language) + = select_tag('email_inviter[locale]', options_from_collection_for_select(available_language_options, "second", "first", :selected => current_user.language)) %br %br %h4 = t('.personal_message') - = invite.text_area :invite_messages, :rows => 3, :value => t('.check_out_diaspora') + = text_area_tag 'email_inviter[message]',nil, :rows => 3, :value => t('.check_out_diaspora') %p - = invite.submit t('.send_an_invitation') + = submit_tag t('.send_an_invitation') + + .clearfix + .span-7.prepend-3.last + + or paste them this link! + = invite_link(@invite_code) + = "#{@invite_code.count} invites left on this code" %br %br diff --git a/app/views/notifier/invite.erb b/app/views/notifier/invite.erb new file mode 100644 index 000000000..7093e499f --- /dev/null +++ b/app/views/notifier/invite.erb @@ -0,0 +1,140 @@ +<%- self.extend NotifierHelper -%> + + <%=invite_email_title %> + +

<%= t('.displaying_correctly', :link => link_to(t('.view_in'), invite_email_url(:invitation_token => @resource.invitation_token), :style => "color: #3F8FBA; text-decoration: none;")).html_safe %>

+ + + + +
+ + + + + + + + + + + + + + + + + + + <% if @inviter.present? %> + <%= @inviter.inspect %> + <% end %> + + + + + + + + + + + + + + + + + + + + +
+ + Diaspora + +
+ <%= t('.finally') %>
+
+ +
+ <%= t('.arrived', :strong_diaspora => content_tag(:strong, "DIASPORA*")).html_safe %> +
+
+ <%= link_to(t('.sign_up_now').html_safe, invite_code_url(@invitation_code), :style => "color: #3F8FBA; text-decoration: underline; font-weight: bold; font-size: 20px;", :target => "_blank").html_safe %> +
+ 1. <%= t('.get_connected') %>
+ + + + + +
+ + + <%= t('.get_connected_paragraph', :strong_diaspora => content_tag(:strong, "DIASPORA*")).html_safe %> +
+ +
+
+ 2. <%= t('.be_yourself') %>
+ + + + + + +
+ + <%= t('.be_yourself_paragraph', :strong_diaspora => content_tag(:strong, "DIASPORA*")).html_safe %> + +
+ +
+ +
+
+ 3. <%= t('.have_fun') %>
+ + + + + +
+ + + + <%= t('.have_fun_paragraph', :strong_diaspora => content_tag(:strong, "DIASPORA*"), :link => link_to(t('.cubbies'), "https://cubbi.es", :style => "color: #3F8FBA; text-decoration: underline; font-weight: bold; font-size: 20px;", :target => "_blank")).html_safe %> + +
+
+ + + + + + +
+ <%= link_to(t('.sign_up_now').html_safe, invite_code_url(@invitation_code), :style => "color: #3F8FBA; text-decoration: underline; font-weight: bold; font-size: 20px;", :target => "_blank").html_safe %> +
+
+ + + + + + +
+ <%= t('.made_by_people', :strong_diaspora => content_tag(:strong, "DIASPORA*"), :jointeam => link_to(t('.join_team'), "https://github.com/diaspora/diaspora/wiki/Become-a-Contributor", :style =>"color: #3F8FBA; text-decoration: underline; font-weight: bold; font-size: 18px;", :target => "_blank"), :helpfund => link_to(t('.help_fund'), "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=QG4L6VYD8YGPU", :style =>"color: #3F8FBA; text-decoration: underline; font-weight: bold; font-size: 18px;", :target => "_blank")).html_safe %> + +
+
+ <%= t('.love') %>
+ <%= t('.team_diaspora') %>
+
+ <% if AppConfig[:pod_uri].host.match(/joindiaspora.com/) %> + <%= t('.unsubscribe', :link => link_to(t('.here'), "http://joindiaspora.us1.list-manage.com/unsubscribe?u=d759919b94f9cdcf39d204f3f&id=7b5ceb2f8b", :style => "color: #3F8FBA; text-decoration: none;")).html_safe %> + <% end %> + <%= t('.email_us', :email => link_to(t('.email_address'), "mailto:questions@joindiaspora.com", :style => "color: #3F8FBA; text-decoration: none;")).html_safe %> +
+
diff --git a/app/views/notifier/invite.html.erb b/app/views/notifier/invite.html.erb new file mode 100644 index 000000000..7093e499f --- /dev/null +++ b/app/views/notifier/invite.html.erb @@ -0,0 +1,140 @@ +<%- self.extend NotifierHelper -%> + + <%=invite_email_title %> + +

<%= t('.displaying_correctly', :link => link_to(t('.view_in'), invite_email_url(:invitation_token => @resource.invitation_token), :style => "color: #3F8FBA; text-decoration: none;")).html_safe %>

+ + + + +
+ + + + + + + + + + + + + + + + + + + <% if @inviter.present? %> + <%= @inviter.inspect %> + <% end %> + + + + + + + + + + + + + + + + + + + + +
+ + Diaspora + +
+ <%= t('.finally') %>
+
+ +
+ <%= t('.arrived', :strong_diaspora => content_tag(:strong, "DIASPORA*")).html_safe %> +
+
+ <%= link_to(t('.sign_up_now').html_safe, invite_code_url(@invitation_code), :style => "color: #3F8FBA; text-decoration: underline; font-weight: bold; font-size: 20px;", :target => "_blank").html_safe %> +
+ 1. <%= t('.get_connected') %>
+ + + + + +
+ + + <%= t('.get_connected_paragraph', :strong_diaspora => content_tag(:strong, "DIASPORA*")).html_safe %> +
+ +
+
+ 2. <%= t('.be_yourself') %>
+ + + + + + +
+ + <%= t('.be_yourself_paragraph', :strong_diaspora => content_tag(:strong, "DIASPORA*")).html_safe %> + +
+ +
+ +
+
+ 3. <%= t('.have_fun') %>
+ + + + + +
+ + + + <%= t('.have_fun_paragraph', :strong_diaspora => content_tag(:strong, "DIASPORA*"), :link => link_to(t('.cubbies'), "https://cubbi.es", :style => "color: #3F8FBA; text-decoration: underline; font-weight: bold; font-size: 20px;", :target => "_blank")).html_safe %> + +
+
+ + + + + + +
+ <%= link_to(t('.sign_up_now').html_safe, invite_code_url(@invitation_code), :style => "color: #3F8FBA; text-decoration: underline; font-weight: bold; font-size: 20px;", :target => "_blank").html_safe %> +
+
+ + + + + + +
+ <%= t('.made_by_people', :strong_diaspora => content_tag(:strong, "DIASPORA*"), :jointeam => link_to(t('.join_team'), "https://github.com/diaspora/diaspora/wiki/Become-a-Contributor", :style =>"color: #3F8FBA; text-decoration: underline; font-weight: bold; font-size: 18px;", :target => "_blank"), :helpfund => link_to(t('.help_fund'), "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=QG4L6VYD8YGPU", :style =>"color: #3F8FBA; text-decoration: underline; font-weight: bold; font-size: 18px;", :target => "_blank")).html_safe %> + +
+
+ <%= t('.love') %>
+ <%= t('.team_diaspora') %>
+
+ <% if AppConfig[:pod_uri].host.match(/joindiaspora.com/) %> + <%= t('.unsubscribe', :link => link_to(t('.here'), "http://joindiaspora.us1.list-manage.com/unsubscribe?u=d759919b94f9cdcf39d204f3f&id=7b5ceb2f8b", :style => "color: #3F8FBA; text-decoration: none;")).html_safe %> + <% end %> + <%= t('.email_us', :email => link_to(t('.email_address'), "mailto:questions@joindiaspora.com", :style => "color: #3F8FBA; text-decoration: none;")).html_safe %> +
+
diff --git a/app/views/notifier/invite.text.erb b/app/views/notifier/invite.text.erb new file mode 100644 index 000000000..90cb006df --- /dev/null +++ b/app/views/notifier/invite.text.erb @@ -0,0 +1,12 @@ +Hello! + +You have been invited to join Diaspora*! + +Click this link to get started + +<%= invitation_code_url(@invitation_code)%> + + +Love, + +The Diaspora* email robot! \ No newline at end of file diff --git a/app/views/people/_add_contact.html.haml b/app/views/people/_add_contact.html.haml new file mode 100644 index 000000000..7026e5d47 --- /dev/null +++ b/app/views/people/_add_contact.html.haml @@ -0,0 +1,5 @@ +.alert-message.block-message.success + you were invited by + = person_image_link inviter + = person_link inviter + = aspect_membership_dropdown(contact, inviter, false) diff --git a/app/views/registrations/new.html.haml b/app/views/registrations/new.html.haml index b4a27188e..472a5fdaf 100644 --- a/app/views/registrations/new.html.haml +++ b/app/views/registrations/new.html.haml @@ -11,16 +11,19 @@ = t('welcome') %h3.accept_invitation_text = t('.sign_up_message') + - flash.each do |name, msg| %p{:class => "login_#{name}"}= msg = image_tag 'diaspora_collage.png', :style => "margin-left:-50px;" - .span-10 %br %br %br %br + + = invite_welcome_message + = form_for(resource, :as => resource_name, :html => {:class => 'new_user_form'}, :url => registration_path(resource_name), :validate => true) do |f| %fieldset .clearfix @@ -42,6 +45,10 @@ = t('password_confirmation') = f.password_field :password_confirmation, :title => t('registrations.new.enter_password_again') + .clearfix + + = invite_hidden_tag(invite) + .submit_field = f.submit t('registrations.new.create_my_account'), :class => 'in_aspects' diff --git a/app/views/users/getting_started.haml b/app/views/users/getting_started.haml index 770b3bb36..7347c2cfc 100644 --- a/app/views/users/getting_started.haml +++ b/app/views/users/getting_started.haml @@ -14,6 +14,8 @@ %p.center = t(".community_welcome") + = invited_by_message + .clearfix %br %br diff --git a/config/locales/diaspora/en.yml b/config/locales/diaspora/en.yml index fc3ebca21..a0eb0adde 100644 --- a/config/locales/diaspora/en.yml +++ b/config/locales/diaspora/en.yml @@ -319,6 +319,8 @@ en: simplicity_explanation: "Diaspora makes sharing clean and easy – and this goes for privacy too. Inherently private, Diaspora doesn’t make you wade through pages of settings and options just to keep your profile secure." learn_about_host: "Learn about how to host your own Diaspora server." + invitation_codes: + excited: "%{name} is excited to see you here." invitations: create: sent: "Invitations have been sent to: " @@ -332,6 +334,7 @@ en: invite_someone_to_join: "Invite someone to join Diaspora!" if_they_accept_info: "if they accept, they will be added to the aspect you invited them." comma_seperated_plz: "You can enter multiple email addresses separated by commas." + check_out_diaspora: "Hey! You should check out Diaspora*" to: "To" personal_message: "Personal message" send_an_invitation: "Send an invitation" diff --git a/config/routes.rb b/config/routes.rb index 44e6aa3b7..0be9978c6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,9 +3,7 @@ # the COPYRIGHT file. Diaspora::Application.routes.draw do - # Posting and Reading - resources :reshares resources :status_messages, :only => [:new, :create] @@ -33,6 +31,7 @@ Diaspora::Application.routes.draw do get "liked" => "streams#liked", :as => "liked_stream" get "commented" => "streams#commented", :as => "commented_stream" get "aspects" => "streams#aspects", :as => "aspects_stream" + resources :aspects do put :toggle_contact_visibility @@ -97,11 +96,13 @@ Diaspora::Application.routes.draw do devise_for :users, :controllers => {:registrations => "registrations", :password => "devise/passwords", - :sessions => "sessions", - :invitations => "invitations"} do - get 'invitations/resend/:id' => 'invitations#resend', :as => 'invitation_resend' - get 'invitations/email' => 'invitations#email', :as => 'invite_email' - end + :sessions => "sessions"} + + #legacy routes to support old invite routes + get 'users/invitations/accept' => 'invitations#edit' + get 'users/invitations/email' => 'invitations#email', :as => 'invite_email' + get 'users/invitations' => 'invitations#new', :as => 'new_user_invitation' + post 'users/invitations' => 'invitations#create', :as => 'new_user_invitation' get 'login' => redirect('/users/sign_in') @@ -122,7 +123,7 @@ Diaspora::Application.routes.draw do resources :share_visibilities, :only => [:update] resources :blocks, :only => [:create, :destroy] - get 'community_spotlight' => "contacts#spotlight", :as => 'community_spotlight' + get 'i/:id' => 'invitation_codes#show', :as => 'invite_code' get 'people/refresh_search' => "people#refresh_search" resources :people, :except => [:edit, :update] do @@ -189,7 +190,7 @@ Diaspora::Application.routes.draw do end end - + get 'community_spotlight' => "contacts#spotlight", :as => 'community_spotlight' # Mobile site get 'mobile/toggle', :to => 'home#toggle_mobile', :as => 'toggle_mobile' diff --git a/db/migrate/20111211213438_create_invitation_codes.rb b/db/migrate/20111211213438_create_invitation_codes.rb new file mode 100644 index 000000000..f2bdde858 --- /dev/null +++ b/db/migrate/20111211213438_create_invitation_codes.rb @@ -0,0 +1,15 @@ +class CreateInvitationCodes < ActiveRecord::Migration + def self.up + create_table :invitation_codes do |t| + t.string :token + t.integer :user_id + t.integer :count + + t.timestamps + end + end + + def self.down + drop_table :invitation_codes + end +end diff --git a/db/schema.rb b/db/schema.rb index cc3532834..f662bf314 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -109,6 +109,14 @@ ActiveRecord::Schema.define(:version => 20120301143226) do add_index "conversations", ["author_id"], :name => "conversations_author_id_fk" + create_table "invitation_codes", :force => true do |t| + t.string "token" + t.integer "user_id" + t.integer "count" + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "invitations", :force => true do |t| t.text "message" t.integer "sender_id" diff --git a/features/invitations.feature b/features/invitations.feature new file mode 100644 index 000000000..d3b92045c --- /dev/null +++ b/features/invitations.feature @@ -0,0 +1,10 @@ +@javascript +Feature: Invitations + + Scenario: Accepting an invitation + When I visit alice's invitation code url + Then I should see the "alice is excited" message + When I fill in the new user form + And I press "Create my account!" + Then I should see the "welcome to diaspora" message + And I should be able to friend Alice diff --git a/features/step_definitions/message_steps.rb b/features/step_definitions/message_steps.rb index dc8d140ad..004acffca 100644 --- a/features/step_definitions/message_steps.rb +++ b/features/step_definitions/message_steps.rb @@ -11,6 +11,7 @@ Then /^I should see the "(.*)" message$/ do |message| I18n.translate('profiles.edit.you_are_nsfw') else raise "muriel, you don't have that message key, add one here" - end + end + page.should have_content(text) -end \ No newline at end of file +end diff --git a/features/step_definitions/user_steps.rb b/features/step_definitions/user_steps.rb index 8fd3f009a..350a3730a 100644 --- a/features/step_definitions/user_steps.rb +++ b/features/step_definitions/user_steps.rb @@ -167,3 +167,22 @@ When /^I view "([^\"]*)"'s first post$/ do |email| post = user.posts.first visit post_path(post) end + +Given /^I visit alice's invitation code url$/ do + @alice ||= Factory(:user, :username => 'alice', :getting_started => false) + invite_code = InvitationCode.find_or_create_by_user_id(@alice.id) + visit invite_code_path(invite_code) +end + +When /^I fill in the new user form$/ do + step 'I fill in "user_username" with "ohai"' + step 'I fill in "user_email" with "ohai@example.com"' + step 'I fill in "user_password" with "secret"' + step 'I fill in "user_password_confirmation" with "secret"' +end + +And /^I should be able to friend Alice$/ do + alice = User.find_by_username 'alice' + step 'I should see "Add contact"' + step "I should see \"#{alice.name}\"" +end diff --git a/lib/email_inviter.rb b/lib/email_inviter.rb new file mode 100644 index 000000000..d5ec92b81 --- /dev/null +++ b/lib/email_inviter.rb @@ -0,0 +1,34 @@ +class EmailInviter + attr_accessor :emails, :message, :inviter, :locale + + def initialize(emails, options={}) + self.message = options[:message] + self.inviter = options[:inviter] + self.locale = options.fetch(:locale, 'en') + self.emails = emails + end + + def emails=(list) + emails = list.split(%r{[,\s]+}) + emails.reject!{|x| x == inviter.email } unless inviter.nil? + @emails = emails + end + + def invitation_code + @invitation_code ||= inviter.nil? ? self.admin_code : inviter.invitation_code + end + + def self.admin_code + "foo" + end + + def send! + self.emails.each{ |email| mail(email)} + end + + private + + def mail(email) + Notifier.invite(email, message, inviter, invitation_code, locale) + end +end \ No newline at end of file diff --git a/spec/controllers/invitations_controller_spec.rb b/spec/controllers/invitations_controller_spec.rb index be3ca2447..b2e6afe50 100644 --- a/spec/controllers/invitations_controller_spec.rb +++ b/spec/controllers/invitations_controller_spec.rb @@ -5,16 +5,11 @@ require 'spec_helper' describe InvitationsController do - include Devise::TestHelpers before do AppConfig[:open_invitations] = true @user = alice - @aspect = @user.aspects.first - @invite = {:invite_message=>"test", :aspects=> @aspect.id.to_s, :email=>"abc@example.com"} - - request.env["devise.mapping"] = Devise.mappings[:user] - Webfinger.stub_chain(:new, :fetch).and_return(Factory(:person)) + @invite = {'email_inviter' => {'message' => "test", 'emails' => "abc@example.com"}} end describe "#create" do @@ -24,121 +19,28 @@ describe InvitationsController do request.env["HTTP_REFERER"]= 'http://test.host/cats/foo' end - it 'saves an invitation' do - expect { - post :create, :user => @invite - }.should change(Invitation, :count).by(1) + it 'creates an EmailInviter' do + inviter = stub(:emails => ['mbs@gmail.com']) + EmailInviter.should_receive(:new).with(@invite['email_inviter']['emails'], @invite['email_inviter']). + and_return(inviter) + post :create, @invite end - it 'handles a comma-separated list of emails' do - expect{ - post :create, :user => @invite.merge( - :email => "foofoofoofoo@example.com, mbs@gmail.com") - }.should change(Invitation, :count).by(2) - end - - it 'handles a comma-separated list of emails with whitespace' do - expect { - post :create, :user => @invite.merge( - :email => "foofoofoofoo@example.com , mbs@gmail.com") - }.should change(Invitation, :count).by(2) - end - - it "allows invitations without if invitations are open" do + it "redirects if invitations are closed" do open_bit = AppConfig[:open_invitations] - AppConfig[:open_invitations] = true + AppConfig[:open_invitations] = false - expect{ - post :create, :user => @invite - }.to change(Invitation, :count).by(1) + post :create, @invite + response.should be_redirect AppConfig[:open_invitations] = open_bit end it 'returns to the previous page on success' do - post :create, :user => @invite + post :create, @invite response.should redirect_to("http://test.host/cats/foo") end - - it 'strips out your own email' do - lambda { - post :create, :user => @invite.merge(:email => @user.email) - }.should_not change(Invitation, :count) - - expect{ - post :create, :user => @invite.merge(:email => "hello@example.org, #{@user.email}") - }.should change(Invitation, :count).by(1) - end end - describe "#email" do - before do - invites = Invitation.batch_invite(["foo@example.com"], :message => "hi", :sender => @user, :aspect => @user.aspects.first, :service => 'email', :language => "en-US") - invites.first.send! - @invited_user = User.find_by_email("foo@example.com") - end - - it "succeeds" do - get :email, :invitation_token => @invited_user.invitation_token - response.should be_success - end - - it "shows an error if there's no such invitation token" do - get :email, :invitation_token => "12345" - response.should render_template(:token_not_found) - end - end - - describe "#update" do - before do - invite = Factory(:invitation, :sender => @user, :service => 'email', :identifier => "a@a.com") - @invited_user = invite.attach_recipient! - - @accept_params = {:user=> - {:password_confirmation =>"password", - :email => "a@a.com", - :username=>"josh", - :password=>"password", - :invitation_token => @invited_user.invitation_token}} - - end - - context 'success' do - let(:invited) {User.find_by_username(@accept_params[:user][:username])} - - it 'creates a user' do - put :update, @accept_params - invited.should_not be_nil - end - - it 'seeds the aspects' do - put :update, @accept_params - invited.aspects.count.should == 4 - end - - it 'adds a contact' do - lambda { - put :update, @accept_params - }.should change(@user.contacts, :count).by(1) - end - end - - context 'failure' do - before do - @fail_params = @accept_params - @fail_params[:user][:username] = @user.username - end - - it 'stays on the invitation accept form' do - put :update, @fail_params - response.location.include?(accept_user_invitation_path).should be_true - end - - it 'keeps the invitation token' do - put :update, @fail_params - response.location.include?("invitation_token=#{@invited_user.invitation_token}").should be_true - end - end - end describe '#new' do it 'renders' do @@ -146,43 +48,4 @@ describe InvitationsController do get :new end end - - describe '#resend' do - before do - sign_in :user, @user - @controller.stub!(:current_user).and_return(@user) - request.env["HTTP_REFERER"]= 'http://test.host/cats/foo' - - invite = Factory(:invitation, :sender => @user, :service => 'email', :identifier => "a@a.com") - @invited_user = invite.attach_recipient! - end - - it 'calls resend invitation if one exists' do - @user.reload.invitations_from_me.count.should == 1 - invitation = @user.invitations_from_me.first - Resque.should_receive(:enqueue) - put :resend, :id => invitation.id - end - - it 'does not send an invitation for a different user' do - invitation2 = Factory(:invitation, :sender => bob, :service => 'email', :identifier => "a@a.com") - - Resque.should_not_receive(:enqueue) - put :resend, :id => invitation2.id - end - end - - - describe '#extract_messages' do - before do - sign_in alice - end - it 'displays a message that tells the user how many invites were sent, and which REJECTED' do - post :create, :user => @invite.merge( - :email => "mbs@gmail.com, foo@bar.com, foo.com, lala@foo, cool@bar.com") - flash[:notice].should_not be_blank - flash[:notice].should =~ /foo\.com/ - flash[:notice].should =~ /lala@foo/ - end - end end diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index c75c379a0..a6b4a2777 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -39,6 +39,17 @@ describe RegistrationsController do flash[:error].should == I18n.t('registrations.closed') response.should redirect_to new_user_session_path end + + it 'does not redirect if there is a valid invite token' do + i = InvitationCode.create(:user => bob) + get :new, :invite => {:token => i.token} + response.should_not be_redirect + end + + it 'does redirect if there is an invalid invite token' do + get :new, :invite => {:token => 'fssdfsd'} + response.should be_redirect + end end describe "#create" do diff --git a/spec/factories.rb b/spec/factories.rb index 56358bb82..4f13b6ec6 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -130,6 +130,12 @@ FactoryGirl.define do end end + factory :invitation_code do + sequence(:token){|n| "sdfsdsf#{n}"} + association :user + count 0 + end + factory :service do |service| nickname "sirrobertking" type "Services::Twitter" @@ -239,4 +245,4 @@ FactoryGirl.define do end factory(:status, :parent => :status_message) -end \ No newline at end of file +end diff --git a/spec/helpers/invitation_codes_helper_spec.rb b/spec/helpers/invitation_codes_helper_spec.rb new file mode 100644 index 000000000..c2dfae147 --- /dev/null +++ b/spec/helpers/invitation_codes_helper_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +# Specs in this file have access to a helper object that includes +# the InvitationCodesHelper. For example: +# +# describe InvitationCodesHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# helper.concat_strings("this","that").should == "this that" +# end +# end +# end +describe InvitationCodesHelper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/lib/email_inviter_spec.rb b/spec/lib/email_inviter_spec.rb new file mode 100644 index 000000000..22e216da0 --- /dev/null +++ b/spec/lib/email_inviter_spec.rb @@ -0,0 +1,68 @@ +require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'email_inviter') + +describe EmailInviter do + before do + @user = stub(:invitation_code => 'coolcodebro', :present? => true, + :email => 'foo@bar.com') + @emails = "mbs333@gmail.com, foo@bar.com maxwell@dude.com" + end + + it 'has a list of emails' do + inviter = EmailInviter.new(@emails) + inviter.emails.should_not be_empty + end + + it 'should parse three emails' do + inviter = EmailInviter.new(@emails) + inviter.emails.count.should == 3 + end + + it 'an optional inviter' do + inviter = EmailInviter.new(@emails, :inviter => @user) + inviter.inviter.should_not be_nil + end + + it 'can have a message' do + message = "you guys suck hard" + inviter = EmailInviter.new("emails", :message => message) + inviter.message.should == message + end + + describe '#emails' do + it 'rejects the inviter email if present' do + inviter = EmailInviter.new(@emails + " #{@user.email}", :inviter => @user) + inviter.emails.should_not include(@user.email) + end + end + + describe 'language' do + it 'defaults to english' do + inviter = EmailInviter.new(@emails) + inviter.locale.should == 'en' + end + + it 'listens to the langauge option' do + inviter = EmailInviter.new(@emails, :locale => 'es') + inviter.locale.should == 'es' + end + end + + describe '#invitation_code' do + it 'delegates to the user if it exists' do + inviter = EmailInviter.new(@emails, :inviter => @user) + inviter.invitation_code.should == @user.invitation_code + end + + it 'calls admin_code if it does not' do + inviter = EmailInviter.new(@emails) + inviter.should_receive(:admin_code).and_return("foo") + inviter.invitation_code.should == "foo" + end + end + + describe 'admin code' do + it 'is hella pending' do + pending + end + end +end \ No newline at end of file diff --git a/spec/models/invitation_code_spec.rb b/spec/models/invitation_code_spec.rb new file mode 100644 index 000000000..142cc2944 --- /dev/null +++ b/spec/models/invitation_code_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +describe InvitationCode do + it 'has a valid factory' do + Factory(:invitation_code).should be_valid + end +end diff --git a/spec/models/invitation_spec.rb b/spec/models/invitation_spec.rb index ad19d8b59..c379ee819 100644 --- a/spec/models/invitation_spec.rb +++ b/spec/models/invitation_spec.rb @@ -48,44 +48,6 @@ describe Invitation do @invitation.message.should == "!" end - describe 'the invite process' do - before do - end - - it 'works for a new user' do - invite = Invitation.new(:sender => alice, :aspect => alice.aspects.first, :service => 'email', :identifier => 'foo@bar.com', :language => alice.language) - lambda { - invite.save - invite.send! - }.should change(User, :count).by(1) - end - - it 'works for a current user(with the right email)' do - invite = Invitation.create(:sender => alice, :aspect => alice.aspects.first, :service => 'email', :identifier => bob.email, :language => alice.language) - lambda { - invite.send! - }.should_not change(User, :count) - end - - it 'works for a current user(with the same fb id)' do - bob.services << Factory.build(:service, :type => "Services::Facebook") - invite = Invitation.create(:sender => alice, :aspect => alice.aspects.first, :service => 'facebook', :identifier => bob.services.first.uid) - lambda { - invite.send! - }.should_not change(User, :count) - end - end - - describe '#convert_to_admin!' do - it 'reset sender and aspect to nil, and sets admin flag to true' do - invite = Factory(:invitation) - invite.convert_to_admin! - invite.reload - invite.admin?.should be_true - invite.sender_id.should be_nil - invite.aspect_id.should be_nil - end - end describe '.batch_invite' do before do @@ -109,63 +71,4 @@ describe Invitation do end end - - describe 'send' do - before do - @invitation = Factory(:invitation, :sender => alice, :aspect => alice.aspects.first, :service => 'email', :identifier => 'a@a.com' , :language => alice.language) - end - - it 'sends an email' do - lambda { - @invitation.send! - }.should change(Devise.mailer.deliveries, :count).by(1) - end - - it 'sends an email with from header' do - @invitation.send! - Devise.mailer.deliveries.first.from.should_not be_blank - end - - it 'sends an email with from header' do - @invitation.send! - Devise.mailer.deliveries.first.to.should == ["a@a.com"] - end - - context "re-send" do - it 'sends another email' do - lambda { - @invitation.resend - }.should change(Devise.mailer.deliveries, :count).by(1) - end - end - end - - describe '#recipient_identifier' do - it 'calls email if the invitation_service is email' do - email = 'abc@abc.com' - invitation = Factory(:invitation, :sender => alice, :service => 'email', :identifier => email, :aspect => alice.aspects.first, :language => alice.language) - invitation.recipient_identifier.should == email - end - - context 'facebook' do - before do - @uid = '23526464' - @service = "facebook" - alice.services << Services::Facebook.new(:uid => "13234895") - alice.reload.services(true).first.service_users.create(:uid => @uid, :photo_url => 'url', :name => "Remote User") - end - - it 'gets the name if the invitation_service is facebook' do - invitation = Factory(:invitation, :sender => alice, :identifier => @uid, :service => @service, :aspect => alice.aspects.first) - invitation.recipient_identifier.should == "Remote User" - end - - it 'does not error if the facebook user is not recorded' do - invitation = Factory(:invitation, :sender => alice, :identifier => @uid, :service => @service, :aspect => alice.aspects.first) - alice.services.first.service_users.delete_all - invitation.recipient_identifier.should == "A Facebook user" - end - end - end -end - +end \ No newline at end of file diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f4f286622..1738beda5 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -33,28 +33,6 @@ describe User do alice.save end end - - describe '#infer_email_from_invitation_provider' do - it 'sets corresponding email if invitation_service is email' do - addr = '12345@alice.com' - alice.invitation_service = 'email' - alice.invitation_identifier = addr - - lambda { - alice.infer_email_from_invitation_provider - }.should change(alice, :email) - end - - it 'does not set an email if invitation_service is not email' do - addr = '1233123' - alice.invitation_service = 'facebook' - alice.invitation_identifier = addr - - lambda { - alice.infer_email_from_invitation_provider - }.should_not change(alice, :email) - end - end end describe 'hidden_shareables' do @@ -400,70 +378,13 @@ describe User do end end - describe '.find_by_invitation' do - let(:invited_user) { - inv = Factory.build(:invitation, :recipient => @recipient, :service => @type, :identifier => @identifier) - User.find_by_invitation(inv) - } - context 'send a request to an existing' do - before do - @recipient = alice - end - - context 'active user' do - it 'by service' do - @type = 'facebook' - @identifier = '123456' - - @recipient.services << Services::Facebook.new(:uid => @identifier) - @recipient.save - - invited_user.should == @recipient - end - - it 'by email' do - @type = 'email' - @identifier = alice.email - - invited_user.should == @recipient - end - end - - context 'invited user' do - it 'by service and identifier' do - @identifier = alice.email - @type = 'email' - invited_user.should == alice - end - end - - context 'not on server (not yet invited)' do - it 'returns nil' do - @recipient = nil - @identifier = 'foo@bar.com' - @type = 'email' - invited_user.should be_nil - end - end - end - end - - describe '.find_or_create_by_invitation' - - describe '.create_from_invitation!' do - before do - @identifier = 'max@foobar.com' - @inv = Factory.build(:invitation, :admin => true, :service => 'email', :identifier => @identifier) - @user = User.create_from_invitation!(@inv) - end - - it 'creates a persisted user' do - @user.should be_persisted - end - - it 'sets the email if the service is email' do - @user.email.should == @inv.identifier + describe '#process_invite_acceptence' do + it 'sets the inviter on user' do + inv = InvitationCode.create(:user => bob) + user = Factory(:user) + user.process_invite_acceptence(inv) + user.invited_by_id.should == bob.id end end @@ -842,54 +763,6 @@ describe User do end end - describe "#accept_invitation!" do - before do - fantasy_resque do - @invitation = Factory(:invitation, :sender => eve, :identifier => 'invitee@example.org', :aspect => eve.aspects.first) - end - - @invitation.reload - @form_params = { - :invitation_token => "abc", - :email => "a@a.com", - :username => "user", - :password => "secret", - :password_confirmation => "secret", - :person => { - :profile => {:first_name => "Bob", :last_name => "Smith"} - } - } - end - - context 'after invitation acceptance' do - it 'destroys the invitations' do - user = @invitation.recipient.accept_invitation!(@form_params) - user.invitations_to_me.count.should == 0 - end - - it "should create the person with the passed in params" do - lambda { - @invitation.recipient.accept_invitation!(@form_params) - }.should change(Person, :count).by(1) - end - - it 'resolves incoming invitations into contact requests' do - user = @invitation.recipient.accept_invitation!(@form_params) - eve.contacts.where(:person_id => user.person.id).count.should == 1 - end - end - - context 'from an admin' do - it 'should work' do - i = nil - fantasy_resque do - i = Invitation.create!(:admin => true, :service => 'email', :identifier => "new_invitee@example.com") - end - i.reload - i.recipient.accept_invitation!(@form_params) - end - end - end describe '#retract' do before do