Commit 25856a47e5a757e88a33218cc51367f6171db1c4

Authored by Dmitriy Zaporozhets
1 parent 3c3baf8f

save each notification setting with ajax on change

app/assets/stylesheets/sections/profile.scss
... ... @@ -20,3 +20,16 @@
20 20 border: 1px solid #ddd;
21 21 }
22 22 }
  23 +
  24 +.save-status-fixed {
  25 + position: fixed;
  26 + left: 20px;
  27 + bottom: 50px;
  28 +}
  29 +
  30 +.update-notifications {
  31 + margin-bottom: 0;
  32 + label {
  33 + margin-bottom: 0;
  34 + }
  35 +}
... ...
app/controllers/notifications_controller.rb
... ... @@ -3,11 +3,19 @@ class NotificationsController < ApplicationController
3 3  
4 4 def show
5 5 @notification = current_user.notification
6   - @projects = current_user.authorized_projects
  6 + @users_projects = current_user.users_projects
7 7 end
8 8  
9 9 def update
10   - current_user.notification_level = params[:notification_level]
11   - @saved = current_user.save
  10 + type = params[:notification_type]
  11 +
  12 + @saved = if type == 'global'
  13 + current_user.notification_level = params[:notification_level]
  14 + current_user.save
  15 + else
  16 + users_project = current_user.users_projects.find(params[:notification_id])
  17 + users_project.notification_level = params[:notification_level]
  18 + users_project.save
  19 + end
12 20 end
13 21 end
... ...
app/models/notification.rb
... ... @@ -5,26 +5,35 @@ class Notification
5 5 N_DISABLED = 0
6 6 N_PARTICIPATING = 1
7 7 N_WATCH = 2
  8 + N_GLOBAL = 3
8 9  
9   - attr_accessor :user
  10 + attr_accessor :target
10 11  
11 12 def self.notification_levels
12 13 [N_DISABLED, N_PARTICIPATING, N_WATCH]
13 14 end
14 15  
15   - def initialize(user)
16   - @user = user
  16 + def self.project_notification_levels
  17 + [N_DISABLED, N_PARTICIPATING, N_WATCH, N_GLOBAL]
  18 + end
  19 +
  20 + def initialize(target)
  21 + @target = target
17 22 end
18 23  
19 24 def disabled?
20   - user.notification_level == N_DISABLED
  25 + target.notification_level == N_DISABLED
21 26 end
22 27  
23 28 def participating?
24   - user.notification_level == N_PARTICIPATING
  29 + target.notification_level == N_PARTICIPATING
25 30 end
26 31  
27 32 def watch?
28   - user.notification_level == N_WATCH
  33 + target.notification_level == N_WATCH
  34 + end
  35 +
  36 + def global?
  37 + target.notification_level == N_GLOBAL
29 38 end
30 39 end
... ...
app/models/project.rb
... ... @@ -19,6 +19,7 @@
19 19 # issues_tracker :string(255) default("gitlab"), not null
20 20 # issues_tracker_id :string(255)
21 21 # snippets_enabled :boolean default(TRUE), not null
  22 +# last_activity_at :datetime
22 23 #
23 24  
24 25 require "grit"
... ...
app/models/users_project.rb
... ... @@ -2,12 +2,13 @@
2 2 #
3 3 # Table name: users_projects
4 4 #
5   -# id :integer not null, primary key
6   -# user_id :integer not null
7   -# project_id :integer not null
8   -# created_at :datetime not null
9   -# updated_at :datetime not null
10   -# project_access :integer default(0), not null
  5 +# id :integer not null, primary key
  6 +# user_id :integer not null
  7 +# project_id :integer not null
  8 +# created_at :datetime not null
  9 +# updated_at :datetime not null
  10 +# project_access :integer default(0), not null
  11 +# notification_level :integer default(3), not null
11 12 #
12 13  
13 14 class UsersProject < ActiveRecord::Base
... ... @@ -29,6 +30,7 @@ class UsersProject &lt; ActiveRecord::Base
29 30 validates :user_id, uniqueness: { scope: [:project_id], message: "already exists in project" }
30 31 validates :project_access, inclusion: { in: [GUEST, REPORTER, DEVELOPER, MASTER] }, presence: true
31 32 validates :project, presence: true
  33 + validates :notification_level, inclusion: { in: Notification.project_notification_levels }, presence: true
