survey notification support
Special survey account notifications (announcements) can be set up on the site_admin account. These survey notifications will only appear for accounts that have the "Account Surveys" setting enabled in their account settings, and they'll only show up for 1/N users in those accounts each month. N is configurable, defaults to 9. closes CNVS-6036 test plan: * On a regular account, create an announcement in account settings. There shouldn't be any options related to surveys available. * On the site admin account, create an announcement. Select to make it a survey. You can leave N at 9 or change it. * Verify that the survey doesn't show up for any users on accounts that don't have "Account Surveys" enabled. * Enable the "Account Surveys" setting on an account. Verify that the survey shows up for (roughly) 1 out of every N users in the account, on their dashboard. Change the time on the computer running canvas to another month. Verify that the survey shows up for a different set of 1/N users in the account. Change-Id: If11467d2153acee24a010ba45d516b0b320a4634 Reviewed-on: https://gerrit.instructure.com/21432 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Cody Cutrer <cody@instructure.com> QA-Review: Jeremy Putnam <jeremyp@instructure.com> Product-Review: Brian Palmer <brianp@instructure.com>
This commit is contained in:
parent
851c5efa16
commit
cd4f95e209
|
@ -258,7 +258,7 @@ class AccountsController < ApplicationController
|
|||
:default_group_storage_quota, :default_group_storage_quota_mb].each { |key| params[:account].delete key }
|
||||
end
|
||||
if params[:account][:services]
|
||||
params[:account][:services].slice(*Account.services_exposed_to_ui_hash(nil, @current_user).keys).each do |key, value|
|
||||
params[:account][:services].slice(*Account.services_exposed_to_ui_hash(nil, @current_user, @account).keys).each do |key, value|
|
||||
@account.set_service_availability(key, value == '1')
|
||||
end
|
||||
params[:account].delete :services
|
||||
|
|
|
@ -1123,7 +1123,14 @@ class Account < ActiveRecord::Base
|
|||
:description => "",
|
||||
:default => false,
|
||||
:expose_to_ui => :setting
|
||||
}
|
||||
},
|
||||
:account_survey_notifications => {
|
||||
:name => "Account Surveys",
|
||||
:description => "",
|
||||
:default => false,
|
||||
:expose_to_ui => :setting,
|
||||
:expose_to_ui_proc => proc { |user, account| user && account && account.grants_right?(user, :manage_site_settings) },
|
||||
},
|
||||
}.merge(@plugin_services || {}).freeze
|
||||
end
|
||||
|
||||
|
@ -1198,12 +1205,12 @@ class Account < ActiveRecord::Base
|
|||
|
||||
# if expose_as is nil, all services exposed in the ui are returned
|
||||
# if it's :service or :setting, then only services set to be exposed as that type are returned
|
||||
def self.services_exposed_to_ui_hash(expose_as = nil, current_user = nil)
|
||||
def self.services_exposed_to_ui_hash(expose_as = nil, current_user = nil, account = nil)
|
||||
if expose_as
|
||||
self.allowable_services.reject { |key, setting| setting[:expose_to_ui] != expose_as }
|
||||
else
|
||||
self.allowable_services.reject { |key, setting| !setting[:expose_to_ui] }
|
||||
end.reject { |key, setting| setting[:expose_to_ui_proc] && !setting[:expose_to_ui_proc].call(current_user) }
|
||||
end.reject { |key, setting| setting[:expose_to_ui_proc] && !setting[:expose_to_ui_proc].call(current_user, account) }
|
||||
end
|
||||
|
||||
def service_enabled?(service)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
class AccountNotification < ActiveRecord::Base
|
||||
attr_accessible :subject, :icon, :message,
|
||||
:account, :user, :start_at, :end_at
|
||||
:account, :user, :start_at, :end_at,
|
||||
:required_account_service, :months_in_display_cycle
|
||||
|
||||
validates_presence_of :start_at, :end_at, :account_id
|
||||
before_validation :infer_defaults
|
||||
|
@ -8,7 +9,12 @@ class AccountNotification < ActiveRecord::Base
|
|||
belongs_to :user
|
||||
validates_length_of :message, :maximum => maximum_text_length, :allow_nil => false, :allow_blank => false
|
||||
sanitize_field :message, Instructure::SanitizeField::SANITIZE
|
||||
|
||||
|
||||
ACCOUNT_SERVICE_NOTIFICATION_FLAGS = %w[account_survey_notifications]
|
||||
validates_inclusion_of :required_account_service, in: ACCOUNT_SERVICE_NOTIFICATION_FLAGS, allow_nil: true
|
||||
|
||||
validates_inclusion_of :months_in_display_cycle, in: 1..48, allow_nil: true
|
||||
|
||||
def infer_defaults
|
||||
self.start_at ||= Time.now.utc
|
||||
self.end_at ||= self.start_at + 2.weeks
|
||||
|
@ -16,16 +22,10 @@ class AccountNotification < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def self.for_user_and_account(user, account)
|
||||
closed_ids = user.preferences[:closed_notifications] || []
|
||||
now = Time.now.utc
|
||||
# Refreshes every 10 minutes at the longest
|
||||
current = Rails.cache.fetch(['account_notifications2', account].cache_key, :expires_in => 10.minutes) do
|
||||
Shard.partition_by_shard([Account.site_admin, account]) do |accounts|
|
||||
AccountNotification.where("account_id IN (?) AND start_at <? AND end_at>?", accounts, now, now).order('start_at DESC').all
|
||||
end
|
||||
end
|
||||
current = self.for_account(account)
|
||||
|
||||
user.shard.activate do
|
||||
closed_ids = user.preferences[:closed_notifications] || []
|
||||
# If there are ids marked as 'closed' that are no longer
|
||||
# applicable, they probably need to be cleared out.
|
||||
current_ids = current.map(&:id)
|
||||
|
@ -33,7 +33,50 @@ class AccountNotification < ActiveRecord::Base
|
|||
closed_ids = user.preferences[:closed_notifications] &= current_ids
|
||||
user.save!
|
||||
end
|
||||
current.reject { |announcement| closed_ids.include?(announcement.id) }
|
||||
current.reject! { |announcement| closed_ids.include?(announcement.id) }
|
||||
|
||||
# filter out announcements that have a periodic cycle of display,
|
||||
# and the user isn't in the set of users to display it to this month (based
|
||||
# on user id)
|
||||
current.reject! do |announcement|
|
||||
if months_in_period = announcement.months_in_display_cycle
|
||||
!self.display_for_user?(user.id, months_in_period)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
current
|
||||
end
|
||||
|
||||
def self.for_account(account)
|
||||
# Refreshes every 10 minutes at the longest
|
||||
Rails.cache.fetch(['account_notifications2', account].cache_key, :expires_in => 10.minutes) do
|
||||
now = Time.now.utc
|
||||
# we always check the given account for the flag, even if the announcement is from the site_admin account
|
||||
# this allows us to make a global announcement that is filtered to only accounts with this flag
|
||||
enabled_flags = ACCOUNT_SERVICE_NOTIFICATION_FLAGS & account.allowed_services_hash.keys.map(&:to_s)
|
||||
|
||||
Shard.partition_by_shard([Account.site_admin, account]) do |accounts|
|
||||
AccountNotification.where("account_id IN (?) AND start_at <? AND end_at>?", accounts, now, now).
|
||||
where("required_account_service IS NULL OR required_account_service IN (?)", enabled_flags).
|
||||
order('start_at DESC').all
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.default_months_in_display_cycle
|
||||
Setting.get_cached("account_notification_default_months_in_display_cycle", "9").to_i
|
||||
end
|
||||
|
||||
# private
|
||||
def self.display_for_user?(user_id, months_in_period, current_time = Time.now.utc)
|
||||
# we just need a stable reference point, doesn't matter what it is, so
|
||||
# let's use unix epoch
|
||||
start_time = Time.at(0).utc
|
||||
months_since_start_time = (current_time.year - start_time.year) * 12 + (current_time.month - start_time.month)
|
||||
periods_since_start_time = months_since_start_time / months_in_period
|
||||
months_into_current_period = months_since_start_time % months_in_period
|
||||
mod_value = (Random.new(periods_since_start_time).rand(months_in_period) + months_into_current_period) % months_in_period
|
||||
user_id % months_in_period == mod_value
|
||||
end
|
||||
end
|
||||
|
|
|
@ -93,3 +93,10 @@
|
|||
legend
|
||||
font-size: 15px
|
||||
font-weight: bold
|
||||
|
||||
#survey_announcement_field
|
||||
line-height: 30px
|
||||
input#account_notification_months_in_display_cycle
|
||||
width: 25px
|
||||
padding: 0
|
||||
margin-top: 6px
|
||||
|
|
|
@ -282,7 +282,7 @@ TEXT
|
|||
<% end %>
|
||||
<% end %>
|
||||
<% f.fields_for :services do |services| %>
|
||||
<% Account.services_exposed_to_ui_hash(:setting, @current_user).sort_by { |k,h| h[:name] }.each do |key, service| %>
|
||||
<% Account.services_exposed_to_ui_hash(:setting, @current_user, @account).sort_by { |k,h| h[:name] }.each do |key, service| %>
|
||||
<div>
|
||||
<%= services.check_box key, :checked => @account.service_enabled?(key) %>
|
||||
<%= services.label key, service[:name] + " " %>
|
||||
|
@ -652,6 +652,16 @@ TEXT
|
|||
</span>
|
||||
<%= link_to(context_user_name(@account, announcement.user_id), user_path(announcement.user_id)) %>
|
||||
</div>
|
||||
<% if announcement.required_account_service %>
|
||||
<div>
|
||||
<%= Account.allowable_services[announcement.required_account_service.to_sym].try(:[], :name) %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if announcement.months_in_display_cycle %>
|
||||
<div>
|
||||
<%= t :announcement_sent_to_subset, "Sent to 1 / %{denominator} users each month", denominator: announcement.months_in_display_cycle %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="message user_content">
|
||||
<%= user_content(announcement.message) %>
|
||||
</div>
|
||||
|
@ -684,8 +694,16 @@ TEXT
|
|||
</td>
|
||||
</tr>
|
||||
<% if @account.site_admin? %>
|
||||
<tr id="confirm_global_announcement_field">
|
||||
<td colspan=2><input type="checkbox" id="confirm_global_announcement" name="confirm_global_announcement"> <%= label_tag :confirm_global_announcement, t(:confirm_global_announcement, "This announcement will be shown to *all* Canvas users. Confirm that you want to create it.", :wrapper => '<b>\1</b>') %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan=2><input type=checkbox id=confirm_global_announcement name=confirm_global_announcement> <%= label_tag :confirm_global_announcement, t(:confirm_global_announcement, "This announcement will be shown to *all* Canvas users. Confirm that you want to create it.", :wrapper => '<b>\1</b>') %></td>
|
||||
<td colspan=2>
|
||||
<div id="survey_announcement_field">
|
||||
<%# don't use the rails check_box helper here, because we don't want the extra hidden element when not checked %>
|
||||
<input type="checkbox" name="account_notification[required_account_service]" id="account_notification_required_account_service" value="account_survey_notifications" />
|
||||
<label><%= t :survey_announcement, "This is a survey announcement. It will be sent to 1 / %{denominator} of users in enabled accounts for each month that it's active.", denominator: f.text_field(:months_in_display_cycle, value: AccountNotification.default_months_in_display_cycle, disabled: true) %></label>
|
||||
</label>
|
||||
</tr>
|
||||
<% end %>
|
||||
<tr>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
class AddSurveyFunctionalityToAccountNotifications < ActiveRecord::Migration
|
||||
tag :predeploy
|
||||
|
||||
def self.up
|
||||
add_column :account_notifications, :required_account_service, :string
|
||||
add_column :account_notifications, :months_in_display_cycle, :int
|
||||
# this table is small enough for transactional index creation
|
||||
add_index :account_notifications, [:account_id, :end_at, :start_at], name: "index_account_notifications_by_account_and_timespan"
|
||||
remove_index :account_notifications, [:account_id, :start_at]
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :account_notifications, :required_account_setting
|
||||
remove_column :account_notifications, :months_in_display_cycle
|
||||
add_index :account_notifications, [:account_id, :start_at]
|
||||
remove_index :account_notifications, name: "index_account_notifications_by_account_and_timespan"
|
||||
end
|
||||
end
|
|
@ -37,20 +37,30 @@ define([
|
|||
});
|
||||
$("#add_notification_form").submit(function(event) {
|
||||
var $this = $(this);
|
||||
var $confirmation = $this.find('#confirm_global_announcement:not(:checked)');
|
||||
var $confirmation = $this.find('#confirm_global_announcement:visible:not(:checked)');
|
||||
if ($confirmation.length > 0) {
|
||||
$confirmation.errorBox(I18n.t('confirms.global_announcement', "You must confirm the global announcement"));
|
||||
return false;
|
||||
}
|
||||
var result = $this.validateForm({
|
||||
var validations = {
|
||||
object_name: 'account_notification',
|
||||
required: ['start_at', 'end_at', 'subject', 'message'],
|
||||
date_fields: ['start_at', 'end_at']
|
||||
});
|
||||
date_fields: ['start_at', 'end_at'],
|
||||
numbers: []
|
||||
};
|
||||
if ($('#account_notification_months_in_display_cycle').length > 0) {
|
||||
validations.numbers.push('months_in_display_cycle');
|
||||
}
|
||||
var result = $this.validateForm(validations);
|
||||
if(!result) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
$("#account_notification_required_account_service").click(function(event) {
|
||||
$this = $(this);
|
||||
$("#confirm_global_announcement_field").showIf(!$this.is(":checked"));
|
||||
$("#account_notification_months_in_display_cycle").prop("disabled", !$this.is(":checked"));
|
||||
});
|
||||
$(".delete_notification_link").click(function(event) {
|
||||
event.preventDefault();
|
||||
var $link = $(this);
|
||||
|
|
|
@ -293,19 +293,22 @@ describe AccountsController do
|
|||
|
||||
it "should allow updating services that appear in the ui for the current user" do
|
||||
Account.register_service(:test1, { name: 'test1', description: '', expose_to_ui: :setting, default: false })
|
||||
Account.register_service(:test2, { name: 'test2', description: '', expose_to_ui: :setting, default: false, expose_to_ui_proc: proc { |user| false } })
|
||||
Account.register_service(:test2, { name: 'test2', description: '', expose_to_ui: :setting, default: false, expose_to_ui_proc: proc { |user, account| false } })
|
||||
user_session(user)
|
||||
@account = Account.create!
|
||||
Account.register_service(:test3, { name: 'test3', description: '', expose_to_ui: :setting, default: false, expose_to_ui_proc: proc { |user, account| account == @account } })
|
||||
Account.site_admin.add_user(@user)
|
||||
post 'update', id: @account.id, account: {
|
||||
services: {
|
||||
'test1' => '1',
|
||||
'test2' => '1',
|
||||
'test3' => '1',
|
||||
}
|
||||
}
|
||||
@account.reload
|
||||
@account.allowed_services.should match(%r{\+test1})
|
||||
@account.allowed_services.should_not match(%r{\+test2})
|
||||
@account.allowed_services.should match(%r{\+test3})
|
||||
end
|
||||
|
||||
describe "quotas" do
|
||||
|
|
|
@ -49,6 +49,41 @@ describe AccountNotification do
|
|||
@user.preferences[:closed_notifications].should == []
|
||||
end
|
||||
|
||||
describe "survey notifications" do
|
||||
it "should only display for flagged accounts" do
|
||||
flag = AccountNotification::ACCOUNT_SERVICE_NOTIFICATION_FLAGS.first
|
||||
@announcement = Account.site_admin.announcements.create!(message: "hello", required_account_service: flag)
|
||||
@a1 = account_model
|
||||
@a2 = account_model
|
||||
@a2.enable_service(flag)
|
||||
@a2.save!
|
||||
AccountNotification.for_account(@a1).should == []
|
||||
AccountNotification.for_account(@a2).should == [@announcement]
|
||||
end
|
||||
|
||||
describe "display_for_user?" do
|
||||
it "should select each mod value once throughout the cycle" do
|
||||
AccountNotification.display_for_user?(5, 3, Time.zone.parse('2012-04-02')).should == false
|
||||
AccountNotification.display_for_user?(6, 3, Time.zone.parse('2012-04-02')).should == false
|
||||
AccountNotification.display_for_user?(7, 3, Time.zone.parse('2012-04-02')).should == true
|
||||
|
||||
AccountNotification.display_for_user?(5, 3, Time.zone.parse('2012-05-05')).should == true
|
||||
AccountNotification.display_for_user?(6, 3, Time.zone.parse('2012-05-05')).should == false
|
||||
AccountNotification.display_for_user?(7, 3, Time.zone.parse('2012-05-05')).should == false
|
||||
|
||||
AccountNotification.display_for_user?(5, 3, Time.zone.parse('2012-06-04')).should == false
|
||||
AccountNotification.display_for_user?(6, 3, Time.zone.parse('2012-06-04')).should == true
|
||||
AccountNotification.display_for_user?(7, 3, Time.zone.parse('2012-06-04')).should == false
|
||||
end
|
||||
|
||||
it "should shift the mod values each new cycle" do
|
||||
AccountNotification.display_for_user?(7, 3, Time.zone.parse('2012-04-02')).should == true
|
||||
AccountNotification.display_for_user?(7, 3, Time.zone.parse('2012-07-02')).should == false
|
||||
AccountNotification.display_for_user?(7, 3, Time.zone.parse('2012-09-02')).should == true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "sharding" do
|
||||
specs_require_sharding
|
||||
|
||||
|
|
|
@ -266,7 +266,7 @@ describe Account do
|
|||
Account.services_exposed_to_ui_hash(:setting).keys.should == Account.allowable_services.reject { |h,k| k[:expose_to_ui] != :setting || (k[:expose_to_ui_proc] && !k[:expose_to_ui_proc].call(nil)) }.keys
|
||||
end
|
||||
|
||||
it "should filter based on user if a proc is specified" do
|
||||
it "should filter based on user and account if a proc is specified" do
|
||||
user1 = User.create!
|
||||
user2 = User.create!
|
||||
Account.register_service(:myservice, {
|
||||
|
@ -274,11 +274,11 @@ describe Account do
|
|||
description: "Nope",
|
||||
expose_to_ui: :setting,
|
||||
default: false,
|
||||
expose_to_ui_proc: proc { |user| user == user2 },
|
||||
expose_to_ui_proc: proc { |user, account| user == user2 && account == Account.default },
|
||||
})
|
||||
Account.services_exposed_to_ui_hash(:setting).keys.should_not be_include(:myservice)
|
||||
Account.services_exposed_to_ui_hash(:setting, user1).keys.should_not be_include(:myservice)
|
||||
Account.services_exposed_to_ui_hash(:setting, user2).keys.should be_include(:myservice)
|
||||
Account.services_exposed_to_ui_hash(:setting, user1, Account.default).keys.should_not be_include(:myservice)
|
||||
Account.services_exposed_to_ui_hash(:setting, user2, Account.default).keys.should be_include(:myservice)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ describe "settings tabs" do
|
|||
notification.start_at.to_s.should include_text today
|
||||
notification.end_at.to_s.should include_text tomorrow
|
||||
f("#tab-announcements .user_content").text.should == "this is a message"
|
||||
notification
|
||||
end
|
||||
|
||||
describe "site admin" do
|
||||
|
@ -70,6 +71,16 @@ describe "settings tabs" do
|
|||
f("#confirm_global_announcement").click
|
||||
end
|
||||
end
|
||||
|
||||
it "should create survey announcements" do
|
||||
notification = add_announcement do
|
||||
f("#account_notification_required_account_service").click
|
||||
get_value("#account_notification_months_in_display_cycle").should == AccountNotification.default_months_in_display_cycle.to_s
|
||||
set_value(f("#account_notification_months_in_display_cycle"), "12")
|
||||
end
|
||||
notification.required_account_service.should == "account_survey_notifications"
|
||||
notification.months_in_display_cycle.should == 12
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -162,7 +173,9 @@ describe "settings tabs" do
|
|||
|
||||
context "announcements tab" do
|
||||
it "should add an announcement" do
|
||||
add_announcement
|
||||
notification = add_announcement
|
||||
notification.required_account_service.should be_nil
|
||||
notification.months_in_display_cycle.should be_nil
|
||||
end
|
||||
|
||||
it "should delete an announcement" do
|
||||
|
|
Loading…
Reference in New Issue