Merge branch 'decouple-member-notification' into 'master'
Decouple membership and notifications This allow you to have notification setting per project even if you are member of group. It also creates background for having notification settings in project you are not member of. - [x] Make it work - [x] Migrations - [x] CHANGELOG - [x] More tests - [x] API For #3359 After this merge request there is still some work to be done: * create migration that remove duplicates in notification settings table and create uniq index (8.8 probably) * remove notification_level field from Member model in 9.0 * make proper API for notification settings * use `MemberCreateService` instead of Member#after_create callback for creating notification settings (after #14709) * maybe more tests Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> See merge request !3421
This commit is contained in:
commit
4516f40dfe
37 changed files with 447 additions and 338 deletions
|
@ -31,6 +31,7 @@ v 8.7.0 (unreleased)
|
|||
- Hide `Create a group` help block when creating a new project in a group
|
||||
- Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
|
||||
- Gracefully handle notes on deleted commits in merge requests (Stan Hu)
|
||||
- Decouple membership and notifications
|
||||
- Fix creation of merge requests for orphaned branches (Stan Hu)
|
||||
- API: Ability to retrieve a single tag (Robert Schilling)
|
||||
- Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla)
|
||||
|
|
|
@ -18,8 +18,11 @@ class @Profile
|
|||
$(this).find('.btn-save').enable()
|
||||
$(this).find('.loading-gif').hide()
|
||||
|
||||
$('.update-notifications').on 'ajax:complete', ->
|
||||
$(this).find('.btn-save').enable()
|
||||
$('.update-notifications').on 'ajax:success', (e, data) ->
|
||||
if data.saved
|
||||
new Flash("Notification settings saved", "notice")
|
||||
else
|
||||
new Flash("Failed to save new settings", "alert")
|
||||
|
||||
@bindEvents()
|
||||
|
||||
|
|
|
@ -37,19 +37,20 @@ class @Project
|
|||
$('.update-notification').on 'click', (e) ->
|
||||
e.preventDefault()
|
||||
notification_level = $(@).data 'notification-level'
|
||||
$('#notification_level').val(notification_level)
|
||||
label = $(@).data 'notification-title'
|
||||
$('#notification_setting_level').val(notification_level)
|
||||
$('#notification-form').submit()
|
||||
label = null
|
||||
switch notification_level
|
||||
when 0 then label = ' Disabled '
|
||||
when 1 then label = ' Participating '
|
||||
when 2 then label = ' Watching '
|
||||
when 3 then label = ' Global '
|
||||
when 4 then label = ' On Mention '
|
||||
$('#notifications-button').empty().append("<i class='fa fa-bell'></i>" + label + "<i class='fa fa-angle-down'></i>")
|
||||
$(@).parents('ul').find('li.active').removeClass 'active'
|
||||
$(@).parent().addClass 'active'
|
||||
|
||||
$('#notification-form').on 'ajax:success', (e, data) ->
|
||||
if data.saved
|
||||
new Flash("Notification settings saved", "notice")
|
||||
else
|
||||
new Flash("Failed to save new settings", "alert")
|
||||
|
||||
|
||||
@projectSelectDropdown()
|
||||
|
||||
projectSelectDropdown: ->
|
||||
|
|
16
app/controllers/groups/notification_settings_controller.rb
Normal file
16
app/controllers/groups/notification_settings_controller.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
class Groups::NotificationSettingsController < Groups::ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def update
|
||||
notification_setting = current_user.notification_settings_for(group)
|
||||
saved = notification_setting.update_attributes(notification_setting_params)
|
||||
|
||||
render json: { saved: saved }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notification_setting_params
|
||||
params.require(:notification_setting).permit(:level)
|
||||
end
|
||||
end
|
|
@ -1,39 +1,18 @@
|
|||
class Profiles::NotificationsController < Profiles::ApplicationController
|
||||
def show
|
||||
@user = current_user
|
||||
@notification = current_user.notification
|
||||
@project_members = current_user.project_members
|
||||
@group_members = current_user.group_members
|
||||
@group_notifications = current_user.notification_settings.for_groups
|
||||
@project_notifications = current_user.notification_settings.for_projects
|
||||
end
|
||||
|
||||
def update
|
||||
type = params[:notification_type]
|
||||
|
||||
@saved = if type == 'global'
|
||||
current_user.update_attributes(user_params)
|
||||
elsif type == 'group'
|
||||
group_member = current_user.group_members.find(params[:notification_id])
|
||||
group_member.notification_level = params[:notification_level]
|
||||
group_member.save
|
||||
else
|
||||
project_member = current_user.project_members.find(params[:notification_id])
|
||||
project_member.notification_level = params[:notification_level]
|
||||
project_member.save
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
if @saved
|
||||
flash[:notice] = "Notification settings saved"
|
||||
else
|
||||
flash[:alert] = "Failed to save new settings"
|
||||
end
|
||||
|
||||
redirect_back_or_default(default: profile_notifications_path)
|
||||
end
|
||||
|
||||
format.js
|
||||
if current_user.update_attributes(user_params)
|
||||
flash[:notice] = "Notification settings saved"
|
||||
else
|
||||
flash[:alert] = "Failed to save new settings"
|
||||
end
|
||||
|
||||
redirect_back_or_default(default: profile_notifications_path)
|
||||
end
|
||||
|
||||
def user_params
|
||||
|
|
16
app/controllers/projects/notification_settings_controller.rb
Normal file
16
app/controllers/projects/notification_settings_controller.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
class Projects::NotificationSettingsController < Projects::ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def update
|
||||
notification_setting = current_user.notification_settings_for(project)
|
||||
saved = notification_setting.update_attributes(notification_setting_params)
|
||||
|
||||
render json: { saved: saved }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notification_setting_params
|
||||
params.require(:notification_setting).permit(:level)
|
||||
end
|
||||
end
|
|
@ -101,14 +101,18 @@ class ProjectsController < Projects::ApplicationController
|
|||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
if current_user
|
||||
@membership = @project.team.find_member(current_user.id)
|
||||
|
||||
if @membership
|
||||
@notification_setting = current_user.notification_settings_for(@project)
|
||||
end
|
||||
end
|
||||
|
||||
if @project.repository_exists?
|
||||
if @project.empty_repo?
|
||||
render 'projects/empty'
|
||||
else
|
||||
if current_user
|
||||
@membership = @project.team.find_member(current_user.id)
|
||||
end
|
||||
|
||||
render :show
|
||||
end
|
||||
else
|
||||
|
|
|
@ -1,48 +1,48 @@
|
|||
module NotificationsHelper
|
||||
include IconsHelper
|
||||
|
||||
def notification_icon(notification)
|
||||
if notification.disabled?
|
||||
icon('volume-off', class: 'ns-mute')
|
||||
elsif notification.participating?
|
||||
icon('volume-down', class: 'ns-part')
|
||||
elsif notification.watch?
|
||||
icon('volume-up', class: 'ns-watch')
|
||||
else
|
||||
icon('circle-o', class: 'ns-default')
|
||||
def notification_icon_class(level)
|
||||
case level.to_sym
|
||||
when :disabled
|
||||
'microphone-slash'
|
||||
when :participating
|
||||
'volume-up'
|
||||
when :watch
|
||||
'eye'
|
||||
when :mention
|
||||
'at'
|
||||
when :global
|
||||
'globe'
|
||||
end
|
||||
end
|
||||
|
||||
def notification_list_item(notification_level, user_membership)
|
||||
case notification_level
|
||||
when Notification::N_DISABLED
|
||||
update_notification_link(Notification::N_DISABLED, user_membership, 'Disabled', 'microphone-slash')
|
||||
when Notification::N_PARTICIPATING
|
||||
update_notification_link(Notification::N_PARTICIPATING, user_membership, 'Participate', 'volume-up')
|
||||
when Notification::N_WATCH
|
||||
update_notification_link(Notification::N_WATCH, user_membership, 'Watch', 'eye')
|
||||
when Notification::N_MENTION
|
||||
update_notification_link(Notification::N_MENTION, user_membership, 'On mention', 'at')
|
||||
when Notification::N_GLOBAL
|
||||
update_notification_link(Notification::N_GLOBAL, user_membership, 'Global', 'globe')
|
||||
def notification_icon(level, text = nil)
|
||||
icon("#{notification_icon_class(level)} fw", text: text)
|
||||
end
|
||||
|
||||
def notification_title(level)
|
||||
case level.to_sym
|
||||
when :participating
|
||||
'Participate'
|
||||
when :mention
|
||||
'On mention'
|
||||
else
|
||||
# do nothing
|
||||
level.to_s.titlecase
|
||||
end
|
||||
end
|
||||
|
||||
def update_notification_link(notification_level, user_membership, title, icon)
|
||||
content_tag(:li, class: active_level_for(user_membership, notification_level)) do
|
||||
link_to '#', class: 'update-notification', data: { notification_level: notification_level } do
|
||||
icon("#{icon} fw", text: title)
|
||||
def notification_list_item(level, setting)
|
||||
title = notification_title(level)
|
||||
|
||||
data = {
|
||||
notification_level: level,
|
||||
notification_title: title
|
||||
}
|
||||
|
||||
content_tag(:li, class: ('active' if setting.level == level)) do
|
||||
link_to '#', class: 'update-notification', data: data do
|
||||
notification_icon(level, title)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def notification_label(user_membership)
|
||||
Notification.new(user_membership).to_s
|
||||
end
|
||||
|
||||
def active_level_for(user_membership, level)
|
||||
'active' if user_membership.notification_level == level
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
# == Notifiable concern
|
||||
#
|
||||
# Contains notification functionality
|
||||
#
|
||||
module Notifiable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
validates :notification_level, inclusion: { in: Notification.project_notification_levels }, presence: true
|
||||
end
|
||||
|
||||
def notification
|
||||
@notification ||= Notification.new(self)
|
||||
end
|
||||
end
|
|
@ -27,6 +27,7 @@ class Group < Namespace
|
|||
has_many :users, through: :group_members
|
||||
has_many :project_group_links, dependent: :destroy
|
||||
has_many :shared_projects, through: :project_group_links, source: :project
|
||||
has_many :notification_settings, dependent: :destroy, as: :source
|
||||
|
||||
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
|
||||
validate :visibility_level_allowed_by_projects
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
class Member < ActiveRecord::Base
|
||||
include Sortable
|
||||
include Notifiable
|
||||
include Gitlab::Access
|
||||
|
||||
attr_accessor :raw_invite_token
|
||||
|
@ -56,12 +55,15 @@ class Member < ActiveRecord::Base
|
|||
|
||||
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
|
||||
after_create :send_invite, if: :invite?
|
||||
after_create :create_notification_setting, unless: :invite?
|
||||
after_create :post_create_hook, unless: :invite?
|
||||
after_update :post_update_hook, unless: :invite?
|
||||
after_destroy :post_destroy_hook, unless: :invite?
|
||||
|
||||
delegate :name, :username, :email, to: :user, prefix: true
|
||||
|
||||
default_value_for :notification_level, NotificationSetting.levels[:global]
|
||||
|
||||
class << self
|
||||
def find_by_invite_token(invite_token)
|
||||
invite_token = Devise.token_generator.digest(self, :invite_token, invite_token)
|
||||
|
@ -160,6 +162,14 @@ class Member < ActiveRecord::Base
|
|||
send_invite
|
||||
end
|
||||
|
||||
def create_notification_setting
|
||||
user.notification_settings.find_or_create_for(source)
|
||||
end
|
||||
|
||||
def notification_setting
|
||||
@notification_setting ||= user.notification_settings_for(source)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_invite
|
||||
|
|
|
@ -24,7 +24,6 @@ class GroupMember < Member
|
|||
|
||||
# Make sure group member points only to group as it source
|
||||
default_value_for :source_type, SOURCE_TYPE
|
||||
default_value_for :notification_level, Notification::N_GLOBAL
|
||||
validates_format_of :source_type, with: /\ANamespace\z/
|
||||
default_scope { where(source_type: SOURCE_TYPE) }
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@ class ProjectMember < Member
|
|||
|
||||
# Make sure project member points only to project as it source
|
||||
default_value_for :source_type, SOURCE_TYPE
|
||||
default_value_for :notification_level, Notification::N_GLOBAL
|
||||
validates_format_of :source_type, with: /\AProject\z/
|
||||
default_scope { where(source_type: SOURCE_TYPE) }
|
||||
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
class Notification
|
||||
#
|
||||
# Notification levels
|
||||
#
|
||||
N_DISABLED = 0
|
||||
N_PARTICIPATING = 1
|
||||
N_WATCH = 2
|
||||
N_GLOBAL = 3
|
||||
N_MENTION = 4
|
||||
|
||||
attr_accessor :target
|
||||
|
||||
class << self
|
||||
def notification_levels
|
||||
[N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH]
|
||||
end
|
||||
|
||||
def options_with_labels
|
||||
{
|
||||
disabled: N_DISABLED,
|
||||
participating: N_PARTICIPATING,
|
||||
watch: N_WATCH,
|
||||
mention: N_MENTION,
|
||||
global: N_GLOBAL
|
||||
}
|
||||
end
|
||||
|
||||
def project_notification_levels
|
||||
[N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH, N_GLOBAL]
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(target)
|
||||
@target = target
|
||||
end
|
||||
|
||||
def disabled?
|
||||
target.notification_level == N_DISABLED
|
||||
end
|
||||
|
||||
def participating?
|
||||
target.notification_level == N_PARTICIPATING
|
||||
end
|
||||
|
||||
def watch?
|
||||
target.notification_level == N_WATCH
|
||||
end
|
||||
|
||||
def global?
|
||||
target.notification_level == N_GLOBAL
|
||||
end
|
||||
|
||||
def mention?
|
||||
target.notification_level == N_MENTION
|
||||
end
|
||||
|
||||
def level
|
||||
target.notification_level
|
||||
end
|
||||
|
||||
def to_s
|
||||
case level
|
||||
when N_DISABLED
|
||||
'Disabled'
|
||||
when N_PARTICIPATING
|
||||
'Participating'
|
||||
when N_WATCH
|
||||
'Watching'
|
||||
when N_MENTION
|
||||
'On mention'
|
||||
when N_GLOBAL
|
||||
'Global'
|
||||
else
|
||||
# do nothing
|
||||
end
|
||||
end
|
||||
end
|
28
app/models/notification_setting.rb
Normal file
28
app/models/notification_setting.rb
Normal file
|
@ -0,0 +1,28 @@
|
|||
class NotificationSetting < ActiveRecord::Base
|
||||
enum level: { disabled: 0, participating: 1, watch: 2, global: 3, mention: 4 }
|
||||
|
||||
default_value_for :level, NotificationSetting.levels[:global]
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :source, polymorphic: true
|
||||
|
||||
validates :user, presence: true
|
||||
validates :source, presence: true
|
||||
validates :level, presence: true
|
||||
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
|
||||
message: "already exists in source",
|
||||
allow_nil: true }
|
||||
|
||||
scope :for_groups, -> { where(source_type: 'Namespace') }
|
||||
scope :for_projects, -> { where(source_type: 'Project') }
|
||||
|
||||
def self.find_or_create_for(source)
|
||||
setting = find_or_initialize_by(source: source)
|
||||
|
||||
unless setting.persisted?
|
||||
setting.save
|
||||
end
|
||||
|
||||
setting
|
||||
end
|
||||
end
|
|
@ -154,6 +154,7 @@ class Project < ActiveRecord::Base
|
|||
has_many :project_group_links, dependent: :destroy
|
||||
has_many :invited_groups, through: :project_group_links, source: :group
|
||||
has_many :todos, dependent: :destroy
|
||||
has_many :notification_settings, dependent: :destroy, as: :source
|
||||
|
||||
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
|
||||
|
||||
|
|
|
@ -143,6 +143,7 @@ class User < ActiveRecord::Base
|
|||
has_many :spam_logs, dependent: :destroy
|
||||
has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
|
||||
has_many :todos, dependent: :destroy
|
||||
has_many :notification_settings, dependent: :destroy
|
||||
|
||||
#
|
||||
# Validations
|
||||
|
@ -157,7 +158,7 @@ class User < ActiveRecord::Base
|
|||
presence: true,
|
||||
uniqueness: { case_sensitive: false }
|
||||
|
||||
validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true
|
||||
validates :notification_level, presence: true
|
||||
validate :namespace_uniq, if: ->(user) { user.username_changed? }
|
||||
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
|
||||
validate :unique_email, if: ->(user) { user.email_changed? }
|
||||
|
@ -190,6 +191,13 @@ class User < ActiveRecord::Base
|
|||
# Note: When adding an option, it MUST go on the end of the array.
|
||||
enum project_view: [:readme, :activity, :files]
|
||||
|
||||
# Notification level
|
||||
# Note: When adding an option, it MUST go on the end of the array.
|
||||
#
|
||||
# TODO: Add '_prefix: :notification' to enum when update to Rails 5. https://github.com/rails/rails/pull/19813
|
||||
# Because user.notification_disabled? is much better than user.disabled?
|
||||
enum notification_level: [:disabled, :participating, :watch, :global, :mention]
|
||||
|
||||
alias_attribute :private_token, :authentication_token
|
||||
|
||||
delegate :path, to: :namespace, allow_nil: true, prefix: true
|
||||
|
@ -349,10 +357,6 @@ class User < ActiveRecord::Base
|
|||
"#{self.class.reference_prefix}#{username}"
|
||||
end
|
||||
|
||||
def notification
|
||||
@notification ||= Notification.new(self)
|
||||
end
|
||||
|
||||
def generate_password
|
||||
if self.force_random_password
|
||||
self.password = self.password_confirmation = Devise.friendly_token.first(8)
|
||||
|
@ -827,6 +831,10 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def notification_settings_for(source)
|
||||
notification_settings.find_or_initialize_by(source: source)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def projects_union
|
||||
|
|
|
@ -253,8 +253,8 @@ class NotificationService
|
|||
def project_watchers(project)
|
||||
project_members = project_member_notification(project)
|
||||
|
||||
users_with_project_level_global = project_member_notification(project, Notification::N_GLOBAL)
|
||||
users_with_group_level_global = group_member_notification(project, Notification::N_GLOBAL)
|
||||
users_with_project_level_global = project_member_notification(project, :global)
|
||||
users_with_group_level_global = group_member_notification(project, :global)
|
||||
users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
|
||||
|
||||
users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
|
||||
|
@ -264,18 +264,16 @@ class NotificationService
|
|||
end
|
||||
|
||||
def project_member_notification(project, notification_level=nil)
|
||||
project_members = project.project_members
|
||||
|
||||
if notification_level
|
||||
project_members.where(notification_level: notification_level).pluck(:user_id)
|
||||
project.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id)
|
||||
else
|
||||
project_members.pluck(:user_id)
|
||||
project.notification_settings.pluck(:user_id)
|
||||
end
|
||||
end
|
||||
|
||||
def group_member_notification(project, notification_level)
|
||||
if project.group
|
||||
project.group.group_members.where(notification_level: notification_level).pluck(:user_id)
|
||||
project.group.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
@ -284,13 +282,13 @@ class NotificationService
|
|||
def users_with_global_level_watch(ids)
|
||||
User.where(
|
||||
id: ids,
|
||||
notification_level: Notification::N_WATCH
|
||||
notification_level: NotificationSetting.levels[:watch]
|
||||
).pluck(:id)
|
||||
end
|
||||
|
||||
# Build a list of users based on project notifcation settings
|
||||
def select_project_member_setting(project, global_setting, users_global_level_watch)
|
||||
users = project_member_notification(project, Notification::N_WATCH)
|
||||
users = project_member_notification(project, :watch)
|
||||
|
||||
# If project setting is global, add to watch list if global setting is watch
|
||||
global_setting.each do |user_id|
|
||||
|
@ -304,7 +302,7 @@ class NotificationService
|
|||
|
||||
# Build a list of users based on group notification settings
|
||||
def select_group_member_setting(project, project_members, global_setting, users_global_level_watch)
|
||||
uids = group_member_notification(project, Notification::N_WATCH)
|
||||
uids = group_member_notification(project, :watch)
|
||||
|
||||
# Group setting is watch, add to users list if user is not project member
|
||||
users = []
|
||||
|
@ -331,40 +329,46 @@ class NotificationService
|
|||
# Remove users with disabled notifications from array
|
||||
# Also remove duplications and nil recipients
|
||||
def reject_muted_users(users, project = nil)
|
||||
reject_users(users, :disabled?, project)
|
||||
reject_users(users, :disabled, project)
|
||||
end
|
||||
|
||||
# Remove users with notification level 'Mentioned'
|
||||
def reject_mention_users(users, project = nil)
|
||||
reject_users(users, :mention?, project)
|
||||
reject_users(users, :mention, project)
|
||||
end
|
||||
|
||||
# Reject users which method_name from notification object returns true.
|
||||
# Reject users which has certain notification level
|
||||
#
|
||||
# Example:
|
||||
# reject_users(users, :watch?, project)
|
||||
# reject_users(users, :watch, project)
|
||||
#
|
||||
def reject_users(users, method_name, project = nil)
|
||||
def reject_users(users, level, project = nil)
|
||||
level = level.to_s
|
||||
|
||||
unless NotificationSetting.levels.keys.include?(level)
|
||||
raise 'Invalid notification level'
|
||||
end
|
||||
|
||||
users = users.to_a.compact.uniq
|
||||
users = users.reject(&:blocked?)
|
||||
|
||||
users.reject do |user|
|
||||
next user.notification.send(method_name) unless project
|
||||
next user.notification_level == level unless project
|
||||
|
||||
member = project.project_members.find_by(user_id: user.id)
|
||||
setting = user.notification_settings_for(project)
|
||||
|
||||
if !member && project.group
|
||||
member = project.group.group_members.find_by(user_id: user.id)
|
||||
if !setting && project.group
|
||||
setting = user.notification_settings_for(project.group)
|
||||
end
|
||||
|
||||
# reject users who globally set mention notification and has no membership
|
||||
next user.notification.send(method_name) unless member
|
||||
# reject users who globally set mention notification and has no setting per project/group
|
||||
next user.notification_level == level unless setting
|
||||
|
||||
# reject users who set mention notification in project
|
||||
next true if member.notification.send(method_name)
|
||||
next true if setting.level == level
|
||||
|
||||
# reject users who have N_MENTION in project and disabled in global settings
|
||||
member.notification.global? && user.notification.send(method_name)
|
||||
# reject users who have mention level in project and disabled in global settings
|
||||
setting.global? && user.notification_level == level
|
||||
end
|
||||
end
|
||||
|
||||
|
|
13
app/views/profiles/notifications/_group_settings.html.haml
Normal file
13
app/views/profiles/notifications/_group_settings.html.haml
Normal file
|
@ -0,0 +1,13 @@
|
|||
%li.notification-list-item
|
||||
%span.notification.fa.fa-holder.append-right-5
|
||||
- if setting.global?
|
||||
= notification_icon(current_user.notification_level)
|
||||
- else
|
||||
= notification_icon(setting.level)
|
||||
|
||||
%span.str-truncated
|
||||
= link_to group.name, group_path(group)
|
||||
|
||||
.pull-right
|
||||
= form_for [group, setting], remote: true, html: { class: 'update-notifications' } do |f|
|
||||
= f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit'
|
13
app/views/profiles/notifications/_project_settings.html.haml
Normal file
13
app/views/profiles/notifications/_project_settings.html.haml
Normal file
|
@ -0,0 +1,13 @@
|
|||
%li.notification-list-item
|
||||
%span.notification.fa.fa-holder.append-right-5
|
||||
- if setting.global?
|
||||
= notification_icon(current_user.notification_level)
|
||||
- else
|
||||
= notification_icon(setting.level)
|
||||
|
||||
%span.str-truncated
|
||||
= link_to_project(project)
|
||||
|
||||
.pull-right
|
||||
= form_for [project.namespace.becomes(Namespace), project, setting], remote: true, html: { class: 'update-notifications' } do |f|
|
||||
= f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit'
|
|
@ -1,17 +0,0 @@
|
|||
%li.notification-list-item
|
||||
%span.notification.fa.fa-holder.append-right-5
|
||||
- if notification.global?
|
||||
= notification_icon(@notification)
|
||||
- else
|
||||
= notification_icon(notification)
|
||||
|
||||
%span.str-truncated
|
||||
- if membership.kind_of? GroupMember
|
||||
= link_to membership.group.name, membership.group
|
||||
- else
|
||||
= link_to_project(membership.project)
|
||||
.pull-right
|
||||
= form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do
|
||||
= hidden_field_tag :notification_type, type, id: dom_id(membership, 'notification_type')
|
||||
= hidden_field_tag :notification_id, membership.id, id: dom_id(membership, 'notification_id')
|
||||
= select_tag :notification_level, options_for_select(Notification.options_with_labels, notification.level), class: 'form-control trigger-submit'
|
|
@ -1,8 +1,12 @@
|
|||
- page_title "Notifications"
|
||||
- header_title page_title, profile_notifications_path
|
||||
|
||||
= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f|
|
||||
= form_errors(@user)
|
||||
%div
|
||||
- if @user.errors.any?
|
||||
%div.alert.alert-danger
|
||||
%ul
|
||||
- @user.errors.full_messages.each do |msg|
|
||||
%li= msg
|
||||
|
||||
= hidden_field_tag :notification_type, 'global'
|
||||
.row
|
||||
|
@ -16,56 +20,55 @@
|
|||
.col-lg-9
|
||||
%h5
|
||||
Global notification settings
|
||||
.form-group
|
||||
= f.label :notification_email, class: "label-light"
|
||||
= f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2"
|
||||
.form-group
|
||||
= f.label :notification_level, class: 'label-light'
|
||||
.radio
|
||||
= f.label :notification_level, value: Notification::N_DISABLED do
|
||||
= f.radio_button :notification_level, Notification::N_DISABLED
|
||||
.level-title
|
||||
Disabled
|
||||
%p You will not get any notifications via email
|
||||
|
||||
.radio
|
||||
= f.label :notification_level, value: Notification::N_MENTION do
|
||||
= f.radio_button :notification_level, Notification::N_MENTION
|
||||
.level-title
|
||||
On Mention
|
||||
%p You will receive notifications only for comments in which you were @mentioned
|
||||
= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f|
|
||||
.form-group
|
||||
= f.label :notification_email, class: "label-light"
|
||||
= f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2"
|
||||
.form-group
|
||||
= f.label :notification_level, class: 'label-light'
|
||||
.radio
|
||||
= f.label :notification_level, value: :disabled do
|
||||
= f.radio_button :notification_level, :disabled
|
||||
.level-title
|
||||
Disabled
|
||||
%p You will not get any notifications via email
|
||||
|
||||
.radio
|
||||
= f.label :notification_level, value: Notification::N_PARTICIPATING do
|
||||
= f.radio_button :notification_level, Notification::N_PARTICIPATING
|
||||
.level-title
|
||||
Participating
|
||||
%p You will only receive notifications from related resources (e.g. from your commits or assigned issues)
|
||||
.radio
|
||||
= f.label :notification_level, value: :mention do
|
||||
= f.radio_button :notification_level, :mention
|
||||
.level-title
|
||||
On Mention
|
||||
%p You will receive notifications only for comments in which you were @mentioned
|
||||
|
||||
.radio
|
||||
= f.label :notification_level, value: Notification::N_WATCH do
|
||||
= f.radio_button :notification_level, Notification::N_WATCH
|
||||
.level-title
|
||||
Watch
|
||||
%p You will receive notifications for any activity
|
||||
.radio
|
||||
= f.label :notification_level, value: :participating do
|
||||
= f.radio_button :notification_level, :participating
|
||||
.level-title
|
||||
Participating
|
||||
%p You will only receive notifications from related resources (e.g. from your commits or assigned issues)
|
||||
|
||||
.prepend-top-default
|
||||
= f.submit 'Update settings', class: "btn btn-create"
|
||||
.radio
|
||||
= f.label :notification_level, value: :watch do
|
||||
= f.radio_button :notification_level, :watch
|
||||
.level-title
|
||||
Watch
|
||||
%p You will receive notifications for any activity
|
||||
|
||||
.prepend-top-default
|
||||
= f.submit 'Update settings', class: "btn btn-create"
|
||||
%hr
|
||||
.col-lg-9.col-lg-push-3
|
||||
%h5
|
||||
Groups (#{@group_members.count})
|
||||
%div
|
||||
%ul.bordered-list
|
||||
- @group_members.each do |group_member|
|
||||
- notification = Notification.new(group_member)
|
||||
= render 'settings', type: 'group', membership: group_member, notification: notification
|
||||
%h5
|
||||
Projects (#{@project_members.count})
|
||||
%p.account-well
|
||||
To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group.
|
||||
.append-bottom-default
|
||||
%ul.bordered-list
|
||||
- @project_members.each do |project_member|
|
||||
- notification = Notification.new(project_member)
|
||||
= render 'settings', type: 'project', membership: project_member, notification: notification
|
||||
%h5
|
||||
Groups (#{@group_notifications.count})
|
||||
%div
|
||||
%ul.bordered-list
|
||||
- @group_notifications.each do |setting|
|
||||
= render 'group_settings', setting: setting, group: setting.source
|
||||
%h5
|
||||
Projects (#{@project_notifications.count})
|
||||
%p.account-well
|
||||
To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there.
|
||||
.append-bottom-default
|
||||
%ul.bordered-list
|
||||
- @project_notifications.each do |setting|
|
||||
= render 'project_settings', setting: setting, project: setting.source
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
- if @saved
|
||||
:plain
|
||||
new Flash("Notification settings saved", "notice")
|
||||
- else
|
||||
:plain
|
||||
new Flash("Failed to save new settings", "alert")
|
|
@ -1,20 +1,11 @@
|
|||
- case @membership
|
||||
- when ProjectMember
|
||||
= form_tag profile_notifications_path, method: :put, remote: true, class: 'inline', id: 'notification-form' do
|
||||
= hidden_field_tag :notification_type, 'project'
|
||||
= hidden_field_tag :notification_id, @membership.id
|
||||
= hidden_field_tag :notification_level
|
||||
- if @notification_setting
|
||||
= form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f|
|
||||
= f.hidden_field :level
|
||||
%span.dropdown
|
||||
%a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"}
|
||||
= icon('bell')
|
||||
= notification_label(@membership)
|
||||
= notification_title(@notification_setting.level)
|
||||
= icon('angle-down')
|
||||
%ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
|
||||
- Notification.project_notification_levels.each do |level|
|
||||
= notification_list_item(level, @membership)
|
||||
|
||||
- when GroupMember
|
||||
.btn.disabled.notifications-btn.has-tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."}
|
||||
= icon('bell')
|
||||
= notification_label(@membership)
|
||||
= icon('angle-down')
|
||||
- NotificationSetting.levels.each do |level|
|
||||
= notification_list_item(level.first, @notification_setting)
|
||||
|
|
|
@ -406,6 +406,7 @@ Rails.application.routes.draw do
|
|||
|
||||
resource :avatar, only: [:destroy]
|
||||
resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
|
||||
resource :notification_setting, only: [:update]
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -607,6 +608,7 @@ Rails.application.routes.draw do
|
|||
|
||||
resources :forks, only: [:index, :new, :create]
|
||||
resource :import, only: [:new, :create, :show]
|
||||
resource :notification_setting, only: [:update]
|
||||
|
||||
resources :refs, only: [] do
|
||||
collection do
|
||||
|
|
11
db/migrate/20160328112808_create_notification_settings.rb
Normal file
11
db/migrate/20160328112808_create_notification_settings.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
class CreateNotificationSettings < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :notification_settings do |t|
|
||||
t.references :user, null: false
|
||||
t.references :source, polymorphic: true, null: false
|
||||
t.integer :level, default: 0, null: false
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# This migration will create one row of NotificationSetting for each Member row
|
||||
# It can take long time on big instances.
|
||||
#
|
||||
# This migration can be done online but with following effects:
|
||||
# - during migration some users will receive notifications based on their global settings (project/group settings will be ignored)
|
||||
# - its possible to get duplicate records for notification settings since we don't create uniq index yet
|
||||
#
|
||||
class MigrateNewNotificationSetting < ActiveRecord::Migration
|
||||
def up
|
||||
timestamp = Time.now
|
||||
execute "INSERT INTO notification_settings ( user_id, source_id, source_type, level, created_at, updated_at ) SELECT user_id, source_id, source_type, notification_level, '#{timestamp}', '#{timestamp}' FROM members WHERE user_id IS NOT NULL"
|
||||
end
|
||||
|
||||
def down
|
||||
execute "DELETE FROM notification_settings"
|
||||
end
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
class AddNotificationSettingIndex < ActiveRecord::Migration
|
||||
def change
|
||||
add_index :notification_settings, :user_id
|
||||
add_index :notification_settings, [:source_id, :source_type]
|
||||
end
|
||||
end
|
12
db/schema.rb
12
db/schema.rb
|
@ -637,6 +637,18 @@ ActiveRecord::Schema.define(version: 20160331223143) do
|
|||
add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree
|
||||
add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree
|
||||
|
||||
create_table "notification_settings", force: :cascade do |t|
|
||||
t.integer "user_id", null: false
|
||||
t.integer "source_id", null: false
|
||||
t.string "source_type", null: false
|
||||
t.integer "level", default: 0, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree
|
||||
add_index "notification_settings", ["user_id"], name: "index_notification_settings_on_user_id", using: :btree
|
||||
|
||||
create_table "oauth_access_grants", force: :cascade do |t|
|
||||
t.integer "resource_owner_id", null: false
|
||||
t.integer "application_id", null: false
|
||||
|
|
|
@ -7,3 +7,9 @@ Feature: Profile Notifications
|
|||
Scenario: I visit notifications tab
|
||||
When I visit profile notifications page
|
||||
Then I should see global notifications settings
|
||||
|
||||
@javascript
|
||||
Scenario: I edit Project Notifications
|
||||
Given I visit profile notifications page
|
||||
When I select Mention setting from dropdown
|
||||
Then I should see Notification saved message
|
||||
|
|
|
@ -9,4 +9,14 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps
|
|||
step 'I should see global notifications settings' do
|
||||
expect(page).to have_content "Notifications"
|
||||
end
|
||||
|
||||
step 'I select Mention setting from dropdown' do
|
||||
select 'mention', from: 'notification_setting_level'
|
||||
end
|
||||
|
||||
step 'I should see Notification saved message' do
|
||||
page.within '.flash-container' do
|
||||
expect(page).to have_content 'Notification settings saved'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -263,14 +263,19 @@ module API
|
|||
expose :id, :path, :kind
|
||||
end
|
||||
|
||||
class ProjectAccess < Grape::Entity
|
||||
class Member < Grape::Entity
|
||||
expose :access_level
|
||||
expose :notification_level
|
||||
expose :notification_level do |member, options|
|
||||
if member.notification_setting
|
||||
NotificationSetting.levels[member.notification_setting.level]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class GroupAccess < Grape::Entity
|
||||
expose :access_level
|
||||
expose :notification_level
|
||||
class ProjectAccess < Member
|
||||
end
|
||||
|
||||
class GroupAccess < Member
|
||||
end
|
||||
|
||||
class ProjectService < Grape::Entity
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Groups::NotificationSettingsController do
|
||||
let(:group) { create(:group) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
describe '#update' do
|
||||
context 'when not authorized' do
|
||||
it 'redirects to sign in page' do
|
||||
put :update,
|
||||
group_id: group.to_param,
|
||||
notification_setting: { level: :participating }
|
||||
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authorized' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'returns success' do
|
||||
put :update,
|
||||
group_id: group.to_param,
|
||||
notification_setting: { level: :participating }
|
||||
|
||||
expect(response.status).to eq 200
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,38 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Projects::NotificationSettingsController do
|
||||
let(:project) { create(:empty_project) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
project.team << [user, :developer]
|
||||
end
|
||||
|
||||
describe '#update' do
|
||||
context 'when not authorized' do
|
||||
it 'redirects to sign in page' do
|
||||
put :update,
|
||||
namespace_id: project.namespace.to_param,
|
||||
project_id: project.to_param,
|
||||
notification_setting: { level: :participating }
|
||||
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authorized' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'returns success' do
|
||||
put :update,
|
||||
namespace_id: project.namespace.to_param,
|
||||
project_id: project.to_param,
|
||||
notification_setting: { level: :participating }
|
||||
|
||||
expect(response.status).to eq 200
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,34 +2,15 @@ require 'spec_helper'
|
|||
|
||||
describe NotificationsHelper do
|
||||
describe 'notification_icon' do
|
||||
let(:notification) { double(disabled?: false, participating?: false, watch?: false) }
|
||||
it { expect(notification_icon(:disabled)).to match('class="fa fa-microphone-slash fa-fw"') }
|
||||
it { expect(notification_icon(:participating)).to match('class="fa fa-volume-up fa-fw"') }
|
||||
it { expect(notification_icon(:mention)).to match('class="fa fa-at fa-fw"') }
|
||||
it { expect(notification_icon(:global)).to match('class="fa fa-globe fa-fw"') }
|
||||
it { expect(notification_icon(:watch)).to match('class="fa fa-eye fa-fw"') }
|
||||
end
|
||||
|
||||
context "disabled notification" do
|
||||
before { allow(notification).to receive(:disabled?).and_return(true) }
|
||||
|
||||
it "has a red icon" do
|
||||
expect(notification_icon(notification)).to match('class="fa fa-volume-off ns-mute"')
|
||||
end
|
||||
end
|
||||
|
||||
context "participating notification" do
|
||||
before { allow(notification).to receive(:participating?).and_return(true) }
|
||||
|
||||
it "has a blue icon" do
|
||||
expect(notification_icon(notification)).to match('class="fa fa-volume-down ns-part"')
|
||||
end
|
||||
end
|
||||
|
||||
context "watched notification" do
|
||||
before { allow(notification).to receive(:watch?).and_return(true) }
|
||||
|
||||
it "has a green icon" do
|
||||
expect(notification_icon(notification)).to match('class="fa fa-volume-up ns-watch"')
|
||||
end
|
||||
end
|
||||
|
||||
it "has a blue icon" do
|
||||
expect(notification_icon(notification)).to match('class="fa fa-circle-o ns-default"')
|
||||
end
|
||||
describe 'notification_title' do
|
||||
it { expect(notification_title(:watch)).to match('Watch') }
|
||||
it { expect(notification_title(:mention)).to match('On mention') }
|
||||
end
|
||||
end
|
||||
|
|
17
spec/models/notification_setting_spec.rb
Normal file
17
spec/models/notification_setting_spec.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe NotificationSetting, type: :model do
|
||||
describe "Associations" do
|
||||
it { is_expected.to belong_to(:user) }
|
||||
it { is_expected.to belong_to(:source) }
|
||||
end
|
||||
|
||||
describe "Validation" do
|
||||
subject { NotificationSetting.new(source_id: 1, source_type: 'Project') }
|
||||
|
||||
it { is_expected.to validate_presence_of(:user) }
|
||||
it { is_expected.to validate_presence_of(:source) }
|
||||
it { is_expected.to validate_presence_of(:level) }
|
||||
it { is_expected.to validate_uniqueness_of(:user_id).scoped_to([:source_id, :source_type]).with_message(/already exists in source/) }
|
||||
end
|
||||
end
|
|
@ -88,12 +88,9 @@ describe NotificationService, services: true do
|
|||
note.project.namespace_id = group.id
|
||||
note.project.group.add_user(@u_watcher, GroupMember::MASTER)
|
||||
note.project.save
|
||||
user_project = note.project.project_members.find_by_user_id(@u_watcher.id)
|
||||
user_project.notification_level = Notification::N_PARTICIPATING
|
||||
user_project.save
|
||||
group_member = note.project.group.group_members.find_by_user_id(@u_watcher.id)
|
||||
group_member.notification_level = Notification::N_GLOBAL
|
||||
group_member.save
|
||||
|
||||
@u_watcher.notification_settings_for(note.project).participating!
|
||||
@u_watcher.notification_settings_for(note.project.group).global!
|
||||
ActionMailer::Base.deliveries.clear
|
||||
end
|
||||
|
||||
|
@ -215,7 +212,7 @@ describe NotificationService, services: true do
|
|||
end
|
||||
|
||||
it do
|
||||
@u_committer.update_attributes(notification_level: Notification::N_MENTION)
|
||||
@u_committer.update_attributes(notification_level: :mention)
|
||||
notification.new_note(note)
|
||||
should_not_email(@u_committer)
|
||||
end
|
||||
|
@ -246,7 +243,7 @@ describe NotificationService, services: true do
|
|||
end
|
||||
|
||||
it do
|
||||
issue.assignee.update_attributes(notification_level: Notification::N_MENTION)
|
||||
issue.assignee.update_attributes(notification_level: :mention)
|
||||
notification.new_issue(issue, @u_disabled)
|
||||
|
||||
should_not_email(issue.assignee)
|
||||
|
@ -596,13 +593,13 @@ describe NotificationService, services: true do
|
|||
end
|
||||
|
||||
def build_team(project)
|
||||
@u_watcher = create(:user, notification_level: Notification::N_WATCH)
|
||||
@u_participating = create(:user, notification_level: Notification::N_PARTICIPATING)
|
||||
@u_participant_mentioned = create(:user, username: 'participant', notification_level: Notification::N_PARTICIPATING)
|
||||
@u_disabled = create(:user, notification_level: Notification::N_DISABLED)
|
||||
@u_mentioned = create(:user, username: 'mention', notification_level: Notification::N_MENTION)
|
||||
@u_watcher = create(:user, notification_level: :watch)
|
||||
@u_participating = create(:user, notification_level: :participating)
|
||||
@u_participant_mentioned = create(:user, username: 'participant', notification_level: :participating)
|
||||
@u_disabled = create(:user, notification_level: :disabled)
|
||||
@u_mentioned = create(:user, username: 'mention', notification_level: :mention)
|
||||
@u_committer = create(:user, username: 'committer')
|
||||
@u_not_mentioned = create(:user, username: 'regular', notification_level: Notification::N_PARTICIPATING)
|
||||
@u_not_mentioned = create(:user, username: 'regular', notification_level: :participating)
|
||||
@u_outsider_mentioned = create(:user, username: 'outsider')
|
||||
|
||||
project.team << [@u_watcher, :master]
|
||||
|
@ -617,8 +614,8 @@ describe NotificationService, services: true do
|
|||
def add_users_with_subscription(project, issuable)
|
||||
@subscriber = create :user
|
||||
@unsubscriber = create :user
|
||||
@subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: Notification::N_PARTICIPATING)
|
||||
@watcher_and_subscriber = create(:user, notification_level: Notification::N_WATCH)
|
||||
@subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: :participating)
|
||||
@watcher_and_subscriber = create(:user, notification_level: :watch)
|
||||
|
||||
project.team << [@subscribed_participant, :master]
|
||||
project.team << [@subscriber, :master]
|
||||
|
|
Loading…
Reference in a new issue