32 34  
33 35 delegate :name, :username, :email, to: :user, prefix: true
34 36  
... ... @@ -134,4 +136,8 @@ class UsersProject &lt; ActiveRecord::Base
134 136 def skip_git?
135 137 !!@skip_git
136 138 end
  139 +
  140 + def notification
  141 + @notification ||= Notification.new(self)
  142 + end
137 143 end
... ...
app/services/notification_service.rb
... ... @@ -80,7 +80,7 @@ class NotificationService
80 80 # * project team members with notification level higher then Participating
81 81 #
82 82 def merge_mr(merge_request)
83   - recipients = reject_muted_users([merge_request.author, merge_request.assignee])
  83 + recipients = reject_muted_users([merge_request.author, merge_request.assignee], merge_request.project)
84 84 recipients = recipients.concat(project_watchers(merge_request.project)).uniq
85 85  
86 86 recipients.each do |recipient|
... ... @@ -122,7 +122,7 @@ class NotificationService
122 122 recipients = recipients.concat(project_watchers(note.project)).compact.uniq
123 123  
124 124 # Reject mutes users
125   - recipients = reject_muted_users(recipients)
  125 + recipients = reject_muted_users(recipients, note.project)
126 126  
127 127 # Reject author
128 128 recipients.delete(note.author)
... ... @@ -147,19 +147,41 @@ class NotificationService
147 147  
148 148 # Get project users with WATCH notification level
149 149 def project_watchers(project)
150   - project.users.where(notification_level: Notification::N_WATCH)
  150 +
  151 + # Get project notification settings since it has higher priority
  152 + user_ids = project.users_projects.where(notification_level: Notification::N_WATCH).pluck(:user_id)
  153 + project_watchers = User.where(id: user_ids)
  154 +
  155 + # next collect users who use global settings with watch state
  156 + user_ids = project.users_projects.where(notification_level: Notification::N_GLOBAL).pluck(:user_id)
  157 + project_watchers += User.where(id: user_ids, notification_level: Notification::N_WATCH)
  158 +
  159 + project_watchers.uniq
151 160 end
152 161  
153 162 # Remove users with disabled notifications from array
154 163 # Also remove duplications and nil recipients
155   - def reject_muted_users(users)
156   - users.compact.uniq.reject do |user|
157   - user.notification.disabled?
  164 + def reject_muted_users(users, project = nil)
  165 + users = users.compact.uniq
  166 +
  167 + users.reject do |user|
  168 + next user.notification.disabled? unless project
  169 +
  170 + tm = project.users_projects.find_by_user_id(user.id)
  171 +
  172 + # reject users who globally disabled notification and has no membership
  173 + next user.notification.disabled? unless tm
  174 +
  175 + # reject users who disabled notification in project
  176 + next true if tm.notification.disabled?
  177 +
  178 + # reject users who have N_GLOBAL in project and disabled in global settings
  179 + tm.notification.global? && user.notification.disabled?
158 180 end
159 181 end
160 182  
161 183 def new_resource_email(target, method)
162   - recipients = reject_muted_users([target.assignee])
  184 + recipients = reject_muted_users([target.assignee], target.project)
163 185 recipients = recipients.concat(project_watchers(target.project)).uniq
164 186 recipients.delete(target.author)
165 187  
... ... @@ -169,7 +191,7 @@ class NotificationService
169 191 end
170 192  
171 193 def close_resource_email(target, current_user, method)
172   - recipients = reject_muted_users([target.author, target.assignee])
  194 + recipients = reject_muted_users([target.author, target.assignee], target.project)
173 195 recipients = recipients.concat(project_watchers(target.project)).uniq
174 196 recipients.delete(current_user)
175 197  
... ... @@ -185,7 +207,7 @@ class NotificationService
185 207 recipients = recipients.concat(project_watchers(target.project))
186 208  
187 209 # reject users with disabled notifications
188   - recipients = reject_muted_users(recipients)
  210 + recipients = reject_muted_users(recipients, target.project)
