Skip to content

Commit 2840baa

Browse files
authored
Merge branch 'develop' into art/1738/activerecord-caching
2 parents b8c6dd0 + 4153f8d commit 2840baa

32 files changed

+509
-103
lines changed

app/assets/javascripts/categories.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ $(() => {
4343
$topic.val(union).trigger('change');
4444
});
4545

46-
$('.js-category-change-select').each((_i, el) => {
46+
$('.js-category-select').each((_i, el) => {
4747
const $tgt = $(el);
4848
$tgt.select2({
4949
ajax: {

app/assets/javascripts/posts.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,6 @@ $(() => {
114114
$postField.val($postField.val()?.toString().replace(placeholder, ''));
115115
});
116116

117-
$('.js-category-select').select2({
118-
tags: true
119-
});
120-
121117
/**
122118
* @typedef {{
123119
* body: string
@@ -322,7 +318,7 @@ $(() => {
322318
}
323319
}
324320
else {
325-
console.error('Failed to delete draft.');
321+
QPixel.createNotification('danger', `Failed to delete post draft. (${resp.status})`);
326322
}
327323
}
328324

app/assets/javascripts/subscriptions.js

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,79 @@
1-
$(() => {
1+
document.addEventListener('DOMContentLoaded', () => {
2+
/**
3+
* Extracts qualifier type from the field's dataset
4+
* @returns {string | null}
5+
*/
6+
const getQualifierType = () => {
7+
const field = document.querySelector('.js-sub-type-select');
8+
return field instanceof HTMLSelectElement ? field.value : null;
9+
};
10+
11+
/**
12+
* Is a given subscription type qualifiable?
13+
* @param {string} type subscription type
14+
* @returns {boolean}
15+
*/
16+
const isQualifiable = (type) => {
17+
return ['category', 'tag', 'user'].includes(type);
18+
};
19+
20+
/**
21+
* Synchronizes qualifier field with the given type
22+
* @param {string} type subscription type
23+
* @param {boolean} [clear] whether to clear qualifier value
24+
*/
25+
const syncQualifier = (type, clear = true) => {
26+
const field = document.querySelector('.js-sub-qualifier-select');
27+
const label = document.querySelector('.js-sub-qualifier-label');
28+
29+
if (field instanceof HTMLElement) {
30+
if (clear) {
31+
$(field).val(null).trigger('change');
32+
}
33+
34+
field.closest('.form-group')?.classList.toggle('hide', !isQualifiable(type));
35+
}
36+
37+
if (label instanceof HTMLElement) {
38+
label.textContent = type.slice(0, 1).toUpperCase() + type.slice(1).toLowerCase();
39+
}
40+
};
41+
42+
/**
43+
* Is a given element a subscription type select?
44+
* @param {Element} element
45+
* @returns {element is HTMLSelectElement}
46+
*/
47+
const isTypeSelect = (element) => {
48+
return element.matches('.js-sub-type-select');
49+
};
50+
51+
document.querySelectorAll('.js-sub-type-select, .js-sub-frequency-select').forEach((el) => {
52+
$(el).select2().on('change', ($event) => {
53+
if (isTypeSelect($event.target)) {
54+
syncQualifier($event.target.value);
55+
}
56+
});
57+
58+
if (isTypeSelect(el)) {
59+
syncQualifier(el.value, false);
60+
}
61+
});
62+
63+
$('.js-sub-qualifier-select').select2({
64+
ajax: {
65+
url: () => {
66+
const type = getQualifierType();
67+
return `/subscriptions/qualifiers?type=${type}`
68+
},
69+
headers: { 'Accept': 'application/json' },
70+
delay: 100,
71+
processResults: (results) => {
72+
return { results }
73+
},
74+
}
75+
});
76+
277
$('.js-enable-subscription').on('change', async (evt) => {
378
const $tgt = $(evt.target);
479
const $sub = $tgt.parents('details');

app/assets/javascripts/users.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,16 @@ $(() => {
7777
location.reload();
7878
}
7979
});
80+
81+
$('.js-user-select').each((_i, el) => {
82+
const $tgt = $(el);
83+
$tgt.select2({
84+
ajax: {
85+
url: '/users',
86+
headers: { 'Accept': 'application/json' },
87+
delay: 100,
88+
processResults: (data) => ({results: data.map((u) => ({id: u.id, text: u.username}))}),
89+
}
90+
});
91+
});
8092
});
Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
1-
// Place all the styles related to the Subscriptions controller here.
2-
// They will automatically be included in application.css.
3-
// You can use Sass (SCSS) here: http://sass-lang.com/
1+
@import 'variables';
2+
3+
.new_subscription {
4+
.select2-container {
5+
height: 37px;
6+
margin: 4px 0px;
7+
}
8+
9+
.select2-container {
10+
.selection {
11+
.select2-selection {
12+
border-color: $muted-graphic;
13+
height: 100%;
14+
padding: 4px 0;
15+
}
16+
17+
.select2-selection__arrow {
18+
top: 50%;
19+
translate: 0 -50%;
20+
}
21+
}
22+
}
23+
}

app/controllers/subscriptions_controller.rb

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
class SubscriptionsController < ApplicationController
22
before_action :authenticate_user!
33
before_action :stop_the_awful_troll
4-
helper_method :phrase_for
54

65
def new
7-
@phrasing = phrase_for params[:type]
8-
@subscription = Subscription.new
6+
@subscription = Subscription.new(new_sub_params)
97
end
108

119
def create
@@ -14,14 +12,41 @@ def create
1412
flash[:success] = 'Your subscription was saved successfully.'
1513
redirect_to params[:return_to].presence || root_path
1614
else
17-
render :error, status: :internal_server_error
15+
render :new, status: :bad_request
1816
end
1917
end
2018

2119
def index
2220
@subscriptions = current_user.subscriptions
2321
end
2422

23+
def qualifiers
24+
per_page = 20
25+
26+
@items = case params[:type]
27+
when 'category'
28+
Category.accessible_to(current_user)
29+
.order(sequence: :asc, id: :asc)
30+
when 'tag'
31+
Tag.order(name: :asc)
32+
when 'user'
33+
User.accessible_to(current_user)
34+
.joins(:community_user)
35+
.undeleted
36+
.where.not(community_users: { deleted: true })
37+
.order(username: :asc)
38+
end
39+
40+
@items = params[:q].present? ? @items&.search(params[:q]) : @items
41+
@items = @items&.paginate(page: params[:page], per_page: per_page).to_a
42+
43+
@items = @items.map do |item|
44+
{ id: item.is_a?(Tag) ? item.name : item.id, text: item.name }
45+
end
46+
47+
render json: @items
48+
end
49+
2550
def enable
2651
@subscription = Subscription.find params[:id]
2752
if current_user.admin? || current_user.id == @subscription.user_id
@@ -56,29 +81,12 @@ def destroy
5681
end
5782
end
5883

59-
protected
84+
private
6085

61-
def phrase_for(type, qualifier = nil)
62-
case type
63-
when 'all'
64-
'all new questions'
65-
when 'tag'
66-
"new questions in the tag '#{Tag.find_by(name: qualifier || params[:qualifier])&.name}'"
67-
when 'user'
68-
"new questions by the user '#{User.find_by(id: qualifier || params[:qualifier])&.username}'"
69-
when 'interesting'
70-
'new questions classed as interesting'
71-
when 'category'
72-
"new questions in the category '#{Category.find_by(id: qualifier || params[:qualifier])&.name}'"
73-
when 'moderators'
74-
'announcements and newsletters for moderators'
75-
else
76-
'nothing, apparently. How did you get here, again?'
77-
end
86+
def new_sub_params
87+
params.permit(:type, :qualifier, :frequency, :name)
7888
end
7989

80-
private
81-
8290
def sub_params
8391
params.require(:subscription).permit(:type, :qualifier, :frequency, :name)
8492
end

app/controllers/users_controller.rb

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ def index
2929
.paginate(page: params[:page], per_page: 48)
3030

3131
@post_counts = Post.where(user_id: @users.pluck(:id).uniq).group(:user_id).count
32+
33+
respond_to do |format|
34+
format.html
35+
format.json do
36+
render json: @users
37+
end
38+
end
3239
end
3340

3441
def show
@@ -682,11 +689,9 @@ def set_user
682689
end
683690

684691
def user_scope
685-
if current_user&.at_least_moderator?
686-
User.all
687-
else
688-
User.undeleted
689-
end.joins(:community_user).includes(:community_user, :avatar_attachment)
692+
User.accessible_to(current_user)
693+
.joins(:community_user)
694+
.includes(:community_user, :avatar_attachment)
690695
end
691696

692697
def check_deleted
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,36 @@
11
module SubscriptionsHelper
2+
# Generates <select> options for available subscription frequences
3+
# @return [Array(String, Integer)]
4+
def frequency_choice
5+
[
6+
['Every day', 1],
7+
['Every week', 7],
8+
['Every month', 30],
9+
['Every quarter', 90]
10+
]
11+
end
12+
13+
# Gets human-readable representation of a given subscription type
14+
# @param type [String] subscription type
15+
# @return [String]
16+
def phrase_for(type)
17+
phrase_map = {
18+
all: 'all new questions',
19+
tag: 'new questions with the tag',
20+
user: 'new questions by the user',
21+
interesting: 'new questions classed as interesting',
22+
category: 'new questions in the category',
23+
moderators: 'announcements and newsletters for moderators'
24+
}
25+
26+
phrase_map[type.to_sym] || 'nothing, apparently. How did you get here, again?'
27+
end
28+
29+
# Generates <select> options for available subscription types for a given user
30+
# @param user [User] user to perform access control checks for
31+
# @return [Array(String, String)]
32+
def type_choice_for(user)
33+
Subscription.types_accessible_to(user)
34+
.map { |type| [phrase_for(type).capitalize, type] }
35+
end
236
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class PotentialSpamProfilesJob < ApplicationJob
2+
queue_as :default
3+
4+
def perform
5+
sql = File.read(Rails.root.join('db/scripts/potential_spam_profiles.sql'))
6+
user_ids = ActiveRecord::Base.connection.execute(sql).to_a.flatten
7+
users = User.where(id: user_ids)
8+
9+
ability_ids = Ability.unscoped.where(internal_id: 'unrestricted').map(&:id)
10+
11+
users.each do |user|
12+
cu_ids = user.community_users.map(&:id)
13+
UserAbility.where(community_user_id: cu_ids, ability_id: ability_ids)
14+
.update_all(is_suspended: true, suspension_message: 'This ability has been automatically suspended.')
15+
end
16+
end
17+
end

app/models/category.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,8 @@ def self.by_id(id)
6868
end
6969
categories[id]
7070
end
71+
72+
def self.search(term)
73+
where('name LIKE ?', "%#{sanitize_sql_like(term)}%")
74+
end
7175
end

0 commit comments

Comments
 (0)