Version bump
This commit is contained in:
commit
85d6de7b00
@ -0,0 +1,9 @@
|
||||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RestAdapter.extend({
|
||||
jsonMode: true,
|
||||
|
||||
pathFor(store, type, id) {
|
||||
return `/review/${id}/explain.json`;
|
||||
}
|
||||
});
|
||||
@ -19,12 +19,12 @@ export default Ember.Component.extend({
|
||||
@computed("model.visibility_level", "model.public_admission")
|
||||
disableMembershipRequestSetting(visibility_level, publicAdmission) {
|
||||
visibility_level = parseInt(visibility_level);
|
||||
return ![0, 1].includes(visibility_level) || publicAdmission;
|
||||
return publicAdmission || visibility_level > 1;
|
||||
},
|
||||
|
||||
@computed("model.visibility_level", "model.allow_membership_requests")
|
||||
disablePublicSetting(visibility_level, allowMembershipRequests) {
|
||||
visibility_level = parseInt(visibility_level);
|
||||
return ![0, 1].includes(visibility_level) || allowMembershipRequests;
|
||||
return allowMembershipRequests || visibility_level > 1;
|
||||
}
|
||||
});
|
||||
|
||||
@ -3,6 +3,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import Category from "discourse/models/category";
|
||||
import optionalService from "discourse/lib/optional-service";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
|
||||
let _components = {};
|
||||
|
||||
@ -140,6 +141,13 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
actions: {
|
||||
explainReviewable(reviewable) {
|
||||
showModal("explain-reviewable", {
|
||||
title: "review.explain.title",
|
||||
model: reviewable
|
||||
});
|
||||
},
|
||||
|
||||
edit() {
|
||||
this.set("editing", true);
|
||||
this._updates = { payload: {} };
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
loading: null,
|
||||
reviewableExplanation: null,
|
||||
|
||||
onShow() {
|
||||
this.setProperties({ loading: true, reviewableExplanation: null });
|
||||
|
||||
this.store
|
||||
.find("reviewable-explanation", this.model.id)
|
||||
.then(result => this.set("reviewableExplanation", result))
|
||||
.finally(() => this.set("loading", false));
|
||||
}
|
||||
});
|
||||
5
app/assets/javascripts/discourse/helpers/float.js.es6
Normal file
5
app/assets/javascripts/discourse/helpers/float.js.es6
Normal file
@ -0,0 +1,5 @@
|
||||
import { registerUnbound } from "discourse-common/lib/helpers";
|
||||
|
||||
registerUnbound("float", function(n) {
|
||||
return parseFloat(n).toFixed(1);
|
||||
});
|
||||
@ -139,7 +139,7 @@ const Group = RestModel.extend({
|
||||
|
||||
@computed("visibility_level")
|
||||
isPrivate(visibilityLevel) {
|
||||
return ![0, 1].includes(visibilityLevel);
|
||||
return visibilityLevel > 1;
|
||||
},
|
||||
|
||||
@observes("isPrivate", "canEveryoneMention")
|
||||
|
||||
@ -53,7 +53,7 @@ const LoginMethod = Ember.Object.extend({
|
||||
}
|
||||
LoginMethod.buildPostForm(authUrl).then(form => {
|
||||
const windowState = window.open(
|
||||
authUrl,
|
||||
"about:blank",
|
||||
"auth_popup",
|
||||
`menubar=no,status=no,height=${height},width=${width},left=${left},top=${top}`
|
||||
);
|
||||
|
||||
@ -73,8 +73,9 @@ export default Discourse.Route.extend({
|
||||
const params = filterQueryParams(transition.to.queryParams, {});
|
||||
const categorySlug = this.categorySlug;
|
||||
const parentCategorySlug = this.parentCategorySlug;
|
||||
const filter = this.navMode;
|
||||
const topicFilter = this.navMode;
|
||||
const tagId = tag ? tag.id.toLowerCase() : "none";
|
||||
let filter;
|
||||
|
||||
if (categorySlug) {
|
||||
const category = Discourse.Category.findBySlug(
|
||||
@ -82,31 +83,25 @@ export default Discourse.Route.extend({
|
||||
parentCategorySlug
|
||||
);
|
||||
if (parentCategorySlug) {
|
||||
params.filter = `tags/c/${parentCategorySlug}/${categorySlug}/${tagId}/l/${filter}`;
|
||||
filter = `tags/c/${parentCategorySlug}/${categorySlug}/${tagId}/l/${topicFilter}`;
|
||||
} else {
|
||||
params.filter = `tags/c/${categorySlug}/${tagId}/l/${filter}`;
|
||||
filter = `tags/c/${categorySlug}/${tagId}/l/${topicFilter}`;
|
||||
}
|
||||
if (category) {
|
||||
category.setupGroupsAndPermissions();
|
||||
this.set("category", category);
|
||||
}
|
||||
} else if (this.additionalTags) {
|
||||
params.filter = `tags/intersection/${tagId}/${this.get(
|
||||
"additionalTags"
|
||||
).join("/")}`;
|
||||
filter = `tags/intersection/${tagId}/${this.additionalTags.join("/")}`;
|
||||
this.set("category", null);
|
||||
} else {
|
||||
params.filter = `tags/${tagId}/l/${filter}`;
|
||||
filter = `tags/${tagId}/l/${topicFilter}`;
|
||||
this.set("category", null);
|
||||
}
|
||||
|
||||
return findTopicList(
|
||||
this.store,
|
||||
this.topicTrackingState,
|
||||
params.filter,
|
||||
params,
|
||||
{ cached: true }
|
||||
).then(list => {
|
||||
return findTopicList(this.store, this.topicTrackingState, filter, params, {
|
||||
cached: true
|
||||
}).then(list => {
|
||||
if (list.topic_list.tags && list.topic_list.tags.length === 1) {
|
||||
// Update name of tag (case might be different)
|
||||
tag.setProperties({
|
||||
|
||||
@ -10,6 +10,9 @@
|
||||
<span class='status'>
|
||||
{{reviewable-status reviewable.status}}
|
||||
</span>
|
||||
<a {{action "explainReviewable" reviewable}} class='explain' title={{i18n "review.explain.why"}}>
|
||||
{{d-icon "question-circle"}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class='reviewable-contents'>
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
{{#if value}}
|
||||
<span class='score-value'>
|
||||
<span class='score-number'>{{float value}}</span>
|
||||
{{#if label}}
|
||||
<span class='score-value-type' title={{i18n (concat "review.explain." label ".title")}}>
|
||||
{{i18n (concat "review.explain." label ".name")}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class='op'>+</span>
|
||||
{{/if}}
|
||||
@ -0,0 +1,47 @@
|
||||
{{#d-modal-body class="explain-reviewable"}}
|
||||
{{#conditional-loading-spinner condition=loading}}
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{i18n "review.explain.formula"}}</th>
|
||||
<th>{{i18n "review.explain.subtotal"}}</th>
|
||||
</tr>
|
||||
{{#each reviewableExplanation.scores as |s|}}
|
||||
<tr>
|
||||
<td>
|
||||
{{score-value value="1.0" tagName=""}}
|
||||
{{score-value value=s.type_bonus label="type_bonus" tagName=""}}
|
||||
{{score-value value=s.take_action_bonus label="take_action_bonus" tagName=""}}
|
||||
{{score-value value=s.trust_level_bonus label="trust_level_bonus" tagName=""}}
|
||||
{{score-value value=s.user_accuracy_bonus label="user_accuracy_bonus" tagName=""}}
|
||||
</td>
|
||||
<td class='sum'>{{float s.score}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
<tr class="total">
|
||||
<td>{{i18n "review.explain.total"}}</td>
|
||||
<td class='sum'>{{float reviewableExplanation.total_score}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class='thresholds'>
|
||||
<tr>
|
||||
<td>{{i18n "review.explain.min_score_visibility"}}</td>
|
||||
<td class='sum'>
|
||||
{{float reviewableExplanation.min_score_visibility}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{i18n "review.explain.score_to_hide"}}</td>
|
||||
<td class='sum'>
|
||||
{{float reviewableExplanation.hide_post_score}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{/conditional-loading-spinner}}
|
||||
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
{{d-button action=(route-action "closeModal") label="close"}}
|
||||
</div>
|
||||
@ -145,6 +145,7 @@
|
||||
topic=model
|
||||
expanded=info.topicProgressExpanded
|
||||
jumpToPost=(action "jumpToPost")}}
|
||||
{{plugin-outlet name="before-topic-progress" args=(hash model=model jumpToPost=(action "jumpToPost"))}}
|
||||
{{#if info.renderAdminMenuButton}}
|
||||
{{topic-admin-menu-button
|
||||
topic=model
|
||||
|
||||
@ -69,15 +69,14 @@ registerButton("read-count", attrs => {
|
||||
});
|
||||
|
||||
registerButton("read", attrs => {
|
||||
const disabled = attrs.readCount === 0;
|
||||
if (attrs.showReadIndicator) {
|
||||
const readBySomeone = attrs.readCount > 0;
|
||||
if (attrs.showReadIndicator && readBySomeone) {
|
||||
return {
|
||||
action: "toggleWhoRead",
|
||||
title: "post.controls.read_indicator",
|
||||
icon: "book-reader",
|
||||
before: "read-count",
|
||||
addContainer: false,
|
||||
disabled
|
||||
addContainer: false
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
37
app/assets/stylesheets/common/base/explain-reviewable.scss
Normal file
37
app/assets/stylesheets/common/base/explain-reviewable.scss
Normal file
@ -0,0 +1,37 @@
|
||||
.explain-reviewable {
|
||||
min-width: 500px;
|
||||
|
||||
.thresholds {
|
||||
margin-top: 1em;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
table td {
|
||||
padding: 0.5em;
|
||||
}
|
||||
td.sum {
|
||||
text-align: right;
|
||||
}
|
||||
td.sum.total {
|
||||
font-weight: bold;
|
||||
}
|
||||
tr.total {
|
||||
td {
|
||||
background-color: $primary-low;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.op {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.score-value-type {
|
||||
color: $primary-medium;
|
||||
}
|
||||
|
||||
.op:last-of-type {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@ -20,6 +20,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.explain {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.nav-pills {
|
||||
margin-bottom: 1em;
|
||||
|
||||
@ -40,6 +40,10 @@
|
||||
display: inherit;
|
||||
}
|
||||
}
|
||||
.timeline-controls {
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
&.timeline-fullscreen {
|
||||
|
||||
@ -204,7 +204,7 @@ class PostsController < ApplicationController
|
||||
|
||||
if !guardian.public_send("can_edit?", post) &&
|
||||
post.user_id == current_user.id &&
|
||||
post.edit_time_limit_expired?
|
||||
post.edit_time_limit_expired?(current_user)
|
||||
|
||||
return render_json_error(I18n.t('too_late_to_edit'))
|
||||
end
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
require_dependency 'reviewable_explanation_serializer'
|
||||
|
||||
class ReviewablesController < ApplicationController
|
||||
requires_login
|
||||
@ -102,6 +103,17 @@ class ReviewablesController < ApplicationController
|
||||
)
|
||||
end
|
||||
|
||||
def explain
|
||||
reviewable = find_reviewable
|
||||
|
||||
render_serialized(
|
||||
{ reviewable: reviewable, scores: reviewable.explain_score },
|
||||
ReviewableExplanationSerializer,
|
||||
rest_serializer: true,
|
||||
root: 'reviewable_explanation'
|
||||
)
|
||||
end
|
||||
|
||||
def show
|
||||
reviewable = find_reviewable
|
||||
|
||||
|
||||
31
app/jobs/onceoff/clean_up_post_timings.rb
Normal file
31
app/jobs/onceoff/clean_up_post_timings.rb
Normal file
@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class CleanUpPostTimings < Jobs::Onceoff
|
||||
|
||||
# Remove post timings that are remnants of previous post moves
|
||||
# or other shenanigans and don't reference a valid user or post anymore.
|
||||
def execute_onceoff(args)
|
||||
DB.exec <<~SQL
|
||||
DELETE
|
||||
FROM post_timings pt
|
||||
WHERE NOT EXISTS(
|
||||
SELECT 1
|
||||
FROM posts p
|
||||
WHERE p.topic_id = pt.topic_id
|
||||
AND p.post_number = pt.post_number
|
||||
)
|
||||
SQL
|
||||
|
||||
DB.exec <<~SQL
|
||||
DELETE
|
||||
FROM post_timings pt
|
||||
WHERE NOT EXISTS(
|
||||
SELECT 1
|
||||
FROM users u
|
||||
WHERE pt.user_id = u.id
|
||||
)
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -3,11 +3,22 @@
|
||||
module LimitedEdit
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def edit_time_limit_expired?
|
||||
if created_at && SiteSetting.post_edit_time_limit.to_i > 0
|
||||
created_at < SiteSetting.post_edit_time_limit.to_i.minutes.ago
|
||||
def edit_time_limit_expired?(user)
|
||||
time_limit = user_time_limit(user)
|
||||
if created_at && time_limit > 0
|
||||
created_at < time_limit.minutes.ago
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_time_limit(user)
|
||||
if user.trust_level < 2
|
||||
SiteSetting.post_edit_time_limit.to_i
|
||||
else
|
||||
SiteSetting.tl2_post_edit_time_limit.to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -90,7 +90,10 @@ class PostMover
|
||||
new_topic_title VARCHAR,
|
||||
new_post_id INTEGER,
|
||||
new_post_number INTEGER
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX moved_posts_old_post_number ON moved_posts(old_post_number);
|
||||
CREATE INDEX moved_posts_old_post_id ON moved_posts(old_post_id);
|
||||
SQL
|
||||
end
|
||||
|
||||
@ -105,11 +108,8 @@ class PostMover
|
||||
@move_map = {}
|
||||
@reply_count = {}
|
||||
posts.each_with_index do |post, offset|
|
||||
unless post.is_first_post?
|
||||
@move_map[post.post_number] = offset + max_post_number
|
||||
else
|
||||
@move_map[post.post_number] = 1
|
||||
end
|
||||
@move_map[post.post_number] = offset + max_post_number
|
||||
|
||||
if post.reply_to_post_number.present?
|
||||
@reply_count[post.reply_to_post_number] = (@reply_count[post.reply_to_post_number] || 0) + 1
|
||||
end
|
||||
@ -131,6 +131,9 @@ class PostMover
|
||||
update_reply_counts
|
||||
move_first_post_replies
|
||||
delete_post_replies
|
||||
copy_first_post_timings
|
||||
move_post_timings
|
||||
copy_topic_users
|
||||
end
|
||||
|
||||
def create_first_post(post)
|
||||
@ -260,7 +263,7 @@ class PostMover
|
||||
DB.exec <<~SQL
|
||||
UPDATE post_replies pr
|
||||
SET post_id = mp.new_post_id
|
||||
FROM moved_posts mp, moved_posts mr
|
||||
FROM moved_posts mp
|
||||
WHERE mp.old_post_id <> mp.new_post_id AND pr.post_id = mp.old_post_id AND
|
||||
EXISTS (SELECT 1 FROM moved_posts mr WHERE mr.new_post_id = pr.reply_id)
|
||||
SQL
|
||||
@ -275,11 +278,124 @@ class PostMover
|
||||
SQL
|
||||
end
|
||||
|
||||
def copy_first_post_timings
|
||||
DB.exec <<~SQL
|
||||
INSERT INTO post_timings (topic_id, user_id, post_number, msecs)
|
||||
SELECT mp.new_topic_id, pt.user_id, mp.new_post_number, pt.msecs
|
||||
FROM post_timings pt
|
||||
JOIN moved_posts mp ON (pt.topic_id = mp.old_topic_id AND pt.post_number = mp.old_post_number)
|
||||
WHERE mp.old_post_id <> mp.new_post_id
|
||||
ON CONFLICT (topic_id, post_number, user_id) DO UPDATE
|
||||
SET msecs = GREATEST(post_timings.msecs, excluded.msecs)
|
||||
SQL
|
||||
end
|
||||
|
||||
def move_post_timings
|
||||
DB.exec <<~SQL
|
||||
UPDATE post_timings pt
|
||||
SET topic_id = mp.new_topic_id,
|
||||
post_number = mp.new_post_number
|
||||
FROM moved_posts mp
|
||||
WHERE pt.topic_id = mp.old_topic_id
|
||||
AND pt.post_number = mp.old_post_number
|
||||
AND mp.old_post_id = mp.new_post_id
|
||||
SQL
|
||||
end
|
||||
|
||||
def copy_topic_users
|
||||
params = {
|
||||
old_topic_id: original_topic.id,
|
||||
new_topic_id: destination_topic.id,
|
||||
old_highest_post_number: destination_topic.highest_post_number,
|
||||
old_highest_staff_post_number: destination_topic.highest_staff_post_number
|
||||
}
|
||||
|
||||
DB.exec(<<~SQL, params)
|
||||
INSERT INTO topic_users(user_id, topic_id, posted, last_read_post_number, highest_seen_post_number,
|
||||
last_emailed_post_number, first_visited_at, last_visited_at, notification_level,
|
||||
notifications_changed_at, notifications_reason_id)
|
||||
SELECT tu.user_id,
|
||||
:new_topic_id AS topic_id,
|
||||
EXISTS(
|
||||
SELECT 1
|
||||
FROM posts p
|
||||
WHERE p.topic_id = tu.topic_id
|
||||
AND p.user_id = tu.user_id
|
||||
) AS posted,
|
||||
MAX(lr.new_post_number) AS last_read_post_number,
|
||||
MAX(hs.new_post_number) AS highest_seen_post_number,
|
||||
MAX(le.new_post_number) AS last_emailed_post_number,
|
||||
GREATEST(tu.first_visited_at, t.created_at) AS first_visited_at,
|
||||
GREATEST(tu.last_visited_at, t.created_at) AS last_visited_at,
|
||||
tu.notification_level,
|
||||
tu.notifications_changed_at,
|
||||
tu.notifications_reason_id
|
||||
FROM topic_users tu
|
||||
JOIN topics t
|
||||
ON (t.id = :new_topic_id)
|
||||
LEFT OUTER JOIN moved_posts lr
|
||||
ON (lr.old_topic_id = tu.topic_id AND lr.old_post_number <= tu.last_read_post_number)
|
||||
LEFT OUTER JOIN moved_posts hs
|
||||
ON (hs.old_topic_id = tu.topic_id AND hs.old_post_number <= tu.highest_seen_post_number)
|
||||
LEFT OUTER JOIN moved_posts le
|
||||
ON (le.old_topic_id = tu.topic_id AND le.old_post_number <= tu.last_emailed_post_number)
|
||||
WHERE tu.topic_id = :old_topic_id
|
||||
AND GREATEST(
|
||||
tu.last_read_post_number,
|
||||
tu.highest_seen_post_number,
|
||||
tu.last_emailed_post_number
|
||||
) >= (SELECT MIN(old_post_number) FROM moved_posts)
|
||||
GROUP BY tu.topic_id, tu.user_id, tu.first_visited_at, tu.last_visited_at, t.created_at, tu.notification_level,
|
||||
tu.notifications_changed_at,
|
||||
tu.notifications_reason_id
|
||||
ON CONFLICT (topic_id, user_id) DO UPDATE
|
||||
SET posted = excluded.posted,
|
||||
last_read_post_number = CASE
|
||||
WHEN topic_users.last_read_post_number = :old_highest_staff_post_number OR (
|
||||
:old_highest_post_number < :old_highest_staff_post_number
|
||||
AND topic_users.last_read_post_number = :old_highest_post_number
|
||||
AND NOT EXISTS(SELECT 1
|
||||
FROM users u
|
||||
WHERE u.id = topic_users.user_id
|
||||
AND (admin OR moderator))
|
||||
) THEN
|
||||
GREATEST(topic_users.last_read_post_number,
|
||||
excluded.last_read_post_number)
|
||||
ELSE topic_users.last_read_post_number END,
|
||||
highest_seen_post_number = CASE
|
||||
WHEN topic_users.highest_seen_post_number = :old_highest_staff_post_number OR (
|
||||
:old_highest_post_number < :old_highest_staff_post_number
|
||||
AND topic_users.highest_seen_post_number = :old_highest_post_number
|
||||
AND NOT EXISTS(SELECT 1
|
||||
FROM users u
|
||||
WHERE u.id = topic_users.user_id
|
||||
AND (admin OR moderator))
|
||||
) THEN
|
||||
GREATEST(topic_users.highest_seen_post_number,
|
||||
excluded.highest_seen_post_number)
|
||||
ELSE topic_users.highest_seen_post_number END,
|
||||
last_emailed_post_number = CASE
|
||||
WHEN topic_users.last_emailed_post_number = :old_highest_staff_post_number OR (
|
||||
:old_highest_post_number < :old_highest_staff_post_number
|
||||
AND topic_users.last_emailed_post_number = :old_highest_post_number
|
||||
AND NOT EXISTS(SELECT 1
|
||||
FROM users u
|
||||
WHERE u.id = topic_users.user_id
|
||||
AND (admin OR moderator))
|
||||
) THEN
|
||||
GREATEST(topic_users.last_emailed_post_number,
|
||||
excluded.last_emailed_post_number)
|
||||
ELSE topic_users.last_emailed_post_number END,
|
||||
first_visited_at = LEAST(topic_users.first_visited_at, excluded.first_visited_at),
|
||||
last_visited_at = GREATEST(topic_users.last_visited_at, excluded.last_visited_at)
|
||||
SQL
|
||||
end
|
||||
|
||||
def update_statistics
|
||||
destination_topic.update_statistics
|
||||
original_topic.update_statistics
|
||||
TopicUser.update_post_action_cache(topic_id: original_topic.id, post_action_type: :bookmark)
|
||||
TopicUser.update_post_action_cache(topic_id: destination_topic.id, post_action_type: :bookmark)
|
||||
TopicUser.update_post_action_cache(topic_id: original_topic.id)
|
||||
TopicUser.update_post_action_cache(topic_id: destination_topic.id)
|
||||
end
|
||||
|
||||
def update_user_actions
|
||||
@ -340,7 +456,7 @@ class PostMover
|
||||
attrs = {}
|
||||
attrs[:last_posted_at] = post.created_at
|
||||
attrs[:last_post_user_id] = post.user_id
|
||||
attrs[:bumped_at] = post.created_at unless post.no_bump
|
||||
attrs[:bumped_at] = Time.now
|
||||
attrs[:updated_at] = Time.now
|
||||
destination_topic.update_columns(attrs)
|
||||
end
|
||||
|
||||
@ -481,6 +481,25 @@ class Reviewable < ActiveRecord::Base
|
||||
.count
|
||||
end
|
||||
|
||||
def explain_score
|
||||
DB.query(<<~SQL, reviewable_id: id)
|
||||
SELECT rs.reviewable_id,
|
||||
rs.user_id,
|
||||
CASE WHEN (u.admin OR u.moderator) THEN 5.0 ELSE u.trust_level END AS trust_level_bonus,
|
||||
us.flags_agreed,
|
||||
us.flags_disagreed,
|
||||
us.flags_ignored,
|
||||
rs.score,
|
||||
rs.take_action_bonus,
|
||||
COALESCE(pat.score_bonus, 0.0) AS type_bonus
|
||||
FROM reviewable_scores AS rs
|
||||
INNER JOIN users AS u ON u.id = rs.user_id
|
||||
LEFT OUTER JOIN user_stats AS us ON us.user_id = rs.user_id
|
||||
LEFT OUTER JOIN post_action_types AS pat ON pat.id = rs.reviewable_score_type
|
||||
WHERE rs.reviewable_id = :reviewable_id
|
||||
SQL
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def recalculate_score
|
||||
|
||||
@ -59,10 +59,22 @@ class ReviewableScore < ActiveRecord::Base
|
||||
user_stat = user&.user_stat
|
||||
return 0.0 if user_stat.blank?
|
||||
|
||||
total = (user_stat.flags_agreed + user_stat.flags_disagreed + user_stat.flags_ignored).to_f
|
||||
calc_user_accuracy_bonus(
|
||||
user_stat.flags_agreed,
|
||||
user_stat.flags_disagreed,
|
||||
user_stat.flags_ignored
|
||||
)
|
||||
end
|
||||
|
||||
def self.calc_user_accuracy_bonus(agreed, disagreed, ignored)
|
||||
agreed ||= 0
|
||||
disagreed ||= 0
|
||||
ignored ||= 0
|
||||
|
||||
total = (agreed + disagreed + ignored).to_f
|
||||
return 0.0 if total <= 5
|
||||
|
||||
(user_stat.flags_agreed / total) * 5.0
|
||||
(agreed / total) * 5.0
|
||||
end
|
||||
|
||||
def reviewable_conversation
|
||||
|
||||
38
app/serializers/reviewable_explanation_serializer.rb
Normal file
38
app/serializers/reviewable_explanation_serializer.rb
Normal file
@ -0,0 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
require_dependency 'reviewable_score_explanation_serializer'
|
||||
|
||||
class ReviewableExplanationSerializer < ApplicationSerializer
|
||||
attributes(
|
||||
:id,
|
||||
:total_score,
|
||||
:scores,
|
||||
:min_score_visibility,
|
||||
:hide_post_score
|
||||
)
|
||||
|
||||
has_many :scores, serializer: ReviewableScoreExplanationSerializer, embed: :objects
|
||||
|
||||
def id
|
||||
object[:reviewable].id
|
||||
end
|
||||
|
||||
def hide_post_score
|
||||
Reviewable.score_required_to_hide_post
|
||||
end
|
||||
|
||||
def spam_silence_score
|
||||
Reviewable.spam_score_to_silence_new_user
|
||||
end
|
||||
|
||||
def min_score_visibility
|
||||
Reviewable.min_score_for_priority
|
||||
end
|
||||
|
||||
def total_score
|
||||
object[:reviewable].score
|
||||
end
|
||||
|
||||
def scores
|
||||
object[:scores]
|
||||
end
|
||||
end
|
||||
24
app/serializers/reviewable_score_explanation_serializer.rb
Normal file
24
app/serializers/reviewable_score_explanation_serializer.rb
Normal file
@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ReviewableScoreExplanationSerializer < ApplicationSerializer
|
||||
attributes(
|
||||
:user_id,
|
||||
:type_bonus,
|
||||
:trust_level_bonus,
|
||||
:take_action_bonus,
|
||||
:flags_agreed,
|
||||
:flags_disagreed,
|
||||
:flags_ignored,
|
||||
:user_accuracy_bonus,
|
||||
:score
|
||||
)
|
||||
|
||||
def user_accuracy_bonus
|
||||
ReviewableScore.calc_user_accuracy_bonus(
|
||||
object.flags_agreed,
|
||||
object.flags_disagreed,
|
||||
object.flags_ignored
|
||||
)
|
||||
end
|
||||
|
||||
end
|
||||
@ -278,7 +278,7 @@ class InlineUploads
|
||||
/(upload:\/\/([a-zA-Z0-9]+)[a-zA-Z0-9\.]*)/,
|
||||
/(\/uploads\/short-url\/([a-zA-Z0-9]+)[a-zA-Z0-9\.]*)/,
|
||||
/(#{base_url}\/uploads\/short-url\/([a-zA-Z0-9]+)[a-zA-Z0-9\.]*)/,
|
||||
/(\/uploads\/#{db}#{UPLOAD_REGEXP_PATTERN})/,
|
||||
/(#{GlobalSetting.relative_url_root}\/uploads\/#{db}#{UPLOAD_REGEXP_PATTERN})/,
|
||||
/(#{base_url}\/uploads\/#{db}#{UPLOAD_REGEXP_PATTERN})/,
|
||||
]
|
||||
|
||||
|
||||
@ -116,7 +116,7 @@ class UserDestroyer
|
||||
end
|
||||
|
||||
# After the user is deleted, remove the reviewable
|
||||
if reviewable = Reviewable.pending.find_by(target: user)
|
||||
if reviewable = ReviewableUser.pending.find_by(target: user)
|
||||
reviewable.perform(@actor, :reject_user_delete)
|
||||
end
|
||||
|
||||
|
||||
@ -16,7 +16,10 @@
|
||||
</div>
|
||||
<%- end %>
|
||||
<div class='topic-author-avatar-timestamp'>
|
||||
<img src="<%= t.user.avatar_template.gsub('{size}', '20') %>">
|
||||
<img src="<%= t.user.avatar_template.gsub('{size}', '40') %>">
|
||||
<span class="topic-author-username">
|
||||
<%= t.user.username %>
|
||||
</span>
|
||||
<span class="topic-created-at" title="<%= t.created_at.strftime("%B %e, %Y %l:%M%P") %>">
|
||||
<%= "#{I18n.t('embed.created')} #{time_ago_in_words(t.created_at, scope: :'datetime.distance_in_words_verbose')}" %>
|
||||
</span>
|
||||
|
||||
@ -370,6 +370,23 @@ en:
|
||||
review:
|
||||
order_by: "Order by"
|
||||
in_reply_to: "in reply to"
|
||||
explain:
|
||||
why: "explain why this item ended up in the queue"
|
||||
title: "Reviewable Scoring"
|
||||
formula: "Formula"
|
||||
subtotal: "Subtotal"
|
||||
total: "Total"
|
||||
min_score_visibility: "Minimum Score for Visibility"
|
||||
score_to_hide: "Score to Hide Post"
|
||||
user_accuracy_bonus:
|
||||
name: "user accuracy"
|
||||
title: "Users whose flags have been historically agreed with are given a bonus."
|
||||
trust_level_bonus:
|
||||
name: "trust level"
|
||||
title: "Reviewable items created by higher trust level users have a higher score."
|
||||
type_bonus:
|
||||
name: "type bonus"
|
||||
title: "Certain reviewable types can be assigned a bonus by staff to make them a higher priority."
|
||||
claim_help:
|
||||
optional: "You can claim this item to prevent others from reviewing it."
|
||||
required: "You must claim items before you can review them."
|
||||
|
||||
@ -1361,7 +1361,8 @@ en:
|
||||
editing_grace_period_max_diff: "Maximum number of character changes allowed in editing grace period, if more changed store another post revision (trust level 0 and 1)"
|
||||
editing_grace_period_max_diff_high_trust: "Maximum number of character changes allowed in editing grace period, if more changed store another post revision (trust level 2 and up)"
|
||||
staff_edit_locks_post: "Posts will be locked from editing if they are edited by staff members"
|
||||
post_edit_time_limit: "The author can edit their post for (n) minutes after posting. Set to 0 for forever."
|
||||
post_edit_time_limit: "A tl0 or tl1 author can edit their post for (n) minutes after posting. Set to 0 for forever."
|
||||
tl2_post_edit_time_limit: "A tl2 author can edit their post for (n) minutes after posting. Set to 0 for forever."
|
||||
edit_history_visible_to_public: "Allow everyone to see previous versions of an edited post. When disabled, only staff members can view."
|
||||
delete_removed_posts_after: "Posts removed by the author will be automatically deleted after (n) hours. If set to 0, posts will be deleted immediately."
|
||||
max_image_width: "Maximum thumbnail width of images in a post"
|
||||
|
||||
@ -324,6 +324,7 @@ Discourse::Application.routes.draw do
|
||||
|
||||
get "review" => "reviewables#index" # For ember app
|
||||
get "review/:reviewable_id" => "reviewables#show", constraints: { reviewable_id: /\d+/ }
|
||||
get "review/:reviewable_id/explain" => "reviewables#explain", constraints: { reviewable_id: /\d+/ }
|
||||
get "review/topics" => "reviewables#topics"
|
||||
get "review/settings" => "reviewables#settings"
|
||||
put "review/settings" => "reviewables#settings"
|
||||
|
||||
@ -700,6 +700,9 @@ posting:
|
||||
type: category
|
||||
default: ""
|
||||
post_edit_time_limit:
|
||||
default: 1440
|
||||
max: 10080
|
||||
tl2_post_edit_time_limit:
|
||||
default: 43200
|
||||
max: 525600
|
||||
edit_history_visible_to_public:
|
||||
|
||||
@ -12,7 +12,7 @@ Create your new cloud server, for example [on DigitalOcean][do]:
|
||||
|
||||
- The default of **New York** is a good choice for most US and European audiences. Or select a region that is geographically closer to your audience.
|
||||
|
||||
- Enter your domain `discourse.example.com` as the name.
|
||||
- Enter your domain `discourse.example.com` as the Droplet name.
|
||||
|
||||
Create your new Droplet. You will receive an email with the root password. (However, if you know [how to use SSH keys](https://www.google.com/search?q=digitalocean+ssh+keys), you may not need a password to log in.)
|
||||
|
||||
|
||||
@ -158,7 +158,7 @@ module PostGuardian
|
||||
return true
|
||||
end
|
||||
|
||||
return !post.edit_time_limit_expired?
|
||||
return !post.edit_time_limit_expired?(@user)
|
||||
end
|
||||
|
||||
false
|
||||
@ -238,7 +238,7 @@ module PostGuardian
|
||||
|
||||
if @user.has_trust_level?(SiteSetting.min_trust_to_allow_self_wiki) && is_my_own?(post)
|
||||
return false if post.hidden?
|
||||
return !post.edit_time_limit_expired?
|
||||
return !post.edit_time_limit_expired?(@user)
|
||||
end
|
||||
|
||||
false
|
||||
|
||||
@ -106,7 +106,7 @@ module TopicGuardian
|
||||
|
||||
return false if topic.archived
|
||||
is_my_own?(topic) &&
|
||||
!topic.edit_time_limit_expired? &&
|
||||
!topic.edit_time_limit_expired?(user) &&
|
||||
!Post.where(topic_id: topic.id, post_number: 1).where.not(locked_by_id: nil).exists?
|
||||
end
|
||||
|
||||
|
||||
@ -863,6 +863,7 @@ class TopicQuery
|
||||
|
||||
list
|
||||
end
|
||||
|
||||
def remove_muted_categories(list, user, opts = nil)
|
||||
category_id = get_category_id(opts[:exclude]) if opts
|
||||
|
||||
@ -885,6 +886,7 @@ class TopicQuery
|
||||
|
||||
list
|
||||
end
|
||||
|
||||
def remove_muted_tags(list, user, opts = nil)
|
||||
if user.nil? || !SiteSetting.tagging_enabled || SiteSetting.remove_muted_tags_from_latest == 'never'
|
||||
return list
|
||||
@ -895,16 +897,9 @@ class TopicQuery
|
||||
return list
|
||||
end
|
||||
|
||||
showing_tag = if opts[:filter]
|
||||
f = opts[:filter].split('/')
|
||||
f[0] == 'tags' ? f[1] : nil
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
# if viewing the topic list for a muted tag, show all the topics
|
||||
if showing_tag.present? && TagUser.lookup(user, :muted).joins(:tag).where('tags.name = ?', showing_tag).exists?
|
||||
return list
|
||||
if !opts[:no_tags] && opts[:tags].present?
|
||||
return list if TagUser.lookup(user, :muted).joins(:tag).where('tags.name = ?', opts[:tags].first).exists?
|
||||
end
|
||||
|
||||
if SiteSetting.remove_muted_tags_from_latest == 'always'
|
||||
|
||||
@ -9,7 +9,7 @@ module Discourse
|
||||
MAJOR = 2
|
||||
MINOR = 4
|
||||
TINY = 0
|
||||
PRE = 'beta3'
|
||||
PRE = 'beta4'
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
|
||||
end
|
||||
|
||||
@ -1389,32 +1389,65 @@ describe Guardian do
|
||||
expect(Guardian.new(post.user).can_edit?(post)).to be_truthy
|
||||
end
|
||||
|
||||
context 'post is older than post_edit_time_limit' do
|
||||
let(:old_post) { build(:post, topic: topic, user: topic.user, created_at: 6.minutes.ago) }
|
||||
describe 'post edit time limits' do
|
||||
context 'post is older than post_edit_time_limit' do
|
||||
let(:old_post) { build(:post, topic: topic, user: topic.user, created_at: 6.minutes.ago) }
|
||||
|
||||
before do
|
||||
SiteSetting.post_edit_time_limit = 5
|
||||
before do
|
||||
topic.user.update_columns(trust_level: 1)
|
||||
SiteSetting.post_edit_time_limit = 5
|
||||
end
|
||||
|
||||
it 'returns false to the author of the post' do
|
||||
expect(Guardian.new(old_post.user).can_edit?(old_post)).to be_falsey
|
||||
end
|
||||
|
||||
it 'returns true as a moderator' do
|
||||
expect(Guardian.new(moderator).can_edit?(old_post)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns true as an admin' do
|
||||
expect(Guardian.new(admin).can_edit?(old_post)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false for another regular user trying to edit your post' do
|
||||
expect(Guardian.new(coding_horror).can_edit?(old_post)).to be_falsey
|
||||
end
|
||||
|
||||
it 'returns true for another regular user trying to edit a wiki post' do
|
||||
old_post.wiki = true
|
||||
expect(Guardian.new(coding_horror).can_edit?(old_post)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns false to the author of the post' do
|
||||
expect(Guardian.new(old_post.user).can_edit?(old_post)).to be_falsey
|
||||
end
|
||||
context 'post is older than tl2_post_edit_time_limit' do
|
||||
let(:old_post) { build(:post, topic: topic, user: topic.user, created_at: 12.minutes.ago) }
|
||||
|
||||
it 'returns true as a moderator' do
|
||||
expect(Guardian.new(moderator).can_edit?(old_post)).to eq(true)
|
||||
end
|
||||
before do
|
||||
topic.user.update_columns(trust_level: 2)
|
||||
SiteSetting.tl2_post_edit_time_limit = 10
|
||||
end
|
||||
|
||||
it 'returns true as an admin' do
|
||||
expect(Guardian.new(admin).can_edit?(old_post)).to eq(true)
|
||||
end
|
||||
it 'returns false to the author of the post' do
|
||||
expect(Guardian.new(old_post.user).can_edit?(old_post)).to be_falsey
|
||||
end
|
||||
|
||||
it 'returns false for another regular user trying to edit your post' do
|
||||
expect(Guardian.new(coding_horror).can_edit?(old_post)).to be_falsey
|
||||
end
|
||||
it 'returns true as a moderator' do
|
||||
expect(Guardian.new(moderator).can_edit?(old_post)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns true for another regular user trying to edit a wiki post' do
|
||||
old_post.wiki = true
|
||||
expect(Guardian.new(coding_horror).can_edit?(old_post)).to be_truthy
|
||||
it 'returns true as an admin' do
|
||||
expect(Guardian.new(admin).can_edit?(old_post)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false for another regular user trying to edit your post' do
|
||||
expect(Guardian.new(coding_horror).can_edit?(old_post)).to be_falsey
|
||||
end
|
||||
|
||||
it 'returns true for another regular user trying to edit a wiki post' do
|
||||
old_post.wiki = true
|
||||
expect(Guardian.new(coding_horror).can_edit?(old_post)).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -2854,7 +2887,7 @@ describe Guardian do
|
||||
let(:old_post) { build(:post, user: trust_level_2, created_at: 6.minutes.ago) }
|
||||
before do
|
||||
SiteSetting.min_trust_to_allow_self_wiki = 2
|
||||
SiteSetting.post_edit_time_limit = 5
|
||||
SiteSetting.tl2_post_edit_time_limit = 5
|
||||
end
|
||||
|
||||
it 'returns false when user satisfies trust level and owns the post' do
|
||||
|
||||
@ -162,3 +162,7 @@ Fabricator(:post_via_email, from: :post) do
|
||||
incoming_email.user = post.user
|
||||
end
|
||||
end
|
||||
|
||||
Fabricator(:whisper, from: :post) do
|
||||
post_type Post.types[:whisper]
|
||||
end
|
||||
|
||||
@ -114,6 +114,17 @@ describe PostMover do
|
||||
before do
|
||||
TopicUser.update_last_read(user, topic.id, p4.post_number, p4.post_number, 0)
|
||||
TopicLink.extract_from(p2)
|
||||
|
||||
freeze_time Time.now
|
||||
end
|
||||
|
||||
def create_post_timing(post, user, msecs)
|
||||
PostTiming.create!(
|
||||
topic_id: post.topic_id,
|
||||
user_id: user.id,
|
||||
post_number: post.post_number,
|
||||
msecs: msecs
|
||||
)
|
||||
end
|
||||
|
||||
context "post replies" do
|
||||
@ -207,7 +218,7 @@ describe PostMover do
|
||||
p4.reload
|
||||
expect(new_topic.last_post_user_id).to eq(p4.user_id)
|
||||
expect(new_topic.last_posted_at).to eq(p4.created_at)
|
||||
expect(new_topic.bumped_at).to eq(p4.created_at)
|
||||
expect(new_topic.bumped_at).to be_within(1.second).of(Time.now)
|
||||
|
||||
p2.reload
|
||||
expect(p2.sort_order).to eq(1)
|
||||
@ -234,7 +245,7 @@ describe PostMover do
|
||||
notification_level: TopicUser.notification_levels[:watching],
|
||||
notifications_reason_id: TopicUser.notification_reasons[:created_topic]
|
||||
)).to eq(true)
|
||||
expect(TopicUser.exists?(user_id: user, topic_id: new_topic.id)).to eq(false)
|
||||
expect(TopicUser.exists?(user_id: user, topic_id: new_topic.id)).to eq(true)
|
||||
end
|
||||
|
||||
it "moving all posts will close the topic" do
|
||||
@ -334,11 +345,113 @@ describe PostMover do
|
||||
expect(Notification.exists?(user_notification.id)).to eq(false)
|
||||
expect(Notification.exists?(admin_notification.id)).to eq(true)
|
||||
end
|
||||
|
||||
it "moves post timings" do
|
||||
some_user = Fabricate(:user)
|
||||
create_post_timing(p1, some_user, 500)
|
||||
create_post_timing(p2, some_user, 1000)
|
||||
create_post_timing(p3, some_user, 1500)
|
||||
create_post_timing(p4, some_user, 750)
|
||||
|
||||
new_topic = topic.move_posts(user, [p1.id, p4.id], title: "new testing topic name")
|
||||
|
||||
expect(PostTiming.where(topic_id: topic.id, user_id: some_user.id).pluck(:post_number, :msecs))
|
||||
.to contain_exactly([1, 500], [2, 1000], [3, 1500])
|
||||
|
||||
expect(PostTiming.where(topic_id: new_topic.id, user_id: some_user.id).pluck(:post_number, :msecs))
|
||||
.to contain_exactly([1, 500], [2, 750])
|
||||
end
|
||||
|
||||
context "read state and other stats per user" do
|
||||
def create_topic_user(user, opts = {})
|
||||
notification_level = opts.delete(:notification_level) || :regular
|
||||
|
||||
Fabricate(:topic_user, opts.merge(
|
||||
notification_level: TopicUser.notification_levels[notification_level],
|
||||
topic: topic,
|
||||
user: user
|
||||
))
|
||||
end
|
||||
|
||||
fab!(:user1) { Fabricate(:user) }
|
||||
fab!(:user2) { Fabricate(:user) }
|
||||
fab!(:user3) { Fabricate(:user) }
|
||||
fab!(:admin1) { Fabricate(:admin) }
|
||||
fab!(:admin2) { Fabricate(:admin) }
|
||||
|
||||
it "correctly moves topic_user records" do
|
||||
create_topic_user(
|
||||
user1,
|
||||
last_read_post_number: 4,
|
||||
highest_seen_post_number: 4,
|
||||
last_emailed_post_number: 3,
|
||||
notification_level: :tracking
|
||||
)
|
||||
create_topic_user(
|
||||
user3,
|
||||
last_read_post_number: 1,
|
||||
highest_seen_post_number: 2,
|
||||
last_emailed_post_number: 4,
|
||||
notification_level: :watching
|
||||
)
|
||||
|
||||
new_topic = topic.move_posts(user, [p1.id, p2.id], title: "new testing topic name")
|
||||
|
||||
expect(TopicUser.where(topic_id: topic.id).count).to eq(3)
|
||||
expect(TopicUser.find_by(topic: topic, user: user))
|
||||
.to have_attributes(
|
||||
last_read_post_number: 4,
|
||||
highest_seen_post_number: 4,
|
||||
last_emailed_post_number: nil,
|
||||
notification_level: TopicUser.notification_levels[:tracking]
|
||||
)
|
||||
expect(TopicUser.find_by(topic: topic, user: user1))
|
||||
.to have_attributes(
|
||||
last_read_post_number: 4,
|
||||
highest_seen_post_number: 4,
|
||||
last_emailed_post_number: 3,
|
||||
notification_level: TopicUser.notification_levels[:tracking]
|
||||
)
|
||||
expect(TopicUser.find_by(topic: topic, user: user3))
|
||||
.to have_attributes(
|
||||
last_read_post_number: 1,
|
||||
highest_seen_post_number: 2,
|
||||
last_emailed_post_number: 4,
|
||||
notification_level: TopicUser.notification_levels[:watching]
|
||||
)
|
||||
|
||||
expect(TopicUser.where(topic_id: new_topic.id).count).to eq(3)
|
||||
expect(TopicUser.find_by(topic: new_topic, user: user))
|
||||
.to have_attributes(
|
||||
last_read_post_number: 1,
|
||||
highest_seen_post_number: 1,
|
||||
last_emailed_post_number: nil,
|
||||
notification_level: TopicUser.notification_levels[:watching],
|
||||
posted: true
|
||||
)
|
||||
expect(TopicUser.find_by(topic: new_topic, user: user1))
|
||||
.to have_attributes(
|
||||
last_read_post_number: 2,
|
||||
highest_seen_post_number: 2,
|
||||
last_emailed_post_number: 2,
|
||||
notification_level: TopicUser.notification_levels[:tracking],
|
||||
posted: false
|
||||
)
|
||||
expect(TopicUser.find_by(topic: new_topic, user: user3))
|
||||
.to have_attributes(
|
||||
last_read_post_number: 1,
|
||||
highest_seen_post_number: 2,
|
||||
last_emailed_post_number: 2,
|
||||
notification_level: TopicUser.notification_levels[:watching],
|
||||
posted: false
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "to an existing topic" do
|
||||
fab!(:destination_topic) { Fabricate(:topic, user: another_user) }
|
||||
fab!(:destination_op) { Fabricate(:post, topic: destination_topic, user: another_user) }
|
||||
fab!(:destination_op) { Fabricate(:post, topic: destination_topic, user: another_user, created_at: 1.day.ago) }
|
||||
|
||||
it "works correctly" do
|
||||
topic.expects(:add_moderator_post).once
|
||||
@ -355,7 +468,7 @@ describe PostMover do
|
||||
p4.reload
|
||||
expect(moved_to.last_post_user_id).to eq(p4.user_id)
|
||||
expect(moved_to.last_posted_at).to eq(p4.created_at)
|
||||
expect(moved_to.bumped_at).to eq(p4.created_at)
|
||||
expect(moved_to.bumped_at).to be_within(1.second).of(Time.now)
|
||||
|
||||
# Posts should be re-ordered
|
||||
p2.reload
|
||||
@ -440,6 +553,189 @@ describe PostMover do
|
||||
expect(Notification.exists?(user_notification.id)).to eq(false)
|
||||
expect(Notification.exists?(admin_notification.id)).to eq(true)
|
||||
end
|
||||
|
||||
it "moves post timings" do
|
||||
some_user = Fabricate(:user)
|
||||
create_post_timing(p1, some_user, 500)
|
||||
create_post_timing(p2, some_user, 1000)
|
||||
create_post_timing(p3, some_user, 1500)
|
||||
create_post_timing(p4, some_user, 750)
|
||||
|
||||
moved_to = topic.move_posts(user, [p1.id, p4.id], destination_topic_id: destination_topic.id)
|
||||
|
||||
expect(PostTiming.where(topic_id: topic.id, user_id: some_user.id).pluck(:post_number, :msecs))
|
||||
.to contain_exactly([1, 500], [2, 1000], [3, 1500])
|
||||
|
||||
expect(PostTiming.where(topic_id: moved_to.id, user_id: some_user.id).pluck(:post_number, :msecs))
|
||||
.to contain_exactly([2, 500], [3, 750])
|
||||
end
|
||||
|
||||
context "read state and other stats per user" do
|
||||
def create_topic_user(user, topic, opts = {})
|
||||
notification_level = opts.delete(:notification_level) || :regular
|
||||
|
||||
Fabricate(:topic_user, opts.merge(
|
||||
notification_level: TopicUser.notification_levels[notification_level],
|
||||
topic: topic,
|
||||
user: user
|
||||
))
|
||||
end
|
||||
|
||||
fab!(:user1) { Fabricate(:user) }
|
||||
fab!(:user2) { Fabricate(:user) }
|
||||
fab!(:user3) { Fabricate(:user) }
|
||||
fab!(:admin1) { Fabricate(:admin) }
|
||||
fab!(:admin2) { Fabricate(:admin) }
|
||||
|
||||
it "leaves post numbers unchanged when they were lower then the topic's highest post number" do
|
||||
Fabricate(:post, topic: destination_topic)
|
||||
Fabricate(:whisper, topic: destination_topic)
|
||||
|
||||
destination_topic.reload
|
||||
expect(destination_topic.highest_post_number).to eq(2)
|
||||
expect(destination_topic.highest_staff_post_number).to eq(3)
|
||||
|
||||
create_topic_user(
|
||||
user1, topic,
|
||||
last_read_post_number: 3,
|
||||
highest_seen_post_number: 3,
|
||||
last_emailed_post_number: 3
|
||||
)
|
||||
create_topic_user(
|
||||
user1, destination_topic,
|
||||
last_read_post_number: 1,
|
||||
highest_seen_post_number: 2,
|
||||
last_emailed_post_number: 1
|
||||
)
|
||||
|
||||
create_topic_user(
|
||||
user2, topic,
|
||||
last_read_post_number: 3,
|
||||
highest_seen_post_number: 3,
|
||||
last_emailed_post_number: 3
|
||||
)
|
||||
create_topic_user(
|
||||
user2, destination_topic,
|
||||
last_read_post_number: 2,
|
||||
highest_seen_post_number: 1,
|
||||
last_emailed_post_number: 2
|
||||
)
|
||||
|
||||
create_topic_user(
|
||||
admin1, topic,
|
||||
last_read_post_number: 3,
|
||||
highest_seen_post_number: 3,
|
||||
last_emailed_post_number: 3
|
||||
)
|
||||
create_topic_user(
|
||||
admin1, destination_topic,
|
||||
last_read_post_number: 2,
|
||||
highest_seen_post_number: 3,
|
||||
last_emailed_post_number: 1
|
||||
)
|
||||
|
||||
create_topic_user(
|
||||
admin2, topic,
|
||||
last_read_post_number: 3,
|
||||
highest_seen_post_number: 3,
|
||||
last_emailed_post_number: 3
|
||||
)
|
||||
create_topic_user(
|
||||
admin2, destination_topic,
|
||||
last_read_post_number: 3,
|
||||
highest_seen_post_number: 2,
|
||||
last_emailed_post_number: 3
|
||||
)
|
||||
|
||||
moved_to_topic = topic.move_posts(user, [p1.id, p2.id], destination_topic_id: destination_topic.id)
|
||||
|
||||
expect(TopicUser.find_by(topic: moved_to_topic, user: user1))
|
||||
.to have_attributes(
|
||||
last_read_post_number: 1,
|
||||
highest_seen_post_number: 5,
|
||||
last_emailed_post_number: 1
|
||||
)
|
||||
|
||||
expect(TopicUser.find_by(topic: moved_to_topic, user: user2))
|
||||
.to have_attributes(
|
||||
last_read_post_number: 5,
|
||||
highest_seen_post_number: 1,
|
||||
last_emailed_post_number: 5
|
||||
)
|
||||
|
||||
expect(TopicUser.find_by(topic: moved_to_topic, user: admin1))
|
||||
.to have_attributes(
|
||||
last_read_post_number: 2,
|
||||
highest_seen_post_number: 5,
|
||||
last_emailed_post_number: 1
|
||||
)
|
||||
|
||||
expect(TopicUser.find_by(topic: moved_to_topic, user: admin2))
|
||||
.to have_attributes(
|
||||
last_read_post_number: 5,
|
||||
highest_seen_post_number: 2,
|
||||
last_emailed_post_number: 5
|
||||
)
|
||||
end
|
||||
|
||||
it "correctly updates existing topic_user records" do
|
||||
destination_topic.update!(created_at: 1.day.ago)
|
||||
|
||||
original_topic_user1 = create_topic_user(
|
||||
user1, topic,
|
||||
highest_seen_post_number: 5,
|
||||
first_visited_at: 5.hours.ago,
|
||||
last_visited_at: 30.minutes.ago,
|
||||
notification_level: :tracking
|
||||
).reload
|
||||
destination_topic_user1 = create_topic_user(
|
||||
user1, destination_topic,
|
||||
highest_seen_post_number: 5,
|
||||
first_visited_at: 7.hours.ago,
|
||||
last_visited_at: 2.hours.ago,
|
||||
notification_level: :watching
|
||||
).reload
|
||||
|
||||
original_topic_user2 = create_topic_user(
|
||||
user2, topic,
|
||||
highest_seen_post_number: 5,
|
||||
first_visited_at: 3.hours.ago,
|
||||
last_visited_at: 1.hour.ago,
|
||||
notification_level: :watching
|
||||
).reload
|
||||
destination_topic_user2 = create_topic_user(
|
||||
user2, destination_topic,
|
||||
highest_seen_post_number: 5,
|
||||
first_visited_at: 2.hours.ago,
|
||||
last_visited_at: 1.hour.ago,
|
||||
notification_level: :tracking
|
||||
).reload
|
||||
|
||||
new_topic = topic.move_posts(user, [p1.id, p2.id], destination_topic_id: destination_topic.id)
|
||||
|
||||
expect(TopicUser.find_by(topic: new_topic, user: user))
|
||||
.to have_attributes(
|
||||
notification_level: TopicUser.notification_levels[:tracking],
|
||||
posted: true
|
||||
)
|
||||
|
||||
expect(TopicUser.find_by(topic: new_topic, user: user1))
|
||||
.to have_attributes(
|
||||
first_visited_at: destination_topic_user1.first_visited_at,
|
||||
last_visited_at: original_topic_user1.last_visited_at,
|
||||
notification_level: destination_topic_user1.notification_level,
|
||||
posted: false
|
||||
)
|
||||
|
||||
expect(TopicUser.find_by(topic: new_topic, user: user2))
|
||||
.to have_attributes(
|
||||
first_visited_at: original_topic_user2.first_visited_at,
|
||||
last_visited_at: destination_topic_user2.last_visited_at,
|
||||
notification_level: destination_topic_user2.notification_level,
|
||||
posted: false
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "to a message" do
|
||||
@ -465,7 +761,7 @@ describe PostMover do
|
||||
p4.reload
|
||||
expect(new_topic.last_post_user_id).to eq(p4.user_id)
|
||||
expect(new_topic.last_posted_at).to eq(p4.created_at)
|
||||
expect(new_topic.bumped_at).to eq(p4.created_at)
|
||||
expect(new_topic.bumped_at).to be_within(1.second).of(Time.now)
|
||||
|
||||
p2.reload
|
||||
expect(p2.sort_order).to eq(1)
|
||||
|
||||
@ -313,8 +313,26 @@ describe PostsController do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'does not allow to update when edit time limit expired' do
|
||||
it 'does not allow TL0 or TL1 to update when edit time limit expired' do
|
||||
SiteSetting.post_edit_time_limit = 5
|
||||
SiteSetting.tl2_post_edit_time_limit = 30
|
||||
|
||||
post = Fabricate(:post, created_at: 10.minutes.ago, user: user)
|
||||
|
||||
user.update_columns(trust_level: 1)
|
||||
|
||||
put "/posts/#{post.id}.json", params: update_params
|
||||
|
||||
expect(response.status).to eq(422)
|
||||
expect(JSON.parse(response.body)['errors']).to include(I18n.t('too_late_to_edit'))
|
||||
end
|
||||
|
||||
it 'does not allow TL2 to update when edit time limit expired' do
|
||||
SiteSetting.post_edit_time_limit = 12
|
||||
SiteSetting.tl2_post_edit_time_limit = 8
|
||||
|
||||
user.update_columns(trust_level: 2)
|
||||
|
||||
post = Fabricate(:post, created_at: 10.minutes.ago, user: user)
|
||||
|
||||
put "/posts/#{post.id}.json", params: update_params
|
||||
|
||||
@ -236,6 +236,30 @@ describe ReviewablesController do
|
||||
end
|
||||
end
|
||||
|
||||
context "#explain" do
|
||||
context "basics" do
|
||||
fab!(:reviewable) { Fabricate(:reviewable) }
|
||||
|
||||
before do
|
||||
sign_in(Fabricate(:moderator))
|
||||
end
|
||||
|
||||
it "returns the explanation as json" do
|
||||
get "/review/#{reviewable.id}/explain.json"
|
||||
expect(response.code).to eq("200")
|
||||
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json['reviewable_explanation']['id']).to eq(reviewable.id)
|
||||
expect(json['reviewable_explanation']['total_score']).to eq(reviewable.score)
|
||||
end
|
||||
|
||||
it "returns 404 for a missing reviewable" do
|
||||
get "/review/123456789/explain.json"
|
||||
expect(response.code).to eq("404")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#perform" do
|
||||
fab!(:reviewable) { Fabricate(:reviewable) }
|
||||
before do
|
||||
|
||||
@ -207,6 +207,11 @@ describe TagsController do
|
||||
end
|
||||
|
||||
context 'tagging enabled' do
|
||||
def parse_topic_ids
|
||||
JSON.parse(response.body)["topic_list"]["topics"]
|
||||
.map { |topic| topic["id"] }
|
||||
end
|
||||
|
||||
it "can filter by tag" do
|
||||
get "/tags/#{tag.name}/l/latest.json"
|
||||
expect(response.status).to eq(200)
|
||||
@ -221,9 +226,7 @@ describe TagsController do
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
topic_ids = JSON.parse(response.body)["topic_list"]["topics"]
|
||||
.map { |topic| topic["id"] }
|
||||
|
||||
topic_ids = parse_topic_ids
|
||||
expect(topic_ids).to include(all_tag_topic.id)
|
||||
expect(topic_ids).to include(multi_tag_topic.id)
|
||||
expect(topic_ids).to_not include(single_tag_topic.id)
|
||||
@ -238,9 +241,7 @@ describe TagsController do
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
topic_ids = JSON.parse(response.body)["topic_list"]["topics"]
|
||||
.map { |topic| topic["id"] }
|
||||
|
||||
topic_ids = parse_topic_ids
|
||||
expect(topic_ids).to include(all_tag_topic.id)
|
||||
expect(topic_ids).to_not include(multi_tag_topic.id)
|
||||
expect(topic_ids).to_not include(single_tag_topic.id)
|
||||
@ -255,9 +256,7 @@ describe TagsController do
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
topic_ids = JSON.parse(response.body)["topic_list"]["topics"]
|
||||
.map { |topic| topic["id"] }
|
||||
|
||||
topic_ids = parse_topic_ids
|
||||
expect(topic_ids).to_not include(single_tag_topic.id)
|
||||
end
|
||||
|
||||
@ -288,17 +287,53 @@ describe TagsController do
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
topic_ids = JSON.parse(response.body)["topic_list"]["topics"]
|
||||
.map { |topic| topic["id"] }
|
||||
|
||||
topic_ids = parse_topic_ids
|
||||
expect(topic_ids).to include(t.id)
|
||||
end
|
||||
|
||||
it "can filter by bookmarked" do
|
||||
sign_in(Fabricate(:user))
|
||||
get "/tags/#{tag.name}/l/bookmarks.json"
|
||||
context "when logged in" do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it "can filter by bookmarked" do
|
||||
get "/tags/#{tag.name}/l/bookmarks.json"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
context "muted tags" do
|
||||
before do
|
||||
TagUser.create!(
|
||||
user_id: user.id,
|
||||
tag_id: tag.id,
|
||||
notification_level: CategoryUser.notification_levels[:muted]
|
||||
)
|
||||
end
|
||||
|
||||
it "includes topics when filtered by muted tag" do
|
||||
single_tag_topic
|
||||
|
||||
get "/tags/#{tag.name}/l/latest.json"
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
topic_ids = parse_topic_ids
|
||||
expect(topic_ids).to include(single_tag_topic.id)
|
||||
end
|
||||
|
||||
it "includes topics when filtered by category and muted tag" do
|
||||
category = Fabricate(:category)
|
||||
single_tag_topic.update!(category: category)
|
||||
|
||||
get "/tags/c/#{category.slug}/#{tag.name}/l/latest.json"
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
topic_ids = parse_topic_ids
|
||||
expect(topic_ids).to include(single_tag_topic.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -206,6 +206,28 @@ RSpec.describe InlineUploads do
|
||||
MD
|
||||
end
|
||||
|
||||
context "subfolder" do
|
||||
before do
|
||||
global_setting :relative_url_root, "/community"
|
||||
ActionController::Base.config.relative_url_root = "/community"
|
||||
end
|
||||
|
||||
after do
|
||||
ActionController::Base.config.relative_url_root = nil
|
||||
end
|
||||
|
||||
it "should correct subfolder images" do
|
||||
|
||||
md = <<~MD
|
||||
<img src="/community#{upload.url}">
|
||||
MD
|
||||
|
||||
expect(InlineUploads.process(md)).to eq(<<~MD)
|
||||

|
||||
MD
|
||||
end
|
||||
end
|
||||
|
||||
it "should correct raw image URLs to the short url and paths" do
|
||||
md = <<~MD
|
||||
#{Discourse.base_url}#{upload.url}
|
||||
|
||||
@ -100,6 +100,16 @@ describe UserDestroyer do
|
||||
end
|
||||
end
|
||||
|
||||
context "with a reviewable user" do
|
||||
let(:reviewable) { Fabricate(:reviewable, created_by: admin) }
|
||||
|
||||
it 'sets the reviewable user as rejected' do
|
||||
UserDestroyer.new(admin).destroy(reviewable.target)
|
||||
|
||||
expect(reviewable.reload.status).to eq(Reviewable.statuses[:rejected])
|
||||
end
|
||||
end
|
||||
|
||||
context "with a directory item record" do
|
||||
|
||||
it "removes the directory item" do
|
||||
|
||||
Reference in New Issue
Block a user