mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: backend support for user-selectable components
* FEATURE: backend support for user-selectable components * fix problems with previewing default theme * rename preview_key => preview_theme_id * omit default theme from child themes dropdown and try a different fix * cache & freeze stylesheets arrays
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
import { default as computed } from "ember-addons/ember-computed-decorators";
|
import {
|
||||||
|
default as computed,
|
||||||
|
observes
|
||||||
|
} from "ember-addons/ember-computed-decorators";
|
||||||
import { url } from "discourse/lib/computed";
|
import { url } from "discourse/lib/computed";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import showModal from "discourse/lib/show-modal";
|
import showModal from "discourse/lib/show-modal";
|
||||||
@@ -9,6 +12,18 @@ const THEME_UPLOAD_VAR = 2;
|
|||||||
export default Ember.Controller.extend({
|
export default Ember.Controller.extend({
|
||||||
editRouteName: "adminCustomizeThemes.edit",
|
editRouteName: "adminCustomizeThemes.edit",
|
||||||
|
|
||||||
|
@observes("allowChildThemes")
|
||||||
|
setSelectedThemeId() {
|
||||||
|
const available = this.get("selectableChildThemes");
|
||||||
|
if (
|
||||||
|
!this.get("selectedChildThemeId") &&
|
||||||
|
available &&
|
||||||
|
available.length > 0
|
||||||
|
) {
|
||||||
|
this.set("selectedChildThemeId", available[0].get("id"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
@computed("model", "allThemes")
|
@computed("model", "allThemes")
|
||||||
parentThemes(model, allThemes) {
|
parentThemes(model, allThemes) {
|
||||||
let parents = allThemes.filter(theme =>
|
let parents = allThemes.filter(theme =>
|
||||||
@@ -64,16 +79,21 @@ export default Ember.Controller.extend({
|
|||||||
|
|
||||||
let themes = [];
|
let themes = [];
|
||||||
available.forEach(t => {
|
available.forEach(t => {
|
||||||
if (!childThemes || childThemes.indexOf(t) === -1) {
|
if (
|
||||||
|
(!childThemes || childThemes.indexOf(t) === -1) &&
|
||||||
|
Em.isEmpty(t.get("childThemes")) &&
|
||||||
|
!t.get("user_selectable") &&
|
||||||
|
!t.get("default")
|
||||||
|
) {
|
||||||
themes.push(t);
|
themes.push(t);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return themes.length === 0 ? null : themes;
|
return themes.length === 0 ? null : themes;
|
||||||
},
|
},
|
||||||
|
|
||||||
@computed("allThemes", "allThemes.length", "model")
|
@computed("allThemes", "allThemes.length", "model", "parentThemes")
|
||||||
availableChildThemes(allThemes, count) {
|
availableChildThemes(allThemes, count) {
|
||||||
if (count === 1) {
|
if (count === 1 || this.get("parentThemes")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import RestModel from "discourse/models/rest";
|
import RestModel from "discourse/models/rest";
|
||||||
import { default as computed } from "ember-addons/ember-computed-decorators";
|
import { default as computed } from "ember-addons/ember-computed-decorators";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
|
||||||
const THEME_UPLOAD_VAR = 2;
|
const THEME_UPLOAD_VAR = 2;
|
||||||
|
|
||||||
@@ -150,7 +151,9 @@ const Theme = RestModel.extend({
|
|||||||
|
|
||||||
saveChanges() {
|
saveChanges() {
|
||||||
const hash = this.getProperties.apply(this, arguments);
|
const hash = this.getProperties.apply(this, arguments);
|
||||||
return this.save(hash).then(() => this.set("changed", false));
|
return this.save(hash)
|
||||||
|
.finally(() => this.set("changed", false))
|
||||||
|
.catch(popupAjaxError);
|
||||||
},
|
},
|
||||||
|
|
||||||
saveSettings(name, value) {
|
saveSettings(name, value) {
|
||||||
|
@@ -137,7 +137,7 @@
|
|||||||
{{/unless}}
|
{{/unless}}
|
||||||
{{#if selectableChildThemes}}
|
{{#if selectableChildThemes}}
|
||||||
<p>
|
<p>
|
||||||
{{combo-box content=selectableChildThemes value=selectedChildThemeId}}
|
{{combo-box filterable=true content=selectableChildThemes value=selectedChildThemeId}}
|
||||||
{{#d-button action="addChildTheme" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
|
{{#d-button action="addChildTheme" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
|
||||||
</p>
|
</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@@ -66,7 +66,7 @@ export default Ember.Controller.extend(PreferencesTabController, {
|
|||||||
@observes("themeId")
|
@observes("themeId")
|
||||||
themeIdChanged() {
|
themeIdChanged() {
|
||||||
const id = this.get("themeId");
|
const id = this.get("themeId");
|
||||||
previewTheme(id);
|
previewTheme([id]);
|
||||||
},
|
},
|
||||||
|
|
||||||
homeChanged() {
|
homeChanged() {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import DiscourseURL from "discourse/lib/url";
|
import DiscourseURL from "discourse/lib/url";
|
||||||
import { currentThemeId, refreshCSS } from "discourse/lib/theme-selector";
|
import { currentThemeIds, refreshCSS } from "discourse/lib/theme-selector";
|
||||||
|
|
||||||
// Use the message bus for live reloading of components for faster development.
|
// Use the message bus for live reloading of components for faster development.
|
||||||
export default {
|
export default {
|
||||||
@@ -58,12 +58,16 @@ export default {
|
|||||||
// Refresh if necessary
|
// Refresh if necessary
|
||||||
document.location.reload(true);
|
document.location.reload(true);
|
||||||
} else {
|
} else {
|
||||||
let themeId = currentThemeId();
|
const themeIds = currentThemeIds();
|
||||||
|
|
||||||
$("link").each(function() {
|
$("link").each(function() {
|
||||||
if (me.hasOwnProperty("theme_id") && me.new_href) {
|
if (me.hasOwnProperty("theme_id") && me.new_href) {
|
||||||
let target = $(this).data("target");
|
const target = $(this).data("target");
|
||||||
if (me.theme_id === themeId && target === me.target) {
|
const themeId = $(this).data("theme-id");
|
||||||
|
if (
|
||||||
|
themeIds.indexOf(me.theme_id) !== -1 &&
|
||||||
|
target === me.target &&
|
||||||
|
(!themeId || themeId === me.theme_id)
|
||||||
|
) {
|
||||||
refreshCSS(this, null, me.new_href);
|
refreshCSS(this, null, me.new_href);
|
||||||
}
|
}
|
||||||
} else if (this.href.match(me.name) && (me.hash || me.new_href)) {
|
} else if (this.href.match(me.name) && (me.hash || me.new_href)) {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import deprecated from "discourse-common/lib/deprecated";
|
import deprecated from "discourse-common/lib/deprecated";
|
||||||
|
|
||||||
const keySelector = "meta[name=discourse_theme_id]";
|
const keySelector = "meta[name=discourse_theme_ids]";
|
||||||
|
|
||||||
export function currentThemeKey() {
|
export function currentThemeKey() {
|
||||||
if (console && console.warn && console.trace) {
|
if (console && console.warn && console.trace) {
|
||||||
@@ -12,21 +12,26 @@ export function currentThemeKey() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function currentThemeId() {
|
export function currentThemeIds() {
|
||||||
let themeId = null;
|
const themeIds = [];
|
||||||
let elem = _.first($(keySelector));
|
const elem = _.first($(keySelector));
|
||||||
if (elem) {
|
if (elem) {
|
||||||
themeId = elem.content;
|
elem.content.split(",").forEach(num => {
|
||||||
if (_.isEmpty(themeId)) {
|
num = parseInt(num, 10);
|
||||||
themeId = null;
|
if (!isNaN(num)) {
|
||||||
} else {
|
themeIds.push(num);
|
||||||
themeId = parseInt(themeId);
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
return themeId;
|
return themeIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentThemeId() {
|
||||||
|
return currentThemeIds()[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setLocalTheme(ids, themeSeq) {
|
export function setLocalTheme(ids, themeSeq) {
|
||||||
|
ids = ids.reject(id => !id);
|
||||||
if (ids && ids.length > 0) {
|
if (ids && ids.length > 0) {
|
||||||
$.cookie("theme_ids", `${ids.join(",")}|${themeSeq}`, {
|
$.cookie("theme_ids", `${ids.join(",")}|${themeSeq}`, {
|
||||||
path: "/",
|
path: "/",
|
||||||
@@ -76,23 +81,28 @@ export function refreshCSS(node, hash, newHref, options) {
|
|||||||
$orig.data("copy", reloaded);
|
$orig.data("copy", reloaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function previewTheme(id) {
|
export function previewTheme(ids = []) {
|
||||||
if (currentThemeId() !== id) {
|
ids = ids.reject(id => !id);
|
||||||
|
if (!ids.includes(currentThemeId())) {
|
||||||
Discourse.set("assetVersion", "forceRefresh");
|
Discourse.set("assetVersion", "forceRefresh");
|
||||||
|
|
||||||
ajax(`/themes/assets/${id ? id : "default"}`).then(results => {
|
ajax(`/themes/assets/${ids.length > 0 ? ids.join("-") : "default"}`).then(
|
||||||
let elem = _.first($(keySelector));
|
results => {
|
||||||
if (elem) {
|
const elem = _.first($(keySelector));
|
||||||
elem.content = id;
|
if (elem) {
|
||||||
}
|
elem.content = ids.join(",");
|
||||||
|
|
||||||
results.themes.forEach(theme => {
|
|
||||||
let node = $(`link[rel=stylesheet][data-target=${theme.target}]`)[0];
|
|
||||||
if (node) {
|
|
||||||
refreshCSS(node, null, theme.url, { force: true });
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
results.themes.forEach(theme => {
|
||||||
|
const node = $(
|
||||||
|
`link[rel=stylesheet][data-target=${theme.target}]`
|
||||||
|
)[0];
|
||||||
|
if (node) {
|
||||||
|
refreshCSS(node, null, theme.new_href, { force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -182,11 +182,13 @@ class Admin::ThemesController < Admin::AdminController
|
|||||||
log_theme_change(original_json, @theme)
|
log_theme_change(original_json, @theme)
|
||||||
format.json { render json: @theme, status: :ok }
|
format.json { render json: @theme, status: :ok }
|
||||||
else
|
else
|
||||||
format.json {
|
format.json do
|
||||||
|
error = @theme.errors.full_messages.join(", ").presence
|
||||||
|
error = I18n.t("themes.bad_color_scheme") if @theme.errors[:color_scheme].present?
|
||||||
|
error ||= I18n.t("themes.other_error")
|
||||||
|
|
||||||
error = @theme.errors[:color_scheme] ? I18n.t("themes.bad_color_scheme") : I18n.t("themes.other_error")
|
|
||||||
render json: { errors: [ error ] }, status: :unprocessable_entity
|
render json: { errors: [ error ] }, status: :unprocessable_entity
|
||||||
}
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@@ -22,7 +22,7 @@ class ApplicationController < ActionController::Base
|
|||||||
include GlobalPath
|
include GlobalPath
|
||||||
include Hijack
|
include Hijack
|
||||||
|
|
||||||
attr_reader :theme_id
|
attr_reader :theme_ids
|
||||||
|
|
||||||
serialization_scope :guardian
|
serialization_scope :guardian
|
||||||
|
|
||||||
@@ -62,8 +62,8 @@ class ApplicationController < ActionController::Base
|
|||||||
after_action :remember_theme_id
|
after_action :remember_theme_id
|
||||||
|
|
||||||
def remember_theme_id
|
def remember_theme_id
|
||||||
if @theme_id
|
if @theme_ids.present?
|
||||||
Stylesheet::Watcher.theme_id = @theme_id if defined? Stylesheet::Watcher
|
Stylesheet::Watcher.theme_id = @theme_ids.first if defined? Stylesheet::Watcher
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -331,28 +331,33 @@ class ApplicationController < ActionController::Base
|
|||||||
resolve_safe_mode
|
resolve_safe_mode
|
||||||
return if request.env[NO_CUSTOM]
|
return if request.env[NO_CUSTOM]
|
||||||
|
|
||||||
theme_id = request[:preview_theme_id]&.to_i
|
theme_ids = []
|
||||||
|
|
||||||
|
if preview_theme_id = request[:preview_theme_id]&.to_i
|
||||||
|
theme_ids << preview_theme_id
|
||||||
|
end
|
||||||
|
|
||||||
user_option = current_user&.user_option
|
user_option = current_user&.user_option
|
||||||
|
|
||||||
unless theme_id
|
if theme_ids.blank?
|
||||||
ids, seq = cookies[:theme_ids]&.split("|")
|
ids, seq = cookies[:theme_ids]&.split("|")
|
||||||
ids = ids&.split(",")&.map(&:to_i)
|
ids = ids&.split(",")&.map(&:to_i)
|
||||||
if ids && ids.size > 0 && seq && seq.to_i == user_option&.theme_key_seq.to_i
|
if ids.present? && seq && seq.to_i == user_option&.theme_key_seq.to_i
|
||||||
theme_id = ids.first
|
theme_ids = ids if guardian.allow_themes?(ids)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
theme_id ||= user_option&.theme_ids&.first
|
theme_ids = user_option&.theme_ids || [] if theme_ids.blank?
|
||||||
|
|
||||||
if theme_id && !guardian.allow_themes?(theme_id)
|
unless guardian.allow_themes?(theme_ids)
|
||||||
theme_id = nil
|
theme_ids = []
|
||||||
end
|
end
|
||||||
|
|
||||||
theme_id ||= SiteSetting.default_theme_id
|
if theme_ids.blank? && SiteSetting.default_theme_id != -1
|
||||||
theme_id = nil if theme_id.blank? || theme_id == -1
|
theme_ids << SiteSetting.default_theme_id
|
||||||
|
end
|
||||||
|
|
||||||
@theme_id = request.env[:resolved_theme_id] = theme_id
|
@theme_ids = request.env[:resolved_theme_ids] = theme_ids
|
||||||
end
|
end
|
||||||
|
|
||||||
def guardian
|
def guardian
|
||||||
@@ -502,10 +507,10 @@ class ApplicationController < ActionController::Base
|
|||||||
target = view_context.mobile_view? ? :mobile : :desktop
|
target = view_context.mobile_view? ? :mobile : :desktop
|
||||||
|
|
||||||
data =
|
data =
|
||||||
if @theme_id
|
if @theme_ids.present?
|
||||||
{
|
{
|
||||||
top: Theme.lookup_field(@theme_id, target, "after_header"),
|
top: Theme.lookup_field(@theme_ids, target, "after_header"),
|
||||||
footer: Theme.lookup_field(@theme_id, target, "footer")
|
footer: Theme.lookup_field(@theme_ids, target, "footer")
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{}
|
{}
|
||||||
|
@@ -29,7 +29,7 @@ class StylesheetsController < ApplicationController
|
|||||||
# we hold of re-compilation till someone asks for asset
|
# we hold of re-compilation till someone asks for asset
|
||||||
if target.include?("theme")
|
if target.include?("theme")
|
||||||
split_target, theme_id = target.split(/_(-?[0-9]+)/)
|
split_target, theme_id = target.split(/_(-?[0-9]+)/)
|
||||||
theme = Theme.find(theme_id) if theme_id
|
theme = Theme.find_by(id: theme_id) if theme_id.present?
|
||||||
else
|
else
|
||||||
split_target, color_scheme_id = target.split(/_(-?[0-9]+)/)
|
split_target, color_scheme_id = target.split(/_(-?[0-9]+)/)
|
||||||
theme = Theme.find_by(color_scheme_id: color_scheme_id)
|
theme = Theme.find_by(color_scheme_id: color_scheme_id)
|
||||||
|
@@ -1,27 +1,26 @@
|
|||||||
class ThemesController < ::ApplicationController
|
class ThemesController < ::ApplicationController
|
||||||
def assets
|
def assets
|
||||||
theme_id = params[:id].to_i
|
theme_ids = params[:ids].to_s.split("-").map(&:to_i)
|
||||||
|
|
||||||
if params[:id] == "default"
|
if params[:ids] == "default"
|
||||||
theme_id = nil
|
theme_ids = nil
|
||||||
else
|
else
|
||||||
raise Discourse::NotFound unless Theme.where(id: theme_id).exists?
|
raise Discourse::NotFound unless guardian.allow_themes?(theme_ids)
|
||||||
end
|
end
|
||||||
|
|
||||||
object = [:mobile, :desktop, :desktop_theme, :mobile_theme].map do |target|
|
targets = view_context.mobile_view? ? [:mobile, :mobile_theme] : [:desktop, :desktop_theme]
|
||||||
link = Stylesheet::Manager.stylesheet_link_tag(target, 'all', params[:id])
|
targets << :admin if guardian.is_staff?
|
||||||
if link
|
|
||||||
href = link.split(/["']/)[1]
|
object = targets.map do |target|
|
||||||
if Rails.env.development?
|
Stylesheet::Manager.stylesheet_data(target, theme_ids).map do |hash|
|
||||||
href << (href.include?("?") ? "&" : "?")
|
return hash unless Rails.env.development?
|
||||||
href << SecureRandom.hex
|
|
||||||
end
|
dup_hash = hash.dup
|
||||||
{
|
dup_hash[:new_href] << (dup_hash[:new_href].include?("?") ? "&" : "?")
|
||||||
target: target,
|
dup_hash[:new_href] << SecureRandom.hex
|
||||||
url: href
|
dup_hash
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end.compact
|
end.flatten
|
||||||
|
|
||||||
render json: object.as_json
|
render json: object.as_json
|
||||||
end
|
end
|
||||||
|
@@ -350,11 +350,11 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def theme_id
|
def theme_ids
|
||||||
if customization_disabled?
|
if customization_disabled?
|
||||||
nil
|
nil
|
||||||
else
|
else
|
||||||
request.env[:resolved_theme_id]
|
request.env[:resolved_theme_ids]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -378,17 +378,17 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def theme_lookup(name)
|
def theme_lookup(name)
|
||||||
lookup = Theme.lookup_field(theme_id, mobile_view? ? :mobile : :desktop, name)
|
lookup = Theme.lookup_field(theme_ids, mobile_view? ? :mobile : :desktop, name)
|
||||||
lookup.html_safe if lookup
|
lookup.html_safe if lookup
|
||||||
end
|
end
|
||||||
|
|
||||||
def discourse_stylesheet_link_tag(name, opts = {})
|
def discourse_stylesheet_link_tag(name, opts = {})
|
||||||
if opts.key?(:theme_id)
|
if opts.key?(:theme_ids)
|
||||||
id = opts[:theme_id] unless customization_disabled?
|
ids = opts[:theme_ids] unless customization_disabled?
|
||||||
else
|
else
|
||||||
id = theme_id
|
ids = theme_ids
|
||||||
end
|
end
|
||||||
|
|
||||||
Stylesheet::Manager.stylesheet_link_tag(name, 'all', id)
|
Stylesheet::Manager.stylesheet_link_tag(name, 'all', ids)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@@ -1,6 +1,24 @@
|
|||||||
class ChildTheme < ActiveRecord::Base
|
class ChildTheme < ActiveRecord::Base
|
||||||
belongs_to :parent_theme, class_name: 'Theme'
|
belongs_to :parent_theme, class_name: 'Theme'
|
||||||
belongs_to :child_theme, class_name: 'Theme'
|
belongs_to :child_theme, class_name: 'Theme'
|
||||||
|
|
||||||
|
validate :child_validations
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def child_validations
|
||||||
|
if ChildTheme.exists?(["parent_theme_id = ? OR child_theme_id = ?", child_theme_id, parent_theme_id])
|
||||||
|
errors.add(:base, I18n.t("themes.errors.no_multilevels_components"))
|
||||||
|
end
|
||||||
|
|
||||||
|
if Theme.exists?(id: child_theme_id, user_selectable: true)
|
||||||
|
errors.add(:base, I18n.t("themes.errors.component_no_user_selectable"))
|
||||||
|
end
|
||||||
|
|
||||||
|
if child_theme_id == SiteSetting.default_theme_id
|
||||||
|
errors.add(:base, I18n.t("themes.errors.component_no_default"))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# == Schema Information
|
# == Schema Information
|
||||||
|
@@ -241,12 +241,15 @@ class ColorScheme < ActiveRecord::Base
|
|||||||
|
|
||||||
def publish_discourse_stylesheet
|
def publish_discourse_stylesheet
|
||||||
if self.id
|
if self.id
|
||||||
themes = Theme.where(color_scheme_id: self.id).to_a
|
theme_ids = Theme.where(color_scheme_id: self.id).pluck(:id)
|
||||||
if themes.present?
|
if theme_ids.present?
|
||||||
Stylesheet::Manager.cache.clear
|
Stylesheet::Manager.cache.clear
|
||||||
themes.each do |theme|
|
Theme.notify_theme_change(
|
||||||
theme.notify_scheme_change(_clear_manager_cache = false)
|
theme_ids,
|
||||||
end
|
with_scheme: true,
|
||||||
|
clear_manager_cache: false,
|
||||||
|
all_themes: true
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@@ -20,6 +20,12 @@ class Theme < ActiveRecord::Base
|
|||||||
has_many :color_schemes
|
has_many :color_schemes
|
||||||
belongs_to :remote_theme
|
belongs_to :remote_theme
|
||||||
|
|
||||||
|
validate :user_selectable_validation
|
||||||
|
|
||||||
|
scope :user_selectable, ->() {
|
||||||
|
where('user_selectable OR id = ?', SiteSetting.default_theme_id)
|
||||||
|
}
|
||||||
|
|
||||||
def notify_color_change(color)
|
def notify_color_change(color)
|
||||||
changed_colors << color
|
changed_colors << color
|
||||||
end
|
end
|
||||||
@@ -45,7 +51,6 @@ class Theme < ActiveRecord::Base
|
|||||||
|
|
||||||
remove_from_cache!
|
remove_from_cache!
|
||||||
clear_cached_settings!
|
clear_cached_settings!
|
||||||
notify_scheme_change if saved_change_to_color_scheme_id?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
after_destroy do
|
after_destroy do
|
||||||
@@ -70,29 +75,37 @@ class Theme < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
after_commit ->(theme) do
|
after_commit ->(theme) do
|
||||||
theme.notify_theme_change
|
theme.notify_theme_change(with_scheme: theme.saved_change_to_color_scheme_id?)
|
||||||
end, on: :update
|
end, on: [:create, :update]
|
||||||
|
|
||||||
|
def self.get_set_cache(key, &blk)
|
||||||
|
if val = @cache[key]
|
||||||
|
return val
|
||||||
|
end
|
||||||
|
@cache[key] = blk.call
|
||||||
|
end
|
||||||
|
|
||||||
def self.theme_ids
|
def self.theme_ids
|
||||||
if ids = @cache["theme_ids"]
|
get_set_cache "theme_ids" do
|
||||||
return ids
|
Theme.pluck(:id)
|
||||||
end
|
end
|
||||||
@cache["theme_ids"] = Set.new(Theme.pluck(:id))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.user_theme_ids
|
def self.user_theme_ids
|
||||||
if ids = @cache["user_theme_ids"]
|
get_set_cache "user_theme_ids" do
|
||||||
return ids
|
Theme.user_selectable.pluck(:id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.components_for(theme_id)
|
||||||
|
get_set_cache "theme_components_for_#{theme_id}" do
|
||||||
|
ChildTheme.where(parent_theme_id: theme_id).distinct.pluck(:child_theme_id)
|
||||||
end
|
end
|
||||||
@cache["user_theme_ids"] = Set.new(
|
|
||||||
Theme
|
|
||||||
.where('user_selectable OR id = ?', SiteSetting.default_theme_id)
|
|
||||||
.pluck(:id)
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.expire_site_cache!
|
def self.expire_site_cache!
|
||||||
Site.clear_anon_cache!
|
Site.clear_anon_cache!
|
||||||
|
clear_cache!
|
||||||
ApplicationSerializer.expire_cache_fragment!("user_themes")
|
ApplicationSerializer.expire_cache_fragment!("user_themes")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -101,7 +114,25 @@ class Theme < ActiveRecord::Base
|
|||||||
expire_site_cache!
|
expire_site_cache!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.transform_ids(ids, extend: true)
|
||||||
|
return [] if ids.blank?
|
||||||
|
|
||||||
|
ids.uniq!
|
||||||
|
parent = ids.first
|
||||||
|
|
||||||
|
components = ids[1..-1]
|
||||||
|
components.push(*components_for(parent)) if extend
|
||||||
|
components.sort!.uniq!
|
||||||
|
|
||||||
|
[parent, *components]
|
||||||
|
end
|
||||||
|
|
||||||
def set_default!
|
def set_default!
|
||||||
|
if component?
|
||||||
|
raise Discourse::InvalidParameters.new(
|
||||||
|
I18n.t("themes.errors.component_no_default")
|
||||||
|
)
|
||||||
|
end
|
||||||
SiteSetting.default_theme_id = id
|
SiteSetting.default_theme_id = id
|
||||||
Theme.expire_site_cache!
|
Theme.expire_site_cache!
|
||||||
end
|
end
|
||||||
@@ -110,22 +141,32 @@ class Theme < ActiveRecord::Base
|
|||||||
SiteSetting.default_theme_id == id
|
SiteSetting.default_theme_id == id
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.lookup_field(theme_id, target, field)
|
def component?
|
||||||
return if theme_id.blank?
|
ChildTheme.exists?(child_theme_id: id)
|
||||||
|
end
|
||||||
|
|
||||||
cache_key = "#{theme_id}:#{target}:#{field}:#{ThemeField::COMPILER_VERSION}"
|
def user_selectable_validation
|
||||||
|
if component? && user_selectable
|
||||||
|
errors.add(:base, I18n.t("themes.errors.component_no_user_selectable"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.lookup_field(theme_ids, target, field)
|
||||||
|
return if theme_ids.blank?
|
||||||
|
theme_ids = [theme_ids] unless Array === theme_ids
|
||||||
|
|
||||||
|
theme_ids = transform_ids(theme_ids)
|
||||||
|
cache_key = "#{theme_ids.join(",")}:#{target}:#{field}:#{ThemeField::COMPILER_VERSION}"
|
||||||
lookup = @cache[cache_key]
|
lookup = @cache[cache_key]
|
||||||
return lookup.html_safe if lookup
|
return lookup.html_safe if lookup
|
||||||
|
|
||||||
target = target.to_sym
|
target = target.to_sym
|
||||||
theme = find_by(id: theme_id)
|
val = resolve_baked_field(theme_ids, target, field)
|
||||||
|
|
||||||
val = theme.resolve_baked_field(target, field) if theme
|
|
||||||
|
|
||||||
(@cache[cache_key] = val || "").html_safe
|
(@cache[cache_key] = val || "").html_safe
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.remove_from_cache!(themes = nil)
|
def self.remove_from_cache!
|
||||||
clear_cache!
|
clear_cache!
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -141,33 +182,32 @@ class Theme < ActiveRecord::Base
|
|||||||
self.targets.invert[target_id]
|
self.targets.invert[target_id]
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_scheme_change(clear_manager_cache = true)
|
def self.notify_theme_change(theme_ids, with_scheme: false, clear_manager_cache: true, all_themes: false)
|
||||||
Stylesheet::Manager.cache.clear if clear_manager_cache
|
|
||||||
message = refresh_message_for_targets(["desktop", "mobile", "admin"], self)
|
|
||||||
MessageBus.publish('/file-change', message)
|
|
||||||
end
|
|
||||||
|
|
||||||
def notify_theme_change
|
|
||||||
Stylesheet::Manager.clear_theme_cache!
|
Stylesheet::Manager.clear_theme_cache!
|
||||||
|
targets = [:mobile_theme, :desktop_theme]
|
||||||
|
|
||||||
themes = [self] + dependant_themes
|
if with_scheme
|
||||||
|
targets.prepend(:desktop, :mobile, :admin)
|
||||||
|
Stylesheet::Manager.cache.clear if clear_manager_cache
|
||||||
|
end
|
||||||
|
|
||||||
|
if all_themes
|
||||||
|
message = theme_ids.map { |id| refresh_message_for_targets(targets, id) }.flatten
|
||||||
|
else
|
||||||
|
message = refresh_message_for_targets(targets, theme_ids).flatten
|
||||||
|
end
|
||||||
|
|
||||||
message = themes.map do |theme|
|
|
||||||
refresh_message_for_targets([:mobile_theme, :desktop_theme], theme)
|
|
||||||
end.compact.flatten
|
|
||||||
MessageBus.publish('/file-change', message)
|
MessageBus.publish('/file-change', message)
|
||||||
end
|
end
|
||||||
|
|
||||||
def refresh_message_for_targets(targets, theme)
|
def notify_theme_change(with_scheme: false)
|
||||||
|
theme_ids = (dependant_themes&.pluck(:id) || []).unshift(self.id)
|
||||||
|
self.class.notify_theme_change(theme_ids, with_scheme: with_scheme)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.refresh_message_for_targets(targets, theme_ids)
|
||||||
targets.map do |target|
|
targets.map do |target|
|
||||||
href = Stylesheet::Manager.stylesheet_href(target.to_sym, theme.id)
|
Stylesheet::Manager.stylesheet_data(target.to_sym, theme_ids)
|
||||||
if href
|
|
||||||
{
|
|
||||||
target: target,
|
|
||||||
new_href: href,
|
|
||||||
theme_id: theme.id
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -180,48 +220,34 @@ class Theme < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def resolve_dependant_themes(direction)
|
def resolve_dependant_themes(direction)
|
||||||
|
|
||||||
select_field, where_field = nil
|
|
||||||
|
|
||||||
if direction == :up
|
if direction == :up
|
||||||
select_field = "parent_theme_id"
|
join_field = "parent_theme_id"
|
||||||
where_field = "child_theme_id"
|
where_field = "child_theme_id"
|
||||||
elsif direction == :down
|
elsif direction == :down
|
||||||
select_field = "child_theme_id"
|
join_field = "child_theme_id"
|
||||||
where_field = "parent_theme_id"
|
where_field = "parent_theme_id"
|
||||||
else
|
else
|
||||||
raise "Unknown direction"
|
raise "Unknown direction"
|
||||||
end
|
end
|
||||||
|
|
||||||
themes = []
|
|
||||||
return [] unless id
|
return [] unless id
|
||||||
|
|
||||||
uniq = Set.new
|
Theme.joins("JOIN child_themes ON themes.id = child_themes.#{join_field}").where("#{where_field} = ?", id)
|
||||||
uniq << id
|
end
|
||||||
|
|
||||||
iterations = 0
|
def self.resolve_baked_field(theme_ids, target, name)
|
||||||
added = [id]
|
list_baked_fields(theme_ids, target, name).map { |f| f.value_baked || f.value }.join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
while added.length > 0 && iterations < 5
|
def self.list_baked_fields(theme_ids, target, name)
|
||||||
|
target = target.to_sym
|
||||||
|
|
||||||
iterations += 1
|
fields = ThemeField.find_by_theme_ids(theme_ids)
|
||||||
|
.where(target_id: [Theme.targets[target], Theme.targets[:common]])
|
||||||
|
.where(name: name.to_s)
|
||||||
|
|
||||||
new_themes = Theme.where("id in (SELECT #{select_field}
|
fields.each(&:ensure_baked!)
|
||||||
FROM child_themes
|
fields
|
||||||
WHERE #{where_field} in (?))", added).to_a
|
|
||||||
|
|
||||||
added = []
|
|
||||||
new_themes.each do |theme|
|
|
||||||
unless uniq.include?(theme.id)
|
|
||||||
added << theme.id
|
|
||||||
uniq << theme.id
|
|
||||||
themes << theme
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
themes
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_baked_field(target, name)
|
def resolve_baked_field(target, name)
|
||||||
@@ -229,22 +255,8 @@ class Theme < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def list_baked_fields(target, name)
|
def list_baked_fields(target, name)
|
||||||
|
theme_ids = (included_themes&.pluck(:id) || []).unshift(self.id)
|
||||||
target = target.to_sym
|
self.class.list_baked_fields(theme_ids, target, name)
|
||||||
|
|
||||||
theme_ids = [self.id] + (included_themes.map(&:id) || [])
|
|
||||||
fields = ThemeField.where(target_id: [Theme.targets[target], Theme.targets[:common]])
|
|
||||||
.where(name: name.to_s)
|
|
||||||
.includes(:theme)
|
|
||||||
.joins("
|
|
||||||
JOIN (
|
|
||||||
SELECT #{theme_ids.map.with_index { |id, idx| "#{id} AS theme_id, #{idx} AS sort_column" }.join(" UNION ALL SELECT ")}
|
|
||||||
) as X ON X.theme_id = theme_fields.theme_id"
|
|
||||||
)
|
|
||||||
.order('sort_column, target_id')
|
|
||||||
|
|
||||||
fields.each(&:ensure_baked!)
|
|
||||||
fields
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_from_cache!
|
def remove_from_cache!
|
||||||
@@ -288,21 +300,23 @@ class Theme < ActiveRecord::Base
|
|||||||
|
|
||||||
def all_theme_variables
|
def all_theme_variables
|
||||||
fields = {}
|
fields = {}
|
||||||
([self] + (included_themes || [])).each do |theme|
|
ids = (included_themes&.pluck(:id) || []).unshift(self.id)
|
||||||
theme&.theme_fields.each do |field|
|
ThemeField.find_by_theme_ids(ids).where(type_id: ThemeField.theme_var_type_ids).each do |field|
|
||||||
next unless ThemeField.theme_var_type_ids.include?(field.type_id)
|
next if fields.key?(field.name)
|
||||||
next if fields.key?(field.name)
|
fields[field.name] = field
|
||||||
fields[field.name] = field
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
fields.values
|
fields.values
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_child_theme!(theme)
|
def add_child_theme!(theme)
|
||||||
child_theme_relation.create!(child_theme_id: theme.id)
|
new_relation = child_theme_relation.new(child_theme_id: theme.id)
|
||||||
@included_themes = nil
|
if new_relation.save
|
||||||
child_themes.reload
|
@included_themes = nil
|
||||||
save!
|
child_themes.reload
|
||||||
|
save!
|
||||||
|
else
|
||||||
|
raise Discourse::InvalidParameters.new(new_relation.errors.full_messages.join(", "))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def settings
|
def settings
|
||||||
|
@@ -4,6 +4,17 @@ class ThemeField < ActiveRecord::Base
|
|||||||
|
|
||||||
belongs_to :upload
|
belongs_to :upload
|
||||||
|
|
||||||
|
scope :find_by_theme_ids, ->(theme_ids) {
|
||||||
|
return none unless theme_ids.present?
|
||||||
|
|
||||||
|
where(theme_id: theme_ids)
|
||||||
|
.joins(
|
||||||
|
"JOIN (
|
||||||
|
SELECT #{theme_ids.map.with_index { |id, idx| "#{id.to_i} AS theme_id, #{idx} AS sort_column" }.join(" UNION ALL SELECT ")}
|
||||||
|
) as X ON X.theme_id = theme_fields.theme_id")
|
||||||
|
.order("sort_column")
|
||||||
|
}
|
||||||
|
|
||||||
def self.types
|
def self.types
|
||||||
@types ||= Enum.new(html: 0,
|
@types ||= Enum.new(html: 0,
|
||||||
scss: 1,
|
scss: 1,
|
||||||
|
@@ -8,6 +8,6 @@
|
|||||||
<%= discourse_stylesheet_link_tag(:admin) %>
|
<%= discourse_stylesheet_link_tag(:admin) %>
|
||||||
<%- end %>
|
<%- end %>
|
||||||
|
|
||||||
<%- if theme_id %>
|
<%- if theme_ids.present? %>
|
||||||
<%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_theme : :desktop_theme) %>
|
<%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_theme : :desktop_theme) %>
|
||||||
<%- end %>
|
<%- end %>
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title><%= content_for?(:title) ? yield(:title) : SiteSetting.title %></title>
|
<title><%= content_for?(:title) ? yield(:title) : SiteSetting.title %></title>
|
||||||
<meta name="description" content="<%= @description_meta || SiteSetting.site_description %>">
|
<meta name="description" content="<%= @description_meta || SiteSetting.site_description %>">
|
||||||
<meta name="discourse_theme_id" content="<%= theme_id %>">
|
<meta name="discourse_theme_ids" content="<%= theme_ids&.join(",") %>">
|
||||||
<meta name="discourse_current_homepage" content="<%= current_homepage %>">
|
<meta name="discourse_current_homepage" content="<%= current_homepage %>">
|
||||||
<%= render partial: "layouts/head" %>
|
<%= render partial: "layouts/head" %>
|
||||||
<%= discourse_csrf_tags %>
|
<%= discourse_csrf_tags %>
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
<%- else %>
|
<%- else %>
|
||||||
<%= discourse_stylesheet_link_tag(mobile_view? ? :mobile : :desktop) %>
|
<%= discourse_stylesheet_link_tag(mobile_view? ? :mobile : :desktop) %>
|
||||||
<%- end %>
|
<%- end %>
|
||||||
<%- if theme_id %>
|
<%- if theme_ids.present? %>
|
||||||
<%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_theme : :desktop_theme) %>
|
<%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_theme : :desktop_theme) %>
|
||||||
<%- end %>
|
<%- end %>
|
||||||
<%= theme_lookup("head_tag") %>
|
<%= theme_lookup("head_tag") %>
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
|
||||||
<%= discourse_stylesheet_link_tag 'embed', theme_id: nil %>
|
<%= discourse_stylesheet_link_tag 'embed', theme_ids: nil %>
|
||||||
<%- unless customization_disabled? %>
|
<%- unless customization_disabled? %>
|
||||||
<%= discourse_stylesheet_link_tag :embedded_theme %>
|
<%= discourse_stylesheet_link_tag :embedded_theme %>
|
||||||
<%- end %>
|
<%- end %>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<%= discourse_stylesheet_link_tag 'wizard', theme_id: nil %>
|
<%= discourse_stylesheet_link_tag 'wizard', theme_ids: nil %>
|
||||||
<%= render partial: "common/special_font_face" %>
|
<%= render partial: "common/special_font_face" %>
|
||||||
<%= preload_script 'ember_jquery' %>
|
<%= preload_script 'ember_jquery' %>
|
||||||
<%= preload_script 'wizard-vendor' %>
|
<%= preload_script 'wizard-vendor' %>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<%= discourse_stylesheet_link_tag :wizard, theme_id: nil %>
|
<%= discourse_stylesheet_link_tag :wizard, theme_ids: nil %>
|
||||||
<%= preload_script 'ember_jquery' %>
|
<%= preload_script 'ember_jquery' %>
|
||||||
<%= preload_script 'wizard-vendor' %>
|
<%= preload_script 'wizard-vendor' %>
|
||||||
<%= preload_script 'wizard-application' %>
|
<%= preload_script 'wizard-application' %>
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<title>QUnit Test Runner</title>
|
<title>QUnit Test Runner</title>
|
||||||
<%= stylesheet_link_tag "qunit" %>
|
<%= stylesheet_link_tag "qunit" %>
|
||||||
<%= stylesheet_link_tag "test_helper" %>
|
<%= stylesheet_link_tag "test_helper" %>
|
||||||
<%= discourse_stylesheet_link_tag :wizard %>
|
<%= discourse_stylesheet_link_tag :wizard, theme_ids: nil %>
|
||||||
<%= javascript_include_tag "qunit" %>
|
<%= javascript_include_tag "qunit" %>
|
||||||
<%= javascript_include_tag "wizard/test/test_helper" %>
|
<%= javascript_include_tag "wizard/test/test_helper" %>
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
|
@@ -60,6 +60,10 @@ en:
|
|||||||
bad_color_scheme: "Can not update theme, invalid color scheme"
|
bad_color_scheme: "Can not update theme, invalid color scheme"
|
||||||
other_error: "Something went wrong updating theme"
|
other_error: "Something went wrong updating theme"
|
||||||
error_importing: "Error cloning git repository, access is denied or repository is not found"
|
error_importing: "Error cloning git repository, access is denied or repository is not found"
|
||||||
|
errors:
|
||||||
|
component_no_user_selectable: "Theme components can't be user-selectable"
|
||||||
|
component_no_default: "Theme components can't be default theme"
|
||||||
|
no_multilevels_components: "Themes with child themes can't be child themes themselves"
|
||||||
settings_errors:
|
settings_errors:
|
||||||
invalid_yaml: "Provided YAML is invalid."
|
invalid_yaml: "Provided YAML is invalid."
|
||||||
data_type_not_a_number: "Setting `%{name}` type is unsupported. Supported types are `integer`, `bool`, `list` and `enum`"
|
data_type_not_a_number: "Setting `%{name}` type is unsupported. Supported types are `integer`, `bool`, `list` and `enum`"
|
||||||
|
@@ -814,7 +814,7 @@ Discourse::Application.routes.draw do
|
|||||||
get "/safe-mode" => "safe_mode#index"
|
get "/safe-mode" => "safe_mode#index"
|
||||||
post "/safe-mode" => "safe_mode#enter", as: "safe_mode_enter"
|
post "/safe-mode" => "safe_mode#enter", as: "safe_mode_enter"
|
||||||
|
|
||||||
get "/themes/assets/:id" => "themes#assets"
|
get "/themes/assets/:ids" => "themes#assets"
|
||||||
|
|
||||||
if Rails.env == "test" || Rails.env == "development"
|
if Rails.env == "test" || Rails.env == "development"
|
||||||
get "/qunit" => "qunit#index"
|
get "/qunit" => "qunit#index"
|
||||||
|
@@ -0,0 +1,68 @@
|
|||||||
|
class DisallowMultiLevelsThemeComponents < ActiveRecord::Migration[5.2]
|
||||||
|
def up
|
||||||
|
@handled = []
|
||||||
|
top_parents = DB.query("
|
||||||
|
SELECT parent_theme_id, child_theme_id
|
||||||
|
FROM child_themes
|
||||||
|
WHERE parent_theme_id NOT IN (SELECT child_theme_id FROM child_themes)
|
||||||
|
")
|
||||||
|
|
||||||
|
top_parents.each do |top_parent|
|
||||||
|
migrate_child(top_parent, top_parent)
|
||||||
|
end
|
||||||
|
|
||||||
|
if @handled.size > 0
|
||||||
|
execute("
|
||||||
|
DELETE FROM child_themes
|
||||||
|
WHERE parent_theme_id NOT IN (#{top_parents.map(&:parent_theme_id).join(", ")})
|
||||||
|
")
|
||||||
|
end
|
||||||
|
|
||||||
|
execute("
|
||||||
|
UPDATE themes
|
||||||
|
SET user_selectable = false
|
||||||
|
FROM child_themes
|
||||||
|
WHERE themes.id = child_themes.child_theme_id
|
||||||
|
AND themes.user_selectable = true
|
||||||
|
")
|
||||||
|
|
||||||
|
default = DB.query_single("SELECT value FROM site_settings WHERE name = 'default_theme_id'").first
|
||||||
|
if default
|
||||||
|
default_child = DB.query("SELECT 1 AS one FROM child_themes WHERE child_theme_id = ?", default.to_i).present?
|
||||||
|
execute("DELETE FROM site_settings WHERE name = 'default_theme_id'") if default_child
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
raise ActiveRecord::IrreversibleMigration
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def migrate_child(parent, top_parent)
|
||||||
|
unless already_exists?(top_parent.parent_theme_id, parent.child_theme_id)
|
||||||
|
execute("
|
||||||
|
INSERT INTO child_themes (parent_theme_id, child_theme_id, created_at, updated_at)
|
||||||
|
VALUES (#{top_parent.parent_theme_id}, #{parent.child_theme_id}, now(), now())
|
||||||
|
")
|
||||||
|
end
|
||||||
|
|
||||||
|
@handled << [top_parent.parent_theme_id, parent.parent_theme_id, parent.child_theme_id]
|
||||||
|
|
||||||
|
children = DB.query("
|
||||||
|
SELECT parent_theme_id, child_theme_id
|
||||||
|
FROM child_themes
|
||||||
|
WHERE parent_theme_id = :child", child: parent.child_theme_id
|
||||||
|
)
|
||||||
|
|
||||||
|
children.each do |child|
|
||||||
|
unless @handled.include?([top_parent.parent_theme_id, child.parent_theme_id, child.child_theme_id])
|
||||||
|
migrate_child(child, top_parent)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def already_exists?(parent, child)
|
||||||
|
DB.query("SELECT 1 AS one FROM child_themes WHERE child_theme_id = :child AND parent_theme_id = :parent", child: child, parent: parent).present?
|
||||||
|
end
|
||||||
|
end
|
@@ -359,9 +359,15 @@ class Guardian
|
|||||||
end
|
end
|
||||||
|
|
||||||
def allow_themes?(theme_ids)
|
def allow_themes?(theme_ids)
|
||||||
theme_ids = [theme_ids] unless theme_ids.is_a?(Array)
|
if is_staff? && (theme_ids - Theme.theme_ids).blank?
|
||||||
allowed_ids = is_staff? ? Theme.theme_ids : Theme.user_theme_ids
|
return true
|
||||||
(theme_ids - allowed_ids.to_a).empty?
|
end
|
||||||
|
|
||||||
|
parent = theme_ids.first
|
||||||
|
components = theme_ids[1..-1] || []
|
||||||
|
|
||||||
|
Theme.user_theme_ids.include?(parent) &&
|
||||||
|
(components - Theme.components_for(parent)).empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@@ -66,16 +66,16 @@ module Middleware
|
|||||||
end
|
end
|
||||||
|
|
||||||
def cache_key
|
def cache_key
|
||||||
@cache_key ||= "ANON_CACHE_#{@env["HTTP_ACCEPT"]}_#{@env["HTTP_HOST"]}#{@env["REQUEST_URI"]}|m=#{is_mobile?}|c=#{is_crawler?}|b=#{has_brotli?}|t=#{theme_id}"
|
@cache_key ||= "ANON_CACHE_#{@env["HTTP_ACCEPT"]}_#{@env["HTTP_HOST"]}#{@env["REQUEST_URI"]}|m=#{is_mobile?}|c=#{is_crawler?}|b=#{has_brotli?}|t=#{theme_ids.join(",")}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def theme_id
|
def theme_ids
|
||||||
ids, _ = @request.cookies['theme_ids']&.split('|')
|
ids, _ = @request.cookies['theme_ids']&.split('|')
|
||||||
ids = ids&.split(",")&.map(&:to_i)
|
ids = ids&.split(",")&.map(&:to_i)
|
||||||
if ids && Guardian.new.allow_themes?(ids)
|
if ids && Guardian.new.allow_themes?(ids)
|
||||||
ids.first
|
Theme.transform_ids(ids)
|
||||||
else
|
else
|
||||||
nil
|
[]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@ class Stylesheet::Manager
|
|||||||
CACHE_PATH ||= 'tmp/stylesheet-cache'
|
CACHE_PATH ||= 'tmp/stylesheet-cache'
|
||||||
MANIFEST_DIR ||= "#{Rails.root}/tmp/cache/assets/#{Rails.env}"
|
MANIFEST_DIR ||= "#{Rails.root}/tmp/cache/assets/#{Rails.env}"
|
||||||
MANIFEST_FULL_PATH ||= "#{MANIFEST_DIR}/stylesheet-manifest"
|
MANIFEST_FULL_PATH ||= "#{MANIFEST_DIR}/stylesheet-manifest"
|
||||||
|
THEME_REGEX ||= /_theme$/
|
||||||
|
|
||||||
@lock = Mutex.new
|
@lock = Mutex.new
|
||||||
|
|
||||||
@@ -19,38 +20,65 @@ class Stylesheet::Manager
|
|||||||
cache.hash.keys.select { |k| k =~ /theme/ }.each { |k|cache.delete(k) }
|
cache.hash.keys.select { |k| k =~ /theme/ }.each { |k|cache.delete(k) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.stylesheet_href(target = :desktop, theme_id = :missing)
|
def self.stylesheet_data(target = :desktop, theme_ids = :missing)
|
||||||
href = stylesheet_link_tag(target, 'all', theme_id)
|
stylesheet_details(target, "all", theme_ids)
|
||||||
if href
|
|
||||||
href.split(/["']/)[1]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.stylesheet_link_tag(target = :desktop, media = 'all', theme_id = :missing)
|
def self.stylesheet_link_tag(target = :desktop, media = 'all', theme_ids = :missing)
|
||||||
|
stylesheets = stylesheet_details(target, media, theme_ids)
|
||||||
|
stylesheets.map do |stylesheet|
|
||||||
|
href = stylesheet[:new_href]
|
||||||
|
theme_id = stylesheet[:theme_id]
|
||||||
|
data_theme_id = theme_id ? "data-theme-id=\"#{theme_id}\"" : ""
|
||||||
|
%[<link href="#{href}" media="#{media}" rel="stylesheet" data-target="#{target}" #{data_theme_id}/>]
|
||||||
|
end.join("\n").html_safe
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.stylesheet_details(target = :desktop, media = 'all', theme_ids = :missing)
|
||||||
|
if theme_ids == :missing
|
||||||
|
theme_ids = [SiteSetting.default_theme_id]
|
||||||
|
end
|
||||||
|
|
||||||
target = target.to_sym
|
target = target.to_sym
|
||||||
|
|
||||||
if theme_id == :missing
|
theme_ids = [theme_ids] unless Array === theme_ids
|
||||||
theme_id = SiteSetting.default_theme_id
|
theme_ids = [theme_ids.first] unless target =~ THEME_REGEX
|
||||||
end
|
theme_ids = Theme.transform_ids(theme_ids, extend: false)
|
||||||
|
|
||||||
current_hostname = Discourse.current_hostname
|
current_hostname = Discourse.current_hostname
|
||||||
cache_key = "#{target}_#{theme_id}_#{current_hostname}"
|
|
||||||
tag = cache[cache_key]
|
|
||||||
|
|
||||||
return tag.dup.html_safe if tag
|
array_cache_key = "array_themes_#{theme_ids.join(",")}_#{target}_#{current_hostname}"
|
||||||
|
stylesheets = cache[array_cache_key]
|
||||||
|
return stylesheets if stylesheets.present?
|
||||||
|
|
||||||
@lock.synchronize do
|
@lock.synchronize do
|
||||||
builder = self.new(target, theme_id)
|
stylesheets = []
|
||||||
if builder.is_theme? && !builder.theme
|
theme_ids.each do |theme_id|
|
||||||
tag = ""
|
data = { target: target }
|
||||||
else
|
cache_key = "path_#{target}_#{theme_id}_#{current_hostname}"
|
||||||
builder.compile unless File.exists?(builder.stylesheet_fullpath)
|
href = cache[cache_key]
|
||||||
tag = %[<link href="#{builder.stylesheet_path(current_hostname)}" media="#{media}" rel="stylesheet" data-target="#{target}"/>]
|
|
||||||
end
|
|
||||||
|
|
||||||
cache[cache_key] = tag
|
unless href
|
||||||
tag.dup.html_safe
|
builder = self.new(target, theme_id)
|
||||||
|
is_theme = builder.is_theme?
|
||||||
|
has_theme = builder.theme.present?
|
||||||
|
|
||||||
|
if is_theme && !has_theme
|
||||||
|
next
|
||||||
|
else
|
||||||
|
data[:theme_id] = builder.theme.id if has_theme && is_theme
|
||||||
|
builder.compile unless File.exists?(builder.stylesheet_fullpath)
|
||||||
|
href = builder.stylesheet_path(current_hostname)
|
||||||
|
end
|
||||||
|
cache[cache_key] = href
|
||||||
|
end
|
||||||
|
|
||||||
|
data[:theme_id] = theme_id if theme_id.present? && data[:theme_id].blank?
|
||||||
|
data[:new_href] = href
|
||||||
|
stylesheets << data
|
||||||
|
end
|
||||||
|
cache[array_cache_key] = stylesheets.freeze
|
||||||
|
stylesheets
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -100,6 +128,10 @@ class Stylesheet::Manager
|
|||||||
end.compact.max.to_i
|
end.compact.max.to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.cache_fullpath
|
||||||
|
"#{Rails.root}/#{CACHE_PATH}"
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(target = :desktop, theme_id)
|
def initialize(target = :desktop, theme_id)
|
||||||
@target = target
|
@target = target
|
||||||
@theme_id = theme_id
|
@theme_id = theme_id
|
||||||
@@ -162,10 +194,6 @@ class Stylesheet::Manager
|
|||||||
css
|
css
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.cache_fullpath
|
|
||||||
"#{Rails.root}/#{CACHE_PATH}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def cache_fullpath
|
def cache_fullpath
|
||||||
self.class.cache_fullpath
|
self.class.cache_fullpath
|
||||||
end
|
end
|
||||||
@@ -225,7 +253,7 @@ class Stylesheet::Manager
|
|||||||
end
|
end
|
||||||
|
|
||||||
def is_theme?
|
def is_theme?
|
||||||
!!(@target.to_s =~ /_theme$/)
|
!!(@target.to_s =~ THEME_REGEX)
|
||||||
end
|
end
|
||||||
|
|
||||||
# digest encodes the things that trigger a recompile
|
# digest encodes the things that trigger a recompile
|
||||||
@@ -240,7 +268,7 @@ class Stylesheet::Manager
|
|||||||
end
|
end
|
||||||
|
|
||||||
def theme
|
def theme
|
||||||
@theme ||= (Theme.find_by(id: @theme_id) || :nil)
|
@theme ||= Theme.find_by(id: @theme_id) || :nil
|
||||||
@theme == :nil ? nil : @theme
|
@theme == :nil ? nil : @theme
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -271,7 +299,16 @@ class Stylesheet::Manager
|
|||||||
end
|
end
|
||||||
|
|
||||||
def settings_digest
|
def settings_digest
|
||||||
Digest::SHA1.hexdigest((theme&.included_settings || {}).to_json)
|
fields = ThemeField.where(
|
||||||
|
name: "yaml",
|
||||||
|
type_id: ThemeField.types[:yaml],
|
||||||
|
theme_id: @theme_id
|
||||||
|
).pluck(:updated_at)
|
||||||
|
|
||||||
|
settings = ThemeSetting.where(theme_id: @theme_id).pluck(:updated_at)
|
||||||
|
timestamps = fields.concat(settings).map!(&:to_f).sort!.join(",")
|
||||||
|
|
||||||
|
Digest::SHA1.hexdigest(timestamps)
|
||||||
end
|
end
|
||||||
|
|
||||||
def uploads_digest
|
def uploads_digest
|
||||||
|
@@ -8,7 +8,10 @@ module Stylesheet
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.theme_id
|
def self.theme_id
|
||||||
@theme_id || SiteSetting.default_theme_id
|
if @theme_id.blank? && SiteSetting.default_theme_id != -1
|
||||||
|
@theme_id = SiteSetting.default_theme_id
|
||||||
|
end
|
||||||
|
@theme_id
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.watch(paths = nil)
|
def self.watch(paths = nil)
|
||||||
@@ -76,12 +79,8 @@ module Stylesheet
|
|||||||
Stylesheet::Manager.cache.clear
|
Stylesheet::Manager.cache.clear
|
||||||
|
|
||||||
message = ["desktop", "mobile", "admin"].map do |name|
|
message = ["desktop", "mobile", "admin"].map do |name|
|
||||||
{
|
Stylesheet::Manager.stylesheet_data(name.to_sym, Stylesheet::Watcher.theme_id)
|
||||||
target: name,
|
end.flatten
|
||||||
new_href: Stylesheet::Manager.stylesheet_href(name.to_sym),
|
|
||||||
theme_id: Stylesheet::Watcher.theme_id
|
|
||||||
}
|
|
||||||
end
|
|
||||||
MessageBus.publish '/file-change', message
|
MessageBus.publish '/file-change', message
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@@ -2537,6 +2537,46 @@ describe Guardian do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#allow_themes?" do
|
||||||
|
let(:theme) { Fabricate(:theme) }
|
||||||
|
let(:theme2) { Fabricate(:theme) }
|
||||||
|
|
||||||
|
it "allows staff to use any themes" do
|
||||||
|
expect(Guardian.new(moderator).allow_themes?([theme.id, theme2.id])).to eq(true)
|
||||||
|
expect(Guardian.new(admin).allow_themes?([theme.id, theme2.id])).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "only allows normal users to use user-selectable themes or default theme" do
|
||||||
|
user_guardian = Guardian.new(user)
|
||||||
|
|
||||||
|
expect(user_guardian.allow_themes?([theme.id, theme2.id])).to eq(false)
|
||||||
|
expect(user_guardian.allow_themes?([theme.id])).to eq(false)
|
||||||
|
expect(user_guardian.allow_themes?([theme2.id])).to eq(false)
|
||||||
|
|
||||||
|
theme.set_default!
|
||||||
|
expect(user_guardian.allow_themes?([theme.id])).to eq(true)
|
||||||
|
expect(user_guardian.allow_themes?([theme2.id])).to eq(false)
|
||||||
|
expect(user_guardian.allow_themes?([theme.id, theme2.id])).to eq(false)
|
||||||
|
|
||||||
|
theme2.update!(user_selectable: true)
|
||||||
|
expect(user_guardian.allow_themes?([theme2.id])).to eq(true)
|
||||||
|
expect(user_guardian.allow_themes?([theme2.id, theme.id])).to eq(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows child themes to be only used with their parent" do
|
||||||
|
user_guardian = Guardian.new(user)
|
||||||
|
|
||||||
|
theme.update!(user_selectable: true)
|
||||||
|
theme2.update!(user_selectable: true)
|
||||||
|
expect(user_guardian.allow_themes?([theme.id, theme2.id])).to eq(false)
|
||||||
|
|
||||||
|
theme2.update!(user_selectable: false)
|
||||||
|
theme.add_child_theme!(theme2)
|
||||||
|
expect(user_guardian.allow_themes?([theme.id, theme2.id])).to eq(true)
|
||||||
|
expect(user_guardian.allow_themes?([theme2.id])).to eq(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'can_wiki?' do
|
describe 'can_wiki?' do
|
||||||
let(:post) { build(:post, created_at: 1.minute.ago) }
|
let(:post) { build(:post, created_at: 1.minute.ago) }
|
||||||
|
|
||||||
|
@@ -32,7 +32,7 @@ describe Middleware::AnonymousCache::Helper do
|
|||||||
|
|
||||||
context "per theme cache" do
|
context "per theme cache" do
|
||||||
it "handles theme keys" do
|
it "handles theme keys" do
|
||||||
theme = Theme.create(name: "test", user_id: -1, user_selectable: true)
|
theme = Fabricate(:theme, user_selectable: true)
|
||||||
|
|
||||||
with_bad_theme_key = new_helper("HTTP_COOKIE" => "theme_ids=abc").cache_key
|
with_bad_theme_key = new_helper("HTTP_COOKIE" => "theme_ids=abc").cache_key
|
||||||
with_no_theme_key = new_helper().cache_key
|
with_no_theme_key = new_helper().cache_key
|
||||||
|
@@ -8,7 +8,7 @@ describe Stylesheet::Manager do
|
|||||||
link = Stylesheet::Manager.stylesheet_link_tag(:embedded_theme)
|
link = Stylesheet::Manager.stylesheet_link_tag(:embedded_theme)
|
||||||
expect(link).to eq("")
|
expect(link).to eq("")
|
||||||
|
|
||||||
theme = Theme.create(name: "embedded", user_id: -1)
|
theme = Fabricate(:theme)
|
||||||
SiteSetting.default_theme_id = theme.id
|
SiteSetting.default_theme_id = theme.id
|
||||||
|
|
||||||
link = Stylesheet::Manager.stylesheet_link_tag(:embedded_theme)
|
link = Stylesheet::Manager.stylesheet_link_tag(:embedded_theme)
|
||||||
@@ -16,10 +16,7 @@ describe Stylesheet::Manager do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'can correctly compile theme css' do
|
it 'can correctly compile theme css' do
|
||||||
theme = Theme.new(
|
theme = Fabricate(:theme)
|
||||||
name: 'parent',
|
|
||||||
user_id: -1
|
|
||||||
)
|
|
||||||
|
|
||||||
theme.set_field(target: :common, name: "scss", value: ".common{.scss{color: red;}}")
|
theme.set_field(target: :common, name: "scss", value: ".common{.scss{color: red;}}")
|
||||||
theme.set_field(target: :desktop, name: "scss", value: ".desktop{.scss{color: red;}}")
|
theme.set_field(target: :desktop, name: "scss", value: ".desktop{.scss{color: red;}}")
|
||||||
@@ -28,10 +25,7 @@ describe Stylesheet::Manager do
|
|||||||
|
|
||||||
theme.save!
|
theme.save!
|
||||||
|
|
||||||
child_theme = Theme.new(
|
child_theme = Fabricate(:theme)
|
||||||
name: 'parent',
|
|
||||||
user_id: -1,
|
|
||||||
)
|
|
||||||
|
|
||||||
child_theme.set_field(target: :common, name: "scss", value: ".child_common{.scss{color: red;}}")
|
child_theme.set_field(target: :common, name: "scss", value: ".child_common{.scss{color: red;}}")
|
||||||
child_theme.set_field(target: :desktop, name: "scss", value: ".child_desktop{.scss{color: red;}}")
|
child_theme.set_field(target: :desktop, name: "scss", value: ".child_desktop{.scss{color: red;}}")
|
||||||
@@ -72,10 +66,7 @@ describe Stylesheet::Manager do
|
|||||||
|
|
||||||
it 'can correctly account for plugins in digest' do
|
it 'can correctly account for plugins in digest' do
|
||||||
|
|
||||||
theme = Theme.create!(
|
theme = Fabricate(:theme)
|
||||||
name: 'parent',
|
|
||||||
user_id: -1
|
|
||||||
)
|
|
||||||
|
|
||||||
manager = Stylesheet::Manager.new(:desktop_theme, theme.id)
|
manager = Stylesheet::Manager.new(:desktop_theme, theme.id)
|
||||||
digest1 = manager.digest
|
digest1 = manager.digest
|
||||||
@@ -92,10 +83,7 @@ describe Stylesheet::Manager do
|
|||||||
let(:image2) { file_from_fixtures("logo-dev.png") }
|
let(:image2) { file_from_fixtures("logo-dev.png") }
|
||||||
|
|
||||||
it 'can correctly account for theme uploads in digest' do
|
it 'can correctly account for theme uploads in digest' do
|
||||||
theme = Theme.create!(
|
theme = Fabricate(:theme)
|
||||||
name: 'parent',
|
|
||||||
user_id: -1
|
|
||||||
)
|
|
||||||
|
|
||||||
upload = UploadCreator.new(image, "logo.png").create_for(-1)
|
upload = UploadCreator.new(image, "logo.png").create_for(-1)
|
||||||
field = ThemeField.create!(
|
field = ThemeField.create!(
|
||||||
@@ -130,10 +118,7 @@ describe Stylesheet::Manager do
|
|||||||
|
|
||||||
describe 'color_scheme_digest' do
|
describe 'color_scheme_digest' do
|
||||||
it "changes with category background image" do
|
it "changes with category background image" do
|
||||||
theme = Theme.new(
|
theme = Fabricate(:theme)
|
||||||
name: 'parent',
|
|
||||||
user_id: -1
|
|
||||||
)
|
|
||||||
category1 = Fabricate(:category, uploaded_background_id: 123, updated_at: 1.week.ago)
|
category1 = Fabricate(:category, uploaded_background_id: 123, updated_at: 1.week.ago)
|
||||||
category2 = Fabricate(:category, uploaded_background_id: 456, updated_at: 2.days.ago)
|
category2 = Fabricate(:category, uploaded_background_id: 456, updated_at: 2.days.ago)
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@ require 'theme_settings_manager'
|
|||||||
describe ThemeSettingsManager do
|
describe ThemeSettingsManager do
|
||||||
|
|
||||||
let(:theme_settings) do
|
let(:theme_settings) do
|
||||||
theme = Theme.create!(name: "awesome theme", user_id: -1)
|
theme = Fabricate(:theme)
|
||||||
yaml = File.read("#{Rails.root}/spec/fixtures/theme_settings/valid_settings.yaml")
|
yaml = File.read("#{Rails.root}/spec/fixtures/theme_settings/valid_settings.yaml")
|
||||||
theme.set_field(target: :settings, name: "yaml", value: yaml)
|
theme.set_field(target: :settings, name: "yaml", value: yaml)
|
||||||
theme.save!
|
theme.save!
|
||||||
|
@@ -194,6 +194,7 @@ describe Wizard::StepUpdater do
|
|||||||
|
|
||||||
context "without an existing scheme" do
|
context "without an existing scheme" do
|
||||||
it "creates the scheme" do
|
it "creates the scheme" do
|
||||||
|
ColorScheme.destroy_all
|
||||||
updater = wizard.create_updater('colors', theme_previews: 'Dark', allow_dark_light_selection: true)
|
updater = wizard.create_updater('colors', theme_previews: 'Dark', allow_dark_light_selection: true)
|
||||||
updater.update
|
updater.update
|
||||||
expect(updater.success?).to eq(true)
|
expect(updater.success?).to eq(true)
|
||||||
|
4
spec/fabricators/theme_fabricator.rb
Normal file
4
spec/fabricators/theme_fabricator.rb
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Fabricator(:theme) do
|
||||||
|
name { sequence(:name) { |i| "Cool theme #{i + 1}" } }
|
||||||
|
user
|
||||||
|
end
|
@@ -191,12 +191,12 @@ describe UserNotifications do
|
|||||||
Fabricate(:color_scheme_color, name: 'header_background', hex: '1E1E1E'),
|
Fabricate(:color_scheme_color, name: 'header_background', hex: '1E1E1E'),
|
||||||
Fabricate(:color_scheme_color, name: 'tertiary', hex: '858585')
|
Fabricate(:color_scheme_color, name: 'tertiary', hex: '858585')
|
||||||
])
|
])
|
||||||
theme = Theme.create!(
|
theme = Fabricate(:theme,
|
||||||
name: 'my name',
|
|
||||||
user_id: Fabricate(:admin).id,
|
|
||||||
user_selectable: true,
|
user_selectable: true,
|
||||||
|
user: Fabricate(:admin),
|
||||||
color_scheme_id: cs.id
|
color_scheme_id: cs.id
|
||||||
)
|
)
|
||||||
|
|
||||||
theme.set_default!
|
theme.set_default!
|
||||||
|
|
||||||
html = subject.html_part.body.to_s
|
html = subject.html_part.body.to_s
|
||||||
|
@@ -338,7 +338,7 @@ describe AdminDashboardData do
|
|||||||
|
|
||||||
describe '#out_of_date_themes' do
|
describe '#out_of_date_themes' do
|
||||||
let(:remote) { RemoteTheme.create!(remote_url: "https://github.com/org/testtheme") }
|
let(:remote) { RemoteTheme.create!(remote_url: "https://github.com/org/testtheme") }
|
||||||
let!(:theme) { Theme.create!(remote_theme_id: remote.id, name: "Test< Theme", user_id: -1) }
|
let!(:theme) { Fabricate(:theme, remote_theme: remote, name: "Test< Theme") }
|
||||||
|
|
||||||
it "outputs correctly formatted html" do
|
it "outputs correctly formatted html" do
|
||||||
remote.update!(local_version: "old version", remote_version: "new version", commits_behind: 2)
|
remote.update!(local_version: "old version", remote_version: "new version", commits_behind: 2)
|
||||||
|
43
spec/models/child_theme_spec.rb
Normal file
43
spec/models/child_theme_spec.rb
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe ChildTheme do
|
||||||
|
describe "validations" do
|
||||||
|
it "doesn't allow children to become parents or parents to become children" do
|
||||||
|
theme = Fabricate(:theme)
|
||||||
|
child = Fabricate(:theme)
|
||||||
|
|
||||||
|
child_theme = ChildTheme.new(parent_theme: theme, child_theme: child)
|
||||||
|
expect(child_theme.valid?).to eq(true)
|
||||||
|
child_theme.save!
|
||||||
|
|
||||||
|
grandchild = Fabricate(:theme)
|
||||||
|
child_theme = ChildTheme.new(parent_theme: child, child_theme: grandchild)
|
||||||
|
expect(child_theme.valid?).to eq(false)
|
||||||
|
expect(child_theme.errors.full_messages).to contain_exactly(I18n.t("themes.errors.no_multilevels_components"))
|
||||||
|
|
||||||
|
grandparent = Fabricate(:theme)
|
||||||
|
child_theme = ChildTheme.new(parent_theme: grandparent, child_theme: theme)
|
||||||
|
expect(child_theme.valid?).to eq(false)
|
||||||
|
expect(child_theme.errors.full_messages).to contain_exactly(I18n.t("themes.errors.no_multilevels_components"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't allow a user selectable theme to be a child" do
|
||||||
|
parent = Fabricate(:theme)
|
||||||
|
selectable_theme = Fabricate(:theme, user_selectable: true)
|
||||||
|
|
||||||
|
child_theme = ChildTheme.new(parent_theme: parent, child_theme: selectable_theme)
|
||||||
|
expect(child_theme.valid?).to eq(false)
|
||||||
|
expect(child_theme.errors.full_messages).to contain_exactly(I18n.t("themes.errors.component_no_user_selectable"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't allow a default theme to be child" do
|
||||||
|
parent = Fabricate(:theme)
|
||||||
|
default = Fabricate(:theme)
|
||||||
|
default.set_default!
|
||||||
|
|
||||||
|
child_theme = ChildTheme.new(parent_theme: parent, child_theme: default)
|
||||||
|
expect(child_theme.valid?).to eq(false)
|
||||||
|
expect(child_theme.errors.full_messages).to contain_exactly(I18n.t("themes.errors.component_no_default"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@@ -10,15 +10,15 @@ describe ColorScheme do
|
|||||||
|
|
||||||
it "correctly invalidates theme css when changed" do
|
it "correctly invalidates theme css when changed" do
|
||||||
scheme = ColorScheme.create_from_base(name: 'Bob')
|
scheme = ColorScheme.create_from_base(name: 'Bob')
|
||||||
theme = Theme.new(name: 'Amazing Theme', color_scheme_id: scheme.id, user_id: -1)
|
theme = Fabricate(:theme, color_scheme_id: scheme.id)
|
||||||
theme.set_field(name: :scss, target: :desktop, value: '.bob {color: $primary;}')
|
theme.set_field(name: :scss, target: :desktop, value: '.bob {color: $primary;}')
|
||||||
theme.save!
|
theme.save!
|
||||||
|
|
||||||
href = Stylesheet::Manager.stylesheet_href(:desktop_theme, theme.id)
|
href = Stylesheet::Manager.stylesheet_data(:desktop_theme, theme.id)[0][:new_href]
|
||||||
|
|
||||||
ColorSchemeRevisor.revise(scheme, colors: [{ name: 'primary', hex: 'bbb' }])
|
ColorSchemeRevisor.revise(scheme, colors: [{ name: 'primary', hex: 'bbb' }])
|
||||||
|
|
||||||
href2 = Stylesheet::Manager.stylesheet_href(:desktop_theme, theme.id)
|
href2 = Stylesheet::Manager.stylesheet_data(:desktop_theme, theme.id)[0][:new_href]
|
||||||
|
|
||||||
expect(href).not_to eq(href2)
|
expect(href).not_to eq(href2)
|
||||||
end
|
end
|
||||||
|
@@ -189,7 +189,7 @@ describe RemoteTheme do
|
|||||||
|
|
||||||
context ".out_of_date_themes" do
|
context ".out_of_date_themes" do
|
||||||
let(:remote) { RemoteTheme.create!(remote_url: "https://github.com/org/testtheme") }
|
let(:remote) { RemoteTheme.create!(remote_url: "https://github.com/org/testtheme") }
|
||||||
let!(:theme) { Theme.create!(remote_theme_id: remote.id, name: "Test Theme", user_id: -1) }
|
let!(:theme) { Fabricate(:theme, remote_theme: remote) }
|
||||||
|
|
||||||
it "finds out of date themes" do
|
it "finds out of date themes" do
|
||||||
remote.update!(local_version: "old version", remote_version: "new version", commits_behind: 2)
|
remote.update!(local_version: "old version", remote_version: "new version", commits_behind: 2)
|
||||||
|
@@ -17,9 +17,9 @@ describe Site do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "includes user themes and expires them as needed" do
|
it "includes user themes and expires them as needed" do
|
||||||
default_theme = Theme.create!(user_id: -1, name: 'default')
|
default_theme = Fabricate(:theme)
|
||||||
SiteSetting.default_theme_id = default_theme.id
|
SiteSetting.default_theme_id = default_theme.id
|
||||||
user_theme = Theme.create!(user_id: -1, name: 'user theme', user_selectable: true)
|
user_theme = Fabricate(:theme, user_selectable: true)
|
||||||
|
|
||||||
anon_guardian = Guardian.new
|
anon_guardian = Guardian.new
|
||||||
user_guardian = Guardian.new(Fabricate(:user))
|
user_guardian = Guardian.new(Fabricate(:user))
|
||||||
|
@@ -7,6 +7,26 @@ describe ThemeField do
|
|||||||
ThemeField.destroy_all
|
ThemeField.destroy_all
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "scope: find_by_theme_ids" do
|
||||||
|
it "returns result in the specified order" do
|
||||||
|
theme = Fabricate(:theme)
|
||||||
|
theme2 = Fabricate(:theme)
|
||||||
|
theme3 = Fabricate(:theme)
|
||||||
|
|
||||||
|
(0..1).each do |num|
|
||||||
|
ThemeField.create!(theme: theme, target_id: num, name: "header", value: "<a>html</a>")
|
||||||
|
ThemeField.create!(theme: theme2, target_id: num, name: "header", value: "<a>html</a>")
|
||||||
|
ThemeField.create!(theme: theme3, target_id: num, name: "header", value: "<a>html</a>")
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(ThemeField.find_by_theme_ids(
|
||||||
|
[theme3.id, theme.id, theme2.id]
|
||||||
|
).pluck(:theme_id)).to eq(
|
||||||
|
[theme3.id, theme3.id, theme.id, theme.id, theme2.id, theme2.id]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it "correctly generates errors for transpiled js" do
|
it "correctly generates errors for transpiled js" do
|
||||||
html = <<HTML
|
html = <<HTML
|
||||||
<script type="text/discourse-plugin" version="0.8">
|
<script type="text/discourse-plugin" version="0.8">
|
||||||
|
@@ -19,15 +19,16 @@ describe Theme do
|
|||||||
end
|
end
|
||||||
|
|
||||||
let :customization do
|
let :customization do
|
||||||
Theme.create!(customization_params)
|
Fabricate(:theme, customization_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
let(:theme) { Fabricate(:theme, user: user) }
|
||||||
|
let(:child) { Fabricate(:theme, user: user) }
|
||||||
it 'can properly clean up color schemes' do
|
it 'can properly clean up color schemes' do
|
||||||
theme = Theme.create!(name: 'bob', user_id: -1)
|
|
||||||
scheme = ColorScheme.create!(theme_id: theme.id, name: 'test')
|
scheme = ColorScheme.create!(theme_id: theme.id, name: 'test')
|
||||||
scheme2 = ColorScheme.create!(theme_id: theme.id, name: 'test2')
|
scheme2 = ColorScheme.create!(theme_id: theme.id, name: 'test2')
|
||||||
|
|
||||||
Theme.create!(name: 'bob', user_id: -1, color_scheme_id: scheme2.id)
|
Fabricate(:theme, color_scheme_id: scheme2.id)
|
||||||
|
|
||||||
theme.destroy!
|
theme.destroy!
|
||||||
scheme2.reload
|
scheme2.reload
|
||||||
@@ -38,8 +39,6 @@ describe Theme do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'can support child themes' do
|
it 'can support child themes' do
|
||||||
child = Theme.new(name: '2', user_id: user.id)
|
|
||||||
|
|
||||||
child.set_field(target: :common, name: "header", value: "World")
|
child.set_field(target: :common, name: "header", value: "World")
|
||||||
child.set_field(target: :desktop, name: "header", value: "Desktop")
|
child.set_field(target: :desktop, name: "header", value: "Desktop")
|
||||||
child.set_field(target: :mobile, name: "header", value: "Mobile")
|
child.set_field(target: :mobile, name: "header", value: "Mobile")
|
||||||
@@ -54,7 +53,7 @@ describe Theme do
|
|||||||
|
|
||||||
expect(Theme.lookup_field(child.id, :mobile, :header)).to eq("Worldie\nMobile")
|
expect(Theme.lookup_field(child.id, :mobile, :header)).to eq("Worldie\nMobile")
|
||||||
|
|
||||||
parent = Theme.new(name: '1', user_id: user.id)
|
parent = Fabricate(:theme, user: user)
|
||||||
|
|
||||||
parent.set_field(target: :common, name: "header", value: "Common Parent")
|
parent.set_field(target: :common, name: "header", value: "Common Parent")
|
||||||
parent.set_field(target: :mobile, name: "header", value: "Mobile Parent")
|
parent.set_field(target: :mobile, name: "header", value: "Mobile Parent")
|
||||||
@@ -68,18 +67,39 @@ describe Theme do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'can correctly find parent themes' do
|
it 'can correctly find parent themes' do
|
||||||
grandchild = Theme.create!(name: 'grandchild', user_id: user.id)
|
theme.add_child_theme!(child)
|
||||||
child = Theme.create!(name: 'child', user_id: user.id)
|
|
||||||
theme = Theme.create!(name: 'theme', user_id: user.id)
|
expect(child.dependant_themes.length).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't allow multi-level theme components" do
|
||||||
|
grandchild = Fabricate(:theme, user: user)
|
||||||
|
grandparent = Fabricate(:theme, user: user)
|
||||||
|
|
||||||
theme.add_child_theme!(child)
|
theme.add_child_theme!(child)
|
||||||
child.add_child_theme!(grandchild)
|
expect do
|
||||||
|
child.add_child_theme!(grandchild)
|
||||||
|
end.to raise_error(Discourse::InvalidParameters, I18n.t("themes.errors.no_multilevels_components"))
|
||||||
|
|
||||||
expect(grandchild.dependant_themes.length).to eq(2)
|
expect do
|
||||||
|
grandparent.add_child_theme!(theme)
|
||||||
|
end.to raise_error(Discourse::InvalidParameters, I18n.t("themes.errors.no_multilevels_components"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't allow a child to be user selectable" do
|
||||||
|
theme.add_child_theme!(child)
|
||||||
|
child.update(user_selectable: true)
|
||||||
|
expect(child.errors.full_messages).to contain_exactly(I18n.t("themes.errors.component_no_user_selectable"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't allow a child to be set as the default theme" do
|
||||||
|
theme.add_child_theme!(child)
|
||||||
|
expect do
|
||||||
|
child.set_default!
|
||||||
|
end.to raise_error(Discourse::InvalidParameters, I18n.t("themes.errors.component_no_default"))
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'should correct bad html in body_tag_baked and head_tag_baked' do
|
it 'should correct bad html in body_tag_baked and head_tag_baked' do
|
||||||
theme = Theme.new(user_id: -1, name: "test")
|
|
||||||
theme.set_field(target: :common, name: "head_tag", value: "<b>I am bold")
|
theme.set_field(target: :common, name: "head_tag", value: "<b>I am bold")
|
||||||
theme.save!
|
theme.save!
|
||||||
|
|
||||||
@@ -95,7 +115,6 @@ describe Theme do
|
|||||||
{{hello}}
|
{{hello}}
|
||||||
</script>
|
</script>
|
||||||
HTML
|
HTML
|
||||||
theme = Theme.new(user_id: -1, name: "test")
|
|
||||||
theme.set_field(target: :common, name: "header", value: with_template)
|
theme.set_field(target: :common, name: "header", value: with_template)
|
||||||
theme.save!
|
theme.save!
|
||||||
|
|
||||||
@@ -106,8 +125,6 @@ HTML
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'should create body_tag_baked on demand if needed' do
|
it 'should create body_tag_baked on demand if needed' do
|
||||||
|
|
||||||
theme = Theme.new(user_id: -1, name: "test")
|
|
||||||
theme.set_field(target: :common, name: :body_tag, value: "<b>test")
|
theme.set_field(target: :common, name: :body_tag, value: "<b>test")
|
||||||
theme.save
|
theme.save
|
||||||
|
|
||||||
@@ -116,6 +133,41 @@ HTML
|
|||||||
expect(Theme.lookup_field(theme.id, :desktop, :body_tag)).to match(/<b>test<\/b>/)
|
expect(Theme.lookup_field(theme.id, :desktop, :body_tag)).to match(/<b>test<\/b>/)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'can find fields for multiple themes' do
|
||||||
|
theme2 = Fabricate(:theme)
|
||||||
|
|
||||||
|
theme.set_field(target: :common, name: :body_tag, value: "<b>testtheme1</b>")
|
||||||
|
theme2.set_field(target: :common, name: :body_tag, value: "<b>theme2test</b>")
|
||||||
|
theme.save!
|
||||||
|
theme2.save!
|
||||||
|
|
||||||
|
field = Theme.lookup_field([theme.id, theme2.id], :desktop, :body_tag)
|
||||||
|
expect(field).to match(/<b>testtheme1<\/b>/)
|
||||||
|
expect(field).to match(/<b>theme2test<\/b>/)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".transform_ids" do
|
||||||
|
it "adds the child themes of the parent" do
|
||||||
|
child = Fabricate(:theme, id: 97)
|
||||||
|
child2 = Fabricate(:theme, id: 96)
|
||||||
|
|
||||||
|
theme.add_child_theme!(child)
|
||||||
|
theme.add_child_theme!(child2)
|
||||||
|
expect(Theme.transform_ids([theme.id])).to eq([theme.id, child2.id, child.id])
|
||||||
|
expect(Theme.transform_ids([theme.id, 94, 90])).to eq([theme.id, 90, 94, child2.id, child.id])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't insert children when extend is false" do
|
||||||
|
child = Fabricate(:theme, id: 97)
|
||||||
|
child2 = Fabricate(:theme, id: 96)
|
||||||
|
|
||||||
|
theme.add_child_theme!(child)
|
||||||
|
theme.add_child_theme!(child2)
|
||||||
|
expect(Theme.transform_ids([theme.id], extend: false)).to eq([theme.id])
|
||||||
|
expect(Theme.transform_ids([theme.id, 94, 90, 70, 70], extend: false)).to eq([theme.id, 70, 90, 94])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "plugin api" do
|
context "plugin api" do
|
||||||
def transpile(html)
|
def transpile(html)
|
||||||
f = ThemeField.create!(target_id: Theme.targets[:mobile], theme_id: 1, name: "after_header", value: html)
|
f = ThemeField.create!(target_id: Theme.targets[:mobile], theme_id: 1, name: "after_header", value: html)
|
||||||
@@ -152,14 +204,12 @@ HTML
|
|||||||
context 'theme vars' do
|
context 'theme vars' do
|
||||||
|
|
||||||
it 'works in parent theme' do
|
it 'works in parent theme' do
|
||||||
|
|
||||||
theme = Theme.new(name: 'theme', user_id: -1)
|
|
||||||
theme.set_field(target: :common, name: :scss, value: 'body {color: $magic; }')
|
theme.set_field(target: :common, name: :scss, value: 'body {color: $magic; }')
|
||||||
theme.set_field(target: :common, name: :magic, value: 'red', type: :theme_var)
|
theme.set_field(target: :common, name: :magic, value: 'red', type: :theme_var)
|
||||||
theme.set_field(target: :common, name: :not_red, value: 'red', type: :theme_var)
|
theme.set_field(target: :common, name: :not_red, value: 'red', type: :theme_var)
|
||||||
theme.save
|
theme.save
|
||||||
|
|
||||||
parent_theme = Theme.new(name: 'parent theme', user_id: -1)
|
parent_theme = Fabricate(:theme)
|
||||||
parent_theme.set_field(target: :common, name: :scss, value: 'body {background-color: $not_red; }')
|
parent_theme.set_field(target: :common, name: :scss, value: 'body {background-color: $not_red; }')
|
||||||
parent_theme.set_field(target: :common, name: :not_red, value: 'blue', type: :theme_var)
|
parent_theme.set_field(target: :common, name: :not_red, value: 'blue', type: :theme_var)
|
||||||
parent_theme.save
|
parent_theme.save
|
||||||
@@ -171,7 +221,6 @@ HTML
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'can generate scss based off theme vars' do
|
it 'can generate scss based off theme vars' do
|
||||||
theme = Theme.new(name: 'theme', user_id: -1)
|
|
||||||
theme.set_field(target: :common, name: :scss, value: 'body {color: $magic; content: quote($content)}')
|
theme.set_field(target: :common, name: :scss, value: 'body {color: $magic; content: quote($content)}')
|
||||||
theme.set_field(target: :common, name: :magic, value: 'red', type: :theme_var)
|
theme.set_field(target: :common, name: :magic, value: 'red', type: :theme_var)
|
||||||
theme.set_field(target: :common, name: :content, value: 'Sam\'s Test', type: :theme_var)
|
theme.set_field(target: :common, name: :content, value: 'Sam\'s Test', type: :theme_var)
|
||||||
@@ -187,7 +236,6 @@ HTML
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'can handle uploads based of ThemeField' do
|
it 'can handle uploads based of ThemeField' do
|
||||||
theme = Theme.new(name: 'theme', user_id: -1)
|
|
||||||
upload = UploadCreator.new(image, "logo.png").create_for(-1)
|
upload = UploadCreator.new(image, "logo.png").create_for(-1)
|
||||||
theme.set_field(target: :common, name: :logo, upload_id: upload.id, type: :theme_upload_var)
|
theme.set_field(target: :common, name: :logo, upload_id: upload.id, type: :theme_upload_var)
|
||||||
theme.set_field(target: :common, name: :scss, value: 'body {background-image: url($logo)}')
|
theme.set_field(target: :common, name: :scss, value: 'body {background-image: url($logo)}')
|
||||||
@@ -210,7 +258,6 @@ HTML
|
|||||||
|
|
||||||
context "theme settings" do
|
context "theme settings" do
|
||||||
it "allows values to be used in scss" do
|
it "allows values to be used in scss" do
|
||||||
theme = Theme.new(name: "awesome theme", user_id: -1)
|
|
||||||
theme.set_field(target: :settings, name: :yaml, value: "background_color: red\nfont_size: 25px")
|
theme.set_field(target: :settings, name: :yaml, value: "background_color: red\nfont_size: 25px")
|
||||||
theme.set_field(target: :common, name: :scss, value: 'body {background-color: $background_color; font-size: $font-size}')
|
theme.set_field(target: :common, name: :scss, value: 'body {background-color: $background_color; font-size: $font-size}')
|
||||||
theme.save!
|
theme.save!
|
||||||
@@ -227,7 +274,6 @@ HTML
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "allows values to be used in JS" do
|
it "allows values to be used in JS" do
|
||||||
theme = Theme.new(name: "awesome theme", user_id: -1)
|
|
||||||
theme.set_field(target: :settings, name: :yaml, value: "name: bob")
|
theme.set_field(target: :settings, name: :yaml, value: "name: bob")
|
||||||
theme.set_field(target: :common, name: :after_header, value: '<script type="text/discourse-plugin" version="1.0">alert(settings.name); let a = ()=>{};</script>')
|
theme.set_field(target: :common, name: :after_header, value: '<script type="text/discourse-plugin" version="1.0">alert(settings.name); let a = ()=>{};</script>')
|
||||||
theme.save!
|
theme.save!
|
||||||
@@ -263,26 +309,30 @@ HTML
|
|||||||
it 'correctly caches theme ids' do
|
it 'correctly caches theme ids' do
|
||||||
Theme.destroy_all
|
Theme.destroy_all
|
||||||
|
|
||||||
theme = Theme.create!(name: "bob", user_id: -1)
|
theme
|
||||||
|
theme2 = Fabricate(:theme)
|
||||||
|
|
||||||
expect(Theme.theme_ids).to eq(Set.new([theme.id]))
|
expect(Theme.theme_ids).to contain_exactly(theme.id, theme2.id)
|
||||||
expect(Theme.user_theme_ids).to eq(Set.new([]))
|
expect(Theme.user_theme_ids).to eq([])
|
||||||
|
|
||||||
theme.user_selectable = true
|
theme.update!(user_selectable: true)
|
||||||
theme.save
|
|
||||||
|
|
||||||
expect(Theme.user_theme_ids).to eq(Set.new([theme.id]))
|
expect(Theme.user_theme_ids).to contain_exactly(theme.id)
|
||||||
|
|
||||||
theme.user_selectable = false
|
theme2.update!(user_selectable: true)
|
||||||
theme.save
|
expect(Theme.user_theme_ids).to contain_exactly(theme.id, theme2.id)
|
||||||
|
|
||||||
|
theme.update!(user_selectable: false)
|
||||||
|
theme2.update!(user_selectable: false)
|
||||||
|
|
||||||
theme.set_default!
|
theme.set_default!
|
||||||
expect(Theme.user_theme_ids).to eq(Set.new([theme.id]))
|
expect(Theme.user_theme_ids).to contain_exactly(theme.id)
|
||||||
|
|
||||||
theme.destroy
|
theme.destroy
|
||||||
|
theme2.destroy
|
||||||
|
|
||||||
expect(Theme.theme_ids).to eq(Set.new([]))
|
expect(Theme.theme_ids).to eq([])
|
||||||
expect(Theme.user_theme_ids).to eq(Set.new([]))
|
expect(Theme.user_theme_ids).to eq([])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'correctly caches user_themes template' do
|
it 'correctly caches user_themes template' do
|
||||||
@@ -292,8 +342,7 @@ HTML
|
|||||||
user_themes = JSON.parse(json)["user_themes"]
|
user_themes = JSON.parse(json)["user_themes"]
|
||||||
expect(user_themes).to eq([])
|
expect(user_themes).to eq([])
|
||||||
|
|
||||||
theme = Theme.create!(name: "bob", user_id: -1, user_selectable: true)
|
theme = Fabricate(:theme, name: "bob", user_selectable: true)
|
||||||
theme.save!
|
|
||||||
|
|
||||||
json = Site.json_for(guardian)
|
json = Site.json_for(guardian)
|
||||||
user_themes = JSON.parse(json)["user_themes"].map { |t| t["name"] }
|
user_themes = JSON.parse(json)["user_themes"].map { |t| t["name"] }
|
||||||
@@ -320,7 +369,6 @@ HTML
|
|||||||
it 'handles settings cache correctly' do
|
it 'handles settings cache correctly' do
|
||||||
Theme.destroy_all
|
Theme.destroy_all
|
||||||
|
|
||||||
theme = Theme.create!(name: "awesome theme", user_id: -1)
|
|
||||||
expect(cached_settings(theme.id)).to eq("{}")
|
expect(cached_settings(theme.id)).to eq("{}")
|
||||||
|
|
||||||
theme.set_field(target: :settings, name: "yaml", value: "boolean_setting: true")
|
theme.set_field(target: :settings, name: "yaml", value: "boolean_setting: true")
|
||||||
@@ -330,7 +378,6 @@ HTML
|
|||||||
theme.settings.first.value = "false"
|
theme.settings.first.value = "false"
|
||||||
expect(cached_settings(theme.id)).to match(/\"boolean_setting\":false/)
|
expect(cached_settings(theme.id)).to match(/\"boolean_setting\":false/)
|
||||||
|
|
||||||
child = Theme.create!(name: "child theme", user_id: -1)
|
|
||||||
child.set_field(target: :settings, name: "yaml", value: "integer_setting: 54")
|
child.set_field(target: :settings, name: "yaml", value: "integer_setting: 54")
|
||||||
|
|
||||||
child.save!
|
child.save!
|
||||||
@@ -347,5 +394,4 @@ HTML
|
|||||||
expect(json).not_to match(/\"integer_setting\":54/)
|
expect(json).not_to match(/\"integer_setting\":54/)
|
||||||
expect(json).to match(/\"boolean_setting\":false/)
|
expect(json).to match(/\"boolean_setting\":false/)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@@ -30,7 +30,7 @@ describe Admin::StaffActionLogsController do
|
|||||||
|
|
||||||
describe '#diff' do
|
describe '#diff' do
|
||||||
it 'can generate diffs for theme changes' do
|
it 'can generate diffs for theme changes' do
|
||||||
theme = Theme.new(user_id: -1, name: 'bob')
|
theme = Fabricate(:theme)
|
||||||
theme.set_field(target: :mobile, name: :scss, value: 'body {.up}')
|
theme.set_field(target: :mobile, name: :scss, value: 'body {.up}')
|
||||||
theme.set_field(target: :common, name: :scss, value: 'omit-dupe')
|
theme.set_field(target: :common, name: :scss, value: 'omit-dupe')
|
||||||
|
|
||||||
|
@@ -49,7 +49,7 @@ describe Admin::ThemesController do
|
|||||||
|
|
||||||
it 'can import a theme with an upload' do
|
it 'can import a theme with an upload' do
|
||||||
upload = Fabricate(:upload)
|
upload = Fabricate(:upload)
|
||||||
theme = Theme.new(name: 'with-upload', user_id: -1)
|
theme = Fabricate(:theme)
|
||||||
upload = UploadCreator.new(image, "logo.png").create_for(-1)
|
upload = UploadCreator.new(image, "logo.png").create_for(-1)
|
||||||
theme.set_field(target: :common, name: :logo, upload_id: upload.id, type: :theme_upload_var)
|
theme.set_field(target: :common, name: :logo, upload_id: upload.id, type: :theme_upload_var)
|
||||||
theme.save!
|
theme.save!
|
||||||
@@ -93,7 +93,7 @@ describe Admin::ThemesController do
|
|||||||
ColorScheme.destroy_all
|
ColorScheme.destroy_all
|
||||||
Theme.destroy_all
|
Theme.destroy_all
|
||||||
|
|
||||||
theme = Theme.new(name: 'my name', user_id: -1)
|
theme = Fabricate(:theme)
|
||||||
theme.set_field(target: :common, name: :scss, value: '.body{color: black;}')
|
theme.set_field(target: :common, name: :scss, value: '.body{color: black;}')
|
||||||
theme.set_field(target: :desktop, name: :after_header, value: '<b>test</b>')
|
theme.set_field(target: :desktop, name: :after_header, value: '<b>test</b>')
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ describe Admin::ThemesController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe '#update' do
|
describe '#update' do
|
||||||
let(:theme) { Theme.create(name: 'my name', user_id: -1) }
|
let(:theme) { Fabricate(:theme) }
|
||||||
|
|
||||||
it 'can change default theme' do
|
it 'can change default theme' do
|
||||||
SiteSetting.default_theme_id = -1
|
SiteSetting.default_theme_id = -1
|
||||||
@@ -169,7 +169,7 @@ describe Admin::ThemesController do
|
|||||||
theme.set_field(target: :common, name: :scss, value: '.body{color: black;}')
|
theme.set_field(target: :common, name: :scss, value: '.body{color: black;}')
|
||||||
theme.save
|
theme.save
|
||||||
|
|
||||||
child_theme = Theme.create(name: 'my name', user_id: -1)
|
child_theme = Fabricate(:theme)
|
||||||
|
|
||||||
upload = Fabricate(:upload)
|
upload = Fabricate(:upload)
|
||||||
|
|
||||||
@@ -198,5 +198,17 @@ describe Admin::ThemesController do
|
|||||||
expect(json["theme"]["child_themes"].length).to eq(1)
|
expect(json["theme"]["child_themes"].length).to eq(1)
|
||||||
expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
|
expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns the right error message' do
|
||||||
|
parent = Fabricate(:theme)
|
||||||
|
parent.add_child_theme!(theme)
|
||||||
|
|
||||||
|
put "/admin/themes/#{theme.id}.json", params: {
|
||||||
|
theme: { default: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response.status).to eq(400)
|
||||||
|
expect(JSON.parse(response.body)["errors"].first).to include(I18n.t("themes.errors.component_no_default"))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@@ -33,4 +33,74 @@ RSpec.describe ApplicationController do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#handle_theme" do
|
||||||
|
let(:theme) { Fabricate(:theme, user_selectable: true) }
|
||||||
|
let(:theme2) { Fabricate(:theme, user_selectable: true) }
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:admin) { Fabricate(:admin) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "selects the theme the user has selected" do
|
||||||
|
user.user_option.update_columns(theme_ids: [theme.id])
|
||||||
|
|
||||||
|
get "/"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(controller.theme_ids).to eq([theme.id])
|
||||||
|
|
||||||
|
theme.update_attribute(:user_selectable, false)
|
||||||
|
|
||||||
|
get "/"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(controller.theme_ids).to eq([SiteSetting.default_theme_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can be overridden with a cookie" do
|
||||||
|
user.user_option.update_columns(theme_ids: [theme.id])
|
||||||
|
|
||||||
|
cookies['theme_ids'] = "#{theme2.id}|#{user.user_option.theme_key_seq}"
|
||||||
|
|
||||||
|
get "/"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(controller.theme_ids).to eq([theme2.id])
|
||||||
|
|
||||||
|
theme2.update!(user_selectable: false)
|
||||||
|
theme.add_child_theme!(theme2)
|
||||||
|
cookies['theme_ids'] = "#{theme.id},#{theme2.id}|#{user.user_option.theme_key_seq}"
|
||||||
|
|
||||||
|
get "/"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(controller.theme_ids).to eq([theme.id, theme2.id])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "falls back to the default theme when the user has no cookies or preferences" do
|
||||||
|
user.user_option.update_columns(theme_ids: [])
|
||||||
|
cookies["theme_ids"] = nil
|
||||||
|
theme2.set_default!
|
||||||
|
|
||||||
|
get "/"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(controller.theme_ids).to eq([theme2.id])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can be overridden with preview_theme_id param" do
|
||||||
|
sign_in(admin)
|
||||||
|
cookies['theme_ids'] = "#{theme.id},#{theme2.id}|#{admin.user_option.theme_key_seq}"
|
||||||
|
|
||||||
|
get "/", params: { preview_theme_id: theme2.id }
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(controller.theme_ids).to eq([theme2.id])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "cookie can fail back to user if out of sync" do
|
||||||
|
user.user_option.update_columns(theme_ids: [theme.id])
|
||||||
|
cookies['theme_ids'] = "#{theme2.id}|#{user.user_option.theme_key_seq - 1}"
|
||||||
|
|
||||||
|
get "/"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(controller.theme_ids).to eq([theme.id])
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@@ -83,7 +83,7 @@ describe EmbedController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "includes CSS from embedded_scss field" do
|
it "includes CSS from embedded_scss field" do
|
||||||
theme = Theme.create!(name: "Awesome blog", user_id: -1)
|
theme = Fabricate(:theme)
|
||||||
theme.set_default!
|
theme.set_default!
|
||||||
|
|
||||||
ThemeField.create!(
|
ThemeField.create!(
|
||||||
|
@@ -27,7 +27,7 @@ describe StylesheetsController do
|
|||||||
|
|
||||||
it 'can lookup theme specific css' do
|
it 'can lookup theme specific css' do
|
||||||
scheme = ColorScheme.create_from_base(name: "testing", colors: [])
|
scheme = ColorScheme.create_from_base(name: "testing", colors: [])
|
||||||
theme = Theme.create!(name: "test", color_scheme_id: scheme.id, user_id: -1)
|
theme = Fabricate(:theme, color_scheme_id: scheme.id)
|
||||||
|
|
||||||
builder = Stylesheet::Manager.new(:desktop, theme.id)
|
builder = Stylesheet::Manager.new(:desktop, theme.id)
|
||||||
builder.compile
|
builder.compile
|
||||||
|
@@ -1218,48 +1218,6 @@ RSpec.describe TopicsController do
|
|||||||
expect(response.headers['X-Robots-Tag']).to eq(nil)
|
expect(response.headers['X-Robots-Tag']).to eq(nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "themes" do
|
|
||||||
let(:theme) { Theme.create!(user_id: -1, name: 'bob', user_selectable: true) }
|
|
||||||
let(:theme2) { Theme.create!(user_id: -1, name: 'bobbob', user_selectable: true) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
sign_in(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "selects the theme the user has selected" do
|
|
||||||
user.user_option.update_columns(theme_ids: [theme.id])
|
|
||||||
|
|
||||||
get "/t/#{topic.id}"
|
|
||||||
expect(response).to be_redirect
|
|
||||||
expect(controller.theme_id).to eq(theme.id)
|
|
||||||
|
|
||||||
theme.update_attribute(:user_selectable, false)
|
|
||||||
|
|
||||||
get "/t/#{topic.id}"
|
|
||||||
expect(response).to be_redirect
|
|
||||||
expect(controller.theme_id).not_to eq(theme.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "can be overridden with a cookie" do
|
|
||||||
user.user_option.update_columns(theme_ids: [theme.id])
|
|
||||||
|
|
||||||
cookies['theme_ids'] = "#{theme2.id}|#{user.user_option.theme_key_seq}"
|
|
||||||
|
|
||||||
get "/t/#{topic.id}"
|
|
||||||
expect(response).to be_redirect
|
|
||||||
expect(controller.theme_id).to eq(theme2.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "cookie can fail back to user if out of sync" do
|
|
||||||
user.user_option.update_columns(theme_ids: [theme.id])
|
|
||||||
cookies['theme_ids'] = "#{theme2.id}|#{user.user_option.theme_key_seq - 1}"
|
|
||||||
|
|
||||||
get "/t/#{topic.id}"
|
|
||||||
expect(response).to be_redirect
|
|
||||||
expect(controller.theme_id).to eq(theme.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "doesn't store an incoming link when there's no referer" do
|
it "doesn't store an incoming link when there's no referer" do
|
||||||
expect {
|
expect {
|
||||||
get "/t/#{topic.id}.json"
|
get "/t/#{topic.id}.json"
|
||||||
|
@@ -1440,7 +1440,7 @@ describe UsersController do
|
|||||||
notification_level: TagUser.notification_levels[:watching]
|
notification_level: TagUser.notification_levels[:watching]
|
||||||
).pluck(:tag_id)).to contain_exactly(tags[0].id, tags[1].id)
|
).pluck(:tag_id)).to contain_exactly(tags[0].id, tags[1].id)
|
||||||
|
|
||||||
theme = Theme.create(name: "test", user_selectable: true, user_id: -1)
|
theme = Fabricate(:theme, user_selectable: true)
|
||||||
|
|
||||||
put "/u/#{user.username}.json", params: {
|
put "/u/#{user.username}.json", params: {
|
||||||
muted_usernames: "",
|
muted_usernames: "",
|
||||||
|
@@ -154,7 +154,7 @@ describe StaffActionLogger do
|
|||||||
end
|
end
|
||||||
|
|
||||||
let :theme do
|
let :theme do
|
||||||
Theme.new(name: 'bob', user_id: -1)
|
Fabricate(:theme)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "logs new site customizations" do
|
it "logs new site customizations" do
|
||||||
@@ -188,7 +188,7 @@ describe StaffActionLogger do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "creates a new UserHistory record" do
|
it "creates a new UserHistory record" do
|
||||||
theme = Theme.new(name: 'Banana')
|
theme = Fabricate(:theme)
|
||||||
theme.set_field(target: :common, name: :scss, value: "body{margin: 10px;}")
|
theme.set_field(target: :common, name: :scss, value: "body{margin: 10px;}")
|
||||||
|
|
||||||
log_record = logger.log_theme_destroy(theme)
|
log_record = logger.log_theme_destroy(theme)
|
||||||
|
@@ -478,7 +478,7 @@ describe UserMerger do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "updates themes" do
|
it "updates themes" do
|
||||||
theme = Theme.create!(name: 'my name', user_id: source_user.id)
|
theme = Fabricate(:theme, user: source_user)
|
||||||
merge_users!
|
merge_users!
|
||||||
|
|
||||||
expect(theme.reload.user_id).to eq(target_user.id)
|
expect(theme.reload.user_id).to eq(target_user.id)
|
||||||
|
@@ -82,7 +82,7 @@ describe UserUpdater do
|
|||||||
updater = UserUpdater.new(acting_user, user)
|
updater = UserUpdater.new(acting_user, user)
|
||||||
date_of_birth = Time.zone.now
|
date_of_birth = Time.zone.now
|
||||||
|
|
||||||
theme = Theme.create!(user_id: -1, name: "test", user_selectable: true)
|
theme = Fabricate(:theme, user_selectable: true)
|
||||||
|
|
||||||
seq = user.user_option.theme_key_seq
|
seq = user.user_option.theme_key_seq
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user