Commit 29cfd33d949d21d67f3892473c24d4f0a127dfe6

Authored by Jason Hollingsworth
1 parent d41e404e

Add email aliases for users

Emails are used to associate commits with users. The emails
are not verified and don't have to be valid email addresses. They
are assigned on a first come, first serve basis.

Notifications are sent when an email is added.
app/controllers/profiles/emails_controller.rb 0 → 100644
@@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
  1 +class Profiles::EmailsController < ApplicationController
  2 + layout "profile"
  3 +
  4 + def index
  5 + @primary = current_user.email
  6 + @emails = current_user.emails
  7 + end
  8 +
  9 + def create
  10 + @email = current_user.emails.new(params[:email])
  11 +
  12 + flash[:alert] = @email.errors.full_messages.first unless @email.save
  13 +
  14 + redirect_to profile_emails_url
  15 + end
  16 +
  17 + def destroy
  18 + @email = current_user.emails.find(params[:id])
  19 + @email.destroy
  20 +
  21 + respond_to do |format|
  22 + format.html { redirect_to profile_emails_url }
  23 + format.js { render nothing: true }
  24 + end
  25 + end
  26 +end
app/helpers/commits_helper.rb
@@ -122,17 +122,18 @@ module CommitsHelper @@ -122,17 +122,18 @@ module CommitsHelper
122 def commit_person_link(commit, options = {}) 122 def commit_person_link(commit, options = {})
123 source_name = commit.send "#{options[:source]}_name".to_sym 123 source_name = commit.send "#{options[:source]}_name".to_sym
124 source_email = commit.send "#{options[:source]}_email".to_sym 124 source_email = commit.send "#{options[:source]}_email".to_sym
  125 +
  126 + user = User.find_for_commit(source_email, source_name)
  127 + person_name = user.nil? ? source_name : user.name
  128 + person_email = user.nil? ? source_email : user.email
  129 +
