FEATURE: add period filter in top topics route for tags. (#13415)

And also move all the "top topics by period" routes to query string param.

/top/monthly => /top?period=monthly
/c/:slug/:id/l/top/monthly => /c/:slug/:id/l/top?period=monthly
/tag/:slug/l/top/daily => /tag/:slug/l/top?period=daily (new)
This commit is contained in:
Vinoth Kannan 2021-07-06 15:25:11 +05:30 committed by GitHub
parent 34387c5a38
commit 33eae4cbd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 127 additions and 137 deletions

View File

@ -13,6 +13,7 @@ export const queryParams = {
before: { replace: true, refreshModel: true },
bumped_before: { replace: true, refreshModel: true },
f: { replace: true, refreshModel: true },
period: { replace: true, refreshModel: true },
};
// Basic controller options

View File

@ -37,9 +37,10 @@ export default Controller.extend({
}/l`;
}
url += "/top/" + period;
url += "/top";
const queryParams = this.router.currentRoute.queryParams;
let queryParams = this.router.currentRoute.queryParams;
queryParams.period = period;
if (Object.keys(queryParams).length) {
url =
`${url}?` +

View File

@ -1,12 +1,4 @@
import {
alias,
empty,
equal,
gt,
not,
notEmpty,
readOnly,
} from "@ember/object/computed";
import { alias, empty, equal, gt, not, readOnly } from "@ember/object/computed";
import BulkTopicSelection from "discourse/mixins/bulk-topic-selection";
import DiscoveryController from "discourse/controllers/discovery";
import I18n from "I18n";
@ -140,7 +132,7 @@ const controllerOpts = {
allLoaded: empty("model.more_topics_url"),
latest: endWith("model.filter", "latest"),
new: endWith("model.filter", "new"),
top: notEmpty("period"),
top: endWith("model.filter", "top"),
yearly: equal("period", "yearly"),
quarterly: equal("period", "quarterly"),
monthly: equal("period", "monthly"),

View File

@ -8,6 +8,7 @@ import Topic from "discourse/models/topic";
import { alias } from "@ember/object/computed";
import bootbox from "bootbox";
import { queryParams } from "discourse/controllers/discovery-sortable";
import { endWith } from "discourse/lib/computed";
export default Controller.extend(BulkTopicSelection, FilterModeMixin, {
application: controller(),
@ -27,6 +28,8 @@ export default Controller.extend(BulkTopicSelection, FilterModeMixin, {
max_posts: null,
q: null,
showInfo: false,
top: endWith("list.filter", "top"),
period: alias("list.for_period"),
@discourseComputed(
"canCreateTopic",
@ -131,8 +134,30 @@ export default Controller.extend(BulkTopicSelection, FilterModeMixin, {
this.setProperties({ order, ascending: false });
}
let params = { order, ascending: this.ascending };
if (this.period) {
params.period = this.period;
}
this.transitionToRoute({
queryParams: { order, ascending: this.ascending },
queryParams: params,
});
},
changePeriod(p) {
this.set("period", p);
let params = { period: this.period };
if (!(this.order === "default" && this.ascending === false)) {
params = Object.assign(params, {
order: this.order,
ascending: this.ascending,
});
}
this.transitionToRoute({
queryParams: params,
});
},

View File

@ -36,13 +36,7 @@ export default Mixin.create({
set(key, value) {
this.set("rawFilterMode", value);
const parts = value.split("/");
if (parts.length >= 2 && parts[parts.length - 2] === "top") {
this.set("filterType", "top");
} else {
this.set("filterType", parts.pop());
}
this.set("filterType", value.split("/").pop());
return value;
},

View File

@ -34,7 +34,18 @@ export default {
app[
`Discovery${filterCapitalized}CategoryNoneController`
] = DiscoverySortableController.extend();
if (filter !== "top") {
if (filter === "top") {
app.DiscoveryTopRoute = buildTopicRoute("top", {
actions: {
willTransition() {
User.currentProp("should_be_redirected_to_top", false);
User.currentProp("redirected_to_top.reason", null);
return this._super(...arguments);
},
},
});
} else {
app[`Discovery${filterCapitalized}Route`] = buildTopicRoute(filter);
}
@ -46,38 +57,6 @@ export default {
] = buildCategoryRoute(filter, { no_subcategories: true });
});
app.DiscoveryTopRoute = buildTopicRoute("top", {
actions: {
willTransition() {
User.currentProp("should_be_redirected_to_top", false);
User.currentProp("redirected_to_top.reason", null);
return this._super(...arguments);
},
},
});
site.get("periods").forEach((period) => {
const periodCapitalized = period.capitalize();
app[
`DiscoveryTop${periodCapitalized}Controller`
] = DiscoverySortableController.extend();
app[
`DiscoveryTop${periodCapitalized}CategoryController`
] = DiscoverySortableController.extend();
app[
`DiscoveryTop${periodCapitalized}CategoryNoneController`
] = DiscoverySortableController.extend();
app[`DiscoveryTop${periodCapitalized}Route`] = buildTopicRoute(
"top/" + period
);
app[`DiscoveryTop${periodCapitalized}CategoryRoute`] = buildCategoryRoute(
"top/" + period
);
app[
`DiscoveryTop${periodCapitalized}CategoryNoneRoute`
] = buildCategoryRoute("top/" + period, { no_subcategories: true });
});
app["TagsShowCategoryRoute"] = TagShowRoute.extend();
app["TagsShowCategoryNoneRoute"] = TagShowRoute.extend({
noSubcategories: true,

View File

@ -27,14 +27,7 @@ export default function () {
});
this.route("discovery", { path: "/", resetNamespace: true }, function () {
// top
this.route("top");
this.route("topCategoryNone", {
path: "/c/*category_slug_path_with_id/none/l/top",
});
this.route("topCategory", { path: "/c/*category_slug_path_with_id/l/top" });
// top by periods
// top by periods - legacy route
Site.currentProp("periods").forEach((period) => {
const top = "top" + period.capitalize();
@ -47,7 +40,7 @@ export default function () {
});
});
// filters (e.g. bookmarks, posted, read, unread, latest)
// filters (e.g. bookmarks, posted, read, unread, latest, top)
Site.currentProp("filters").forEach((filter) => {
this.route(filter, { path: "/" + filter });
this.route(filter + "CategoryNone", {

View File

@ -148,8 +148,7 @@ export default (filterArg, params) => {
category = model.category,
canCreateTopic = topics.get("can_create_topic"),
canCreateTopicOnCategory =
canCreateTopic && category.get("permission") === PermissionType.FULL,
filter = this.filter(category);
canCreateTopic && category.get("permission") === PermissionType.FULL;
this.controllerFor("navigation/category").setProperties({
canCreateTopicOnCategory: canCreateTopicOnCategory,
@ -162,7 +161,7 @@ export default (filterArg, params) => {
category,
period:
topics.get("for_period") ||
(filter.indexOf("/") > 0 ? filter.split("/")[1] : ""),
(model.modelParams && model.modelParams.period),
selected: [],
noSubcategories: params && !!params.no_subcategories,
expandAllPinned: true,

View File

@ -130,9 +130,7 @@ export default function (filter, extras) {
const topicOpts = {
model,
category: null,
period:
model.get("for_period") ||
(filter.indexOf("top/") >= 0 ? filter.split("/")[1] : ""),
period: model.get("for_period") || model.get("params.period"),
selected: [],
expandAllPinned: false,
expandGloballyPinned: true,

View File

@ -7,6 +7,7 @@ import OpenComposer from "discourse/mixins/open-composer";
import User from "discourse/models/user";
import { scrollTop } from "discourse/mixins/scroll-top";
import { setTopicList } from "discourse/lib/topic-list-tracker";
import Site from "discourse/models/site";
export default DiscourseRoute.extend(OpenComposer, {
queryParams: {
@ -19,6 +20,7 @@ export default DiscourseRoute.extend(OpenComposer, {
beforeModel(transition) {
const url = transition.intent.url;
let matches;
if (
(url === "/" || url === "/latest" || url === "/categories") &&
transition.targetName.indexOf("discovery.top") === -1 &&
@ -26,7 +28,19 @@ export default DiscourseRoute.extend(OpenComposer, {
) {
User.currentProp("should_be_redirected_to_top", false);
const period = User.currentProp("redirected_to_top.period") || "all";
this.replaceWith(`discovery.top${period.capitalize()}`);
this.replaceWith("discovery.top", {
queryParams: {
period: period,
},
});
} else if (url && (matches = url.match(/top\/(.*)$/))) {
if (Site.currentProp("periods").includes(matches[1])) {
this.replaceWith("discovery.top", {
queryParams: {
period: matches[1],
},
});
}
}
},

View File

@ -45,21 +45,26 @@
<div class="full-width">
<div id="list-area">
{{#unless loading}}
{{#if list.topics}}
{{#discovery-topics-list
model=list
refresh=(action "refresh")
autoAddTopicsToBulkSelect=autoAddTopicsToBulkSelect
bulkSelectEnabled=bulkSelectEnabled
addTopicsToBulkSelect=(action "addTopicsToBulkSelect")
as |discoveryTopicList|
{{#discovery-topics-list
model=list
refresh=(action "refresh")
autoAddTopicsToBulkSelect=autoAddTopicsToBulkSelect
bulkSelectEnabled=bulkSelectEnabled
addTopicsToBulkSelect=(action "addTopicsToBulkSelect")
as |discoveryTopicList|
}}
{{#if top}}
<div class="top-lists">
{{period-chooser period=period action=(action "changePeriod") fullDay=false}}
</div>
{{/if}}
{{bulk-select-button
selected=selected
action=(action "refresh")
category=category
}}
{{bulk-select-button
selected=selected
action=(action "refresh")
category=category
}}
{{#if list.topics}}
{{topic-list
topics=list.topics
canBulkSelect=canBulkSelect
@ -74,8 +79,8 @@
onScroll=discoveryTopicList.saveScrollPosition
scrollOnLoad=true
}}
{{/discovery-topics-list}}
{{/if}}
{{/if}}
{{/discovery-topics-list}}
<footer class="topic-list-bottom">
{{topic-dismiss-buttons position="bottom" selectedTopics=selected

View File

@ -2,13 +2,13 @@ import {
acceptance,
updateCurrentUser,
} from "discourse/tests/helpers/qunit-helpers";
import { currentRouteName, visit } from "@ember/test-helpers";
import { currentURL, visit } from "@ember/test-helpers";
import DiscoveryFixtures from "discourse/tests/fixtures/discovery-fixtures";
import { test } from "qunit";
acceptance("Redirect to Top", function (needs) {
needs.pretender((server, helper) => {
server.get("/top/weekly.json", () => {
server.get("/top.json?period=weekly", () => {
return helper.response(DiscoveryFixtures["/latest.json"]);
});
server.get("/top/monthly.json", () => {
@ -30,11 +30,7 @@ acceptance("Redirect to Top", function (needs) {
});
await visit("/categories");
assert.equal(
currentRouteName(),
"discovery.topWeekly",
"it works for categories"
);
assert.equal(currentURL(), "/top?period=weekly", "it works for categories");
});
test("redirects latest to monthly top", async function (assert) {
@ -47,11 +43,7 @@ acceptance("Redirect to Top", function (needs) {
});
await visit("/latest");
assert.equal(
currentRouteName(),
"discovery.topMonthly",
"it works for latest"
);
assert.equal(currentURL(), "/top?period=monthly", "it works for latest");
});
test("redirects root to All top", async function (assert) {
@ -64,6 +56,6 @@ acceptance("Redirect to Top", function (needs) {
});
await visit("/");
assert.equal(currentRouteName(), "discovery.topAll", "it works for root");
assert.equal(currentURL(), "/top?period=all", "it works for root");
});
});

View File

@ -124,7 +124,7 @@ acceptance("Topic Discovery", function (needs) {
await periodChooser.selectRowByValue("yearly");
assert.ok(
DiscourseURL.routeTo.calledWith("/top/yearly?f=foo&d=bar"),
DiscourseURL.routeTo.calledWith("/top?f=foo&d=bar&period=yearly"),
"it keeps the query params"
);
});

View File

@ -35,6 +35,7 @@ export default {
"latest",
"unread",
"new",
"top",
"starred",
"read",
"posted",

View File

@ -40,6 +40,7 @@ PreloadStore.store("site", {
"latest",
"unread",
"new",
"top",
"starred",
"read",
"posted",

View File

@ -84,6 +84,7 @@ class ListController < ApplicationController
if Discourse.anonymous_filters.include?(filter)
@description = SiteSetting.site_description
@rss = filter
@rss_description = filter
# Note the first is the default and we don't add a title
if (filter.to_s != current_homepage) && use_crawler_layout?
@ -216,7 +217,8 @@ class ListController < ApplicationController
@link = "#{Discourse.base_url}/top"
@atom_link = "#{Discourse.base_url}/top.rss"
@description = I18n.t("rss_description.top")
@topic_list = TopicQuery.new(nil).list_top_for(SiteSetting.top_page_default_timeframe.to_sym)
period = params[:period] || SiteSetting.top_page_default_timeframe.to_sym
@topic_list = TopicQuery.new(nil).list_top_for(period)
render 'list', formats: [:rss]
end
@ -253,7 +255,8 @@ class ListController < ApplicationController
def top(options = nil)
options ||= {}
period = ListController.best_period_for(current_user.try(:previous_visit_at), options[:category])
period = params[:period]
period ||= ListController.best_period_for(current_user.try(:previous_visit_at), options[:category])
public_send("top_#{period}", options)
end
@ -276,7 +279,9 @@ class ListController < ApplicationController
list.for_period = period
list.more_topics_url = construct_url_with(:next, top_options)
list.prev_topics_url = construct_url_with(:prev, top_options)
@rss = "top_#{period}"
@rss = "top"
@params = { period: period }
@rss_description = "top_#{period}"
if use_crawler_layout?
@title = I18n.t("js.filters.top.#{period}.title") + " - #{SiteSetting.title}"
@ -302,8 +307,8 @@ class ListController < ApplicationController
@description = I18n.t("rss_description.top_#{period}")
@title = "#{SiteSetting.title} - #{@description}"
@link = "#{Discourse.base_url}/top/#{period}"
@atom_link = "#{Discourse.base_url}/top/#{period}.rss"
@link = "#{Discourse.base_url}/top?period=#{period}"
@atom_link = "#{Discourse.base_url}/top.rss?period=#{period}"
@topic_list = TopicQuery.new(nil).list_top_for(period)
render 'list', formats: [:rss]
@ -338,6 +343,7 @@ class ListController < ApplicationController
end
route_params[:username] = UrlHelper.encode_component(params[:username]) if params[:username].present?
route_params[:period] = params[:period] if params[:period].present?
route_params
end

View File

@ -89,7 +89,9 @@ class TagsController < ::ApplicationController
@list = nil
if filter == :top
@list = TopicQuery.new(current_user, list_opts).public_send("list_top_for", SiteSetting.top_page_default_timeframe.to_sym)
period = params[:period] || SiteSetting.top_page_default_timeframe.to_sym
@list = TopicQuery.new(current_user, list_opts).public_send("list_top_for", period)
@list.for_period = period
else
@list = TopicQuery.new(current_user, list_opts).public_send("list_#{filter}")
end

View File

@ -8,10 +8,6 @@ class ApiKeyScope < ActiveRecord::Base
def list_actions
actions = %w[list#category_feed]
TopTopic.periods.each do |p|
actions.concat(["list#category_top_#{p}", "list#top_#{p}", "list#top_#{p}_feed"])
end
%i[latest unread new top].each { |f| actions.concat(["list#category_#{f}", "list##{f}"]) }
actions

View File

@ -1,22 +0,0 @@
# frozen_string_literal: true
class TopListSerializer < ApplicationSerializer
attributes :can_create_topic
def can_create_topic
scope.can_create?(Topic)
end
TopTopic.periods.each do |period|
attribute period
define_method(period) do
if resolved = object.public_send(period)
TopicListSerializer.new(resolved, scope: scope).as_json
end
end
end
end

View File

@ -136,7 +136,7 @@
<% if @rss %>
<% content_for :head do %>
<%= auto_discovery_link_tag(:rss, "#{Discourse.base_url}/posts.rss", title: I18n.t("rss_description.posts")) %>
<%= auto_discovery_link_tag(:rss, { action: "#{@rss}_feed" }, title: I18n.t("rss_description.#{@rss}")) %>
<%= auto_discovery_link_tag(:rss, { action: "#{@rss}_feed", params: @params || {} }, title: I18n.t("rss_description.#{@rss_description}")) %>
<% end %>
<% end %>

View File

@ -705,8 +705,8 @@ Discourse::Application.routes.draw do
get "/none" => "list#category_none_latest"
TopTopic.periods.each do |period|
get "/none/l/top/#{period}" => "list#category_none_top_#{period}", as: "category_none_top_#{period}"
get "/l/top/#{period}" => "list#category_top_#{period}", as: "category_top_#{period}"
get "/none/l/top/#{period}", to: redirect("/none/l/top?period=#{period}", status: 301)
get "/l/top/#{period}", to: redirect("/l/top?period=#{period}", status: 301)
end
Discourse.filters.each do |filter|
@ -721,8 +721,9 @@ Discourse::Application.routes.draw do
get "hashtags" => "hashtags#show"
TopTopic.periods.each do |period|
get "top/#{period}.rss" => "list#top_#{period}_feed", format: :rss
get "top/#{period}" => "list#top_#{period}"
get "top/#{period}.rss", to: redirect("top.rss?period=#{period}", status: 301)
get "top/#{period}.json", to: redirect("top.json?period=#{period}", status: 301)
get "top/#{period}", to: redirect("top?period=#{period}", status: 301)
end
Discourse.anonymous_filters.each do |filter|

View File

@ -743,7 +743,7 @@ describe 'topics' do
end
end
path '/top/{flag}.json' do
path '/top.json?period={flag}' do
get 'Get the top topics filtered by a flag' do
tags 'Topics'
consumes 'application/json'

View File

@ -412,7 +412,7 @@ RSpec.describe ListController do
TopTopic.periods.each do |period|
it "renders #{period} top RSS" do
get "/top/#{period}.rss"
get "/top.rss?period=#{period}"
expect(response.status).to eq(200)
expect(response.media_type).to eq('application/rss+xml')
end

View File

@ -683,11 +683,13 @@ describe TagsController do
fab!(:category) { Fabricate(:category) }
fab!(:topic) { Fabricate(:topic, category: category) }
fab!(:tag_topic) { Fabricate(:topic, category: category, tags: [tag]) }
fab!(:tag_topic2) { Fabricate(:topic, category: category, tags: [tag]) }
before do
SiteSetting.top_page_default_timeframe = 'all'
TopTopic.create!(topic: topic, all_score: 1)
TopTopic.create!(topic: tag_topic, all_score: 1)
TopTopic.create!(topic: tag_topic2, daily_score: 1)
end
it "can filter by tag" do
@ -698,6 +700,16 @@ describe TagsController do
expect(topic_ids).to eq([tag_topic.id])
end
it "can filter by tag and period" do
get "/tag/#{tag.name}/l/top.json?period=daily"
expect(response.status).to eq(200)
list = response.parsed_body["topic_list"]
topic_ids = list["topics"].map { |topic| topic["id"] }
expect(topic_ids).to eq([tag_topic2.id])
expect(list["for_period"]).to eq("daily")
end
it "can filter by both category and tag" do
get "/tags/c/#{category.slug}/#{category.id}/#{tag.name}/l/top.json"
expect(response.status).to eq(200)