FEATURE: support uploads for themes
This allows themes to bundle various assets
This commit is contained in:
parent
f709899a1d
commit
bc0b9af576
|
@ -1,6 +1,9 @@
|
|||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
import { url } from 'discourse/lib/computed';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
|
||||
const THEME_UPLOAD_VAR = 2;
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
|
||||
|
@ -96,6 +99,16 @@ export default Ember.Controller.extend({
|
|||
});
|
||||
},
|
||||
|
||||
addUploadModal() {
|
||||
showModal('admin-add-upload', {admin: true, name: ''});
|
||||
},
|
||||
|
||||
addUpload(info) {
|
||||
let model = this.get("model");
|
||||
model.setField('common', info.name, '', info.upload_id, THEME_UPLOAD_VAR);
|
||||
model.saveChanges('theme_fields').catch(e => popupAjaxError(e));
|
||||
},
|
||||
|
||||
cancelChangeScheme() {
|
||||
this.set("colorSchemeId", this.get("model.color_scheme_id"));
|
||||
},
|
||||
|
@ -154,6 +167,10 @@ export default Ember.Controller.extend({
|
|||
this.get("model").addChildTheme(theme);
|
||||
},
|
||||
|
||||
removeUpload(upload) {
|
||||
this.get("model").removeField(upload);
|
||||
},
|
||||
|
||||
removeChildTheme(theme) {
|
||||
this.get("model").removeChildTheme(theme);
|
||||
},
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
// import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
adminCustomizeThemesShow: Ember.inject.controller(),
|
||||
|
||||
actions: {
|
||||
updateName() {
|
||||
let name = this.get('name');
|
||||
if (Em.isEmpty(name)) {
|
||||
name = $('#file-input')[0].files[0].name;
|
||||
this.set('name', name.split(".")[0]);
|
||||
}
|
||||
},
|
||||
upload() {
|
||||
|
||||
let options = {
|
||||
type: 'POST'
|
||||
};
|
||||
|
||||
options.processData = false;
|
||||
options.contentType = false;
|
||||
options.data = new FormData();
|
||||
let file = $('#file-input')[0].files[0];
|
||||
options.data.append('file', file);
|
||||
|
||||
ajax('/admin/themes/upload_asset', options).then(result=>{
|
||||
let upload = {
|
||||
upload_id: result.upload_id,
|
||||
name: this.get('name'),
|
||||
original_filename: file.name
|
||||
};
|
||||
this.get('adminCustomizeThemesShow').send('addUpload', upload);
|
||||
this.send('closeModal');
|
||||
}).catch(e => {
|
||||
popupAjaxError(e);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,6 +1,8 @@
|
|||
import RestModel from 'discourse/models/rest';
|
||||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const THEME_UPLOAD_VAR = 2;
|
||||
|
||||
const Theme = RestModel.extend({
|
||||
|
||||
@computed('theme_fields')
|
||||
|
@ -14,12 +16,26 @@ const Theme = RestModel.extend({
|
|||
let hash = {};
|
||||
if (fields) {
|
||||
fields.forEach(field=>{
|
||||
hash[field.target + " " + field.name] = field;
|
||||
if (!field.type_id || field.type_id < THEME_UPLOAD_VAR) {
|
||||
hash[this.getKey(field)] = field;
|
||||
}
|
||||
});
|
||||
}
|
||||
return hash;
|
||||
},
|
||||
|
||||
@computed('theme_fields', 'theme_fields.@each')
|
||||
uploads(fields) {
|
||||
if (!fields) {
|
||||
return [];
|
||||
}
|
||||
return fields.filter((f)=> f.target === 'common' && f.type_id === THEME_UPLOAD_VAR);
|
||||
},
|
||||
|
||||
getKey(field){
|
||||
return field.target + " " + field.name;
|
||||
},
|
||||
|
||||
hasEdited(target, name){
|
||||
if (name) {
|
||||
return !Em.isEmpty(this.getField(target, name));
|
||||
|
@ -31,30 +47,56 @@ const Theme = RestModel.extend({
|
|||
|
||||
getError(target, name) {
|
||||
let themeFields = this.get("themeFields");
|
||||
let key = target + " " + name;
|
||||
let key = this.getKey({target,name});
|
||||
let field = themeFields[key];
|
||||
return field ? field.error : "";
|
||||
},
|
||||
|
||||
getField(target, name) {
|
||||
let themeFields = this.get("themeFields");
|
||||
let key = target + " " + name;
|
||||
let key = this.getKey({target, name})
|
||||
let field = themeFields[key];
|
||||
return field ? field.value : "";
|
||||
},
|
||||
|
||||
setField(target, name, value) {
|
||||
removeField(field) {
|
||||
this.set("changed", true);
|
||||
|
||||
field.upload_id = null;
|
||||
field.value = null;
|
||||
|
||||
return this.saveChanges("theme_fields");
|
||||
},
|
||||
|
||||
setField(target, name, value, upload_id, type_id) {
|
||||
this.set("changed", true);
|
||||
let themeFields = this.get("themeFields");
|
||||
let key = target + " " + name;
|
||||
let field = themeFields[key];
|
||||
if (!field) {
|
||||
field = {name, target, value};
|
||||
let field = {name, target, value, upload_id, type_id};
|
||||
|
||||
// slow path for uploads and so on
|
||||
if (type_id && type_id > 1) {
|
||||
let fields = this.get("theme_fields");
|
||||
let existing = fields.find((f) =>
|
||||
f.target === target &&
|
||||
f.name === name &&
|
||||
f.type_id === type_id);
|
||||
if (existing) {
|
||||
existing.value = value;
|
||||
existing.upload_id = upload_id;
|
||||
} else {
|
||||
fields.push(field);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// fast path
|
||||
let key = this.getKey({target,name});
|
||||
let existingField = themeFields[key];
|
||||
if (!existingField) {
|
||||
this.theme_fields.push(field);
|
||||
themeFields[key] = field;
|
||||
} else {
|
||||
field.value = value;
|
||||
existingField.value = value;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -51,7 +51,6 @@
|
|||
|
||||
<h3>{{i18n "admin.customize.theme.css_html"}}</h3>
|
||||
{{#if hasEditedFields}}
|
||||
|
||||
<p>{{i18n "admin.customize.theme.custom_sections"}}</p>
|
||||
<ul>
|
||||
{{#each editedDescriptions as |desc|}}
|
||||
|
@ -87,6 +86,21 @@
|
|||
{{/if}}
|
||||
</p>
|
||||
|
||||
|
||||
<h3>{{i18n "admin.customize.theme.uploads"}}</h3>
|
||||
{{#if model.uploads}}
|
||||
<ul class='removable-list'>
|
||||
{{#each model.uploads as |upload|}}
|
||||
<li><span class='first'>${{upload.name}}: <a href={{upload.url}} target='_blank'>{{upload.filename}}</a></span>{{d-button action="removeUpload" actionParam=upload class="second btn-small cancel-edit" icon="times"}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<p>{{i18n "admin.customize.theme.no_uploads"}}</p>
|
||||
{{/if}}
|
||||
<p>
|
||||
{{#d-button action="addUploadModal" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
|
||||
</p>
|
||||
|
||||
{{#if availableChildThemes}}
|
||||
<h3>{{i18n "admin.customize.theme.theme_components"}}</h3>
|
||||
{{#unless model.childThemes.length}}
|
||||
|
@ -97,9 +111,9 @@
|
|||
</label>
|
||||
</p>
|
||||
{{else}}
|
||||
<ul>
|
||||
<ul class='removable-list'>
|
||||
{{#each model.childThemes as |child|}}
|
||||
<li>{{#link-to 'adminCustomizeThemes.show' child replace=true}}{{child.name}}{{/link-to}} {{d-button action="removeChildTheme" actionParam=child class="btn-small cancel-edit" icon="times"}}</li>
|
||||
<li>{{#link-to 'adminCustomizeThemes.show' child replace=true class='first'}}{{child.name}}{{/link-to}} {{d-button action="removeChildTheme" actionParam=child class="btn-small cancel-edit second" icon="times"}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/unless}}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{{#d-modal-body class='upload-selector' title="admin.customize.theme.add_upload"}}
|
||||
<div class="inputs">
|
||||
<input id="name" placeholder={{i18n 'admin.customize.theme.upload_name'}} value={{name}}><br>
|
||||
<input onchange={{action "updateName"}} type="file" id="file-input" accept='*'><br>
|
||||
<span class="description">{{i18n 'admin.customize.theme.upload_file_tip'}}</span>
|
||||
</div>
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
{{d-button action="upload" disabled=loading class='btn btn-primary' icon='upload' label='admin.customize.theme.upload'}}
|
||||
<a href {{action "closeModal"}}>{{i18n 'cancel'}}</a>
|
||||
</div>
|
|
@ -187,5 +187,21 @@
|
|||
font-size: 0.8em;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.removable-list {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
li {
|
||||
display: table-row;
|
||||
.first {
|
||||
padding-right: 8px;
|
||||
padding-bottom: 10px;
|
||||
min-width: 100px;
|
||||
}
|
||||
.first, .second {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,22 @@ class Admin::ThemesController < Admin::AdminController
|
|||
redirect_to path("/"), flash: {preview_theme_key: @theme.key}
|
||||
end
|
||||
|
||||
def upload_asset
|
||||
path = params[:file].path
|
||||
File.open(path) do |file|
|
||||
upload = Upload.create_for(current_user.id,
|
||||
file,
|
||||
params[:original_filename] || File.basename(path),
|
||||
File.size(path),
|
||||
for_theme: true)
|
||||
if upload.errors.count > 0
|
||||
render json: upload.errors, status: :unprocessable_entity
|
||||
else
|
||||
render json: {upload_id: upload.id}, status: :created
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def import
|
||||
|
||||
@theme = nil
|
||||
|
@ -184,7 +200,7 @@ class Admin::ThemesController < Admin::AdminController
|
|||
:color_scheme_id,
|
||||
:default,
|
||||
:user_selectable,
|
||||
theme_fields: [:name, :target, :value],
|
||||
theme_fields: [:name, :target, :value, :upload_id, :type_id],
|
||||
child_theme_ids: [])
|
||||
end
|
||||
end
|
||||
|
@ -194,7 +210,13 @@ class Admin::ThemesController < Admin::AdminController
|
|||
return unless fields = theme_params[:theme_fields]
|
||||
|
||||
fields.each do |field|
|
||||
@theme.set_field(target: field[:target], name: field[:name], value: field[:value], type_id: field[:type_id])
|
||||
@theme.set_field(
|
||||
target: field[:target],
|
||||
name: field[:name],
|
||||
value: field[:value],
|
||||
type_id: field[:type_id],
|
||||
upload_id: field[:upload_id]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -46,6 +46,34 @@ class RemoteTheme < ActiveRecord::Base
|
|||
importer.import!
|
||||
end
|
||||
|
||||
theme_info = JSON.parse(importer["about.json"])
|
||||
|
||||
theme_info["assets"]&.each do |name, relative_path|
|
||||
if path = importer.real_path(relative_path)
|
||||
upload = Upload.create_for(theme.user_id, File.open(path), File.basename(relative_path), File.size(path), for_theme: true)
|
||||
theme.set_field(target: :common, name: name, type: :theme_upload_var, upload_id: upload.id)
|
||||
end
|
||||
end
|
||||
|
||||
theme_info["fields"]&.each do |name, info|
|
||||
unless Hash === info
|
||||
info = {
|
||||
"target" => :common,
|
||||
"type" => :theme_var,
|
||||
"value" => info
|
||||
}
|
||||
end
|
||||
|
||||
if info["type"] == "color"
|
||||
info["type"] = :theme_color_var
|
||||
end
|
||||
|
||||
theme.set_field(target: info["target"] || :common,
|
||||
name: name,
|
||||
value: info["value"],
|
||||
type: info["type"] || :theme_var)
|
||||
end
|
||||
|
||||
Theme.targets.keys.each do |target|
|
||||
ALLOWED_FIELDS.each do |field|
|
||||
lookup =
|
||||
|
@ -62,7 +90,6 @@ class RemoteTheme < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
theme_info = JSON.parse(importer["about.json"])
|
||||
self.license_url ||= theme_info["license_url"]
|
||||
self.about_url ||= theme_info["about_url"]
|
||||
self.remote_updated_at = Time.zone.now
|
||||
|
|
|
@ -252,19 +252,16 @@ class Theme < ActiveRecord::Base
|
|||
type_id ||= type ? ThemeField.types[type.to_sym] : ThemeField.guess_type(name)
|
||||
raise "Unknown type #{type} passed to set field" unless type_id
|
||||
|
||||
if upload_id && !value
|
||||
value = ""
|
||||
end
|
||||
|
||||
raise "Missing value for theme field" unless value
|
||||
value ||= ""
|
||||
|
||||
field = theme_fields.find{|f| f.name==name && f.target_id == target_id && f.type_id == type_id}
|
||||
if field
|
||||
if value.blank?
|
||||
if value.blank? && !upload_id
|
||||
theme_fields.delete field.destroy
|
||||
else
|
||||
if field.value != value
|
||||
if field.value != value || field.upload_id != upload_id
|
||||
field.value = value
|
||||
field.upload_id = upload_id
|
||||
changed_fields << field
|
||||
end
|
||||
end
|
||||
|
|
|
@ -112,7 +112,9 @@ COMPILED
|
|||
begin
|
||||
Stylesheet::Compiler.compile("@import \"theme_variables\"; @import \"theme_field\";",
|
||||
"theme.scss",
|
||||
theme_field: self.value.dup)
|
||||
theme_field: self.value.dup,
|
||||
theme: self.theme
|
||||
)
|
||||
self.error = nil unless error.nil?
|
||||
rescue SassC::SyntaxError => e
|
||||
self.error = e.message
|
||||
|
|
|
@ -15,6 +15,7 @@ class Upload < ActiveRecord::Base
|
|||
has_many :optimized_images, dependent: :destroy
|
||||
|
||||
attr_accessor :is_attachment_for_group_message
|
||||
attr_accessor :for_theme
|
||||
|
||||
validates_presence_of :filesize
|
||||
validates_presence_of :original_filename
|
||||
|
@ -38,7 +39,7 @@ class Upload < ActiveRecord::Base
|
|||
crop: crop
|
||||
}
|
||||
|
||||
if thumbnail = OptimizedImage.create_for(self, width, height, opts)
|
||||
if _thumbnail = OptimizedImage.create_for(self, width, height, opts)
|
||||
self.width = width
|
||||
self.height = height
|
||||
save(validate: false)
|
||||
|
@ -99,6 +100,7 @@ class Upload < ActiveRecord::Base
|
|||
# - origin (url)
|
||||
# - image_type ("avatar", "profile_background", "card_background", "custom_emoji")
|
||||
# - is_attachment_for_group_message (boolean)
|
||||
# - for_theme (boolean)
|
||||
def self.create_for(user_id, file, filename, filesize, options = {})
|
||||
upload = Upload.new
|
||||
|
||||
|
@ -196,6 +198,10 @@ class Upload < ActiveRecord::Base
|
|||
upload.is_attachment_for_group_message = true
|
||||
end
|
||||
|
||||
if options[:for_theme]
|
||||
upload.for_theme = true
|
||||
end
|
||||
|
||||
if is_dimensionless_image?(filename, upload.width, upload.height)
|
||||
upload.errors.add(:base, I18n.t("upload.images.size_not_found"))
|
||||
return upload
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
class ThemeFieldSerializer < ApplicationSerializer
|
||||
attributes :name, :target, :value, :error, :type_id
|
||||
attributes :name, :target, :value, :error, :type_id, :upload_id, :url, :filename
|
||||
|
||||
def include_url?
|
||||
object.upload
|
||||
end
|
||||
|
||||
def include_upload_id?
|
||||
object.upload
|
||||
end
|
||||
|
||||
def include_filename?
|
||||
object.upload
|
||||
end
|
||||
|
||||
def url
|
||||
object.upload&.url
|
||||
end
|
||||
|
||||
def filename
|
||||
object.upload&.original_filename
|
||||
end
|
||||
|
||||
def target
|
||||
case object.target_id
|
||||
|
|
|
@ -2841,10 +2841,16 @@ en:
|
|||
color_scheme_select: "Select colors to be used by theme"
|
||||
custom_sections: "Custom sections:"
|
||||
theme_components: "Theme Components"
|
||||
uploads: "Uploads"
|
||||
no_uploads: "You can upload assets associated with your theme such as fonts and images"
|
||||
add_upload: "Add Upload"
|
||||
upload_file_tip: "Choose an asset to upload (png, woff2, etc...)"
|
||||
upload: "Upload"
|
||||
child_themes_check: "Theme includes other child themes"
|
||||
css_html: "Custom CSS/HTML"
|
||||
edit_css_html: "Edit CSS/HTML"
|
||||
edit_css_html_help: "You have not edited any CSS or HTML"
|
||||
upload_name: "Name"
|
||||
import_web_tip: "Repository containing theme"
|
||||
import_file_tip: ".dcstyle.json file containing theme"
|
||||
about_theme: "About Theme"
|
||||
|
|
|
@ -1255,6 +1255,7 @@ en:
|
|||
max_image_size_kb: "The maximum image upload size in kB. This must be configured in nginx (client_max_body_size) / apache or proxy as well."
|
||||
max_attachment_size_kb: "The maximum attachment files upload size in kB. This must be configured in nginx (client_max_body_size) / apache or proxy as well."
|
||||
authorized_extensions: "A list of file extensions allowed for upload (use '*' to enable all file types)"
|
||||
theme_authorized_extensions: "A list of file extensions allowed for theme uploads (use '*' to enable all file types)"
|
||||
max_similar_results: "How many similar topics to show above the editor when composing a new topic. Comparison is based on title and body."
|
||||
|
||||
max_image_megapixels: "Maximum number of megapixels allowed for an image."
|
||||
|
|
|
@ -189,6 +189,7 @@ Discourse::Application.routes.draw do
|
|||
|
||||
resources :themes, constraints: AdminConstraint.new
|
||||
post "themes/import" => "themes#import"
|
||||
post "themes/upload_asset" => "themes#upload_asset"
|
||||
get "themes/:id/preview" => "themes#preview"
|
||||
|
||||
scope "/customize", constraints: AdminConstraint.new do
|
||||
|
|
|
@ -713,6 +713,9 @@ files:
|
|||
default: 40
|
||||
min: 5
|
||||
max: 150
|
||||
theme_authorized_extensions:
|
||||
default: 'jpg|jpeg|png|woff|woff2|svg|eot|ttf|otf'
|
||||
type: list
|
||||
authorized_extensions:
|
||||
client: true
|
||||
default: 'jpg|jpeg|png|gif'
|
||||
|
|
|
@ -35,15 +35,24 @@ class GitImporter
|
|||
FileUtils.rm_rf(@temp_folder)
|
||||
end
|
||||
|
||||
def [](value)
|
||||
fullpath = "#{@temp_folder}/#{value}"
|
||||
def real_path(relative)
|
||||
fullpath = "#{@temp_folder}/#{relative}"
|
||||
return nil unless File.exist?(fullpath)
|
||||
|
||||
# careful to handle symlinks here, don't want to expose random data
|
||||
fullpath = Pathname.new(fullpath).realpath.to_s
|
||||
|
||||
if fullpath && fullpath.start_with?(@temp_folder)
|
||||
File.read(fullpath)
|
||||
fullpath
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def [](value)
|
||||
fullpath = real_path(value)
|
||||
return nil unless fullpath
|
||||
File.read(fullpath)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -40,6 +40,7 @@ module Stylesheet
|
|||
source_map_file: source_map_file,
|
||||
source_map_contents: true,
|
||||
theme_id: options[:theme_id],
|
||||
theme: options[:theme],
|
||||
theme_field: options[:theme_field],
|
||||
load_paths: [ASSET_ROOT])
|
||||
|
||||
|
|
|
@ -41,7 +41,9 @@ module Stylesheet
|
|||
colors.each do |n, hex|
|
||||
contents << "$#{n}: ##{hex} !default;\n"
|
||||
end
|
||||
theme&.theme_fields&.where(type_id: ThemeField.theme_var_type_ids)&.each do |field|
|
||||
theme&.theme_fields&.each do |field|
|
||||
next unless ThemeField.theme_var_type_ids.include?(field.type_id)
|
||||
|
||||
if field.type_id == ThemeField.types[:theme_upload_var]
|
||||
if upload = field.upload
|
||||
url = upload_cdn_path(upload.url)
|
||||
|
@ -84,8 +86,13 @@ module Stylesheet
|
|||
end
|
||||
|
||||
def initialize(options)
|
||||
@theme = options[:theme]
|
||||
@theme_id = options[:theme_id]
|
||||
@theme_field = options[:theme_field]
|
||||
if @theme && !@theme_id
|
||||
# make up an id so other stuff does not bail out
|
||||
@theme_id = @theme.id || -1
|
||||
end
|
||||
end
|
||||
|
||||
def import_files(files)
|
||||
|
|
|
@ -24,11 +24,11 @@ class Validators::UploadValidator < ActiveModel::Validator
|
|||
end
|
||||
|
||||
def is_authorized?(upload, extension)
|
||||
authorized_extensions(upload, extension, authorized_uploads)
|
||||
authorized_extensions(upload, extension, authorized_uploads(upload))
|
||||
end
|
||||
|
||||
def authorized_image_extension(upload, extension)
|
||||
authorized_extensions(upload, extension, authorized_images)
|
||||
authorized_extensions(upload, extension, authorized_images(upload))
|
||||
end
|
||||
|
||||
def maximum_image_file_size(upload)
|
||||
|
@ -36,7 +36,7 @@ class Validators::UploadValidator < ActiveModel::Validator
|
|||
end
|
||||
|
||||
def authorized_attachment_extension(upload, extension)
|
||||
authorized_extensions(upload, extension, authorized_attachments)
|
||||
authorized_extensions(upload, extension, authorized_attachments(upload))
|
||||
end
|
||||
|
||||
def maximum_attachment_file_size(upload)
|
||||
|
@ -45,10 +45,12 @@ class Validators::UploadValidator < ActiveModel::Validator
|
|||
|
||||
private
|
||||
|
||||
def authorized_uploads
|
||||
def authorized_uploads(upload)
|
||||
authorized_uploads = Set.new
|
||||
|
||||
SiteSetting.authorized_extensions
|
||||
extensions = upload.for_theme ? SiteSetting.theme_authorized_extensions : SiteSetting.authorized_extensions
|
||||
|
||||
extensions
|
||||
.gsub(/[\s\.]+/, "")
|
||||
.downcase
|
||||
.split("|")
|
||||
|
@ -57,20 +59,21 @@ class Validators::UploadValidator < ActiveModel::Validator
|
|||
authorized_uploads
|
||||
end
|
||||
|
||||
def authorized_images
|
||||
authorized_uploads & FileHelper.images
|
||||
def authorized_images(upload)
|
||||
authorized_uploads(upload) & FileHelper.images
|
||||
end
|
||||
|
||||
def authorized_attachments
|
||||
authorized_uploads - FileHelper.images
|
||||
def authorized_attachments(upload)
|
||||
authorized_uploads(upload) - FileHelper.images
|
||||
end
|
||||
|
||||
def authorizes_all_extensions?
|
||||
SiteSetting.authorized_extensions.include?("*")
|
||||
def authorizes_all_extensions?(upload)
|
||||
extensions = upload.for_theme ? SiteSetting.theme_authorized_extensions : SiteSetting.authorized_extensions
|
||||
extensions.include?("*")
|
||||
end
|
||||
|
||||
def authorized_extensions(upload, extension, extensions)
|
||||
return true if authorizes_all_extensions?
|
||||
return true if authorizes_all_extensions?(upload)
|
||||
|
||||
unless authorized = extensions.include?(extension.downcase)
|
||||
message = I18n.t("upload.unauthorized", authorized_extensions: extensions.to_a.join(", "))
|
||||
|
|
|
@ -11,6 +11,26 @@ describe Admin::ThemesController do
|
|||
@user = log_in(:admin)
|
||||
end
|
||||
|
||||
context '.upload_asset' do
|
||||
render_views
|
||||
|
||||
let(:upload) do
|
||||
ActionDispatch::Http::UploadedFile.new({
|
||||
filename: 'test.woff2',
|
||||
tempfile: file_from_fixtures("fake.woff2", "woff2")
|
||||
})
|
||||
end
|
||||
|
||||
it 'can create a theme upload' do
|
||||
xhr :post, :upload_asset, file: upload, original_filename: 'wooof.woff2'
|
||||
expect(response.status).to eq(201)
|
||||
upload = Upload.find_by(original_filename: "wooof.woff2")
|
||||
expect(upload.id).not_to be_nil
|
||||
expect(JSON.parse(response.body)["upload_id"]).to eq(upload.id)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context '.import' do
|
||||
let(:theme_file) do
|
||||
ActionDispatch::Http::UploadedFile.new({
|
||||
|
@ -93,30 +113,35 @@ describe Admin::ThemesController do
|
|||
end
|
||||
|
||||
it 'updates a theme' do
|
||||
#focus
|
||||
theme = Theme.new(name: 'my name', user_id: -1)
|
||||
theme.set_field(target: :common, name: :scss, value: '.body{color: black;}')
|
||||
theme.save
|
||||
|
||||
child_theme = Theme.create(name: 'my name', user_id: -1)
|
||||
|
||||
upload = Fabricate(:upload)
|
||||
|
||||
xhr :put, :update, id: theme.id,
|
||||
theme: {
|
||||
child_theme_ids: [child_theme.id],
|
||||
name: 'my test name',
|
||||
theme_fields: [
|
||||
{ name: 'scss', target: 'common', value: '' },
|
||||
{ name: 'scss', target: 'desktop', value: 'body{color: blue;}' }
|
||||
{ name: 'scss', target: 'desktop', value: 'body{color: blue;}' },
|
||||
{ name: 'bob', target: 'common', value: '', type_id: 2, upload_id: upload.id },
|
||||
]
|
||||
}
|
||||
expect(response).to be_success
|
||||
|
||||
json = ::JSON.parse(response.body)
|
||||
|
||||
fields = json["theme"]["theme_fields"]
|
||||
fields = json["theme"]["theme_fields"].sort{|a,b| a["value"] <=> b["value"]}
|
||||
|
||||
expect(fields.first["value"]).to eq('body{color: blue;}')
|
||||
expect(fields.length).to eq(1)
|
||||
expect(fields[0]["value"]).to eq('')
|
||||
expect(fields[0]["upload_id"]).to eq(upload.id)
|
||||
expect(fields[1]["value"]).to eq('body{color: blue;}')
|
||||
|
||||
expect(fields.length).to eq(2)
|
||||
|
||||
expect(json["theme"]["child_themes"].length).to eq(1)
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
not a woff
|
|
@ -9,7 +9,7 @@ describe RemoteTheme do
|
|||
`cd #{repo_dir} && git init . `
|
||||
`cd #{repo_dir} && git config user.email 'someone@cool.com'`
|
||||
`cd #{repo_dir} && git config user.name 'The Cool One'`
|
||||
`cd #{repo_dir} && mkdir desktop mobile common`
|
||||
`cd #{repo_dir} && mkdir desktop mobile common assets`
|
||||
files.each do |name, data|
|
||||
File.write("#{repo_dir}/#{name}", data)
|
||||
`cd #{repo_dir} && git add #{name}`
|
||||
|
@ -26,6 +26,17 @@ describe RemoteTheme do
|
|||
"name": "awesome theme",
|
||||
"about_url": "https://www.site.com/about",
|
||||
"license_url": "https://www.site.com/license",
|
||||
"assets": {
|
||||
"font": "assets/awesome.woff2"
|
||||
},
|
||||
"fields": {
|
||||
"color": {
|
||||
"target": "desktop",
|
||||
"value": "#FEF",
|
||||
"type": "color"
|
||||
},
|
||||
"name": "sam"
|
||||
},
|
||||
"color_schemes": {
|
||||
"Amazing": {
|
||||
"love": "#{options[:love]}"
|
||||
|
@ -35,13 +46,18 @@ describe RemoteTheme do
|
|||
JSON
|
||||
end
|
||||
|
||||
let :scss_data do
|
||||
"@font-face { font-family: magic; src: url($font)}; body {color: $color; content: $name;}"
|
||||
end
|
||||
|
||||
let :initial_repo do
|
||||
setup_git_repo(
|
||||
"about.json" => about_json,
|
||||
"desktop/desktop.scss" => "body {color: red;}",
|
||||
"desktop/desktop.scss" => scss_data,
|
||||
"common/header.html" => "I AM HEADER",
|
||||
"common/random.html" => "I AM SILLY",
|
||||
"common/embedded.scss" => "EMBED",
|
||||
"assets/awesome.woff2" => "FAKE FONT",
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -65,14 +81,20 @@ JSON
|
|||
expect(remote.about_url).to eq("https://www.site.com/about")
|
||||
expect(remote.license_url).to eq("https://www.site.com/license")
|
||||
|
||||
expect(@theme.theme_fields.length).to eq(3)
|
||||
expect(@theme.theme_fields.length).to eq(6)
|
||||
|
||||
mapped = Hash[*@theme.theme_fields.map{|f| ["#{f.target_id}-#{f.name}", f.value]}.flatten]
|
||||
|
||||
expect(mapped["0-header"]).to eq("I AM HEADER")
|
||||
expect(mapped["1-scss"]).to eq("body {color: red;}")
|
||||
expect(mapped["1-scss"]).to eq(scss_data)
|
||||
expect(mapped["0-embedded_scss"]).to eq("EMBED")
|
||||
|
||||
expect(mapped["1-color"]).to eq("#FEF")
|
||||
expect(mapped["0-font"]).to eq("")
|
||||
expect(mapped["0-name"]).to eq("sam")
|
||||
|
||||
expect(mapped.length).to eq(6)
|
||||
|
||||
expect(remote.remote_updated_at).to eq(time)
|
||||
|
||||
scheme = ColorScheme.find_by(theme_id: @theme.id)
|
||||
|
@ -104,7 +126,7 @@ JSON
|
|||
mapped = Hash[*@theme.theme_fields.map{|f| ["#{f.target_id}-#{f.name}", f.value]}.flatten]
|
||||
|
||||
expect(mapped["0-header"]).to eq("I AM UPDATED")
|
||||
expect(mapped["1-scss"]).to eq("body {color: red;}")
|
||||
expect(mapped["1-scss"]).to eq(scss_data)
|
||||
expect(remote.remote_updated_at).to eq(time)
|
||||
|
||||
end
|
||||
|
|
|
@ -182,6 +182,9 @@ HTML
|
|||
|
||||
expect(Upload.where(id: upload.id)).to be_exist
|
||||
|
||||
# no error for theme field
|
||||
theme.reload
|
||||
expect(theme.theme_fields.find_by(name: :scss).error).to eq(nil)
|
||||
|
||||
scss,_map = Stylesheet::Compiler.compile('@import "theme_variables"; @import "desktop_theme"; ', "theme.scss", theme_id: theme.id)
|
||||
expect(scss).to include(upload.url)
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import Theme from 'admin/models/theme';
|
||||
|
||||
module("model:theme");
|
||||
|
||||
test('can add an upload correctly', function(assert) {
|
||||
let theme = Theme.create();
|
||||
|
||||
assert.equal(theme.get("uploads.length"), 0, "uploads should be an empty array");
|
||||
|
||||
theme.setField('common', 'bob', '', 999, 2);
|
||||
let fields = theme.get("theme_fields");
|
||||
assert.equal(fields.length, 1, 'expecting 1 theme field');
|
||||
assert.equal(fields[0].upload_id, 999, 'expecting upload id to be set');
|
||||
assert.equal(fields[0].type_id, 2, 'expecting type id to be set');
|
||||
|
||||
assert.equal(theme.get("uploads.length"), 1, "expecting an upload");
|
||||
});
|
Loading…
Reference in New Issue