Compare commits
3 Commits
main
...
generic-im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6d0281171 | ||
|
|
1a18c8af97 | ||
|
|
760d6633b1 |
16
.github/workflows/tests.yml
vendored
16
.github/workflows/tests.yml
vendored
@ -159,22 +159,6 @@ jobs:
|
|||||||
path: tmp/turbo_rspec_runtime.log
|
path: tmp/turbo_rspec_runtime.log
|
||||||
key: rspec-runtime-backend-core
|
key: rspec-runtime-backend-core
|
||||||
|
|
||||||
- name: Run Zeitwerk check
|
|
||||||
if: matrix.build_type == 'backend'
|
|
||||||
env:
|
|
||||||
LOAD_PLUGINS: ${{ (matrix.target == 'plugins') && '1' || '0' }}
|
|
||||||
run: |
|
|
||||||
if ! bin/rails zeitwerk:check --trace; then
|
|
||||||
echo
|
|
||||||
echo "---------------------------------------------"
|
|
||||||
echo
|
|
||||||
echo "::error::'bin/rails zeitwerk:check' failed - the app will fail to boot with 'eager_load=true' (e.g. in production)."
|
|
||||||
echo "To reproduce locally, run 'bin/rails zeitwerk:check'."
|
|
||||||
echo "Alternatively, you can run your local server/tests with the 'DISCOURSE_ZEITWERK_EAGER_LOAD=1' environment variable."
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Core RSpec
|
- name: Core RSpec
|
||||||
if: matrix.build_type == 'backend' && matrix.target == 'core'
|
if: matrix.build_type == 'backend' && matrix.target == 'core'
|
||||||
run: bin/turbo_rspec --verbose
|
run: bin/turbo_rspec --verbose
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import RestAdapter from "discourse/adapters/rest";
|
import RESTAdapter from "discourse/adapters/rest";
|
||||||
|
|
||||||
export default class ApiKey extends RestAdapter {
|
export default RESTAdapter.extend({
|
||||||
jsonMode = true;
|
jsonMode: true,
|
||||||
|
|
||||||
basePath() {
|
basePath() {
|
||||||
return "/admin/api/";
|
return "/admin/api/";
|
||||||
}
|
},
|
||||||
|
|
||||||
apiNameFor() {
|
apiNameFor() {
|
||||||
return "key";
|
return "key";
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import RestAdapter from "discourse/adapters/rest";
|
import RestAdapter from "discourse/adapters/rest";
|
||||||
|
|
||||||
export default function buildPluginAdapter(pluginName) {
|
export default function buildPluginAdapter(pluginName) {
|
||||||
return class extends RestAdapter {
|
return RestAdapter.extend({
|
||||||
pathFor(store, type, findArgs) {
|
pathFor(store, type, findArgs) {
|
||||||
return (
|
return (
|
||||||
"/admin/plugins/" + pluginName + super.pathFor(store, type, findArgs)
|
"/admin/plugins/" + pluginName + this._super(store, type, findArgs)
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import RestAdapter from "discourse/adapters/rest";
|
import RestAdapter from "discourse/adapters/rest";
|
||||||
|
|
||||||
export default class CustomizationBase extends RestAdapter {
|
export default RestAdapter.extend({
|
||||||
basePath() {
|
basePath() {
|
||||||
return "/admin/customize/";
|
return "/admin/customize/";
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import RestAdapter from "discourse/adapters/rest";
|
import RestAdapter from "discourse/adapters/rest";
|
||||||
|
|
||||||
export default class EmailStyle extends RestAdapter {
|
export default RestAdapter.extend({
|
||||||
pathFor() {
|
pathFor() {
|
||||||
return "/admin/customize/email_style";
|
return "/admin/customize/email_style";
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import RestAdapter from "discourse/adapters/rest";
|
import RestAdapter from "discourse/adapters/rest";
|
||||||
|
|
||||||
export default class Embedding extends RestAdapter {
|
export default RestAdapter.extend({
|
||||||
pathFor() {
|
pathFor() {
|
||||||
return "/admin/customize/embedding";
|
return "/admin/customize/embedding";
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import RestAdapter from "discourse/adapters/rest";
|
import RestAdapter from "discourse/adapters/rest";
|
||||||
|
|
||||||
export default class StaffActionLog extends RestAdapter {
|
export default RestAdapter.extend({
|
||||||
basePath() {
|
basePath() {
|
||||||
return "/admin/logs/";
|
return "/admin/logs/";
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import RestAdapter from "discourse/adapters/rest";
|
import RestAdapter from "discourse/adapters/rest";
|
||||||
|
|
||||||
export default class TagGroup extends RestAdapter {
|
export default RestAdapter.extend({
|
||||||
jsonMode = true;
|
jsonMode: true,
|
||||||
}
|
});
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import RestAdapter from "discourse/adapters/rest";
|
import RestAdapter from "discourse/adapters/rest";
|
||||||
|
|
||||||
export default class Theme extends RestAdapter {
|
export default RestAdapter.extend({
|
||||||
jsonMode = true;
|
|
||||||
basePath() {
|
basePath() {
|
||||||
return "/admin/";
|
return "/admin/";
|
||||||
}
|
},
|
||||||
|
|
||||||
afterFindAll(results) {
|
afterFindAll(results) {
|
||||||
let map = {};
|
let map = {};
|
||||||
@ -21,5 +20,7 @@ export default class Theme extends RestAdapter {
|
|||||||
theme.set("parentThemes", mappedParents);
|
theme.set("parentThemes", mappedParents);
|
||||||
});
|
});
|
||||||
return results;
|
return results;
|
||||||
}
|
},
|
||||||
}
|
|
||||||
|
jsonMode: true,
|
||||||
|
});
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import RestAdapter from "discourse/adapters/rest";
|
import RESTAdapter from "discourse/adapters/rest";
|
||||||
|
|
||||||
export default class WebHookEvent extends RestAdapter {
|
export default RESTAdapter.extend({
|
||||||
basePath() {
|
basePath() {
|
||||||
return "/admin/api/";
|
return "/admin/api/";
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import RestAdapter from "discourse/adapters/rest";
|
import RESTAdapter from "discourse/adapters/rest";
|
||||||
|
|
||||||
export default class WebHook extends RestAdapter {
|
export default RESTAdapter.extend({
|
||||||
basePath() {
|
basePath() {
|
||||||
return "/admin/api/";
|
return "/admin/api/";
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import Helper from "@ember/component/helper";
|
|||||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
|
|
||||||
export default class DispositionIcon extends Helper {
|
export default Helper.extend({
|
||||||
compute([disposition]) {
|
compute([disposition]) {
|
||||||
if (!disposition) {
|
if (!disposition) {
|
||||||
return null;
|
return null;
|
||||||
@ -24,5 +24,5 @@ export default class DispositionIcon extends Helper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return htmlSafe(iconHTML(icon, { title }));
|
return htmlSafe(iconHTML(icon, { title }));
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -7,17 +7,19 @@ const GENERAL_ATTRIBUTES = [
|
|||||||
"release_notes_link",
|
"release_notes_link",
|
||||||
];
|
];
|
||||||
|
|
||||||
export default class AdminDashboard extends EmberObject {
|
const AdminDashboard = EmberObject.extend({});
|
||||||
static fetch() {
|
|
||||||
|
AdminDashboard.reopenClass({
|
||||||
|
fetch() {
|
||||||
return ajax("/admin/dashboard.json").then((json) => {
|
return ajax("/admin/dashboard.json").then((json) => {
|
||||||
const model = AdminDashboard.create();
|
const model = AdminDashboard.create();
|
||||||
model.set("version_check", json.version_check);
|
model.set("version_check", json.version_check);
|
||||||
|
|
||||||
return model;
|
return model;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
static fetchGeneral() {
|
fetchGeneral() {
|
||||||
return ajax("/admin/dashboard/general.json").then((json) => {
|
return ajax("/admin/dashboard/general.json").then((json) => {
|
||||||
const model = AdminDashboard.create();
|
const model = AdminDashboard.create();
|
||||||
|
|
||||||
@ -32,13 +34,15 @@ export default class AdminDashboard extends EmberObject {
|
|||||||
|
|
||||||
return model;
|
return model;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
static fetchProblems() {
|
fetchProblems() {
|
||||||
return ajax("/admin/dashboard/problems.json").then((json) => {
|
return ajax("/admin/dashboard/problems.json").then((json) => {
|
||||||
const model = AdminDashboard.create(json);
|
const model = AdminDashboard.create(json);
|
||||||
model.set("loaded", true);
|
model.set("loaded", true);
|
||||||
return model;
|
return model;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default AdminDashboard;
|
||||||
|
|||||||
@ -10,30 +10,14 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
|
|||||||
import { propertyNotEqual } from "discourse/lib/computed";
|
import { propertyNotEqual } from "discourse/lib/computed";
|
||||||
import { userPath } from "discourse/lib/url";
|
import { userPath } from "discourse/lib/url";
|
||||||
|
|
||||||
export default class AdminUser extends User {
|
const wrapAdmin = (user) => (user ? AdminUser.create(user) : null);
|
||||||
static find(user_id) {
|
|
||||||
return ajax(`/admin/users/${user_id}.json`).then((result) => {
|
|
||||||
result.loadedDetails = true;
|
|
||||||
return AdminUser.create(result);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static findAll(query, userFilter) {
|
const AdminUser = User.extend({
|
||||||
return ajax(`/admin/users/list/${query}.json`, {
|
adminUserView: true,
|
||||||
data: userFilter,
|
customGroups: filter("groups", (g) => !g.automatic && Group.create(g)),
|
||||||
}).then((users) => users.map((u) => AdminUser.create(u)));
|
automaticGroups: filter("groups", (g) => g.automatic && Group.create(g)),
|
||||||
}
|
|
||||||
|
|
||||||
adminUserView = true;
|
canViewProfile: or("active", "staged"),
|
||||||
|
|
||||||
@filter("groups", (g) => !g.automatic && Group.create(g)) customGroups;
|
|
||||||
@filter("groups", (g) => g.automatic && Group.create(g)) automaticGroups;
|
|
||||||
@or("active", "staged") canViewProfile;
|
|
||||||
@gt("bounce_score", 0) canResetBounceScore;
|
|
||||||
@propertyNotEqual("originalTrustLevel", "trust_level") dirty;
|
|
||||||
@lt("trust_level", 4) canLockTrustLevel;
|
|
||||||
@not("staff") canSuspend;
|
|
||||||
@not("staff") canSilence;
|
|
||||||
|
|
||||||
@discourseComputed("bounce_score", "reset_bounce_score_after")
|
@discourseComputed("bounce_score", "reset_bounce_score_after")
|
||||||
bounceScore(bounce_score, reset_bounce_score_after) {
|
bounceScore(bounce_score, reset_bounce_score_after) {
|
||||||
@ -44,7 +28,7 @@ export default class AdminUser extends User {
|
|||||||
} else {
|
} else {
|
||||||
return bounce_score;
|
return bounce_score;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("bounce_score")
|
@discourseComputed("bounce_score")
|
||||||
bounceScoreExplanation(bounce_score) {
|
bounceScoreExplanation(bounce_score) {
|
||||||
@ -55,12 +39,14 @@ export default class AdminUser extends User {
|
|||||||
} else {
|
} else {
|
||||||
return I18n.t("admin.user.bounce_score_explanation.threshold_reached");
|
return I18n.t("admin.user.bounce_score_explanation.threshold_reached");
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed
|
@discourseComputed
|
||||||
bounceLink() {
|
bounceLink() {
|
||||||
return getURL("/admin/email/bounced");
|
return getURL("/admin/email/bounced");
|
||||||
}
|
},
|
||||||
|
|
||||||
|
canResetBounceScore: gt("bounce_score", 0),
|
||||||
|
|
||||||
resetBounceScore() {
|
resetBounceScore() {
|
||||||
return ajax(`/admin/users/${this.id}/reset_bounce_score`, {
|
return ajax(`/admin/users/${this.id}/reset_bounce_score`, {
|
||||||
@ -71,14 +57,14 @@ export default class AdminUser extends User {
|
|||||||
reset_bounce_score_after: null,
|
reset_bounce_score_after: null,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
groupAdded(added) {
|
groupAdded(added) {
|
||||||
return ajax(`/admin/users/${this.id}/groups`, {
|
return ajax(`/admin/users/${this.id}/groups`, {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
data: { group_id: added.id },
|
data: { group_id: added.id },
|
||||||
}).then(() => this.groups.pushObject(added));
|
}).then(() => this.groups.pushObject(added));
|
||||||
}
|
},
|
||||||
|
|
||||||
groupRemoved(groupId) {
|
groupRemoved(groupId) {
|
||||||
return ajax(`/admin/users/${this.id}/groups/${groupId}`, {
|
return ajax(`/admin/users/${this.id}/groups/${groupId}`, {
|
||||||
@ -89,13 +75,13 @@ export default class AdminUser extends User {
|
|||||||
this.set("primary_group_id", null);
|
this.set("primary_group_id", null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
deleteAllPosts() {
|
deleteAllPosts() {
|
||||||
return ajax(`/admin/users/${this.get("id")}/delete_posts_batch`, {
|
return ajax(`/admin/users/${this.get("id")}/delete_posts_batch`, {
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
revokeAdmin() {
|
revokeAdmin() {
|
||||||
return ajax(`/admin/users/${this.id}/revoke_admin`, {
|
return ajax(`/admin/users/${this.id}/revoke_admin`, {
|
||||||
@ -111,7 +97,7 @@ export default class AdminUser extends User {
|
|||||||
can_delete_all_posts: resp.can_delete_all_posts,
|
can_delete_all_posts: resp.can_delete_all_posts,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
grantAdmin(data) {
|
grantAdmin(data) {
|
||||||
return ajax(`/admin/users/${this.id}/grant_admin`, {
|
return ajax(`/admin/users/${this.id}/grant_admin`, {
|
||||||
@ -128,7 +114,7 @@ export default class AdminUser extends User {
|
|||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
revokeModeration() {
|
revokeModeration() {
|
||||||
return ajax(`/admin/users/${this.id}/revoke_moderation`, {
|
return ajax(`/admin/users/${this.id}/revoke_moderation`, {
|
||||||
@ -144,7 +130,7 @@ export default class AdminUser extends User {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(popupAjaxError);
|
.catch(popupAjaxError);
|
||||||
}
|
},
|
||||||
|
|
||||||
grantModeration() {
|
grantModeration() {
|
||||||
return ajax(`/admin/users/${this.id}/grant_moderation`, {
|
return ajax(`/admin/users/${this.id}/grant_moderation`, {
|
||||||
@ -160,7 +146,7 @@ export default class AdminUser extends User {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(popupAjaxError);
|
.catch(popupAjaxError);
|
||||||
}
|
},
|
||||||
|
|
||||||
disableSecondFactor() {
|
disableSecondFactor() {
|
||||||
return ajax(`/admin/users/${this.id}/disable_second_factor`, {
|
return ajax(`/admin/users/${this.id}/disable_second_factor`, {
|
||||||
@ -170,7 +156,7 @@ export default class AdminUser extends User {
|
|||||||
this.set("second_factor_enabled", false);
|
this.set("second_factor_enabled", false);
|
||||||
})
|
})
|
||||||
.catch(popupAjaxError);
|
.catch(popupAjaxError);
|
||||||
}
|
},
|
||||||
|
|
||||||
approve(approvedBy) {
|
approve(approvedBy) {
|
||||||
return ajax(`/admin/users/${this.id}/approve`, {
|
return ajax(`/admin/users/${this.id}/approve`, {
|
||||||
@ -182,76 +168,83 @@ export default class AdminUser extends User {
|
|||||||
approved_by: approvedBy,
|
approved_by: approvedBy,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
setOriginalTrustLevel() {
|
setOriginalTrustLevel() {
|
||||||
this.set("originalTrustLevel", this.trust_level);
|
this.set("originalTrustLevel", this.trust_level);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
dirty: propertyNotEqual("originalTrustLevel", "trust_level"),
|
||||||
|
|
||||||
saveTrustLevel() {
|
saveTrustLevel() {
|
||||||
return ajax(`/admin/users/${this.id}/trust_level`, {
|
return ajax(`/admin/users/${this.id}/trust_level`, {
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
data: { level: this.trust_level },
|
data: { level: this.trust_level },
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
restoreTrustLevel() {
|
restoreTrustLevel() {
|
||||||
this.set("trust_level", this.originalTrustLevel);
|
this.set("trust_level", this.originalTrustLevel);
|
||||||
}
|
},
|
||||||
|
|
||||||
lockTrustLevel(locked) {
|
lockTrustLevel(locked) {
|
||||||
return ajax(`/admin/users/${this.id}/trust_level_lock`, {
|
return ajax(`/admin/users/${this.id}/trust_level_lock`, {
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
data: { locked: !!locked },
|
data: { locked: !!locked },
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
|
canLockTrustLevel: lt("trust_level", 4),
|
||||||
|
|
||||||
|
canSuspend: not("staff"),
|
||||||
|
canSilence: not("staff"),
|
||||||
|
|
||||||
@discourseComputed("suspended_till", "suspended_at")
|
@discourseComputed("suspended_till", "suspended_at")
|
||||||
suspendDuration(suspendedTill, suspendedAt) {
|
suspendDuration(suspendedTill, suspendedAt) {
|
||||||
suspendedAt = moment(suspendedAt);
|
suspendedAt = moment(suspendedAt);
|
||||||
suspendedTill = moment(suspendedTill);
|
suspendedTill = moment(suspendedTill);
|
||||||
return suspendedAt.format("L") + " - " + suspendedTill.format("L");
|
return suspendedAt.format("L") + " - " + suspendedTill.format("L");
|
||||||
}
|
},
|
||||||
|
|
||||||
suspend(data) {
|
suspend(data) {
|
||||||
return ajax(`/admin/users/${this.id}/suspend`, {
|
return ajax(`/admin/users/${this.id}/suspend`, {
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
data,
|
data,
|
||||||
}).then((result) => this.setProperties(result.suspension));
|
}).then((result) => this.setProperties(result.suspension));
|
||||||
}
|
},
|
||||||
|
|
||||||
unsuspend() {
|
unsuspend() {
|
||||||
return ajax(`/admin/users/${this.id}/unsuspend`, {
|
return ajax(`/admin/users/${this.id}/unsuspend`, {
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
}).then((result) => this.setProperties(result.suspension));
|
}).then((result) => this.setProperties(result.suspension));
|
||||||
}
|
},
|
||||||
|
|
||||||
logOut() {
|
logOut() {
|
||||||
return ajax("/admin/users/" + this.id + "/log_out", {
|
return ajax("/admin/users/" + this.id + "/log_out", {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
data: { username_or_email: this.username },
|
data: { username_or_email: this.username },
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
impersonate() {
|
impersonate() {
|
||||||
return ajax("/admin/impersonate", {
|
return ajax("/admin/impersonate", {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
data: { username_or_email: this.username },
|
data: { username_or_email: this.username },
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
activate() {
|
activate() {
|
||||||
return ajax(`/admin/users/${this.id}/activate`, {
|
return ajax(`/admin/users/${this.id}/activate`, {
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
deactivate() {
|
deactivate() {
|
||||||
return ajax(`/admin/users/${this.id}/deactivate`, {
|
return ajax(`/admin/users/${this.id}/deactivate`, {
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
data: { context: document.location.pathname },
|
data: { context: document.location.pathname },
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
unsilence() {
|
unsilence() {
|
||||||
this.set("silencingUser", true);
|
this.set("silencingUser", true);
|
||||||
@ -261,7 +254,7 @@ export default class AdminUser extends User {
|
|||||||
})
|
})
|
||||||
.then((result) => this.setProperties(result.unsilence))
|
.then((result) => this.setProperties(result.unsilence))
|
||||||
.finally(() => this.set("silencingUser", false));
|
.finally(() => this.set("silencingUser", false));
|
||||||
}
|
},
|
||||||
|
|
||||||
silence(data) {
|
silence(data) {
|
||||||
this.set("silencingUser", true);
|
this.set("silencingUser", true);
|
||||||
@ -272,20 +265,20 @@ export default class AdminUser extends User {
|
|||||||
})
|
})
|
||||||
.then((result) => this.setProperties(result.silence))
|
.then((result) => this.setProperties(result.silence))
|
||||||
.finally(() => this.set("silencingUser", false));
|
.finally(() => this.set("silencingUser", false));
|
||||||
}
|
},
|
||||||
|
|
||||||
sendActivationEmail() {
|
sendActivationEmail() {
|
||||||
return ajax(userPath("action/send_activation_email"), {
|
return ajax(userPath("action/send_activation_email"), {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
data: { username: this.username },
|
data: { username: this.username },
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
anonymize() {
|
anonymize() {
|
||||||
return ajax(`/admin/users/${this.id}/anonymize.json`, {
|
return ajax(`/admin/users/${this.id}/anonymize.json`, {
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
destroy(formData) {
|
destroy(formData) {
|
||||||
return ajax(`/admin/users/${this.id}.json`, {
|
return ajax(`/admin/users/${this.id}.json`, {
|
||||||
@ -302,14 +295,14 @@ export default class AdminUser extends User {
|
|||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.find(this.id).then((u) => this.setProperties(u));
|
this.find(this.id).then((u) => this.setProperties(u));
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
merge(formData) {
|
merge(formData) {
|
||||||
return ajax(`/admin/users/${this.id}/merge.json`, {
|
return ajax(`/admin/users/${this.id}/merge.json`, {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
data: formData,
|
data: formData,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
loadDetails() {
|
loadDetails() {
|
||||||
if (this.loadedDetails) {
|
if (this.loadedDetails) {
|
||||||
@ -320,29 +313,23 @@ export default class AdminUser extends User {
|
|||||||
const userProperties = Object.assign(result, { loadedDetails: true });
|
const userProperties = Object.assign(result, { loadedDetails: true });
|
||||||
this.setProperties(userProperties);
|
this.setProperties(userProperties);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("tl3_requirements")
|
@discourseComputed("tl3_requirements")
|
||||||
tl3Requirements(requirements) {
|
tl3Requirements(requirements) {
|
||||||
if (requirements) {
|
if (requirements) {
|
||||||
return this.store.createRecord("tl3Requirements", requirements);
|
return this.store.createRecord("tl3Requirements", requirements);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("suspended_by")
|
@discourseComputed("suspended_by")
|
||||||
suspendedBy(user) {
|
suspendedBy: wrapAdmin,
|
||||||
return user ? AdminUser.create(user) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@discourseComputed("silenced_by")
|
@discourseComputed("silenced_by")
|
||||||
silencedBy(user) {
|
silencedBy: wrapAdmin,
|
||||||
return user ? AdminUser.create(user) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@discourseComputed("approved_by")
|
@discourseComputed("approved_by")
|
||||||
approvedBy(user) {
|
approvedBy: wrapAdmin,
|
||||||
return user ? AdminUser.create(user) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteSSORecord() {
|
deleteSSORecord() {
|
||||||
return ajax(`/admin/users/${this.id}/sso_record.json`, {
|
return ajax(`/admin/users/${this.id}/sso_record.json`, {
|
||||||
@ -352,5 +339,22 @@ export default class AdminUser extends User {
|
|||||||
this.set("single_sign_on_record", null);
|
this.set("single_sign_on_record", null);
|
||||||
})
|
})
|
||||||
.catch(popupAjaxError);
|
.catch(popupAjaxError);
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
|
AdminUser.reopenClass({
|
||||||
|
find(user_id) {
|
||||||
|
return ajax(`/admin/users/${user_id}.json`).then((result) => {
|
||||||
|
result.loadedDetails = true;
|
||||||
|
return AdminUser.create(result);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
findAll(query, userFilter) {
|
||||||
|
return ajax(`/admin/users/list/${query}.json`, {
|
||||||
|
data: userFilter,
|
||||||
|
}).then((users) => users.map((u) => AdminUser.create(u)));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AdminUser;
|
||||||
|
|||||||
@ -1,26 +1,24 @@
|
|||||||
import { computed } from "@ember/object";
|
|
||||||
import AdminUser from "admin/models/admin-user";
|
import AdminUser from "admin/models/admin-user";
|
||||||
import RestModel from "discourse/models/rest";
|
import RestModel from "discourse/models/rest";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { computed } from "@ember/object";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import { fmt } from "discourse/lib/computed";
|
import { fmt } from "discourse/lib/computed";
|
||||||
|
|
||||||
export default class ApiKey extends RestModel {
|
const ApiKey = RestModel.extend({
|
||||||
@fmt("truncated_key", "%@...") truncatedKey;
|
user: computed("_user", {
|
||||||
|
get() {
|
||||||
@computed("_user")
|
return this._user;
|
||||||
get user() {
|
},
|
||||||
return this._user;
|
set(key, value) {
|
||||||
}
|
if (value && !(value instanceof AdminUser)) {
|
||||||
|
this.set("_user", AdminUser.create(value));
|
||||||
set user(value) {
|
} else {
|
||||||
if (value && !(value instanceof AdminUser)) {
|
this.set("_user", value);
|
||||||
this.set("_user", AdminUser.create(value));
|
}
|
||||||
} else {
|
return this._user;
|
||||||
this.set("_user", value);
|
},
|
||||||
}
|
}),
|
||||||
return this._user;
|
|
||||||
}
|
|
||||||
|
|
||||||
@discourseComputed("description")
|
@discourseComputed("description")
|
||||||
shortDescription(description) {
|
shortDescription(description) {
|
||||||
@ -28,28 +26,32 @@ export default class ApiKey extends RestModel {
|
|||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
return `${description.substring(0, 40)}...`;
|
return `${description.substring(0, 40)}...`;
|
||||||
}
|
},
|
||||||
|
|
||||||
|
truncatedKey: fmt("truncated_key", "%@..."),
|
||||||
|
|
||||||
revoke() {
|
revoke() {
|
||||||
return ajax(`${this.basePath}/revoke`, {
|
return ajax(`${this.basePath}/revoke`, {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
}).then((result) => this.setProperties(result.api_key));
|
}).then((result) => this.setProperties(result.api_key));
|
||||||
}
|
},
|
||||||
|
|
||||||
undoRevoke() {
|
undoRevoke() {
|
||||||
return ajax(`${this.basePath}/undo-revoke`, {
|
return ajax(`${this.basePath}/undo-revoke`, {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
}).then((result) => this.setProperties(result.api_key));
|
}).then((result) => this.setProperties(result.api_key));
|
||||||
}
|
},
|
||||||
|
|
||||||
createProperties() {
|
createProperties() {
|
||||||
return this.getProperties("description", "username", "scopes");
|
return this.getProperties("description", "username", "scopes");
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed()
|
@discourseComputed()
|
||||||
basePath() {
|
basePath() {
|
||||||
return this.store
|
return this.store
|
||||||
.adapterFor("api-key")
|
.adapterFor("api-key")
|
||||||
.pathFor(this.store, "api-key", this.id);
|
.pathFor(this.store, "api-key", this.id);
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default ApiKey;
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { not } from "@ember/object/computed";
|
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
|
import { not } from "@ember/object/computed";
|
||||||
|
|
||||||
export default class BackupStatus extends EmberObject {
|
export default EmberObject.extend({
|
||||||
@not("restoreEnabled") restoreDisabled;
|
restoreDisabled: not("restoreEnabled"),
|
||||||
|
|
||||||
@discourseComputed("allowRestore", "isOperationRunning")
|
@discourseComputed("allowRestore", "isOperationRunning")
|
||||||
restoreEnabled(allowRestore, isOperationRunning) {
|
restoreEnabled(allowRestore, isOperationRunning) {
|
||||||
return allowRestore && !isOperationRunning;
|
return allowRestore && !isOperationRunning;
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -2,12 +2,25 @@ import EmberObject from "@ember/object";
|
|||||||
import MessageBus from "message-bus-client";
|
import MessageBus from "message-bus-client";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
export default class Backup extends EmberObject {
|
const Backup = EmberObject.extend({
|
||||||
static find() {
|
destroy() {
|
||||||
return ajax("/admin/backups.json");
|
return ajax("/admin/backups/" + this.filename, { type: "DELETE" });
|
||||||
}
|
},
|
||||||
|
|
||||||
static start(withUploads) {
|
restore() {
|
||||||
|
return ajax("/admin/backups/" + this.filename + "/restore", {
|
||||||
|
type: "POST",
|
||||||
|
data: { client_id: MessageBus.clientId },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Backup.reopenClass({
|
||||||
|
find() {
|
||||||
|
return ajax("/admin/backups.json");
|
||||||
|
},
|
||||||
|
|
||||||
|
start(withUploads) {
|
||||||
if (withUploads === undefined) {
|
if (withUploads === undefined) {
|
||||||
withUploads = true;
|
withUploads = true;
|
||||||
}
|
}
|
||||||
@ -18,28 +31,19 @@ export default class Backup extends EmberObject {
|
|||||||
client_id: MessageBus.clientId,
|
client_id: MessageBus.clientId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
static cancel() {
|
cancel() {
|
||||||
return ajax("/admin/backups/cancel.json", {
|
return ajax("/admin/backups/cancel.json", {
|
||||||
type: "DELETE",
|
type: "DELETE",
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
static rollback() {
|
rollback() {
|
||||||
return ajax("/admin/backups/rollback.json", {
|
return ajax("/admin/backups/rollback.json", {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
|
||||||
destroy() {
|
export default Backup;
|
||||||
return ajax("/admin/backups/" + this.filename, { type: "DELETE" });
|
|
||||||
}
|
|
||||||
|
|
||||||
restore() {
|
|
||||||
return ajax("/admin/backups/" + this.filename + "/restore", {
|
|
||||||
type: "POST",
|
|
||||||
data: { client_id: MessageBus.clientId },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed, {
|
||||||
import { observes, on } from "@ember-decorators/object";
|
observes,
|
||||||
|
on,
|
||||||
|
} from "discourse-common/utils/decorators";
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { propertyNotEqual } from "discourse/lib/computed";
|
import { propertyNotEqual } from "discourse/lib/computed";
|
||||||
|
|
||||||
export default class ColorSchemeColor extends EmberObject {
|
const ColorSchemeColor = EmberObject.extend({
|
||||||
// Whether the current value is different than Discourse's default color scheme.
|
|
||||||
@propertyNotEqual("hex", "default_hex") overridden;
|
|
||||||
@on("init")
|
@on("init")
|
||||||
startTrackingChanges() {
|
startTrackingChanges() {
|
||||||
this.set("originals", { hex: this.hex || "FFFFFF" });
|
this.set("originals", { hex: this.hex || "FFFFFF" });
|
||||||
|
|
||||||
// force changed property to be recalculated
|
// force changed property to be recalculated
|
||||||
this.notifyPropertyChange("hex");
|
this.notifyPropertyChange("hex");
|
||||||
}
|
},
|
||||||
|
|
||||||
// Whether value has changed since it was last saved.
|
// Whether value has changed since it was last saved.
|
||||||
@discourseComputed("hex")
|
@discourseComputed("hex")
|
||||||
@ -26,23 +26,26 @@ export default class ColorSchemeColor extends EmberObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
},
|
||||||
|
|
||||||
|
// Whether the current value is different than Discourse's default color scheme.
|
||||||
|
overridden: propertyNotEqual("hex", "default_hex"),
|
||||||
|
|
||||||
// Whether the saved value is different than Discourse's default color scheme.
|
// Whether the saved value is different than Discourse's default color scheme.
|
||||||
@discourseComputed("default_hex", "hex")
|
@discourseComputed("default_hex", "hex")
|
||||||
savedIsOverriden(defaultHex) {
|
savedIsOverriden(defaultHex) {
|
||||||
return this.originals.hex !== defaultHex;
|
return this.originals.hex !== defaultHex;
|
||||||
}
|
},
|
||||||
|
|
||||||
revert() {
|
revert() {
|
||||||
this.set("hex", this.default_hex);
|
this.set("hex", this.default_hex);
|
||||||
}
|
},
|
||||||
|
|
||||||
undo() {
|
undo() {
|
||||||
if (this.originals) {
|
if (this.originals) {
|
||||||
this.set("hex", this.originals.hex);
|
this.set("hex", this.originals.hex);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("name")
|
@discourseComputed("name")
|
||||||
translatedName(name) {
|
translatedName(name) {
|
||||||
@ -51,7 +54,7 @@ export default class ColorSchemeColor extends EmberObject {
|
|||||||
} else {
|
} else {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("name")
|
@discourseComputed("name")
|
||||||
description(name) {
|
description(name) {
|
||||||
@ -60,7 +63,7 @@ export default class ColorSchemeColor extends EmberObject {
|
|||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
brightness returns a number between 0 (darkest) to 255 (brightest).
|
brightness returns a number between 0 (darkest) to 255 (brightest).
|
||||||
@ -87,17 +90,19 @@ export default class ColorSchemeColor extends EmberObject {
|
|||||||
1000
|
1000
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
@observes("hex")
|
@observes("hex")
|
||||||
hexValueChanged() {
|
hexValueChanged() {
|
||||||
if (this.hex) {
|
if (this.hex) {
|
||||||
this.set("hex", this.hex.toString().replace(/[^0-9a-fA-F]/g, ""));
|
this.set("hex", this.hex.toString().replace(/[^0-9a-fA-F]/g, ""));
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("hex")
|
@discourseComputed("hex")
|
||||||
valid(hex) {
|
valid(hex) {
|
||||||
return hex.match(/^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/) !== null;
|
return hex.match(/^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/) !== null;
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default ColorSchemeColor;
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { not } from "@ember/object/computed";
|
|
||||||
import { A } from "@ember/array";
|
import { A } from "@ember/array";
|
||||||
import ArrayProxy from "@ember/array/proxy";
|
import ArrayProxy from "@ember/array/proxy";
|
||||||
import ColorSchemeColor from "admin/models/color-scheme-color";
|
import ColorSchemeColor from "admin/models/color-scheme-color";
|
||||||
@ -6,56 +5,26 @@ import EmberObject from "@ember/object";
|
|||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
|
import { not } from "@ember/object/computed";
|
||||||
|
|
||||||
class ColorSchemes extends ArrayProxy {}
|
const ColorScheme = EmberObject.extend({
|
||||||
|
|
||||||
export default class ColorScheme extends EmberObject {
|
|
||||||
static findAll() {
|
|
||||||
const colorSchemes = ColorSchemes.create({ content: [], loading: true });
|
|
||||||
return ajax("/admin/color_schemes").then((all) => {
|
|
||||||
all.forEach((colorScheme) => {
|
|
||||||
colorSchemes.pushObject(
|
|
||||||
ColorScheme.create({
|
|
||||||
id: colorScheme.id,
|
|
||||||
name: colorScheme.name,
|
|
||||||
is_base: colorScheme.is_base,
|
|
||||||
theme_id: colorScheme.theme_id,
|
|
||||||
theme_name: colorScheme.theme_name,
|
|
||||||
base_scheme_id: colorScheme.base_scheme_id,
|
|
||||||
user_selectable: colorScheme.user_selectable,
|
|
||||||
colors: colorScheme.colors.map((c) => {
|
|
||||||
return ColorSchemeColor.create({
|
|
||||||
name: c.name,
|
|
||||||
hex: c.hex,
|
|
||||||
default_hex: c.default_hex,
|
|
||||||
is_advanced: c.is_advanced,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return colorSchemes;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@not("id") newRecord;
|
|
||||||
init() {
|
init() {
|
||||||
super.init(...arguments);
|
this._super(...arguments);
|
||||||
|
|
||||||
this.startTrackingChanges();
|
this.startTrackingChanges();
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed
|
@discourseComputed
|
||||||
description() {
|
description() {
|
||||||
return "" + this.name;
|
return "" + this.name;
|
||||||
}
|
},
|
||||||
|
|
||||||
startTrackingChanges() {
|
startTrackingChanges() {
|
||||||
this.set("originals", {
|
this.set("originals", {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
user_selectable: this.user_selectable,
|
user_selectable: this.user_selectable,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
schemeJson() {
|
schemeJson() {
|
||||||
const buffer = [];
|
const buffer = [];
|
||||||
@ -64,7 +33,7 @@ export default class ColorScheme extends EmberObject {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return [`"${this.name}": {`, buffer.join(",\n"), "}"].join("\n");
|
return [`"${this.name}": {`, buffer.join(",\n"), "}"].join("\n");
|
||||||
}
|
},
|
||||||
|
|
||||||
copy() {
|
copy() {
|
||||||
const newScheme = ColorScheme.create({
|
const newScheme = ColorScheme.create({
|
||||||
@ -78,7 +47,7 @@ export default class ColorScheme extends EmberObject {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
return newScheme;
|
return newScheme;
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed(
|
@discourseComputed(
|
||||||
"name",
|
"name",
|
||||||
@ -101,7 +70,7 @@ export default class ColorScheme extends EmberObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("changed")
|
@discourseComputed("changed")
|
||||||
disableSave(changed) {
|
disableSave(changed) {
|
||||||
@ -110,7 +79,9 @@ export default class ColorScheme extends EmberObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return !changed || this.saving || this.colors.any((c) => !c.get("valid"));
|
return !changed || this.saving || this.colors.any((c) => !c.get("valid"));
|
||||||
}
|
},
|
||||||
|
|
||||||
|
newRecord: not("id"),
|
||||||
|
|
||||||
save(opts) {
|
save(opts) {
|
||||||
if (this.is_base || this.disableSave) {
|
if (this.is_base || this.disableSave) {
|
||||||
@ -153,7 +124,7 @@ export default class ColorScheme extends EmberObject {
|
|||||||
this.setProperties({ savingStatus: I18n.t("saved"), saving: false });
|
this.setProperties({ savingStatus: I18n.t("saved"), saving: false });
|
||||||
this.notifyPropertyChange("description");
|
this.notifyPropertyChange("description");
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
updateUserSelectable(value) {
|
updateUserSelectable(value) {
|
||||||
if (!this.id) {
|
if (!this.id) {
|
||||||
@ -166,11 +137,45 @@ export default class ColorScheme extends EmberObject {
|
|||||||
dataType: "json",
|
dataType: "json",
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if (this.id) {
|
if (this.id) {
|
||||||
return ajax(`/admin/color_schemes/${this.id}`, { type: "DELETE" });
|
return ajax(`/admin/color_schemes/${this.id}`, { type: "DELETE" });
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
|
const ColorSchemes = ArrayProxy.extend({});
|
||||||
|
|
||||||
|
ColorScheme.reopenClass({
|
||||||
|
findAll() {
|
||||||
|
const colorSchemes = ColorSchemes.create({ content: [], loading: true });
|
||||||
|
return ajax("/admin/color_schemes").then((all) => {
|
||||||
|
all.forEach((colorScheme) => {
|
||||||
|
colorSchemes.pushObject(
|
||||||
|
ColorScheme.create({
|
||||||
|
id: colorScheme.id,
|
||||||
|
name: colorScheme.name,
|
||||||
|
is_base: colorScheme.is_base,
|
||||||
|
theme_id: colorScheme.theme_id,
|
||||||
|
theme_name: colorScheme.theme_name,
|
||||||
|
base_scheme_id: colorScheme.base_scheme_id,
|
||||||
|
user_selectable: colorScheme.user_selectable,
|
||||||
|
colors: colorScheme.colors.map((c) => {
|
||||||
|
return ColorSchemeColor.create({
|
||||||
|
name: c.name,
|
||||||
|
hex: c.hex,
|
||||||
|
default_hex: c.default_hex,
|
||||||
|
is_advanced: c.is_advanced,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return colorSchemes;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ColorScheme;
|
||||||
|
|||||||
@ -3,8 +3,10 @@ import EmberObject from "@ember/object";
|
|||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import getURL from "discourse-common/lib/get-url";
|
import getURL from "discourse-common/lib/get-url";
|
||||||
|
|
||||||
export default class EmailLog extends EmberObject {
|
const EmailLog = EmberObject.extend({});
|
||||||
static create(attrs) {
|
|
||||||
|
EmailLog.reopenClass({
|
||||||
|
create(attrs) {
|
||||||
attrs = attrs || {};
|
attrs = attrs || {};
|
||||||
|
|
||||||
if (attrs.user) {
|
if (attrs.user) {
|
||||||
@ -15,10 +17,10 @@ export default class EmailLog extends EmberObject {
|
|||||||
attrs.post_url = getURL(attrs.post_url);
|
attrs.post_url = getURL(attrs.post_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.create(attrs);
|
return this._super(attrs);
|
||||||
}
|
},
|
||||||
|
|
||||||
static findAll(filter, offset) {
|
findAll(filter, offset) {
|
||||||
filter = filter || {};
|
filter = filter || {};
|
||||||
offset = offset || 0;
|
offset = offset || 0;
|
||||||
|
|
||||||
@ -28,5 +30,7 @@ export default class EmailLog extends EmberObject {
|
|||||||
return ajax(`/admin/email/${status}.json?offset=${offset}`, {
|
return ajax(`/admin/email/${status}.json?offset=${offset}`, {
|
||||||
data: filter,
|
data: filter,
|
||||||
}).then((logs) => logs.map((log) => EmailLog.create(log)));
|
}).then((logs) => logs.map((log) => EmailLog.create(log)));
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default EmailLog;
|
||||||
|
|||||||
@ -1,21 +1,25 @@
|
|||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
export default class EmailPreview extends EmberObject {
|
const EmailPreview = EmberObject.extend({});
|
||||||
static findDigest(username, lastSeenAt) {
|
|
||||||
return ajax("/admin/email/preview-digest.json", {
|
|
||||||
data: { last_seen_at: lastSeenAt || oneWeekAgo(), username },
|
|
||||||
}).then((result) => EmailPreview.create(result));
|
|
||||||
}
|
|
||||||
|
|
||||||
static sendDigest(username, lastSeenAt, email) {
|
|
||||||
return ajax("/admin/email/send-digest.json", {
|
|
||||||
type: "POST",
|
|
||||||
data: { last_seen_at: lastSeenAt || oneWeekAgo(), username, email },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function oneWeekAgo() {
|
export function oneWeekAgo() {
|
||||||
return moment().locale("en").subtract(7, "days").format("YYYY-MM-DD");
|
return moment().locale("en").subtract(7, "days").format("YYYY-MM-DD");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EmailPreview.reopenClass({
|
||||||
|
findDigest(username, lastSeenAt) {
|
||||||
|
return ajax("/admin/email/preview-digest.json", {
|
||||||
|
data: { last_seen_at: lastSeenAt || oneWeekAgo(), username },
|
||||||
|
}).then((result) => EmailPreview.create(result));
|
||||||
|
},
|
||||||
|
|
||||||
|
sendDigest(username, lastSeenAt, email) {
|
||||||
|
return ajax("/admin/email/send-digest.json", {
|
||||||
|
type: "POST",
|
||||||
|
data: { last_seen_at: lastSeenAt || oneWeekAgo(), username, email },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default EmailPreview;
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
export default class EmailSettings extends EmberObject {
|
const EmailSettings = EmberObject.extend({});
|
||||||
static find() {
|
|
||||||
|
EmailSettings.reopenClass({
|
||||||
|
find() {
|
||||||
return ajax("/admin/email.json").then(function (settings) {
|
return ajax("/admin/email.json").then(function (settings) {
|
||||||
return EmailSettings.create(settings);
|
return EmailSettings.create(settings);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default EmailSettings;
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import RestModel from "discourse/models/rest";
|
import RestModel from "discourse/models/rest";
|
||||||
|
|
||||||
export default class EmailStyle extends RestModel {
|
export default RestModel.extend({
|
||||||
changed = false;
|
changed: false,
|
||||||
|
|
||||||
setField(fieldName, value) {
|
setField(fieldName, value) {
|
||||||
this.set(`${fieldName}`, value);
|
this.set(`${fieldName}`, value);
|
||||||
this.set("changed", true);
|
this.set("changed", true);
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -2,12 +2,12 @@ import RestModel from "discourse/models/rest";
|
|||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { getProperties } from "@ember/object";
|
import { getProperties } from "@ember/object";
|
||||||
|
|
||||||
export default class EmailTemplate extends RestModel {
|
export default RestModel.extend({
|
||||||
revert() {
|
revert() {
|
||||||
return ajax(`/admin/customize/email_templates/${this.id}`, {
|
return ajax(`/admin/customize/email_templates/${this.id}`, {
|
||||||
type: "DELETE",
|
type: "DELETE",
|
||||||
}).then((result) =>
|
}).then((result) =>
|
||||||
getProperties(result.email_template, "subject", "body", "can_revert")
|
getProperties(result.email_template, "subject", "body", "can_revert")
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import I18n from "I18n";
|
|||||||
import RestModel from "discourse/models/rest";
|
import RestModel from "discourse/models/rest";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
export default class FlagType extends RestModel {
|
export default RestModel.extend({
|
||||||
@discourseComputed("id")
|
@discourseComputed("id")
|
||||||
name(id) {
|
name(id) {
|
||||||
return I18n.t(`admin.flags.summary.action_type_${id}`, { count: 1 });
|
return I18n.t(`admin.flags.summary.action_type_${id}`, { count: 1 });
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -1,51 +1,53 @@
|
|||||||
import RestModel from "discourse/models/rest";
|
import RestModel from "discourse/models/rest";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
export default class FormTemplate extends RestModel {
|
export default class FormTemplate extends RestModel {}
|
||||||
static createTemplate(data) {
|
|
||||||
|
FormTemplate.reopenClass({
|
||||||
|
createTemplate(data) {
|
||||||
return ajax("/admin/customize/form-templates.json", {
|
return ajax("/admin/customize/form-templates.json", {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
static updateTemplate(id, data) {
|
updateTemplate(id, data) {
|
||||||
return ajax(`/admin/customize/form-templates/${id}.json`, {
|
return ajax(`/admin/customize/form-templates/${id}.json`, {
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
static createOrUpdateTemplate(data) {
|
createOrUpdateTemplate(data) {
|
||||||
if (data.id) {
|
if (data.id) {
|
||||||
return this.updateTemplate(data.id, data);
|
return this.updateTemplate(data.id, data);
|
||||||
} else {
|
} else {
|
||||||
return this.createTemplate(data);
|
return this.createTemplate(data);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
static deleteTemplate(id) {
|
deleteTemplate(id) {
|
||||||
return ajax(`/admin/customize/form-templates/${id}.json`, {
|
return ajax(`/admin/customize/form-templates/${id}.json`, {
|
||||||
type: "DELETE",
|
type: "DELETE",
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
static findAll() {
|
findAll() {
|
||||||
return ajax(`/admin/customize/form-templates.json`).then((model) => {
|
return ajax(`/admin/customize/form-templates.json`).then((model) => {
|
||||||
return model.form_templates.sort((a, b) => a.id - b.id);
|
return model.form_templates.sort((a, b) => a.id - b.id);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
static findById(id) {
|
findById(id) {
|
||||||
return ajax(`/admin/customize/form-templates/${id}.json`).then((model) => {
|
return ajax(`/admin/customize/form-templates/${id}.json`).then((model) => {
|
||||||
return model.form_template;
|
return model.form_template;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
static validateTemplate(data) {
|
validateTemplate(data) {
|
||||||
return ajax(`/admin/customize/form-templates/preview.json`, {
|
return ajax(`/admin/customize/form-templates/preview.json`, {
|
||||||
type: "GET",
|
type: "GET",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -2,8 +2,10 @@ import AdminUser from "admin/models/admin-user";
|
|||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
export default class IncomingEmail extends EmberObject {
|
const IncomingEmail = EmberObject.extend({});
|
||||||
static create(attrs) {
|
|
||||||
|
IncomingEmail.reopenClass({
|
||||||
|
create(attrs) {
|
||||||
attrs = attrs || {};
|
attrs = attrs || {};
|
||||||
|
|
||||||
if (attrs.user) {
|
if (attrs.user) {
|
||||||
@ -11,17 +13,17 @@ export default class IncomingEmail extends EmberObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this._super(attrs);
|
return this._super(attrs);
|
||||||
}
|
},
|
||||||
|
|
||||||
static find(id) {
|
find(id) {
|
||||||
return ajax(`/admin/email/incoming/${id}.json`);
|
return ajax(`/admin/email/incoming/${id}.json`);
|
||||||
}
|
},
|
||||||
|
|
||||||
static findByBounced(id) {
|
findByBounced(id) {
|
||||||
return ajax(`/admin/email/incoming_from_bounced/${id}.json`);
|
return ajax(`/admin/email/incoming_from_bounced/${id}.json`);
|
||||||
}
|
},
|
||||||
|
|
||||||
static findAll(filter, offset) {
|
findAll(filter, offset) {
|
||||||
filter = filter || {};
|
filter = filter || {};
|
||||||
offset = offset || 0;
|
offset = offset || 0;
|
||||||
|
|
||||||
@ -33,9 +35,11 @@ export default class IncomingEmail extends EmberObject {
|
|||||||
}).then((incomings) =>
|
}).then((incomings) =>
|
||||||
incomings.map((incoming) => IncomingEmail.create(incoming))
|
incomings.map((incoming) => IncomingEmail.create(incoming))
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
static loadRawEmail(id) {
|
loadRawEmail(id) {
|
||||||
return ajax(`/admin/email/incoming/${id}/raw.json`);
|
return ajax(`/admin/email/incoming/${id}/raw.json`);
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default IncomingEmail;
|
||||||
|
|||||||
@ -4,15 +4,7 @@ import EmberObject from "@ember/object";
|
|||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
export default class Permalink extends EmberObject {
|
const Permalink = EmberObject.extend({
|
||||||
static findAll(filter) {
|
|
||||||
return ajax("/admin/permalinks.json", { data: { filter } }).then(function (
|
|
||||||
permalinks
|
|
||||||
) {
|
|
||||||
return permalinks.map((p) => Permalink.create(p));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
return ajax("/admin/permalinks.json", {
|
return ajax("/admin/permalinks.json", {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
@ -22,21 +14,33 @@ export default class Permalink extends EmberObject {
|
|||||||
permalink_type_value: this.permalink_type_value,
|
permalink_type_value: this.permalink_type_value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("category_id")
|
@discourseComputed("category_id")
|
||||||
category(category_id) {
|
category(category_id) {
|
||||||
return Category.findById(category_id);
|
return Category.findById(category_id);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("external_url")
|
@discourseComputed("external_url")
|
||||||
linkIsExternal(external_url) {
|
linkIsExternal(external_url) {
|
||||||
return !DiscourseURL.isInternal(external_url);
|
return !DiscourseURL.isInternal(external_url);
|
||||||
}
|
},
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
return ajax("/admin/permalinks/" + this.id + ".json", {
|
return ajax("/admin/permalinks/" + this.id + ".json", {
|
||||||
type: "DELETE",
|
type: "DELETE",
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
|
Permalink.reopenClass({
|
||||||
|
findAll(filter) {
|
||||||
|
return ajax("/admin/permalinks.json", { data: { filter } }).then(function (
|
||||||
|
permalinks
|
||||||
|
) {
|
||||||
|
return permalinks.map((p) => Permalink.create(p));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Permalink;
|
||||||
|
|||||||
@ -19,188 +19,12 @@ import round from "discourse/lib/round";
|
|||||||
// and you want to ensure cache is reset
|
// and you want to ensure cache is reset
|
||||||
export const SCHEMA_VERSION = 4;
|
export const SCHEMA_VERSION = 4;
|
||||||
|
|
||||||
export default class Report extends EmberObject {
|
const Report = EmberObject.extend({
|
||||||
static groupingForDatapoints(count) {
|
average: false,
|
||||||
if (count < DAILY_LIMIT_DAYS) {
|
percent: false,
|
||||||
return "daily";
|
higher_is_better: true,
|
||||||
}
|
description_link: null,
|
||||||
|
description: null,
|
||||||
if (count >= DAILY_LIMIT_DAYS && count < WEEKLY_LIMIT_DAYS) {
|
|
||||||
return "weekly";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count >= WEEKLY_LIMIT_DAYS) {
|
|
||||||
return "monthly";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static unitForDatapoints(count) {
|
|
||||||
if (count >= DAILY_LIMIT_DAYS && count < WEEKLY_LIMIT_DAYS) {
|
|
||||||
return "week";
|
|
||||||
} else if (count >= WEEKLY_LIMIT_DAYS) {
|
|
||||||
return "month";
|
|
||||||
} else {
|
|
||||||
return "day";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static unitForGrouping(grouping) {
|
|
||||||
switch (grouping) {
|
|
||||||
case "monthly":
|
|
||||||
return "month";
|
|
||||||
case "weekly":
|
|
||||||
return "week";
|
|
||||||
default:
|
|
||||||
return "day";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static collapse(model, data, grouping) {
|
|
||||||
grouping = grouping || Report.groupingForDatapoints(data.length);
|
|
||||||
|
|
||||||
if (grouping === "daily") {
|
|
||||||
return data;
|
|
||||||
} else if (grouping === "weekly" || grouping === "monthly") {
|
|
||||||
const isoKind = grouping === "weekly" ? "isoWeek" : "month";
|
|
||||||
const kind = grouping === "weekly" ? "week" : "month";
|
|
||||||
const startMoment = moment(model.start_date, "YYYY-MM-DD");
|
|
||||||
|
|
||||||
let currentIndex = 0;
|
|
||||||
let currentStart = startMoment.clone().startOf(isoKind);
|
|
||||||
let currentEnd = startMoment.clone().endOf(isoKind);
|
|
||||||
const transformedData = [
|
|
||||||
{
|
|
||||||
x: currentStart.format("YYYY-MM-DD"),
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let appliedAverage = false;
|
|
||||||
data.forEach((d) => {
|
|
||||||
const date = moment(d.x, "YYYY-MM-DD");
|
|
||||||
|
|
||||||
if (
|
|
||||||
!date.isSame(currentStart) &&
|
|
||||||
!date.isBetween(currentStart, currentEnd)
|
|
||||||
) {
|
|
||||||
if (model.average) {
|
|
||||||
transformedData[currentIndex].y = applyAverage(
|
|
||||||
transformedData[currentIndex].y,
|
|
||||||
currentStart,
|
|
||||||
currentEnd
|
|
||||||
);
|
|
||||||
|
|
||||||
appliedAverage = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentIndex += 1;
|
|
||||||
currentStart = currentStart.add(1, kind).startOf(isoKind);
|
|
||||||
currentEnd = currentEnd.add(1, kind).endOf(isoKind);
|
|
||||||
} else {
|
|
||||||
appliedAverage = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transformedData[currentIndex]) {
|
|
||||||
transformedData[currentIndex].y += d.y;
|
|
||||||
} else {
|
|
||||||
transformedData[currentIndex] = {
|
|
||||||
x: d.x,
|
|
||||||
y: d.y,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (model.average && !appliedAverage) {
|
|
||||||
transformedData[currentIndex].y = applyAverage(
|
|
||||||
transformedData[currentIndex].y,
|
|
||||||
currentStart,
|
|
||||||
moment(model.end_date).subtract(1, "day") // remove 1 day as model end date is at 00:00 of next day
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return transformedData;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure we return something if grouping is unknown
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
static fillMissingDates(report, options = {}) {
|
|
||||||
const dataField = options.dataField || "data";
|
|
||||||
const filledField = options.filledField || "data";
|
|
||||||
const startDate = options.startDate || "start_date";
|
|
||||||
const endDate = options.endDate || "end_date";
|
|
||||||
|
|
||||||
if (Array.isArray(report[dataField])) {
|
|
||||||
const startDateFormatted = moment
|
|
||||||
.utc(report[startDate])
|
|
||||||
.locale("en")
|
|
||||||
.format("YYYY-MM-DD");
|
|
||||||
const endDateFormatted = moment
|
|
||||||
.utc(report[endDate])
|
|
||||||
.locale("en")
|
|
||||||
.format("YYYY-MM-DD");
|
|
||||||
|
|
||||||
if (report.modes[0] === "stacked_chart") {
|
|
||||||
report[filledField] = report[dataField].map((rep) => {
|
|
||||||
return {
|
|
||||||
req: rep.req,
|
|
||||||
label: rep.label,
|
|
||||||
color: rep.color,
|
|
||||||
data: fillMissingDates(
|
|
||||||
JSON.parse(JSON.stringify(rep.data)),
|
|
||||||
startDateFormatted,
|
|
||||||
endDateFormatted
|
|
||||||
),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
report[filledField] = fillMissingDates(
|
|
||||||
JSON.parse(JSON.stringify(report[dataField])),
|
|
||||||
startDateFormatted,
|
|
||||||
endDateFormatted
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static find(type, startDate, endDate, categoryId, groupId) {
|
|
||||||
return ajax("/admin/reports/" + type, {
|
|
||||||
data: {
|
|
||||||
start_date: startDate,
|
|
||||||
end_date: endDate,
|
|
||||||
category_id: categoryId,
|
|
||||||
group_id: groupId,
|
|
||||||
},
|
|
||||||
}).then((json) => {
|
|
||||||
// don’t fill for large multi column tables
|
|
||||||
// which are not date based
|
|
||||||
const modes = json.report.modes;
|
|
||||||
if (modes.length !== 1 && modes[0] !== "table") {
|
|
||||||
Report.fillMissingDates(json.report);
|
|
||||||
}
|
|
||||||
|
|
||||||
const model = Report.create({ type });
|
|
||||||
model.setProperties(json.report);
|
|
||||||
|
|
||||||
if (json.report.related_report) {
|
|
||||||
// TODO: fillMissingDates if xaxis is date
|
|
||||||
const related = Report.create({
|
|
||||||
type: json.report.related_report.type,
|
|
||||||
});
|
|
||||||
related.setProperties(json.report.related_report);
|
|
||||||
model.set("relatedReport", related);
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
average = false;
|
|
||||||
percent = false;
|
|
||||||
higher_is_better = true;
|
|
||||||
description_link = null;
|
|
||||||
description = null;
|
|
||||||
|
|
||||||
@discourseComputed("type", "start_date", "end_date")
|
@discourseComputed("type", "start_date", "end_date")
|
||||||
reportUrl(type, start_date, end_date) {
|
reportUrl(type, start_date, end_date) {
|
||||||
@ -211,7 +35,7 @@ export default class Report extends EmberObject {
|
|||||||
return getURL(
|
return getURL(
|
||||||
`/admin/reports/${type}?start_date=${start_date}&end_date=${end_date}`
|
`/admin/reports/${type}?start_date=${start_date}&end_date=${end_date}`
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
valueAt(numDaysAgo) {
|
valueAt(numDaysAgo) {
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
@ -225,7 +49,7 @@ export default class Report extends EmberObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
},
|
||||||
|
|
||||||
valueFor(startDaysAgo, endDaysAgo) {
|
valueFor(startDaysAgo, endDaysAgo) {
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
@ -246,46 +70,46 @@ export default class Report extends EmberObject {
|
|||||||
}
|
}
|
||||||
return round(sum, -2);
|
return round(sum, -2);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("data", "average")
|
@discourseComputed("data", "average")
|
||||||
todayCount() {
|
todayCount() {
|
||||||
return this.valueAt(0);
|
return this.valueAt(0);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("data", "average")
|
@discourseComputed("data", "average")
|
||||||
yesterdayCount() {
|
yesterdayCount() {
|
||||||
return this.valueAt(1);
|
return this.valueAt(1);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("data", "average")
|
@discourseComputed("data", "average")
|
||||||
sevenDaysAgoCount() {
|
sevenDaysAgoCount() {
|
||||||
return this.valueAt(7);
|
return this.valueAt(7);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("data", "average")
|
@discourseComputed("data", "average")
|
||||||
thirtyDaysAgoCount() {
|
thirtyDaysAgoCount() {
|
||||||
return this.valueAt(30);
|
return this.valueAt(30);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("data", "average")
|
@discourseComputed("data", "average")
|
||||||
lastSevenDaysCount() {
|
lastSevenDaysCount() {
|
||||||
return this.averageCount(7, this.valueFor(1, 7));
|
return this.averageCount(7, this.valueFor(1, 7));
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("data", "average")
|
@discourseComputed("data", "average")
|
||||||
lastThirtyDaysCount() {
|
lastThirtyDaysCount() {
|
||||||
return this.averageCount(30, this.valueFor(1, 30));
|
return this.averageCount(30, this.valueFor(1, 30));
|
||||||
}
|
},
|
||||||
|
|
||||||
averageCount(count, value) {
|
averageCount(count, value) {
|
||||||
return this.average ? value / count : value;
|
return this.average ? value / count : value;
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("yesterdayCount", "higher_is_better")
|
@discourseComputed("yesterdayCount", "higher_is_better")
|
||||||
yesterdayTrend(yesterdayCount, higherIsBetter) {
|
yesterdayTrend(yesterdayCount, higherIsBetter) {
|
||||||
return this._computeTrend(this.valueAt(2), yesterdayCount, higherIsBetter);
|
return this._computeTrend(this.valueAt(2), yesterdayCount, higherIsBetter);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("lastSevenDaysCount", "higher_is_better")
|
@discourseComputed("lastSevenDaysCount", "higher_is_better")
|
||||||
sevenDaysTrend(lastSevenDaysCount, higherIsBetter) {
|
sevenDaysTrend(lastSevenDaysCount, higherIsBetter) {
|
||||||
@ -294,39 +118,39 @@ export default class Report extends EmberObject {
|
|||||||
lastSevenDaysCount,
|
lastSevenDaysCount,
|
||||||
higherIsBetter
|
higherIsBetter
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("data")
|
@discourseComputed("data")
|
||||||
currentTotal(data) {
|
currentTotal(data) {
|
||||||
return data.reduce((cur, pair) => cur + pair.y, 0);
|
return data.reduce((cur, pair) => cur + pair.y, 0);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("data", "currentTotal")
|
@discourseComputed("data", "currentTotal")
|
||||||
currentAverage(data, total) {
|
currentAverage(data, total) {
|
||||||
return makeArray(data).length === 0
|
return makeArray(data).length === 0
|
||||||
? 0
|
? 0
|
||||||
: parseFloat((total / parseFloat(data.length)).toFixed(1));
|
: parseFloat((total / parseFloat(data.length)).toFixed(1));
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("trend", "higher_is_better")
|
@discourseComputed("trend", "higher_is_better")
|
||||||
trendIcon(trend, higherIsBetter) {
|
trendIcon(trend, higherIsBetter) {
|
||||||
return this._iconForTrend(trend, higherIsBetter);
|
return this._iconForTrend(trend, higherIsBetter);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("sevenDaysTrend", "higher_is_better")
|
@discourseComputed("sevenDaysTrend", "higher_is_better")
|
||||||
sevenDaysTrendIcon(sevenDaysTrend, higherIsBetter) {
|
sevenDaysTrendIcon(sevenDaysTrend, higherIsBetter) {
|
||||||
return this._iconForTrend(sevenDaysTrend, higherIsBetter);
|
return this._iconForTrend(sevenDaysTrend, higherIsBetter);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("thirtyDaysTrend", "higher_is_better")
|
@discourseComputed("thirtyDaysTrend", "higher_is_better")
|
||||||
thirtyDaysTrendIcon(thirtyDaysTrend, higherIsBetter) {
|
thirtyDaysTrendIcon(thirtyDaysTrend, higherIsBetter) {
|
||||||
return this._iconForTrend(thirtyDaysTrend, higherIsBetter);
|
return this._iconForTrend(thirtyDaysTrend, higherIsBetter);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("yesterdayTrend", "higher_is_better")
|
@discourseComputed("yesterdayTrend", "higher_is_better")
|
||||||
yesterdayTrendIcon(yesterdayTrend, higherIsBetter) {
|
yesterdayTrendIcon(yesterdayTrend, higherIsBetter) {
|
||||||
return this._iconForTrend(yesterdayTrend, higherIsBetter);
|
return this._iconForTrend(yesterdayTrend, higherIsBetter);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed(
|
@discourseComputed(
|
||||||
"prev_period",
|
"prev_period",
|
||||||
@ -337,7 +161,7 @@ export default class Report extends EmberObject {
|
|||||||
trend(prev, currentTotal, currentAverage, higherIsBetter) {
|
trend(prev, currentTotal, currentAverage, higherIsBetter) {
|
||||||
const total = this.average ? currentAverage : currentTotal;
|
const total = this.average ? currentAverage : currentTotal;
|
||||||
return this._computeTrend(prev, total, higherIsBetter);
|
return this._computeTrend(prev, total, higherIsBetter);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed(
|
@discourseComputed(
|
||||||
"prev30Days",
|
"prev30Days",
|
||||||
@ -356,7 +180,7 @@ export default class Report extends EmberObject {
|
|||||||
lastThirtyDaysCount,
|
lastThirtyDaysCount,
|
||||||
higherIsBetter
|
higherIsBetter
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("type")
|
@discourseComputed("type")
|
||||||
method(type) {
|
method(type) {
|
||||||
@ -365,7 +189,7 @@ export default class Report extends EmberObject {
|
|||||||
} else {
|
} else {
|
||||||
return "sum";
|
return "sum";
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
percentChangeString(val1, val2) {
|
percentChangeString(val1, val2) {
|
||||||
const change = this._computeChange(val1, val2);
|
const change = this._computeChange(val1, val2);
|
||||||
@ -377,7 +201,7 @@ export default class Report extends EmberObject {
|
|||||||
} else {
|
} else {
|
||||||
return change.toFixed(0) + "%";
|
return change.toFixed(0) + "%";
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("prev_period", "currentTotal", "currentAverage")
|
@discourseComputed("prev_period", "currentTotal", "currentAverage")
|
||||||
trendTitle(prev, currentTotal, currentAverage) {
|
trendTitle(prev, currentTotal, currentAverage) {
|
||||||
@ -400,7 +224,7 @@ export default class Report extends EmberObject {
|
|||||||
prev,
|
prev,
|
||||||
current,
|
current,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
changeTitle(valAtT1, valAtT2, prevPeriodString) {
|
changeTitle(valAtT1, valAtT2, prevPeriodString) {
|
||||||
const change = this.percentChangeString(valAtT1, valAtT2);
|
const change = this.percentChangeString(valAtT1, valAtT2);
|
||||||
@ -410,12 +234,12 @@ export default class Report extends EmberObject {
|
|||||||
}
|
}
|
||||||
title += `Was ${number(valAtT1)} ${prevPeriodString}.`;
|
title += `Was ${number(valAtT1)} ${prevPeriodString}.`;
|
||||||
return title;
|
return title;
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("yesterdayCount")
|
@discourseComputed("yesterdayCount")
|
||||||
yesterdayCountTitle(yesterdayCount) {
|
yesterdayCountTitle(yesterdayCount) {
|
||||||
return this.changeTitle(this.valueAt(2), yesterdayCount, "two days ago");
|
return this.changeTitle(this.valueAt(2), yesterdayCount, "two days ago");
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("lastSevenDaysCount")
|
@discourseComputed("lastSevenDaysCount")
|
||||||
sevenDaysCountTitle(lastSevenDaysCount) {
|
sevenDaysCountTitle(lastSevenDaysCount) {
|
||||||
@ -424,12 +248,12 @@ export default class Report extends EmberObject {
|
|||||||
lastSevenDaysCount,
|
lastSevenDaysCount,
|
||||||
"two weeks ago"
|
"two weeks ago"
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("prev30Days", "prev_period")
|
@discourseComputed("prev30Days", "prev_period")
|
||||||
canDisplayTrendIcon(prev30Days, prev_period) {
|
canDisplayTrendIcon(prev30Days, prev_period) {
|
||||||
return prev30Days ?? prev_period;
|
return prev30Days ?? prev_period;
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("prev30Days", "prev_period", "lastThirtyDaysCount")
|
@discourseComputed("prev30Days", "prev_period", "lastThirtyDaysCount")
|
||||||
thirtyDaysCountTitle(prev30Days, prev_period, lastThirtyDaysCount) {
|
thirtyDaysCountTitle(prev30Days, prev_period, lastThirtyDaysCount) {
|
||||||
@ -438,12 +262,12 @@ export default class Report extends EmberObject {
|
|||||||
lastThirtyDaysCount,
|
lastThirtyDaysCount,
|
||||||
"in the previous 30 day period"
|
"in the previous 30 day period"
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("data")
|
@discourseComputed("data")
|
||||||
sortedData(data) {
|
sortedData(data) {
|
||||||
return this.xAxisIsDate ? data.toArray().reverse() : data.toArray();
|
return this.xAxisIsDate ? data.toArray().reverse() : data.toArray();
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("data")
|
@discourseComputed("data")
|
||||||
xAxisIsDate() {
|
xAxisIsDate() {
|
||||||
@ -451,7 +275,7 @@ export default class Report extends EmberObject {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/);
|
return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("labels")
|
@discourseComputed("labels")
|
||||||
computedLabels(labels) {
|
computedLabels(labels) {
|
||||||
@ -535,7 +359,7 @@ export default class Report extends EmberObject {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
_userLabel(properties, row) {
|
_userLabel(properties, row) {
|
||||||
const username = row[properties.username];
|
const username = row[properties.username];
|
||||||
@ -564,7 +388,7 @@ export default class Report extends EmberObject {
|
|||||||
value: username,
|
value: username,
|
||||||
formattedValue: username ? formattedValue() : "—",
|
formattedValue: username ? formattedValue() : "—",
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
_topicLabel(properties, row) {
|
_topicLabel(properties, row) {
|
||||||
const topicTitle = row[properties.title];
|
const topicTitle = row[properties.title];
|
||||||
@ -579,7 +403,7 @@ export default class Report extends EmberObject {
|
|||||||
value: topicTitle,
|
value: topicTitle,
|
||||||
formattedValue: topicTitle ? formattedValue() : "—",
|
formattedValue: topicTitle ? formattedValue() : "—",
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
_postLabel(properties, row) {
|
_postLabel(properties, row) {
|
||||||
const postTitle = row[properties.truncated_raw];
|
const postTitle = row[properties.truncated_raw];
|
||||||
@ -595,21 +419,21 @@ export default class Report extends EmberObject {
|
|||||||
? `<a href='${href}'>${escapeExpression(postTitle)}</a>`
|
? `<a href='${href}'>${escapeExpression(postTitle)}</a>`
|
||||||
: "—",
|
: "—",
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
_secondsLabel(value) {
|
_secondsLabel(value) {
|
||||||
return {
|
return {
|
||||||
value: toNumber(value),
|
value: toNumber(value),
|
||||||
formattedValue: durationTiny(value),
|
formattedValue: durationTiny(value),
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
_percentLabel(value) {
|
_percentLabel(value) {
|
||||||
return {
|
return {
|
||||||
value: toNumber(value),
|
value: toNumber(value),
|
||||||
formattedValue: value ? `${value}%` : "—",
|
formattedValue: value ? `${value}%` : "—",
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
_numberLabel(value, options = {}) {
|
_numberLabel(value, options = {}) {
|
||||||
const formatNumbers = isEmpty(options.formatNumbers)
|
const formatNumbers = isEmpty(options.formatNumbers)
|
||||||
@ -622,21 +446,21 @@ export default class Report extends EmberObject {
|
|||||||
value: toNumber(value),
|
value: toNumber(value),
|
||||||
formattedValue: value ? formattedValue() : "—",
|
formattedValue: value ? formattedValue() : "—",
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
_bytesLabel(value) {
|
_bytesLabel(value) {
|
||||||
return {
|
return {
|
||||||
value: toNumber(value),
|
value: toNumber(value),
|
||||||
formattedValue: I18n.toHumanSize(value),
|
formattedValue: I18n.toHumanSize(value),
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
_dateLabel(value, date, format = "LL") {
|
_dateLabel(value, date, format = "LL") {
|
||||||
return {
|
return {
|
||||||
value,
|
value,
|
||||||
formattedValue: value ? date.format(format) : "—",
|
formattedValue: value ? date.format(format) : "—",
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
_textLabel(value) {
|
_textLabel(value) {
|
||||||
const escaped = escapeExpression(value);
|
const escaped = escapeExpression(value);
|
||||||
@ -645,7 +469,7 @@ export default class Report extends EmberObject {
|
|||||||
value,
|
value,
|
||||||
formattedValue: value ? escaped : "—",
|
formattedValue: value ? escaped : "—",
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
_linkLabel(properties, row) {
|
_linkLabel(properties, row) {
|
||||||
const property = properties[0];
|
const property = properties[0];
|
||||||
@ -660,11 +484,11 @@ export default class Report extends EmberObject {
|
|||||||
value,
|
value,
|
||||||
formattedValue: value ? formattedValue(value, row[properties[1]]) : "—",
|
formattedValue: value ? formattedValue(value, row[properties[1]]) : "—",
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
_computeChange(valAtT1, valAtT2) {
|
_computeChange(valAtT1, valAtT2) {
|
||||||
return ((valAtT2 - valAtT1) / valAtT1) * 100;
|
return ((valAtT2 - valAtT1) / valAtT1) * 100;
|
||||||
}
|
},
|
||||||
|
|
||||||
_computeTrend(valAtT1, valAtT2, higherIsBetter) {
|
_computeTrend(valAtT1, valAtT2, higherIsBetter) {
|
||||||
const change = this._computeChange(valAtT1, valAtT2);
|
const change = this._computeChange(valAtT1, valAtT2);
|
||||||
@ -680,7 +504,7 @@ export default class Report extends EmberObject {
|
|||||||
} else if (change < -2) {
|
} else if (change < -2) {
|
||||||
return higherIsBetter ? "trending-down" : "trending-up";
|
return higherIsBetter ? "trending-down" : "trending-up";
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
_iconForTrend(trend, higherIsBetter) {
|
_iconForTrend(trend, higherIsBetter) {
|
||||||
switch (trend) {
|
switch (trend) {
|
||||||
@ -695,8 +519,8 @@ export default class Report extends EmberObject {
|
|||||||
default:
|
default:
|
||||||
return "minus";
|
return "minus";
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
export const WEEKLY_LIMIT_DAYS = 365;
|
export const WEEKLY_LIMIT_DAYS = 365;
|
||||||
export const DAILY_LIMIT_DAYS = 34;
|
export const DAILY_LIMIT_DAYS = 34;
|
||||||
@ -705,3 +529,183 @@ function applyAverage(value, start, end) {
|
|||||||
const count = end.diff(start, "day") + 1; // 1 to include start
|
const count = end.diff(start, "day") + 1; // 1 to include start
|
||||||
return parseFloat((value / count).toFixed(2));
|
return parseFloat((value / count).toFixed(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Report.reopenClass({
|
||||||
|
groupingForDatapoints(count) {
|
||||||
|
if (count < DAILY_LIMIT_DAYS) {
|
||||||
|
return "daily";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count >= DAILY_LIMIT_DAYS && count < WEEKLY_LIMIT_DAYS) {
|
||||||
|
return "weekly";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count >= WEEKLY_LIMIT_DAYS) {
|
||||||
|
return "monthly";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
unitForDatapoints(count) {
|
||||||
|
if (count >= DAILY_LIMIT_DAYS && count < WEEKLY_LIMIT_DAYS) {
|
||||||
|
return "week";
|
||||||
|
} else if (count >= WEEKLY_LIMIT_DAYS) {
|
||||||
|
return "month";
|
||||||
|
} else {
|
||||||
|
return "day";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
unitForGrouping(grouping) {
|
||||||
|
switch (grouping) {
|
||||||
|
case "monthly":
|
||||||
|
return "month";
|
||||||
|
case "weekly":
|
||||||
|
return "week";
|
||||||
|
default:
|
||||||
|
return "day";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
collapse(model, data, grouping) {
|
||||||
|
grouping = grouping || Report.groupingForDatapoints(data.length);
|
||||||
|
|
||||||
|
if (grouping === "daily") {
|
||||||
|
return data;
|
||||||
|
} else if (grouping === "weekly" || grouping === "monthly") {
|
||||||
|
const isoKind = grouping === "weekly" ? "isoWeek" : "month";
|
||||||
|
const kind = grouping === "weekly" ? "week" : "month";
|
||||||
|
const startMoment = moment(model.start_date, "YYYY-MM-DD");
|
||||||
|
|
||||||
|
let currentIndex = 0;
|
||||||
|
let currentStart = startMoment.clone().startOf(isoKind);
|
||||||
|
let currentEnd = startMoment.clone().endOf(isoKind);
|
||||||
|
const transformedData = [
|
||||||
|
{
|
||||||
|
x: currentStart.format("YYYY-MM-DD"),
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let appliedAverage = false;
|
||||||
|
data.forEach((d) => {
|
||||||
|
const date = moment(d.x, "YYYY-MM-DD");
|
||||||
|
|
||||||
|
if (
|
||||||
|
!date.isSame(currentStart) &&
|
||||||
|
!date.isBetween(currentStart, currentEnd)
|
||||||
|
) {
|
||||||
|
if (model.average) {
|
||||||
|
transformedData[currentIndex].y = applyAverage(
|
||||||
|
transformedData[currentIndex].y,
|
||||||
|
currentStart,
|
||||||
|
currentEnd
|
||||||
|
);
|
||||||
|
|
||||||
|
appliedAverage = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex += 1;
|
||||||
|
currentStart = currentStart.add(1, kind).startOf(isoKind);
|
||||||
|
currentEnd = currentEnd.add(1, kind).endOf(isoKind);
|
||||||
|
} else {
|
||||||
|
appliedAverage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transformedData[currentIndex]) {
|
||||||
|
transformedData[currentIndex].y += d.y;
|
||||||
|
} else {
|
||||||
|
transformedData[currentIndex] = {
|
||||||
|
x: d.x,
|
||||||
|
y: d.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (model.average && !appliedAverage) {
|
||||||
|
transformedData[currentIndex].y = applyAverage(
|
||||||
|
transformedData[currentIndex].y,
|
||||||
|
currentStart,
|
||||||
|
moment(model.end_date).subtract(1, "day") // remove 1 day as model end date is at 00:00 of next day
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure we return something if grouping is unknown
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
fillMissingDates(report, options = {}) {
|
||||||
|
const dataField = options.dataField || "data";
|
||||||
|
const filledField = options.filledField || "data";
|
||||||
|
const startDate = options.startDate || "start_date";
|
||||||
|
const endDate = options.endDate || "end_date";
|
||||||
|
|
||||||
|
if (Array.isArray(report[dataField])) {
|
||||||
|
const startDateFormatted = moment
|
||||||
|
.utc(report[startDate])
|
||||||
|
.locale("en")
|
||||||
|
.format("YYYY-MM-DD");
|
||||||
|
const endDateFormatted = moment
|
||||||
|
.utc(report[endDate])
|
||||||
|
.locale("en")
|
||||||
|
.format("YYYY-MM-DD");
|
||||||
|
|
||||||
|
if (report.modes[0] === "stacked_chart") {
|
||||||
|
report[filledField] = report[dataField].map((rep) => {
|
||||||
|
return {
|
||||||
|
req: rep.req,
|
||||||
|
label: rep.label,
|
||||||
|
color: rep.color,
|
||||||
|
data: fillMissingDates(
|
||||||
|
JSON.parse(JSON.stringify(rep.data)),
|
||||||
|
startDateFormatted,
|
||||||
|
endDateFormatted
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
report[filledField] = fillMissingDates(
|
||||||
|
JSON.parse(JSON.stringify(report[dataField])),
|
||||||
|
startDateFormatted,
|
||||||
|
endDateFormatted
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
find(type, startDate, endDate, categoryId, groupId) {
|
||||||
|
return ajax("/admin/reports/" + type, {
|
||||||
|
data: {
|
||||||
|
start_date: startDate,
|
||||||
|
end_date: endDate,
|
||||||
|
category_id: categoryId,
|
||||||
|
group_id: groupId,
|
||||||
|
},
|
||||||
|
}).then((json) => {
|
||||||
|
// don’t fill for large multi column tables
|
||||||
|
// which are not date based
|
||||||
|
const modes = json.report.modes;
|
||||||
|
if (modes.length !== 1 && modes[0] !== "table") {
|
||||||
|
Report.fillMissingDates(json.report);
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = Report.create({ type });
|
||||||
|
model.setProperties(json.report);
|
||||||
|
|
||||||
|
if (json.report.related_report) {
|
||||||
|
// TODO: fillMissingDates if xaxis is date
|
||||||
|
const related = Report.create({
|
||||||
|
type: json.report.related_report.type,
|
||||||
|
});
|
||||||
|
related.setProperties(json.report.related_report);
|
||||||
|
model.set("relatedReport", related);
|
||||||
|
}
|
||||||
|
|
||||||
|
return model;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Report;
|
||||||
|
|||||||
@ -3,8 +3,21 @@ import I18n from "I18n";
|
|||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
export default class ScreenedEmail extends EmberObject {
|
const ScreenedEmail = EmberObject.extend({
|
||||||
static findAll() {
|
@discourseComputed("action")
|
||||||
|
actionName(action) {
|
||||||
|
return I18n.t("admin.logs.screened_actions." + action);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearBlock() {
|
||||||
|
return ajax("/admin/logs/screened_emails/" + this.id, {
|
||||||
|
type: "DELETE",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ScreenedEmail.reopenClass({
|
||||||
|
findAll() {
|
||||||
return ajax("/admin/logs/screened_emails.json").then(function (
|
return ajax("/admin/logs/screened_emails.json").then(function (
|
||||||
screened_emails
|
screened_emails
|
||||||
) {
|
) {
|
||||||
@ -12,16 +25,7 @@ export default class ScreenedEmail extends EmberObject {
|
|||||||
return ScreenedEmail.create(b);
|
return ScreenedEmail.create(b);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
|
||||||
@discourseComputed("action")
|
export default ScreenedEmail;
|
||||||
actionName(action) {
|
|
||||||
return I18n.t("admin.logs.screened_actions." + action);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearBlock() {
|
|
||||||
return ajax("/admin/logs/screened_emails/" + this.id, {
|
|
||||||
type: "DELETE",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,28 +1,21 @@
|
|||||||
import { equal } from "@ember/object/computed";
|
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
|
import { equal } from "@ember/object/computed";
|
||||||
|
|
||||||
export default class ScreenedIpAddress extends EmberObject {
|
const ScreenedIpAddress = EmberObject.extend({
|
||||||
static findAll(filter) {
|
|
||||||
return ajax("/admin/logs/screened_ip_addresses.json", {
|
|
||||||
data: { filter },
|
|
||||||
}).then((screened_ips) =>
|
|
||||||
screened_ips.map((b) => ScreenedIpAddress.create(b))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@equal("action_name", "block") isBlocked;
|
|
||||||
@discourseComputed("action_name")
|
@discourseComputed("action_name")
|
||||||
actionName(actionName) {
|
actionName(actionName) {
|
||||||
return I18n.t(`admin.logs.screened_ips.actions.${actionName}`);
|
return I18n.t(`admin.logs.screened_ips.actions.${actionName}`);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
isBlocked: equal("action_name", "block"),
|
||||||
|
|
||||||
@discourseComputed("ip_address")
|
@discourseComputed("ip_address")
|
||||||
isRange(ipAddress) {
|
isRange(ipAddress) {
|
||||||
return ipAddress.indexOf("/") > 0;
|
return ipAddress.indexOf("/") > 0;
|
||||||
}
|
},
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
return ajax(
|
return ajax(
|
||||||
@ -37,11 +30,23 @@ export default class ScreenedIpAddress extends EmberObject {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
return ajax("/admin/logs/screened_ip_addresses/" + this.id + ".json", {
|
return ajax("/admin/logs/screened_ip_addresses/" + this.id + ".json", {
|
||||||
type: "DELETE",
|
type: "DELETE",
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
|
ScreenedIpAddress.reopenClass({
|
||||||
|
findAll(filter) {
|
||||||
|
return ajax("/admin/logs/screened_ip_addresses.json", {
|
||||||
|
data: { filter },
|
||||||
|
}).then((screened_ips) =>
|
||||||
|
screened_ips.map((b) => ScreenedIpAddress.create(b))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ScreenedIpAddress;
|
||||||
|
|||||||
@ -3,8 +3,15 @@ import I18n from "I18n";
|
|||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
export default class ScreenedUrl extends EmberObject {
|
const ScreenedUrl = EmberObject.extend({
|
||||||
static findAll() {
|
@discourseComputed("action")
|
||||||
|
actionName(action) {
|
||||||
|
return I18n.t("admin.logs.screened_actions." + action);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ScreenedUrl.reopenClass({
|
||||||
|
findAll() {
|
||||||
return ajax("/admin/logs/screened_urls.json").then(function (
|
return ajax("/admin/logs/screened_urls.json").then(function (
|
||||||
screened_urls
|
screened_urls
|
||||||
) {
|
) {
|
||||||
@ -12,10 +19,7 @@ export default class ScreenedUrl extends EmberObject {
|
|||||||
return ScreenedUrl.create(b);
|
return ScreenedUrl.create(b);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
|
||||||
@discourseComputed("action")
|
export default ScreenedUrl;
|
||||||
actionName(action) {
|
|
||||||
return I18n.t("admin.logs.screened_actions." + action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -4,8 +4,22 @@ import Setting from "admin/mixins/setting-object";
|
|||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
export default class SiteSetting extends EmberObject.extend(Setting) {
|
const SiteSetting = EmberObject.extend(Setting, {
|
||||||
static findAll() {
|
@discourseComputed("setting")
|
||||||
|
staffLogFilter(setting) {
|
||||||
|
if (!setting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subject: setting,
|
||||||
|
action_name: "change_site_setting",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
SiteSetting.reopenClass({
|
||||||
|
findAll() {
|
||||||
return ajax("/admin/site_settings").then(function (settings) {
|
return ajax("/admin/site_settings").then(function (settings) {
|
||||||
// Group the results by category
|
// Group the results by category
|
||||||
const categories = {};
|
const categories = {};
|
||||||
@ -24,9 +38,9 @@ export default class SiteSetting extends EmberObject.extend(Setting) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
static update(key, value, opts = {}) {
|
update(key, value, opts = {}) {
|
||||||
const data = {};
|
const data = {};
|
||||||
data[key] = value;
|
data[key] = value;
|
||||||
|
|
||||||
@ -35,17 +49,7 @@ export default class SiteSetting extends EmberObject.extend(Setting) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ajax(`/admin/site_settings/${key}`, { type: "PUT", data });
|
return ajax(`/admin/site_settings/${key}`, { type: "PUT", data });
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
|
||||||
@discourseComputed("setting")
|
export default SiteSetting;
|
||||||
staffLogFilter(setting) {
|
|
||||||
if (!setting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
subject: setting,
|
|
||||||
action_name: "change_site_setting",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -2,10 +2,10 @@ import RestModel from "discourse/models/rest";
|
|||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { getProperties } from "@ember/object";
|
import { getProperties } from "@ember/object";
|
||||||
|
|
||||||
export default class SiteText extends RestModel {
|
export default RestModel.extend({
|
||||||
revert(locale) {
|
revert(locale) {
|
||||||
return ajax(`/admin/customize/site_texts/${this.id}?locale=${locale}`, {
|
return ajax(`/admin/customize/site_texts/${this.id}?locale=${locale}`, {
|
||||||
type: "DELETE",
|
type: "DELETE",
|
||||||
}).then((result) => getProperties(result.site_text, "value", "can_revert"));
|
}).then((result) => getProperties(result.site_text, "value", "can_revert"));
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -11,36 +11,13 @@ function format(label, value, escape = true) {
|
|||||||
: "";
|
: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class StaffActionLog extends RestModel {
|
const StaffActionLog = RestModel.extend({
|
||||||
static munge(json) {
|
showFullDetails: false,
|
||||||
if (json.acting_user) {
|
|
||||||
json.acting_user = AdminUser.create(json.acting_user);
|
|
||||||
}
|
|
||||||
if (json.target_user) {
|
|
||||||
json.target_user = AdminUser.create(json.target_user);
|
|
||||||
}
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
static findAll(data) {
|
|
||||||
return ajax("/admin/logs/staff_action_logs.json", { data }).then(
|
|
||||||
(result) => {
|
|
||||||
return {
|
|
||||||
staff_action_logs: result.staff_action_logs.map((s) =>
|
|
||||||
StaffActionLog.create(s)
|
|
||||||
),
|
|
||||||
user_history_actions: result.user_history_actions,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
showFullDetails = false;
|
|
||||||
|
|
||||||
@discourseComputed("action_name")
|
@discourseComputed("action_name")
|
||||||
actionName(actionName) {
|
actionName(actionName) {
|
||||||
return I18n.t(`admin.logs.staff_actions.actions.${actionName}`);
|
return I18n.t(`admin.logs.staff_actions.actions.${actionName}`);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed(
|
@discourseComputed(
|
||||||
"email",
|
"email",
|
||||||
@ -95,15 +72,42 @@ export default class StaffActionLog extends RestModel {
|
|||||||
|
|
||||||
const formatted = lines.filter((l) => l.length > 0).join("<br/>");
|
const formatted = lines.filter((l) => l.length > 0).join("<br/>");
|
||||||
return formatted.length > 0 ? formatted + "<br/>" : "";
|
return formatted.length > 0 ? formatted + "<br/>" : "";
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("details")
|
@discourseComputed("details")
|
||||||
useModalForDetails(details) {
|
useModalForDetails(details) {
|
||||||
return details && details.length > 100;
|
return details && details.length > 100;
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("action_name")
|
@discourseComputed("action_name")
|
||||||
useCustomModalForDetails(actionName) {
|
useCustomModalForDetails(actionName) {
|
||||||
return ["change_theme", "delete_theme"].includes(actionName);
|
return ["change_theme", "delete_theme"].includes(actionName);
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
|
StaffActionLog.reopenClass({
|
||||||
|
munge(json) {
|
||||||
|
if (json.acting_user) {
|
||||||
|
json.acting_user = AdminUser.create(json.acting_user);
|
||||||
|
}
|
||||||
|
if (json.target_user) {
|
||||||
|
json.target_user = AdminUser.create(json.target_user);
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
},
|
||||||
|
|
||||||
|
findAll(data) {
|
||||||
|
return ajax("/admin/logs/staff_action_logs.json", { data }).then(
|
||||||
|
(result) => {
|
||||||
|
return {
|
||||||
|
staff_action_logs: result.staff_action_logs.map((s) =>
|
||||||
|
StaffActionLog.create(s)
|
||||||
|
),
|
||||||
|
user_history_actions: result.user_history_actions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default StaffActionLog;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import Setting from "admin/mixins/setting-object";
|
import Setting from "admin/mixins/setting-object";
|
||||||
|
|
||||||
export default class ThemeSettings extends EmberObject.extend(Setting) {}
|
export default EmberObject.extend(Setting, {});
|
||||||
|
|||||||
@ -13,13 +13,11 @@ export const THEMES = "themes";
|
|||||||
export const COMPONENTS = "components";
|
export const COMPONENTS = "components";
|
||||||
const SETTINGS_TYPE_ID = 5;
|
const SETTINGS_TYPE_ID = 5;
|
||||||
|
|
||||||
class Theme extends RestModel {
|
const Theme = RestModel.extend({
|
||||||
@or("default", "user_selectable") isActive;
|
isActive: or("default", "user_selectable"),
|
||||||
@gt("remote_theme.commits_behind", 0) isPendingUpdates;
|
isPendingUpdates: gt("remote_theme.commits_behind", 0),
|
||||||
@gt("editedFields.length", 0) hasEditedFields;
|
hasEditedFields: gt("editedFields.length", 0),
|
||||||
@gt("parent_themes.length", 0) hasParents;
|
hasParents: gt("parent_themes.length", 0),
|
||||||
|
|
||||||
changed = false;
|
|
||||||
|
|
||||||
@discourseComputed("theme_fields.[]")
|
@discourseComputed("theme_fields.[]")
|
||||||
targets() {
|
targets() {
|
||||||
@ -47,7 +45,7 @@ class Theme extends RestModel {
|
|||||||
target["error"] = this.hasError(target.name);
|
target["error"] = this.hasError(target.name);
|
||||||
return target;
|
return target;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("theme_fields.[]")
|
@discourseComputed("theme_fields.[]")
|
||||||
fieldNames() {
|
fieldNames() {
|
||||||
@ -86,7 +84,7 @@ class Theme extends RestModel {
|
|||||||
],
|
],
|
||||||
extra_scss: scss_fields,
|
extra_scss: scss_fields,
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed(
|
@discourseComputed(
|
||||||
"fieldNames",
|
"fieldNames",
|
||||||
@ -120,7 +118,7 @@ class Theme extends RestModel {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
return hash;
|
return hash;
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("theme_fields")
|
@discourseComputed("theme_fields")
|
||||||
themeFields(fields) {
|
themeFields(fields) {
|
||||||
@ -136,7 +134,7 @@ class Theme extends RestModel {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
return hash;
|
return hash;
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("theme_fields", "theme_fields.[]")
|
@discourseComputed("theme_fields", "theme_fields.[]")
|
||||||
uploads(fields) {
|
uploads(fields) {
|
||||||
@ -146,32 +144,32 @@ class Theme extends RestModel {
|
|||||||
return fields.filter(
|
return fields.filter(
|
||||||
(f) => f.target === "common" && f.type_id === THEME_UPLOAD_VAR
|
(f) => f.target === "common" && f.type_id === THEME_UPLOAD_VAR
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("theme_fields", "theme_fields.@each.error")
|
@discourseComputed("theme_fields", "theme_fields.@each.error")
|
||||||
isBroken(fields) {
|
isBroken(fields) {
|
||||||
return (
|
return (
|
||||||
fields && fields.any((field) => field.error && field.error.length > 0)
|
fields && fields.any((field) => field.error && field.error.length > 0)
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("theme_fields.[]")
|
@discourseComputed("theme_fields.[]")
|
||||||
editedFields(fields) {
|
editedFields(fields) {
|
||||||
return fields.filter(
|
return fields.filter(
|
||||||
(field) => !isBlank(field.value) && field.type_id !== SETTINGS_TYPE_ID
|
(field) => !isBlank(field.value) && field.type_id !== SETTINGS_TYPE_ID
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("remote_theme.last_error_text")
|
@discourseComputed("remote_theme.last_error_text")
|
||||||
remoteError(errorText) {
|
remoteError(errorText) {
|
||||||
if (errorText && errorText.length > 0) {
|
if (errorText && errorText.length > 0) {
|
||||||
return errorText;
|
return errorText;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
getKey(field) {
|
getKey(field) {
|
||||||
return `${field.target} ${field.name}`;
|
return `${field.target} ${field.name}`;
|
||||||
}
|
},
|
||||||
|
|
||||||
hasEdited(target, name) {
|
hasEdited(target, name) {
|
||||||
if (name) {
|
if (name) {
|
||||||
@ -182,27 +180,27 @@ class Theme extends RestModel {
|
|||||||
(field) => field.target === target && !isEmpty(field.value)
|
(field) => field.target === target && !isEmpty(field.value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
hasError(target, name) {
|
hasError(target, name) {
|
||||||
return this.theme_fields
|
return this.theme_fields
|
||||||
.filter((f) => f.target === target && (!name || name === f.name))
|
.filter((f) => f.target === target && (!name || name === f.name))
|
||||||
.any((f) => f.error);
|
.any((f) => f.error);
|
||||||
}
|
},
|
||||||
|
|
||||||
getError(target, name) {
|
getError(target, name) {
|
||||||
let themeFields = this.themeFields;
|
let themeFields = this.themeFields;
|
||||||
let key = this.getKey({ target, name });
|
let key = this.getKey({ target, name });
|
||||||
let field = themeFields[key];
|
let field = themeFields[key];
|
||||||
return field ? field.error : "";
|
return field ? field.error : "";
|
||||||
}
|
},
|
||||||
|
|
||||||
getField(target, name) {
|
getField(target, name) {
|
||||||
let themeFields = this.themeFields;
|
let themeFields = this.themeFields;
|
||||||
let key = this.getKey({ target, name });
|
let key = this.getKey({ target, name });
|
||||||
let field = themeFields[key];
|
let field = themeFields[key];
|
||||||
return field ? field.value : "";
|
return field ? field.value : "";
|
||||||
}
|
},
|
||||||
|
|
||||||
removeField(field) {
|
removeField(field) {
|
||||||
this.set("changed", true);
|
this.set("changed", true);
|
||||||
@ -211,7 +209,7 @@ class Theme extends RestModel {
|
|||||||
field.value = null;
|
field.value = null;
|
||||||
|
|
||||||
return this.saveChanges("theme_fields");
|
return this.saveChanges("theme_fields");
|
||||||
}
|
},
|
||||||
|
|
||||||
setField(target, name, value, upload_id, type_id) {
|
setField(target, name, value, upload_id, type_id) {
|
||||||
this.set("changed", true);
|
this.set("changed", true);
|
||||||
@ -251,25 +249,25 @@ class Theme extends RestModel {
|
|||||||
this.notifyPropertyChange("theme_fields.[]");
|
this.notifyPropertyChange("theme_fields.[]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("childThemes.[]")
|
@discourseComputed("childThemes.[]")
|
||||||
child_theme_ids(childThemes) {
|
child_theme_ids(childThemes) {
|
||||||
if (childThemes) {
|
if (childThemes) {
|
||||||
return childThemes.map((theme) => get(theme, "id"));
|
return childThemes.map((theme) => get(theme, "id"));
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("recentlyInstalled", "component", "hasParents")
|
@discourseComputed("recentlyInstalled", "component", "hasParents")
|
||||||
warnUnassignedComponent(recent, component, hasParents) {
|
warnUnassignedComponent(recent, component, hasParents) {
|
||||||
return recent && component && !hasParents;
|
return recent && component && !hasParents;
|
||||||
}
|
},
|
||||||
|
|
||||||
removeChildTheme(theme) {
|
removeChildTheme(theme) {
|
||||||
const childThemes = this.childThemes;
|
const childThemes = this.childThemes;
|
||||||
childThemes.removeObject(theme);
|
childThemes.removeObject(theme);
|
||||||
return this.saveChanges("child_theme_ids");
|
return this.saveChanges("child_theme_ids");
|
||||||
}
|
},
|
||||||
|
|
||||||
addChildTheme(theme) {
|
addChildTheme(theme) {
|
||||||
let childThemes = this.childThemes;
|
let childThemes = this.childThemes;
|
||||||
@ -280,7 +278,7 @@ class Theme extends RestModel {
|
|||||||
childThemes.removeObject(theme);
|
childThemes.removeObject(theme);
|
||||||
childThemes.pushObject(theme);
|
childThemes.pushObject(theme);
|
||||||
return this.saveChanges("child_theme_ids");
|
return this.saveChanges("child_theme_ids");
|
||||||
}
|
},
|
||||||
|
|
||||||
addParentTheme(theme) {
|
addParentTheme(theme) {
|
||||||
let parentThemes = this.parentThemes;
|
let parentThemes = this.parentThemes;
|
||||||
@ -289,36 +287,38 @@ class Theme extends RestModel {
|
|||||||
this.set("parentThemes", parentThemes);
|
this.set("parentThemes", parentThemes);
|
||||||
}
|
}
|
||||||
parentThemes.addObject(theme);
|
parentThemes.addObject(theme);
|
||||||
}
|
},
|
||||||
|
|
||||||
checkForUpdates() {
|
checkForUpdates() {
|
||||||
return this.save({ remote_check: true }).then(() =>
|
return this.save({ remote_check: true }).then(() =>
|
||||||
this.set("changed", false)
|
this.set("changed", false)
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
updateToLatest() {
|
updateToLatest() {
|
||||||
return this.save({ remote_update: true }).then(() =>
|
return this.save({ remote_update: true }).then(() =>
|
||||||
this.set("changed", false)
|
this.set("changed", false)
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
changed: false,
|
||||||
|
|
||||||
saveChanges() {
|
saveChanges() {
|
||||||
const hash = this.getProperties.apply(this, arguments);
|
const hash = this.getProperties.apply(this, arguments);
|
||||||
return this.save(hash)
|
return this.save(hash)
|
||||||
.finally(() => this.set("changed", false))
|
.finally(() => this.set("changed", false))
|
||||||
.catch(popupAjaxError);
|
.catch(popupAjaxError);
|
||||||
}
|
},
|
||||||
|
|
||||||
saveSettings(name, value) {
|
saveSettings(name, value) {
|
||||||
const settings = {};
|
const settings = {};
|
||||||
settings[name] = value;
|
settings[name] = value;
|
||||||
return this.save({ settings });
|
return this.save({ settings });
|
||||||
}
|
},
|
||||||
|
|
||||||
saveTranslation(name, value) {
|
saveTranslation(name, value) {
|
||||||
return this.save({ translations: { [name]: value } });
|
return this.save({ translations: { [name]: value } });
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
export default Theme;
|
export default Theme;
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
export default class Tl3Requirements extends EmberObject {
|
export default EmberObject.extend({
|
||||||
@discourseComputed("days_visited", "time_period")
|
@discourseComputed("days_visited", "time_period")
|
||||||
days_visited_percent(daysVisited, timePeriod) {
|
days_visited_percent(daysVisited, timePeriod) {
|
||||||
return Math.round((daysVisited * 100) / timePeriod);
|
return Math.round((daysVisited * 100) / timePeriod);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("min_days_visited", "time_period")
|
@discourseComputed("min_days_visited", "time_period")
|
||||||
min_days_visited_percent(minDaysVisited, timePeriod) {
|
min_days_visited_percent(minDaysVisited, timePeriod) {
|
||||||
return Math.round((minDaysVisited * 100) / timePeriod);
|
return Math.round((minDaysVisited * 100) / timePeriod);
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("num_topics_replied_to", "min_topics_replied_to")
|
@discourseComputed("num_topics_replied_to", "min_topics_replied_to")
|
||||||
capped_topics_replied_to(numReplied, minReplied) {
|
capped_topics_replied_to(numReplied, minReplied) {
|
||||||
return numReplied > minReplied;
|
return numReplied > minReplied;
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed(
|
@discourseComputed(
|
||||||
"days_visited",
|
"days_visited",
|
||||||
@ -71,5 +71,5 @@ export default class Tl3Requirements extends EmberObject {
|
|||||||
silenced: this.get("penalty_counts.silenced") === 0,
|
silenced: this.get("penalty_counts.silenced") === 0,
|
||||||
suspended: this.get("penalty_counts.suspended") === 0,
|
suspended: this.get("penalty_counts.suspended") === 0,
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -2,8 +2,14 @@ import EmberObject from "@ember/object";
|
|||||||
import RestModel from "discourse/models/rest";
|
import RestModel from "discourse/models/rest";
|
||||||
import { i18n } from "discourse/lib/computed";
|
import { i18n } from "discourse/lib/computed";
|
||||||
|
|
||||||
export default class UserField extends RestModel {
|
const UserField = RestModel.extend();
|
||||||
static fieldTypes() {
|
|
||||||
|
const UserFieldType = EmberObject.extend({
|
||||||
|
name: i18n("id", "admin.user_fields.field_types.%@"),
|
||||||
|
});
|
||||||
|
|
||||||
|
UserField.reopenClass({
|
||||||
|
fieldTypes() {
|
||||||
if (!this._fieldTypes) {
|
if (!this._fieldTypes) {
|
||||||
this._fieldTypes = [
|
this._fieldTypes = [
|
||||||
UserFieldType.create({ id: "text" }),
|
UserFieldType.create({ id: "text" }),
|
||||||
@ -14,13 +20,11 @@ export default class UserField extends RestModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this._fieldTypes;
|
return this._fieldTypes;
|
||||||
}
|
},
|
||||||
|
|
||||||
static fieldTypeById(id) {
|
fieldTypeById(id) {
|
||||||
return this.fieldTypes().findBy("id", id);
|
return this.fieldTypes().findBy("id", id);
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
class UserFieldType extends EmberObject {
|
export default UserField;
|
||||||
@i18n("id", "admin.user_fields.field_types.%@") name;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -2,39 +2,43 @@ import EmberObject from "@ember/object";
|
|||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
export default class VersionCheck extends EmberObject {
|
const VersionCheck = EmberObject.extend({
|
||||||
static find() {
|
|
||||||
return ajax("/admin/version_check").then((json) =>
|
|
||||||
VersionCheck.create(json)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@discourseComputed("updated_at")
|
@discourseComputed("updated_at")
|
||||||
noCheckPerformed(updatedAt) {
|
noCheckPerformed(updatedAt) {
|
||||||
return updatedAt === null;
|
return updatedAt === null;
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("missing_versions_count")
|
@discourseComputed("missing_versions_count")
|
||||||
upToDate(missingVersionsCount) {
|
upToDate(missingVersionsCount) {
|
||||||
return missingVersionsCount === 0 || missingVersionsCount === null;
|
return missingVersionsCount === 0 || missingVersionsCount === null;
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("missing_versions_count")
|
@discourseComputed("missing_versions_count")
|
||||||
behindByOneVersion(missingVersionsCount) {
|
behindByOneVersion(missingVersionsCount) {
|
||||||
return missingVersionsCount === 1;
|
return missingVersionsCount === 1;
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("installed_sha")
|
@discourseComputed("installed_sha")
|
||||||
gitLink(installedSHA) {
|
gitLink(installedSHA) {
|
||||||
if (installedSHA) {
|
if (installedSHA) {
|
||||||
return `https://github.com/discourse/discourse/commits/${installedSHA}`;
|
return `https://github.com/discourse/discourse/commits/${installedSHA}`;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("installed_sha")
|
@discourseComputed("installed_sha")
|
||||||
shortSha(installedSHA) {
|
shortSha(installedSHA) {
|
||||||
if (installedSHA) {
|
if (installedSHA) {
|
||||||
return installedSHA.slice(0, 10);
|
return installedSHA.slice(0, 10);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
|
VersionCheck.reopenClass({
|
||||||
|
find() {
|
||||||
|
return ajax("/admin/version_check").then((json) =>
|
||||||
|
VersionCheck.create(json)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default VersionCheck;
|
||||||
|
|||||||
@ -2,8 +2,34 @@ import EmberObject from "@ember/object";
|
|||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
export default class WatchedWord extends EmberObject {
|
const WatchedWord = EmberObject.extend({
|
||||||
static findAll() {
|
save() {
|
||||||
|
return ajax(
|
||||||
|
"/admin/customize/watched_words" +
|
||||||
|
(this.id ? "/" + this.id : "") +
|
||||||
|
".json",
|
||||||
|
{
|
||||||
|
type: this.id ? "PUT" : "POST",
|
||||||
|
data: {
|
||||||
|
word: this.word,
|
||||||
|
replacement: this.replacement,
|
||||||
|
action_key: this.action,
|
||||||
|
case_sensitive: this.isCaseSensitive,
|
||||||
|
},
|
||||||
|
dataType: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
return ajax("/admin/customize/watched_words/" + this.id + ".json", {
|
||||||
|
type: "DELETE",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
WatchedWord.reopenClass({
|
||||||
|
findAll() {
|
||||||
return ajax("/admin/customize/watched_words.json").then((list) => {
|
return ajax("/admin/customize/watched_words.json").then((list) => {
|
||||||
const actions = {};
|
const actions = {};
|
||||||
|
|
||||||
@ -24,29 +50,7 @@ export default class WatchedWord extends EmberObject {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
|
||||||
save() {
|
export default WatchedWord;
|
||||||
return ajax(
|
|
||||||
"/admin/customize/watched_words" +
|
|
||||||
(this.id ? "/" + this.id : "") +
|
|
||||||
".json",
|
|
||||||
{
|
|
||||||
type: this.id ? "PUT" : "POST",
|
|
||||||
data: {
|
|
||||||
word: this.word,
|
|
||||||
replacement: this.replacement,
|
|
||||||
action_key: this.action,
|
|
||||||
case_sensitive: this.isCaseSensitive,
|
|
||||||
},
|
|
||||||
dataType: "json",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
return ajax("/admin/customize/watched_words/" + this.id + ".json", {
|
|
||||||
type: "DELETE",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,34 +1,33 @@
|
|||||||
import { computed } from "@ember/object";
|
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
|
||||||
import { observes } from "@ember-decorators/object";
|
|
||||||
import Category from "discourse/models/category";
|
import Category from "discourse/models/category";
|
||||||
import Group from "discourse/models/group";
|
import Group from "discourse/models/group";
|
||||||
import RestModel from "discourse/models/rest";
|
import RestModel from "discourse/models/rest";
|
||||||
import Site from "discourse/models/site";
|
import Site from "discourse/models/site";
|
||||||
import { isEmpty } from "@ember/utils";
|
import { isEmpty } from "@ember/utils";
|
||||||
|
|
||||||
export default class WebHook extends RestModel {
|
export default RestModel.extend({
|
||||||
content_type = 1; // json
|
content_type: 1, // json
|
||||||
last_delivery_status = 1; // inactive
|
last_delivery_status: 1, // inactive
|
||||||
wildcard_web_hook = false;
|
wildcard_web_hook: false,
|
||||||
verify_certificate = true;
|
verify_certificate: true,
|
||||||
active = false;
|
active: false,
|
||||||
web_hook_event_types = null;
|
web_hook_event_types: null,
|
||||||
groupsFilterInName = null;
|
groupsFilterInName: null,
|
||||||
|
|
||||||
@computed("wildcard_web_hook")
|
@discourseComputed("wildcard_web_hook")
|
||||||
get wildcard() {
|
webhookType: {
|
||||||
return this.wildcard_web_hook ? "wildcard" : "individual";
|
get(wildcard) {
|
||||||
}
|
return wildcard ? "wildcard" : "individual";
|
||||||
|
},
|
||||||
set wildcard(value) {
|
set(value) {
|
||||||
this.set("wildcard_web_hook", value === "wildcard");
|
this.set("wildcard_web_hook", value === "wildcard");
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
|
||||||
@discourseComputed("category_ids")
|
@discourseComputed("category_ids")
|
||||||
categories(categoryIds) {
|
categories(categoryIds) {
|
||||||
return Category.findByIds(categoryIds);
|
return Category.findByIds(categoryIds);
|
||||||
}
|
},
|
||||||
|
|
||||||
@observes("group_ids")
|
@observes("group_ids")
|
||||||
updateGroupsFilter() {
|
updateGroupsFilter() {
|
||||||
@ -42,11 +41,11 @@ export default class WebHook extends RestModel {
|
|||||||
return groupNames;
|
return groupNames;
|
||||||
}, [])
|
}, [])
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
groupFinder(term) {
|
groupFinder(term) {
|
||||||
return Group.findAll({ term, ignore_automatic: false });
|
return Group.findAll({ term, ignore_automatic: false });
|
||||||
}
|
},
|
||||||
|
|
||||||
@discourseComputed("wildcard_web_hook", "web_hook_event_types.[]")
|
@discourseComputed("wildcard_web_hook", "web_hook_event_types.[]")
|
||||||
description(isWildcardWebHook, types) {
|
description(isWildcardWebHook, types) {
|
||||||
@ -58,7 +57,7 @@ export default class WebHook extends RestModel {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return isWildcardWebHook ? "*" : desc;
|
return isWildcardWebHook ? "*" : desc;
|
||||||
}
|
},
|
||||||
|
|
||||||
createProperties() {
|
createProperties() {
|
||||||
const types = this.web_hook_event_types;
|
const types = this.web_hook_event_types;
|
||||||
@ -93,9 +92,9 @@ export default class WebHook extends RestModel {
|
|||||||
return groupIds;
|
return groupIds;
|
||||||
}, []),
|
}, []),
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
updateProperties() {
|
updateProperties() {
|
||||||
return this.createProperties();
|
return this.createProperties();
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -36,8 +36,12 @@ export default class AdminLogsStaffActionLogsRoute extends DiscourseRoute {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
onFiltersChange(filters) {
|
onFiltersChange(filters) {
|
||||||
this.transitionTo("adminLogs.staffActionLogs", {
|
if (filters && Object.keys(filters) === 0) {
|
||||||
queryParams: { filters },
|
this.transitionTo("adminLogs.staffActionLogs");
|
||||||
});
|
} else {
|
||||||
|
this.transitionTo("adminLogs.staffActionLogs", {
|
||||||
|
queryParams: { filters },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,8 +10,8 @@ import { htmlSafe } from "@ember/template";
|
|||||||
// A service that can act as a bridge between the front end Discourse application
|
// A service that can act as a bridge between the front end Discourse application
|
||||||
// and the admin application. Use this if you need front end code to access admin
|
// and the admin application. Use this if you need front end code to access admin
|
||||||
// modules. Inject it optionally, and if it exists go to town!
|
// modules. Inject it optionally, and if it exists go to town!
|
||||||
export default class AdminToolsService extends Service {
|
export default Service.extend({
|
||||||
@service dialog;
|
dialog: service(),
|
||||||
|
|
||||||
showActionLogs(target, filters) {
|
showActionLogs(target, filters) {
|
||||||
const controller = getOwner(target).lookup(
|
const controller = getOwner(target).lookup(
|
||||||
@ -20,15 +20,15 @@ export default class AdminToolsService extends Service {
|
|||||||
target.transitionToRoute("adminLogs.staffActionLogs").then(() => {
|
target.transitionToRoute("adminLogs.staffActionLogs").then(() => {
|
||||||
controller.changeFilters(filters);
|
controller.changeFilters(filters);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
checkSpammer(userId) {
|
checkSpammer(userId) {
|
||||||
return AdminUser.find(userId).then((au) => this.spammerDetails(au));
|
return AdminUser.find(userId).then((au) => this.spammerDetails(au));
|
||||||
}
|
},
|
||||||
|
|
||||||
deleteUser(id, formData) {
|
deleteUser(id, formData) {
|
||||||
return AdminUser.find(id).then((user) => user.destroy(formData));
|
return AdminUser.find(id).then((user) => user.destroy(formData));
|
||||||
}
|
},
|
||||||
|
|
||||||
spammerDetails(adminUser) {
|
spammerDetails(adminUser) {
|
||||||
return {
|
return {
|
||||||
@ -37,7 +37,7 @@ export default class AdminToolsService extends Service {
|
|||||||
adminUser.get("can_be_deleted") &&
|
adminUser.get("can_be_deleted") &&
|
||||||
adminUser.get("can_delete_all_posts"),
|
adminUser.get("can_delete_all_posts"),
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
_showControlModal(type, user, opts) {
|
_showControlModal(type, user, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
@ -67,15 +67,15 @@ export default class AdminToolsService extends Service {
|
|||||||
|
|
||||||
controller.finishedSetup();
|
controller.finishedSetup();
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
showSilenceModal(user, opts) {
|
showSilenceModal(user, opts) {
|
||||||
this._showControlModal("silence", user, opts);
|
this._showControlModal("silence", user, opts);
|
||||||
}
|
},
|
||||||
|
|
||||||
showSuspendModal(user, opts) {
|
showSuspendModal(user, opts) {
|
||||||
this._showControlModal("suspend", user, opts);
|
this._showControlModal("suspend", user, opts);
|
||||||
}
|
},
|
||||||
|
|
||||||
_deleteSpammer(adminUser) {
|
_deleteSpammer(adminUser) {
|
||||||
// Try loading the email if the site supports it
|
// Try loading the email if the site supports it
|
||||||
@ -131,5 +131,5 @@ export default class AdminToolsService extends Service {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -2,7 +2,7 @@ export const POPULAR_THEMES = [
|
|||||||
{
|
{
|
||||||
name: "Graceful",
|
name: "Graceful",
|
||||||
value: "https://github.com/discourse/graceful",
|
value: "https://github.com/discourse/graceful",
|
||||||
preview: "https://discourse.theme-creator.io/theme/awesomerobot/graceful",
|
preview: "https://theme-creator.discourse.org/theme/awesomerobot/graceful",
|
||||||
description: "A light and graceful theme for Discourse.",
|
description: "A light and graceful theme for Discourse.",
|
||||||
meta_url:
|
meta_url:
|
||||||
"https://meta.discourse.org/t/a-graceful-theme-for-discourse/93040",
|
"https://meta.discourse.org/t/a-graceful-theme-for-discourse/93040",
|
||||||
@ -10,7 +10,8 @@ export const POPULAR_THEMES = [
|
|||||||
{
|
{
|
||||||
name: "Material Design Theme",
|
name: "Material Design Theme",
|
||||||
value: "https://github.com/discourse/material-design-stock-theme",
|
value: "https://github.com/discourse/material-design-stock-theme",
|
||||||
preview: "https://discourse.theme-creator.io/theme/tshenry/material-design",
|
preview:
|
||||||
|
"https://theme-creator.discourse.org/theme/tshenry/material-design",
|
||||||
description:
|
description:
|
||||||
"Inspired by Material Design, this theme comes with several color palettes (incl. a dark one).",
|
"Inspired by Material Design, this theme comes with several color palettes (incl. a dark one).",
|
||||||
meta_url: "https://meta.discourse.org/t/material-design-stock-theme/47142",
|
meta_url: "https://meta.discourse.org/t/material-design-stock-theme/47142",
|
||||||
@ -18,7 +19,7 @@ export const POPULAR_THEMES = [
|
|||||||
{
|
{
|
||||||
name: "Minima",
|
name: "Minima",
|
||||||
value: "https://github.com/discourse/minima",
|
value: "https://github.com/discourse/minima",
|
||||||
preview: "https://discourse.theme-creator.io/theme/awesomerobot/minima",
|
preview: "https://theme-creator.discourse.org/theme/awesomerobot/minima",
|
||||||
description: "A minimal theme with reduced UI elements and focus on text.",
|
description: "A minimal theme with reduced UI elements and focus on text.",
|
||||||
meta_url:
|
meta_url:
|
||||||
"https://meta.discourse.org/t/minima-a-minimal-theme-for-discourse/108178",
|
"https://meta.discourse.org/t/minima-a-minimal-theme-for-discourse/108178",
|
||||||
@ -26,7 +27,7 @@ export const POPULAR_THEMES = [
|
|||||||
{
|
{
|
||||||
name: "Sam's Simple Theme",
|
name: "Sam's Simple Theme",
|
||||||
value: "https://github.com/discourse/discourse-simple-theme",
|
value: "https://github.com/discourse/discourse-simple-theme",
|
||||||
preview: "https://discourse.theme-creator.io/theme/sam/simple",
|
preview: "https://theme-creator.discourse.org/theme/sam/simple",
|
||||||
description:
|
description:
|
||||||
"Simplified front page design with classic colors and typography.",
|
"Simplified front page design with classic colors and typography.",
|
||||||
meta_url:
|
meta_url:
|
||||||
@ -35,8 +36,6 @@ export const POPULAR_THEMES = [
|
|||||||
{
|
{
|
||||||
name: "Brand Header",
|
name: "Brand Header",
|
||||||
value: "https://github.com/discourse/discourse-brand-header",
|
value: "https://github.com/discourse/discourse-brand-header",
|
||||||
preview:
|
|
||||||
"https://discourse.theme-creator.io/theme/vinothkannans/brand-header",
|
|
||||||
description:
|
description:
|
||||||
"Add an extra top header with your logo, navigation links and social icons.",
|
"Add an extra top header with your logo, navigation links and social icons.",
|
||||||
meta_url: "https://meta.discourse.org/t/brand-header-theme-component/77977",
|
meta_url: "https://meta.discourse.org/t/brand-header-theme-component/77977",
|
||||||
@ -46,7 +45,7 @@ export const POPULAR_THEMES = [
|
|||||||
name: "Custom Header Links",
|
name: "Custom Header Links",
|
||||||
value: "https://github.com/discourse/discourse-custom-header-links",
|
value: "https://github.com/discourse/discourse-custom-header-links",
|
||||||
preview:
|
preview:
|
||||||
"https://discourse.theme-creator.io/theme/awesomerobot/custom-header-links",
|
"https://theme-creator.discourse.org/theme/Johani/custom-header-links",
|
||||||
description: "Easily add custom text-based links to the header.",
|
description: "Easily add custom text-based links to the header.",
|
||||||
meta_url: "https://meta.discourse.org/t/custom-header-links/90588",
|
meta_url: "https://meta.discourse.org/t/custom-header-links/90588",
|
||||||
component: true,
|
component: true,
|
||||||
@ -62,7 +61,7 @@ export const POPULAR_THEMES = [
|
|||||||
name: "Category Banners",
|
name: "Category Banners",
|
||||||
value: "https://github.com/discourse/discourse-category-banners",
|
value: "https://github.com/discourse/discourse-category-banners",
|
||||||
preview:
|
preview:
|
||||||
"https://discourse.theme-creator.io/theme/awesomerobot/discourse-category-banners",
|
"https://theme-creator.discourse.org/theme/awesomerobot/discourse-category-banners",
|
||||||
description:
|
description:
|
||||||
"Show banners on category pages using your existing category details.",
|
"Show banners on category pages using your existing category details.",
|
||||||
meta_url: "https://meta.discourse.org/t/discourse-category-banners/86241",
|
meta_url: "https://meta.discourse.org/t/discourse-category-banners/86241",
|
||||||
@ -71,7 +70,7 @@ export const POPULAR_THEMES = [
|
|||||||
{
|
{
|
||||||
name: "Kanban Board",
|
name: "Kanban Board",
|
||||||
value: "https://github.com/discourse/discourse-kanban-theme",
|
value: "https://github.com/discourse/discourse-kanban-theme",
|
||||||
preview: "https://discourse.theme-creator.io/theme/david/kanban",
|
preview: "https://theme-creator.discourse.org/theme/david/kanban",
|
||||||
description: "Display and organize topics using a Kanban board interface.",
|
description: "Display and organize topics using a Kanban board interface.",
|
||||||
meta_url:
|
meta_url:
|
||||||
"https://meta.discourse.org/t/kanban-board-theme-component/118164",
|
"https://meta.discourse.org/t/kanban-board-theme-component/118164",
|
||||||
@ -85,19 +84,10 @@ export const POPULAR_THEMES = [
|
|||||||
meta_url: "https://meta.discourse.org/t/hamburger-theme-selector/61210",
|
meta_url: "https://meta.discourse.org/t/hamburger-theme-selector/61210",
|
||||||
component: true,
|
component: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "Sidebar Theme Toggle",
|
|
||||||
value: "https://github.com/discourse/discourse-sidebar-theme-toggle",
|
|
||||||
description:
|
|
||||||
"Displays a theme selector in the sidebar menu’s footer provided there is more than one user-selectable theme.",
|
|
||||||
meta_url: "https://meta.discourse.org/t/sidebar-theme-toggle/242802",
|
|
||||||
component: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "Header Submenus",
|
name: "Header Submenus",
|
||||||
value: "https://github.com/discourse/discourse-header-submenus",
|
value: "https://github.com/discourse/discourse-header-submenus",
|
||||||
preview:
|
preview: "https://theme-creator.discourse.org/theme/Johani/header-submenus",
|
||||||
"https://discourse.theme-creator.io/theme/awesomerobot/header-submenus",
|
|
||||||
description: "Lets you build a header menu with submenus (dropdowns).",
|
description: "Lets you build a header menu with submenus (dropdowns).",
|
||||||
meta_url: "https://meta.discourse.org/t/header-submenus/94584",
|
meta_url: "https://meta.discourse.org/t/header-submenus/94584",
|
||||||
component: true,
|
component: true,
|
||||||
@ -114,7 +104,7 @@ export const POPULAR_THEMES = [
|
|||||||
{
|
{
|
||||||
name: "Easy Responsive Footer",
|
name: "Easy Responsive Footer",
|
||||||
value: "https://github.com/discourse/Discourse-easy-footer",
|
value: "https://github.com/discourse/Discourse-easy-footer",
|
||||||
preview: "https://discourse.theme-creator.io/theme/Johani/easy-footer",
|
preview: "https://theme-creator.discourse.org/theme/Johani/easy-footer",
|
||||||
description: "Add a fully responsive footer without writing any HTML.",
|
description: "Add a fully responsive footer without writing any HTML.",
|
||||||
meta_url: "https://meta.discourse.org/t/easy-responsive-footer/95818",
|
meta_url: "https://meta.discourse.org/t/easy-responsive-footer/95818",
|
||||||
component: true,
|
component: true,
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: require("./package").name,
|
||||||
|
};
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "discourse-ensure-deprecation-order",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A dummy addon which ensures ember-cli-deprecation-workflow is loaded before @ember/jquery",
|
||||||
|
"author": "Discourse",
|
||||||
|
"license": "GPL-2.0-only",
|
||||||
|
"keywords": [
|
||||||
|
"ember-addon"
|
||||||
|
],
|
||||||
|
"ember-addon": {
|
||||||
|
"before": "@ember/jquery",
|
||||||
|
"after": "ember-cli-deprecation-workflow"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -30,7 +30,7 @@
|
|||||||
/>
|
/>
|
||||||
<TopicStatus @topic={{t}} @disableActions={{true}} />
|
<TopicStatus @topic={{t}} @disableActions={{true}} />
|
||||||
<span class="topic-title">
|
<span class="topic-title">
|
||||||
{{replace-emoji t.title}}
|
{{replace-emoji t.fancy_title}}
|
||||||
</span>
|
</span>
|
||||||
<span class="topic-categories">
|
<span class="topic-categories">
|
||||||
{{bound-category-link
|
{{bound-category-link
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import { alias } from "@ember/object/computed";
|
|||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
import { escape } from "pretty-text/sanitizer";
|
|
||||||
|
|
||||||
const TITLES = {
|
const TITLES = {
|
||||||
[PRIVATE_MESSAGE]: "topic.private_message",
|
[PRIVATE_MESSAGE]: "topic.private_message",
|
||||||
@ -85,9 +84,7 @@ export default Component.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
_formatReplyToUserPost(avatar, link) {
|
_formatReplyToUserPost(avatar, link) {
|
||||||
const htmlLink = `<a class="user-link" href="${link.href}">${escape(
|
const htmlLink = `<a class="user-link" href="${link.href}">${link.anchor}</a>`;
|
||||||
link.anchor
|
|
||||||
)}</a>`;
|
|
||||||
return htmlSafe(`${avatar}${htmlLink}`);
|
return htmlSafe(`${avatar}${htmlLink}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { TextArea } from "@ember/legacy-built-in-components";
|
import TextArea from "@ember/component/text-area";
|
||||||
|
|
||||||
export default TextArea.extend({
|
export default TextArea.extend({
|
||||||
attributeBindings: ["aria-label"],
|
attributeBindings: ["aria-label"],
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { observes, on } from "discourse-common/utils/decorators";
|
import { observes, on } from "discourse-common/utils/decorators";
|
||||||
import { TextArea } from "@ember/legacy-built-in-components";
|
import TextArea from "@ember/component/text-area";
|
||||||
import autosize from "discourse/lib/autosize";
|
import autosize from "discourse/lib/autosize";
|
||||||
import { schedule } from "@ember/runloop";
|
import { schedule } from "@ember/runloop";
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { cancel, next } from "@ember/runloop";
|
import { cancel, next } from "@ember/runloop";
|
||||||
import { isLTR, isRTL, siteDir } from "discourse/lib/text-direction";
|
import { isLTR, isRTL, siteDir } from "discourse/lib/text-direction";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { TextField } from "@ember/legacy-built-in-components";
|
import TextField from "@ember/component/text-field";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import discourseDebounce from "discourse-common/lib/debounce";
|
import discourseDebounce from "discourse-common/lib/debounce";
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import { emojiUnescape } from "discourse/lib/text";
|
import { emojiUnescape } from "discourse/lib/text";
|
||||||
import { htmlSafe, isHTMLSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
import { registerUnbound } from "discourse-common/lib/helpers";
|
import { registerUnbound } from "discourse-common/lib/helpers";
|
||||||
import { escapeExpression } from "discourse/lib/utilities";
|
|
||||||
|
|
||||||
registerUnbound("replace-emoji", (text, options) => {
|
registerUnbound("replace-emoji", (text, options) => {
|
||||||
text = isHTMLSafe(text) ? text.toString() : escapeExpression(text);
|
|
||||||
return htmlSafe(emojiUnescape(text, options));
|
return htmlSafe(emojiUnescape(text, options));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
import TextField from "@ember/component/text-field";
|
||||||
|
import TextArea from "@ember/component/text-area";
|
||||||
|
let initializedOnce = false;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ember-input-component-extensions",
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
if (initializedOnce) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField.reopen({
|
||||||
|
attributeBindings: ["aria-describedby", "aria-invalid"],
|
||||||
|
});
|
||||||
|
TextArea.reopen({
|
||||||
|
attributeBindings: ["aria-describedby", "aria-invalid"],
|
||||||
|
});
|
||||||
|
|
||||||
|
initializedOnce = true;
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -2,7 +2,6 @@
|
|||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import { actionModifier } from "./ember-action-modifier";
|
import { actionModifier } from "./ember-action-modifier";
|
||||||
import Ember from "ember";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Classic Ember components (i.e. "@ember/component") rely upon "event
|
* Classic Ember components (i.e. "@ember/component") rely upon "event
|
||||||
|
|||||||
@ -1,46 +0,0 @@
|
|||||||
import { assert, deprecate } from "@ember/debug";
|
|
||||||
import EmberObject from "@ember/object";
|
|
||||||
import Component from "@ember/component";
|
|
||||||
import jQuery from "jquery";
|
|
||||||
|
|
||||||
let done = false;
|
|
||||||
|
|
||||||
// Adapted from https://github.com/emberjs/ember-jquery/blob/master/vendor/jquery/component.dollar.js
|
|
||||||
// but implemented in a module to avoid transpiled version triggering the Ember Global deprecation.
|
|
||||||
// To be dropped when we remove the jquery integration as part of the 4.x update.
|
|
||||||
export default {
|
|
||||||
name: "deprecate-jquery-integration",
|
|
||||||
|
|
||||||
initialize() {
|
|
||||||
if (done) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
EmberObject.reopen.call(Component, {
|
|
||||||
$(sel) {
|
|
||||||
assert(
|
|
||||||
"You cannot access this.$() on a component with `tagName: ''` specified.",
|
|
||||||
this.tagName !== ""
|
|
||||||
);
|
|
||||||
|
|
||||||
deprecate(
|
|
||||||
"Using this.$() in a component has been deprecated, consider using this.element",
|
|
||||||
false,
|
|
||||||
{
|
|
||||||
id: "ember-views.curly-components.jquery-element",
|
|
||||||
since: "3.4.0",
|
|
||||||
until: "4.0.0",
|
|
||||||
url: "https://emberjs.com/deprecations/v3.x#toc_jquery-apis",
|
|
||||||
for: "ember-source",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.element) {
|
|
||||||
return sel ? jQuery(sel, this.element) : jQuery(this.element);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
done = true;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -94,8 +94,8 @@
|
|||||||
@value={{this.accountName}}
|
@value={{this.accountName}}
|
||||||
@id="new-account-name"
|
@id="new-account-name"
|
||||||
@class={{value-entered this.accountName}}
|
@class={{value-entered this.accountName}}
|
||||||
aria-describedby="fullname-validation"
|
@aria-describedby="fullname-validation"
|
||||||
aria-invalid={{this.nameValidation.failed}}
|
@aria-invalid={{this.nameValidation.failed}}
|
||||||
/>
|
/>
|
||||||
<label class="alt-placeholder" for="new-account-name">
|
<label class="alt-placeholder" for="new-account-name">
|
||||||
{{i18n "user.name.title"}}
|
{{i18n "user.name.title"}}
|
||||||
@ -134,8 +134,8 @@
|
|||||||
id="new-account-password"
|
id="new-account-password"
|
||||||
@autocomplete="current-password"
|
@autocomplete="current-password"
|
||||||
@capsLockOn={{this.capsLockOn}}
|
@capsLockOn={{this.capsLockOn}}
|
||||||
aria-describedby="password-validation"
|
@aria-describedby="password-validation"
|
||||||
aria-invalid={{this.passwordValidation.failed}}
|
@aria-invalid={{this.passwordValidation.failed}}
|
||||||
/>
|
/>
|
||||||
<label class="alt-placeholder" for="new-account-password">
|
<label class="alt-placeholder" for="new-account-password">
|
||||||
{{i18n "user.password.title"}}
|
{{i18n "user.password.title"}}
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
data-title={{result.fancy_title}}
|
data-title={{result.fancy_title}}
|
||||||
>
|
>
|
||||||
<TopicStatus @topic={{result}} @disableActions={{true}} />
|
<TopicStatus @topic={{result}} @disableActions={{true}} />
|
||||||
{{replace-emoji result.title}}
|
{{replace-emoji result.fancy_title}}
|
||||||
<div class="search-category">
|
<div class="search-category">
|
||||||
{{#if result.category.parentCategory}}
|
{{#if result.category.parentCategory}}
|
||||||
{{category-link result.category.parentCategory}}
|
{{category-link result.category.parentCategory}}
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
href={{rt.relative_url}}
|
href={{rt.relative_url}}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>{{replace-emoji rt.title}}</a>
|
>{{replace-emoji rt.fancy_title}}</a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="reviewable-count">
|
<td class="reviewable-count">
|
||||||
|
|||||||
@ -3,6 +3,9 @@ globalThis.deprecationWorkflow.config = {
|
|||||||
// We're using RAISE_ON_DEPRECATION in environment.js instead of
|
// We're using RAISE_ON_DEPRECATION in environment.js instead of
|
||||||
// `throwOnUnhandled` here since it is easier to toggle.
|
// `throwOnUnhandled` here since it is easier to toggle.
|
||||||
workflow: [
|
workflow: [
|
||||||
|
{ handler: "silence", matchId: "ember-global" },
|
||||||
|
{ handler: "silence", matchId: "ember.built-in-components.reopen" },
|
||||||
|
{ handler: "silence", matchId: "ember.built-in-components.import" },
|
||||||
{ handler: "silence", matchId: "implicit-injections" },
|
{ handler: "silence", matchId: "implicit-injections" },
|
||||||
{ handler: "silence", matchId: "route-render-template" },
|
{ handler: "silence", matchId: "route-render-template" },
|
||||||
{ handler: "silence", matchId: "routing.transition-methods" },
|
{ handler: "silence", matchId: "routing.transition-methods" },
|
||||||
|
|||||||
@ -163,16 +163,6 @@ module.exports = function (defaults) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ember/jquery introduces a shim which triggers the ember-global deprecation.
|
|
||||||
// We remove that shim, and re-implement ourselves in the deprecate-jquery-integration pre-initializer
|
|
||||||
const vendorScripts = app._scriptOutputFiles["/assets/vendor.js"];
|
|
||||||
const componentDollarShimIndex = vendorScripts.indexOf(
|
|
||||||
"vendor/jquery/component.dollar.js"
|
|
||||||
);
|
|
||||||
if (componentDollarShimIndex) {
|
|
||||||
vendorScripts.splice(componentDollarShimIndex, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// WARNING: We should only import scripts here if they are not in NPM.
|
// WARNING: We should only import scripts here if they are not in NPM.
|
||||||
// For example: our very specific version of bootstrap-modal.
|
// For example: our very specific version of bootstrap-modal.
|
||||||
app.import(vendorJs + "bootbox.js");
|
app.import(vendorJs + "bootbox.js");
|
||||||
|
|||||||
@ -23,7 +23,6 @@
|
|||||||
"@discourse/virtual-dom": "^2.1.2-0",
|
"@discourse/virtual-dom": "^2.1.2-0",
|
||||||
"@ember-compat/tracked-built-ins": "^0.9.1",
|
"@ember-compat/tracked-built-ins": "^0.9.1",
|
||||||
"@ember/jquery": "^2.0.0",
|
"@ember/jquery": "^2.0.0",
|
||||||
"@ember/legacy-built-in-components": "^0.4.2",
|
|
||||||
"@ember/optional-features": "^2.0.0",
|
"@ember/optional-features": "^2.0.0",
|
||||||
"@ember/render-modifiers": "^2.0.5",
|
"@ember/render-modifiers": "^2.0.5",
|
||||||
"@ember/test-helpers": "^2.9.3",
|
"@ember/test-helpers": "^2.9.3",
|
||||||
@ -47,6 +46,7 @@
|
|||||||
"deepmerge": "^4.3.0",
|
"deepmerge": "^4.3.0",
|
||||||
"dialog-holder": "1.0.0",
|
"dialog-holder": "1.0.0",
|
||||||
"discourse-common": "1.0.0",
|
"discourse-common": "1.0.0",
|
||||||
|
"discourse-ensure-deprecation-order": "1.0.0",
|
||||||
"discourse-hbr": "1.0.0",
|
"discourse-hbr": "1.0.0",
|
||||||
"discourse-plugins": "1.0.0",
|
"discourse-plugins": "1.0.0",
|
||||||
"discourse-widget-hbs": "1.0.0",
|
"discourse-widget-hbs": "1.0.0",
|
||||||
|
|||||||
@ -3,13 +3,6 @@
|
|||||||
throw "Unsupported browser detected";
|
throw "Unsupported browser detected";
|
||||||
}
|
}
|
||||||
|
|
||||||
// In Ember 3.28, the `ember` package is responsible for configuring `Helper.helper`,
|
|
||||||
// so we need to require('ember') before setting up any helpers.
|
|
||||||
// https://github.com/emberjs/ember.js/blob/744e536d37/packages/ember/index.js#L493-L493
|
|
||||||
// In modern Ember, the Helper.helper definition has moved to the helper module itself
|
|
||||||
// https://github.com/emberjs/ember.js/blob/0c5518ea7b/packages/%40ember/-internals/glimmer/lib/helper.ts#L134-L138
|
|
||||||
require("ember");
|
|
||||||
|
|
||||||
window.__widget_helpers = require("discourse-widget-hbs/helpers").default;
|
window.__widget_helpers = require("discourse-widget-hbs/helpers").default;
|
||||||
|
|
||||||
// TODO: Eliminate this global
|
// TODO: Eliminate this global
|
||||||
|
|||||||
@ -518,11 +518,11 @@ acceptance("Prioritize Full Name", function (needs) {
|
|||||||
|
|
||||||
test("Reply to post use full name", async function (assert) {
|
test("Reply to post use full name", async function (assert) {
|
||||||
await visit("/t/short-topic-with-two-posts/54079");
|
await visit("/t/short-topic-with-two-posts/54079");
|
||||||
await click("article#post_3 button.reply");
|
await click("article#post_2 button.reply");
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
query(".action-title .user-link").innerHTML.trim(),
|
query(".action-title .user-link").innerText.trim(),
|
||||||
"<h1>Tim Stone</h1>"
|
"james, john, the third"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -139,7 +139,7 @@ acceptance("Search - Anonymous", function (needs) {
|
|||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
queryAll(contextSelector)[0].firstChild.textContent.trim(),
|
queryAll(contextSelector)[0].firstChild.textContent.trim(),
|
||||||
`${I18n.t("search.in")} important`,
|
`${I18n.t("search.in")} test`,
|
||||||
"contextual tag search is first available option with no term"
|
"contextual tag search is first available option with no term"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ acceptance("Search - Anonymous", function (needs) {
|
|||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
queryAll(contextSelector)[1].firstChild.textContent.trim(),
|
queryAll(contextSelector)[1].firstChild.textContent.trim(),
|
||||||
`smth ${I18n.t("search.in")} important`,
|
`smth ${I18n.t("search.in")} test`,
|
||||||
"tag-scoped search is second available option"
|
"tag-scoped search is second available option"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -4077,7 +4077,7 @@ export default {
|
|||||||
tags: [
|
tags: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "important",
|
name: "test",
|
||||||
topic_count: 2,
|
topic_count: 2,
|
||||||
staff: false,
|
staff: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -6497,7 +6497,7 @@ export default {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 419,
|
id: 419,
|
||||||
name: "<h1>Tim Stone</h1>",
|
name: "Tim Stone",
|
||||||
username: "tms",
|
username: "tms",
|
||||||
avatar_template: "/letter_avatar_proxy/v4/letter/t/3be4f8/{size}.png",
|
avatar_template: "/letter_avatar_proxy/v4/letter/t/3be4f8/{size}.png",
|
||||||
uploaded_avatar_id: 40181,
|
uploaded_avatar_id: 40181,
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
import { module, test } from "qunit";
|
|
||||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
|
||||||
import { render } from "@ember/test-helpers";
|
|
||||||
import { hbs } from "ember-cli-htmlbars";
|
|
||||||
|
|
||||||
module("Integration | Helper | replace-emoji", function (hooks) {
|
|
||||||
setupRenderingTest(hooks);
|
|
||||||
|
|
||||||
test("it replaces the emoji", async function (assert) {
|
|
||||||
await render(hbs`<span>{{replace-emoji "some text :heart:"}}</span>`);
|
|
||||||
|
|
||||||
assert.dom(`span`).includesText("some text");
|
|
||||||
assert.dom(`.emoji[title="heart"]`).exists();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it escapes the text", async function (assert) {
|
|
||||||
await render(
|
|
||||||
hbs`<span>{{replace-emoji "<style>body: {background: red;}</style>"}}</span>`
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.dom(`span`).hasText("<style>body: {background: red;}</style>");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it renders html-safe text", async function (assert) {
|
|
||||||
await render(hbs`<span>{{replace-emoji (html-safe "safe text")}}</span>`);
|
|
||||||
|
|
||||||
assert.dom(`span`).hasText("safe text");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -6,6 +6,7 @@
|
|||||||
"dialog-holder",
|
"dialog-holder",
|
||||||
"discourse",
|
"discourse",
|
||||||
"discourse-common",
|
"discourse-common",
|
||||||
|
"discourse-ensure-deprecation-order",
|
||||||
"discourse-hbr",
|
"discourse-hbr",
|
||||||
"discourse-plugins",
|
"discourse-plugins",
|
||||||
"discourse-widget-hbs",
|
"discourse-widget-hbs",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<TopicStatus @topic={{this.item}} @disableActions={{true}} />
|
<TopicStatus @topic={{this.item}} @disableActions={{true}} />
|
||||||
<div class="topic-title">{{replace-emoji this.item.title}}</div>
|
<div class="topic-title">{{replace-emoji this.item.fancy_title}}</div>
|
||||||
<div class="topic-categories">
|
<div class="topic-categories">
|
||||||
{{bound-category-link
|
{{bound-category-link
|
||||||
this.item.category
|
this.item.category
|
||||||
|
|||||||
@ -1088,16 +1088,6 @@
|
|||||||
jquery "^3.5.0"
|
jquery "^3.5.0"
|
||||||
resolve "^1.15.1"
|
resolve "^1.15.1"
|
||||||
|
|
||||||
"@ember/legacy-built-in-components@^0.4.2":
|
|
||||||
version "0.4.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@ember/legacy-built-in-components/-/legacy-built-in-components-0.4.2.tgz#79a97d66153ff17909759b368b2a117bc9e168e5"
|
|
||||||
integrity sha512-rJulbyVQIVe1zEDQDqAQHechHy44DsS2qxO24+NmU/AYxwPFSzWC/OZNCDFSfLU+Y5BVd/00qjxF0pu7Nk+TNA==
|
|
||||||
dependencies:
|
|
||||||
"@embroider/macros" "^1.0.0"
|
|
||||||
ember-cli-babel "^7.26.6"
|
|
||||||
ember-cli-htmlbars "^5.7.1"
|
|
||||||
ember-cli-typescript "^4.1.0"
|
|
||||||
|
|
||||||
"@ember/optional-features@^2.0.0":
|
"@ember/optional-features@^2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@ember/optional-features/-/optional-features-2.0.0.tgz#c809abd5a27d5b0ef3c6de3941334ab6153313f0"
|
resolved "https://registry.yarnpkg.com/@ember/optional-features/-/optional-features-2.0.0.tgz#c809abd5a27d5b0ef3c6de3941334ab6153313f0"
|
||||||
@ -4009,22 +3999,6 @@ ember-cli-typescript@^2.0.2:
|
|||||||
stagehand "^1.0.0"
|
stagehand "^1.0.0"
|
||||||
walk-sync "^1.0.0"
|
walk-sync "^1.0.0"
|
||||||
|
|
||||||
ember-cli-typescript@^4.1.0:
|
|
||||||
version "4.2.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/ember-cli-typescript/-/ember-cli-typescript-4.2.1.tgz#54d08fc90318cc986f3ea562f93ce58a6cc4c24d"
|
|
||||||
integrity sha512-0iKTZ+/wH6UB/VTWKvGuXlmwiE8HSIGcxHamwNhEC5x1mN3z8RfvsFZdQWYUzIWFN2Tek0gmepGRPTwWdBYl/A==
|
|
||||||
dependencies:
|
|
||||||
ansi-to-html "^0.6.15"
|
|
||||||
broccoli-stew "^3.0.0"
|
|
||||||
debug "^4.0.0"
|
|
||||||
execa "^4.0.0"
|
|
||||||
fs-extra "^9.0.1"
|
|
||||||
resolve "^1.5.0"
|
|
||||||
rsvp "^4.8.1"
|
|
||||||
semver "^7.3.2"
|
|
||||||
stagehand "^1.0.0"
|
|
||||||
walk-sync "^2.2.0"
|
|
||||||
|
|
||||||
ember-cli-typescript@^5.0.0:
|
ember-cli-typescript@^5.0.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/ember-cli-typescript/-/ember-cli-typescript-5.1.0.tgz#460eb848564e29d64f2b36b2a75bbe98172b72a4"
|
resolved "https://registry.yarnpkg.com/ember-cli-typescript/-/ember-cli-typescript-5.1.0.tgz#460eb848564e29d64f2b36b2a75bbe98172b72a4"
|
||||||
|
|||||||
@ -155,7 +155,6 @@
|
|||||||
.quick-access-panel {
|
.quick-access-panel {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
padding: 0.75em;
|
padding: 0.75em;
|
||||||
padding-bottom: env(safe-area-inset-bottom, 0.75em);
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-width: 0; // makes sure menu tabs don't go off screen
|
min-width: 0; // makes sure menu tabs don't go off screen
|
||||||
@ -717,7 +716,9 @@ body.footer-nav-ipad {
|
|||||||
--100dvh: 100dvh;
|
--100dvh: 100dvh;
|
||||||
}
|
}
|
||||||
|
|
||||||
--base-height: calc(var(--100dvh) - var(--header-top));
|
--base-height: calc(
|
||||||
|
var(--100dvh) - var(--header-top) - env(safe-area-inset-bottom, 0px)
|
||||||
|
);
|
||||||
|
|
||||||
height: var(--base-height);
|
height: var(--base-height);
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,6 @@
|
|||||||
border-top: 1.5px solid var(--primary-low);
|
border-top: 1.5px solid var(--primary-low);
|
||||||
background: var(--primary-very-low);
|
background: var(--primary-very-low);
|
||||||
padding: 0.5em 0.8em;
|
padding: 0.5em 0.8em;
|
||||||
padding-bottom: env(safe-area-inset-bottom, 0.5em);
|
|
||||||
&:before {
|
&:before {
|
||||||
// fade to make scroll more apparent
|
// fade to make scroll more apparent
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@ -34,14 +34,6 @@ class Admin::BackupsController < Admin::AdminController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
RateLimiter.new(
|
|
||||||
current_user,
|
|
||||||
"max-backups-per-minute",
|
|
||||||
1,
|
|
||||||
1.minute,
|
|
||||||
apply_limit_to_staff: true,
|
|
||||||
).performed!
|
|
||||||
|
|
||||||
opts = {
|
opts = {
|
||||||
publish_to_message_bus: true,
|
publish_to_message_bus: true,
|
||||||
with_uploads: params.fetch(:with_uploads) == "true",
|
with_uploads: params.fetch(:with_uploads) == "true",
|
||||||
|
|||||||
@ -32,14 +32,20 @@ class ApplicationRequest < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.write_cache!(req_type, count, date)
|
def self.write_cache!(req_type, count, date)
|
||||||
|
id = req_id(date, req_type)
|
||||||
|
where(id: id).update_all(["count = count + ?", count])
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.req_id(date, req_type, retries = 0)
|
||||||
req_type_id = req_types[req_type]
|
req_type_id = req_types[req_type]
|
||||||
|
|
||||||
DB.exec(<<~SQL, date: date, req_type_id: req_type_id, count: count)
|
create_or_find_by!(date: date, req_type: req_type_id).id
|
||||||
INSERT INTO application_requests (date, req_type, count)
|
rescue StandardError # primary key violation
|
||||||
VALUES (:date, :req_type_id, :count)
|
if retries == 0
|
||||||
ON CONFLICT (date, req_type)
|
req_id(date, req_type, 1)
|
||||||
DO UPDATE SET count = application_requests.count + excluded.count
|
else
|
||||||
SQL
|
raise
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.stats
|
def self.stats
|
||||||
|
|||||||
@ -46,8 +46,7 @@ class Bookmark < ActiveRecord::Base
|
|||||||
validates :name, length: { maximum: 100 }
|
validates :name, length: { maximum: 100 }
|
||||||
|
|
||||||
def registered_bookmarkable
|
def registered_bookmarkable
|
||||||
type = Bookmark.polymorphic_class_for(self.bookmarkable_type).name
|
Bookmark.registered_bookmarkable_from_type(self.bookmarkable_type)
|
||||||
Bookmark.registered_bookmarkable_from_type(type)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def polymorphic_columns_present
|
def polymorphic_columns_present
|
||||||
|
|||||||
@ -129,7 +129,7 @@ class Reviewable < ActiveRecord::Base
|
|||||||
update_args = {
|
update_args = {
|
||||||
status: statuses[:pending],
|
status: statuses[:pending],
|
||||||
id: target.id,
|
id: target.id,
|
||||||
type: target.class.sti_name,
|
type: target.class.name,
|
||||||
potential_spam: potential_spam == true ? true : nil,
|
potential_spam: potential_spam == true ? true : nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1047,6 +1047,7 @@ class Topic < ActiveRecord::Base
|
|||||||
skip_validations: true,
|
skip_validations: true,
|
||||||
custom_fields: opts[:custom_fields],
|
custom_fields: opts[:custom_fields],
|
||||||
import_mode: opts[:import_mode],
|
import_mode: opts[:import_mode],
|
||||||
|
created_at: opts[:created_at],
|
||||||
)
|
)
|
||||||
|
|
||||||
if (new_post = creator.create) && new_post.present?
|
if (new_post = creator.create) && new_post.present?
|
||||||
|
|||||||
@ -118,6 +118,7 @@ class UserAvatar < ActiveRecord::Base
|
|||||||
max_file_size: SiteSetting.max_image_size_kb.kilobytes,
|
max_file_size: SiteSetting.max_image_size_kb.kilobytes,
|
||||||
tmp_file_name: "sso-avatar",
|
tmp_file_name: "sso-avatar",
|
||||||
follow_redirect: true,
|
follow_redirect: true,
|
||||||
|
skip_rate_limit: !!options&.fetch(:skip_rate_limit),
|
||||||
)
|
)
|
||||||
|
|
||||||
return unless tempfile
|
return unless tempfile
|
||||||
|
|||||||
@ -117,6 +117,7 @@ class UserProfile < ActiveRecord::Base
|
|||||||
max_file_size: SiteSetting.max_image_size_kb.kilobytes,
|
max_file_size: SiteSetting.max_image_size_kb.kilobytes,
|
||||||
tmp_file_name: "sso-profile-background",
|
tmp_file_name: "sso-profile-background",
|
||||||
follow_redirect: true,
|
follow_redirect: true,
|
||||||
|
skip_rate_limit: true,
|
||||||
)
|
)
|
||||||
|
|
||||||
return unless tempfile
|
return unless tempfile
|
||||||
|
|||||||
@ -9,7 +9,8 @@ Discourse::Application.configure do
|
|||||||
config.cache_classes = false
|
config.cache_classes = false
|
||||||
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
|
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
|
||||||
|
|
||||||
config.eager_load = ENV["DISCOURSE_ZEITWERK_EAGER_LOAD"] == "1"
|
# Log error messages when you accidentally call methods on nil.
|
||||||
|
config.eager_load = false
|
||||||
|
|
||||||
# Use the schema_cache.yml file generated during db:migrate (via db:schema:cache:dump)
|
# Use the schema_cache.yml file generated during db:migrate (via db:schema:cache:dump)
|
||||||
config.active_record.use_schema_cache_dump = true
|
config.active_record.use_schema_cache_dump = true
|
||||||
|
|||||||
@ -44,7 +44,7 @@ Discourse::Application.configure do
|
|||||||
config.assets.compile = true
|
config.assets.compile = true
|
||||||
config.assets.digest = false
|
config.assets.digest = false
|
||||||
|
|
||||||
config.eager_load = ENV["DISCOURSE_ZEITWERK_EAGER_LOAD"] == "1"
|
config.eager_load = false
|
||||||
|
|
||||||
if ENV["RAILS_ENABLE_TEST_LOG"]
|
if ENV["RAILS_ENABLE_TEST_LOG"]
|
||||||
config.logger = Logger.new(STDOUT)
|
config.logger = Logger.new(STDOUT)
|
||||||
|
|||||||
@ -1864,7 +1864,7 @@ en:
|
|||||||
composer_media_optimization_image_bytes_optimization_threshold: "Minimum image file size to trigger client-side optimization"
|
composer_media_optimization_image_bytes_optimization_threshold: "Minimum image file size to trigger client-side optimization"
|
||||||
composer_media_optimization_image_resize_dimensions_threshold: "Minimum image width to trigger client-side resize"
|
composer_media_optimization_image_resize_dimensions_threshold: "Minimum image width to trigger client-side resize"
|
||||||
composer_media_optimization_image_resize_width_target: "Images with widths larger than `composer_media_optimization_image_dimensions_resize_threshold` will be resized to this width. Must be >= than `composer_media_optimization_image_dimensions_resize_threshold`."
|
composer_media_optimization_image_resize_width_target: "Images with widths larger than `composer_media_optimization_image_dimensions_resize_threshold` will be resized to this width. Must be >= than `composer_media_optimization_image_dimensions_resize_threshold`."
|
||||||
composer_media_optimization_image_encode_quality: "JPG encode quality used in the re-encode process."
|
composer_media_optimization_image_encode_quality: "JPEG encode quality used in the re-encode process."
|
||||||
|
|
||||||
min_ratio_to_crop: "Ratio used to crop tall images. Enter the result of width / height."
|
min_ratio_to_crop: "Ratio used to crop tall images. Enter the result of width / height."
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ require "file_store/s3_store"
|
|||||||
|
|
||||||
module BackupRestore
|
module BackupRestore
|
||||||
class Backuper
|
class Backuper
|
||||||
attr_reader :success, :store
|
attr_reader :success
|
||||||
|
|
||||||
def initialize(user_id, opts = {})
|
def initialize(user_id, opts = {})
|
||||||
@user_id = user_id
|
@user_id = user_id
|
||||||
@ -46,6 +46,7 @@ module BackupRestore
|
|||||||
rescue Exception => ex
|
rescue Exception => ex
|
||||||
log "EXCEPTION: " + ex.message
|
log "EXCEPTION: " + ex.message
|
||||||
log ex.backtrace.join("\n")
|
log ex.backtrace.join("\n")
|
||||||
|
@success = false
|
||||||
else
|
else
|
||||||
@success = true
|
@success = true
|
||||||
@backup_filename
|
@backup_filename
|
||||||
@ -54,7 +55,7 @@ module BackupRestore
|
|||||||
clean_up
|
clean_up
|
||||||
notify_user
|
notify_user
|
||||||
log "Finished!"
|
log "Finished!"
|
||||||
publish_completion
|
publish_completion(@success)
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
@ -336,12 +337,12 @@ module BackupRestore
|
|||||||
end
|
end
|
||||||
|
|
||||||
def upload_archive
|
def upload_archive
|
||||||
return unless store.remote?
|
return unless @store.remote?
|
||||||
|
|
||||||
log "Uploading archive..."
|
log "Uploading archive..."
|
||||||
content_type = MiniMime.lookup_by_filename(@backup_filename).content_type
|
content_type = MiniMime.lookup_by_filename(@backup_filename).content_type
|
||||||
archive_path = File.join(@archive_directory, @backup_filename)
|
archive_path = File.join(@archive_directory, @backup_filename)
|
||||||
store.upload_file(@backup_filename, archive_path, content_type)
|
@store.upload_file(@backup_filename, archive_path, content_type)
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_create_hook
|
def after_create_hook
|
||||||
@ -353,16 +354,16 @@ module BackupRestore
|
|||||||
return if Rails.env.development?
|
return if Rails.env.development?
|
||||||
|
|
||||||
log "Deleting old backups..."
|
log "Deleting old backups..."
|
||||||
store.delete_old
|
@store.delete_old
|
||||||
rescue => ex
|
rescue => ex
|
||||||
log "Something went wrong while deleting old backups.", ex
|
log "Something went wrong while deleting old backups.", ex
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_user
|
def notify_user
|
||||||
return if success && @user.id == Discourse::SYSTEM_USER_ID
|
return if @success && @user.id == Discourse::SYSTEM_USER_ID
|
||||||
|
|
||||||
log "Notifying '#{@user.username}' of the end of the backup..."
|
log "Notifying '#{@user.username}' of the end of the backup..."
|
||||||
status = success ? :backup_succeeded : :backup_failed
|
status = @success ? :backup_succeeded : :backup_failed
|
||||||
|
|
||||||
logs = Discourse::Utils.logs_markdown(@logs, user: @user)
|
logs = Discourse::Utils.logs_markdown(@logs, user: @user)
|
||||||
post = SystemMessage.create_from_system_user(@user, status, logs: logs)
|
post = SystemMessage.create_from_system_user(@user, status, logs: logs)
|
||||||
@ -377,11 +378,11 @@ module BackupRestore
|
|||||||
delete_uploaded_archive
|
delete_uploaded_archive
|
||||||
remove_tar_leftovers
|
remove_tar_leftovers
|
||||||
mark_backup_as_not_running
|
mark_backup_as_not_running
|
||||||
refresh_disk_space if success
|
refresh_disk_space
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_uploaded_archive
|
def delete_uploaded_archive
|
||||||
return unless store.remote?
|
return unless @store.remote?
|
||||||
|
|
||||||
archive_path = File.join(@archive_directory, @backup_filename)
|
archive_path = File.join(@archive_directory, @backup_filename)
|
||||||
|
|
||||||
@ -395,7 +396,7 @@ module BackupRestore
|
|||||||
|
|
||||||
def refresh_disk_space
|
def refresh_disk_space
|
||||||
log "Refreshing disk stats..."
|
log "Refreshing disk stats..."
|
||||||
store.reset_cache
|
@store.reset_cache
|
||||||
rescue => ex
|
rescue => ex
|
||||||
log "Something went wrong while refreshing disk stats.", ex
|
log "Something went wrong while refreshing disk stats.", ex
|
||||||
end
|
end
|
||||||
@ -449,7 +450,7 @@ module BackupRestore
|
|||||||
@logs << "[#{timestamp}] #{message}"
|
@logs << "[#{timestamp}] #{message}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def publish_completion
|
def publish_completion(success)
|
||||||
if success
|
if success
|
||||||
log("[SUCCESS]")
|
log("[SUCCESS]")
|
||||||
DiscourseEvent.trigger(:backup_complete, logs: @logs, ticket: @ticket)
|
DiscourseEvent.trigger(:backup_complete, logs: @logs, ticket: @ticket)
|
||||||
|
|||||||
@ -193,7 +193,7 @@ module CookedProcessorMixin
|
|||||||
if upload && upload.width && upload.width > 0
|
if upload && upload.width && upload.width > 0
|
||||||
@size_cache[url] = [upload.width, upload.height]
|
@size_cache[url] = [upload.width, upload.height]
|
||||||
else
|
else
|
||||||
@size_cache[url] = FinalDestination::FastImage.size(absolute_url)
|
@size_cache[url] = FastImage.size(absolute_url)
|
||||||
end
|
end
|
||||||
rescue Zlib::BufError, URI::Error, OpenSSL::SSL::SSLError
|
rescue Zlib::BufError, URI::Error, OpenSSL::SSL::SSLError
|
||||||
# FastImage.size raises BufError for some gifs, leave it.
|
# FastImage.size raises BufError for some gifs, leave it.
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class FinalDestination::FastImage < ::FastImage
|
|
||||||
def initialize(url, options = {})
|
|
||||||
uri = URI(normalized_url(url))
|
|
||||||
options.merge!(http_header: { "Host" => uri.hostname })
|
|
||||||
uri.hostname = resolved_ip(uri)
|
|
||||||
|
|
||||||
super(uri.to_s, options)
|
|
||||||
rescue FinalDestination::SSRFDetector::DisallowedIpError, SocketError, Timeout::Error
|
|
||||||
super("")
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def resolved_ip(uri)
|
|
||||||
FinalDestination::SSRFDetector.lookup_and_filter_ips(uri.hostname).first
|
|
||||||
end
|
|
||||||
|
|
||||||
def normalized_url(uri)
|
|
||||||
UrlHelper.normalized_encode(uri)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -7,47 +7,18 @@ class FinalDestination
|
|||||||
class LookupFailedError < SocketError
|
class LookupFailedError < SocketError
|
||||||
end
|
end
|
||||||
|
|
||||||
# This is a list of private IPv4 IP ranges that are not allowed to be globally reachable as given by
|
def self.standard_private_ranges
|
||||||
# https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml.
|
@private_ranges ||= [
|
||||||
PRIVATE_IPV4_RANGES = [
|
IPAddr.new("0.0.0.0/8"),
|
||||||
IPAddr.new("0.0.0.0/8"),
|
IPAddr.new("127.0.0.1"),
|
||||||
IPAddr.new("10.0.0.0/8"),
|
IPAddr.new("172.16.0.0/12"),
|
||||||
IPAddr.new("100.64.0.0/10"),
|
IPAddr.new("192.168.0.0/16"),
|
||||||
IPAddr.new("127.0.0.0/8"),
|
IPAddr.new("10.0.0.0/8"),
|
||||||
IPAddr.new("169.254.0.0/16"),
|
IPAddr.new("::1"),
|
||||||
IPAddr.new("172.16.0.0/12"),
|
IPAddr.new("fc00::/7"),
|
||||||
IPAddr.new("192.0.0.0/24"),
|
IPAddr.new("fe80::/10"),
|
||||||
IPAddr.new("192.0.0.0/29"),
|
]
|
||||||
IPAddr.new("192.0.0.8/32"),
|
end
|
||||||
IPAddr.new("192.0.0.170/32"),
|
|
||||||
IPAddr.new("192.0.0.171/32"),
|
|
||||||
IPAddr.new("192.0.2.0/24"),
|
|
||||||
IPAddr.new("192.168.0.0/16"),
|
|
||||||
IPAddr.new("192.175.48.0/24"),
|
|
||||||
IPAddr.new("198.18.0.0/15"),
|
|
||||||
IPAddr.new("198.51.100.0/24"),
|
|
||||||
IPAddr.new("203.0.113.0/24"),
|
|
||||||
IPAddr.new("240.0.0.0/4"),
|
|
||||||
IPAddr.new("255.255.255.255/32"),
|
|
||||||
]
|
|
||||||
|
|
||||||
# This is a list of private IPv6 IP ranges that are not allowed to be globally reachable as given by
|
|
||||||
# https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml.
|
|
||||||
#
|
|
||||||
# ::ffff:0:0/96 is excluded from the list because it is used for IPv4-mapped IPv6 addresses which is something we want to allow.
|
|
||||||
PRIVATE_IPV6_RANGES = [
|
|
||||||
IPAddr.new("::1/128"),
|
|
||||||
IPAddr.new("::/128"),
|
|
||||||
IPAddr.new("64:ff9b:1::/48"),
|
|
||||||
IPAddr.new("100::/64"),
|
|
||||||
IPAddr.new("2001::/23"),
|
|
||||||
IPAddr.new("2001:2::/48"),
|
|
||||||
IPAddr.new("2001:db8::/32"),
|
|
||||||
IPAddr.new("fc00::/7"),
|
|
||||||
IPAddr.new("fe80::/10"),
|
|
||||||
]
|
|
||||||
|
|
||||||
PRIVATE_IP_RANGES = PRIVATE_IPV4_RANGES + PRIVATE_IPV6_RANGES
|
|
||||||
|
|
||||||
def self.blocked_ip_blocks
|
def self.blocked_ip_blocks
|
||||||
SiteSetting
|
SiteSetting
|
||||||
@ -83,9 +54,10 @@ class FinalDestination
|
|||||||
|
|
||||||
def self.ip_allowed?(ip)
|
def self.ip_allowed?(ip)
|
||||||
ip = ip.is_a?(IPAddr) ? ip : IPAddr.new(ip)
|
ip = ip.is_a?(IPAddr) ? ip : IPAddr.new(ip)
|
||||||
ip = ip.native
|
|
||||||
|
|
||||||
return false if ip_in_ranges?(ip, blocked_ip_blocks) || ip_in_ranges?(ip, PRIVATE_IP_RANGES)
|
if ip_in_ranges?(ip, blocked_ip_blocks) || ip_in_ranges?(ip, standard_private_ranges)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# This is a patch to avoid the direct use of `Net::HTTP` in the `webpush` gem and instead rely on `FinalDestination::HTTP`
|
|
||||||
# which protects us from DNS rebinding attacks as well as server side forgery requests.
|
|
||||||
#
|
|
||||||
# This patch is considered temporary until we can decide on a longer term solution. In the meantime, we need to patch
|
|
||||||
# the SSRF vulnerability being exposed by this gem.
|
|
||||||
module WebPushPatch
|
|
||||||
def perform
|
|
||||||
http = FinalDestination::HTTP.new(uri.host, uri.port, *proxy_options)
|
|
||||||
http.use_ssl = true
|
|
||||||
http.ssl_timeout = @options[:ssl_timeout] unless @options[:ssl_timeout].nil?
|
|
||||||
http.open_timeout = @options[:open_timeout] unless @options[:open_timeout].nil?
|
|
||||||
http.read_timeout = @options[:read_timeout] unless @options[:read_timeout].nil?
|
|
||||||
|
|
||||||
req = FinalDestination::HTTP::Post.new(uri.request_uri, headers)
|
|
||||||
req.body = body
|
|
||||||
|
|
||||||
resp = http.request(req)
|
|
||||||
verify_response(resp)
|
|
||||||
|
|
||||||
resp
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
klass = defined?(WebPush) ? WebPush : Webpush
|
|
||||||
klass::Request.prepend(WebPushPatch)
|
|
||||||
@ -213,10 +213,7 @@ task "docker:test" do
|
|||||||
@good &&= run_or_fail("bundle exec rspec #{params.join(" ")}".strip)
|
@good &&= run_or_fail("bundle exec rspec #{params.join(" ")}".strip)
|
||||||
end
|
end
|
||||||
|
|
||||||
if ENV["RUN_SYSTEM_TESTS"]
|
@good &&= run_or_fail("bundle exec rspec spec/system".strip) if ENV["RUN_SYSTEM_TESTS"]
|
||||||
@good &&= run_or_fail("bin/ember-cli --build")
|
|
||||||
@good &&= run_or_fail("bundle exec rspec spec/system")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
unless ENV["SKIP_PLUGINS"]
|
unless ENV["SKIP_PLUGINS"]
|
||||||
|
|||||||
@ -11,6 +11,7 @@ task "import:ensure_consistency" => :environment do
|
|||||||
insert_topic_views
|
insert_topic_views
|
||||||
insert_user_actions
|
insert_user_actions
|
||||||
insert_user_options
|
insert_user_options
|
||||||
|
insert_user_profiles
|
||||||
insert_user_stats
|
insert_user_stats
|
||||||
insert_user_visits
|
insert_user_visits
|
||||||
insert_draft_sequences
|
insert_draft_sequences
|
||||||
@ -194,6 +195,17 @@ def insert_user_options
|
|||||||
SQL
|
SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def insert_user_profiles
|
||||||
|
log "Inserting user profiles..."
|
||||||
|
|
||||||
|
DB.exec <<-SQL
|
||||||
|
INSERT INTO user_profiles (user_id)
|
||||||
|
SELECT id
|
||||||
|
FROM users
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
def insert_user_stats
|
def insert_user_stats
|
||||||
log "Inserting user stats..."
|
log "Inserting user stats..."
|
||||||
|
|
||||||
@ -569,3 +581,73 @@ task "import:update_first_post_created_at" => :environment do
|
|||||||
|
|
||||||
log "Done"
|
log "Done"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "Update avatars from external_avatar_url in SSO records"
|
||||||
|
task "import:update_avatars_from_sso" => :environment do
|
||||||
|
log "Updating avatars from SSO records"
|
||||||
|
|
||||||
|
sql = <<~SQL
|
||||||
|
SELECT user_id, external_avatar_url
|
||||||
|
FROM single_sign_on_records s
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM user_avatars a
|
||||||
|
WHERE a.user_id = s.user_id
|
||||||
|
)
|
||||||
|
SQL
|
||||||
|
|
||||||
|
queue = SizedQueue.new(1000)
|
||||||
|
threads = []
|
||||||
|
|
||||||
|
threads << Thread.new do ||
|
||||||
|
DB.query_each(sql) do |row|
|
||||||
|
queue << { user_id: row.user_id, url: row.external_avatar_url }
|
||||||
|
end
|
||||||
|
queue.close
|
||||||
|
end
|
||||||
|
|
||||||
|
max_count = DB.query_single(<<~SQL).first
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM single_sign_on_records s
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM user_avatars a
|
||||||
|
WHERE a.user_id = s.user_id
|
||||||
|
)
|
||||||
|
SQL
|
||||||
|
|
||||||
|
status_queue = Queue.new
|
||||||
|
status_thread = Thread.new do
|
||||||
|
error_count = 0
|
||||||
|
current_count = 0
|
||||||
|
|
||||||
|
while !(status = status_queue.pop).nil?
|
||||||
|
error_count += 1 if !status
|
||||||
|
current_count += 1
|
||||||
|
|
||||||
|
print "\r%7d / %7d (%d errors)" % [current_count, max_count, error_count]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
20.times do
|
||||||
|
threads << Thread.new do
|
||||||
|
while row = queue.pop
|
||||||
|
begin
|
||||||
|
UserAvatar.import_url_for_user(
|
||||||
|
row[:url],
|
||||||
|
User.find(row[:user_id]),
|
||||||
|
override_gravatar: true,
|
||||||
|
skip_rate_limit: true
|
||||||
|
)
|
||||||
|
status_queue << true
|
||||||
|
rescue
|
||||||
|
status_queue << false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
threads.each(&:join)
|
||||||
|
status_queue.close
|
||||||
|
status_thread.join
|
||||||
|
end
|
||||||
|
|||||||
@ -10,7 +10,7 @@ module Discourse
|
|||||||
MAJOR = 3
|
MAJOR = 3
|
||||||
MINOR = 1
|
MINOR = 1
|
||||||
TINY = 0
|
TINY = 0
|
||||||
PRE = "beta3"
|
PRE = "beta2"
|
||||||
|
|
||||||
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
|
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
|
||||||
end
|
end
|
||||||
|
|||||||
@ -0,0 +1,60 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Chat::AdminIncomingChatWebhooksController < Admin::AdminController
|
||||||
|
requires_plugin Chat::PLUGIN_NAME
|
||||||
|
|
||||||
|
def index
|
||||||
|
render_serialized(
|
||||||
|
{
|
||||||
|
chat_channels: ChatChannel.public_channels,
|
||||||
|
incoming_chat_webhooks: IncomingChatWebhook.includes(:chat_channel).all,
|
||||||
|
},
|
||||||
|
AdminChatIndexSerializer,
|
||||||
|
root: false,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
params.require(%i[name chat_channel_id])
|
||||||
|
|
||||||
|
chat_channel = ChatChannel.find_by(id: params[:chat_channel_id])
|
||||||
|
raise Discourse::NotFound if chat_channel.nil? || chat_channel.direct_message_channel?
|
||||||
|
|
||||||
|
webhook = IncomingChatWebhook.new(name: params[:name], chat_channel: chat_channel)
|
||||||
|
if webhook.save
|
||||||
|
render_serialized(webhook, IncomingChatWebhookSerializer, root: false)
|
||||||
|
else
|
||||||
|
render_json_error(webhook)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
params.require(%i[incoming_chat_webhook_id name chat_channel_id])
|
||||||
|
|
||||||
|
webhook = IncomingChatWebhook.find_by(id: params[:incoming_chat_webhook_id])
|
||||||
|
raise Discourse::NotFound unless webhook
|
||||||
|
|
||||||
|
chat_channel = ChatChannel.find_by(id: params[:chat_channel_id])
|
||||||
|
raise Discourse::NotFound if chat_channel.nil? || chat_channel.direct_message_channel?
|
||||||
|
|
||||||
|
if webhook.update(
|
||||||
|
name: params[:name],
|
||||||
|
description: params[:description],
|
||||||
|
emoji: params[:emoji],
|
||||||
|
username: params[:username],
|
||||||
|
chat_channel: chat_channel,
|
||||||
|
)
|
||||||
|
render json: success_json
|
||||||
|
else
|
||||||
|
render_json_error(webhook)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
params.require(:incoming_chat_webhook_id)
|
||||||
|
|
||||||
|
webhook = IncomingChatWebhook.find_by(id: params[:incoming_chat_webhook_id])
|
||||||
|
webhook.destroy if webhook
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -1,9 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Chat::Api::ChannelThreadsController < Chat::ApiController
|
class Chat::Api::ChatChannelThreadsController < Chat::Api
|
||||||
def show
|
def show
|
||||||
with_service(::Chat::LookupThread) do
|
with_service(Chat::Service::LookupThread) do
|
||||||
on_success { render_serialized(result.thread, ::Chat::ThreadSerializer, root: "thread") }
|
on_success { render_serialized(result.thread, ChatThreadSerializer, root: "thread") }
|
||||||
on_failed_policy(:threaded_discussions_enabled) { raise Discourse::NotFound }
|
on_failed_policy(:threaded_discussions_enabled) { raise Discourse::NotFound }
|
||||||
on_failed_policy(:threading_enabled_for_channel) { raise Discourse::NotFound }
|
on_failed_policy(:threading_enabled_for_channel) { raise Discourse::NotFound }
|
||||||
on_model_not_found(:thread) { raise Discourse::NotFound }
|
on_model_not_found(:thread) { raise Discourse::NotFound }
|
||||||
@ -1,13 +1,13 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Chat::Api::ChannelsArchivesController < Chat::Api::ChannelsController
|
class Chat::Api::ChatChannelsArchivesController < Chat::Api::ChatChannelsController
|
||||||
def create
|
def create
|
||||||
existing_archive = channel_from_params.chat_channel_archive
|
existing_archive = channel_from_params.chat_channel_archive
|
||||||
|
|
||||||
if existing_archive.present?
|
if existing_archive.present?
|
||||||
guardian.ensure_can_change_channel_status!(channel_from_params, :archived)
|
guardian.ensure_can_change_channel_status!(channel_from_params, :archived)
|
||||||
raise Discourse::InvalidAccess if !existing_archive.failed?
|
raise Discourse::InvalidAccess if !existing_archive.failed?
|
||||||
Chat::ChannelArchiveService.retry_archive_process(chat_channel: channel_from_params)
|
Chat::ChatChannelArchiveService.retry_archive_process(chat_channel: channel_from_params)
|
||||||
return render json: success_json
|
return render json: success_json
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -20,12 +20,12 @@ class Chat::Api::ChannelsArchivesController < Chat::Api::ChannelsController
|
|||||||
end
|
end
|
||||||
|
|
||||||
begin
|
begin
|
||||||
Chat::ChannelArchiveService.create_archive_process(
|
Chat::ChatChannelArchiveService.create_archive_process(
|
||||||
chat_channel: channel_from_params,
|
chat_channel: channel_from_params,
|
||||||
acting_user: current_user,
|
acting_user: current_user,
|
||||||
topic_params: topic_params,
|
topic_params: topic_params,
|
||||||
)
|
)
|
||||||
rescue Chat::ChannelArchiveService::ArchiveValidationError => err
|
rescue Chat::ChatChannelArchiveService::ArchiveValidationError => err
|
||||||
return render json: failed_json.merge(errors: err.errors), status: 400
|
return render json: failed_json.merge(errors: err.errors), status: 400
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -3,19 +3,19 @@
|
|||||||
CHANNEL_EDITABLE_PARAMS = %i[name description slug]
|
CHANNEL_EDITABLE_PARAMS = %i[name description slug]
|
||||||
CATEGORY_CHANNEL_EDITABLE_PARAMS = %i[auto_join_users allow_channel_wide_mentions]
|
CATEGORY_CHANNEL_EDITABLE_PARAMS = %i[auto_join_users allow_channel_wide_mentions]
|
||||||
|
|
||||||
class Chat::Api::ChannelsController < Chat::ApiController
|
class Chat::Api::ChatChannelsController < Chat::Api
|
||||||
def index
|
def index
|
||||||
permitted = params.permit(:filter, :limit, :offset, :status)
|
permitted = params.permit(:filter, :limit, :offset, :status)
|
||||||
|
|
||||||
options = { filter: permitted[:filter], limit: (permitted[:limit] || 25).to_i }
|
options = { filter: permitted[:filter], limit: (permitted[:limit] || 25).to_i }
|
||||||
options[:offset] = permitted[:offset].to_i
|
options[:offset] = permitted[:offset].to_i
|
||||||
options[:status] = Chat::Channel.statuses[permitted[:status]] ? permitted[:status] : nil
|
options[:status] = ChatChannel.statuses[permitted[:status]] ? permitted[:status] : nil
|
||||||
|
|
||||||
memberships = Chat::ChannelMembershipManager.all_for_user(current_user)
|
memberships = Chat::ChatChannelMembershipManager.all_for_user(current_user)
|
||||||
channels = Chat::ChannelFetcher.secured_public_channels(guardian, memberships, options)
|
channels = Chat::ChatChannelFetcher.secured_public_channels(guardian, memberships, options)
|
||||||
serialized_channels =
|
serialized_channels =
|
||||||
channels.map do |channel|
|
channels.map do |channel|
|
||||||
Chat::ChannelSerializer.new(
|
ChatChannelSerializer.new(
|
||||||
channel,
|
channel,
|
||||||
scope: Guardian.new(current_user),
|
scope: Guardian.new(current_user),
|
||||||
membership: memberships.find { |membership| membership.chat_channel_id == channel.id },
|
membership: memberships.find { |membership| membership.chat_channel_id == channel.id },
|
||||||
@ -29,7 +29,7 @@ class Chat::Api::ChannelsController < Chat::ApiController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
with_service Chat::TrashChannel do
|
with_service Chat::Service::TrashChannel do
|
||||||
on_model_not_found(:channel) { raise ActiveRecord::RecordNotFound }
|
on_model_not_found(:channel) { raise ActiveRecord::RecordNotFound }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -43,7 +43,7 @@ class Chat::Api::ChannelsController < Chat::ApiController
|
|||||||
raise Discourse::InvalidParameters.new(:name)
|
raise Discourse::InvalidParameters.new(:name)
|
||||||
end
|
end
|
||||||
|
|
||||||
if Chat::Channel.exists?(
|
if ChatChannel.exists?(
|
||||||
chatable_type: "Category",
|
chatable_type: "Category",
|
||||||
chatable_id: channel_params[:chatable_id],
|
chatable_id: channel_params[:chatable_id],
|
||||||
name: channel_params[:name],
|
name: channel_params[:name],
|
||||||
@ -69,12 +69,12 @@ class Chat::Api::ChannelsController < Chat::ApiController
|
|||||||
channel.user_chat_channel_memberships.create!(user: current_user, following: true)
|
channel.user_chat_channel_memberships.create!(user: current_user, following: true)
|
||||||
|
|
||||||
if channel.auto_join_users
|
if channel.auto_join_users
|
||||||
Chat::ChannelMembershipManager.new(channel).enforce_automatic_channel_memberships
|
Chat::ChatChannelMembershipManager.new(channel).enforce_automatic_channel_memberships
|
||||||
end
|
end
|
||||||
|
|
||||||
render_serialized(
|
render_serialized(
|
||||||
channel,
|
channel,
|
||||||
Chat::ChannelSerializer,
|
ChatChannelSerializer,
|
||||||
membership: channel.membership_for(current_user),
|
membership: channel.membership_for(current_user),
|
||||||
root: "channel",
|
root: "channel",
|
||||||
)
|
)
|
||||||
@ -83,7 +83,7 @@ class Chat::Api::ChannelsController < Chat::ApiController
|
|||||||
def show
|
def show
|
||||||
render_serialized(
|
render_serialized(
|
||||||
channel_from_params,
|
channel_from_params,
|
||||||
Chat::ChannelSerializer,
|
ChatChannelSerializer,
|
||||||
membership: channel_from_params.membership_for(current_user),
|
membership: channel_from_params.membership_for(current_user),
|
||||||
root: "channel",
|
root: "channel",
|
||||||
)
|
)
|
||||||
@ -96,11 +96,11 @@ class Chat::Api::ChannelsController < Chat::ApiController
|
|||||||
auto_join_limiter(channel_from_params).performed!
|
auto_join_limiter(channel_from_params).performed!
|
||||||
end
|
end
|
||||||
|
|
||||||
with_service(Chat::UpdateChannel, **params_to_edit) do
|
with_service(Chat::Service::UpdateChannel, **params_to_edit) do
|
||||||
on_success do
|
on_success do
|
||||||
render_serialized(
|
render_serialized(
|
||||||
result.channel,
|
result.channel,
|
||||||
Chat::ChannelSerializer,
|
ChatChannelSerializer,
|
||||||
root: "channel",
|
root: "channel",
|
||||||
membership: result.channel.membership_for(current_user),
|
membership: result.channel.membership_for(current_user),
|
||||||
)
|
)
|
||||||
@ -116,7 +116,7 @@ class Chat::Api::ChannelsController < Chat::ApiController
|
|||||||
def channel_from_params
|
def channel_from_params
|
||||||
@channel ||=
|
@channel ||=
|
||||||
begin
|
begin
|
||||||
channel = Chat::Channel.find(params.require(:channel_id))
|
channel = ChatChannel.find(params.require(:channel_id))
|
||||||
guardian.ensure_can_preview_chat_channel!(channel)
|
guardian.ensure_can_preview_chat_channel!(channel)
|
||||||
channel
|
channel
|
||||||
end
|
end
|
||||||
@ -126,7 +126,7 @@ class Chat::Api::ChannelsController < Chat::ApiController
|
|||||||
@membership ||=
|
@membership ||=
|
||||||
begin
|
begin
|
||||||
membership =
|
membership =
|
||||||
Chat::ChannelMembershipManager.new(channel_from_params).find_for_user(current_user)
|
Chat::ChatChannelMembershipManager.new(channel_from_params).find_for_user(current_user)
|
||||||
raise Discourse::NotFound if membership.blank?
|
raise Discourse::NotFound if membership.blank?
|
||||||
membership
|
membership
|
||||||
end
|
end
|
||||||
@ -1,12 +1,12 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Chat::Api::ChannelsCurrentUserMembershipController < Chat::Api::ChannelsController
|
class Chat::Api::ChatChannelsCurrentUserMembershipController < Chat::Api::ChatChannelsController
|
||||||
def create
|
def create
|
||||||
guardian.ensure_can_join_chat_channel!(channel_from_params)
|
guardian.ensure_can_join_chat_channel!(channel_from_params)
|
||||||
|
|
||||||
render_serialized(
|
render_serialized(
|
||||||
channel_from_params.add(current_user),
|
channel_from_params.add(current_user),
|
||||||
Chat::UserChannelMembershipSerializer,
|
UserChatChannelMembershipSerializer,
|
||||||
root: "membership",
|
root: "membership",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@ -14,7 +14,7 @@ class Chat::Api::ChannelsCurrentUserMembershipController < Chat::Api::ChannelsCo
|
|||||||
def destroy
|
def destroy
|
||||||
render_serialized(
|
render_serialized(
|
||||||
channel_from_params.remove(current_user),
|
channel_from_params.remove(current_user),
|
||||||
Chat::UserChannelMembershipSerializer,
|
UserChatChannelMembershipSerializer,
|
||||||
root: "membership",
|
root: "membership",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
MEMBERSHIP_EDITABLE_PARAMS = %i[muted desktop_notification_level mobile_notification_level]
|
MEMBERSHIP_EDITABLE_PARAMS = %i[muted desktop_notification_level mobile_notification_level]
|
||||||
|
|
||||||
class Chat::Api::ChannelsCurrentUserNotificationsSettingsController < Chat::Api::ChannelsController
|
class Chat::Api::ChatChannelsCurrentUserNotificationsSettingsController < Chat::Api::ChatChannelsController
|
||||||
def update
|
def update
|
||||||
settings_params = params.require(:notifications_settings).permit(MEMBERSHIP_EDITABLE_PARAMS)
|
settings_params = params.require(:notifications_settings).permit(MEMBERSHIP_EDITABLE_PARAMS)
|
||||||
membership_from_params.update!(settings_params.to_h)
|
membership_from_params.update!(settings_params.to_h)
|
||||||
render_serialized(
|
render_serialized(
|
||||||
membership_from_params,
|
membership_from_params,
|
||||||
Chat::UserChannelMembershipSerializer,
|
UserChatChannelMembershipSerializer,
|
||||||
root: "membership",
|
root: "membership",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Chat::Api::ChannelsMembershipsController < Chat::Api::ChannelsController
|
class Chat::Api::ChatChannelsMembershipsController < Chat::Api::ChatChannelsController
|
||||||
def index
|
def index
|
||||||
params.permit(:username, :offset, :limit)
|
params.permit(:username, :offset, :limit)
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ class Chat::Api::ChannelsMembershipsController < Chat::Api::ChannelsController
|
|||||||
limit = (params[:limit] || 50).to_i.clamp(1, 50)
|
limit = (params[:limit] || 50).to_i.clamp(1, 50)
|
||||||
|
|
||||||
memberships =
|
memberships =
|
||||||
Chat::ChannelMembershipsQuery.call(
|
ChatChannelMembershipsQuery.call(
|
||||||
channel: channel_from_params,
|
channel: channel_from_params,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
@ -17,7 +17,7 @@ class Chat::Api::ChannelsMembershipsController < Chat::Api::ChannelsController
|
|||||||
|
|
||||||
render_serialized(
|
render_serialized(
|
||||||
memberships,
|
memberships,
|
||||||
Chat::UserChannelMembershipSerializer,
|
UserChatChannelMembershipSerializer,
|
||||||
root: "memberships",
|
root: "memberships",
|
||||||
meta: {
|
meta: {
|
||||||
total_rows: channel_from_params.user_count,
|
total_rows: channel_from_params.user_count,
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Chat::Api::ChannelsMessagesMovesController < Chat::Api::ChannelsController
|
class Chat::Api::ChatChannelsMessagesMovesController < Chat::Api::ChatChannelsController
|
||||||
def create
|
def create
|
||||||
move_params = params.require(:move)
|
move_params = params.require(:move)
|
||||||
move_params.require(:message_ids)
|
move_params.require(:message_ids)
|
||||||
@ -8,7 +8,10 @@ class Chat::Api::ChannelsMessagesMovesController < Chat::Api::ChannelsController
|
|||||||
|
|
||||||
raise Discourse::InvalidAccess if !guardian.can_move_chat_messages?(channel_from_params)
|
raise Discourse::InvalidAccess if !guardian.can_move_chat_messages?(channel_from_params)
|
||||||
destination_channel =
|
destination_channel =
|
||||||
Chat::ChannelFetcher.find_with_access_check(move_params[:destination_channel_id], guardian)
|
Chat::ChatChannelFetcher.find_with_access_check(
|
||||||
|
move_params[:destination_channel_id],
|
||||||
|
guardian,
|
||||||
|
)
|
||||||
|
|
||||||
begin
|
begin
|
||||||
message_ids = move_params[:message_ids].map(&:to_i)
|
message_ids = move_params[:message_ids].map(&:to_i)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user