189 211  
190 212 # Reject me from recipients if I reassign an item
191 213 recipients.delete(current_user)
... ...
app/views/notifications/show.html.haml
... ... @@ -13,56 +13,65 @@
13 13 &ndash; You will receive all notifications from projects in which you participate
14 14 %hr
15 15  
16   -= form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do
17   - %ul.well-list
  16 +.row
  17 + .span4
  18 + %h5 Global
  19 + .span7
  20 + = form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do
  21 + = hidden_field_tag :notification_type, 'global'
  22 +
  23 + = label_tag do
  24 + = radio_button_tag :notification_level, Notification::N_DISABLED, @notification.disabled?, class: 'trigger-submit'
  25 + %span Disabled
  26 +
  27 + = label_tag do
  28 + = radio_button_tag :notification_level, Notification::N_PARTICIPATING, @notification.participating?, class: 'trigger-submit'
  29 + %span Participating
  30 +
  31 + = label_tag do
  32 + = radio_button_tag :notification_level, Notification::N_WATCH, @notification.watch?, class: 'trigger-submit'
  33 + %span Watch
  34 +
  35 +%hr
  36 += link_to '#', class: 'js-toggle-visibility-link' do
  37 + %h6.btn.btn-tiny
  38 + %i.icon-chevron-down
  39 + %span Per project notifications settings
  40 +
  41 +%ul.well-list.js-toggle-visibility-container.hide
  42 + - @users_projects.each do |users_project|
  43 + - notification = Notification.new(users_project)
18 44 %li
19 45 .row
20 46 .span4
21   - %h5 Global
  47 + %span
  48 + = link_to_project(users_project.project)
22 49 .span7
23   - = label_tag do
24   - = radio_button_tag :notification_level, Notification::N_DISABLED, @notification.disabled?
25   - %span Disabled
26   -
27   - = label_tag do
28   - = radio_button_tag :notification_level, Notification::N_PARTICIPATING, @notification.participating?
29   - %span Participating
30   -
31   - = label_tag do
32   - = radio_button_tag :notification_level, Notification::N_WATCH, @notification.watch?
33   - %span Watch
  50 + = form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do
  51 + = hidden_field_tag :notification_type, 'project', id: dom_id(users_project, 'notification_type')
  52 + = hidden_field_tag :notification_id, users_project.id, id: dom_id(users_project, 'notification_id')
34 53  
  54 + = label_tag do
  55 + = radio_button_tag :notification_level, Notification::N_GLOBAL, notification.global?, id: dom_id(users_project, 'notification_level'), class: 'trigger-submit'
  56 + %span Use global settings
35 57  
36   - = link_to '#', class: 'js-toggle-visibility-link' do
37   - %h6.btn.btn-tiny
38   - %i.icon-chevron-down
39   - %span Per project notifications settings
40   - %ul.well-list.js-toggle-visibility-container.hide
41   - - @projects.each do |project|
42   - %li
43   - .row
44   - .span4
45   - %span
46   - = project.name_with_namespace
47   - .span7
48 58 = label_tag do
49   - = radio_button_tag :"notification_level[#{project.id}]", Notification::N_DISABLED, @notification.disabled?, disabled: true
  59 + = radio_button_tag :notification_level, Notification::N_DISABLED, notification.disabled?, id: dom_id(users_project, 'notification_level'), class: 'trigger-submit'
50 60 %span Disabled
51 61  
52 62 = label_tag do
53   - = radio_button_tag :"notification_level[#{project.id}]", Notification::N_PARTICIPATING, @notification.participating?, disabled: true
  63 + = radio_button_tag :notification_level, Notification::N_PARTICIPATING, notification.participating?, id: dom_id(users_project, 'notification_level'), class: 'trigger-submit'
54 64 %span Participating
55 65  
56 66 = label_tag do
57   - = radio_button_tag :"notification_level[#{project.id}]", Notification::N_WATCH, @notification.watch?, disabled: true
  67 + = radio_button_tag :notification_level, Notification::N_WATCH, notification.watch?, id: dom_id(users_project, 'notification_level'), class: 'trigger-submit'