125 text = if options[:avatar] 130 text = if options[:avatar]
126 - avatar = image_tag(avatar_icon(source_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "")  
127 - %Q{#{avatar} <span class="commit-#{options[:source]}-name">#{source_name}</span>} 131 + avatar = image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "")
  132 + %Q{#{avatar} <span class="commit-#{options[:source]}-name">#{person_name}</span>}
128 else 133 else
129 - source_name 134 + person_name
130 end 135 end
131 136
132 - # Prefer email match over name match  
133 - user = User.where(email: source_email).first  
134 - user ||= User.where(name: source_name).first  
135 -  
136 options = { 137 options = {
137 class: "commit-#{options[:source]}-link has_tooltip", 138 class: "commit-#{options[:source]}-link has_tooltip",
138 data: { :'original-title' => sanitize(source_email) } 139 data: { :'original-title' => sanitize(source_email) }
app/mailers/emails/profile.rb
@@ -6,6 +6,12 @@ module Emails @@ -6,6 +6,12 @@ module Emails
6 mail(to: @user.email, subject: subject("Account was created for you")) 6 mail(to: @user.email, subject: subject("Account was created for you"))
7 end 7 end
8 8
  9 + def new_email_email(email_id)
  10 + @email = Email.find(email_id)
  11 + @user = @email.user
  12 + mail(to: @user.email, subject: subject("Email was added to your account"))
  13 + end
  14 +
9 def new_ssh_key_email(key_id) 15 def new_ssh_key_email(key_id)
10 @key = Key.find(key_id) 16 @key = Key.find(key_id)
11 @user = @key.user 17 @user = @key.user
app/models/email.rb 0 → 100644
@@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
  1 +# == Schema Information
  2 +#
  3 +# Table name: emails
  4 +#
  5 +# id :integer not null, primary key
  6 +# user_id :integer not null
  7 +# email :string not null
  8 +# created_at :datetime not null
  9 +class Email < ActiveRecord::Base
  10 + attr_accessible :email, :user_id
  11 +
  12 + #
  13 + # Relations
  14 + #
  15 + belongs_to :user
  16 +
  17 + #
  18 + # Validations
  19 + #
  20 + validates :user_id, presence: true
  21 + validates :email, presence: true, email: { strict_mode: true }, uniqueness: true
  22 + validate :unique_email, if: ->(email) { email.email_changed? }
  23 +
  24 + before_validation :cleanup_email
  25 +
  26 + def cleanup_email
  27 + self.email = self.email.downcase.strip
  28 + end
  29 +
  30 + def unique_email
  31 + self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email)
  32 + end
  33 +end
0 \ No newline at end of file 34 \ No newline at end of file
app/models/user.rb
@@ -78,6 +78,7 @@ class User &lt; ActiveRecord::Base @@ -78,6 +78,7 @@ class User &lt; ActiveRecord::Base
78 78
79 # Profile 79 # Profile
80 has_many :keys, dependent: :destroy 80 has_many :keys, dependent: :destroy
  81 + has_many :emails, dependent: :destroy
81 82
82 # Groups 83 # Groups
83 has_many :users_groups, dependent: :destroy 84 has_many :users_groups, dependent: :destroy
@@ -116,6 +117,7 @@ class User &lt; ActiveRecord::Base @@ -116,6 +117,7 @@ class User &lt; ActiveRecord::Base
116 validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true 117 validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true
117 validate :namespace_uniq, if: ->(user) { user.username_changed? } 118 validate :namespace_uniq, if: ->(user) { user.username_changed? }
118 validate :avatar_type, if: ->(user) { user.avatar_changed? } 119 validate :avatar_type, if: ->(user) { user.avatar_changed? }
  120 + validate :unique_email, if: ->(user) { user.email_changed? }
119 validates :avatar, file_size: { maximum: 100.kilobytes.to_i } 121 validates :avatar, file_size: { maximum: 100.kilobytes.to_i }
120 122
121 before_validation :generate_password, on: :create 123 before_validation :generate_password, on: :create
@@ -183,6 +185,13 @@ class User &lt; ActiveRecord::Base @@ -183,6 +185,13 @@ class User &lt; ActiveRecord::Base
183 where(conditions).first 185 where(conditions).first
184 end 186 end
185 end 187 end
  188 +
  189 + def find_for_commit(email, name)
  190 + # Prefer email match over name match
  191 + User.where(email: email).first ||
  192 + User.joins(:emails).where(emails: { email: email }).first ||
  193 + User.where(name: name).first
  194 + end
186 195
187 def filter filter_name 196 def filter filter_name
188 case filter_name 197 case filter_name
@@ -250,6 +259,10 @@ class User &lt; ActiveRecord::Base @@ -250,6 +259,10 @@ class User &lt; ActiveRecord::Base
250 end 259 end
251 end 260 end
252 261
  262 + def unique_email
  263 + self.errors.add(:email, 'has already been taken') if Email.exists?(email: self.email)
  264 + end
  265 +
253 # Groups user has access to 266 # Groups user has access to
254 def authorized_groups 267 def authorized_groups
255 @authorized_groups ||= begin 268 @authorized_groups ||= begin
app/observers/email_observer.rb 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +class EmailObserver < BaseObserver
  2 + def after_create(email)
  3 + notification.new_email(email)
  4 + end
  5 +end
app/services/git_push_service.rb
@@ -188,8 +188,6 @@ class GitPushService @@ -188,8 +188,6 @@ class GitPushService
188 end 188 end
189 189
190 def commit_user commit 190 def commit_user commit
191 - User.where(email: commit.author_email).first ||  
192 - User.where(name: commit.author_name).first ||  
193 - user 191 + User.find_for_commit(commit.author_email, commit.author_name) || user
194 end 192 end
195 end 193 end
app/services/notification_service.rb
@@ -17,6 +17,13 @@ class NotificationService @@ -17,6 +17,13 @@ class NotificationService
17 end 17 end
18 end 18 end
19 19
  20 + # Always notify user about email added to profile
  21 + def new_email(email)
  22 + if email.user
  23 + mailer.new_email_email(email.id)
  24 + end
  25 + end
  26 +
20 # When create an issue we should send next emails: 27 # When create an issue we should send next emails:
21 # 28 #
22 # * issue assignee if their notification level is not Disabled 29 # * issue assignee if their notification level is not Disabled
app/views/layouts/nav/_profile.html.haml
@@ -4,6 +4,10 @@ @@ -4,6 +4,10 @@
4 %i.icon-home 4 %i.icon-home
5 = nav_link(controller: :accounts) do 5 = nav_link(controller: :accounts) do
6 = link_to "Account", profile_account_path 6 = link_to "Account", profile_account_path
  7 + = nav_link(controller: :emails) do
  8 + = link_to profile_emails_path do
  9 + Emails
  10 + %span.count= current_user.emails.count + 1
7 - unless current_user.ldap_user? 11 - unless current_user.ldap_user?
8 = nav_link(controller: :passwords) do 12 = nav_link(controller: :passwords) do
9 = link_to "Password", edit_profile_password_path 13 = link_to "Password", edit_profile_password_path
app/views/notify/new_email_email.html.haml 0 → 100644
@@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
  1 +%p
  2 + Hi #{@user.name}!
  3 +%p
  4 + A new email was added to your account:
  5 +%p
  6 + email:
  7 + %code= @email.email
  8 +%p
  9 + If this email was added in error, you can remove it here:
  10 + = link_to "Emails", profile_emails_url
app/views/notify/new_email_email.text.erb 0 → 100644
@@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
  1 +Hi <%= @user.name %>!
  2 +
  3 +A new email was added to your account:
  4 +
  5 +email.................. <%= @email.email %>
  6 +
  7 +If this email was added in error, you can remove it here: <%= profile_emails_url %>
app/views/profiles/emails/index.html.haml 0 → 100644
@@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
  1 +%h3.page-title
  2 + My Email Addresses
  3 +%p.light
  4 + Your
  5 + %b Primary Email
  6 + will be used for account notifications, avatar detection and web based operations, such as edits and merges. All email addresses will be used to identify your commits.
  7 +
  8 +.ui-box
  9 + .title
  10 + Emails (#{@emails.count + 1})
  11 + %ul.well-list#emails-table
  12 + %li
  13 + %strong= @primary
  14 + %span.label.label-success Primary Email
  15 + - @emails.each do |email|
  16 + %li
  17 + %strong= email.email
  18 + %span.cgray
  19 + added #{time_ago_with_tooltip(email.created_at)}
  20 + = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-small btn-remove pull-right'
  21 +
  22 +%h3.page-title Add Email Address
  23 += form_for 'email', url: profile_emails_path, html: { class: 'form-horizontal' } do |f|
  24 + .form-group
  25 + = f.label :email, class: 'control-label'
  26 + .col-sm-10
  27 + = f.text_field :email, class: 'form-control'
  28 + .form-actions
  29 + = f.submit 'Add', class: 'btn btn-create'
0 \ No newline at end of file 30 \ No newline at end of file
config/routes.rb
@@ -124,6 +124,7 @@ Gitlab::Application.routes.draw do @@ -124,6 +124,7 @@ Gitlab::Application.routes.draw do
124 end 124 end
125 end 125 end
126 resources :keys 126 resources :keys
  127 + resources :emails, only: [:index, :create, :destroy]
127 resources :groups, only: [:index] do 128 resources :groups, only: [:index] do
128 member do 129 member do
129 delete :leave 130 delete :leave
db/migrate/20140209025651_create_emails.rb 0 → 100644
@@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
  1 +class CreateEmails < ActiveRecord::Migration
  2 + def change
  3 + create_table :emails do |t|
  4 + t.integer :user_id, null: false
  5 + t.string :email, null: false
  6 +
  7 + t.timestamps
  8 + end
  9 +
  10 + add_index :emails, :user_id
  11 + add_index :emails, :email, unique: true
  12 + end
  13 +end
@@ -11,7 +11,7 @@ @@ -11,7 +11,7 @@
11 # 11 #
12 # It's strongly recommended that you check this file into your version control system. 12 # It's strongly recommended that you check this file into your version control system.
13 13
14 -ActiveRecord::Schema.define(version: 20140127170938) do 14 +ActiveRecord::Schema.define(version: 20140209025651) do
15 15
16 create_table "broadcast_messages", force: true do |t| 16 create_table "broadcast_messages", force: true do |t|
17 t.text "message", null: false 17 t.text "message", null: false
@@ -33,6 +33,16 @@ ActiveRecord::Schema.define(version: 20140127170938) do @@ -33,6 +33,16 @@ ActiveRecord::Schema.define(version: 20140127170938) do
33 33
34 add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree 34 add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree
35 35
  36 + create_table "emails", force: true do |t|
  37 + t.integer "user_id", null: false
  38 + t.string "email", null: false
  39 + t.datetime "created_at"
  40 + t.datetime "updated_at"
  41 + end
  42 +
  43 + add_index "emails", ["email"], name: "index_emails_on_email", unique: true, using: :btree
  44 + add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree
  45 +
36 create_table "events", force: true do |t| 46 create_table "events", force: true do |t|
37 t.string "target_type" 47 t.string "target_type"
38 t.integer "target_id" 48 t.integer "target_id"
@@ -66,8 +76,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do @@ -66,8 +76,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do
66 t.integer "assignee_id" 76 t.integer "assignee_id"
67 t.integer "author_id" 77 t.integer "author_id"
68 t.integer "project_id" 78 t.integer "project_id"
69 - t.datetime "created_at"  
70 - t.datetime "updated_at" 79 + t.datetime "created_at", null: false
  80 + t.datetime "updated_at", null: false
71 t.integer "position", default: 0 81 t.integer "position", default: 0
72 t.string "branch_name" 82 t.string "branch_name"
73 t.text "description" 83 t.text "description"
@@ -85,8 +95,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do @@ -85,8 +95,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do
85 95
86 create_table "keys", force: true do |t| 96 create_table "keys", force: true do |t|
87 t.integer "user_id" 97 t.integer "user_id"
88 - t.datetime "created_at"  
89 - t.datetime "updated_at" 98 + t.datetime "created_at", null: false
  99 + t.datetime "updated_at", null: false
90 t.text "key" 100 t.text "key"
91 t.string "title" 101 t.string "title"
92 t.string "type" 102 t.string "type"
@@ -111,8 +121,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do @@ -111,8 +121,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do
111 t.integer "author_id" 121 t.integer "author_id"
112 t.integer "assignee_id" 122 t.integer "assignee_id"
113 t.string "title" 123 t.string "title"
114 - t.datetime "created_at"  
115 - t.datetime "updated_at" 124 + t.datetime "created_at", null: false
  125 + t.datetime "updated_at", null: false
116 t.integer "milestone_id" 126 t.integer "milestone_id"
117 t.string "state" 127 t.string "state"
118 t.string "merge_status" 128 t.string "merge_status"
@@ -164,8 +174,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do @@ -164,8 +174,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do
164 t.text "note" 174 t.text "note"
165 t.string "noteable_type" 175 t.string "noteable_type"
166 t.integer "author_id" 176 t.integer "author_id"
167 - t.datetime "created_at"  
168 - t.datetime "updated_at" 177 + t.datetime "created_at", null: false
  178 + t.datetime "updated_at", null: false
169 t.integer "project_id" 179 t.integer "project_id"
170 t.string "attachment" 180 t.string "attachment"
171 t.string "line_code" 181 t.string "line_code"
@@ -187,8 +197,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do @@ -187,8 +197,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do
187 t.string "name" 197 t.string "name"
188 t.string "path" 198 t.string "path"
189 t.text "description" 199 t.text "description"
190 - t.datetime "created_at"  
191 - t.datetime "updated_at" 200 + t.datetime "created_at", null: false
  201 + t.datetime "updated_at", null: false
192 t.integer "creator_id" 202 t.integer "creator_id"
193 t.boolean "issues_enabled", default: true, null: false 203 t.boolean "issues_enabled", default: true, null: false
194 t.boolean "wall_enabled", default: true, null: false 204 t.boolean "wall_enabled", default: true, null: false
@@ -239,8 +249,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do @@ -239,8 +249,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do
239 t.text "content", limit: 2147483647 249 t.text "content", limit: 2147483647
240 t.integer "author_id", null: false 250 t.integer "author_id", null: false
241 t.integer "project_id" 251 t.integer "project_id"
242 - t.datetime "created_at"  
243 - t.datetime "updated_at" 252 + t.datetime "created_at", null: false
  253 + t.datetime "updated_at", null: false
244 t.string "file_name" 254 t.string "file_name"
245 t.datetime "expires_at" 255 t.datetime "expires_at"
246 t.boolean "private", default: true, null: false 256 t.boolean "private", default: true, null: false
@@ -262,42 +272,45 @@ ActiveRecord::Schema.define(version: 20140127170938) do @@ -262,42 +272,45 @@ ActiveRecord::Schema.define(version: 20140127170938) do
262 t.datetime "created_at" 272 t.datetime "created_at"
263 end 273 end
264 274
  275 + add_index "taggings", ["tag_id"], name: "index_taggings_on_tag_id", using: :btree
  276 + add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
  277 +
265 create_table "tags", force: true do |t| 278 create_table "tags", force: true do |t|
266 t.string "name" 279 t.string "name"
267 end 280 end
268 281
269 create_table "users", force: true do |t| 282 create_table "users", force: true do |t|
270 - t.string "email", default: "", null: false  
271 - t.string "encrypted_password", limit: 128, default: "", null: false 283 + t.string "email", default: "", null: false
  284 + t.string "encrypted_password", default: "", null: false
272 t.string "reset_password_token" 285 t.string "reset_password_token"
273 t.datetime "reset_password_sent_at" 286 t.datetime "reset_password_sent_at"
274 t.datetime "remember_created_at" 287 t.datetime "remember_created_at"
275 - t.integer "sign_in_count", default: 0 288 + t.integer "sign_in_count", default: 0
276 t.datetime "current_sign_in_at" 289 t.datetime "current_sign_in_at"
277 t.datetime "last_sign_in_at" 290 t.datetime "last_sign_in_at"
278 t.string "current_sign_in_ip" 291 t.string "current_sign_in_ip"
279 t.string "last_sign_in_ip" 292 t.string "last_sign_in_ip"
280 - t.datetime "created_at"  
281 - t.datetime "updated_at" 293 + t.datetime "created_at", null: false
  294 + t.datetime "updated_at", null: false
282 t.string "name" 295 t.string "name"
283 - t.boolean "admin", default: false, null: false  
284 - t.integer "projects_limit", default: 10  
285 - t.string "skype", default: "", null: false  
286 - t.string "linkedin", default: "", null: false  
287 - t.string "twitter", default: "", null: false 296 + t.boolean "admin", default: false, null: false
  297 + t.integer "projects_limit", default: 10
  298 + t.string "skype", default: "", null: false
  299 + t.string "linkedin", default: "", null: false
  300 + t.string "twitter", default: "", null: false
288 t.string "authentication_token" 301 t.string "authentication_token"
289 - t.integer "theme_id", default: 1, null: false 302 + t.integer "theme_id", default: 1, null: false
290 t.string "bio" 303 t.string "bio"
291 - t.integer "failed_attempts", default: 0 304 + t.integer "failed_attempts", default: 0
292 t.datetime "locked_at" 305 t.datetime "locked_at"
293 t.string "extern_uid" 306 t.string "extern_uid"
294 t.string "provider" 307 t.string "provider"
295 t.string "username" 308 t.string "username"
296 - t.boolean "can_create_group", default: true, null: false  
297 - t.boolean "can_create_team", default: true, null: false 309 + t.boolean "can_create_group", default: true, null: false
  310 + t.boolean "can_create_team", default: true, null: false
298 t.string "state" 311 t.string "state"
299 - t.integer "color_scheme_id", default: 1, null: false  
300 - t.integer "notification_level", default: 1, null: false 312 + t.integer "color_scheme_id", default: 1, null: false
  313 + t.integer "notification_level", default: 1, null: false
301 t.datetime "password_expires_at" 314 t.datetime "password_expires_at"
302 t.integer "created_by_id" 315 t.integer "created_by_id"
303 t.string "avatar" 316 t.string "avatar"
@@ -305,14 +318,15 @@ ActiveRecord::Schema.define(version: 20140127170938) do @@ -305,14 +318,15 @@ ActiveRecord::Schema.define(version: 20140127170938) do
305 t.datetime "confirmed_at" 318 t.datetime "confirmed_at"
306 t.datetime "confirmation_sent_at" 319 t.datetime "confirmation_sent_at"
307 t.string "unconfirmed_email" 320 t.string "unconfirmed_email"
308 - t.boolean "hide_no_ssh_key", default: false  
309 - t.string "website_url", default: "", null: false 321 + t.boolean "hide_no_ssh_key", default: false
  322 + t.string "website_url", default: "", null: false
310 end 323 end
311 324
312 add_index "users", ["admin"], name: "index_users_on_admin", using: :btree 325 add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
313 add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree 326 add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree
314 add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree 327 add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
315 add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree 328 add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
  329 + add_index "users", ["extern_uid", "provider"], name: "index_users_on_extern_uid_and_provider", unique: true, using: :btree
316 add_index "users", ["name"], name: "index_users_on_name", using: :btree 330 add_index "users", ["name"], name: "index_users_on_name", using: :btree
317 add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree 331 add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
318 add_index "users", ["username"], name: "index_users_on_username", using: :btree 332 add_index "users", ["username"], name: "index_users_on_username", using: :btree
@@ -331,8 +345,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do @@ -331,8 +345,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do
331 create_table "users_projects", force: true do |t| 345 create_table "users_projects", force: true do |t|
332 t.integer "user_id", null: false 346 t.integer "user_id", null: false
333 t.integer "project_id", null: false 347 t.integer "project_id", null: false
334 - t.datetime "created_at"  
335 - t.datetime "updated_at" 348 + t.datetime "created_at", null: false
  349 + t.datetime "updated_at", null: false
336 t.integer "project_access", default: 0, null: false 350 t.integer "project_access", default: 0, null: false
337 t.integer "notification_level", default: 3, null: false 351 t.integer "notification_level", default: 3, null: false
338 end 352 end
@@ -344,8 +358,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do @@ -344,8 +358,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do
344 create_table "web_hooks", force: true do |t| 358 create_table "web_hooks", force: true do |t|
345 t.string "url" 359 t.string "url"
346 t.integer "project_id" 360 t.integer "project_id"
347 - t.datetime "created_at"  
348 - t.datetime "updated_at" 361 + t.datetime "created_at", null: false
  362 + t.datetime "updated_at", null: false
349 t.string "type", default: "ProjectHook" 363 t.string "type", default: "ProjectHook"
350 t.integer "service_id" 364 t.integer "service_id"
351 t.boolean "push_events", default: true, null: false 365 t.boolean "push_events", default: true, null: false
features/profile/emails.feature 0 → 100644
@@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
  1 +Feature: Profile Emails
  2 + Background:
  3 + Given I sign in as a user
  4 + And I visit profile emails page
  5 +
  6 + Scenario: I should see emails
  7 + Then I should see my emails
  8 +
  9 + Scenario: Add new email
  10 + Given I submit new email "my@email.com"
  11 + Then I should see new email "my@email.com"
  12 + And I should see my emails
  13 +
  14 + Scenario: Add duplicate email
  15 + Given I submit duplicate email @user.email
  16 + Then I should not have @user.email added
  17 + And I should see my emails
  18 +
  19 + Scenario: Remove email
  20 + Given I submit new email "my@email.com"
  21 + Then I should see new email "my@email.com"
  22 + And I should see my emails
  23 + Then I click link "Remove" for "my@email.com"
  24 + Then I should not see email "my@email.com"
  25 + And I should see my emails
features/project/commits/commits_user_lookup.feature 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +Feature: Project Browse Commits User Lookup
  2 + Background:
  3 + Given I sign in as a user
  4 + And I own a project
  5 + And I have the user that authored the commits
  6 + And I visit my project's commits page
  7 +
  8 + Scenario: I browse commit from list
  9 + Given I click on commit link
  10 + Then I see commit info
  11 +
  12 + Scenario: I browse another commit from list
  13 + Given I click on another commit link
  14 + Then I see other commit info
0 \ No newline at end of file 15 \ No newline at end of file
features/steps/profile/profile_emails.rb 0 → 100644
@@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
  1 +class ProfileEmails < Spinach::FeatureSteps
  2 + include SharedAuthentication
  3 +
  4 + Then 'I visit profile emails page' do
  5 + visit profile_emails_path
  6 + end
  7 +
  8 + Then 'I should see my emails' do
  9 + page.should have_content(@user.email)
  10 + @user.emails.each do |email|
  11 + page.should have_content(email.email)
  12 + end
  13 + end
  14 +
  15 + And 'I submit new email "my@email.com"' do
  16 + fill_in "email_email", with: "my@email.com"
  17 + click_button "Add"
  18 + end
  19 +
  20 + Then 'I should see new email "my@email.com"' do
  21 + email = @user.emails.find_by(email: "my@email.com")
  22 + email.should_not be_nil
  23 + page.should have_content("my@email.com")
  24 + end
  25 +
  26 + Then 'I should not see email "my@email.com"' do
  27 + email = @user.emails.find_by(email: "my@email.com")
  28 + email.should be_nil
  29 + page.should_not have_content("my@email.com")
  30 + end
  31 +
  32 + Then 'I click link "Remove" for "my@email.com"' do
  33 + # there should only be one remove button at this time
  34 + click_link "Remove"
  35 + # force these to reload as they have been cached
  36 + @user.emails.reload
  37 + end
  38 +
  39 + And 'I submit duplicate email @user.email' do
  40 + fill_in "email_email", with: @user.email
  41 + click_button "Add"
  42 + end
  43 +
  44 + Then 'I should not have @user.email added' do
  45 + email = @user.emails.find_by(email: @user.email)
  46 + email.should be_nil
  47 + end
  48 +end
features/steps/project/project_browse_commits_user_lookup.rb 0 → 100644
@@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
  1 +class ProjectBrowseCommitsUserLookup < Spinach::FeatureSteps
  2 + include SharedAuthentication
  3 + include SharedProject
  4 + include SharedPaths
  5 +
  6 + Given 'I have the user that authored the commits' do
  7 + @user = create(:user, email: 'dmitriy.zaporozhets@gmail.com')
  8 + create(:email, { user: @user, email: 'dzaporozhets@sphereconsultinginc.com' })
  9 + end
  10 +
  11 + Given 'I click on commit link' do
  12 + visit project_commit_path(@project, ValidCommit::ID)
  13 + end
  14 +
  15 + Given 'I click on another commit link' do
  16 + visit project_commit_path(@project, ValidCommitWithAltEmail::ID)
  17 + end
  18 +
  19 + Then 'I see commit info' do
  20 + page.should have_content ValidCommit::MESSAGE
  21 + check_author_link(ValidCommit::AUTHOR_EMAIL)
  22 + end
  23 +
  24 + Then 'I see other commit info' do
  25 + page.should have_content ValidCommitWithAltEmail::MESSAGE
  26 + check_author_link(ValidCommitWithAltEmail::AUTHOR_EMAIL)
  27 + end
  28 +
  29 + def check_author_link(email)
  30 + author_link = find('.commit-author-link')
  31 + author_link['href'].should == user_path(@user)
  32 + author_link['data-original-title'].should == email
  33 + find('.commit-author-name').text.should == @user.name
  34 + end
  35 +end
features/support/env.rb
@@ -15,7 +15,7 @@ require &#39;spinach/capybara&#39; @@ -15,7 +15,7 @@ require &#39;spinach/capybara&#39;
15 require 'sidekiq/testing/inline' 15 require 'sidekiq/testing/inline'
16 16
17 17
18 -%w(valid_commit big_commits select2_helper test_env).each do |f| 18 +%w(valid_commit valid_commit_with_alt_email big_commits select2_helper test_env).each do |f|
19 require Rails.root.join('spec', 'support', f) 19 require Rails.root.join('spec', 'support', f)
20 end 20 end
21 21
spec/factories.rb
@@ -219,6 +219,19 @@ FactoryGirl.define do @@ -219,6 +219,19 @@ FactoryGirl.define do
219 end 219 end
220 end 220 end
221 end 221 end
  222 +
  223 + factory :email do
  224 + user
  225 + email do
  226 + Faker::Internet.email('alias')
  227 + end
  228 +
  229 + factory :another_email do
  230 + email do
  231 + Faker::Internet.email('another.alias')
  232 + end
  233 + end
  234 + end
222 235
223 factory :milestone do 236 factory :milestone do
224 title 237 title
spec/mailers/notify_spec.rb
@@ -90,6 +90,28 @@ describe Notify do @@ -90,6 +90,28 @@ describe Notify do
90 end 90 end
91 end 91 end
92 92
  93 + describe 'user added email' do
  94 + let(:email) { create(:email) }
  95 +
  96 + subject { Notify.new_email_email(email.id) }
  97 +
  98 + it 'is sent to the new user' do
  99 + should deliver_to email.user.email
  100 + end
  101 +
  102 + it 'has the correct subject' do
  103 + should have_subject /^gitlab \| Email was added to your account$/i
  104 + end
  105 +
  106 + it 'contains the new email address' do
  107 + should have_body_text /#{email.email}/
  108 + end
  109 +
  110 + it 'includes a link to emails page' do
  111 + should have_body_text /#{profile_emails_path}/
  112 + end
  113 + end
  114 +
93 context 'for a project' do 115 context 'for a project' do
94 describe 'items that are assignable, the email' do 116 describe 'items that are assignable, the email' do
95 let(:assignee) { create(:user, email: 'assignee@example.com') } 117 let(:assignee) { create(:user, email: 'assignee@example.com') }
spec/observers/email_observer_spec.rb 0 → 100644
@@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
  1 +require 'spec_helper'
  2 +
  3 +describe EmailObserver do
  4 + let(:email) { create(:email) }
  5 +
  6 + before { subject.stub(notification: double('NotificationService').as_null_object) }
  7 +
  8 + subject { EmailObserver.instance }
  9 +
  10 + describe '#after_create' do
  11 + it 'trigger notification to send emails' do
  12 + subject.should_receive(:notification)
  13 +
  14 + subject.after_create(email)
  15 + end
  16 + end
  17 +end
spec/routing/routing_spec.rb
@@ -183,6 +183,23 @@ describe Profiles::KeysController, &quot;routing&quot; do @@ -183,6 +183,23 @@ describe Profiles::KeysController, &quot;routing&quot; do
183 end 183 end
184 end 184 end
185 185
  186 +# emails GET /emails(.:format) emails#index
  187 +# POST /keys(.:format) emails#create
  188 +# DELETE /keys/:id(.:format) keys#destroy
  189 +describe Profiles::EmailsController, "routing" do
  190 + it "to #index" do
  191 + get("/profile/emails").should route_to('profiles/emails#index')
  192 + end
  193 +
  194 + it "to #create" do
  195 + post("/profile/emails").should route_to('profiles/emails#create')
  196 + end
  197 +
  198 + it "to #destroy" do
  199 + delete("/profile/emails/1").should route_to('profiles/emails#destroy', id: '1')
  200 + end
  201 +end
  202 +
186 # profile_avatar DELETE /profile/avatar(.:format) profiles/avatars#destroy 203 # profile_avatar DELETE /profile/avatar(.:format) profiles/avatars#destroy
187 describe Profiles::AvatarsController, "routing" do 204 describe Profiles::AvatarsController, "routing" do
188 it "to #destroy" do 205 it "to #destroy" do
spec/services/notification_service_spec.rb
@@ -16,6 +16,19 @@ describe NotificationService do @@ -16,6 +16,19 @@ describe NotificationService do
16 end 16 end
17 end 17 end
18 18
  19 + describe 'Email' do
  20 + describe :new_email do
  21 + let(:email) { create(:email) }
  22 +
  23 + it { notification.new_email(email).should be_true }
  24 +
  25 + it 'should send email to email owner' do
  26 + Notify.should_receive(:new_email_email).with(email.id)
  27 + notification.new_email(email)
  28 + end
  29 + end
  30 + end
  31 +
19 describe 'Notes' do 32 describe 'Notes' do
20 context 'issue note' do 33 context 'issue note' do
21 let(:issue) { create(:issue, assignee: create(:user)) } 34 let(:issue) { create(:issue, assignee: create(:user)) }
spec/support/valid_commit.rb
@@ -2,6 +2,7 @@ module ValidCommit @@ -2,6 +2,7 @@ module ValidCommit
2 ID = "8470d70da67355c9c009e4401746b1d5410af2e3" 2 ID = "8470d70da67355c9c009e4401746b1d5410af2e3"
3 MESSAGE = "notes controller refactored" 3 MESSAGE = "notes controller refactored"
4 AUTHOR_FULL_NAME = "Dmitriy Zaporozhets" 4 AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
  5 + AUTHOR_EMAIL = "dmitriy.zaporozhets@gmail.com"
5 6
6 FILES = [".foreman", ".gitignore", ".rails_footnotes", ".rspec", ".travis.yml", "CHANGELOG", "Gemfile", "Gemfile.lock", "LICENSE", "Procfile", "Procfile.production", "README.md", "Rakefile", "VERSION", "app", "config.ru", "config", "db", "doc", "lib", "log", "public", "resque.sh", "script", "spec", "vendor"] 7 FILES = [".foreman", ".gitignore", ".rails_footnotes", ".rspec", ".travis.yml", "CHANGELOG", "Gemfile", "Gemfile.lock", "LICENSE", "Procfile", "Procfile.production", "README.md", "Rakefile", "VERSION", "app", "config.ru", "config", "db", "doc", "lib", "log", "public", "resque.sh", "script", "spec", "vendor"]
7 FILES_COUNT = 26 8 FILES_COUNT = 26
spec/support/valid_commit_with_alt_email.rb 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +module ValidCommitWithAltEmail
  2 + ID = "1e689bfba39525ead225eaf611948cfbe8ac34cf"
  3 + MESSAGE = "fixed notes logic"
  4 + AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
  5 + AUTHOR_EMAIL = "dzaporozhets@sphereconsultinginc.com"
  6 +end
0 \ No newline at end of file 7 \ No newline at end of file