* FIX: category routes model params should decode their URL parts Ember's route star globbing does not uri decode by default. This is problematic for subcategory globs with encoded URL site settings enabled. Subcategories with encoded URLs will 404 without this decode. I found this https://github.com/tildeio/route-recognizer/pull/91 which explicitly explains that globbing does not decode automatically.
497 lines
13 KiB
JavaScript
497 lines
13 KiB
JavaScript
import discourseComputed from "discourse-common/utils/decorators";
|
|
import { get } from "@ember/object";
|
|
import { ajax } from "discourse/lib/ajax";
|
|
import RestModel from "discourse/models/rest";
|
|
import { on } from "discourse-common/utils/decorators";
|
|
import PermissionType from "discourse/models/permission-type";
|
|
import { NotificationLevels } from "discourse/lib/notification-levels";
|
|
import deprecated from "discourse-common/lib/deprecated";
|
|
import Site from "discourse/models/site";
|
|
|
|
const Category = RestModel.extend({
|
|
permissions: null,
|
|
|
|
@on("init")
|
|
setupGroupsAndPermissions() {
|
|
const availableGroups = this.available_groups;
|
|
if (!availableGroups) {
|
|
return;
|
|
}
|
|
this.set("availableGroups", availableGroups);
|
|
|
|
const groupPermissions = this.group_permissions;
|
|
if (groupPermissions) {
|
|
this.set(
|
|
"permissions",
|
|
groupPermissions.map(elem => {
|
|
availableGroups.removeObject(elem.group_name);
|
|
return {
|
|
group_name: elem.group_name,
|
|
permission: PermissionType.create({ id: elem.permission_type })
|
|
};
|
|
})
|
|
);
|
|
}
|
|
},
|
|
|
|
@on("init")
|
|
setupRequiredTagGroups() {
|
|
if (this.required_tag_group_name) {
|
|
this.set("required_tag_groups", [this.required_tag_group_name]);
|
|
}
|
|
},
|
|
|
|
@discourseComputed
|
|
availablePermissions() {
|
|
return [
|
|
PermissionType.create({ id: PermissionType.FULL }),
|
|
PermissionType.create({ id: PermissionType.CREATE_POST }),
|
|
PermissionType.create({ id: PermissionType.READONLY })
|
|
];
|
|
},
|
|
|
|
@discourseComputed("id")
|
|
searchContext(id) {
|
|
return { type: "category", id, category: this };
|
|
},
|
|
|
|
@discourseComputed("parentCategory.ancestors")
|
|
ancestors(parentAncestors) {
|
|
return [...(parentAncestors || []), this];
|
|
},
|
|
|
|
@discourseComputed("parentCategory.level")
|
|
level(parentLevel) {
|
|
return (parentLevel || -1) + 1;
|
|
},
|
|
|
|
@discourseComputed("subcategories")
|
|
isGrandParent(subcategories) {
|
|
return (
|
|
subcategories &&
|
|
subcategories.some(
|
|
cat => cat.subcategories && cat.subcategories.length > 0
|
|
)
|
|
);
|
|
},
|
|
|
|
@discourseComputed("notification_level")
|
|
isMuted(notificationLevel) {
|
|
return notificationLevel === NotificationLevels.MUTED;
|
|
},
|
|
|
|
@discourseComputed("name")
|
|
url() {
|
|
return Discourse.getURL(`/c/${Category.slugFor(this)}/${this.id}`);
|
|
},
|
|
|
|
@discourseComputed
|
|
fullSlug() {
|
|
return Category.slugFor(this).replace(/\//g, "-");
|
|
},
|
|
|
|
@discourseComputed("name")
|
|
nameLower(name) {
|
|
return name.toLowerCase();
|
|
},
|
|
|
|
@discourseComputed("url")
|
|
unreadUrl(url) {
|
|
return `${url}/l/unread`;
|
|
},
|
|
|
|
@discourseComputed("url")
|
|
newUrl(url) {
|
|
return `${url}/l/new`;
|
|
},
|
|
|
|
@discourseComputed("color", "text_color")
|
|
style(color, textColor) {
|
|
return `background-color: #${color}; color: #${textColor}`;
|
|
},
|
|
|
|
@discourseComputed("topic_count")
|
|
moreTopics(topicCount) {
|
|
return topicCount > (this.num_featured_topics || 2);
|
|
},
|
|
|
|
@discourseComputed("topic_count", "subcategories")
|
|
totalTopicCount(topicCount, subcats) {
|
|
let count = topicCount;
|
|
if (subcats) {
|
|
subcats.forEach(s => {
|
|
count += s.get("topic_count");
|
|
});
|
|
}
|
|
return count;
|
|
},
|
|
|
|
save() {
|
|
const id = this.id;
|
|
const url = id ? `/categories/${id}` : "/categories";
|
|
|
|
return ajax(url, {
|
|
data: {
|
|
name: this.name,
|
|
slug: this.slug,
|
|
color: this.color,
|
|
text_color: this.text_color,
|
|
secure: this.secure,
|
|
permissions: this._permissionsForUpdate(),
|
|
auto_close_hours: this.auto_close_hours,
|
|
auto_close_based_on_last_post: this.get(
|
|
"auto_close_based_on_last_post"
|
|
),
|
|
position: this.position,
|
|
email_in: this.email_in,
|
|
email_in_allow_strangers: this.email_in_allow_strangers,
|
|
mailinglist_mirror: this.mailinglist_mirror,
|
|
parent_category_id: this.parent_category_id,
|
|
uploaded_logo_id: this.get("uploaded_logo.id"),
|
|
uploaded_background_id: this.get("uploaded_background.id"),
|
|
allow_badges: this.allow_badges,
|
|
custom_fields: this.custom_fields,
|
|
topic_template: this.topic_template,
|
|
all_topics_wiki: this.all_topics_wiki,
|
|
allowed_tags: this.allowed_tags,
|
|
allowed_tag_groups: this.allowed_tag_groups,
|
|
allow_global_tags: this.allow_global_tags,
|
|
required_tag_group_name: this.required_tag_groups
|
|
? this.required_tag_groups[0]
|
|
: null,
|
|
min_tags_from_required_group: this.min_tags_from_required_group,
|
|
sort_order: this.sort_order,
|
|
sort_ascending: this.sort_ascending,
|
|
topic_featured_link_allowed: this.topic_featured_link_allowed,
|
|
show_subcategory_list: this.show_subcategory_list,
|
|
num_featured_topics: this.num_featured_topics,
|
|
default_view: this.default_view,
|
|
subcategory_list_style: this.subcategory_list_style,
|
|
default_top_period: this.default_top_period,
|
|
minimum_required_tags: this.minimum_required_tags,
|
|
navigate_to_first_post_after_read: this.get(
|
|
"navigate_to_first_post_after_read"
|
|
),
|
|
search_priority: this.search_priority,
|
|
reviewable_by_group_name: this.reviewable_by_group_name
|
|
},
|
|
type: id ? "PUT" : "POST"
|
|
});
|
|
},
|
|
|
|
_permissionsForUpdate() {
|
|
const permissions = this.permissions;
|
|
let rval = {};
|
|
permissions.forEach(p => (rval[p.group_name] = p.permission.id));
|
|
return rval;
|
|
},
|
|
|
|
destroy() {
|
|
return ajax(`/categories/${this.id || this.slug}`, {
|
|
type: "DELETE"
|
|
});
|
|
},
|
|
|
|
addPermission(permission) {
|
|
this.permissions.addObject(permission);
|
|
this.availableGroups.removeObject(permission.group_name);
|
|
},
|
|
|
|
removePermission(permission) {
|
|
this.permissions.removeObject(permission);
|
|
this.availableGroups.addObject(permission.group_name);
|
|
},
|
|
|
|
@discourseComputed("topics")
|
|
latestTopic(topics) {
|
|
if (topics && topics.length) {
|
|
return topics[0];
|
|
}
|
|
},
|
|
|
|
@discourseComputed("topics")
|
|
featuredTopics(topics) {
|
|
if (topics && topics.length) {
|
|
return topics.slice(0, this.num_featured_topics || 2);
|
|
}
|
|
},
|
|
|
|
@discourseComputed("id", "topicTrackingState.messageCount")
|
|
unreadTopics(id) {
|
|
return this.topicTrackingState.countUnread(id);
|
|
},
|
|
|
|
@discourseComputed("id", "topicTrackingState.messageCount")
|
|
newTopics(id) {
|
|
return this.topicTrackingState.countNew(id);
|
|
},
|
|
|
|
setNotification(notification_level) {
|
|
this.set("notification_level", notification_level);
|
|
const url = `/category/${this.id}/notifications`;
|
|
return ajax(url, { data: { notification_level }, type: "POST" });
|
|
},
|
|
|
|
@discourseComputed("id")
|
|
isUncategorizedCategory(id) {
|
|
return id === Site.currentProp("uncategorized_category_id");
|
|
}
|
|
});
|
|
|
|
var _uncategorized;
|
|
|
|
Category.reopenClass({
|
|
findUncategorized() {
|
|
_uncategorized =
|
|
_uncategorized ||
|
|
Category.list().findBy(
|
|
"id",
|
|
Site.currentProp("uncategorized_category_id")
|
|
);
|
|
return _uncategorized;
|
|
},
|
|
|
|
slugFor(category, separator = "/") {
|
|
if (!category) return "";
|
|
|
|
const parentCategory = get(category, "parentCategory");
|
|
let result = "";
|
|
|
|
if (parentCategory) {
|
|
result = Category.slugFor(parentCategory) + separator;
|
|
}
|
|
|
|
const id = get(category, "id"),
|
|
slug = get(category, "slug");
|
|
|
|
return !slug || slug.trim().length === 0
|
|
? `${result}${id}-category`
|
|
: result + slug;
|
|
},
|
|
|
|
list() {
|
|
return Site.currentProp("categoriesList");
|
|
},
|
|
|
|
listByActivity() {
|
|
return Site.currentProp("sortedCategories");
|
|
},
|
|
|
|
_idMap() {
|
|
return Site.currentProp("categoriesById");
|
|
},
|
|
|
|
findSingleBySlug(slug) {
|
|
if (Discourse.SiteSettings.slug_generation_method !== "encoded") {
|
|
return Category.list().find(c => Category.slugFor(c) === slug);
|
|
} else {
|
|
return Category.list().find(c => Category.slugFor(c) === encodeURI(slug));
|
|
}
|
|
},
|
|
|
|
findById(id) {
|
|
if (!id) {
|
|
return;
|
|
}
|
|
return Category._idMap()[id];
|
|
},
|
|
|
|
findByIds(ids = []) {
|
|
const categories = [];
|
|
ids.forEach(id => {
|
|
const found = Category.findById(id);
|
|
if (found) {
|
|
categories.push(found);
|
|
}
|
|
});
|
|
return categories;
|
|
},
|
|
|
|
findBySlugAndParent(slug, parentCategory) {
|
|
if (Discourse.SiteSettings.slug_generation_method === "encoded") {
|
|
slug = encodeURI(slug);
|
|
}
|
|
return Category.list().find(category => {
|
|
return (
|
|
category.slug === slug &&
|
|
(category.parentCategory || null) === parentCategory
|
|
);
|
|
});
|
|
},
|
|
|
|
findBySlugPath(slugPath) {
|
|
let category = null;
|
|
|
|
for (const slug of slugPath) {
|
|
category = this.findBySlugAndParent(slug, category);
|
|
|
|
if (!category) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return category;
|
|
},
|
|
|
|
findBySlugPathWithID(slugPathWithID) {
|
|
let parts = slugPathWithID.split("/");
|
|
// slugs found by star/glob pathing in emeber do not automatically url decode - ensure that these are decoded
|
|
if (Discourse.SiteSettings.slug_generation_method === "encoded") {
|
|
parts = parts.map(urlPart => decodeURI(urlPart));
|
|
}
|
|
let category = null;
|
|
|
|
if (parts.length > 0 && parts[parts.length - 1].match(/^\d+$/)) {
|
|
const id = parseInt(parts.pop(), 10);
|
|
|
|
category = Category.findById(id);
|
|
} else {
|
|
category = Category.findBySlugPath(parts);
|
|
|
|
if (
|
|
!category &&
|
|
parts.length > 0 &&
|
|
parts[parts.length - 1].match(/^\d+-/)
|
|
) {
|
|
const id = parseInt(parts.pop(), 10);
|
|
|
|
category = Category.findById(id);
|
|
}
|
|
}
|
|
|
|
return category;
|
|
},
|
|
|
|
findBySlug(slug, parentSlug) {
|
|
const categories = Category.list();
|
|
let category;
|
|
|
|
if (parentSlug) {
|
|
const parentCategory = Category.findSingleBySlug(parentSlug);
|
|
if (parentCategory) {
|
|
if (slug === "none") {
|
|
return parentCategory;
|
|
}
|
|
|
|
category = categories.find(item => {
|
|
return (
|
|
item &&
|
|
item.get("parentCategory") === parentCategory &&
|
|
((Discourse.SiteSettings.slug_generation_method !== "encoded" &&
|
|
Category.slugFor(item) === parentSlug + "/" + slug) ||
|
|
(Discourse.SiteSettings.slug_generation_method === "encoded" &&
|
|
Category.slugFor(item) ===
|
|
encodeURI(parentSlug) + "/" + encodeURI(slug)))
|
|
);
|
|
});
|
|
}
|
|
} else {
|
|
category = Category.findSingleBySlug(slug);
|
|
|
|
// If we have a parent category, we need to enforce it
|
|
if (category && category.get("parentCategory")) return;
|
|
}
|
|
|
|
// In case the slug didn't work, try to find it by id instead.
|
|
if (!category) {
|
|
category = categories.findBy("id", parseInt(slug, 10));
|
|
}
|
|
|
|
return category;
|
|
},
|
|
|
|
reloadById(id) {
|
|
return ajax(`/c/${id}/show.json`);
|
|
},
|
|
|
|
reloadBySlug(slug, parentSlug) {
|
|
return parentSlug
|
|
? ajax(`/c/${parentSlug}/${slug}/find_by_slug.json`)
|
|
: ajax(`/c/${slug}/find_by_slug.json`);
|
|
},
|
|
|
|
search(term, opts) {
|
|
var limit = 5;
|
|
|
|
if (opts) {
|
|
if (opts.limit === 0) {
|
|
return [];
|
|
} else if (opts.limit) {
|
|
limit = opts.limit;
|
|
}
|
|
}
|
|
|
|
const emptyTerm = term === "";
|
|
let slugTerm = term;
|
|
|
|
if (!emptyTerm) {
|
|
term = term.toLowerCase();
|
|
slugTerm = term;
|
|
term = term.replace(/-/g, " ");
|
|
}
|
|
|
|
const categories = Category.listByActivity();
|
|
const length = categories.length;
|
|
var i;
|
|
var data = [];
|
|
|
|
const done = () => {
|
|
return data.length === limit;
|
|
};
|
|
|
|
for (i = 0; i < length && !done(); i++) {
|
|
const category = categories[i];
|
|
if (
|
|
(emptyTerm && !category.get("parent_category_id")) ||
|
|
(!emptyTerm &&
|
|
(category
|
|
.get("name")
|
|
.toLowerCase()
|
|
.indexOf(term) === 0 ||
|
|
category
|
|
.get("slug")
|
|
.toLowerCase()
|
|
.indexOf(slugTerm) === 0))
|
|
) {
|
|
data.push(category);
|
|
}
|
|
}
|
|
|
|
if (!done()) {
|
|
for (i = 0; i < length && !done(); i++) {
|
|
const category = categories[i];
|
|
|
|
if (
|
|
!emptyTerm &&
|
|
(category
|
|
.get("name")
|
|
.toLowerCase()
|
|
.indexOf(term) > 0 ||
|
|
category
|
|
.get("slug")
|
|
.toLowerCase()
|
|
.indexOf(slugTerm) > 0)
|
|
) {
|
|
if (data.indexOf(category) === -1) data.push(category);
|
|
}
|
|
}
|
|
}
|
|
|
|
return _.sortBy(data, category => {
|
|
return category.get("read_restricted");
|
|
});
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Discourse, "Category", {
|
|
get() {
|
|
deprecated(
|
|
"Import the Category class instead of using Discourse.Category",
|
|
{ since: "2.4.0", dropFrom: "2.5.0" }
|
|
);
|
|
return Category;
|
|
}
|
|
});
|
|
|
|
export default Category;
|