58 68 %span Watch
59 69  
60 70  
61   - .form-actions
62   - = submit_tag 'Save', class: 'btn btn-save'
63   - %span.update-success.cgreen.hide
64   - %i.icon-ok
65   - Saved
66   - %span.update-failed.cred.hide
67   - %i.icon-remove
68   - Failed
  71 +.save-status-fixed
  72 + %span.update-success.cgreen.hide
  73 + %i.icon-ok
  74 + Saved
  75 + %span.update-failed.cred.hide
  76 + %i.icon-remove
  77 + Failed
... ...
app/views/notifications/update.js.haml
1 1 - if @saved
2 2 :plain
3   - $('.update-notifications .update-success').showAndHide();
  3 + $('.save-status-fixed .update-success').showAndHide();
4 4 - else
5 5 :plain
6   - $('.update-notifications .update-failed').showAndHide();
7   -
  6 + $('.save-status-fixed .update-failed').showAndHide();
... ...
db/schema.rb
... ... @@ -11,7 +11,7 @@
11 11 #
12 12 # It's strongly recommended to check this file into your version control system.
13 13  
14   -ActiveRecord::Schema.define(:version => 20130325173941) do
  14 +ActiveRecord::Schema.define(:version => 20130404164628) do
15 15  
16 16 create_table "events", :force => true do |t|
17 17 t.string "target_type"
... ... @@ -156,9 +156,11 @@ ActiveRecord::Schema.define(:version =&gt; 20130325173941) do
156 156 t.string "issues_tracker", :default => "gitlab", :null => false
157 157 t.string "issues_tracker_id"
158 158 t.boolean "snippets_enabled", :default => true, :null => false
  159 + t.datetime "last_activity_at"
159 160 end
160 161  
161 162 add_index "projects", ["creator_id"], :name => "index_projects_on_owner_id"
  163 + add_index "projects", ["last_activity_at"], :name => "index_projects_on_last_activity_at"
162 164 add_index "projects", ["namespace_id"], :name => "index_projects_on_namespace_id"
163 165  
164 166 create_table "protected_branches", :force => true do |t|
... ... @@ -281,11 +283,12 @@ ActiveRecord::Schema.define(:version =&gt; 20130325173941) do
281 283 add_index "users", ["username"], :name => "index_users_on_username"
282 284  
283 285 create_table "users_projects", :force => true do |t|
284   - t.integer "user_id", :null => false
285   - t.integer "project_id", :null => false
286   - t.datetime "created_at", :null => false
287   - t.datetime "updated_at", :null => false
288   - t.integer "project_access", :default => 0, :null => false
  286 + t.integer "user_id", :null => false
  287 + t.integer "project_id", :null => false
  288 + t.datetime "created_at", :null => false
  289 + t.datetime "updated_at", :null => false
  290 + t.integer "project_access", :default => 0, :null => false
  291 + t.integer "notification_level", :default => 3, :null => false
289 292 end
290 293  
291 294 add_index "users_projects", ["project_access"], :name => "index_users_projects_on_project_access"
... ...
spec/models/project_spec.rb
... ... @@ -19,6 +19,7 @@
19 19 # issues_tracker :string(255) default("gitlab"), not null
20 20 # issues_tracker_id :string(255)
21 21 # snippets_enabled :boolean default(TRUE), not null
  22 +# last_activity_at :datetime
22 23 #
23 24  
24 25 require 'spec_helper'
... ...
spec/models/users_project_spec.rb
... ... @@ -2,12 +2,13 @@
2 2 #
3 3 # Table name: users_projects
4 4 #
5   -# id :integer not null, primary key
6   -# user_id :integer not null
7   -# project_id :integer not null
8   -# created_at :datetime not null
9   -# updated_at :datetime not null
10   -# project_access :integer default(0), not null
  5 +# id :integer not null, primary key
  6 +# user_id :integer not null
  7 +# project_id :integer not null
  8 +# created_at :datetime not null
  9 +# updated_at :datetime not null
  10 +# project_access :integer default(0), not null
  11 +# notification_level :integer default(3), not null
11 12 #
12 13  
13 14 require 'spec_helper'
... ...