From b3761653b3a936ab9d511945928aa379578bfbb1 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 11 Apr 2017 00:25:22 +0530 Subject: [PATCH 001/221] FIX: render emoji in title tag on topic page --- app/views/topics/show.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/topics/show.html.erb b/app/views/topics/show.html.erb index e24626f203..606485c7ca 100644 --- a/app/views/topics/show.html.erb +++ b/app/views/topics/show.html.erb @@ -100,7 +100,7 @@ <% end %> <% end %> -<% content_for(:title) { "#{@topic_view.page_title}" } %> +<% content_for(:title) { "#{gsub_emoji_to_unicode(@topic_view.page_title)}" } %> <% if @topic_view.print %> <% content_for :after_body do %> From f0778c50bf3ebe15b37b9fca7c1fdd697f0bbdbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 10 Apr 2017 21:39:19 +0200 Subject: [PATCH 002/221] UX: add href to badge title --- .../discourse/components/badge-card.js.es6 | 29 ++----------------- .../discourse/templates/badges/index.hbs | 2 +- .../templates/components/badge-card.hbs | 2 +- .../discourse/templates/user/badges.hbs | 2 +- .../discourse/templates/user/summary.hbs | 2 +- 5 files changed, 7 insertions(+), 30 deletions(-) diff --git a/app/assets/javascripts/discourse/components/badge-card.js.es6 b/app/assets/javascripts/discourse/components/badge-card.js.es6 index b162e982ce..a5eb0561aa 100644 --- a/app/assets/javascripts/discourse/components/badge-card.js.es6 +++ b/app/assets/javascripts/discourse/components/badge-card.js.es6 @@ -1,37 +1,14 @@ import computed from 'ember-addons/ember-computed-decorators'; -import DiscourseURL from 'discourse/lib/url'; import { sanitize, emojiUnescape } from 'discourse/lib/text'; export default Ember.Component.extend({ size: 'medium', - classNameBindings: [':badge-card', 'size', 'badge.slug', 'navigateOnClick:hyperlink'], - - click(e){ - if (e.target && e.target.nodeName === "A") { - return true; - } - - if (!this.get('navigateOnClick')) { - return false; - } - - var url = this.get('badge.url'); - const username = this.get('username'); - if (username) { - url = url + "?username=" + encodeURIComponent(username); - } - DiscourseURL.routeTo(url); - return true; - }, + classNameBindings: [':badge-card', 'size', 'badge.slug'], @computed('count', 'badge.grant_count') displayCount(count, grantCount) { - if (count == null) { - return grantCount; - } - if (count > 1) { - return count; - } + if (count == null) { return grantCount; } + if (count > 1) { return count; } }, @computed('size') diff --git a/app/assets/javascripts/discourse/templates/badges/index.hbs b/app/assets/javascripts/discourse/templates/badges/index.hbs index b30725693b..08b7c79b5e 100644 --- a/app/assets/javascripts/discourse/templates/badges/index.hbs +++ b/app/assets/javascripts/discourse/templates/badges/index.hbs @@ -10,7 +10,7 @@ {{#each bg.badges as |b|}} - {{badge-card badge=b navigateOnClick="true"}} + {{badge-card badge=b}} {{/each}} {{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/badge-card.hbs b/app/assets/javascripts/discourse/templates/components/badge-card.hbs index 765322dec8..2a41b36605 100644 --- a/app/assets/javascripts/discourse/templates/components/badge-card.hbs +++ b/app/assets/javascripts/discourse/templates/components/badge-card.hbs @@ -10,7 +10,7 @@
-

{{badge.name}}

+

{{badge.name}}

{{{summary}}}
diff --git a/app/assets/javascripts/discourse/templates/user/badges.hbs b/app/assets/javascripts/discourse/templates/user/badges.hbs index 860af54a92..03210567ce 100644 --- a/app/assets/javascripts/discourse/templates/user/badges.hbs +++ b/app/assets/javascripts/discourse/templates/user/badges.hbs @@ -1,5 +1,5 @@ {{#d-section pageClass="user-badges" class="user-content user-badges-list"}} {{#each sortedBadges as |ub|}} - {{badge-card badge=ub.badge count=ub.count navigateOnClick="true" username=username}} + {{badge-card badge=ub.badge count=ub.count username=username}} {{/each}} {{/d-section}} diff --git a/app/assets/javascripts/discourse/templates/user/summary.hbs b/app/assets/javascripts/discourse/templates/user/summary.hbs index 5dfd55a16a..d3444c62b1 100644 --- a/app/assets/javascripts/discourse/templates/user/summary.hbs +++ b/app/assets/javascripts/discourse/templates/user/summary.hbs @@ -179,7 +179,7 @@

{{i18n "user.summary.top_badges"}}

{{#each model.badges as |badge|}} - {{badge-card badge=badge count=badge.count navigateOnClick="true" username=user.username_lower}} + {{badge-card badge=badge count=badge.count username=user.username_lower}} {{else}}

{{i18n "user.summary.no_badges"}}

{{/each}} From db7764063440adae515f8071c6582253a5e74885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 10 Apr 2017 22:11:29 +0200 Subject: [PATCH 003/221] FIX: grant first reply by email job was *brokenated* --- ...rand_first_reply_by_email.rb => grant_first_reply_by_email.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/jobs/onceoff/{grand_first_reply_by_email.rb => grant_first_reply_by_email.rb} (100%) diff --git a/app/jobs/onceoff/grand_first_reply_by_email.rb b/app/jobs/onceoff/grant_first_reply_by_email.rb similarity index 100% rename from app/jobs/onceoff/grand_first_reply_by_email.rb rename to app/jobs/onceoff/grant_first_reply_by_email.rb From 2be14a604c72625ae3b03b5533f7c8bf9bc739b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 10 Apr 2017 23:38:48 +0200 Subject: [PATCH 004/221] FIX: censored_pattern with group capturing wasn't working --- lib/validators/censored_words_validator.rb | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/validators/censored_words_validator.rb b/lib/validators/censored_words_validator.rb index ed97a13d2a..3b566a27ea 100644 --- a/lib/validators/censored_words_validator.rb +++ b/lib/validators/censored_words_validator.rb @@ -1,15 +1,11 @@ class CensoredWordsValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) - if !SiteSetting.censored_words.blank? && - !(censored_words = value.scan(censored_words_regexp)).empty? - + if SiteSetting.censored_words.present? && (censored_words = censor_words(value, censored_words_regexp)).present? record.errors.add( attribute, :contains_censored_words, censored_words: join_censored_words(censored_words) ) - elsif !SiteSetting.censored_pattern.blank? && - !(censored_words = value.scan(/#{SiteSetting.censored_pattern}/i)).empty? - + elsif SiteSetting.censored_pattern.present? && (censored_words = censor_words(value, /#{SiteSetting.censored_pattern}/i)).present? record.errors.add( attribute, :matches_censored_pattern, censored_words: join_censored_words(censored_words) @@ -19,6 +15,16 @@ class CensoredWordsValidator < ActiveModel::EachValidator private + def censor_words(value, regexp) + censored_words = value.scan(regexp) + censored_words.flatten! + censored_words.compact! + censored_words.map!(&:strip) + censored_words.select!(&:present?) + censored_words.uniq! + censored_words + end + def join_censored_words(censored_words) censored_words.map!(&:downcase) censored_words.uniq! From aadf4805a539425021dff8352cc4aa404c4159f0 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 11 Apr 2017 10:31:11 +0800 Subject: [PATCH 005/221] FIX: Topic status update not being deleted once it has been executed. --- app/jobs/regular/publish_topic_to_category.rb | 1 + spec/jobs/publish_topic_to_category_spec.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/app/jobs/regular/publish_topic_to_category.rb b/app/jobs/regular/publish_topic_to_category.rb index 11424dd32f..31a838a2c8 100644 --- a/app/jobs/regular/publish_topic_to_category.rb +++ b/app/jobs/regular/publish_topic_to_category.rb @@ -10,6 +10,7 @@ module Jobs PostTimestampChanger.new(timestamp: Time.zone.now, topic: topic).change! do topic.change_category_to_id(topic_status_update.category_id) topic.update_columns(visible: true) + topic_status_update.trash!(Discourse.system_user) end MessageBus.publish("/topic/#{topic.id}", reload_topic: true, refresh_stream: true) diff --git a/spec/jobs/publish_topic_to_category_spec.rb b/spec/jobs/publish_topic_to_category_spec.rb index e628d71998..e49a319097 100644 --- a/spec/jobs/publish_topic_to_category_spec.rb +++ b/spec/jobs/publish_topic_to_category_spec.rb @@ -47,6 +47,7 @@ RSpec.describe Jobs::PublishTopicToCategory do topic.reload expect(topic.category).to eq(another_category) expect(topic.visible).to eq(true) + expect(TopicStatusUpdate.find_by(id: topic.topic_status_update.id)).to eq(nil) %w{created_at bumped_at updated_at last_posted_at}.each do |attribute| expect(topic.public_send(attribute)).to be_within(1.second).of(Time.zone.now) From c076f7b1aaeca99e0fa17fa1ed532808f622bd23 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 11 Apr 2017 10:43:34 +0800 Subject: [PATCH 006/221] Fix broken specs. --- spec/integration/managing_topic_status_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/integration/managing_topic_status_spec.rb b/spec/integration/managing_topic_status_spec.rb index c283ed60c1..69f55d7b79 100644 --- a/spec/integration/managing_topic_status_spec.rb +++ b/spec/integration/managing_topic_status_spec.rb @@ -78,6 +78,8 @@ RSpec.describe "Managing a topic's status update", type: :request do describe 'publishing topic to category in the future' do it 'should be able to create the topic status update' do + SiteSetting.queue_jobs = true + post "/t/#{topic.id}/status_update.json", time: 24, status_type: TopicStatusUpdate.types[3], From 2d9b31b147498de38be10dc0b65678d28ddad95b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 11 Apr 2017 07:44:10 +0200 Subject: [PATCH 007/221] allow 'max_image_megapixels' up to 150MB --- config/site_settings.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/site_settings.yml b/config/site_settings.yml index 09bead2a13..146b0ebed2 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -717,7 +717,7 @@ files: max_image_megapixels: default: 40 min: 5 - max: 100 + max: 150 authorized_extensions: client: true default: 'jpg|jpeg|png|gif' From 0a4c30bce33b26599bba3b38b6e52f8fc4cc238e Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 11 Apr 2017 12:12:51 +0800 Subject: [PATCH 008/221] FIX: Handle cases where `alt` and `title` tag is blank when parsing excerpt. --- lib/excerpt_parser.rb | 4 ++-- spec/components/pretty_text_spec.rb | 24 ++++++++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/excerpt_parser.rb b/lib/excerpt_parser.rb index c162bd89be..8c1938b06f 100644 --- a/lib/excerpt_parser.rb +++ b/lib/excerpt_parser.rb @@ -68,9 +68,9 @@ class ExcerptParser < Nokogiri::XML::SAX::Document # If include_images is set, include the image in markdown characters("!") if @markdown_images - if attributes["alt"] + if !attributes["alt"].blank? characters("[#{attributes["alt"]}]") - elsif attributes["title"] + elsif !attributes["title"].blank? characters("[#{attributes["title"]}]") else characters("[#{I18n.t 'excerpt_image'}]") diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index 8554b82c33..d8535dcf19 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -124,12 +124,28 @@ HTML expect(PrettyText.excerpt("",100)).to eq("[image]") end - it "should keep alt tags" do - expect(PrettyText.excerpt("car",100)).to eq("[car]") + context 'alt tags' do + it "should keep alt tags" do + expect(PrettyText.excerpt("car", 100)).to eq("[car]") + end + + describe 'when alt tag is empty' do + it "should not keep alt tags" do + expect(PrettyText.excerpt("", 100)).to eq("[#{I18n.t('excerpt_image')}]") + end + end end - it "should keep title tags" do - expect(PrettyText.excerpt("",100)).to eq("[car]") + context 'title tags' do + it "should keep title tags" do + expect(PrettyText.excerpt("", 100)).to eq("[car]") + end + + describe 'when title tag is empty' do + it "should not keep title tags" do + expect(PrettyText.excerpt("", 100)).to eq("[#{I18n.t('excerpt_image')}]") + end + end end it "should convert images to markdown if the option is set" do From 3861bd2793a49d10e8584022f0dc9d7aed364f76 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 11 Apr 2017 15:13:21 +0800 Subject: [PATCH 009/221] FIX: Quotes should be ignored when parsing for onebox source. --- lib/excerpt_parser.rb | 6 +++++- spec/components/pretty_text_spec.rb | 22 +++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/excerpt_parser.rb b/lib/excerpt_parser.rb index 8c1938b06f..b6547b59bb 100644 --- a/lib/excerpt_parser.rb +++ b/lib/excerpt_parser.rb @@ -85,7 +85,11 @@ class ExcerptParser < Nokogiri::XML::SAX::Document end when "aside" - @in_quote = true unless @keep_onebox_source + attributes = Hash[*attributes.flatten] + + unless @keep_onebox_source && attributes['class'].include?('onebox') + @in_quote = true + end when 'article' if @keep_onebox_source && attributes.include?(['class', 'onebox-body']) @in_quote = true diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index d8535dcf19..8b0dfae4e9 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -289,15 +289,23 @@ HTML expect(PrettyText.excerpt(emoji_code, 100)).to eq(":heart:") end - it "should have an option to preserver onebox source" do - onebox = "\n\n\n" - expected = "meta.discourse.org" + context 'option ot preserve onebox source' do + it "should return the right excerpt" do + onebox = "\n\n\n" + expected = "meta.discourse.org" - expect(PrettyText.excerpt(onebox, 100, keep_onebox_source: true)) - .to eq(expected) + expect(PrettyText.excerpt(onebox, 100, keep_onebox_source: true)) + .to eq(expected) - expect(PrettyText.excerpt("#{onebox}\n \n \n \n\n\n #{onebox}", 100, keep_onebox_source: true)) - .to eq("#{expected}\n\n#{expected}") + expect(PrettyText.excerpt("#{onebox}\n \n \n \n\n\n #{onebox}", 100, keep_onebox_source: true)) + .to eq("#{expected}\n\n#{expected}") + end + + it 'should continue to strip quotes' do + expect(PrettyText.excerpt( + "boom", 100, keep_onebox_source: true + )).to eq("boom") + end end end From 73180c8a19d466ce2c898efb8589fdad05a564bb Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 11 Apr 2017 20:43:33 +0800 Subject: [PATCH 010/221] FIX: Private message can be set to publish in the future. --- app/jobs/regular/publish_topic_to_category.rb | 8 ++++- app/models/topic_converter.rb | 14 +++++++-- spec/jobs/publish_topic_to_category_spec.rb | 30 +++++++++++++++++- spec/models/topic_converter_spec.rb | 31 ++++++++++++++++++- 4 files changed, 78 insertions(+), 5 deletions(-) diff --git a/app/jobs/regular/publish_topic_to_category.rb b/app/jobs/regular/publish_topic_to_category.rb index 31a838a2c8..c67adde491 100644 --- a/app/jobs/regular/publish_topic_to_category.rb +++ b/app/jobs/regular/publish_topic_to_category.rb @@ -8,7 +8,13 @@ module Jobs return if topic.blank? PostTimestampChanger.new(timestamp: Time.zone.now, topic: topic).change! do - topic.change_category_to_id(topic_status_update.category_id) + if topic.private_message? + topic = TopicConverter.new(topic, Discourse.system_user) + .convert_to_public_topic(topic_status_update.category_id) + else + topic.change_category_to_id(topic_status_update.category_id) + end + topic.update_columns(visible: true) topic_status_update.trash!(Discourse.system_user) end diff --git a/app/models/topic_converter.rb b/app/models/topic_converter.rb index ddbda3a690..dd5bc30559 100644 --- a/app/models/topic_converter.rb +++ b/app/models/topic_converter.rb @@ -7,9 +7,19 @@ class TopicConverter @user = user end - def convert_to_public_topic + def convert_to_public_topic(category_id = nil) Topic.transaction do - @topic.category_id = SiteSetting.allow_uncategorized_topics ? SiteSetting.uncategorized_category_id : Category.where(read_restricted: false).first.id + @topic.category_id = + if category_id + category_id + elsif SiteSetting.allow_uncategorized_topics + SiteSetting.uncategorized_category_id + else + Category.where(read_restricted: false) + .where.not(id: SiteSetting.uncategorized_category_id) + .first.id + end + @topic.archetype = Archetype.default @topic.save update_user_stats diff --git a/spec/jobs/publish_topic_to_category_spec.rb b/spec/jobs/publish_topic_to_category_spec.rb index e49a319097..976735cc2d 100644 --- a/spec/jobs/publish_topic_to_category_spec.rb +++ b/spec/jobs/publish_topic_to_category_spec.rb @@ -37,7 +37,7 @@ RSpec.describe Jobs::PublishTopicToCategory do end end - it 'should publish the topic to the new category correctly' do + it 'should publish the topic to the new category' do Timecop.travel(1.hour.ago) { topic.update!(visible: false) } message = MessageBus.track_publish do @@ -56,4 +56,32 @@ RSpec.describe Jobs::PublishTopicToCategory do expect(message.data[:reload_topic]).to be_present expect(message.data[:refresh_stream]).to be_present end + + describe 'when topic is a private message' do + before do + expect { topic.convert_to_private_message(Discourse.system_user) } + .to change { topic.private_message? }.to(true) + end + + + it 'should publish the topic to the new category' do + described_class.new.execute(topic_status_update_id: topic.topic_status_update.id) + + message = MessageBus.track_publish do + described_class.new.execute(topic_status_update_id: topic.topic_status_update.id) + end.first + + topic.reload + expect(topic.category).to eq(another_category) + expect(topic.visible).to eq(true) + expect(topic.private_message?).to eq(false) + + %w{created_at bumped_at updated_at last_posted_at}.each do |attribute| + expect(topic.public_send(attribute)).to be_within(1.second).of(Time.zone.now) + end + + expect(message.data[:reload_topic]).to be_present + expect(message.data[:refresh_stream]).to be_present + end + end end diff --git a/spec/models/topic_converter_spec.rb b/spec/models/topic_converter_spec.rb index 96bd61e915..cee779633b 100644 --- a/spec/models/topic_converter_spec.rb +++ b/spec/models/topic_converter_spec.rb @@ -5,13 +5,42 @@ describe TopicConverter do context 'convert_to_public_topic' do let(:admin) { Fabricate(:admin) } let(:author) { Fabricate(:user) } + let(:category) { Fabricate(:category) } let(:private_message) { Fabricate(:private_message_topic, user: author) } context 'success' do it "converts private message to regular topic" do - topic = private_message.convert_to_public_topic(admin) + SiteSetting.allow_uncategorized_topics = true + topic = described_class.new(private_message, admin).convert_to_public_topic + topic.reload + expect(topic).to be_valid expect(topic.archetype).to eq("regular") + expect(topic.category_id).to eq(SiteSetting.uncategorized_category_id) + end + + describe 'when uncategorized category is not allowed' do + before do + SiteSetting.allow_uncategorized_topics = false + category.update!(read_restricted: false) + end + + it 'should convert private message into the right category' do + topic = described_class.new(private_message, admin).convert_to_public_topic + topic.reload + + expect(topic).to be_valid + expect(topic.archetype).to eq("regular") + expect(topic.category_id).to eq(category.id) + end + end + + describe 'when a custom category_id is given' do + it 'should convert private message into the right category' do + topic = described_class.new(private_message, admin).convert_to_public_topic(category.id) + + expect(topic.reload.category).to eq(category) + end end it "updates user stats" do From 5a57278a8e3fc209fadbb0edc10c2ecd39f52edd Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 11 Apr 2017 21:37:57 +0800 Subject: [PATCH 011/221] Fix build. --- spec/jobs/publish_topic_to_category_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/jobs/publish_topic_to_category_spec.rb b/spec/jobs/publish_topic_to_category_spec.rb index 976735cc2d..ab4470c560 100644 --- a/spec/jobs/publish_topic_to_category_spec.rb +++ b/spec/jobs/publish_topic_to_category_spec.rb @@ -59,17 +59,17 @@ RSpec.describe Jobs::PublishTopicToCategory do describe 'when topic is a private message' do before do - expect { topic.convert_to_private_message(Discourse.system_user) } - .to change { topic.private_message? }.to(true) + Timecop.travel(1.hour.ago) do + expect { topic.convert_to_private_message(Discourse.system_user) } + .to change { topic.private_message? }.to(true) + end end it 'should publish the topic to the new category' do - described_class.new.execute(topic_status_update_id: topic.topic_status_update.id) - message = MessageBus.track_publish do described_class.new.execute(topic_status_update_id: topic.topic_status_update.id) - end.first + end.last topic.reload expect(topic.category).to eq(another_category) From 8fb41bf5fbe337f432cae77d04edec9def41f402 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 11 Apr 2017 19:35:35 +0530 Subject: [PATCH 012/221] FIX: update timestamp when resending invite --- app/models/invite.rb | 2 ++ spec/models/invite_spec.rb | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/models/invite.rb b/app/models/invite.rb index 15e5fcccc3..5fd7471f00 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -116,6 +116,8 @@ class Invite < ActiveRecord::Base invite = nil end + invite.update_columns(created_at: Time.zone.now, updated_at: Time.zone.now) if invite + if !invite create_args = { invited_by: invited_by, email: lower_email } create_args[:moderator] = true if opts[:moderator] diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb index b3784aa97b..0080d9267e 100644 --- a/spec/models/invite_spec.rb +++ b/spec/models/invite_spec.rb @@ -102,8 +102,15 @@ describe Invite do expect(topic.invite_by_email(inviter, 'ICEKING@adventuretime.ooo')).to eq(@invite) end + it 'updates timestamp of existing invite' do + @invite.created_at = 10.days.ago + @invite.save + resend_invite = topic.invite_by_email(inviter, 'iceking@adventuretime.ooo') + expect(resend_invite.created_at).to be_within(1.minute).of(Time.zone.now) + end + it 'returns a new invite if the other has expired' do - SiteSetting.stubs(:invite_expiry_days).returns(1) + SiteSetting.invite_expiry_days = 1 @invite.created_at = 2.days.ago @invite.save new_invite = topic.invite_by_email(inviter, 'iceking@adventuretime.ooo') From 82dfe4c5d5867a091f1aa9fef0d04982152d21af Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 11 Apr 2017 20:42:54 +0530 Subject: [PATCH 013/221] FIX: disable browser autocomplete for change-owner modal --- .../javascripts/discourse/templates/modal/change-owner.hbs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/modal/change-owner.hbs b/app/assets/javascripts/discourse/templates/modal/change-owner.hbs index c23cefd469..33cbe1dd24 100644 --- a/app/assets/javascripts/discourse/templates/modal/change-owner.hbs +++ b/app/assets/javascripts/discourse/templates/modal/change-owner.hbs @@ -5,8 +5,11 @@

- - {{user-selector single="true" usernames=new_user placeholderKey="topic.change_owner.placeholder"}} + + {{user-selector single="true" + usernames=new_user + placeholderKey="topic.change_owner.placeholder" + autocomplete="off"}}
{{/d-modal-body}} From e5a50a3c9893686850415fad167b2bd3c8fb4ea4 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 11 Apr 2017 15:04:27 -0400 Subject: [PATCH 014/221] FIX: rtl locale support for category box style --- app/assets/stylesheets/common/base/category-list.scss | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/base/category-list.scss b/app/assets/stylesheets/common/base/category-list.scss index 5d88a6155c..477448dcda 100644 --- a/app/assets/stylesheets/common/base/category-list.scss +++ b/app/assets/stylesheets/common/base/category-list.scss @@ -12,7 +12,10 @@ align-content: flex-start; box-sizing: border-box; - border-width: 0 0 0 6px; + + border-width: 0; + border-left-width: 6px; + border-style: solid; border-color: blend-primary-secondary(20%); @@ -34,7 +37,10 @@ .category-box-inner { width: 100%; padding: 0; - border-width: 2px 2px 2px 0; + + border-width: 2px; + border-left-width: 0; + border-style: solid; border-color: blend-primary-secondary(20%); } From c6cd25b0ff23fb309dc482d1d4e430a375967ca4 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Tue, 11 Apr 2017 15:11:50 -0700 Subject: [PATCH 015/221] Update README.md --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 359c8189f1..5e02bb2b1e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ ![Logo](images/discourse.png) -Discourse is the 100% open source discussion platform built for the next decade of the Internet. It works as: +Discourse is the 100% open source discussion platform built for the next decade of the Internet. Use it as a: -- a mailing list -- a discussion forum -- a long-form chat room +- mailing list +- discussion forum +- long-form chat room To learn more about the philosophy and goals of the project, [visit **discourse.org**](http://www.discourse.org). @@ -38,12 +38,12 @@ If you're looking for business class hosting, see [discourse.org/buy](https://ww Discourse is built for the *next* 10 years of the Internet, so our requirements are high: -| Browsers | Tablets | Smartphones | +| Browsers | Tablets | Phones | | -------- | ------- | ----------- | -| Safari 6.1+| iPad 2+ | iOS 7+ | -| Google Chrome 23+ | Android 4.3+ | Android 4.3+ | -| Internet Explorer 11+ | Windows 8 | Windows Phone 8 | -| Firefox 16+ | | +| Safari 6.1+ | iPad 3+ | iOS 8+ | +| Google Chrome 32+ | Android 4.3+ | Android 4.3+ | +| Internet Explorer 11+ | | | +| Firefox 27+ | | | ## Built With From 3a235d1241b09e1ec1e7988de3ec06fd3f6347b3 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Tue, 11 Apr 2017 16:36:47 -0700 Subject: [PATCH 016/221] clarify on disk space requirements for install --- docs/INSTALL.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/INSTALL.md b/docs/INSTALL.md index f523b49db9..b58adc60d0 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -14,9 +14,10 @@ Hosting Rails applications is complicated. Even if you already have Postgres, Re ### Hardware Requirements -- Dual core CPU recommended +- modern single core CPU, dual core recommended - 1 GB RAM minimum (with [swap][swap]) - 64 bit Linux compatible with Docker +- 10 GB disk space minimum ### Software Requirements From 66a7b0c30b207c824dc1a994ddedc3f601d1b4e5 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 12 Apr 2017 09:50:03 +0800 Subject: [PATCH 017/221] FIX: Add web hook `DiscourseEvent`s in initializer. --- app/models/web_hook.rb | 30 ---------------------- config/initializers/012-web_hook_events.rb | 29 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 30 deletions(-) create mode 100644 config/initializers/012-web_hook_events.rb diff --git a/app/models/web_hook.rb b/app/models/web_hook.rb index 4159dfbc7c..40fed09de6 100644 --- a/app/models/web_hook.rb +++ b/app/models/web_hook.rb @@ -47,36 +47,6 @@ class WebHook < ActiveRecord::Base def self.enqueue_post_hooks(event, post, user=nil) WebHook.enqueue_hooks(:post, post_id: post.id, category_id: post&.topic&.category_id, event_name: event.to_s) end - - %i(topic_destroyed topic_recovered).each do |event| - DiscourseEvent.on(event) do |topic, user| - WebHook.enqueue_topic_hooks(event, topic, user) - end - end - - DiscourseEvent.on(:topic_created) do |topic, _, user| - WebHook.enqueue_topic_hooks(:topic_created, topic, user) - end - - %i(post_created - post_destroyed - post_recovered).each do |event| - - DiscourseEvent.on(event) do |post, _, user| - WebHook.enqueue_post_hooks(event, post, user) - end - end - - DiscourseEvent.on(:post_edited) do |post, topic_changed| - WebHook.enqueue_post_hooks(:post_edited, post) - WebHook.enqueue_topic_hooks(:topic_edited, post.topic) if post.is_first_post? && topic_changed - end - - %i(user_created user_approved user_updated).each do |event| - DiscourseEvent.on(event) do |user| - WebHook.enqueue_hooks(:user, user_id: user.id, event_name: event.to_s) - end - end end # == Schema Information diff --git a/config/initializers/012-web_hook_events.rb b/config/initializers/012-web_hook_events.rb new file mode 100644 index 0000000000..93edb4e00e --- /dev/null +++ b/config/initializers/012-web_hook_events.rb @@ -0,0 +1,29 @@ +%i(topic_destroyed topic_recovered).each do |event| + DiscourseEvent.on(event) do |topic, user| + WebHook.enqueue_topic_hooks(event, topic, user) + end +end + +DiscourseEvent.on(:topic_created) do |topic, _, user| + WebHook.enqueue_topic_hooks(:topic_created, topic, user) +end + +%i(post_created + post_destroyed + post_recovered).each do |event| + + DiscourseEvent.on(event) do |post, _, user| + WebHook.enqueue_post_hooks(event, post, user) + end +end + +DiscourseEvent.on(:post_edited) do |post, topic_changed| + WebHook.enqueue_post_hooks(:post_edited, post) + WebHook.enqueue_topic_hooks(:topic_edited, post.topic) if post.is_first_post? && topic_changed +end + +%i(user_created user_approved user_updated).each do |event| + DiscourseEvent.on(event) do |user| + WebHook.enqueue_hooks(:user, user_id: user.id, event_name: event.to_s) + end +end From 7cb389a235c3cafbef7581e41b7c263492a1f9a7 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 12 Apr 2017 11:44:00 +0800 Subject: [PATCH 018/221] Add `DiscourseEvent` trigger when user's topic notification level changes. --- app/models/topic_user.rb | 12 +++++++++++- spec/models/topic_user_spec.rb | 14 +++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/app/models/topic_user.rb b/app/models/topic_user.rb index 49c43e7b4e..eb6cceeaf5 100644 --- a/app/models/topic_user.rb +++ b/app/models/topic_user.rb @@ -136,7 +136,17 @@ SQL end if attrs[:notification_level] - MessageBus.publish("/topic/#{topic_id}", { notification_level_change: attrs[:notification_level] }, user_ids: [user_id]) + MessageBus.publish( + "/topic/#{topic_id}", + { notification_level_change: attrs[:notification_level] }, + user_ids: [user_id] + ) + + DiscourseEvent.trigger(:topic_notification_level_changed, + attrs[:notification_level], + user_id, + topic_id + ) end rescue ActiveRecord::RecordNotUnique diff --git a/spec/models/topic_user_spec.rb b/spec/models/topic_user_spec.rb index 6b8cc4b889..6b6d886f05 100644 --- a/spec/models/topic_user_spec.rb +++ b/spec/models/topic_user_spec.rb @@ -72,7 +72,7 @@ describe TopicUser do guardian = Guardian.new(u) TopicCreator.create(u, guardian, title: "this is my topic title") } - let(:topic_user) { TopicUser.get(topic,user) } + let(:topic_user) { TopicUser.get(topic, user) } let(:topic_creator_user) { TopicUser.get(topic, topic.user) } let(:post) { Fabricate(:post, topic: topic, user: user) } @@ -99,6 +99,18 @@ describe TopicUser do end describe 'notifications' do + it 'should trigger the right DiscourseEvent' do + begin + called = false + DiscourseEvent.on(:topic_notification_level_changed) { called = true } + + TopicUser.change(user.id, topic.id, notification_level: TopicUser.notification_levels[:tracking]) + + expect(called).to eq(true) + ensure + DiscourseEvent.off(:topic_notification_level_changed) { called = true } + end + end it 'should be set to tracking if auto_track_topics is enabled' do user.user_option.update_column(:auto_track_topics_after_msecs, 0) From 5cb64810cd1d20f6ff132507e0010d9d79b148e9 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 12 Apr 2017 14:54:03 +0800 Subject: [PATCH 019/221] FIX: Changing notification level in topic footer buttons was not updating timeline. --- .../topic-notifications-button.js.es6 | 8 ++-- .../components/topic-timeline.js.es6 | 2 +- .../discourse/lib/keyboard-shortcuts.js.es6 | 8 ++-- .../widgets/topic-notifications-button.js.es6 | 2 +- .../topic-notifications-button-test.js.es6 | 45 +++++++++++++++++++ 5 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 test/javascripts/acceptance/topic-notifications-button-test.js.es6 diff --git a/app/assets/javascripts/discourse/components/topic-notifications-button.js.es6 b/app/assets/javascripts/discourse/components/topic-notifications-button.js.es6 index e39f45fd9b..b53717855d 100644 --- a/app/assets/javascripts/discourse/components/topic-notifications-button.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-notifications-button.js.es6 @@ -9,12 +9,14 @@ export default MountWidget.extend({ }, @observes('topic.details.notification_level') - _triggerRerender() { - this.queueRerender(); + _triggerEvent() { + this.appEvents.trigger('topic-notifications-button:changed', { + type: 'notification', id: this.get('topic.details.notification_level') + }); }, didInsertElement() { this._super(); - this.dispatch('topic-notifications-button:keyboard-trigger', 'topic-notifications-button'); + this.dispatch('topic-notifications-button:changed', 'topic-notifications-button'); } }); diff --git a/app/assets/javascripts/discourse/components/topic-timeline.js.es6 b/app/assets/javascripts/discourse/components/topic-timeline.js.es6 index eaa065857c..e2b493e45e 100644 --- a/app/assets/javascripts/discourse/components/topic-timeline.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-timeline.js.es6 @@ -79,6 +79,6 @@ export default MountWidget.extend(Docking, { } this.dispatch('topic:current-post-scrolled', 'timeline-scrollarea'); - this.dispatch('topic-notifications-button:keyboard-trigger', 'topic-notifications-button'); + this.dispatch('topic-notifications-button:changed', 'topic-notifications-button'); } }); diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 index 15372b27ad..8a598edbca 100644 --- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 +++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 @@ -196,19 +196,19 @@ export default { }, setTrackingToMuted(event) { - this.appEvents.trigger('topic-notifications-button:keyboard-trigger', {type: 'notification', id: 0, event}); + this.appEvents.trigger('topic-notifications-button:changed', {type: 'notification', id: 0, event}); }, setTrackingToRegular(event) { - this.appEvents.trigger('topic-notifications-button:keyboard-trigger', {type: 'notification', id: 1, event}); + this.appEvents.trigger('topic-notifications-button:changed', {type: 'notification', id: 1, event}); }, setTrackingToTracking(event) { - this.appEvents.trigger('topic-notifications-button:keyboard-trigger', {type: 'notification', id: 2, event}); + this.appEvents.trigger('topic-notifications-button:changed', {type: 'notification', id: 2, event}); }, setTrackingToWatching(event) { - this.appEvents.trigger('topic-notifications-button:keyboard-trigger', {type: 'notification', id: 3, event}); + this.appEvents.trigger('topic-notifications-button:changed', {type: 'notification', id: 3, event}); }, sendToTopicListItemView(action) { diff --git a/app/assets/javascripts/discourse/widgets/topic-notifications-button.js.es6 b/app/assets/javascripts/discourse/widgets/topic-notifications-button.js.es6 index 9c9f0cec7c..4bf84e5631 100644 --- a/app/assets/javascripts/discourse/widgets/topic-notifications-button.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-notifications-button.js.es6 @@ -88,7 +88,7 @@ export default createWidget('topic-notifications-button', { return this.attrs.topic.get('details').updateNotifications(id); }, - topicNotificationsButtonKeyboardTrigger(msg) { + topicNotificationsButtonChanged(msg) { switch(msg.type) { case 'notification': this.notificationLevelChanged(msg.id); diff --git a/test/javascripts/acceptance/topic-notifications-button-test.js.es6 b/test/javascripts/acceptance/topic-notifications-button-test.js.es6 new file mode 100644 index 0000000000..bc01ada8b3 --- /dev/null +++ b/test/javascripts/acceptance/topic-notifications-button-test.js.es6 @@ -0,0 +1,45 @@ +import { acceptance } from "helpers/qunit-helpers"; +acceptance("Topic Notifications button", { + loggedIn: true, + setup() { + const response = object => { + return [ + 200, + { "Content-Type": "application/json" }, + object + ]; + }; + + server.post('/t/280/notifications', () => { // eslint-disable-line no-undef + return response({}); + }); + } +}); + +test("Share Popup", () => { + visit("/t/internationalization-localization/280"); + + const notificationOptions = "#topic-footer-buttons .notification-options"; + + andThen(() => { + ok( + exists(`${notificationOptions} .tracking`), + "it should display the notification options button in the topic's footer" + ); + }); + + click(`${notificationOptions} .tracking`); + click(`${notificationOptions} .dropdown-menu .watching`); + + andThen(() => { + ok( + exists(`${notificationOptions} .watching`), + "it should display the right notification level" + ); + + ok( + exists(".timeline-footer-controls .notification-options .watching"), + 'it should display the right notification level in topic timeline' + ); + }); +}); From 1c1b36ad143978a28cf9e4c6ac1a7422a0d09e99 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 12 Apr 2017 00:41:23 +0530 Subject: [PATCH 020/221] FIX: render emoji in topic title --- app/assets/javascripts/discourse/routes/topic.js.es6 | 2 +- app/serializers/topic_view_serializer.rb | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 3fcb115fbf..bcc266dc86 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -19,7 +19,7 @@ const TopicRoute = Discourse.Route.extend({ titleToken() { const model = this.modelFor('topic'); if (model) { - const result = model.get('title'), + const result = model.get('unicode_title') ? model.get('unicode_title') : model.get('title'), cat = model.get('category'); // Only display uncategorized in the title tag if it was renamed diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index 848c3e3f02..802ccad671 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -3,6 +3,7 @@ require_dependency 'new_post_manager' class TopicViewSerializer < ApplicationSerializer include PostStreamSerializerMixin + include ApplicationHelper def self.attributes_from_topic(*list) [list].flatten.each do |attribute| @@ -58,7 +59,8 @@ class TopicViewSerializer < ApplicationSerializer :message_archived, :tags, :featured_link, - :topic_status_update + :topic_status_update, + :unicode_title # TODO: Split off into proper object / serializer def details @@ -264,4 +266,12 @@ class TopicViewSerializer < ApplicationSerializer object.topic.featured_link end + def include_unicode_title? + !!(object.topic.title =~ /:([\w\-+]*):/) + end + + def unicode_title + gsub_emoji_to_unicode(object.topic.title) + end + end From f322f3a6cf5b7a76e83db325c296b57d5a9755aa Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 12 Apr 2017 15:52:02 +0800 Subject: [PATCH 021/221] Fix the build. --- spec/components/post_creator_spec.rb | 1 + .../topic-notifications-button-test.js.es6 | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index 899ff969ab..9c9f915f52 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -85,6 +85,7 @@ describe PostCreator do DiscourseEvent.expects(:trigger).with(:before_create_topic, anything, anything).once DiscourseEvent.expects(:trigger).with(:after_trigger_post_process, anything).once DiscourseEvent.expects(:trigger).with(:markdown_context, anything).at_least_once + DiscourseEvent.expects(:trigger).with(:topic_notification_level_changed, anything, anything, anything).at_least_once creator.create end diff --git a/test/javascripts/acceptance/topic-notifications-button-test.js.es6 b/test/javascripts/acceptance/topic-notifications-button-test.js.es6 index bc01ada8b3..4689b86bb0 100644 --- a/test/javascripts/acceptance/topic-notifications-button-test.js.es6 +++ b/test/javascripts/acceptance/topic-notifications-button-test.js.es6 @@ -16,7 +16,7 @@ acceptance("Topic Notifications button", { } }); -test("Share Popup", () => { +test("Updating topic notification level", () => { visit("/t/internationalization-localization/280"); const notificationOptions = "#topic-footer-buttons .notification-options"; @@ -37,9 +37,11 @@ test("Share Popup", () => { "it should display the right notification level" ); - ok( - exists(".timeline-footer-controls .notification-options .watching"), - 'it should display the right notification level in topic timeline' - ); + // TODO: tgxworld I can't figure out why the topic timeline doesn't show when + // running the tests in phantomjs + // ok( + // exists(".timeline-footer-controls .notification-options .watching"), + // 'it should display the right notification level in topic timeline' + // ); }); }); From af8a0d93a0420a7cce37a741b5fc69b0277a4f2d Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 12 Apr 2017 16:56:00 +0800 Subject: [PATCH 022/221] Upgrade image_optim. --- Gemfile | 5 +---- Gemfile.lock | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/Gemfile b/Gemfile index 3f8f8f153b..6dc920d055 100644 --- a/Gemfile +++ b/Gemfile @@ -66,10 +66,7 @@ gem 'unf', require: false gem 'email_reply_trimmer', '0.1.6' -# note: for image_optim to correctly work you need to follow -# https://github.com/toy/image_optim -# pinned due to https://github.com/toy/image_optim/pull/75, docker image must be upgraded to upgrade -gem 'image_optim', '0.20.2' +gem 'image_optim' gem 'multi_json' gem 'mustache' gem 'nokogiri' diff --git a/Gemfile.lock b/Gemfile.lock index 2572555d91..dcb353855a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -95,7 +95,7 @@ GEM erubis (2.7.0) excon (0.53.0) execjs (2.7.0) - exifr (1.2.4) + exifr (1.2.5) fabrication (2.9.8) fakeweb (1.3.0) faraday (0.11.0) @@ -109,7 +109,7 @@ GEM flamegraph (0.9.5) foreman (0.82.0) thor (~> 0.19.1) - fspath (2.1.1) + fspath (3.1.0) gc_tracer (1.5.1) globalid (0.3.7) activesupport (>= 4.1.0) @@ -122,14 +122,14 @@ GEM domain_name (~> 0.5) http_accept_language (2.0.5) i18n (0.7.0) - image_optim (0.20.2) - exifr (~> 1.1, >= 1.1.3) - fspath (~> 2.1) - image_size (~> 1.3) + image_optim (0.24.2) + exifr (~> 1.2, >= 1.2.2) + fspath (~> 3.0) + image_size (~> 1.5) in_threads (~> 1.3) progress (~> 3.0, >= 3.0.1) - image_size (1.4.1) - in_threads (1.3.1) + image_size (1.5.0) + in_threads (1.4.0) jmespath (1.3.1) jquery-rails (4.2.1) rails-dom-testing (>= 1, < 3) @@ -218,7 +218,7 @@ GEM redis ruby-openid pg (0.19.0) - progress (3.1.1) + progress (3.3.1) pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) @@ -412,7 +412,7 @@ DEPENDENCIES hiredis htmlentities http_accept_language (~> 2.0.5) - image_optim (= 0.20.2) + image_optim listen (= 0.7.3) logster lru_redux From 7e060e8425aca79f3093ec3cb950e0af81ef70f1 Mon Sep 17 00:00:00 2001 From: Dean Taylor Date: Wed, 12 Apr 2017 07:26:25 +0100 Subject: [PATCH 023/221] FEATURE: Add AWS S3 EU (London) "eu-west-2" region --- app/models/s3_region_site_setting.rb | 1 + config/locales/client.en.yml | 1 + spec/models/s3_region_site_setting_spec.rb | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/s3_region_site_setting.rb b/app/models/s3_region_site_setting.rb index 14fbea51d4..153f92171b 100644 --- a/app/models/s3_region_site_setting.rb +++ b/app/models/s3_region_site_setting.rb @@ -15,6 +15,7 @@ class S3RegionSiteSetting < EnumSiteSetting 'us-west-2', 'us-gov-west-1', 'eu-west-1', + 'eu-west-2', 'eu-central-1', 'ap-southeast-1', 'ap-southeast-2', diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index caa2c4e0c5..378a3d2855 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -181,6 +181,7 @@ en: us_west_2: "US West (Oregon)" us_gov_west_1: "AWS GovCloud (US)" eu_west_1: "EU (Ireland)" + eu_west_2: "EU (London)" eu_central_1: "EU (Frankfurt)" ap_southeast_1: "Asia Pacific (Singapore)" ap_southeast_2: "Asia Pacific (Sydney)" diff --git a/spec/models/s3_region_site_setting_spec.rb b/spec/models/s3_region_site_setting_spec.rb index fb8b203350..9f36a09e28 100644 --- a/spec/models/s3_region_site_setting_spec.rb +++ b/spec/models/s3_region_site_setting_spec.rb @@ -14,7 +14,7 @@ describe S3RegionSiteSetting do describe 'values' do it 'returns all the S3 regions' do - expect(S3RegionSiteSetting.values.map {|x| x[:value]}.sort).to eq(['us-east-1', 'us-west-1', 'us-west-2', 'us-gov-west-1', 'eu-west-1', 'eu-central-1', 'ap-southeast-1', 'ap-southeast-2', 'ap-south-1', 'ap-northeast-1', 'ap-northeast-2', 'sa-east-1', 'cn-north-1'].sort) + expect(S3RegionSiteSetting.values.map {|x| x[:value]}.sort).to eq(['us-east-1', 'us-west-1', 'us-west-2', 'us-gov-west-1', 'eu-west-1', 'eu-west-2', 'eu-central-1', 'ap-southeast-1', 'ap-southeast-2', 'ap-south-1', 'ap-northeast-1', 'ap-northeast-2', 'sa-east-1', 'cn-north-1'].sort) end end From 30a8c5cca2dd975a9976fd4819d5c45c1c64a8e1 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 12 Apr 2017 00:41:23 +0530 Subject: [PATCH 024/221] FIX: render emoji in topic title --- app/assets/javascripts/discourse/routes/topic.js.es6 | 2 +- app/serializers/topic_view_serializer.rb | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 3fcb115fbf..bcc266dc86 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -19,7 +19,7 @@ const TopicRoute = Discourse.Route.extend({ titleToken() { const model = this.modelFor('topic'); if (model) { - const result = model.get('title'), + const result = model.get('unicode_title') ? model.get('unicode_title') : model.get('title'), cat = model.get('category'); // Only display uncategorized in the title tag if it was renamed diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index 848c3e3f02..802ccad671 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -3,6 +3,7 @@ require_dependency 'new_post_manager' class TopicViewSerializer < ApplicationSerializer include PostStreamSerializerMixin + include ApplicationHelper def self.attributes_from_topic(*list) [list].flatten.each do |attribute| @@ -58,7 +59,8 @@ class TopicViewSerializer < ApplicationSerializer :message_archived, :tags, :featured_link, - :topic_status_update + :topic_status_update, + :unicode_title # TODO: Split off into proper object / serializer def details @@ -264,4 +266,12 @@ class TopicViewSerializer < ApplicationSerializer object.topic.featured_link end + def include_unicode_title? + !!(object.topic.title =~ /:([\w\-+]*):/) + end + + def unicode_title + gsub_emoji_to_unicode(object.topic.title) + end + end From 24d3c77d19e273a18ea1da2dae28acbcc0f7d481 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 12 Apr 2017 15:52:02 +0800 Subject: [PATCH 025/221] Fix the build. --- spec/components/post_creator_spec.rb | 1 + .../topic-notifications-button-test.js.es6 | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index 899ff969ab..9c9f915f52 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -85,6 +85,7 @@ describe PostCreator do DiscourseEvent.expects(:trigger).with(:before_create_topic, anything, anything).once DiscourseEvent.expects(:trigger).with(:after_trigger_post_process, anything).once DiscourseEvent.expects(:trigger).with(:markdown_context, anything).at_least_once + DiscourseEvent.expects(:trigger).with(:topic_notification_level_changed, anything, anything, anything).at_least_once creator.create end diff --git a/test/javascripts/acceptance/topic-notifications-button-test.js.es6 b/test/javascripts/acceptance/topic-notifications-button-test.js.es6 index bc01ada8b3..4689b86bb0 100644 --- a/test/javascripts/acceptance/topic-notifications-button-test.js.es6 +++ b/test/javascripts/acceptance/topic-notifications-button-test.js.es6 @@ -16,7 +16,7 @@ acceptance("Topic Notifications button", { } }); -test("Share Popup", () => { +test("Updating topic notification level", () => { visit("/t/internationalization-localization/280"); const notificationOptions = "#topic-footer-buttons .notification-options"; @@ -37,9 +37,11 @@ test("Share Popup", () => { "it should display the right notification level" ); - ok( - exists(".timeline-footer-controls .notification-options .watching"), - 'it should display the right notification level in topic timeline' - ); + // TODO: tgxworld I can't figure out why the topic timeline doesn't show when + // running the tests in phantomjs + // ok( + // exists(".timeline-footer-controls .notification-options .watching"), + // 'it should display the right notification level in topic timeline' + // ); }); }); From 4cb688076605a21eb920baa17807bbec200566f2 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 12 Apr 2017 16:56:00 +0800 Subject: [PATCH 026/221] Upgrade image_optim. --- Gemfile | 5 +---- Gemfile.lock | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/Gemfile b/Gemfile index 3f8f8f153b..6dc920d055 100644 --- a/Gemfile +++ b/Gemfile @@ -66,10 +66,7 @@ gem 'unf', require: false gem 'email_reply_trimmer', '0.1.6' -# note: for image_optim to correctly work you need to follow -# https://github.com/toy/image_optim -# pinned due to https://github.com/toy/image_optim/pull/75, docker image must be upgraded to upgrade -gem 'image_optim', '0.20.2' +gem 'image_optim' gem 'multi_json' gem 'mustache' gem 'nokogiri' diff --git a/Gemfile.lock b/Gemfile.lock index 2572555d91..dcb353855a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -95,7 +95,7 @@ GEM erubis (2.7.0) excon (0.53.0) execjs (2.7.0) - exifr (1.2.4) + exifr (1.2.5) fabrication (2.9.8) fakeweb (1.3.0) faraday (0.11.0) @@ -109,7 +109,7 @@ GEM flamegraph (0.9.5) foreman (0.82.0) thor (~> 0.19.1) - fspath (2.1.1) + fspath (3.1.0) gc_tracer (1.5.1) globalid (0.3.7) activesupport (>= 4.1.0) @@ -122,14 +122,14 @@ GEM domain_name (~> 0.5) http_accept_language (2.0.5) i18n (0.7.0) - image_optim (0.20.2) - exifr (~> 1.1, >= 1.1.3) - fspath (~> 2.1) - image_size (~> 1.3) + image_optim (0.24.2) + exifr (~> 1.2, >= 1.2.2) + fspath (~> 3.0) + image_size (~> 1.5) in_threads (~> 1.3) progress (~> 3.0, >= 3.0.1) - image_size (1.4.1) - in_threads (1.3.1) + image_size (1.5.0) + in_threads (1.4.0) jmespath (1.3.1) jquery-rails (4.2.1) rails-dom-testing (>= 1, < 3) @@ -218,7 +218,7 @@ GEM redis ruby-openid pg (0.19.0) - progress (3.1.1) + progress (3.3.1) pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) @@ -412,7 +412,7 @@ DEPENDENCIES hiredis htmlentities http_accept_language (~> 2.0.5) - image_optim (= 0.20.2) + image_optim listen (= 0.7.3) logster lru_redux From 1a9afa976ddd9285b7bfdc7adfde3300ab2e66db Mon Sep 17 00:00:00 2001 From: Dean Taylor Date: Wed, 12 Apr 2017 07:26:25 +0100 Subject: [PATCH 027/221] FEATURE: Add AWS S3 EU (London) "eu-west-2" region --- app/models/s3_region_site_setting.rb | 1 + config/locales/client.en.yml | 1 + spec/models/s3_region_site_setting_spec.rb | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/s3_region_site_setting.rb b/app/models/s3_region_site_setting.rb index 14fbea51d4..153f92171b 100644 --- a/app/models/s3_region_site_setting.rb +++ b/app/models/s3_region_site_setting.rb @@ -15,6 +15,7 @@ class S3RegionSiteSetting < EnumSiteSetting 'us-west-2', 'us-gov-west-1', 'eu-west-1', + 'eu-west-2', 'eu-central-1', 'ap-southeast-1', 'ap-southeast-2', diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index caa2c4e0c5..378a3d2855 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -181,6 +181,7 @@ en: us_west_2: "US West (Oregon)" us_gov_west_1: "AWS GovCloud (US)" eu_west_1: "EU (Ireland)" + eu_west_2: "EU (London)" eu_central_1: "EU (Frankfurt)" ap_southeast_1: "Asia Pacific (Singapore)" ap_southeast_2: "Asia Pacific (Sydney)" diff --git a/spec/models/s3_region_site_setting_spec.rb b/spec/models/s3_region_site_setting_spec.rb index fb8b203350..9f36a09e28 100644 --- a/spec/models/s3_region_site_setting_spec.rb +++ b/spec/models/s3_region_site_setting_spec.rb @@ -14,7 +14,7 @@ describe S3RegionSiteSetting do describe 'values' do it 'returns all the S3 regions' do - expect(S3RegionSiteSetting.values.map {|x| x[:value]}.sort).to eq(['us-east-1', 'us-west-1', 'us-west-2', 'us-gov-west-1', 'eu-west-1', 'eu-central-1', 'ap-southeast-1', 'ap-southeast-2', 'ap-south-1', 'ap-northeast-1', 'ap-northeast-2', 'sa-east-1', 'cn-north-1'].sort) + expect(S3RegionSiteSetting.values.map {|x| x[:value]}.sort).to eq(['us-east-1', 'us-west-1', 'us-west-2', 'us-gov-west-1', 'eu-west-1', 'eu-west-2', 'eu-central-1', 'ap-southeast-1', 'ap-southeast-2', 'ap-south-1', 'ap-northeast-1', 'ap-northeast-2', 'sa-east-1', 'cn-north-1'].sort) end end From a3e8c3cd7ba5b8e0ea5d89e6eded3f995ab24aba Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 12 Apr 2017 10:52:52 -0400 Subject: [PATCH 028/221] FEATURE: Native theme support This feature introduces the concept of themes. Themes are an evolution of site customizations. Themes introduce two very big conceptual changes: - A theme may include other "child themes", children can include grand children and so on. - A theme may specify a color scheme The change does away with the idea of "enabled" color schemes. It also adds a bunch of big niceties like - You can source a theme from a git repo - History for themes is much improved - You can only have a single enabled theme. Themes can be selected by users, if you opt for it. On a technical level this change comes with a whole bunch of goodies - All CSS is now compiled using a custom pipeline that uses libsass see /lib/stylesheet - There is a single pipeline for css compilation (in the past we used one for customizations and another one for the rest of the app - The stylesheet pipeline is now divorced of sprockets, there is no reliance on sprockets for CSS bundling - CSS is generated with source maps everywhere (including themes) this makes debugging much easier - Our "live reloader" is smarter and avoid a flash of unstyled content we run a file watcher in "puma" in dev so you no longer need to run rake autospec to watch for CSS changes --- Gemfile | 4 +- Gemfile.lock | 17 +- .../javascripts/admin/adapters/theme.js.es6 | 20 + .../admin/components/ace-editor.js.es6 | 8 + .../admin/components/color-input.js.es6 | 33 +- .../admin/components/customize-link.js.es6 | 12 - .../components/inline-edit-checkbox.js.es6 | 36 ++ .../admin-customize-colors-show.js.es6 | 48 ++ .../controllers/admin-customize-colors.js.es6 | 106 +--- .../admin-customize-css-html-show.js.es6 | 78 --- .../admin-customize-themes-edit.js.es6 | 106 ++++ .../admin-customize-themes-show.js.es6 | 163 ++++++ .../admin-color-scheme-select-base.js.es6 | 14 + .../modals/admin-import-theme.js.es6 | 35 ++ .../modals/admin-theme-change.js.es6 | 13 + .../change-site-customization-details.js.es6 | 20 - .../delete-site-customization-details.js.es6 | 7 - .../admin/models/color-scheme.js.es6 | 34 +- .../admin/models/site-customization.js.es6 | 31 -- .../admin/models/staff-action-log.js.es6 | 2 +- .../javascripts/admin/models/theme.js.es6 | 94 ++++ .../routes/admin-customize-colors-show.js.es6 | 18 + .../routes/admin-customize-colors.js.es6 | 8 +- .../admin-customize-css-html-show.js.es6 | 11 - .../routes/admin-customize-css-html.js.es6 | 26 - .../admin/routes/admin-customize-index.js.es6 | 2 +- .../routes/admin-customize-themes-edit.js.es6 | 25 + .../routes/admin-customize-themes-show.js.es6 | 21 + .../routes/admin-customize-themes.js.es6 | 36 ++ .../admin-logs-staff-action-logs.js.es6 | 11 +- .../admin/routes/admin-route-map.js.es6 | 10 +- .../templates/components/customize-link.hbs | 5 - .../components/inline-edit-checkbox.hbs | 8 + .../templates/customize-colors-index.hbs | 1 + .../admin/templates/customize-colors-show.hbs | 53 ++ .../admin/templates/customize-colors.hbs | 72 +-- .../templates/customize-css-html-show.hbs | 75 --- .../admin/templates/customize-css-html.hbs | 13 - .../admin/templates/customize-themes-edit.hbs | 62 +++ ...l-index.hbs => customize-themes-index.hbs} | 0 .../admin/templates/customize-themes-show.hbs | 112 ++++ .../admin/templates/customize-themes.hbs | 24 + .../javascripts/admin/templates/customize.hbs | 2 +- .../modal/admin-color-scheme-select-base.hbs | 12 + .../templates/modal/admin-import-theme.hbs | 27 + .../templates/modal/admin-theme-change.hbs | 8 + .../modal/site-customization-change.hbs | 29 - .../components/combo-box.js.es6 | 32 +- .../discourse/adapters/rest.js.es6 | 30 +- .../components/json-file-uploader.js.es6 | 103 ---- .../components/search-advanced-options.js.es6 | 6 +- .../discourse/controllers/preferences.js.es6 | 14 +- .../controllers/upload-customization.js.es6 | 30 -- .../initializers/live-development.js.es6 | 53 +- .../discourse/lib/load-script.js.es6 | 11 +- .../discourse/lib/theme-selector.js.es6 | 62 +++ .../javascripts/discourse/models/store.js.es6 | 10 +- .../discourse/routes/preferences.js.es6 | 3 +- .../templates/components/color-input.hbs | 2 +- .../components/json-file-uploader.hbs | 12 - .../discourse/templates/preferences.hbs | 9 + .../wizard/components/theme-preview.js.es6 | 2 +- .../javascripts/wizard/models/wizard.js.es6 | 2 +- .../stylesheets/common/admin/admin_base.scss | 140 +---- .../stylesheets/common/admin/customize.scss | 157 ++++++ .../{badges.css.scss => badges.scss} | 0 .../{banner.css.scss => banner.scss} | 0 .../{buttons.css.scss => buttons.scss} | 0 ...{date-picker.css.scss => date-picker.scss} | 0 ...tcuts.css.scss => keyboard_shortcuts.scss} | 0 .../components/{navs.css.scss => navs.scss} | 0 .../stylesheets/common/foundation/base.scss | 4 +- .../common/foundation/variables.scss | 2 +- app/assets/stylesheets/desktop.scss | 2 +- .../{embed.css.scss => embed.scss} | 5 +- app/assets/stylesheets/mobile.scss | 2 +- .../{sweetalert.css => sweetalert.scss} | 0 .../admin/color_schemes_controller.rb | 4 +- .../admin/site_customizations_controller.rb | 92 ---- .../admin/staff_action_logs_controller.rb | 69 +++ app/controllers/admin/themes_controller.rb | 192 +++++++ app/controllers/application_controller.rb | 4 +- .../site_customizations_controller.rb | 35 -- app/controllers/stylesheets_controller.rb | 53 +- app/controllers/themes_controller.rb | 28 + app/helpers/application_helper.rb | 23 + app/models/category.rb | 3 +- app/models/child_theme.rb | 20 + app/models/color_scheme.rb | 114 ++-- app/models/remote_theme.rb | 85 +++ app/models/site_customization.rb | 299 ----------- app/models/stylesheet_cache.rb | 6 +- app/models/theme.rb | 256 +++++++++ app/models/theme_field.rb | 117 ++++ app/models/user_history.rb | 10 +- .../color_scheme_color_serializer.rb | 7 +- app/serializers/color_scheme_serializer.rb | 6 +- .../site_customization_serializer.rb | 7 - app/serializers/site_serializer.rb | 14 +- app/serializers/theme_serializer.rb | 42 ++ app/serializers/user_history_serializer.rb | 3 +- app/services/color_scheme_revisor.rb | 51 +- app/services/staff_action_logger.rb | 59 +- .../common/_discourse_stylesheet.html.erb | 10 +- app/views/layouts/application.html.erb | 7 +- app/views/layouts/crawler.html.erb | 16 +- app/views/layouts/embed.html.erb | 2 +- app/views/layouts/no_ember.html.erb | 12 +- app/views/wizard/index.html.erb | 2 +- config/application.rb | 13 +- config/environments/development.rb | 8 +- config/environments/production.rb | 2 - config/initializers/100-sprockets.rb | 19 - config/locales/client.en.yml | 90 +++- config/locales/server.en.yml | 2 + config/routes.rb | 19 +- config/site_settings.yml | 2 + db/migrate/20170313192741_add_themes.rb | 79 +++ ...322155537_add_theme_to_stylesheet_cache.rb | 6 + ...170324144456_amend_css_columns_in_theme.rb | 13 + .../20170328163918_break_up_themes_table.rb | 54 ++ ...22_add_compiler_version_to_theme_fields.rb | 5 + db/migrate/20170407154510_rename_theme_id.rb | 5 + .../20170410170923_add_theme_remote_fields.rb | 17 + lib/autospec/manager.rb | 12 +- lib/freedom_patches/resolve.rb | 19 - lib/git_importer.rb | 49 ++ lib/middleware/turbo_dev.rb | 1 + lib/sass/discourse_safe_sass_importer.rb | 32 -- lib/sass/discourse_sass_compiler.rb | 85 --- lib/sass/discourse_sass_importer.rb | 100 ---- lib/sass/discourse_stylesheets.rb | 178 ------ lib/stylesheet/common.rb | 5 + lib/stylesheet/compiler.rb | 60 +++ lib/stylesheet/functions.rb | 9 + lib/stylesheet/importer.rb | 126 +++++ lib/stylesheet/manager.rb | 270 ++++++++++ lib/stylesheet/watcher.rb | 70 +++ lib/tasks/assets.rake | 6 +- lib/wizard/builder.rb | 31 +- public/javascripts/spectrum.css | 507 ++++++++++++++++++ public/javascripts/spectrum.js | 2 + .../discourse_sass_compiler_spec.rb | 30 -- spec/components/discourse_stylesheets_spec.rb | 46 -- spec/components/step_updater_spec.rb | 14 +- spec/components/stylesheet/compiler_spec.rb | 21 + spec/components/stylesheet/manager_spec.rb | 57 ++ .../admin/color_schemes_controller_spec.rb | 1 - .../site_customizations_controller_spec.rb | 48 -- .../staff_action_logs_controller_spec.rb | 30 +- .../admin/themes_controller_spec.rb | 101 ++++ .../site_customizations_controller_spec.rb | 45 -- .../stylesheets_controller_spec.rb | 31 +- spec/fabricators/color_scheme_fabricator.rb | 1 - spec/models/color_scheme_spec.rb | 29 +- spec/models/remote_theme_spec.rb | 84 +++ spec/models/site_customization_spec.rb | 155 ------ spec/models/site_spec.rb | 38 ++ spec/models/stylesheet_cache_spec.rb | 6 +- spec/models/theme_spec.rb | 141 +++++ spec/services/color_scheme_revisor_spec.rb | 120 +---- spec/services/staff_action_logger_spec.rb | 46 +- test/stylesheets/test_helper.css | 6 +- 163 files changed, 4415 insertions(+), 2424 deletions(-) create mode 100644 app/assets/javascripts/admin/adapters/theme.js.es6 delete mode 100644 app/assets/javascripts/admin/components/customize-link.js.es6 create mode 100644 app/assets/javascripts/admin/components/inline-edit-checkbox.js.es6 create mode 100644 app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6 delete mode 100644 app/assets/javascripts/admin/controllers/admin-customize-css-html-show.js.es6 create mode 100644 app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 create mode 100644 app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 create mode 100644 app/assets/javascripts/admin/controllers/modals/admin-color-scheme-select-base.js.es6 create mode 100644 app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 create mode 100644 app/assets/javascripts/admin/controllers/modals/admin-theme-change.js.es6 delete mode 100644 app/assets/javascripts/admin/controllers/modals/change-site-customization-details.js.es6 delete mode 100644 app/assets/javascripts/admin/controllers/modals/delete-site-customization-details.js.es6 delete mode 100644 app/assets/javascripts/admin/models/site-customization.js.es6 create mode 100644 app/assets/javascripts/admin/models/theme.js.es6 create mode 100644 app/assets/javascripts/admin/routes/admin-customize-colors-show.js.es6 delete mode 100644 app/assets/javascripts/admin/routes/admin-customize-css-html-show.js.es6 delete mode 100644 app/assets/javascripts/admin/routes/admin-customize-css-html.js.es6 create mode 100644 app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 create mode 100644 app/assets/javascripts/admin/routes/admin-customize-themes-show.js.es6 create mode 100644 app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 delete mode 100644 app/assets/javascripts/admin/templates/components/customize-link.hbs create mode 100644 app/assets/javascripts/admin/templates/components/inline-edit-checkbox.hbs create mode 100644 app/assets/javascripts/admin/templates/customize-colors-index.hbs create mode 100644 app/assets/javascripts/admin/templates/customize-colors-show.hbs delete mode 100644 app/assets/javascripts/admin/templates/customize-css-html-show.hbs delete mode 100644 app/assets/javascripts/admin/templates/customize-css-html.hbs create mode 100644 app/assets/javascripts/admin/templates/customize-themes-edit.hbs rename app/assets/javascripts/admin/templates/{customize-css-html-index.hbs => customize-themes-index.hbs} (100%) create mode 100644 app/assets/javascripts/admin/templates/customize-themes-show.hbs create mode 100644 app/assets/javascripts/admin/templates/customize-themes.hbs create mode 100644 app/assets/javascripts/admin/templates/modal/admin-color-scheme-select-base.hbs create mode 100644 app/assets/javascripts/admin/templates/modal/admin-import-theme.hbs create mode 100644 app/assets/javascripts/admin/templates/modal/admin-theme-change.hbs delete mode 100644 app/assets/javascripts/admin/templates/modal/site-customization-change.hbs delete mode 100644 app/assets/javascripts/discourse/components/json-file-uploader.js.es6 delete mode 100644 app/assets/javascripts/discourse/controllers/upload-customization.js.es6 create mode 100644 app/assets/javascripts/discourse/lib/theme-selector.js.es6 delete mode 100644 app/assets/javascripts/discourse/templates/components/json-file-uploader.hbs create mode 100644 app/assets/stylesheets/common/admin/customize.scss rename app/assets/stylesheets/common/components/{badges.css.scss => badges.scss} (100%) rename app/assets/stylesheets/common/components/{banner.css.scss => banner.scss} (100%) rename app/assets/stylesheets/common/components/{buttons.css.scss => buttons.scss} (100%) rename app/assets/stylesheets/common/components/{date-picker.css.scss => date-picker.scss} (100%) rename app/assets/stylesheets/common/components/{keyboard_shortcuts.css.scss => keyboard_shortcuts.scss} (100%) rename app/assets/stylesheets/common/components/{navs.css.scss => navs.scss} (100%) rename app/assets/stylesheets/{embed.css.scss => embed.scss} (97%) rename app/assets/stylesheets/vendor/{sweetalert.css => sweetalert.scss} (100%) mode change 100755 => 100644 delete mode 100644 app/controllers/admin/site_customizations_controller.rb create mode 100644 app/controllers/admin/themes_controller.rb delete mode 100644 app/controllers/site_customizations_controller.rb create mode 100644 app/controllers/themes_controller.rb create mode 100644 app/models/child_theme.rb create mode 100644 app/models/remote_theme.rb delete mode 100644 app/models/site_customization.rb create mode 100644 app/models/theme.rb create mode 100644 app/models/theme_field.rb delete mode 100644 app/serializers/site_customization_serializer.rb create mode 100644 app/serializers/theme_serializer.rb delete mode 100644 config/initializers/100-sprockets.rb create mode 100644 db/migrate/20170313192741_add_themes.rb create mode 100644 db/migrate/20170322155537_add_theme_to_stylesheet_cache.rb create mode 100644 db/migrate/20170324144456_amend_css_columns_in_theme.rb create mode 100644 db/migrate/20170328163918_break_up_themes_table.rb create mode 100644 db/migrate/20170328203122_add_compiler_version_to_theme_fields.rb create mode 100644 db/migrate/20170407154510_rename_theme_id.rb create mode 100644 db/migrate/20170410170923_add_theme_remote_fields.rb delete mode 100644 lib/freedom_patches/resolve.rb create mode 100644 lib/git_importer.rb delete mode 100644 lib/sass/discourse_safe_sass_importer.rb delete mode 100644 lib/sass/discourse_sass_compiler.rb delete mode 100644 lib/sass/discourse_sass_importer.rb delete mode 100644 lib/sass/discourse_stylesheets.rb create mode 100644 lib/stylesheet/common.rb create mode 100644 lib/stylesheet/compiler.rb create mode 100644 lib/stylesheet/functions.rb create mode 100644 lib/stylesheet/importer.rb create mode 100644 lib/stylesheet/manager.rb create mode 100644 lib/stylesheet/watcher.rb create mode 100644 public/javascripts/spectrum.css create mode 100644 public/javascripts/spectrum.js delete mode 100644 spec/components/discourse_sass_compiler_spec.rb delete mode 100644 spec/components/discourse_stylesheets_spec.rb create mode 100644 spec/components/stylesheet/compiler_spec.rb create mode 100644 spec/components/stylesheet/manager_spec.rb delete mode 100644 spec/controllers/admin/site_customizations_controller_spec.rb create mode 100644 spec/controllers/admin/themes_controller_spec.rb delete mode 100644 spec/controllers/site_customizations_controller_spec.rb create mode 100644 spec/models/remote_theme_spec.rb delete mode 100644 spec/models/site_customization_spec.rb create mode 100644 spec/models/theme_spec.rb diff --git a/Gemfile b/Gemfile index 6dc920d055..57e86b7817 100644 --- a/Gemfile +++ b/Gemfile @@ -93,8 +93,6 @@ gem 'thor', require: false gem 'rest-client' gem 'rinku' gem 'sanitize' -gem 'sass' -gem 'sass-rails' gem 'sidekiq' gem 'sidekiq-statistic' @@ -181,3 +179,5 @@ gem 'memory_profiler', require: false, platform: :mri gem 'rmmseg-cpp', require: false gem 'logster' + +gem 'sassc', require: false diff --git a/Gemfile.lock b/Gemfile.lock index dcb353855a..a0f8c1af3d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -105,7 +105,7 @@ GEM rake rake-compiler fast_xs (0.8.0) - ffi (1.9.17) + ffi (1.9.18) flamegraph (0.9.5) foreman (0.82.0) thor (~> 0.19.1) @@ -318,13 +318,11 @@ GEM crass (~> 1.0.2) nokogiri (>= 1.4.4) nokogumbo (~> 1.4.1) - sass (3.2.19) - sass-rails (5.0.4) - railties (>= 4.0.0, < 5.0) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) + sass (3.4.23) + sassc (1.11.2) + bundler + ffi (~> 1.9.6) + sass (>= 3.3.0) seed-fu (2.3.5) activerecord (>= 3.1, < 4.3) activesupport (>= 3.1, < 4.3) @@ -462,8 +460,7 @@ DEPENDENCIES rtlit ruby-readability sanitize - sass - sass-rails + sassc seed-fu (~> 2.3.5) shoulda sidekiq diff --git a/app/assets/javascripts/admin/adapters/theme.js.es6 b/app/assets/javascripts/admin/adapters/theme.js.es6 new file mode 100644 index 0000000000..df9c8830d1 --- /dev/null +++ b/app/assets/javascripts/admin/adapters/theme.js.es6 @@ -0,0 +1,20 @@ +import RestAdapter from 'discourse/adapters/rest'; + +export default RestAdapter.extend({ + basePath() { + return "/admin/"; + }, + + afterFindAll(results) { + let map = {}; + results.forEach(theme => {map[theme.id] = theme;}); + results.forEach(theme => { + let mapped = theme.get("child_themes") || []; + mapped = mapped.map(t => map[t.id]); + theme.set("childThemes", mapped); + }); + return results; + }, + + jsonMode: true +}); diff --git a/app/assets/javascripts/admin/components/ace-editor.js.es6 b/app/assets/javascripts/admin/components/ace-editor.js.es6 index a03865c40c..3c1d12ffe9 100644 --- a/app/assets/javascripts/admin/components/ace-editor.js.es6 +++ b/app/assets/javascripts/admin/components/ace-editor.js.es6 @@ -14,6 +14,13 @@ export default Ember.Component.extend({ } }, + @observes('mode') + modeChanged() { + if (this._editor && !this._skipContentChangeEvent) { + this._editor.getSession().setMode("ace/mode/" + this.get('mode')); + } + }, + _destroyEditor: function() { if (this._editor) { this._editor.destroy(); @@ -41,6 +48,7 @@ export default Ember.Component.extend({ editor.setTheme("ace/theme/chrome"); editor.setShowPrintMargin(false); + editor.setOptions({fontSize: "14px"}); editor.getSession().setMode("ace/mode/" + this.get('mode')); editor.on('change', () => { this._skipContentChangeEvent = true; diff --git a/app/assets/javascripts/admin/components/color-input.js.es6 b/app/assets/javascripts/admin/components/color-input.js.es6 index 98d5f6e6bb..005c4f5d4b 100644 --- a/app/assets/javascripts/admin/components/color-input.js.es6 +++ b/app/assets/javascripts/admin/components/color-input.js.es6 @@ -1,3 +1,5 @@ +import {default as loadScript, loadCSS } from 'discourse/lib/load-script'; + /** An input field for a color. @@ -6,19 +8,36 @@ @params valid is a boolean indicating if the input field is a valid color. **/ export default Ember.Component.extend({ + classNames: ['color-picker'], hexValueChanged: function() { var hex = this.get('hexValue'); + let $text = this.$('input.hex-input'); + if (this.get('valid')) { - this.$('input').attr('style', 'color: ' + (this.get('brightnessValue') > 125 ? 'black' : 'white') + '; background-color: #' + hex + ';'); + $text.attr('style', 'color: ' + (this.get('brightnessValue') > 125 ? 'black' : 'white') + '; background-color: #' + hex + ';'); + + if (this.get('pickerLoaded')) { + this.$('.picker').spectrum({color: "#" + this.get('hexValue')}); + } } else { - this.$('input').attr('style', ''); + $text.attr('style', ''); } }.observes('hexValue', 'brightnessValue', 'valid'), - _triggerHexChanged: function() { - var self = this; - Em.run.schedule('afterRender', function() { - self.hexValueChanged(); + didInsertElement() { + loadScript('/javascripts/spectrum.js').then(()=>{ + loadCSS('/javascripts/spectrum.css').then(()=>{ + Em.run.schedule('afterRender', ()=>{ + this.$('.picker').spectrum({color: "#" + this.get('hexValue')}) + .on("change.spectrum", (me, color)=>{ + this.set('hexValue', color.toHexString().replace("#","")); + }); + this.set('pickerLoaded', true); + }); + }); }); - }.on('didInsertElement') + Em.run.schedule('afterRender', ()=>{ + this.hexValueChanged(); + }); + } }); diff --git a/app/assets/javascripts/admin/components/customize-link.js.es6 b/app/assets/javascripts/admin/components/customize-link.js.es6 deleted file mode 100644 index 0600f6b5cd..0000000000 --- a/app/assets/javascripts/admin/components/customize-link.js.es6 +++ /dev/null @@ -1,12 +0,0 @@ -import { getOwner } from 'discourse-common/lib/get-owner'; - -export default Ember.Component.extend({ - router: function() { - return getOwner(this).lookup('router:main'); - }.property(), - - active: function() { - const id = this.get('customization.id'); - return this.get('router.url').indexOf(`/customize/css_html/${id}/css`) !== -1; - }.property('router.url', 'customization.id') -}); diff --git a/app/assets/javascripts/admin/components/inline-edit-checkbox.js.es6 b/app/assets/javascripts/admin/components/inline-edit-checkbox.js.es6 new file mode 100644 index 0000000000..5c168760c7 --- /dev/null +++ b/app/assets/javascripts/admin/components/inline-edit-checkbox.js.es6 @@ -0,0 +1,36 @@ +import {default as computed, observes} from "ember-addons/ember-computed-decorators"; + +export default Ember.Component.extend({ + init(){ + this._super(); + this.set("checkedInternal", this.get("checked")); + }, + + classNames: ['inline-edit'], + + @observes("checked") + checkedChanged() { + this.set("checkedInternal", this.get("checked")); + }, + + @computed("labelKey") + label(key) { + return I18n.t(key); + }, + + @computed("checked", "checkedInternal") + changed(checked, checkedInternal) { + return (!!checked) !== (!!checkedInternal); + }, + + actions: { + cancelled(){ + this.set("checkedInternal", this.get("checked")); + }, + + finished(){ + this.set("checked", this.get("checkedInternal")); + this.sendAction(); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6 new file mode 100644 index 0000000000..e57606a489 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6 @@ -0,0 +1,48 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Controller.extend({ + @computed("model.colors","onlyOverridden") + colors(allColors, onlyOverridden) { + if (onlyOverridden) { + return allColors.filter(color => color.get("overridden")); + } else { + return allColors; + } + }, + + actions: { + + revert: function(color) { + color.revert(); + }, + + undo: function(color) { + color.undo(); + }, + + copy() { + var newColorScheme = Em.copy(this.get('model'), true); + newColorScheme.set('name', I18n.t('admin.customize.colors.copy_name_prefix') + ' ' + this.get('model.name')); + newColorScheme.save().then(()=>{ + this.get('allColors').pushObject(newColorScheme); + this.replaceRoute('adminCustomize.colors.show', newColorScheme); + }); + }, + + save: function() { + this.get('model').save(); + }, + + destroy: function() { + + return bootbox.confirm(I18n.t("admin.customize.colors.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), result => { + if (result) { + this.get('model').destroy().then(()=>{ + this.get('allColors').removeObject(this.get('model')); + this.replaceRoute('adminCustomize.colors'); + }); + } + }); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 index ae253aec84..87166e386f 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 @@ -1,10 +1,14 @@ -export default Ember.Controller.extend({ - onlyOverridden: false, +import showModal from 'discourse/lib/show-modal'; +export default Ember.Controller.extend({ baseColorScheme: function() { return this.get('model').findBy('is_base', true); }.property('model.@each.id'), + baseColorSchemes: function() { + return this.get('model').filterBy('is_base', true); + }.property('model.@each.id'), + baseColors: function() { var baseColorsHash = Em.Object.create({}); _.each(this.get('baseColorScheme.colors'), function(color){ @@ -13,99 +17,25 @@ export default Ember.Controller.extend({ return baseColorsHash; }.property('baseColorScheme'), - removeSelected() { - this.get('model').removeObject(this.get('selectedItem')); - this.set('selectedItem', null); - }, - - filterContent: function() { - if (!this.get('selectedItem')) { return; } - - if (!this.get('onlyOverridden')) { - this.set('colors', this.get('selectedItem.colors')); - return; - } - - const matches = []; - _.each(this.get('selectedItem.colors'), function(color){ - if (color.get('overridden')) matches.pushObject(color); - }); - - this.set('colors', matches); - }.observes('onlyOverridden'), - - updateEnabled: function() { - var selectedItem = this.get('selectedItem'); - if (selectedItem.get('enabled')) { - this.get('model').forEach(function(c) { - if (c !== selectedItem) { - c.set('enabled', false); - c.startTrackingChanges(); - c.notifyPropertyChange('description'); - } - }); - } - }, - actions: { - selectColorScheme: function(colorScheme) { - if (this.get('selectedItem')) { this.get('selectedItem').set('selected', false); } - this.set('selectedItem', colorScheme); - this.set('colors', colorScheme.get('colors')); - colorScheme.set('savingStatus', null); - colorScheme.set('selected', true); - this.filterContent(); + + newColorSchemeWithBase(baseKey) { + const base = this.get('baseColorSchemes').findBy('base_scheme_id', baseKey); + const newColorScheme = Em.copy(base, true); + newColorScheme.set('name', I18n.t('admin.customize.colors.new_name')); + newColorScheme.set('base_scheme_id', base.get('base_scheme_id')); + newColorScheme.save().then(()=>{ + this.get('model').pushObject(newColorScheme); + newColorScheme.set('savingStatus', null); + this.replaceRoute('adminCustomize.colors.show', newColorScheme); + }); }, newColorScheme() { - const newColorScheme = Em.copy(this.get('baseColorScheme'), true); - newColorScheme.set('name', I18n.t('admin.customize.colors.new_name')); - this.get('model').pushObject(newColorScheme); - this.send('selectColorScheme', newColorScheme); - this.set('onlyOverridden', false); + showModal('admin-color-scheme-select-base', { model: this.get('baseColorSchemes'), admin: true}); }, - revert: function(color) { - color.revert(); - }, - undo: function(color) { - color.undo(); - }, - - toggleEnabled: function() { - var selectedItem = this.get('selectedItem'); - selectedItem.toggleProperty('enabled'); - selectedItem.save({enabledOnly: true}); - this.updateEnabled(); - }, - - save: function() { - this.get('selectedItem').save(); - this.updateEnabled(); - }, - - copy(colorScheme) { - var newColorScheme = Em.copy(colorScheme, true); - newColorScheme.set('name', I18n.t('admin.customize.colors.copy_name_prefix') + ' ' + colorScheme.get('name')); - this.get('model').pushObject(newColorScheme); - this.send('selectColorScheme', newColorScheme); - }, - - destroy: function() { - var self = this, - item = self.get('selectedItem'); - - return bootbox.confirm(I18n.t("admin.customize.colors.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) { - if (result) { - if (item.get('newRecord')) { - self.removeSelected(); - } else { - item.destroy().then(function(){ self.removeSelected(); }); - } - } - }); - } } }); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-css-html-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-css-html-show.js.es6 deleted file mode 100644 index 47cf280ae6..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-customize-css-html-show.js.es6 +++ /dev/null @@ -1,78 +0,0 @@ -import { url } from 'discourse/lib/computed'; - -const sections = ['css', 'header', 'top', 'footer', 'head-tag', 'body-tag', - 'mobile-css', 'mobile-header', 'mobile-top', 'mobile-footer', - 'embedded-css']; - -const activeSections = {}; -sections.forEach(function(s) { - activeSections[Ember.String.camelize(s) + "Active"] = Ember.computed.equal('section', s); -}); - - -export default Ember.Controller.extend(activeSections, { - maximized: false, - section: null, - - previewUrl: url("model.key", "/?preview-style=%@"), - downloadUrl: url('model.id', '/admin/site_customizations/%@'), - - mobile: function() { - return this.get('section').indexOf('mobile-') === 0; - }.property('section'), - - maximizeIcon: function() { - return this.get('maximized') ? 'compress' : 'expand'; - }.property('maximized'), - - saveButtonText: function() { - return this.get('model.isSaving') ? I18n.t('saving') : I18n.t('admin.customize.save'); - }.property('model.isSaving'), - - saveDisabled: function() { - return !this.get('model.changed') || this.get('model.isSaving'); - }.property('model.changed', 'model.isSaving'), - - adminCustomizeCssHtml: Ember.inject.controller(), - - undoPreviewUrl: url('/?preview-style='), - defaultStyleUrl: url('/?preview-style=default'), - - actions: { - save() { - this.get('model').saveChanges(); - }, - - destroy() { - return bootbox.confirm(I18n.t("admin.customize.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), result => { - if (result) { - const model = this.get('model'); - model.destroyRecord().then(() => { - this.get('adminCustomizeCssHtml').get('model').removeObject(model); - this.transitionToRoute('adminCustomizeCssHtml'); - }); - } - }); - }, - - toggleMaximize: function() { - this.toggleProperty('maximized'); - }, - - toggleMobile: function() { - const section = this.get('section'); - - // Try to send to the same tab as before - let dest; - if (this.get('mobile')) { - dest = section.replace('mobile-', ''); - if (sections.indexOf(dest) === -1) { dest = 'css'; } - } else { - dest = 'mobile-' + section; - if (sections.indexOf(dest) === -1) { dest = 'mobile-css'; } - } - this.replaceRoute('adminCustomizeCssHtml.show', this.get('model.id'), dest); - } - } - -}); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 new file mode 100644 index 0000000000..507ea7136b --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 @@ -0,0 +1,106 @@ +import { url } from 'discourse/lib/computed'; +import { default as computed } from 'ember-addons/ember-computed-decorators'; + +export default Ember.Controller.extend({ + maximized: false, + section: null, + + targets: [ + {id: 0, name: I18n.t('admin.customize.theme.common')}, + {id: 1, name: I18n.t('admin.customize.theme.desktop')}, + {id: 2, name: I18n.t('admin.customize.theme.mobile')} + ], + + currentTarget: 0, + + setTargetName: function(name) { + let target; + switch(name) { + case "common": target = 0; break; + case "desktop": target = 1; break; + case "mobile": target = 2; break; + } + + this.set("currentTarget", target); + }, + + @computed("currentTarget") + currentTargetName(target) { + switch(parseInt(target)) { + case 0: return "common"; + case 1: return "desktop"; + case 2: return "mobile"; + } + }, + + @computed("fieldName") + activeSectionMode(fieldName) { + return fieldName && fieldName.indexOf("scss") > -1 ? "css" : "html"; + }, + + @computed("fieldName", "currentTargetName", "model") + activeSection: { + get(fieldName, target, model) { + return model.getField(target, fieldName); + }, + set(value, fieldName, target, model) { + model.setField(target, fieldName, value); + return value; + } + }, + + + @computed("currentTarget") + fields(target) { + let fields = [ + "scss", "head_tag", "header", "after_header", "body_tag", "footer" + ]; + + if (parseInt(target) === 0) { + fields.push("embedded_scss"); + } + + return fields.map(name=>{ + let hash = { + key: (`admin.customize.theme.${name}.text`), + name: name + }; + + if (name.indexOf("_tag") > 0) { + hash.icon = "file-text-o"; + } + + hash.title = I18n.t(`admin.customize.theme.${name}.title`); + + return hash; + }); + }, + + previewUrl: url('model.key', '/?preview-style=%@'), + + maximizeIcon: function() { + return this.get('maximized') ? 'compress' : 'expand'; + }.property('maximized'), + + saveButtonText: function() { + return this.get('model.isSaving') ? I18n.t('saving') : I18n.t('admin.customize.save'); + }.property('model.isSaving'), + + saveDisabled: function() { + return !this.get('model.changed') || this.get('model.isSaving'); + }.property('model.changed', 'model.isSaving'), + + undoPreviewUrl: url('/?preview-style='), + defaultStyleUrl: url('/?preview-style=default'), + + actions: { + save() { + this.get('model').saveChanges("theme_fields"); + }, + + toggleMaximize: function() { + this.toggleProperty('maximized'); + } + } + +}); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 new file mode 100644 index 0000000000..7ca3501241 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 @@ -0,0 +1,163 @@ +import { default as computed } from 'ember-addons/ember-computed-decorators'; +import { url } from 'discourse/lib/computed'; + +export default Ember.Controller.extend({ + + @computed("model.theme_fields.@each") + hasEditedFields(fields) { + return fields.any(f=>!Em.isBlank(f.value)); + }, + + @computed('model.theme_fields.@each') + editedDescriptions(fields) { + let descriptions = []; + let description = target => { + let current = fields.filter(field => field.target === target && !Em.isBlank(field.value)); + if (current.length > 0) { + let text = I18n.t('admin.customize.theme.'+target); + let localized = current.map(f=>I18n.t('admin.customize.theme.'+f.name + '.text')); + return text + ": " + localized.join(" , "); + } + }; + ['common','desktop','mobile'].forEach(target=> { + descriptions.push(description(target)); + }); + return descriptions.reject(d=>Em.isBlank(d)); + }, + + @computed("colorSchemeId", "model.color_scheme_id") + colorSchemeChanged(colorSchemeId, existingId) { + colorSchemeId = colorSchemeId === null ? null : parseInt(colorSchemeId); + return colorSchemeId !== existingId; + }, + + @computed("availableChildThemes", "model.childThemes.@each", "model", "allowChildThemes") + selectableChildThemes(available, childThemes, model, allowChildThemes) { + if (!allowChildThemes && (!childThemes || childThemes.length === 0)) { + return null; + } + + let themes = []; + available.forEach(t=> { + if (!childThemes || (childThemes.indexOf(t) === -1)) { + themes.push(t); + }; + }); + return themes.length === 0 ? null : themes; + }, + + showSchemes: Em.computed.or("model.default", "model.user_selectable"), + + @computed("allThemes", "allThemes.length", "model") + availableChildThemes(allThemes, count) { + if (count === 1) { + return null; + } + + let excludeIds = [this.get("model.id")]; + + let themes = []; + allThemes.forEach(theme => { + if (excludeIds.indexOf(theme.get("id")) === -1) { + themes.push(theme); + } + }); + + return themes; + }, + + downloadUrl: url('model.id', '/admin/themes/%@'), + + actions: { + + updateToLatest() { + this.set("updatingRemote", true); + this.get("model").updateToLatest().finally(()=>{ + this.set("updatingRemote", false); + }); + }, + + checkForThemeUpdates() { + this.set("updatingRemote", true); + this.get("model").checkForUpdates().finally(()=>{ + this.set("updatingRemote", false); + }); + }, + + cancelChangeScheme() { + this.set("colorSchemeId", this.get("model.color_scheme_id")); + }, + changeScheme(){ + let schemeId = this.get("colorSchemeId"); + this.set("model.color_scheme_id", schemeId === null ? null : parseInt(schemeId)); + this.get("model").saveChanges("color_scheme_id"); + }, + startEditingName() { + this.set("oldName", this.get("model.name")); + this.set("editingName", true); + }, + cancelEditingName() { + this.set("model.name", this.get("oldName")); + this.set("editingName", false); + }, + finishedEditingName() { + this.get("model").saveChanges("name"); + this.set("editingName", false); + }, + + editTheme() { + let edit = ()=>this.transitionToRoute('adminCustomizeThemes.edit', {model: this.get('model')}); + + if (this.get("model.remote_theme")) { + bootbox.confirm(I18n.t("admin.customize.theme.edit_confirm"), result => { + if (result) { + edit(); + } + }); + } else { + edit(); + } + }, + + applyDefault() { + const model = this.get("model"); + model.saveChanges("default").then(()=>{ + if (model.get("default")) { + this.get("allThemes").forEach(theme=>{ + if (theme !== model && theme.get('default')) { + theme.set("default", false); + } + }); + } + }); + }, + + applyUserSelectable() { + this.get("model").saveChanges("user_selectable"); + }, + + addChildTheme() { + let themeId = parseInt(this.get("selectedChildThemeId")); + let theme = this.get("allThemes").findBy("id", themeId); + this.get("model").addChildTheme(theme); + }, + + removeChildTheme(theme) { + this.get("model").removeChildTheme(theme); + }, + + destroy() { + return bootbox.confirm(I18n.t("admin.customize.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), result => { + if (result) { + const model = this.get('model'); + model.destroyRecord().then(() => { + this.get('allThemes').removeObject(model); + this.transitionToRoute('adminCustomizeThemes'); + }); + } + }); + }, + + } + +}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-color-scheme-select-base.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-color-scheme-select-base.js.es6 new file mode 100644 index 0000000000..94939fa09f --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-color-scheme-select-base.js.es6 @@ -0,0 +1,14 @@ +import ModalFunctionality from 'discourse/mixins/modal-functionality'; + +export default Ember.Controller.extend(ModalFunctionality, { + + adminCustomizeColors: Ember.inject.controller(), + + actions: { + selectBase() { + this.get('adminCustomizeColors') + .send('newColorSchemeWithBase', this.get('selectedBaseThemeId')); + this.send('closeModal'); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 new file mode 100644 index 0000000000..d59d419ef5 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 @@ -0,0 +1,35 @@ +import ModalFunctionality from 'discourse/mixins/modal-functionality'; +import { ajax } from 'discourse/lib/ajax'; +// import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Controller.extend(ModalFunctionality, { + local: Ember.computed.equal('selection', 'local'), + remote: Ember.computed.equal('selection', 'remote'), + selection: 'local', + adminCustomizeThemes: Ember.inject.controller(), + + actions: { + importTheme() { + + let options = { + type: 'POST' + }; + + if (this.get('local')) { + options.processData = false; + options.contentType = false; + options.data = new FormData(); + options.data.append('theme', $('#file-input')[0].files[0]); + } else { + options.data = {remote: this.get('uploadUrl')}; + } + + ajax('/admin/themes/import', options).then(result=>{ + const theme = this.store.createRecord('theme',result.theme); + this.get('adminCustomizeThemes').send('addTheme', theme); + this.send('closeModal'); + }); + + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-theme-change.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-theme-change.js.es6 new file mode 100644 index 0000000000..82aba506a2 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-theme-change.js.es6 @@ -0,0 +1,13 @@ +import ModalFunctionality from 'discourse/mixins/modal-functionality'; +import { ajax } from 'discourse/lib/ajax'; + +export default Ember.Controller.extend(ModalFunctionality, { + loadDiff() { + this.set('loading', true); + ajax('/admin/logs/staff_action_logs/' + this.get('model.id') + '/diff') + .then(diff=>{ + this.set('loading', false); + this.set('diff', diff.side_by_side); + }); + } +}); diff --git a/app/assets/javascripts/admin/controllers/modals/change-site-customization-details.js.es6 b/app/assets/javascripts/admin/controllers/modals/change-site-customization-details.js.es6 deleted file mode 100644 index ca6ac31db1..0000000000 --- a/app/assets/javascripts/admin/controllers/modals/change-site-customization-details.js.es6 +++ /dev/null @@ -1,20 +0,0 @@ -import ModalFunctionality from 'discourse/mixins/modal-functionality'; - -export default Ember.Controller.extend(ModalFunctionality, { - previousSelected: Ember.computed.equal('selectedTab', 'previous'), - newSelected: Ember.computed.equal('selectedTab', 'new'), - - onShow: function() { - this.send("selectNew"); - }, - - actions: { - selectNew: function() { - this.set('selectedTab', 'new'); - }, - - selectPrevious: function() { - this.set('selectedTab', 'previous'); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/modals/delete-site-customization-details.js.es6 b/app/assets/javascripts/admin/controllers/modals/delete-site-customization-details.js.es6 deleted file mode 100644 index 95537e305a..0000000000 --- a/app/assets/javascripts/admin/controllers/modals/delete-site-customization-details.js.es6 +++ /dev/null @@ -1,7 +0,0 @@ -import ChangeSiteCustomizationDetailsController from "admin/controllers/modals/change-site-customization-details"; - -export default ChangeSiteCustomizationDetailsController.extend({ - onShow() { - this.send("selectPrevious"); - } -}); diff --git a/app/assets/javascripts/admin/models/color-scheme.js.es6 b/app/assets/javascripts/admin/models/color-scheme.js.es6 index 743c779d6c..20bb348d57 100644 --- a/app/assets/javascripts/admin/models/color-scheme.js.es6 +++ b/app/assets/javascripts/admin/models/color-scheme.js.es6 @@ -9,18 +9,17 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, { }, description: function() { - return "" + this.name + (this.enabled ? ' (*)' : ''); + return "" + this.name; }.property(), startTrackingChanges: function() { this.set('originals', { - name: this.get('name'), - enabled: this.get('enabled') + name: this.get('name') }); }, copy: function() { - var newScheme = ColorScheme.create({name: this.get('name'), enabled: false, can_edit: true, colors: Em.A()}); + var newScheme = ColorScheme.create({name: this.get('name'), can_edit: true, colors: Em.A()}); _.each(this.get('colors'), function(c){ newScheme.colors.pushObject(ColorSchemeColor.create({name: c.get('name'), hex: c.get('hex'), default_hex: c.get('default_hex')})); }); @@ -29,19 +28,15 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, { changed: function() { if (!this.originals) return false; - if (this.originals['name'] !== this.get('name') || this.originals['enabled'] !== this.get('enabled')) return true; + if (this.originals['name'] !== this.get('name')) return true; if (_.any(this.get('colors'), function(c){ return c.get('changed'); })) return true; return false; - }.property('name', 'enabled', 'colors.@each.changed', 'saving'), + }.property('name', 'colors.@each.changed', 'saving'), disableSave: function() { return !this.get('changed') || this.get('saving') || _.any(this.get('colors'), function(c) { return !c.get('valid'); }); }.property('changed'), - disableEnable: function() { - return !this.get('id') || this.get('saving'); - }.property('id', 'saving'), - newRecord: function() { return (!this.get('id')); }.property('id'), @@ -53,11 +48,11 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, { this.set('savingStatus', I18n.t('saving')); this.set('saving',true); - var data = { enabled: this.enabled }; + var data = {}; if (!opts || !opts.enabledOnly) { data.name = this.name; - + data.base_scheme_id = this.get('base_scheme_id'); data.colors = []; _.each(this.get('colors'), function(c) { if (!self.id || c.get('changed')) { @@ -78,8 +73,6 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, { _.each(self.get('colors'), function(c) { c.startTrackingChanges(); }); - } else { - self.set('originals.enabled', data.enabled); } self.set('savingStatus', I18n.t('saved')); self.set('saving', false); @@ -96,30 +89,23 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, { }); var ColorSchemes = Ember.ArrayProxy.extend({ - selectedItemChanged: function() { - var selected = this.get('selectedItem'); - _.each(this.get('content'),function(i) { - return i.set('selected', selected === i); - }); - }.observes('selectedItem') }); ColorScheme.reopenClass({ findAll: function() { var colorSchemes = ColorSchemes.create({ content: [], loading: true }); - ajax('/admin/color_schemes').then(function(all) { + return ajax('/admin/color_schemes').then(function(all) { _.each(all, function(colorScheme){ colorSchemes.pushObject(ColorScheme.create({ id: colorScheme.id, name: colorScheme.name, - enabled: colorScheme.enabled, is_base: colorScheme.is_base, + base_scheme_id: colorScheme.base_scheme_id, colors: colorScheme.colors.map(function(c) { return ColorSchemeColor.create({name: c.name, hex: c.hex, default_hex: c.default_hex}); }) })); }); - colorSchemes.set('loading', false); + return colorSchemes; }); - return colorSchemes; } }); diff --git a/app/assets/javascripts/admin/models/site-customization.js.es6 b/app/assets/javascripts/admin/models/site-customization.js.es6 deleted file mode 100644 index fe2176bf11..0000000000 --- a/app/assets/javascripts/admin/models/site-customization.js.es6 +++ /dev/null @@ -1,31 +0,0 @@ -import RestModel from 'discourse/models/rest'; - -const trackedProperties = [ - 'enabled', 'name', 'stylesheet', 'header', 'top', 'footer', 'mobile_stylesheet', - 'mobile_header', 'mobile_top', 'mobile_footer', 'head_tag', 'body_tag', 'embedded_css' -]; - -function changed() { - const originals = this.get('originals'); - if (!originals) { return false; } - return _.some(trackedProperties, (p) => originals[p] !== this.get(p)); -} - -const SiteCustomization = RestModel.extend({ - description: function() { - return "" + this.name + (this.enabled ? ' (*)' : ''); - }.property('selected', 'name', 'enabled'), - - changed: changed.property.apply(changed, trackedProperties.concat('originals')), - - startTrackingChanges: function() { - this.set('originals', this.getProperties(trackedProperties)); - }.on('init'), - - saveChanges() { - return this.save(this.getProperties(trackedProperties)).then(() => this.startTrackingChanges()); - }, - -}); - -export default SiteCustomization; diff --git a/app/assets/javascripts/admin/models/staff-action-log.js.es6 b/app/assets/javascripts/admin/models/staff-action-log.js.es6 index 24ec4fbcc0..f108dd21d8 100644 --- a/app/assets/javascripts/admin/models/staff-action-log.js.es6 +++ b/app/assets/javascripts/admin/models/staff-action-log.js.es6 @@ -39,7 +39,7 @@ const StaffActionLog = Discourse.Model.extend({ }.property('action_name'), useCustomModalForDetails: function() { - return _.contains(['change_site_customization', 'delete_site_customization'], this.get('action_name')); + return _.contains(['change_theme', 'delete_theme'], this.get('action_name')); }.property('action_name') }); diff --git a/app/assets/javascripts/admin/models/theme.js.es6 b/app/assets/javascripts/admin/models/theme.js.es6 new file mode 100644 index 0000000000..463d54b258 --- /dev/null +++ b/app/assets/javascripts/admin/models/theme.js.es6 @@ -0,0 +1,94 @@ +import RestModel from 'discourse/models/rest'; +import { default as computed } from 'ember-addons/ember-computed-decorators'; + +const Theme = RestModel.extend({ + + @computed('theme_fields') + themeFields(fields) { + + if (!fields) { + this.set('theme_fields', []); + return {}; + } + + let hash = {}; + if (fields) { + fields.forEach(field=>{ + hash[field.target + " " + field.name] = field; + }); + } + return hash; + }, + + getField(target, name) { + let themeFields = this.get("themeFields"); + let key = target + " " + name; + let field = themeFields[key]; + return field ? field.value : ""; + }, + + setField(target, name, value) { + this.set("changed", true); + + let themeFields = this.get("themeFields"); + let key = target + " " + name; + let field = themeFields[key]; + if (!field) { + field = {name, target, value}; + this.theme_fields.push(field); + themeFields[key] = field; + } else { + field.value = value; + } + }, + + @computed("childThemes.@each") + child_theme_ids(childThemes) { + if (childThemes) { + return childThemes.map(theme => Ember.get(theme, "id")); + } + }, + + removeChildTheme(theme) { + const childThemes = this.get("childThemes"); + childThemes.removeObject(theme); + return this.saveChanges("child_theme_ids"); + }, + + addChildTheme(theme){ + let childThemes = this.get("childThemes"); + childThemes.removeObject(theme); + childThemes.pushObject(theme); + return this.saveChanges("child_theme_ids"); + }, + + @computed('name', 'default') + description: function(name, isDefault) { + if (isDefault) { + return I18n.t('admin.customize.theme.default_name', {name: name}); + } else { + return name; + } + }, + + checkForUpdates() { + return this.save({remote_check: true}) + .then(() => this.set("changed", false)); + }, + + updateToLatest() { + return this.save({remote_update: true}) + .then(() => this.set("changed", false)); + }, + + changed: false, + + saveChanges() { + const hash = this.getProperties.apply(this, arguments); + return this.save(hash) + .then(() => this.set("changed", false)); + }, + +}); + +export default Theme; diff --git a/app/assets/javascripts/admin/routes/admin-customize-colors-show.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-colors-show.js.es6 new file mode 100644 index 0000000000..3f8bdcddcd --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-customize-colors-show.js.es6 @@ -0,0 +1,18 @@ +export default Ember.Route.extend({ + + model(params) { + const all = this.modelFor('adminCustomize.colors'); + const model = all.findBy('id', parseInt(params.scheme_id)); + return model ? model : this.replaceWith('adminCustomize.colors.index'); + }, + + serialize(model) { + return {scheme_id: model.get('id')}; + }, + + setupController(controller, model) { + controller.set('model', model); + controller.set('allColors', this.modelFor('adminCustomize.colors')); + } +}); + diff --git a/app/assets/javascripts/admin/routes/admin-customize-colors.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-colors.js.es6 index 8a47f1ba21..043e571271 100644 --- a/app/assets/javascripts/admin/routes/admin-customize-colors.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-customize-colors.js.es6 @@ -6,9 +6,7 @@ export default Ember.Route.extend({ return ColorScheme.findAll(); }, - deactivate() { - this._super(); - this.controllerFor('adminCustomizeColors').set('selectedItem', null); - }, - + setupController(controller, model) { + controller.set("model", model); + } }); diff --git a/app/assets/javascripts/admin/routes/admin-customize-css-html-show.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-css-html-show.js.es6 deleted file mode 100644 index 7df829706f..0000000000 --- a/app/assets/javascripts/admin/routes/admin-customize-css-html-show.js.es6 +++ /dev/null @@ -1,11 +0,0 @@ -export default Ember.Route.extend({ - model(params) { - const all = this.modelFor('adminCustomizeCssHtml'); - const model = all.findBy('id', parseInt(params.site_customization_id)); - return model ? { model, section: params.section } : this.replaceWith('adminCustomizeCssHtml.index'); - }, - - setupController(controller, hash) { - controller.setProperties(hash); - } -}); diff --git a/app/assets/javascripts/admin/routes/admin-customize-css-html.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-css-html.js.es6 deleted file mode 100644 index 5bbb460959..0000000000 --- a/app/assets/javascripts/admin/routes/admin-customize-css-html.js.es6 +++ /dev/null @@ -1,26 +0,0 @@ -import showModal from 'discourse/lib/show-modal'; -import { popupAjaxError } from 'discourse/lib/ajax-error'; - -export default Ember.Route.extend({ - model() { - return this.store.findAll('site-customization'); - }, - - actions: { - importModal() { - showModal('upload-customization'); - }, - - newCustomization(obj) { - obj = obj || {name: I18n.t("admin.customize.new_style")}; - const item = this.store.createRecord('site-customization'); - - const all = this.modelFor('adminCustomizeCssHtml'); - const self = this; - item.save(obj).then(function() { - all.pushObject(item); - self.transitionTo('adminCustomizeCssHtml.show', item.get('id'), 'css'); - }).catch(popupAjaxError); - } - } -}); diff --git a/app/assets/javascripts/admin/routes/admin-customize-index.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-index.js.es6 index 45cb6e21fb..d8b1446dfb 100644 --- a/app/assets/javascripts/admin/routes/admin-customize-index.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-customize-index.js.es6 @@ -1,5 +1,5 @@ export default Ember.Route.extend({ beforeModel() { - this.transitionTo('adminCustomize.colors'); + this.transitionTo('adminCustomizeThemes'); } }); diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 new file mode 100644 index 0000000000..aed0709e55 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 @@ -0,0 +1,25 @@ +export default Ember.Route.extend({ + model(params) { + const all = this.modelFor('adminCustomizeThemes'); + const model = all.findBy('id', parseInt(params.theme_id)); + return model ? { model, target: params.target, field_name: params.field_name} : this.replaceWith('adminCustomizeThemes.index'); + }, + + serialize(wrapper) { + return { + model: wrapper.model, + target: wrapper.target || "common", + field_name: wrapper.field_name || "scss", + theme_id: wrapper.model.get("id") + }; + }, + + + setupController(controller, wrapper) { + controller.set("model", wrapper.model); + controller.setTargetName(wrapper.target || "common"); + controller.set("fieldName", wrapper.field_name || "scss"); + this.controllerFor("adminCustomizeThemes").set("editingTheme", true); + }, + +}); diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes-show.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes-show.js.es6 new file mode 100644 index 0000000000..8e925ba6dd --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-customize-themes-show.js.es6 @@ -0,0 +1,21 @@ +export default Ember.Route.extend({ + + serialize(model) { + return {theme_id: model.get('id')}; + }, + + model(params) { + const all = this.modelFor('adminCustomizeThemes'); + const model = all.findBy('id', parseInt(params.theme_id)); + return model ? model : this.replaceWith('adminCustomizeTheme.index'); + }, + + setupController(controller, model) { + controller.set("model", model); + const parentController = this.controllerFor("adminCustomizeThemes"); + parentController.set("editingTheme", false); + controller.set("allThemes", parentController.get("model")); + controller.set("colorSchemes", parentController.get("model.extras.color_schemes")); + controller.set("colorSchemeId", model.get("color_scheme_id")); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 new file mode 100644 index 0000000000..6e5e19b7f5 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 @@ -0,0 +1,36 @@ +import showModal from 'discourse/lib/show-modal'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; + +export default Ember.Route.extend({ + model() { + return this.store.findAll('theme'); + }, + + setupController(controller, model) { + this._super(controller, model); + // TODO ColorScheme to model + controller.set("editingTheme", false); + }, + + actions: { + importModal() { + showModal('admin-import-theme', {admin: true}); + }, + + addTheme(theme) { + const all = this.modelFor('adminCustomizeThemes'); + all.pushObject(theme); + this.transitionTo('adminCustomizeThemes.show', theme.get('id')); + }, + + + newTheme(obj) { + obj = obj || {name: I18n.t("admin.customize.new_style")}; + const item = this.store.createRecord('theme'); + + item.save(obj).then(() => { + this.send('addTheme', item); + }).catch(popupAjaxError); + } + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js.es6 b/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js.es6 index 8b19ccb0dc..698f90d77c 100644 --- a/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js.es6 @@ -13,14 +13,9 @@ export default Discourse.Route.extend({ }, showCustomDetailsModal(model) { - const modalName = (model.action_name + '_details').replace(/\_/g, "-"); - - showModal(modalName, { - model, - admin: true, - templateName: 'site-customization-change' - }); - this.controllerFor('modal').set('modalClass', 'tabbed-modal log-details-modal'); + let modal = showModal('admin-theme-change', { model, admin: true}); + this.controllerFor('modal').set('modalClass', 'history-modal'); + modal.loadDiff(); } } }); diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 index bd38784bb7..dd87207156 100644 --- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 @@ -15,10 +15,14 @@ export default function() { }); this.route('adminCustomize', { path: '/customize', resetNamespace: true } ,function() { - this.route('colors'); - this.route('adminCustomizeCssHtml', { path: 'css_html', resetNamespace: true }, function() { - this.route('show', {path: '/:site_customization_id/:section'}); + this.route('colors', function() { + this.route('show', {path: '/:scheme_id'}); + }); + + this.route('adminCustomizeThemes', { path: 'themes', resetNamespace: true }, function() { + this.route('show', {path: '/:theme_id'}); + this.route('edit', {path: '/:theme_id/:target/:field_name/edit'}); }); this.route('adminSiteText', { path: '/site_texts', resetNamespace: true }, function() { diff --git a/app/assets/javascripts/admin/templates/components/customize-link.hbs b/app/assets/javascripts/admin/templates/components/customize-link.hbs deleted file mode 100644 index dd3c4104c7..0000000000 --- a/app/assets/javascripts/admin/templates/components/customize-link.hbs +++ /dev/null @@ -1,5 +0,0 @@ -
  • - - {{customization.description}} - -
  • diff --git a/app/assets/javascripts/admin/templates/components/inline-edit-checkbox.hbs b/app/assets/javascripts/admin/templates/components/inline-edit-checkbox.hbs new file mode 100644 index 0000000000..3a651ad0df --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/inline-edit-checkbox.hbs @@ -0,0 +1,8 @@ + +{{#if changed}} + {{d-button action="finished" class="btn-primary btn-small submit-edit" icon="check"}} + {{d-button action="cancelled" class="btn-small cancel-edit" icon="times"}} +{{/if}} diff --git a/app/assets/javascripts/admin/templates/customize-colors-index.hbs b/app/assets/javascripts/admin/templates/customize-colors-index.hbs new file mode 100644 index 0000000000..62bbb7a8fc --- /dev/null +++ b/app/assets/javascripts/admin/templates/customize-colors-index.hbs @@ -0,0 +1 @@ +

    {{i18n 'admin.customize.colors.about'}}

    diff --git a/app/assets/javascripts/admin/templates/customize-colors-show.hbs b/app/assets/javascripts/admin/templates/customize-colors-show.hbs new file mode 100644 index 0000000000..3c57058c9a --- /dev/null +++ b/app/assets/javascripts/admin/templates/customize-colors-show.hbs @@ -0,0 +1,53 @@ +
    +
    +

    {{text-field class="style-name" value=model.name}}

    + +
    + + + + {{model.savingStatus}} +
    + +
    + +
    + +
    + + {{#if colors.length}} + + + + + + + + + + {{#each colors as |c|}} + + + + + + {{/each}} + +
    {{i18n 'admin.customize.color'}}
    + {{c.translatedName}} +
    + {{c.description}} +
    {{color-input hexValue=c.hex brightnessValue=c.brightness valid=c.valid}} + + +
    + {{else}} +

    {{i18n 'search.no_results'}}

    + {{/if}} +
    +
    diff --git a/app/assets/javascripts/admin/templates/customize-colors.hbs b/app/assets/javascripts/admin/templates/customize-colors.hbs index 62b489075d..354598068b 100644 --- a/app/assets/javascripts/admin/templates/customize-colors.hbs +++ b/app/assets/javascripts/admin/templates/customize-colors.hbs @@ -3,76 +3,16 @@
      {{#each model as |scheme|}} {{#unless scheme.is_base}} -
    • {{scheme.description}}
    • + +
    • + {{#link-to 'adminCustomize.colors.show' scheme replace=true}}{{scheme.description}}{{/link-to}} +
    • {{/unless}} {{/each}}
    - +
    -{{#if selectedItem}} -
    -
    -

    {{text-field class="style-name" value=selectedItem.name}}

    - -
    - - - - - {{selectedItem.savingStatus}} -
    - -
    - -
    - -
    - - {{#if colors.length}} - - - - - - - - - - {{#each colors as |c|}} - - - - - - {{/each}} - -
    {{i18n 'admin.customize.color'}}
    - {{c.translatedName}} -
    - {{c.description}} -
    {{color-input hexValue=c.hex brightnessValue=c.brightness valid=c.valid}} - - -
    - {{else}} -

    {{i18n 'search.no_results'}}

    - {{/if}} -
    -
    -{{else}} -

    {{i18n 'admin.customize.colors.about'}}

    -{{/if}} +{{outlet}}
    diff --git a/app/assets/javascripts/admin/templates/customize-css-html-show.hbs b/app/assets/javascripts/admin/templates/customize-css-html-show.hbs deleted file mode 100644 index 6a10c7ae0b..0000000000 --- a/app/assets/javascripts/admin/templates/customize-css-html-show.hbs +++ /dev/null @@ -1,75 +0,0 @@ -
    -
    - {{text-field class="style-name" value=model.name}} - {{fa-icon "download"}} {{i18n 'admin.export_json.button_text'}} - -
    - -
    - -
    - {{#if cssActive}}{{ace-editor content=model.stylesheet mode="scss"}}{{/if}} - {{#if headerActive}}{{ace-editor content=model.header mode="html"}}{{/if}} - {{#if topActive}}{{ace-editor content=model.top mode="html"}}{{/if}} - {{#if footerActive}}{{ace-editor content=model.footer mode="html"}}{{/if}} - {{#if headTagActive}}{{ace-editor content=model.head_tag mode="html"}}{{/if}} - {{#if bodyTagActive}}{{ace-editor content=model.body_tag mode="html"}}{{/if}} - {{#if embeddedCssActive}}{{ace-editor content=model.embedded_css mode="css"}}{{/if}} - {{#if mobileCssActive}}{{ace-editor content=model.mobile_stylesheet mode="scss"}}{{/if}} - {{#if mobileHeaderActive}}{{ace-editor content=model.mobile_header mode="html"}}{{/if}} - {{#if mobileTopActive}}{{ace-editor content=model.mobile_top mode="html"}}{{/if}} - {{#if mobileFooterActive}}{{ace-editor content=model.mobile_footer mode="html"}}{{/if}} -
    - - -
    -
    diff --git a/app/assets/javascripts/admin/templates/customize-css-html.hbs b/app/assets/javascripts/admin/templates/customize-css-html.hbs deleted file mode 100644 index 73b8e22c9f..0000000000 --- a/app/assets/javascripts/admin/templates/customize-css-html.hbs +++ /dev/null @@ -1,13 +0,0 @@ -
    -

    {{i18n 'admin.customize.css_html.long_title'}}

    -
      - {{#each model as |c|}} - {{customize-link customization=c}} - {{/each}} -
    - - {{d-button label="admin.customize.new" icon="plus" action="newCustomization" class="btn-primary"}} - {{d-button action="importModal" icon="upload" label="admin.customize.import"}} -
    - -{{outlet}} diff --git a/app/assets/javascripts/admin/templates/customize-themes-edit.hbs b/app/assets/javascripts/admin/templates/customize-themes-edit.hbs new file mode 100644 index 0000000000..fbdafdd413 --- /dev/null +++ b/app/assets/javascripts/admin/templates/customize-themes-edit.hbs @@ -0,0 +1,62 @@ +
    +
    +

    {{i18n 'admin.customize.theme.edit_css_html'}} {{#link-to 'adminCustomizeThemes.show' model.id replace=true}}{{model.name}}{{/link-to}}

    + + + +
    + +
    + +
    +
    + {{ace-editor content=activeSection mode=activeSectionMode}} +
    + + +
    +
    diff --git a/app/assets/javascripts/admin/templates/customize-css-html-index.hbs b/app/assets/javascripts/admin/templates/customize-themes-index.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/customize-css-html-index.hbs rename to app/assets/javascripts/admin/templates/customize-themes-index.hbs diff --git a/app/assets/javascripts/admin/templates/customize-themes-show.hbs b/app/assets/javascripts/admin/templates/customize-themes-show.hbs new file mode 100644 index 0000000000..5bbd9cb2ee --- /dev/null +++ b/app/assets/javascripts/admin/templates/customize-themes-show.hbs @@ -0,0 +1,112 @@ +
    +

    + {{#if editingName}} + {{text-field value=model.name autofocus="true"}} + {{d-button action="finishedEditingName" class="btn-primary btn-small submit-edit" icon="check"}} + {{d-button action="cancelEditingName" class="btn-small cancel-edit" icon="times"}} + {{else}} + {{model.name}} {{fa-icon "pencil"}} + {{/if}} +

    + + {{#if model.remote_theme}} +

    + {{i18n "admin.customize.theme.about_theme"}} +

    + {{#if model.remote_theme.license_url}} +

    + {{i18n "admin.customize.theme.license"}} {{fa-icon "copyright"}} +

    + {{/if}} + {{/if}} + + +

    + {{inline-edit-checkbox action="applyDefault" labelKey="admin.customize.theme.is_default" checked=model.default}} + {{inline-edit-checkbox action="applyUserSelectable" labelKey="admin.customize.theme.user_selectable" checked=model.user_selectable}} +

    + + {{#if showSchemes}} +

    {{i18n "admin.customize.theme.color_scheme"}}

    +

    {{i18n "admin.customize.theme.color_scheme_select"}}

    +

    {{combo-box content=colorSchemes + nameProperty="name" + value=colorSchemeId + valueAttribute="id"}} + {{#if colorSchemeChanged}} + {{d-button action="changeScheme" class="btn-primary btn-small submit-edit" icon="check"}} + {{d-button action="cancelChangeScheme" class="btn-small cancel-edit" icon="times"}} + {{/if}} +

    + {{#link-to 'adminCustomize.colors' class="btn edit"}}{{i18n 'admin.customize.colors.edit'}}{{/link-to}} + {{/if}} + +

    {{i18n "admin.customize.theme.css_html"}}

    + {{#if hasEditedFields}} + +

    {{i18n "admin.customize.theme.custom_sections"}}

    +
      + {{#each editedDescriptions as |desc|}} +
    • {{desc}}
    • + {{/each}} +
    + {{else}} +

    + {{i18n "admin.customize.theme.edit_css_html_help"}} +

    + {{/if}} +

    + {{#if model.remote_theme}} + {{#if model.remote_theme.commits_behind}} + {{#d-button action="updateToLatest" icon="download"}}{{i18n "admin.customize.theme.update_to_latest"}}{{/d-button}} + {{else}} + {{#d-button action="checkForThemeUpdates" icon="refresh"}}{{i18n "admin.customize.theme.check_for_updates"}}{{/d-button}} + {{/if}} + {{/if}} + {{#d-button action="editTheme" class="btn edit"}}{{i18n 'admin.customize.theme.edit_css_html'}}{{/d-button}} + {{#if model.remote_theme}} + + {{#if updatingRemote}} + {{i18n 'admin.customize.theme.updating'}} + {{else}} + {{#if model.remote_theme.commits_behind}} + {{i18n 'admin.customize.theme.commits_behind' count=model.remote_theme.commits_behind}} + {{else}} + {{i18n 'admin.customize.theme.up_to_date'}} {{format-date model.remote_theme.updated_at leaveAgo="true"}} + {{/if}} + {{/if}} + + {{/if}} +

    + + {{#if availableChildThemes}} +

    {{i18n "admin.customize.theme.included_themes"}}

    + {{#unless model.childThemes.length}} +

    + +

    + {{else}} +
      + {{#each model.childThemes as |child|}} +
    • {{child.name}} {{d-button action="removeChildTheme" actionParam=child class="btn-small cancel-edit" icon="times"}}
    • + {{/each}} +
    + {{/unless}} + {{#if selectableChildThemes}} +

    {{combo-box content=selectableChildThemes + nameProperty="name" + value=selectedChildThemeId + valueAttribute="id"}} + + {{#d-button action="addChildTheme" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}} +

    + {{/if}} + {{/if}} + + {{fa-icon "download"}} {{i18n 'admin.export_json.button_text'}} + + {{d-button action="destroy" label="admin.customize.delete" icon="trash" class="btn-danger"}} +
    diff --git a/app/assets/javascripts/admin/templates/customize-themes.hbs b/app/assets/javascripts/admin/templates/customize-themes.hbs new file mode 100644 index 0000000000..ce8f682075 --- /dev/null +++ b/app/assets/javascripts/admin/templates/customize-themes.hbs @@ -0,0 +1,24 @@ +{{#unless editingTheme}} +
    +

    {{i18n 'admin.customize.theme.long_title'}}

    +
      + {{#each model as |theme|}} +
    • + {{#link-to 'adminCustomizeThemes.show' theme replace=true}} + {{theme.name}} + {{#if theme.user_selectable}} + {{fa-icon "user"}} + {{/if}} + {{#if theme.default}} + {{fa-icon "asterisk"}} + {{/if}} + {{/link-to}} +
    • + {{/each}} +
    + + {{d-button label="admin.customize.new" icon="plus" action="newTheme" class="btn-primary"}} + {{d-button action="importModal" icon="upload" label="admin.customize.import"}} +
    +{{/unless}} +{{outlet}} diff --git a/app/assets/javascripts/admin/templates/customize.hbs b/app/assets/javascripts/admin/templates/customize.hbs index 7696b34811..3065c09855 100644 --- a/app/assets/javascripts/admin/templates/customize.hbs +++ b/app/assets/javascripts/admin/templates/customize.hbs @@ -1,7 +1,7 @@
    {{#admin-nav}} + {{nav-item route='adminCustomizeThemes' label='admin.customize.theme.title'}} {{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}} - {{nav-item route='adminCustomizeCssHtml' label='admin.customize.css_html.title'}} {{nav-item route='adminSiteText' label='admin.site_text.title'}} {{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}} {{nav-item route='adminUserFields' label='admin.user_fields.title'}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-color-scheme-select-base.hbs b/app/assets/javascripts/admin/templates/modal/admin-color-scheme-select-base.hbs new file mode 100644 index 0000000000..5286bbf0b0 --- /dev/null +++ b/app/assets/javascripts/admin/templates/modal/admin-color-scheme-select-base.hbs @@ -0,0 +1,12 @@ +
    + {{#d-modal-body title="admin.customize.colors.select_base.title"}} + {{i18n "admin.customize.colors.select_base.description"}} + {{combo-box content=model + nameProperty="name" + value=selectedBaseThemeId + valueAttribute="base_scheme_id"}} + {{/d-modal-body}} + +
    diff --git a/app/assets/javascripts/admin/templates/modal/admin-import-theme.hbs b/app/assets/javascripts/admin/templates/modal/admin-import-theme.hbs new file mode 100644 index 0000000000..7c4058d52d --- /dev/null +++ b/app/assets/javascripts/admin/templates/modal/admin-import-theme.hbs @@ -0,0 +1,27 @@ +{{#d-modal-body class='upload-selector' title="admin.customize.theme.import_theme"}} +
    + {{radio-button name="upload" id="local" value="local" selection=selection}} + + {{#if local}} +
    +
    + {{i18n 'admin.customize.theme.import_file_tip'}} +
    + {{/if}} +
    +
    + {{radio-button name="upload" id="remote" value="remote" selection=selection}} + + {{#if remote}} +
    + {{input value=uploadUrl placeholder="https://github.com/discourse/discourse/sample_theme"}} + {{i18n 'admin.customize.theme.import_web_tip'}} +
    + {{/if}} +
    +{{/d-modal-body}} + + diff --git a/app/assets/javascripts/admin/templates/modal/admin-theme-change.hbs b/app/assets/javascripts/admin/templates/modal/admin-theme-change.hbs new file mode 100644 index 0000000000..3fbaf0ac86 --- /dev/null +++ b/app/assets/javascripts/admin/templates/modal/admin-theme-change.hbs @@ -0,0 +1,8 @@ +
    + {{#d-modal-body title="admin.logs.staff_actions.modal_title"}} + {{{diff}}} + {{/d-modal-body}} + +
    diff --git a/app/assets/javascripts/admin/templates/modal/site-customization-change.hbs b/app/assets/javascripts/admin/templates/modal/site-customization-change.hbs deleted file mode 100644 index bbacea995c..0000000000 --- a/app/assets/javascripts/admin/templates/modal/site-customization-change.hbs +++ /dev/null @@ -1,29 +0,0 @@ -
    - - {{#d-modal-body title="admin.logs.staff_actions.modal_title"}} - - - {{/d-modal-body}} - -
    diff --git a/app/assets/javascripts/discourse-common/components/combo-box.js.es6 b/app/assets/javascripts/discourse-common/components/combo-box.js.es6 index 2a2c4e2ba7..b2c66fc127 100644 --- a/app/assets/javascripts/discourse-common/components/combo-box.js.es6 +++ b/app/assets/javascripts/discourse-common/components/combo-box.js.es6 @@ -22,17 +22,40 @@ export default Ember.Component.extend(bufferedRender({ let selected = this.get('value'); if (!Em.isNone(selected)) { selected = selected.toString(); } - if (this.get('content')) { - this.get('content').forEach(o => { + let selectedFound = false; + let firstVal = undefined; + const content = this.get('content'); + + if (content) { + let first = true; + content.forEach(o => { let val = o[this.get('valueAttribute')]; if (typeof val === "undefined") { val = o; } if (!Em.isNone(val)) { val = val.toString(); } const selectedText = (val === selected) ? "selected" : ""; const name = Handlebars.Utils.escapeExpression(Ember.get(o, nameProperty) || o); + + if (val === selected) { + selectedFound = true; + } + if (first) { + firstVal = val; + first = false; + } buffer.push(``); }); } + + if (!selectedFound) { + if (none) { + this.set('value', null); + } else { + this.set('value', firstVal); + } + } + + Ember.run.scheduleOnce('afterRender', this, this._updateSelect2); }, @observes('value') @@ -81,9 +104,14 @@ export default Ember.Component.extend(bufferedRender({ } this.set('value', val); }); + Ember.run.scheduleOnce('afterRender', this, this._triggerChange); }, + _updateSelect2() { + this.$().trigger('change.select2'); + }, + _triggerChange() { this.$().trigger('change'); }, diff --git a/app/assets/javascripts/discourse/adapters/rest.js.es6 b/app/assets/javascripts/discourse/adapters/rest.js.es6 index 72d05fb7dd..b56ac1628a 100644 --- a/app/assets/javascripts/discourse/adapters/rest.js.es6 +++ b/app/assets/javascripts/discourse/adapters/rest.js.es6 @@ -1,7 +1,7 @@ import { ajax } from 'discourse/lib/ajax'; import { hashString } from 'discourse/lib/hash'; -const ADMIN_MODELS = ['plugin', 'site-customization', 'embeddable-host', 'web-hook', 'web-hook-event']; +const ADMIN_MODELS = ['plugin', 'theme', 'embeddable-host', 'web-hook', 'web-hook-event']; export function Result(payload, responseJson) { this.payload = payload; @@ -76,22 +76,38 @@ export default Ember.Object.extend({ this.cached[this.storageKey(type,findArgs,opts)] = hydrated; }, + jsonMode: false, + + getPayload(method, data) { + let payload = {method, data}; + + if (this.jsonMode) { + payload.contentType = "application/json"; + payload.data = JSON.stringify(data); + } + + return payload; + }, + update(store, type, id, attrs) { const data = {}; const typeField = Ember.String.underscore(type); data[typeField] = attrs; - return ajax(this.pathFor(store, type, id), { method: 'PUT', data }).then(function(json) { - return new Result(json[typeField], json); - }); + + return ajax(this.pathFor(store, type, id), this.getPayload('PUT', data)) + .then(function(json) { + return new Result(json[typeField], json); + }); }, createRecord(store, type, attrs) { const data = {}; const typeField = Ember.String.underscore(type); data[typeField] = attrs; - return ajax(this.pathFor(store, type), { method: 'POST', data }).then(function (json) { - return new Result(json[typeField], json); - }); + return ajax(this.pathFor(store, type), this.getPayload('POST', data)) + .then(function (json) { + return new Result(json[typeField], json); + }); }, destroyRecord(store, type, record) { diff --git a/app/assets/javascripts/discourse/components/json-file-uploader.js.es6 b/app/assets/javascripts/discourse/components/json-file-uploader.js.es6 deleted file mode 100644 index e29491d48a..0000000000 --- a/app/assets/javascripts/discourse/components/json-file-uploader.js.es6 +++ /dev/null @@ -1,103 +0,0 @@ - -export default Em.Component.extend({ - fileInput: null, - loading: false, - expectedRootObjectName: null, - hover: 0, - - classNames: ['json-uploader'], - - _initialize: function() { - const $this = this.$(); - const self = this; - - const $fileInput = $this.find('#js-file-input'); - this.set('fileInput', $fileInput[0]); - - $fileInput.on('change', function() { - self.fileSelected(this.files); - }); - - $this.on('dragover', function(e) { - if (e.preventDefault) e.preventDefault(); - return false; - }); - $this.on('dragenter', function(e) { - if (e.preventDefault) e.preventDefault(); - self.set('hover', self.get('hover') + 1); - return false; - }); - $this.on('dragleave', function(e) { - if (e.preventDefault) e.preventDefault(); - self.set('hover', self.get('hover') - 1); - return false; - }); - $this.on('drop', function(e) { - if (e.preventDefault) e.preventDefault(); - - self.set('hover', 0); - self.fileSelected(e.dataTransfer.files); - return false; - }); - - }.on('didInsertElement'), - - accept: function() { - return ".json,application/json,application/x-javascript,text/json" + (this.get('extension') ? "," + this.get('extension') : ""); - }.property('extension'), - - setReady: function() { - let parsed; - try { - parsed = JSON.parse(this.get('value')); - } catch (e) { - this.set('ready', false); - return; - } - - const rootObject = parsed[this.get('expectedRootObjectName')]; - - if (rootObject !== null && rootObject !== undefined) { - this.set('ready', true); - } else { - this.set('ready', false); - } - }.observes('destination', 'expectedRootObjectName'), - - actions: { - selectFile: function() { - const $fileInput = $(this.get('fileInput')); - $fileInput.click(); - } - }, - - fileSelected(fileList) { - const self = this; - let files = []; - for (let i = 0; i < fileList.length; i++) { - files[i] = fileList[i]; - } - const fileNameRegex = /\.(json|txt)$/; - files = files.filter(function(file) { - if (fileNameRegex.test(file.name)) { - return true; - } - if (file.type === "text/plain") { - return true; - } - return false; - }); - const firstFile = fileList[0]; - - this.set('loading', true); - - let reader = new FileReader(); - reader.onload = function(evt) { - self.set('value', evt.target.result); - self.set('loading', false); - }; - - reader.readAsText(firstFile); - } - -}); diff --git a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 index dd6910d18a..0bc886a817 100644 --- a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 +++ b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 @@ -143,12 +143,14 @@ export default Em.Component.extend({ matchRegEx = matchRegEx || replaceRegEx; const match = this.filterBlocks(matchRegEx); + let val = this.get(key); + if (match.length !== 0) { const userInput = match[0].replace(replaceRegEx, ''); - if (this.get(key) !== userInput) { + if (val !== userInput) { this.set(key, userInput); } - } else if(this.get(key).length !== 0) { + } else if(val && val.length !== 0) { this.set(key, ''); } }, diff --git a/app/assets/javascripts/discourse/controllers/preferences.js.es6 b/app/assets/javascripts/discourse/controllers/preferences.js.es6 index 5ee945d4da..9173a77153 100644 --- a/app/assets/javascripts/discourse/controllers/preferences.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences.js.es6 @@ -1,12 +1,23 @@ import { setting } from 'discourse/lib/computed'; import CanCheckEmails from 'discourse/mixins/can-check-emails'; import { popupAjaxError } from 'discourse/lib/ajax-error'; -import computed from "ember-addons/ember-computed-decorators"; +import { default as computed, observes } from "ember-addons/ember-computed-decorators"; import { cook } from 'discourse/lib/text'; import { NotificationLevels } from 'discourse/lib/notification-levels'; +import { listThemes, selectDefaultTheme, previewTheme } from 'discourse/lib/theme-selector'; export default Ember.Controller.extend(CanCheckEmails, { + userSelectableThemes: function(){ + return listThemes(this.site); + }.property(), + + @observes("selectedTheme") + themeKeyChanged() { + let key = this.get("selectedTheme"); + previewTheme(key); + }, + @computed("model.watchedCategories", "model.trackedCategories", "model.mutedCategories") selectedCategories(watched, tracked, muted) { return [].concat(watched, tracked, muted); @@ -162,6 +173,7 @@ export default Ember.Controller.extend(CanCheckEmails, { Discourse.User.currentProp('name', model.get('name')); } model.set('bio_cooked', cook(model.get('bio_raw'))); + selectDefaultTheme(this.get('selectedTheme')); this.set('saved', true); }).catch(popupAjaxError); }, diff --git a/app/assets/javascripts/discourse/controllers/upload-customization.js.es6 b/app/assets/javascripts/discourse/controllers/upload-customization.js.es6 deleted file mode 100644 index ae7575887a..0000000000 --- a/app/assets/javascripts/discourse/controllers/upload-customization.js.es6 +++ /dev/null @@ -1,30 +0,0 @@ -import ModalFunctionality from 'discourse/mixins/modal-functionality'; - -export default Ember.Controller.extend(ModalFunctionality, { - notReady: Em.computed.not('ready'), - adminCustomizeCssHtml: Ember.inject.controller(), - - ready: function() { - try { - const parsed = JSON.parse(this.get('customizationFile')); - return !!parsed["site_customization"]; - } catch (e) { - return false; - } - }.property('customizationFile'), - - actions: { - createCustomization: function() { - const object = JSON.parse(this.get('customizationFile')).site_customization; - - // Slight fixup before creating object - object.enabled = false; - delete object.id; - delete object.key; - - const controller = this.get('adminCustomizeCssHtml'); - controller.send('newCustomization', object); - } - } - -}); diff --git a/app/assets/javascripts/discourse/initializers/live-development.js.es6 b/app/assets/javascripts/discourse/initializers/live-development.js.es6 index f73172e154..de6994b932 100644 --- a/app/assets/javascripts/discourse/initializers/live-development.js.es6 +++ b/app/assets/javascripts/discourse/initializers/live-development.js.es6 @@ -1,5 +1,45 @@ import DiscourseURL from 'discourse/lib/url'; +export function refreshCSS(node, hash, newHref, options) { + + let $orig = $(node); + + if ($orig.data('reloading')) { + + if (options && options.force) { + clearTimeout($orig.data('timeout')); + $orig.data("copy").remove(); + } else { + return; + } + } + + if (!$orig.data('orig')) { + $orig.data('orig', node.href); + } + + $orig.data('reloading', true); + + const orig = $(node).data('orig'); + + let reloaded = $orig.clone(true); + if (hash) { + reloaded[0].href = orig + (orig.indexOf('?') >= 0 ? "&hash=" : "?hash=") + hash; + } else { + reloaded[0].href = newHref; + } + + $orig.after(reloaded); + + let timeout = setTimeout(()=>{ + $orig.remove(); + reloaded.data('reloading', false); + }, 2000); + + $orig.data("timeout", timeout); + $orig.data("copy", reloaded); +} + // Use the message bus for live reloading of components for faster development. export default { name: "live-development", @@ -48,17 +88,8 @@ export default { document.location.reload(true); } else { $('link').each(function() { - // TODO: stop bundling css in DEV please - if (true || (this.href.match(me.name) && me.hash)) { - if (!$(this).data('orig')) { - $(this).data('orig', this.href); - } - const orig = $(this).data('orig'); - if (!me.hash) { - window.__uniq = window.__uniq || 1; - me.hash = window.__uniq++; - } - this.href = orig + (orig.indexOf('?') >= 0 ? "&hash=" : "?hash=") + me.hash; + if (this.href.match(me.name) && (me.hash || me.new_href)) { + refreshCSS(this, me.hash, me.new_href); } }); } diff --git a/app/assets/javascripts/discourse/lib/load-script.js.es6 b/app/assets/javascripts/discourse/lib/load-script.js.es6 index cc24df86de..13dbbb6ecf 100644 --- a/app/assets/javascripts/discourse/lib/load-script.js.es6 +++ b/app/assets/javascripts/discourse/lib/load-script.js.es6 @@ -24,6 +24,10 @@ function loadWithTag(path, cb) { }; } +export function loadCSS(url) { + return loadScript(url, { css: true }); +} + export default function loadScript(url, opts) { // TODO: Remove this once plugins have been updated not to use it: @@ -47,8 +51,11 @@ export default function loadScript(url, opts) { delete _loading[url]; }); - const cb = function() { + const cb = function(data) { _loaded[url] = true; + if (opts && opts.css) { + $("head").append(""); + } done(); resolve(); }; @@ -66,7 +73,7 @@ export default function loadScript(url, opts) { if (opts.scriptTag) { loadWithTag(cdnUrl, cb); } else { - ajax({url: cdnUrl, dataType: "script", cache: true}).then(cb); + ajax({url: cdnUrl, dataType: opts.css ? "text": "script", cache: true}).then(cb); } }); } diff --git a/app/assets/javascripts/discourse/lib/theme-selector.js.es6 b/app/assets/javascripts/discourse/lib/theme-selector.js.es6 new file mode 100644 index 0000000000..de7a81daf9 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/theme-selector.js.es6 @@ -0,0 +1,62 @@ +import { ajax } from 'discourse/lib/ajax'; +import { refreshCSS } from 'discourse/initializers/live-development'; +const keySelector = 'meta[name=discourse_theme_key]'; + +function currentThemeKey() { + let themeKey = null; + let elem = _.first($(keySelector)); + if (elem) { + themeKey = elem.content; + } + return themeKey; +} + +export function selectDefaultTheme(key) { + if (key) { + $.cookie('preview_style', key); + } else { + $.cookie('preview_style', null); + } +} + +export function previewTheme(key) { + if (currentThemeKey() !== key) { + + Discourse.set("assetVersion", "forceRefresh"); + + ajax(`/themes/assets/${key ? key : 'default'}`).then(results => { + let elem = _.first($(keySelector)); + if (elem) { + elem.content = key; + } + + results.themes.forEach(theme => { + let node = $(`link[rel=stylesheet][data-target=${theme.target}]`)[0]; + if (node) { + refreshCSS(node, null, theme.url, {force: true}); + } + }); + }); + } +} + +export function listThemes(site) { + let themes = site.get('user_themes'); + + if (!themes) { + return null; + } + + let hasDefault = !!themes.findBy('default', true); + + let results = []; + if (!hasDefault) { + results.push({name: I18n.t('themes.default_description'), id: null}); + } + + themes.forEach(t=>{ + results.push({name: t.name, id: t.theme_key}); + }); + + return results.length === 0 ? null : results; +} diff --git a/app/assets/javascripts/discourse/models/store.js.es6 b/app/assets/javascripts/discourse/models/store.js.es6 index 753f2dd5ab..0d1ef7cdac 100644 --- a/app/assets/javascripts/discourse/models/store.js.es6 +++ b/app/assets/javascripts/discourse/models/store.js.es6 @@ -56,9 +56,13 @@ export default Ember.Object.extend({ }, findAll(type, findArgs) { - const self = this; - return this.adapterFor(type).findAll(this, type, findArgs).then(function(result) { - return self._resultSet(type, result); + const adapter = this.adapterFor(type); + return adapter.findAll(this, type, findArgs).then((result) => { + let results = this._resultSet(type, result); + if (adapter.afterFindAll) { + results = adapter.afterFindAll(results); + } + return results; }); }, diff --git a/app/assets/javascripts/discourse/routes/preferences.js.es6 b/app/assets/javascripts/discourse/routes/preferences.js.es6 index b26648042c..6de4f04767 100644 --- a/app/assets/javascripts/discourse/routes/preferences.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences.js.es6 @@ -11,7 +11,8 @@ export default RestrictedUserRoute.extend({ controller.reset(); controller.setProperties({ model: user, - newNameInput: user.get('name') + newNameInput: user.get('name'), + selectedTheme: $.cookie('preview_style') }); }, diff --git a/app/assets/javascripts/discourse/templates/components/color-input.hbs b/app/assets/javascripts/discourse/templates/components/color-input.hbs index e69d19d7e1..1b9f36ecb3 100644 --- a/app/assets/javascripts/discourse/templates/components/color-input.hbs +++ b/app/assets/javascripts/discourse/templates/components/color-input.hbs @@ -1 +1 @@ -{{text-field class="hex-input" value=hexValue maxlength="6"}} +{{text-field class="hex-input" value=hexValue maxlength="6"}} diff --git a/app/assets/javascripts/discourse/templates/components/json-file-uploader.hbs b/app/assets/javascripts/discourse/templates/components/json-file-uploader.hbs deleted file mode 100644 index b34f6c4b9f..0000000000 --- a/app/assets/javascripts/discourse/templates/components/json-file-uploader.hbs +++ /dev/null @@ -1,12 +0,0 @@ -
    -
    - - {{d-button class="fileSelect" action="selectFile" class="" icon="upload" label="upload_selector.select_file"}} - {{conditional-loading-spinner condition=loading size="small"}} -
    -
    {{i18n "alternation"}}
    -
    - {{textarea value=value}} -
    -
    {{fa-icon "upload"}}
    -
    diff --git a/app/assets/javascripts/discourse/templates/preferences.hbs b/app/assets/javascripts/discourse/templates/preferences.hbs index 6fea4f7f6f..3a0ec94d69 100644 --- a/app/assets/javascripts/discourse/templates/preferences.hbs +++ b/app/assets/javascripts/discourse/templates/preferences.hbs @@ -354,6 +354,15 @@
    {{/if}} + {{#if userSelectableThemes}} +
    + +
    + {{combo-box content=userSelectableThemes value=selectedTheme}} +
    +
    + {{/if}} + {{plugin-outlet name="user-custom-controls" args=(hash model=model)}}
    diff --git a/app/assets/javascripts/wizard/components/theme-preview.js.es6 b/app/assets/javascripts/wizard/components/theme-preview.js.es6 index 19a9863b15..d4ec95b217 100644 --- a/app/assets/javascripts/wizard/components/theme-preview.js.es6 +++ b/app/assets/javascripts/wizard/components/theme-preview.js.es6 @@ -11,7 +11,7 @@ export default createPreviewComponent(659, 320, { logo: null, avatar: null, - @observes('step.fieldsById.theme_id.value') + @observes('step.fieldsById.base_scheme_id.value') themeChanged() { this.triggerRepaint(); }, diff --git a/app/assets/javascripts/wizard/models/wizard.js.es6 b/app/assets/javascripts/wizard/models/wizard.js.es6 index d98ba5e51e..8960354e58 100644 --- a/app/assets/javascripts/wizard/models/wizard.js.es6 +++ b/app/assets/javascripts/wizard/models/wizard.js.es6 @@ -25,7 +25,7 @@ const Wizard = Ember.Object.extend({ const colorStep = this.get('steps').findBy('id', 'colors'); if (!colorStep) { return; } - const themeChoice = colorStep.get('fieldsById.theme_id'); + const themeChoice = colorStep.get('fieldsById.base_scheme_id'); if (!themeChoice) { return; } const themeId = themeChoice.get('value'); diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 0fba7c199b..fc3044045f 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -3,6 +3,8 @@ @import "common/foundation/mixins"; @import "common/foundation/helpers"; +@import "common/admin/customize"; + $mobile-breakpoint: 700px; // Change the box model for .admin-content @@ -724,138 +726,6 @@ section.details { } } -// Customise area -.customize { - .admin-footer { - margin-top: 20px; - } - .current-style.maximized { - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: 100000; - background-color: white; - width: 100%; - padding: 0; - margin: 0; - .wrapper { - position: absolute; - top: 20px; - bottom: 10px; - left: 20px; - right: 20px; - } - } - .nav.nav-pills { - margin-left: 10px; - } - .content-list, .current-style { - float: left; - } - .content-list ul { - margin-bottom: 10px; - } - .current-style { - .nav.nav-pills{ - position: relative; - } - .toggle-mobile { - position: absolute; - right: 35px; - font-size: 20px; - } - .toggle-maximize { - position: absolute; - right: -5px; - } - .delete-link { - margin-left: 15px; - margin-top: 5px; - } - .preview-link { - margin-left: 15px; - } - .export { - float: right; - } - padding-left: 10px; - width: 70%; - .style-name { - width: 350px; - height: 25px; - // Remove height to for `box-sizing: border-box` - height: auto; - } - .ace-wrapper { - position: relative; - height: 400px; - width: 100%; - } - &.maximized { - .admin-container { - position: absolute; - bottom: 50px; - top: 80px; - width: 100%; - } - .admin-footer { - position: absolute; - bottom: 10px; - } - .ace-wrapper { - height: 100%; - } - } - .ace_editor { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - } - .status-actions { - float: right; - margin-top: 7px; - span { - margin-right: 10px; - } - } - .buttons { - float: left; - width: 200px; - .saving { - padding: 5px 0 0 0; - margin-left: 10px; - width: 80px; - color: $primary; - } - } - } - .color-scheme { - .controls { - span, button, a { - margin-right: 10px; - } - } - } - .colors { - thead th { border: none; } - td.hex { width: 100px; } - td.actions { width: 200px; } - .hex-input { width: 80px; margin-bottom: 0; } - .hex { text-align: center; } - .description { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } - - .invalid .hex input { - background-color: white; - color: black; - border-color: $danger; - } - } -} - .admin-flags { .hidden-post td.excerpt, .hidden-post td.user { @@ -1970,6 +1840,12 @@ table#user-badges { background: $secondary; } } + +.inline-edit label { + display: inline-block; + margin-right: 20px; +} + .cbox0 { background: blend-primary-secondary(0%); } .cbox10 { background: blend-primary-secondary(10%); } .cbox20 { background: blend-primary-secondary(20%); } diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss new file mode 100644 index 0000000000..6b726ec61f --- /dev/null +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -0,0 +1,157 @@ +// Customise area +.customize { + .admin-container { + padding-left: 10px; + padding-right: 10px; + } + .admin-footer { + margin-top: 20px; + } + .show-current-style { + margin-left: 20px; + float: left; + width: 70%; + h2 { + margin-bottom: 15px; + } + h3 { + margin-bottom: 10px; + margin-top: 30px; + } + } + + .current-style.maximized { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 100000; + background-color: white; + width: 100%; + padding: 0; + margin: 0; + .wrapper { + position: absolute; + top: 20px; + bottom: 10px; + left: 20px; + right: 20px; + } + } + + .nav.nav-pills.fields { + margin-left: 10px; + } + .content-list, .current-style { + float: left; + } + .content-list ul { + margin-bottom: 10px; + } + .current-style { + width: 100%; + + .admin-container { + margin: 0; + } + + .nav.target { + margin-top: 15px; + .fa { + margin-left: 3px; + } + .fa-mobile { + font-size: 1.3em; + } + } + + .toggle-maximize { + position: absolute; + right: -5px; + } + + .ace-wrapper { + position: relative; + height: 600px; + width: 100%; + } + + &.maximized { + .admin-container { + position: absolute; + bottom: 50px; + top: 80px; + width: 100%; + } + .admin-footer { + position: absolute; + bottom: 10px; + } + .ace-wrapper { + height: 100%; + } + } + + .custom-ace-gutter { + width: 41px; + background-color: #ebebeb; + height: 15px; + } + + .ace_editor { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + } + + .status-actions { + float: right; + margin-top: 7px; + span { + margin-right: 10px; + } + } + + .buttons { + float: left; + width: 200px; + .saving { + padding: 5px 0 0 0; + margin-left: 10px; + width: 80px; + color: $primary; + } + } + } + .color-scheme { + .controls { + span, button, a { + margin-right: 10px; + } + } + } + .colors { + thead th { border: none; } + td.hex { width: 160px; } + td.actions { width: 200px; } + .hex-input { width: 80px; margin-bottom: 0; } + .hex { text-align: center; } + .description { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } + + .invalid .hex input { + background-color: white; + color: black; + border-color: $danger; + } + } + + .status-message { + display: block; + font-size: 0.8em; + margin-top: 8px; + } +} + diff --git a/app/assets/stylesheets/common/components/badges.css.scss b/app/assets/stylesheets/common/components/badges.scss similarity index 100% rename from app/assets/stylesheets/common/components/badges.css.scss rename to app/assets/stylesheets/common/components/badges.scss diff --git a/app/assets/stylesheets/common/components/banner.css.scss b/app/assets/stylesheets/common/components/banner.scss similarity index 100% rename from app/assets/stylesheets/common/components/banner.css.scss rename to app/assets/stylesheets/common/components/banner.scss diff --git a/app/assets/stylesheets/common/components/buttons.css.scss b/app/assets/stylesheets/common/components/buttons.scss similarity index 100% rename from app/assets/stylesheets/common/components/buttons.css.scss rename to app/assets/stylesheets/common/components/buttons.scss diff --git a/app/assets/stylesheets/common/components/date-picker.css.scss b/app/assets/stylesheets/common/components/date-picker.scss similarity index 100% rename from app/assets/stylesheets/common/components/date-picker.css.scss rename to app/assets/stylesheets/common/components/date-picker.scss diff --git a/app/assets/stylesheets/common/components/keyboard_shortcuts.css.scss b/app/assets/stylesheets/common/components/keyboard_shortcuts.scss similarity index 100% rename from app/assets/stylesheets/common/components/keyboard_shortcuts.css.scss rename to app/assets/stylesheets/common/components/keyboard_shortcuts.scss diff --git a/app/assets/stylesheets/common/components/navs.css.scss b/app/assets/stylesheets/common/components/navs.scss similarity index 100% rename from app/assets/stylesheets/common/components/navs.css.scss rename to app/assets/stylesheets/common/components/navs.scss diff --git a/app/assets/stylesheets/common/foundation/base.scss b/app/assets/stylesheets/common/foundation/base.scss index 2a3c5588d0..35b0418187 100644 --- a/app/assets/stylesheets/common/foundation/base.scss +++ b/app/assets/stylesheets/common/foundation/base.scss @@ -1,5 +1,5 @@ -@import "common/foundation/variables"; -@import "common/foundation/mixins"; +@import "./variables"; +@import "./mixins"; // -------------------------------------------------- // Base styles for HTML elements diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss index b06099e3b5..43eb44cdb2 100644 --- a/app/assets/stylesheets/common/foundation/variables.scss +++ b/app/assets/stylesheets/common/foundation/variables.scss @@ -28,7 +28,7 @@ $base-font-size: 14px !default; $base-line-height: 19px !default; $base-font-family: Helvetica, Arial, sans-serif !default; -/* These files don't actually exist. They're injected by DiscourseSassImporter. */ +/* These files don't actually exist. They're injected by Stylesheet::Compiler. */ @import "theme_variables"; @import "plugins_variables"; @import "common/foundation/math"; diff --git a/app/assets/stylesheets/desktop.scss b/app/assets/stylesheets/desktop.scss index 807b43c726..71f19278ed 100644 --- a/app/assets/stylesheets/desktop.scss +++ b/app/assets/stylesheets/desktop.scss @@ -21,7 +21,7 @@ @import "desktop/menu-panel"; @import "desktop/group"; -/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */ +/* These files doesn't actually exist, they are injected by Stylesheet::Compiler. */ @import "plugins"; @import "plugins_desktop"; diff --git a/app/assets/stylesheets/embed.css.scss b/app/assets/stylesheets/embed.scss similarity index 97% rename from app/assets/stylesheets/embed.css.scss rename to app/assets/stylesheets/embed.scss index afb4a40c40..66af74d3d0 100644 --- a/app/assets/stylesheets/embed.css.scss +++ b/app/assets/stylesheets/embed.scss @@ -1,6 +1,5 @@ -//= require ./vendor/normalize -//= require ./common/foundation/base - +@import "./vendor/normalize"; +@import "./common/foundation/base"; @import "./common/foundation/variables"; @import "./common/foundation/colors"; @import "./common/foundation/mixins"; diff --git a/app/assets/stylesheets/mobile.scss b/app/assets/stylesheets/mobile.scss index ce220071ee..b1592b6d6a 100644 --- a/app/assets/stylesheets/mobile.scss +++ b/app/assets/stylesheets/mobile.scss @@ -24,7 +24,7 @@ @import "mobile/ring"; @import "mobile/group"; -/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */ +/* These files doesn't actually exist, they are injected by Stylesheet::Compiler. */ @import "plugins"; @import "plugins_mobile"; diff --git a/app/assets/stylesheets/vendor/sweetalert.css b/app/assets/stylesheets/vendor/sweetalert.scss old mode 100755 new mode 100644 similarity index 100% rename from app/assets/stylesheets/vendor/sweetalert.css rename to app/assets/stylesheets/vendor/sweetalert.scss diff --git a/app/controllers/admin/color_schemes_controller.rb b/app/controllers/admin/color_schemes_controller.rb index 35f45c6e4a..dda6c9f07c 100644 --- a/app/controllers/admin/color_schemes_controller.rb +++ b/app/controllers/admin/color_schemes_controller.rb @@ -3,7 +3,7 @@ class Admin::ColorSchemesController < Admin::AdminController before_filter :fetch_color_scheme, only: [:update, :destroy] def index - render_serialized([ColorScheme.base] + ColorScheme.current_version.order('id ASC').all.to_a, ColorSchemeSerializer) + render_serialized(ColorScheme.base_color_schemes + ColorScheme.order('id ASC').all.to_a, ColorSchemeSerializer) end def create @@ -37,6 +37,6 @@ class Admin::ColorSchemesController < Admin::AdminController end def color_scheme_params - params.permit(color_scheme: [:enabled, :name, colors: [:name, :hex]])[:color_scheme] + params.permit(color_scheme: [:base_scheme_id, :name, colors: [:name, :hex]])[:color_scheme] end end diff --git a/app/controllers/admin/site_customizations_controller.rb b/app/controllers/admin/site_customizations_controller.rb deleted file mode 100644 index afd3c162e5..0000000000 --- a/app/controllers/admin/site_customizations_controller.rb +++ /dev/null @@ -1,92 +0,0 @@ -class Admin::SiteCustomizationsController < Admin::AdminController - - before_filter :enable_customization - - skip_before_filter :check_xhr, only: [:show] - - def index - @site_customizations = SiteCustomization.order(:name) - - respond_to do |format| - format.json { render json: @site_customizations } - end - end - - def create - @site_customization = SiteCustomization.new(site_customization_params) - @site_customization.user_id = current_user.id - - respond_to do |format| - if @site_customization.save - log_site_customization_change(nil, site_customization_params) - format.json { render json: @site_customization, status: :created} - else - format.json { render json: @site_customization.errors, status: :unprocessable_entity } - end - end - end - - def update - @site_customization = SiteCustomization.find(params[:id]) - log_record = log_site_customization_change(@site_customization, site_customization_params) - - respond_to do |format| - if @site_customization.update_attributes(site_customization_params) - format.json { render json: @site_customization, status: :created} - else - log_record.destroy if log_record - format.json { render json: @site_customization.errors, status: :unprocessable_entity } - end - end - end - - def destroy - @site_customization = SiteCustomization.find(params[:id]) - StaffActionLogger.new(current_user).log_site_customization_destroy(@site_customization) - @site_customization.destroy - - respond_to do |format| - format.json { head :no_content } - end - end - - def show - @site_customization = SiteCustomization.find(params[:id]) - - respond_to do |format| - format.json do - check_xhr - render json: SiteCustomizationSerializer.new(@site_customization) - end - - format.any(:html, :text) do - raise RenderEmpty.new if request.xhr? - - response.headers['Content-Disposition'] = "attachment; filename=#{@site_customization.name.parameterize}.dcstyle.json" - response.sending_file = true - render json: SiteCustomizationSerializer.new(@site_customization) - end - end - - end - - private - - def site_customization_params - params.require(:site_customization) - .permit(:name, :stylesheet, :header, :top, :footer, - :mobile_stylesheet, :mobile_header, :mobile_top, :mobile_footer, - :head_tag, :body_tag, - :position, :enabled, :key, - :stylesheet_baked, :embedded_css) - end - - def log_site_customization_change(old_record, new_params) - StaffActionLogger.new(current_user).log_site_customization_change(old_record, new_params) - end - - def enable_customization - session[:disable_customization] = false - end - -end diff --git a/app/controllers/admin/staff_action_logs_controller.rb b/app/controllers/admin/staff_action_logs_controller.rb index 5324aabc0d..b004601014 100644 --- a/app/controllers/admin/staff_action_logs_controller.rb +++ b/app/controllers/admin/staff_action_logs_controller.rb @@ -6,4 +6,73 @@ class Admin::StaffActionLogsController < Admin::AdminController render_serialized(staff_action_logs, UserHistorySerializer) end + def diff + require_dependency "discourse_diff" + + @history = UserHistory.find(params[:id]) + prev = @history.previous_value + cur = @history.new_value + + prev = JSON.parse(prev) if prev + cur = JSON.parse(cur) if cur + + diff_fields = {} + + output = "

    #{CGI.escapeHTML(cur["name"].to_s)}

    " + + diff_fields["name"] = { + prev: prev["name"].to_s, + cur: cur["name"].to_s, + } + + ["default", "user_selectable"].each do |f| + diff_fields[f] = { + prev: (!!prev[f]).to_s, + cur: (!!cur[f]).to_s + } + end + + diff_fields["color scheme"] = { + prev: prev["color_scheme"]&.fetch("name").to_s, + cur: cur["color_scheme"]&.fetch("name").to_s, + } + + diff_fields["included themes"] = { + prev: child_themes(prev), + cur: child_themes(cur) + } + + + load_diff(diff_fields, :cur, cur) + load_diff(diff_fields, :prev, prev) + + diff_fields.delete_if{|k,v| v[:cur] == v[:prev]} + + + diff_fields.each do |k,v| + output << "

    #{k}

    " + diff = DiscourseDiff.new(v[:prev] || "", v[:cur] || "") + output << diff.side_by_side_markdown + end + + render json: {side_by_side: output} + end + + protected + + def child_themes(theme) + return "" unless children = theme["child_themes"] + + children.map{|row| row["name"]}.join(" ").to_s + end + + def load_diff(hash, key, val) + if f=val["theme_fields"] + f.each do |row| + entry = hash[row["target"] + " " + row["name"]] ||= {} + entry[key] = row["value"] + end + end + end + end diff --git a/app/controllers/admin/themes_controller.rb b/app/controllers/admin/themes_controller.rb new file mode 100644 index 0000000000..083cc61359 --- /dev/null +++ b/app/controllers/admin/themes_controller.rb @@ -0,0 +1,192 @@ +class Admin::ThemesController < Admin::AdminController + + skip_before_filter :check_xhr, only: [:show] + + def import + + @theme = nil + if params[:theme] + json = JSON::parse(params[:theme].read) + theme = json['theme'] + + @theme = Theme.new(name: theme["name"], user_id: current_user.id) + theme["theme_fields"]&.each do |field| + @theme.set_field(field["target"], field["name"], field["value"]) + end + + if @theme.save + log_theme_change(nil, @theme) + render json: @theme, status: :created + else + render json: @theme.errors, status: :unprocessable_entity + end + elsif params[:remote] + @theme = RemoteTheme.import_theme(params[:remote]) + render json: @theme, status: :created + else + render json: @theme.errors, status: :unprocessable_entity + end + + end + + def index + @theme = Theme.order(:name).includes(:theme_fields, :remote_theme) + @color_schemes = ColorScheme.all.to_a + light = ColorScheme.new(name: I18n.t("color_schemes.default")) + @color_schemes.unshift(light) + + payload = { + themes: ActiveModel::ArraySerializer.new(@theme, each_serializer: ThemeSerializer), + extras: { + color_schemes: ActiveModel::ArraySerializer.new(@color_schemes, each_serializer: ColorSchemeSerializer) + } + } + + respond_to do |format| + format.json { render json: payload} + end + end + + def create + @theme = Theme.new(name: theme_params[:name], + user_id: current_user.id, + user_selectable: theme_params[:user_selectable] || false, + color_scheme_id: theme_params[:color_scheme_id]) + set_fields + + respond_to do |format| + if @theme.save + update_default_theme + log_theme_change(nil, @theme) + format.json { render json: @theme, status: :created} + else + format.json { render json: @theme.errors, status: :unprocessable_entity } + end + end + end + + def update + @theme = Theme.find(params[:id]) + + original_json = ThemeSerializer.new(@theme, root: false).to_json + + [:name, :color_scheme_id, :user_selectable].each do |field| + if theme_params.key?(field) + @theme.send("#{field}=", theme_params[field]) + end + end + + if theme_params.key?(:child_theme_ids) + expected = theme_params[:child_theme_ids].map(&:to_i) + + @theme.child_theme_relation.to_a.each do |child| + if expected.include?(child.child_theme_id) + expected.reject!{|id| id == child.child_theme_id} + else + child.destroy + end + end + + Theme.where(id: expected).each do |theme| + @theme.add_child_theme!(theme) + end + + end + + set_fields + + if params[:theme][:remote_check] + @theme.remote_theme.update_remote_version + @theme.remote_theme.save! + end + + if params[:theme][:remote_update] + @theme.remote_theme.update_from_remote + @theme.remote_theme.save! + end + + respond_to do |format| + if @theme.save + + update_default_theme + + log_theme_change(original_json, @theme) + format.json { render json: @theme, status: :created} + else + format.json { render json: @theme.errors, status: :unprocessable_entity } + end + end + end + + def destroy + @theme = Theme.find(params[:id]) + StaffActionLogger.new(current_user).log_theme_destroy(@theme) + @theme.destroy + + respond_to do |format| + format.json { head :no_content } + end + end + + def show + @theme = Theme.find(params[:id]) + + respond_to do |format| + format.json do + check_xhr + render json: ThemeSerializer.new(@theme) + end + + format.any(:html, :text) do + raise RenderEmpty.new if request.xhr? + + response.headers['Content-Disposition'] = "attachment; filename=#{@theme.name.parameterize}.dcstyle.json" + response.sending_file = true + render json: ThemeSerializer.new(@theme) + end + end + + end + + private + + def update_default_theme + if theme_params.key?(:default) + is_default = theme_params[:default] + if @theme.key == SiteSetting.default_theme_key && !is_default + Theme.clear_default! + elsif is_default + @theme.set_default! + end + end + end + + def theme_params + @theme_params ||= + begin + # deep munge is a train wreck, work around it for now + params[:theme][:child_theme_ids] ||= [] if params[:theme].key?(:child_theme_ids) + params.require(:theme) + .permit(:name, + :color_scheme_id, + :default, + :user_selectable, + theme_fields: [:name, :target, :value], + child_theme_ids: []) + end + end + + def set_fields + + return unless fields = theme_params[:theme_fields] + + fields.each do |field| + @theme.set_field(field[:target], field[:name], field[:value]) + end + end + + def log_theme_change(old_record, new_record) + StaffActionLogger.new(current_user).log_theme_change(old_record, new_record) + end + +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d7a37a3a45..f0856406b4 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -411,8 +411,8 @@ class ApplicationController < ActionController::Base def custom_html_json target = view_context.mobile_view? ? :mobile : :desktop data = { - top: SiteCustomization.custom_top(session[:preview_style], target), - footer: SiteCustomization.custom_footer(session[:preview_style], target) + top: Theme.lookup_field(session[:preview_style], target, "after_header"), + footer: Theme.lookup_field(session[:preview_style], target, "footer") } if DiscoursePluginRegistry.custom_html diff --git a/app/controllers/site_customizations_controller.rb b/app/controllers/site_customizations_controller.rb deleted file mode 100644 index 34a314720f..0000000000 --- a/app/controllers/site_customizations_controller.rb +++ /dev/null @@ -1,35 +0,0 @@ -class SiteCustomizationsController < ApplicationController - skip_before_filter :preload_json, :check_xhr, :redirect_to_login_if_required - - def show - no_cookies - - cache_time = request.env["HTTP_IF_MODIFIED_SINCE"] - cache_time = Time.rfc2822(cache_time) rescue nil if cache_time - stylesheet_time = - begin - if params[:key].to_s == SiteCustomization::ENABLED_KEY - SiteCustomization.where(enabled: true) - .order('created_at desc') - .limit(1) - .pluck(:created_at) - .first - else - SiteCustomization.where(key: params[:key].to_s).pluck(:created_at).first - end - end - - if !stylesheet_time - raise Discourse::NotFound - end - - if cache_time && stylesheet_time <= cache_time - return render nothing: true, status: 304 - end - - response.headers["Last-Modified"] = stylesheet_time.httpdate - expires_in 1.year, public: true - render text: SiteCustomization.stylesheet_contents(params[:key], params[:target]), - content_type: "text/css" - end -end diff --git a/app/controllers/stylesheets_controller.rb b/app/controllers/stylesheets_controller.rb index 55c5166d4f..dea3d5d34c 100644 --- a/app/controllers/stylesheets_controller.rb +++ b/app/controllers/stylesheets_controller.rb @@ -1,12 +1,40 @@ class StylesheetsController < ApplicationController - skip_before_filter :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show] + skip_before_filter :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show, :show_source_map] + + def show_source_map + show_resource(source_map: true) + end def show + show_resource + end + + protected + + def show_resource(source_map: false) + + extension = source_map ? ".css.map" : ".css" + + params[:name] no_cookies target,digest = params[:name].split(/_([a-f0-9]{40})/) + if Rails.env == "development" + # TODO add theme + # calling this method ensures we have a cache for said target + # we hold of re-compilation till someone asks for asset + if target.include?("theme") + split_target,theme_id = target.split(/_(-?[0-9]+)/) + theme = Theme.find(theme_id) if theme_id + else + split_target,color_scheme_id = target.split(/_(-?[0-9]+)/) + theme = Theme.find_by(color_scheme_id: color_scheme_id) + end + Stylesheet::Manager.stylesheet_link_tag(split_target, nil, theme&.key) + end + cache_time = request.env["HTTP_IF_MODIFIED_SINCE"] cache_time = Time.rfc2822(cache_time) rescue nil if cache_time @@ -19,7 +47,7 @@ class StylesheetsController < ApplicationController # Security note, safe due to route constraint underscore_digest = digest ? "_" + digest : "" - location = "#{Rails.root}/#{DiscourseStylesheets::CACHE_PATH}/#{target}#{underscore_digest}.css" + location = "#{Rails.root}/#{Stylesheet::Manager::CACHE_PATH}/#{target}#{underscore_digest}#{extension}" stylesheet_time = query.pluck(:created_at).first @@ -33,24 +61,31 @@ class StylesheetsController < ApplicationController unless File.exist?(location) - if current = query.first - File.write(location, current.content) + if current = query.limit(1).pluck(source_map ? :source_map : :content).first + File.write(location, current) else raise Discourse::NotFound end end - response.headers['Last-Modified'] = stylesheet_time.httpdate if stylesheet_time - immutable_for(1.year) unless Rails.env == "development" + if Rails.env == "development" + response.headers['Last-Modified'] = Time.zone.now.httpdate + immutable_for(1.second) + else + response.headers['Last-Modified'] = stylesheet_time.httpdate if stylesheet_time + immutable_for(1.year) + end send_file(location, disposition: :inline) end - protected - def handle_missing_cache(location, name, digest) + location = location.sub(".css.map", ".css") + source_map_location = location + ".map" + existing = File.read(location) rescue nil if existing && digest - StylesheetCache.add(name, digest, existing) + source_map = File.read(source_map_location) rescue nil + StylesheetCache.add(name, digest, existing, source_map) end end diff --git a/app/controllers/themes_controller.rb b/app/controllers/themes_controller.rb new file mode 100644 index 0000000000..a5f26c5cc4 --- /dev/null +++ b/app/controllers/themes_controller.rb @@ -0,0 +1,28 @@ +class ThemesController < ::ApplicationController + def assets + theme_key = params[:key].to_s + + if theme_key == "default" + theme_key = nil + else + raise Discourse::NotFound unless Theme.where(key: theme_key).exists? + end + + object = [:mobile, :desktop, :desktop_theme, :mobile_theme].map do |target| + link = Stylesheet::Manager.stylesheet_link_tag(target, 'all', params[:key]) + if link + href = link.split(/["']/)[1] + if Rails.env.development? + href << (href.include?("?") ? "&" : "?") + href << SecureRandom.hex + end + { + target: target, + url: href + } + end + end.compact + + render json: object.as_json + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a9e4ed40e8..74ad085e70 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -316,4 +316,27 @@ module ApplicationHelper '' end end + + def theme_key + if customization_disabled? + nil + else + session[:preview_style] || SiteSetting.default_theme_key + end + end + + def theme_lookup(name) + lookup = Theme.lookup_field(theme_key, mobile_view? ? :mobile : :desktop, name) + lookup.html_safe if lookup + end + + def discourse_stylesheet_link_tag(name, opts={}) + if opts.key?(:theme_key) + key = opts[:theme_key] unless customization_disabled? + else + key = theme_key + end + + Stylesheet::Manager.stylesheet_link_tag(name, 'all', key) + end end diff --git a/app/models/category.rb b/app/models/category.rb index 2a48c6a53a..a533a3863f 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -1,5 +1,4 @@ require_dependency 'distributed_cache' -require_dependency 'sass/discourse_stylesheets' class Category < ActiveRecord::Base @@ -492,7 +491,7 @@ SQL end def publish_discourse_stylesheet - DiscourseStylesheets.cache.clear + Stylesheet::Manager.cache.clear end def index_search diff --git a/app/models/child_theme.rb b/app/models/child_theme.rb new file mode 100644 index 0000000000..6e101bd8aa --- /dev/null +++ b/app/models/child_theme.rb @@ -0,0 +1,20 @@ +class ChildTheme < ActiveRecord::Base + belongs_to :parent_theme, class_name: 'Theme' + belongs_to :child_theme, class_name: 'Theme' +end + +# == Schema Information +# +# Table name: child_themes +# +# id :integer not null, primary key +# parent_theme_id :integer +# child_theme_id :integer +# created_at :datetime +# updated_at :datetime +# +# Indexes +# +# index_child_themes_on_child_theme_id_and_parent_theme_id (child_theme_id,parent_theme_id) UNIQUE +# index_child_themes_on_parent_theme_id_and_child_theme_id (parent_theme_id,child_theme_id) UNIQUE +# diff --git a/app/models/color_scheme.rb b/app/models/color_scheme.rb index d9c9216c27..f2b056faea 100644 --- a/app/models/color_scheme.rb +++ b/app/models/color_scheme.rb @@ -1,32 +1,36 @@ -require_dependency 'sass/discourse_stylesheets' require_dependency 'distributed_cache' class ColorScheme < ActiveRecord::Base - def self.themes + CUSTOM_SCHEMES = { + dark: { + "primary" => 'dddddd', + "secondary" => '222222', + "tertiary" => '0f82af', + "quaternary" => 'c14924', + "header_background" => '111111', + "header_primary" => '333333', + "highlight" => 'a87137', + "danger" => 'e45735', + "success" => '1ca551', + "love" => 'fa6c8d' + } + } + + def self.base_color_scheme_colors base_with_hash = {} base_colors.each do |name, color| - base_with_hash[name] = "##{color}" + base_with_hash[name] = "#{color}" end - [ - { id: 'default', colors: base_with_hash }, - { - id: 'dark', - colors: { - "primary" => '#dddddd', - "secondary" => '#222222', - "tertiary" => '#0f82af', - "quaternary" => '#c14924', - "header_background" => '#111111', - "header_primary" => '#333333', - "highlight" => '#a87137', - "danger" => '#e45735', - "success" => '#1ca551', - "love" => '#fa6c8d' - } - } + list = [ + { id: 'default', colors: base_with_hash } ] + + CUSTOM_SCHEMES.each do |k,v| + list.push({id: k.to_s, colors: v}) + end + list end def self.hex_cache @@ -39,9 +43,12 @@ class ColorScheme < ActiveRecord::Base alias_method :colors, :color_scheme_colors - scope :current_version, ->{ where(versioned_id: nil) } + before_save do + if self.id + self.version += 1 + end + end - after_destroy :destroy_versions after_save :publish_discourse_stylesheet after_save :dump_hex_cache after_destroy :dump_hex_cache @@ -64,13 +71,18 @@ class ColorScheme < ActiveRecord::Base @base_colors end - def self.enabled - current_version.find_by(enabled: true) + def self.base_color_schemes + base_color_scheme_colors.map do |hash| + scheme = new(name: I18n.t("color_schemes.#{hash[:id]}"), base_scheme_id: hash[:id]) + scheme.colors = hash[:colors].map{|k,v| {name: k.to_s, hex: v.sub("#","")}} + scheme.is_base = true + scheme + end end def self.base return @base_color_scheme if @base_color_scheme - @base_color_scheme = new(name: I18n.t('color_schemes.base_theme_name'), enabled: false) + @base_color_scheme = new(name: I18n.t('color_schemes.base_theme_name')) @base_color_scheme.colors = base_colors.map { |name, hex| {name: name, hex: hex} } @base_color_scheme.is_base = true @base_color_scheme @@ -101,7 +113,7 @@ class ColorScheme < ActiveRecord::Base end # Can't use `where` here because base doesn't allow it - (enabled || base).colors.find {|c| c.name == name }.try(:hex) || :nil + (base).colors.find {|c| c.name == name }.try(:hex) || :nil end def self.hex_for_name(name) @@ -129,17 +141,39 @@ class ColorScheme < ActiveRecord::Base end end - def previous_version - ColorScheme.where(versioned_id: self.id).where('version < ?', self.version).order('version DESC').first + def base_colors + colors = nil + if base_scheme_id && base_scheme_id != "default" + colors = CUSTOM_SCHEMES[base_scheme_id.to_sym] + end + colors || ColorScheme.base_colors end - def destroy_versions - ColorScheme.where(versioned_id: self.id).destroy_all + def resolved_colors + resolved = ColorScheme.base_colors.dup + if base_scheme_id && base_scheme_id != "default" + if scheme = CUSTOM_SCHEMES[base_scheme_id.to_sym] + scheme.each do |name, value| + resolved[name] = value + end + end + end + colors.each do |c| + resolved[c.name] = c.hex + end + resolved end def publish_discourse_stylesheet - MessageBus.publish("/discourse_stylesheet", self.name) - DiscourseStylesheets.cache.clear + if self.id + themes = Theme.where(color_scheme_id: self.id).to_a + if themes.present? + Stylesheet::Manager.cache.clear + themes.each do |theme| + theme.notify_scheme_change(_clear_manager_cache = false) + end + end + end end def dump_hex_cache @@ -152,13 +186,11 @@ end # # Table name: color_schemes # -# id :integer not null, primary key -# name :string not null -# enabled :boolean default(FALSE), not null -# versioned_id :integer -# version :integer default(1), not null -# created_at :datetime not null -# updated_at :datetime not null -# via_wizard :boolean default(FALSE), not null -# theme_id :string +# id :integer not null, primary key +# name :string not null +# version :integer default(1), not null +# created_at :datetime not null +# updated_at :datetime not null +# via_wizard :boolean default(FALSE), not null +# base_scheme_id :string # diff --git a/app/models/remote_theme.rb b/app/models/remote_theme.rb new file mode 100644 index 0000000000..34df065799 --- /dev/null +++ b/app/models/remote_theme.rb @@ -0,0 +1,85 @@ +require_dependency 'git_importer' + +class RemoteTheme < ActiveRecord::Base + has_one :theme + + def self.import_theme(url, user=Discourse.system_user) + importer = GitImporter.new(url) + importer.import! + + theme_info = JSON.parse(importer["about.json"]) + theme = Theme.new(user_id: user&.id || -1, name: theme_info["name"]) + + remote_theme = new + theme.remote_theme = remote_theme + + remote_theme.remote_url = importer.url + remote_theme.update_from_remote(importer) + + theme.save! + theme + ensure + begin + importer.cleanup! + rescue => e + Rails.logger.warn("Failed cleanup remote git #{e}") + end + end + + def update_remote_version + importer = GitImporter.new(remote_url) + importer.import! + self.updated_at = Time.zone.now + self.remote_version, self.commits_behind = importer.commits_since(remote_version) + end + + def update_from_remote(importer=nil) + return unless remote_url + cleanup = false + unless importer + cleanup = true + importer = GitImporter.new(remote_url) + importer.import! + end + + Theme.targets.keys.each do |target| + Theme::ALLOWED_FIELDS.each do |field| + value = importer["#{target}/#{field=="scss"?"#{target}.scss":"#{field}.html"}"] + theme.set_field(target.to_sym, field, value) + 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 + self.remote_version = importer.version + self.local_version = importer.version + self.commits_behind = 0 + + self + ensure + begin + importer.cleanup! if cleanup + rescue => e + Rails.logger.warn("Failed cleanup remote git #{e}") + end + end +end + +# == Schema Information +# +# Table name: remote_themes +# +# id :integer not null, primary key +# remote_url :string not null +# remote_version :string +# local_version :string +# about_url :string +# license_url :string +# commits_behind :integer +# remote_updated_at :datetime +# created_at :datetime +# updated_at :datetime +# diff --git a/app/models/site_customization.rb b/app/models/site_customization.rb deleted file mode 100644 index 4b2e0ab988..0000000000 --- a/app/models/site_customization.rb +++ /dev/null @@ -1,299 +0,0 @@ -require_dependency 'sass/discourse_sass_compiler' -require_dependency 'sass/discourse_stylesheets' -require_dependency 'distributed_cache' - -class SiteCustomization < ActiveRecord::Base - ENABLED_KEY = '7e202ef2-56d7-47d5-98d8-a9c8d15e57dd' - - COMPILER_VERSION = 4 - - @cache = DistributedCache.new('site_customization') - - def self.css_fields - %w(stylesheet mobile_stylesheet embedded_css) - end - - def self.html_fields - %w(body_tag head_tag header mobile_header footer mobile_footer) - end - - before_create do - self.enabled ||= false - self.key ||= SecureRandom.uuid - true - end - - def compile_stylesheet(scss) - DiscourseSassCompiler.compile("@import \"theme_variables\";\n" << scss, 'custom') - rescue => e - puts e.backtrace.join("\n") unless Sass::SyntaxError === e - raise e - end - - def transpile(es6_source, version) - template = Tilt::ES6ModuleTranspilerTemplate.new {} - wrapped = < { - #{es6_source} -}); -PLUGIN_API_JS - - template.babel_transpile(wrapped) - end - - def process_html(html) - doc = Nokogiri::HTML.fragment(html) - doc.css('script[type="text/x-handlebars"]').each do |node| - name = node["name"] || node["data-template-name"] || "broken" - is_raw = name =~ /\.raw$/ - if is_raw - template = "require('discourse-common/lib/raw-handlebars').template(#{Barber::Precompiler.compile(node.inner_html)})" - node.replace < - (function() { - Discourse.RAW_TEMPLATES[#{name.sub(/\.raw$/, '').inspect}] = #{template}; - })(); - -COMPILED - else - template = "Ember.HTMLBars.template(#{Barber::Ember::Precompiler.compile(node.inner_html)})" - node.replace < - (function() { - Ember.TEMPLATES[#{name.inspect}] = #{template}; - })(); - -COMPILED - end - - end - - doc.css('script[type="text/discourse-plugin"]').each do |node| - if node['version'].present? - begin - code = transpile(node.inner_html, node['version']) - node.replace("") - rescue MiniRacer::RuntimeError => ex - node.replace("") - end - end - end - - doc.to_s - end - - before_save do - SiteCustomization.html_fields.each do |html_attr| - if self.send("#{html_attr}_changed?") - self.send("#{html_attr}_baked=", process_html(self.send(html_attr))) - end - end - - SiteCustomization.css_fields.each do |stylesheet_attr| - if self.send("#{stylesheet_attr}_changed?") - begin - self.send("#{stylesheet_attr}_baked=", compile_stylesheet(self.send(stylesheet_attr))) - rescue Sass::SyntaxError => e - self.send("#{stylesheet_attr}_baked=", DiscourseSassCompiler.error_as_css(e, "custom stylesheet")) - end - end - end - end - - def any_stylesheet_changed? - SiteCustomization.css_fields.each do |fieldname| - return true if self.send("#{fieldname}_changed?") - end - false - end - - after_save do - remove_from_cache! - if any_stylesheet_changed? - MessageBus.publish "/file-change/#{key}", SecureRandom.hex - MessageBus.publish "/file-change/#{SiteCustomization::ENABLED_KEY}", SecureRandom.hex - end - MessageBus.publish "/header-change/#{key}", header if header_changed? - MessageBus.publish "/footer-change/#{key}", footer if footer_changed? - DiscourseStylesheets.cache.clear - end - - after_destroy do - remove_from_cache! - end - - def self.enabled_key - ENABLED_KEY.dup << RailsMultisite::ConnectionManagement.current_db - end - - def self.field_for_target(target=nil) - target ||= :desktop - - case target.to_sym - when :mobile then :mobile_stylesheet - when :desktop then :stylesheet - when :embedded then :embedded_css - end - end - - def self.baked_for_target(target=nil) - "#{field_for_target(target)}_baked".to_sym - end - - def self.enabled_stylesheet_contents(target=:desktop) - @cache["enabled_stylesheet_#{target}:#{COMPILER_VERSION}"] ||= where(enabled: true) - .order(:name) - .pluck(baked_for_target(target)) - .compact - .join("\n") - end - - def self.stylesheet_contents(key, target) - if key == ENABLED_KEY - enabled_stylesheet_contents(target) - else - where(key: key) - .pluck(baked_for_target(target)) - .first - end - end - - def self.custom_stylesheet(preview_style=nil, target=:desktop) - preview_style ||= ENABLED_KEY - if preview_style == ENABLED_KEY - stylesheet_link_tag(ENABLED_KEY, target, enabled_stylesheet_contents(target)) - else - lookup_field(preview_style, target, :stylesheet_link_tag) - end - end - - %i{header top footer head_tag body_tag}.each do |name| - define_singleton_method("custom_#{name}") do |preview_style=nil, target=:desktop| - preview_style ||= ENABLED_KEY - lookup_field(preview_style, target, name) - end - end - - def self.lookup_field(key, target, field) - return if key.blank? - - cache_key = "#{key}:#{target}:#{field}:#{COMPILER_VERSION}" - - lookup = @cache[cache_key] - return lookup.html_safe if lookup - - styles = if key == ENABLED_KEY - order(:name).where(enabled:true).to_a - else - [find_by(key: key)].compact - end - - val = if styles.present? - styles.map do |style| - lookup = target == :mobile ? "mobile_#{field}" : field - if html_fields.include?(lookup.to_s) - style.ensure_baked!(lookup) - style.send("#{lookup}_baked") - else - style.send(lookup) - end - end.compact.join("\n") - end - - (@cache[cache_key] = val || "").html_safe - end - - def self.remove_from_cache!(key, broadcast = true) - MessageBus.publish('/site_customization', key: key) if broadcast - clear_cache! - end - - def self.clear_cache! - @cache.clear - end - - def ensure_baked!(field) - - # If the version number changes, clear out all the baked fields - if compiler_version != COMPILER_VERSION - updates = { compiler_version: COMPILER_VERSION } - SiteCustomization.html_fields.each do |f| - updates["#{f}_baked".to_sym] = nil - end - - update_columns(updates) - end - - baked = send("#{field}_baked") - if baked.blank? - if val = self.send(field) - val = process_html(val) rescue "" - self.update_columns("#{field}_baked" => val) - end - end - end - - def remove_from_cache! - self.class.remove_from_cache!(self.class.enabled_key) - self.class.remove_from_cache!(key) - end - - def mobile_stylesheet_link_tag - stylesheet_link_tag(:mobile) - end - - def stylesheet_link_tag(target=:desktop) - content = self.send(SiteCustomization.field_for_target(target)) - SiteCustomization.stylesheet_link_tag(key, target, content) - end - - def self.stylesheet_link_tag(key, target, content) - return "" unless content.present? - - hash = Digest::MD5.hexdigest(content) - link_css_tag "/site_customizations/#{key}.css?target=#{target}&v=#{hash}" - end - - def self.link_css_tag(href) - href = (GlobalSetting.cdn_url || "") + "#{GlobalSetting.relative_url_root}#{href}&__ws=#{Discourse.current_hostname}" - %Q{}.html_safe - end -end - -# == Schema Information -# -# Table name: site_customizations -# -# id :integer not null, primary key -# name :string not null -# stylesheet :text -# header :text -# user_id :integer not null -# enabled :boolean not null -# key :string not null -# created_at :datetime not null -# updated_at :datetime not null -# stylesheet_baked :text default(""), not null -# mobile_stylesheet :text -# mobile_header :text -# mobile_stylesheet_baked :text -# footer :text -# mobile_footer :text -# head_tag :text -# body_tag :text -# top :text -# mobile_top :text -# embedded_css :text -# embedded_css_baked :text -# head_tag_baked :text -# body_tag_baked :text -# header_baked :text -# mobile_header_baked :text -# footer_baked :text -# mobile_footer_baked :text -# compiler_version :integer default(0), not null -# -# Indexes -# -# index_site_customizations_on_key (key) -# diff --git a/app/models/stylesheet_cache.rb b/app/models/stylesheet_cache.rb index c9dfa86762..2c568072cb 100644 --- a/app/models/stylesheet_cache.rb +++ b/app/models/stylesheet_cache.rb @@ -3,11 +3,11 @@ class StylesheetCache < ActiveRecord::Base MAX_TO_KEEP = 50 - def self.add(target,digest,content) + def self.add(target,digest,content,source_map) return false if where(target: target, digest: digest).exists? - success = create(target: target, digest: digest, content: content) + success = create(target: target, digest: digest, content: content, source_map: source_map) count = StylesheetCache.count if count > MAX_TO_KEEP @@ -39,6 +39,8 @@ end # content :text not null # created_at :datetime # updated_at :datetime +# theme_id :integer default(-1), not null +# source_map :text # # Indexes # diff --git a/app/models/theme.rb b/app/models/theme.rb new file mode 100644 index 0000000000..32308c662f --- /dev/null +++ b/app/models/theme.rb @@ -0,0 +1,256 @@ +require_dependency 'distributed_cache' +require_dependency 'stylesheet/compiler' +require_dependency 'stylesheet/manager' + +class Theme < ActiveRecord::Base + + ALLOWED_FIELDS = %w{scss head_tag header after_header body_tag footer} + + @cache = DistributedCache.new('theme') + + belongs_to :color_scheme + has_many :theme_fields, dependent: :destroy + has_many :child_theme_relation, class_name: 'ChildTheme', foreign_key: 'parent_theme_id', dependent: :destroy + has_many :child_themes, through: :child_theme_relation, source: :child_theme + belongs_to :remote_theme + + before_create do + self.key ||= SecureRandom.uuid + true + end + + after_save do + changed_fields.each(&:save!) + changed_fields.clear + + Theme.expire_site_cache! if user_selectable_changed? + + @dependant_themes = nil + @included_themes = nil + end + + after_save do + remove_from_cache! + notify_scheme_change if color_scheme_id_changed? + end + + after_destroy do + remove_from_cache! + if SiteSetting.default_theme_key == self.key + Theme.clear_default! + end + end + + after_commit ->(theme) do + theme.notify_theme_change + end, on: :update + + def self.expire_site_cache! + Site.clear_anon_cache! + ApplicationSerializer.expire_cache_fragment!("user_themes") + end + + def self.clear_default! + SiteSetting.default_theme_key = "" + expire_site_cache! + end + + def set_default! + SiteSetting.default_theme_key = key + Theme.expire_site_cache! + end + + def self.lookup_field(key, target, field) + return if key.blank? + + cache_key = "#{key}:#{target}:#{field}:#{ThemeField::COMPILER_VERSION}" + lookup = @cache[cache_key] + return lookup.html_safe if lookup + + target = target.to_sym + theme = find_by(key: key) + + val = theme.resolve_baked_field(target, field) if theme + + (@cache[cache_key] = val || "").html_safe + end + + def self.remove_from_cache!(themes=nil) + clear_cache! + end + + def self.clear_cache! + @cache.clear + end + + + def self.targets + @targets ||= Enum.new(common: 0, desktop: 1, mobile: 2) + end + + + def notify_scheme_change(clear_manager_cache=true) + Stylesheet::Manager.cache.clear if clear_manager_cache + message = refresh_message_for_targets(["desktop", "mobile", "admin"], self.color_scheme_id, self, Rails.env.development?) + MessageBus.publish('/file-change', message) + end + + def notify_theme_change + Stylesheet::Manager.clear_theme_cache! + + themes = [self] + dependant_themes + + message = themes.map do |theme| + refresh_message_for_targets([:mobile_theme,:desktop_theme], theme.id, theme) + end.compact.flatten + MessageBus.publish('/file-change', message) + end + + def refresh_message_for_targets(targets, id, theme, add_cache_breaker=false) + targets.map do |target| + link = Stylesheet::Manager.stylesheet_link_tag(target.to_sym, 'all', theme.key) + if link + href = link.split(/["']/)[1] + if add_cache_breaker + href << (href.include?("?") ? "&" : "?") + href << SecureRandom.hex + end + { + name: "/stylesheets/#{target}#{id ? "_#{id}": ""}", + new_href: href + } + end + end + end + + def dependant_themes + @dependant_themes ||= resolve_dependant_themes(:up) + end + + def included_themes + @included_themes ||= resolve_dependant_themes(:down) + end + + def resolve_dependant_themes(direction) + + select_field,where_field=nil + + if direction == :up + select_field = "parent_theme_id" + where_field = "child_theme_id" + elsif direction == :down + select_field = "child_theme_id" + where_field = "parent_theme_id" + else + raise "Unknown direction" + end + + themes = [] + return [] unless id + + uniq = Set.new + uniq << id + + iterations = 0 + added = [id] + + while added.length > 0 && iterations < 5 + + iterations += 1 + + new_themes = Theme.where("id in (SELECT #{select_field} + FROM child_themes + 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 + + def resolve_baked_field(target, name) + list_baked_fields(target,name).map{|f| f.value_baked || f.value}.join("\n") + end + + def list_baked_fields(target, name) + + target = target.to_sym + + theme_ids = [self.id] + (included_themes.map(&:id) || []) + fields = ThemeField.where(target: [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') + fields.each(&:ensure_baked!) + fields + end + + def remove_from_cache! + self.class.remove_from_cache! + end + + def changed_fields + @changed_fields ||= [] + end + + def set_field(target, name, value) + name = name.to_s + + target_id = Theme.targets[target.to_sym] + raise "Unknown target #{target} passed to set field" unless target_id + + field = theme_fields.find{|f| f.name==name && f.target == target_id} + if field + if value.blank? + field.destroy + else + if field.value != value + field.value = value + changed_fields << field + end + end + else + theme_fields.build(target: target_id, value: value, name: name) if value.present? + end + end + + def add_child_theme!(theme) + child_theme_relation.create!(child_theme_id: theme.id) + @included_themes = nil + child_themes.reload + save! + end +end + +# == Schema Information +# +# Table name: themes +# +# id :integer not null, primary key +# name :string not null +# user_id :integer not null +# key :string not null +# created_at :datetime not null +# updated_at :datetime not null +# compiler_version :integer default(0), not null +# user_selectable :boolean default(FALSE), not null +# hidden :boolean default(FALSE), not null +# color_scheme_id :integer +# remote_theme_id :integer +# +# Indexes +# +# index_themes_on_key (key) +# index_themes_on_remote_theme_id (remote_theme_id) UNIQUE +# diff --git a/app/models/theme_field.rb b/app/models/theme_field.rb new file mode 100644 index 0000000000..ead11507af --- /dev/null +++ b/app/models/theme_field.rb @@ -0,0 +1,117 @@ +class ThemeField < ActiveRecord::Base + + COMPILER_VERSION = 5 + + belongs_to :theme + + def transpile(es6_source, version) + template = Tilt::ES6ModuleTranspilerTemplate.new {} + wrapped = < { + #{es6_source} +}); +PLUGIN_API_JS + + template.babel_transpile(wrapped) + end + + def process_html(html) + doc = Nokogiri::HTML.fragment(html) + doc.css('script[type="text/x-handlebars"]').each do |node| + name = node["name"] || node["data-template-name"] || "broken" + is_raw = name =~ /\.raw$/ + if is_raw + template = "require('discourse-common/lib/raw-handlebars').template(#{Barber::Precompiler.compile(node.inner_html)})" + node.replace < + (function() { + Discourse.RAW_TEMPLATES[#{name.sub(/\.raw$/, '').inspect}] = #{template}; + })(); + +COMPILED + else + template = "Ember.HTMLBars.template(#{Barber::Ember::Precompiler.compile(node.inner_html)})" + node.replace < + (function() { + Ember.TEMPLATES[#{name.inspect}] = #{template}; + })(); + +COMPILED + end + + end + + doc.css('script[type="text/discourse-plugin"]').each do |node| + if node['version'].present? + begin + code = transpile(node.inner_html, node['version']) + node.replace("") + rescue MiniRacer::RuntimeError => ex + node.replace("") + end + end + end + + doc.to_s + end + + + def self.html_fields + %w(body_tag head_tag header footer after_header) + end + + + def ensure_baked! + if ThemeField.html_fields.include?(self.name) + if !self.value_baked || compiler_version != COMPILER_VERSION + + self.value_baked = process_html(self.value) + self.compiler_version = COMPILER_VERSION + + if self.value_baked_changed? || compiler_version.changed? + self.update_columns(value_baked: value_baked, compiler_version: compiler_version) + end + end + end + end + + def target_name + Theme.targets.invert[target].to_s + end + + before_save do + if value_changed? && !value_baked_changed? + self.value_baked = nil + end + end + + after_commit do + ensure_baked! + + Stylesheet::Manager.clear_theme_cache! if self.name.include?("scss") + + # TODO message for mobile vs desktop + MessageBus.publish "/header-change/#{theme.key}", self.value if self.name == "header" + MessageBus.publish "/footer-change/#{theme.key}", self.value if self.name == "footer" + end +end + +# == Schema Information +# +# Table name: theme_fields +# +# id :integer not null, primary key +# theme_id :integer not null +# target :integer not null +# name :string not null +# value :text not null +# value_baked :text +# created_at :datetime +# updated_at :datetime +# compiler_version :integer default(0), not null +# +# Indexes +# +# index_theme_fields_on_theme_id_and_target_and_name (theme_id,target,name) UNIQUE +# diff --git a/app/models/user_history.rb b/app/models/user_history.rb index a6677de389..e177cd8de4 100644 --- a/app/models/user_history.rb +++ b/app/models/user_history.rb @@ -19,8 +19,8 @@ class UserHistory < ActiveRecord::Base @actions ||= Enum.new(delete_user: 1, change_trust_level: 2, change_site_setting: 3, - change_site_customization: 4, - delete_site_customization: 5, + change_theme: 4, + delete_theme: 5, checked_for_custom_avatar: 6, # not used anymore notified_about_avatar: 7, notified_about_sequential_replies: 8, @@ -71,8 +71,8 @@ class UserHistory < ActiveRecord::Base @staff_actions ||= [:delete_user, :change_trust_level, :change_site_setting, - :change_site_customization, - :delete_site_customization, + :change_theme, + :delete_theme, :change_site_text, :suspend_user, :unsuspend_user, @@ -158,7 +158,7 @@ class UserHistory < ActiveRecord::Base end def new_value_is_json? - [UserHistory.actions[:change_site_customization], UserHistory.actions[:delete_site_customization]].include?(action) + [UserHistory.actions[:change_theme], UserHistory.actions[:delete_theme]].include?(action) end def previous_value_is_json? diff --git a/app/serializers/color_scheme_color_serializer.rb b/app/serializers/color_scheme_color_serializer.rb index b1d3d809b6..3e99c06cf2 100644 --- a/app/serializers/color_scheme_color_serializer.rb +++ b/app/serializers/color_scheme_color_serializer.rb @@ -6,6 +6,11 @@ class ColorSchemeColorSerializer < ApplicationSerializer end def default_hex - ColorScheme.base_colors[object.name] + if object.color_scheme + object.color_scheme.base_colors[object.name] + else + # it is a base color so it is already default + object.hex + end end end diff --git a/app/serializers/color_scheme_serializer.rb b/app/serializers/color_scheme_serializer.rb index 965d592377..2d5ab79d39 100644 --- a/app/serializers/color_scheme_serializer.rb +++ b/app/serializers/color_scheme_serializer.rb @@ -1,8 +1,4 @@ class ColorSchemeSerializer < ApplicationSerializer - attributes :id, :name, :enabled, :is_base + attributes :id, :name, :is_base, :base_scheme_id has_many :colors, serializer: ColorSchemeColorSerializer, embed: :objects - - def base - object.is_base || false - end end diff --git a/app/serializers/site_customization_serializer.rb b/app/serializers/site_customization_serializer.rb deleted file mode 100644 index 6a3e70ff21..0000000000 --- a/app/serializers/site_customization_serializer.rb +++ /dev/null @@ -1,7 +0,0 @@ -class SiteCustomizationSerializer < ApplicationSerializer - - attributes :id, :name, :key, :enabled, :created_at, :updated_at, - :stylesheet, :header, :footer, :top, - :mobile_stylesheet, :mobile_header, :mobile_footer, :mobile_top, - :head_tag, :body_tag, :embedded_css -end diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb index d43c165a96..5ea12e8cbe 100644 --- a/app/serializers/site_serializer.rb +++ b/app/serializers/site_serializer.rb @@ -24,13 +24,25 @@ class SiteSerializer < ApplicationSerializer :tags_filter_regexp, :top_tags, :wizard_required, - :topic_featured_link_allowed_category_ids + :topic_featured_link_allowed_category_ids, + :user_themes has_many :categories, serializer: BasicCategorySerializer, embed: :objects has_many :trust_levels, embed: :objects has_many :archetypes, embed: :objects, serializer: ArchetypeSerializer has_many :user_fields, embed: :objects, serialzer: UserFieldSerializer + def user_themes + cache_fragment("user_themes") do + Theme.where('key = :default OR user_selectable', + default: SiteSetting.default_theme_key) + .order(:name) + .pluck(:key, :name) + .map{|k,n| {theme_key: k, name: n, default: k == SiteSetting.default_theme_key}} + .as_json + end + end + def groups cache_fragment("group_names") do Group.order(:name).pluck(:id,:name).map { |id,name| { id: id, name: name } }.as_json diff --git a/app/serializers/theme_serializer.rb b/app/serializers/theme_serializer.rb new file mode 100644 index 0000000000..051cf5d2f6 --- /dev/null +++ b/app/serializers/theme_serializer.rb @@ -0,0 +1,42 @@ +class ThemeFieldSerializer < ApplicationSerializer + attributes :name, :target, :value + + def target + case object.target + when 0 then "common" + when 1 then "desktop" + when 2 then "mobile" + end + end +end + +class ChildThemeSerializer < ApplicationSerializer + attributes :id, :name, :key, :created_at, :updated_at, :default + + def include_default? + object.key == SiteSetting.default_theme_key + end + + def default + true + end +end + +class RemoteThemeSerializer < ApplicationSerializer + attributes :id, :remote_url, :remote_version, :local_version, :about_url, + :license_url, :commits_behind, :remote_updated_at, :updated_at + + # wow, AMS has some pretty nutty logic where it tries to find the path here + # from action dispatch, tell it not to + def about_url + object.about_url + end +end + +class ThemeSerializer < ChildThemeSerializer + attributes :color_scheme, :color_scheme_id, :user_selectable, :remote_theme_id + + has_many :theme_fields, serializer: ThemeFieldSerializer, embed: :objects + has_many :child_themes, serializer: ChildThemeSerializer, embed: :objects + has_one :remote_theme, serializer: RemoteThemeSerializer, embed: :objects +end diff --git a/app/serializers/user_history_serializer.rb b/app/serializers/user_history_serializer.rb index 039e25c61d..f73d3e424b 100644 --- a/app/serializers/user_history_serializer.rb +++ b/app/serializers/user_history_serializer.rb @@ -12,7 +12,8 @@ class UserHistorySerializer < ApplicationSerializer :post_id, :category_id, :action, - :custom_type + :custom_type, + :id has_one :acting_user, serializer: BasicUserSerializer, embed: :objects has_one :target_user, serializer: BasicUserSerializer, embed: :objects diff --git a/app/services/color_scheme_revisor.rb b/app/services/color_scheme_revisor.rb index 5db7bb79d1..ce66c37332 100644 --- a/app/services/color_scheme_revisor.rb +++ b/app/services/color_scheme_revisor.rb @@ -9,63 +9,26 @@ class ColorSchemeRevisor self.new(color_scheme, params).revise end - def self.revert(color_scheme) - self.new(color_scheme).revert - end - def revise ColorScheme.transaction do - if @params[:enabled] - ColorScheme.where('id != ?', @color_scheme.id).update_all enabled: false - end @color_scheme.name = @params[:name] if @params.has_key?(:name) - @color_scheme.enabled = @params[:enabled] if @params.has_key?(:enabled) - @color_scheme.theme_id = @params[:theme_id] if @params.has_key?(:theme_id) - new_version = false + @color_scheme.base_scheme_id = @params[:base_scheme_id] if @params.has_key?(:base_scheme_id) + has_colors = @params[:colors] - if @params[:colors] - new_version = @params[:colors].any? do |c| - (existing = @color_scheme.colors_by_name[c[:name]]).nil? or existing.hex != c[:hex] - end - end - - if new_version - ColorScheme.create( - name: @color_scheme.name, - enabled: false, - colors: @color_scheme.colors_hashes, - versioned_id: @color_scheme.id, - version: @color_scheme.version) - @color_scheme.version += 1 - end - - if @params[:colors] + if has_colors @params[:colors].each do |c| if existing = @color_scheme.colors_by_name[c[:name]] existing.update_attributes(c) + else + @color_scheme.color_scheme_colors << ColorSchemeColor.new(name: c[:name], hex: c[:hex]) end end - end - - @color_scheme.save - @color_scheme.clear_colors_cache - end - @color_scheme - end - - def revert - ColorScheme.transaction do - if prev = @color_scheme.previous_version - @color_scheme.version = prev.version - @color_scheme.colors.clear - prev.colors.update_all(color_scheme_id: @color_scheme.id) - prev.destroy - @color_scheme.save! @color_scheme.clear_colors_cache end - end + @color_scheme.save if has_colors || @color_scheme.name_changed? || @color_scheme.base_scheme_id_changed? + end @color_scheme end diff --git a/app/services/staff_action_logger.rb b/app/services/staff_action_logger.rb index 71734ab2e2..1cd09176af 100644 --- a/app/services/staff_action_logger.rb +++ b/app/services/staff_action_logger.rb @@ -113,34 +113,49 @@ class StaffActionLogger })) end - SITE_CUSTOMIZATION_LOGGED_ATTRS = [ - 'stylesheet', 'mobile_stylesheet', - 'header', 'mobile_header', - 'top', 'mobile_top', - 'footer', 'mobile_footer', - 'head_tag', - 'body_tag', - 'position', - 'enabled', - 'key' - ] + def theme_json(theme) + ThemeSerializer.new(theme, root:false).to_json + end + + def strip_duplicates(old,cur) + return [old,cur] unless old && cur + + old = JSON.parse(old) + cur = JSON.parse(cur) + + old.each do |k, v| + next if k == "name" + next if k == "id" + if (v == cur[k]) + cur.delete(k) + old.delete(k) + end + end + + [old.to_json, cur.to_json] + end + + def log_theme_change(old_json, new_theme, opts={}) + raise Discourse::InvalidParameters.new(:new_theme) unless new_theme + + new_json = theme_json(new_theme) + + old_json,new_json = strip_duplicates(old_json,new_json) - def log_site_customization_change(old_record, site_customization_params, opts={}) - raise Discourse::InvalidParameters.new(:site_customization_params) unless site_customization_params UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:change_site_customization], - subject: site_customization_params[:name], - previous_value: old_record ? old_record.attributes.slice(*SITE_CUSTOMIZATION_LOGGED_ATTRS).to_json : nil, - new_value: site_customization_params.slice(*(SITE_CUSTOMIZATION_LOGGED_ATTRS.map(&:to_sym))).to_json + action: UserHistory.actions[:change_theme], + subject: new_theme.name, + previous_value: old_json, + new_value: new_json })) end - def log_site_customization_destroy(site_customization, opts={}) - raise Discourse::InvalidParameters.new(:site_customization) unless site_customization + def log_theme_destroy(theme, opts={}) + raise Discourse::InvalidParameters.new(:theme) unless theme UserHistory.create( params(opts).merge({ - action: UserHistory.actions[:delete_site_customization], - subject: site_customization.name, - previous_value: site_customization.attributes.slice(*SITE_CUSTOMIZATION_LOGGED_ATTRS).to_json + action: UserHistory.actions[:delete_theme], + subject: theme.name, + previous_value: theme_json(theme) })) end diff --git a/app/views/common/_discourse_stylesheet.html.erb b/app/views/common/_discourse_stylesheet.html.erb index 702bbd9f02..044ce2fc76 100644 --- a/app/views/common/_discourse_stylesheet.html.erb +++ b/app/views/common/_discourse_stylesheet.html.erb @@ -1,13 +1,13 @@ <%- if rtl? %> - <%= DiscourseStylesheets.stylesheet_link_tag(mobile_view? ? :mobile_rtl : :desktop_rtl) %> + <%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_rtl : :desktop_rtl) %> <%- else %> - <%= DiscourseStylesheets.stylesheet_link_tag(mobile_view? ? :mobile : :desktop) %> + <%= discourse_stylesheet_link_tag(mobile_view? ? :mobile : :desktop) %> <%- end %> <%- if staff? %> - <%= DiscourseStylesheets.stylesheet_link_tag(:admin) %> + <%= discourse_stylesheet_link_tag(:admin) %> <%- end %> -<%- unless customization_disabled? %> - <%= SiteCustomization.custom_stylesheet(session[:preview_style], mobile_view? ? :mobile : :desktop) %> +<%- if theme_key %> + <%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_theme : :desktop_theme) %> <%- end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 41b321f22d..59100bee02 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -4,6 +4,7 @@ <%= content_for?(:title) ? yield(:title) + ' - ' + SiteSetting.title : SiteSetting.title %> + <%= render partial: "layouts/head" %> <%= render partial: "common/special_font_face" %> <%= render partial: "common/discourse_stylesheet" %> @@ -41,7 +42,7 @@ <%- end %> <%- unless customization_disabled? %> - <%= raw SiteCustomization.custom_head_tag(session[:preview_style]) %> + <%= raw theme_lookup("head_tag") %> <%- end %> <%= render_google_universal_analytics_code %> @@ -82,7 +83,7 @@ <%- unless customization_disabled? || loading_admin? %> - <%= SiteCustomization.custom_header(session[:preview_style], mobile_view? ? :mobile : :desktop) %> + <%= theme_lookup("header") %> <%- end %>
    @@ -118,7 +119,7 @@ <%= render_google_analytics_code %> <%- unless customization_disabled? %> - <%= raw SiteCustomization.custom_body_tag(session[:preview_style]) %> + <%= raw theme_lookup("body_tag") %> <%- end %> diff --git a/app/views/layouts/crawler.html.erb b/app/views/layouts/crawler.html.erb index e4e7f9a47e..ac757cc8c2 100644 --- a/app/views/layouts/crawler.html.erb +++ b/app/views/layouts/crawler.html.erb @@ -6,20 +6,16 @@ <%= render partial: "layouts/head" %> <%- if rtl? %> - <%= DiscourseStylesheets.stylesheet_link_tag(mobile_view? ? :mobile_rtl : :desktop_rtl) %> + <%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_rtl : :desktop_rtl) %> <%- else %> - <%= DiscourseStylesheets.stylesheet_link_tag(mobile_view? ? :mobile : :desktop) %> - <%- end %> - <%- unless customization_disabled? %> - <%= raw SiteCustomization.custom_head_tag(session[:preview_style]) %> + <%= discourse_stylesheet_link_tag(mobile_view? ? :mobile : :desktop) %> <%- end %> + <%= theme_lookup("head_tag") %> <%= render_google_universal_analytics_code %> <%= yield :head %> - <%- unless customization_disabled? %> - <%= SiteCustomization.custom_header(session[:preview_style], mobile_view? ? :mobile : :desktop) %> - <%- end %> + <%= theme_lookup("header") %>
    ">
    @@ -37,9 +33,7 @@

    <%= t 'powered_by_html' %>

    <%= render_google_analytics_code %> - <%- unless customization_disabled? %> - <%= raw SiteCustomization.custom_body_tag(session[:preview_style]) %> - <%- end %> + <%= theme_lookup("body_tag") %> <%= yield :after_body %> diff --git a/app/views/layouts/embed.html.erb b/app/views/layouts/embed.html.erb index b4ee7cfa7c..61fa3ff86d 100644 --- a/app/views/layouts/embed.html.erb +++ b/app/views/layouts/embed.html.erb @@ -5,7 +5,7 @@ <%= stylesheet_link_tag 'embed' %> <%- unless customization_disabled? %> - <%= SiteCustomization.custom_stylesheet(session[:preview_style], :embedded) %> + <%= Theme.custom_stylesheet(session[:preview_style], :embedded) %> <%- end %> <%= javascript_include_tag 'break_string' %> diff --git a/app/views/layouts/no_ember.html.erb b/app/views/layouts/no_ember.html.erb index 2c6b3f74be..035a84027b 100644 --- a/app/views/layouts/no_ember.html.erb +++ b/app/views/layouts/no_ember.html.erb @@ -9,23 +9,17 @@ <%= render partial: "common/discourse_stylesheet" %> <%= discourse_csrf_tags %> - <%- unless customization_disabled? %> - <%= raw SiteCustomization.custom_head_tag(session[:preview_style]) %> - <%- end %> + <%= theme_lookup("head_tag") %> <%= yield(:no_ember_head) %> class="<%= @custom_body_class %>"<% end %>> - <%- unless customization_disabled? %> - <%= SiteCustomization.custom_header(session[:preview_style], mobile_view? ? :mobile : :desktop) %> - <%- end %> + <%= theme_lookup("header") %>
    <%= render partial: 'header' %>
    <%= yield %>
    - <%- unless customization_disabled? %> - <%= SiteCustomization.custom_footer(session[:preview_style], mobile_view? ? :mobile : :desktop) %> - <%- end %> + <%= theme_lookup("footer") %> diff --git a/app/views/wizard/index.html.erb b/app/views/wizard/index.html.erb index a0f193526f..f0a0c0d46f 100644 --- a/app/views/wizard/index.html.erb +++ b/app/views/wizard/index.html.erb @@ -1,6 +1,6 @@ - <%= stylesheet_link_tag 'wizard' %> + <%= discourse_stylesheet_link_tag 'wizard', theme_key: nil %> <%= script 'ember_jquery' %> <%= script 'wizard-vendor' %> <%= script 'wizard-application' %> diff --git a/config/application.rb b/config/application.rb index 258ca0c276..ec216260ea 100644 --- a/config/application.rb +++ b/config/application.rb @@ -78,11 +78,12 @@ module Discourse path =~ /assets\/images/ && !%w(.js .css).include?(File.extname(filename)) end] - config.assets.precompile += ['vendor.js', 'common.css', 'desktop.css', 'mobile.css', - 'admin.js', 'admin.css', 'shiny/shiny.css', 'preload-store.js.es6', - 'browser-update.js', 'embed.css', 'break_string.js', 'ember_jquery.js', - 'pretty-text-bundle.js', 'wizard.css', 'wizard-application.js', - 'wizard-vendor.js', 'plugin.js', 'plugin-third-party.js'] + config.assets.precompile += %w{ + vendor.js admin.js preload-store.js.es6 + browser-update.js break_string.js ember_jquery.js + pretty-text-bundle.js wizard-application.js + wizard-vendor.js plugin.js plugin-third-party.js + } # Precompile all available locales Dir.glob("#{config.root}/app/assets/javascripts/locales/*.js.erb").each do |file| @@ -169,6 +170,8 @@ module Discourse config.relative_url_root = GlobalSetting.relative_url_root end + require_dependency 'stylesheet/manager' + config.after_initialize do # require common dependencies that are often required by plugins # in the past observers would load them as side-effects diff --git a/config/environments/development.rb b/config/environments/development.rb index a422173a67..bc345547ab 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -29,7 +29,6 @@ Discourse::Application.configure do config.active_record.migration_error = :page_load config.watchable_dirs['lib'] = [:rb] - config.sass.debug_info = false config.handlebars.precompile = false # we recommend you use mailcatcher https://github.com/sj26/mailcatcher @@ -49,6 +48,13 @@ Discourse::Application.configure do config.enable_anon_caching = false require 'rbtrace' + + require 'stylesheet/watcher' + if defined? Puma + STDERR.puts "Staring CSS change watcher" + @watcher = Stylesheet::Watcher.watch + end + if emails = GlobalSetting.developer_emails config.developer_emails = emails.split(",").map(&:downcase).map(&:strip) end diff --git a/config/environments/production.rb b/config/environments/production.rb index f73b6e2dc9..b4331f15ac 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -14,8 +14,6 @@ Discourse::Application.configure do config.assets.js_compressor = :uglifier - config.assets.css_compressor = :sass - # stuff should be pre-compiled config.assets.compile = false diff --git a/config/initializers/100-sprockets.rb b/config/initializers/100-sprockets.rb deleted file mode 100644 index f6556e220d..0000000000 --- a/config/initializers/100-sprockets.rb +++ /dev/null @@ -1,19 +0,0 @@ -require_dependency 'sass/discourse_stylesheets' -require_dependency 'sass/discourse_sass_importer' -require_dependency 'sass/discourse_safe_sass_importer' - -DiscourseSassTemplate = Class.new(Sass::Rails::SassTemplate) do - def importer_class - DiscourseSassImporter - end -end -DiscourseScssTemplate = Class.new(DiscourseSassTemplate) do - def syntax - :scss - end -end - -Rails.application.config.assets.configure do |env| - env.register_engine '.sass', DiscourseSassTemplate - env.register_engine '.scss', DiscourseScssTemplate -end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 378a3d2855..95c82c2564 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -174,6 +174,9 @@ en: bootstrap_mode_enabled: "To make launching your new site easier, you are in bootstrap mode. All new users will be granted trust level 1 and have daily email digest updates enabled. This will be automatically turned off when total user count exceeds %{min_users} users." bootstrap_mode_disabled: "Bootstrap mode will be disabled in next 24 hours." + themes: + default_description: "Default" + s3: regions: us_east_1: "US East (N. Virginia)" @@ -634,6 +637,7 @@ en: revoke_access: "Revoke Access" undo_revoke_access: "Undo Revoke Access" api_approved: "Approved:" + theme: "Theme" staff_counters: flags_given: "helpful flags" @@ -2780,32 +2784,14 @@ en: customize: title: "Customize" long_title: "Site Customizations" - css: "CSS" - header: "Header" - top: "Top" - footer: "Footer" - embedded_css: "Embedded CSS" - head_tag: - text: "" - title: "HTML that will be inserted before the tag" - body_tag: - text: "" - title: "HTML that will be inserted before the tag" - override_default: "Do not include standard style sheet" - enabled: "Enabled?" preview: "preview" - undo_preview: "remove preview" - rescue_preview: "default style" explain_preview: "See the site with this custom stylesheet" - explain_undo_preview: "Go back to the currently enabled custom stylesheet" - explain_rescue_preview: "See the site with the default stylesheet" save: "Save" new: "New" new_style: "New Style" import: "Import" - import_title: "Select a file or paste text" delete: "Delete" - delete_confirm: "Delete this customization?" + delete_confirm: "Delete this theme?" about: "Modify CSS stylesheets and HTML headers on the site. Add a customization to start." color: "Color" opacity: "Opacity" @@ -2819,13 +2805,67 @@ en: revert: "Revert Changes" revert_confirm: "Are you sure you want to revert your changes?" - css_html: - title: "CSS/HTML" - long_title: "CSS and HTML Customizations" + theme: + import_theme: "Import Theme" + customize_desc: "Customize:" + title: "Themes" + long_title: "Amend colors, CSS and HTML contents of your site" + edit: "Edit" + edit_confirm: "This is a remote theme, if you edit CSS/HTML your changes will be erased next time you update the theme." + common: "Common" + desktop: "Desktop" + mobile: "Mobile" + is_default: "Theme is enabled by default" + user_selectable: "Theme can be selected by users" + color_scheme: "Color Scheme" + color_scheme_select: "Select colors to be used by theme" + custom_sections: "Custom sections:" + included_themes: "Included Themes" + 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" + import_web_tip: "Repository containing theme" + import_file_tip: ".dcstyle.json file containing theme" + about_theme: "About Theme" + license: "License" + update_to_latest: "Update to Latest" + check_for_updates: "Check for Updates" + updating: "Updating..." + up_to_date: "Theme is up-to-date, last checked:" + add: "Add" + commits_behind: + one: "Theme is 1 commit behind!" + other: "Theme is {{count}} commit behind!" + scss: + text: "CSS" + title: "Enter custom CSS, we accept all valid CSS and SCSS styles" + header: + text: "Header" + title: "Enter HTML to display above site header" + after_header: + text: "After Header" + title: "Enter HTML to display on all pages after header" + footer: + text: "Footer" + title: "Enter HTML to display on page footer" + embedded_scss: + text: "Embedded CSS" + title: "Enter custom CSS to deliver with embedded version of comments" + head_tag: + text: "" + title: "HTML that will be inserted before the tag" + body_tag: + text: "" + title: "HTML that will be inserted before the tag" colors: + select_base: + title: "Select base color scheme" + description: "Base scheme:" title: "Colors" + edit: "Edit Color Schemes" long_title: "Color Schemes" - about: "Modify the colors used on the site without writing CSS. Add a scheme to start." + about: "Modify the colors used by your themes. Create a new color scheme to start." new_name: "New Color Scheme" copy_name_prefix: "Copy of" delete_confirm: "Delete this color scheme?" @@ -2966,8 +3006,8 @@ en: change_trust_level: "change trust level" change_username: "change username" change_site_setting: "change site setting" - change_site_customization: "change site customization" - delete_site_customization: "delete site customization" + change_theme: "change theme" + delete_theme: "delete theme" change_site_text: "change site text" suspend_user: "suspend user" unsuspend_user: "unsuspend user" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 2fe259ab23..1ff8f33edb 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2763,6 +2763,8 @@ en: color_schemes: base_theme_name: "Base" + default: "Light Scheme" + dark: "Dark Scheme" about: "About" guidelines: "Guidelines" diff --git a/config/routes.rb b/config/routes.rb index e6c3d9ed71..c534d2d41e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -56,8 +56,6 @@ Discourse::Application.routes.draw do get "site/basic-info" => 'site#basic_info' get "site/statistics" => 'site#statistics' - get "site_customizations/:key" => "site_customizations#show" - get "srv/status" => "forums#status" get "wizard" => "wizard#index" @@ -162,6 +160,7 @@ Discourse::Application.routes.draw do scope "/logs" do resources :staff_action_logs, only: [:index] + get 'staff_action_logs/:id/diff' => 'staff_action_logs#diff' resources :screened_emails, only: [:index, :destroy] resources :screened_ip_addresses, only: [:index, :create, :update, :destroy] do collection do @@ -174,9 +173,9 @@ Discourse::Application.routes.draw do get "/logs" => "staff_action_logs#index" get "customize" => "color_schemes#index", constraints: AdminConstraint.new - get "customize/css_html" => "site_customizations#index", constraints: AdminConstraint.new - get "customize/css_html/:id/:section" => "site_customizations#index", constraints: AdminConstraint.new + get "customize/themes" => "themes#index", constraints: AdminConstraint.new get "customize/colors" => "color_schemes#index", constraints: AdminConstraint.new + get "customize/colors/:id" => "color_schemes#index", constraints: AdminConstraint.new get "customize/permalinks" => "permalinks#index", constraints: AdminConstraint.new get "customize/embedding" => "embedding#show", constraints: AdminConstraint.new put "customize/embedding" => "embedding#update", constraints: AdminConstraint.new @@ -186,12 +185,17 @@ Discourse::Application.routes.draw do post "flags/agree/:id" => "flags#agree" post "flags/disagree/:id" => "flags#disagree" post "flags/defer/:id" => "flags#defer" - resources :site_customizations, constraints: AdminConstraint.new + + resources :themes, constraints: AdminConstraint.new + post "themes/import" => "themes#import" scope "/customize", constraints: AdminConstraint.new do resources :user_fields, constraints: AdminConstraint.new resources :emojis, constraints: AdminConstraint.new + get 'themes/:id/:target/:field_name/edit' => 'themes#index' + get 'themes/:id' => 'themes#index' + # They have periods in their URLs often: get 'site_texts' => 'site_texts#index' get 'site_texts/(:id)' => 'site_texts#show', constraints: { id: /[\w.\-]+/i } @@ -385,7 +389,8 @@ Discourse::Application.routes.draw do get "highlight-js/:hostname/:version.js" => "highlight_js#show", format: false, constraints: { hostname: /[\w\.-]+/ } - get "stylesheets/:name.css" => "stylesheets#show", constraints: { name: /[a-z0-9_]+/ } + get "stylesheets/:name.css.map" => "stylesheets#show_source_map", constraints: { name: /[-a-z0-9_]+/ } + get "stylesheets/:name.css" => "stylesheets#show", constraints: { name: /[-a-z0-9_]+/ } post "uploads" => "uploads#create" @@ -708,6 +713,8 @@ Discourse::Application.routes.draw do get "/safe-mode" => "safe_mode#index" post "/safe-mode" => "safe_mode#enter", as: "safe_mode_enter" + get "/themes/assets/:key" => "themes#assets" + get "*url", to: 'permalinks#show', constraints: PermalinkConstraint.new end diff --git a/config/site_settings.yml b/config/site_settings.yml index 146b0ebed2..71221c04ba 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -184,6 +184,8 @@ basic: enable_mobile_theme: client: true default: true + default_theme_key: + hidden: true relative_date_duration: client: true default: 30 diff --git a/db/migrate/20170313192741_add_themes.rb b/db/migrate/20170313192741_add_themes.rb new file mode 100644 index 0000000000..ab2e3abc3b --- /dev/null +++ b/db/migrate/20170313192741_add_themes.rb @@ -0,0 +1,79 @@ +class AddThemes < ActiveRecord::Migration + def up + rename_table :site_customizations, :themes + + add_column :themes, :user_selectable, :bool, null: false, default: false + add_column :themes, :hidden, :bool, null: false, default: false + add_column :themes, :color_scheme_id, :integer + + create_table :child_themes do |t| + t.integer :parent_theme_id + t.integer :child_theme_id + t.timestamps + end + + add_index :child_themes, [:parent_theme_id, :child_theme_id], unique: true + add_index :child_themes, [:child_theme_id, :parent_theme_id], unique: true + + # versioning in color scheme table was very confusing, remove it + execute "DELETE FROM color_schemes WHERE versioned_id IS NOT NULL" + remove_column :color_schemes, :versioned_id + + enabled_theme_count = execute("SELECT count(*) FROM themes WHERE enabled") + .to_a[0]["count"].to_i + + + enabled_scheme_id = execute("SELECT id FROM color_schemes WHERE enabled") + .to_a[0]&.fetch("id") + + theme_key, theme_id = + execute("SELECT key, id FROM themes WHERE enabled").to_a[0]&.values + + if (enabled_theme_count == 0 && enabled_scheme_id) || enabled_theme_count > 1 + + puts "Creating a new default theme!" + + theme_key = '7e202ef2-6666-47d5-98d8-a9c8d15e57dd' + + sql = < 1 + execute < 0) + puts "Setting default theme" + sql = < 0 +SQL + remove_column :themes, value + end + + %w{ head_tag_baked + body_tag_baked + header_baked + footer_baked + mobile_footer_baked + mobile_header_baked + }.each do |col| + remove_column :themes, col + end + end +end diff --git a/db/migrate/20170328203122_add_compiler_version_to_theme_fields.rb b/db/migrate/20170328203122_add_compiler_version_to_theme_fields.rb new file mode 100644 index 0000000000..7f66ff7eae --- /dev/null +++ b/db/migrate/20170328203122_add_compiler_version_to_theme_fields.rb @@ -0,0 +1,5 @@ +class AddCompilerVersionToThemeFields < ActiveRecord::Migration + def change + add_column :theme_fields, :compiler_version, :integer, null: false, default: 0 + end +end diff --git a/db/migrate/20170407154510_rename_theme_id.rb b/db/migrate/20170407154510_rename_theme_id.rb new file mode 100644 index 0000000000..48d3524f64 --- /dev/null +++ b/db/migrate/20170407154510_rename_theme_id.rb @@ -0,0 +1,5 @@ +class RenameThemeId < ActiveRecord::Migration + def change + rename_column :color_schemes, :theme_id, :base_scheme_id + end +end diff --git a/db/migrate/20170410170923_add_theme_remote_fields.rb b/db/migrate/20170410170923_add_theme_remote_fields.rb new file mode 100644 index 0000000000..95cb5d7e5c --- /dev/null +++ b/db/migrate/20170410170923_add_theme_remote_fields.rb @@ -0,0 +1,17 @@ +class AddThemeRemoteFields < ActiveRecord::Migration + def change + create_table :remote_themes do |t| + t.string :remote_url, null: false + t.string :remote_version + t.string :local_version + t.string :about_url + t.string :license_url + t.integer :commits_behind + t.datetime :remote_updated_at + t.timestamps + end + + add_column :themes, :remote_theme_id, :integer + add_index :themes, :remote_theme_id, unique: true + end +end diff --git a/lib/autospec/manager.rb b/lib/autospec/manager.rb index 4505dd905f..4833f22be3 100644 --- a/lib/autospec/manager.rb +++ b/lib/autospec/manager.rb @@ -222,12 +222,14 @@ class Autospec::Manager end end end + # special watcher for styles/templates - Autospec::ReloadCss::WATCHERS.each do |k, _| - matches = [] - matches << file if k.match(file) - Autospec::ReloadCss.run_on_change(matches) if matches.present? - end + # now handled via libass integration + # Autospec::ReloadCss::WATCHERS.each do |k, _| + # matches = [] + # matches << file if k.match(file) + # Autospec::ReloadCss.run_on_change(matches) if matches.present? + # end end queue_specs(specs) if hit diff --git a/lib/freedom_patches/resolve.rb b/lib/freedom_patches/resolve.rb deleted file mode 100644 index f4fdb619c7..0000000000 --- a/lib/freedom_patches/resolve.rb +++ /dev/null @@ -1,19 +0,0 @@ -# sass-rails expects an actual file to exists when calling `@import`. However, -# we don't actually create the files for our special imports but rather inject -# them dynamically. -module Discourse - module Sprockets - module Resolve - def resolve(path, options = {}) - return [path, []] if DiscourseSassImporter.special_imports.has_key?(File.basename(path, '.scss')) - super - end - end - end -end - -module Sprockets - class Base - prepend Discourse::Sprockets::Resolve - end -end diff --git a/lib/git_importer.rb b/lib/git_importer.rb new file mode 100644 index 0000000000..9ad1b15c6f --- /dev/null +++ b/lib/git_importer.rb @@ -0,0 +1,49 @@ +class GitImporter + + attr_reader :url + + def initialize(url) + @url = url + if @url.start_with?("https://github.com") && !@url.end_with?(".git") + @url += ".git" + end + @temp_folder = "#{Dir.tmpdir}/discourse_theme_#{SecureRandom.hex}" + end + + def import! + Discourse::Utils.execute_command("git", "clone", @url, @temp_folder) + end + + def commits_since(hash) + commit_hash, commits_behind = nil + + Dir.chdir(@temp_folder) do + commit_hash = Discourse::Utils.execute_command("git", "rev-parse", "HEAD").strip + commits_behind = Discourse::Utils.execute_command("git", "rev-list", "#{hash}..HEAD", "--count").strip + end + + [commit_hash, commits_behind] + end + + def version + Dir.chdir(@temp_folder) do + Discourse::Utils.execute_command("git", "rev-parse", "HEAD").strip + end + end + + def cleanup! + FileUtils.rm_rf(@temp_folder) + end + + def [](value) + fullpath = "#{@temp_folder}/#{value}" + 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) + end + end + +end diff --git a/lib/middleware/turbo_dev.rb b/lib/middleware/turbo_dev.rb index d3b7eb3e1b..beda165224 100644 --- a/lib/middleware/turbo_dev.rb +++ b/lib/middleware/turbo_dev.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Middleware # Cheat and bypass Rails in development mode if the client attempts to download a static asset diff --git a/lib/sass/discourse_safe_sass_importer.rb b/lib/sass/discourse_safe_sass_importer.rb deleted file mode 100644 index 54ee6c5d24..0000000000 --- a/lib/sass/discourse_safe_sass_importer.rb +++ /dev/null @@ -1,32 +0,0 @@ -require_dependency 'sass/discourse_sass_importer' - -# This custom importer is used to import stylesheets but excludes plugins and theming. -# It's used as a fallback when compilation of stylesheets fails. - -class DiscourseSafeSassImporter < DiscourseSassImporter - def special_imports - super.merge({ - "plugins" => [], - "plugins_mobile" => [], - "plugins_desktop" => [], - "plugins_variables" => [] - }) - end - - def find(name, options) - if name == "theme_variables" - # Load the default variables - contents = "" - special_imports[name].each do |css_file| - contents << File.read(css_file) - end - ::Sass::Engine.new(contents, options.merge( - filename: "#{name}.scss", - importer: self, - syntax: :scss - )) - else - super(name, options) - end - end -end diff --git a/lib/sass/discourse_sass_compiler.rb b/lib/sass/discourse_sass_compiler.rb deleted file mode 100644 index a9c6c3e5b3..0000000000 --- a/lib/sass/discourse_sass_compiler.rb +++ /dev/null @@ -1,85 +0,0 @@ -require_dependency 'sass/discourse_sass_importer' -require 'pathname' - -module Sass::Script::Functions - def _error(message) - raise Sass::SyntaxError, mesage - end -end - -class DiscourseSassCompiler - - def self.compile(scss, target, opts={}) - self.new(scss, target).compile(opts) - end - - # Takes a Sass::SyntaxError and generates css that will show the - # error at the bottom of the page. - def self.error_as_css(sass_error, label) - error = sass_error.sass_backtrace_str(label) - error.gsub!("\n", '\A ') - error.gsub!("'", '\27 ') - - "footer { white-space: pre; } - footer:after { content: '#{error}' }" - end - - - def initialize(scss, target) - @scss = scss - @target = target - - unless Sass::Script::Functions < Sprockets::SassFunctions - Sass::Script::Functions.send :include, Sprockets::SassFunctions - end - end - - # Compiles the given scss and output the css as a string. - # - # Options: - # safe: (boolean) if true, theme and plugin stylesheets will not be included. Default is false. - def compile(opts={}) - app = Rails.application - env = app.assets || Sprockets::Railtie.build_environment(app) - - pathname = Pathname.new("app/assets/stylesheets/#{@target}.scss") - - context = env.context_class.new( - environment: env, - filename: "#{@target}.scss", - pathname: pathname, - metadata: {} - ) - - debug_opts = Rails.env.production? ? {} : { - line_numbers: true, - # debug_info: true, # great with Firebug + FireSass, but not helpful elsewhere - style: :expanded - } - - importer_class = opts[:safe] ? DiscourseSafeSassImporter : DiscourseSassImporter - - css = ::Sass::Engine.new(@scss, { - syntax: :scss, - cache: false, - read_cache: false, - style: :compressed, - filesystem_importer: importer_class, - load_paths: context.environment.paths.map { |path| importer_class.new(path.to_s) }, - sprockets: { - context: context, - environment: context.environment - } - }.merge(debug_opts)).render - - css_output = css - if opts[:rtl] - begin - require 'r2' - css_output = R2.r2(css) if defined?(R2) - rescue; end - end - css_output - end - -end diff --git a/lib/sass/discourse_sass_importer.rb b/lib/sass/discourse_sass_importer.rb deleted file mode 100644 index 62230db937..0000000000 --- a/lib/sass/discourse_sass_importer.rb +++ /dev/null @@ -1,100 +0,0 @@ -# This custom importer is used for site customizations. This is similar to the -# Sprockets::SassImporter implementation provided in sass-rails since that is used -# during asset precompilation. -class DiscourseSassImporter < Sass::Importers::Filesystem - module Sass - def extensions - { - 'css' => :scss, - 'css.scss' => :scss, - 'css.sass' => :sass, - 'css.erb' => :scss, - 'scss.erb' => :scss, - 'sass.erb' => :sass, - 'css.scss.erb' => :scss, - 'css.sass.erb' => :sass - }.merge!(super) - end - - def special_imports - { - "plugins" => DiscoursePluginRegistry.stylesheets, - "plugins_mobile" => DiscoursePluginRegistry.mobile_stylesheets, - "plugins_desktop" => DiscoursePluginRegistry.desktop_stylesheets, - "plugins_variables" => DiscoursePluginRegistry.sass_variables, - "theme_variables" => [ColorScheme::BASE_COLORS_FILE], - "category_backgrounds" => Proc.new { |c| "body.category-#{c.full_slug} { background-image: url(#{apply_cdn(c.uploaded_background.url)}) }\n" } - } - end - - def find_relative(name, base, options) - engine_from_path(name, File.dirname(base), options) - end - - def apply_cdn(url) - "#{GlobalSetting.cdn_url}#{url}" - end - - def find(name, options) - - if special_imports.has_key? name - case name - when "theme_variables" - contents = "" - ColorScheme.base_colors.each do |n, base_hex| - hex_val = ColorScheme.hex_for_name(n) || base_hex - contents << "$#{n}: ##{hex_val} !default;\n" - end - when "category_backgrounds" - contents = "" - Category.where('uploaded_background_id IS NOT NULL').each do |c| - contents << special_imports[name].call(c) if c.uploaded_background - end - else - stylesheets = special_imports[name] - contents = "" - stylesheets.each do |css_file| - if css_file =~ /\.scss$/ - contents << "@import '#{css_file}';" - else - contents << File.read(css_file) - end - depend_on(css_file) - end - end - - ::Sass::Engine.new(contents, options.merge( - filename: "#{name}.scss", - importer: self, - syntax: :scss - )) - else - engine_from_path(name, root, options) - end - end - - private - - def depend_on(filename) - if @context - @context.depend_on(filename) - @context.depend_on(globbed_file_parent(filename)) - end - end - - def engine_from_path(name, dir, options) - full_filename, _ = ::Sass::Util.destructure(find_real_file(dir, name, options)) - return unless full_filename && File.readable?(full_filename) - - depend_on(full_filename) - ::Sass::Engine.for_file(full_filename, options) - end - end - - include Sass - include ::Sass::Rails::SassImporter::Globbing - - def self.special_imports - self.new('').special_imports - end -end diff --git a/lib/sass/discourse_stylesheets.rb b/lib/sass/discourse_stylesheets.rb deleted file mode 100644 index cce67137e1..0000000000 --- a/lib/sass/discourse_stylesheets.rb +++ /dev/null @@ -1,178 +0,0 @@ -require_dependency 'sass/discourse_sass_compiler' -require_dependency 'distributed_cache' - -class DiscourseStylesheets - - CACHE_PATH ||= 'tmp/stylesheet-cache' - MANIFEST_DIR ||= "#{Rails.root}/tmp/cache/assets/#{Rails.env}" - MANIFEST_FULL_PATH ||= "#{MANIFEST_DIR}/stylesheet-manifest" - - @lock = Mutex.new - - def self.cache - return {} if Rails.env.development? - @cache ||= DistributedCache.new("discourse_stylesheet") - end - - def self.stylesheet_link_tag(target = :desktop, media = 'all') - - tag = cache[target] - - return tag.dup.html_safe if tag - - @lock.synchronize do - builder = self.new(target) - builder.compile unless File.exists?(builder.stylesheet_fullpath) - builder.ensure_digestless_file - tag = %[] - - cache[target] = tag - - tag.dup.html_safe - end - end - - def self.compile(target = :desktop, opts={}) - @lock.synchronize do - FileUtils.rm(MANIFEST_FULL_PATH, force: true) if opts[:force] - builder = self.new(target) - builder.compile(opts) - builder.stylesheet_filename - end - end - - def self.last_file_updated - if Rails.env.production? - @last_file_updated ||= if File.exists?(MANIFEST_FULL_PATH) - File.readlines(MANIFEST_FULL_PATH, 'r')[0] - else - mtime = max_file_mtime - FileUtils.mkdir_p(MANIFEST_DIR) - File.open(MANIFEST_FULL_PATH, "w") { |f| f.print(mtime) } - mtime - end - else - max_file_mtime - end - end - - def self.max_file_mtime - globs = ["#{Rails.root}/app/assets/stylesheets/**/*.*css"] - - Discourse.plugins.map { |plugin| File.dirname(plugin.path) }.each do |path| - globs += [ - "#{path}/plugin.rb", - "#{path}/**/*.*css", - ] - end - - globs.map do |pattern| - Dir.glob(pattern).map { |x| File.mtime(x) }.max - end.compact.max.to_i - end - - def initialize(target = :desktop) - @target = target - end - - def compile(opts={}) - unless opts[:force] - if File.exists?(stylesheet_fullpath) - unless StylesheetCache.where(target: @target, digest: digest).exists? - begin - StylesheetCache.add(@target, digest, File.read(stylesheet_fullpath)) - rescue => e - Rails.logger.warn "Completely unexpected error adding contents of '#{stylesheet_fullpath}' to cache #{e}" - end - end - return true - end - end - - scss = File.read("#{Rails.root}/app/assets/stylesheets/#{@target}.scss") - rtl = @target.to_s =~ /_rtl$/ - css = begin - DiscourseSassCompiler.compile(scss, @target, rtl: rtl) - rescue Sass::SyntaxError => e - Rails.logger.error "Stylesheet failed to compile for '#{@target}'! Recompiling without plugins and theming." - Rails.logger.error e.sass_backtrace_str("#{@target} stylesheet") - DiscourseSassCompiler.compile(scss + DiscourseSassCompiler.error_as_css(e, "#{@target} stylesheet"), @target, safe: true) - end - FileUtils.mkdir_p(cache_fullpath) - File.open(stylesheet_fullpath, "w") do |f| - f.puts css - end - begin - StylesheetCache.add(@target, digest, css) - rescue => e - Rails.logger.warn "Completely unexpected error adding item to cache #{e}" - end - css - end - - def ensure_digestless_file - # file without digest is only for auto-reloading css in dev env - unless Rails.env.production? || (File.exist?(stylesheet_fullpath_no_digest) && File.mtime(stylesheet_fullpath) == File.mtime(stylesheet_fullpath_no_digest)) - FileUtils.cp(stylesheet_fullpath, stylesheet_fullpath_no_digest) - end - end - - def self.cache_fullpath - "#{Rails.root}/#{CACHE_PATH}" - end - - def cache_fullpath - self.class.cache_fullpath - end - - def stylesheet_fullpath - "#{cache_fullpath}/#{stylesheet_filename}" - end - def stylesheet_fullpath_no_digest - "#{cache_fullpath}/#{stylesheet_filename_no_digest}" - end - - def stylesheet_cdnpath - "#{GlobalSetting.cdn_url}#{stylesheet_relpath}?__ws=#{Discourse.current_hostname}" - end - - def root_path - "#{GlobalSetting.relative_url_root}/" - end - - # using uploads cause we already have all the routing in place - def stylesheet_relpath - "#{root_path}stylesheets/#{stylesheet_filename}" - end - - def stylesheet_relpath_no_digest - "#{root_path}stylesheets/#{stylesheet_filename_no_digest}" - end - - def stylesheet_filename - "#{@target}_#{digest}.css" - end - def stylesheet_filename_no_digest - "#{@target}.css" - end - - # digest encodes the things that trigger a recompile - def digest - @digest ||= begin - theme = (cs = ColorScheme.enabled) ? "#{cs.id}-#{cs.version}" : false - category_updated = Category.where("uploaded_background_id IS NOT NULL").last_updated_at - - if theme || category_updated > 0 - Digest::SHA1.hexdigest "#{RailsMultisite::ConnectionManagement.current_db}-#{theme}-#{DiscourseStylesheets.last_file_updated}-#{category_updated}" - else - digest_string = "defaults-#{DiscourseStylesheets.last_file_updated}" - - if cdn_url = GlobalSetting.cdn_url - digest_string = "#{digest_string}-#{cdn_url}" - end - - Digest::SHA1.hexdigest digest_string - end - end - end -end diff --git a/lib/stylesheet/common.rb b/lib/stylesheet/common.rb new file mode 100644 index 0000000000..e133e7cd2d --- /dev/null +++ b/lib/stylesheet/common.rb @@ -0,0 +1,5 @@ +require 'sassc' + +module Stylesheet + ASSET_ROOT = "#{Rails.root}/app/assets/stylesheets" unless defined? ASSET_ROOT +end diff --git a/lib/stylesheet/compiler.rb b/lib/stylesheet/compiler.rb new file mode 100644 index 0000000000..7ddf106689 --- /dev/null +++ b/lib/stylesheet/compiler.rb @@ -0,0 +1,60 @@ +require_dependency 'stylesheet/common' +require_dependency 'stylesheet/importer' +require_dependency 'stylesheet/functions' + +module Stylesheet + + class Compiler + + def self.error_as_css(error, label) + error = error.message + error.gsub!("\n", '\A ') + error.gsub!("'", '\27 ') + + "footer { white-space: pre; } + footer:after { content: '#{error}' }" + end + + def self.compile_asset(asset, options={}) + + if Importer.special_imports[asset.to_s] + filename = "theme.scss" + file = "@import \"#{asset}\";" + else + filename = "#{asset}.scss" + path = "#{ASSET_ROOT}/#{filename}" + file = File.read path + end + + compile(file,filename,options) + + end + + def self.compile(stylesheet, filename, options={}) + + + source_map_file = options[:source_map_file] || "#{filename.sub(".scss","")}.css.map"; + engine = SassC::Engine.new(stylesheet, + importer: Importer, + filename: filename, + style: :compressed, + source_map_file: source_map_file, + source_map_contents: true, + theme_id: options[:theme_id], + load_paths: [ASSET_ROOT]) + + + result = engine.render + + if options[:rtl] + require 'r2' + [R2.r2(result), nil] + else + source_map = engine.source_map + source_map.force_encoding("UTF-8") + + [result, source_map] + end + end + end +end diff --git a/lib/stylesheet/functions.rb b/lib/stylesheet/functions.rb new file mode 100644 index 0000000000..cd21bb9a9c --- /dev/null +++ b/lib/stylesheet/functions.rb @@ -0,0 +1,9 @@ +module Stylesheet + module ScssFunctions + def asset_url(path) + SassC::Script::String.new("url('#{ActionController::Base.helpers.asset_path(path.value)}')") + end + end +end + +::SassC::Script::Functions.send :include, Stylesheet::ScssFunctions diff --git a/lib/stylesheet/importer.rb b/lib/stylesheet/importer.rb new file mode 100644 index 0000000000..9d6824f38c --- /dev/null +++ b/lib/stylesheet/importer.rb @@ -0,0 +1,126 @@ +require_dependency 'stylesheet/common' + +module Stylesheet + class Importer < SassC::Importer + + @special_imports = {} + + def self.special_imports + @special_imports + end + + def self.register_import(name, &blk) + @special_imports[name] = blk + end + + register_import "plugins" do + import_files(DiscoursePluginRegistry.stylesheets) + end + + register_import "plugins_mobile" do + import_files(DiscoursePluginRegistry.mobile_stylesheets) + end + + register_import "plugins_desktop" do + import_files(DiscoursePluginRegistry.desktop_stylesheets) + end + + register_import "plugins_variables" do + import_files(DiscoursePluginRegistry.sass_variables) + end + + register_import "theme_variables" do + contents = "" + colors = (@theme_id && theme.color_scheme) ? theme.color_scheme.resolved_colors : ColorScheme.base_colors + colors.each do |n, hex| + contents << "$#{n}: ##{hex} !default;\n" + end + Import.new("theme_variable.scss", source: contents) + end + + register_import "category_backgrounds" do + contents = "" + Category.where('uploaded_background_id IS NOT NULL').each do |c| + contents << category_css(c) if c.uploaded_background + end + + Import.new("categoy_background.scss", source: contents) + end + + register_import "embedded_theme" do + next unless @theme_id + + theme_import(:common, :embedded_scss) + end + + register_import "mobile_theme" do + next unless @theme_id + + theme_import(:mobile, :scss) + end + + register_import "desktop_theme" do + next unless @theme_id + + theme_import(:desktop, :scss) + end + + def initialize(options) + @theme_id = options[:theme_id] + end + + def import_files(files) + files.map do |file| + # we never want inline css imports, they are a mess + # this tricks libsass so it imports inline instead + if file =~ /\.css$/ + file = file[0..-5] + end + Import.new(file) + end + end + + def theme_import(target, attr) + fields = theme.list_baked_fields(target, attr) + + fields.map do |field| + value = field.value + if value.present? + filename = "#{field.theme.id}/#{field.target_name}-#{field.name}-#{field.theme.name.parameterize}.scss" + with_comment = <] + cache[cache_key] = tag + + tag.dup.html_safe + end + end + + def self.precompile_css + themes = Theme.where('user_selectable OR key = ?', SiteSetting.default_theme_key).pluck(:key,:name) + themes << nil + themes.each do |key,name| + [:desktop, :mobile, :desktop_rtl, :mobile_rtl].each do |target| + STDERR.puts "precompile target: #{target} #{name}" + stylesheet_link_tag(target, nil, key) + end + end + nil + end + + def self.last_file_updated + if Rails.env.production? + @last_file_updated ||= if File.exists?(MANIFEST_FULL_PATH) + File.readlines(MANIFEST_FULL_PATH, 'r')[0] + else + mtime = max_file_mtime + FileUtils.mkdir_p(MANIFEST_DIR) + File.open(MANIFEST_FULL_PATH, "w") { |f| f.print(mtime) } + mtime + end + else + max_file_mtime + end + end + + def self.max_file_mtime + globs = ["#{Rails.root}/app/assets/stylesheets/**/*.*css"] + + Discourse.plugins.map { |plugin| File.dirname(plugin.path) }.each do |path| + globs += [ + "#{path}/plugin.rb", + "#{path}/**/*.*css", + ] + end + + globs.map do |pattern| + Dir.glob(pattern).map { |x| File.mtime(x) }.max + end.compact.max.to_i + end + + def initialize(target = :desktop, theme_key) + @target = target + @theme_key = theme_key + end + + def compile(opts={}) + unless opts[:force] + if File.exists?(stylesheet_fullpath) + unless StylesheetCache.where(target: qualified_target, digest: digest).exists? + begin + source_map = File.read(source_map_fullpath) rescue nil + StylesheetCache.add(qualified_target, digest, File.read(stylesheet_fullpath), source_map) + rescue => e + Rails.logger.warn "Completely unexpected error adding contents of '#{stylesheet_fullpath}' to cache #{e}" + end + end + return true + end + end + + rtl = @target.to_s =~ /_rtl$/ + css,source_map = begin + Stylesheet::Compiler.compile_asset( + @target, + rtl: rtl, + theme_id: theme&.id, + source_map_file: source_map_filename + ) + rescue SassC::SyntaxError => e + Rails.logger.error "Failed to compile #{@target} stylesheet: #{e.message}" + [Stylesheet::Compiler.error_as_css(e, "#{@target} stylesheet"), nil] + end + + FileUtils.mkdir_p(cache_fullpath) + + File.open(stylesheet_fullpath, "w") do |f| + f.puts css + end + + if source_map.present? + File.open(source_map_fullpath, "w") do |f| + f.puts source_map + end + end + + begin + StylesheetCache.add(qualified_target, digest, css, source_map) + rescue => e + Rails.logger.warn "Completely unexpected error adding item to cache #{e}" + end + css + end + + def ensure_digestless_file + # file without digest is only for auto-reloading css in dev env + unless Rails.env.production? || (File.exist?(stylesheet_fullpath_no_digest) && File.mtime(stylesheet_fullpath) == File.mtime(stylesheet_fullpath_no_digest)) + FileUtils.cp(stylesheet_fullpath, stylesheet_fullpath_no_digest) + end + end + + def self.cache_fullpath + "#{Rails.root}/#{CACHE_PATH}" + end + + def cache_fullpath + self.class.cache_fullpath + end + + def stylesheet_fullpath + "#{cache_fullpath}/#{stylesheet_filename}" + end + + def source_map_fullpath + "#{cache_fullpath}/#{source_map_filename}" + end + + def source_map_filename + "#{stylesheet_filename}.map" + end + + def stylesheet_fullpath_no_digest + "#{cache_fullpath}/#{stylesheet_filename_no_digest}" + end + + def stylesheet_cdnpath + "#{GlobalSetting.cdn_url}#{stylesheet_relpath}?__ws=#{Discourse.current_hostname}" + end + + def stylesheet_path + if Rails.env.development? + if @target.to_s =~ /theme/ + stylesheet_relpath + else + stylesheet_relpath_no_digest + end + else + stylesheet_cdnpath + end + end + + def root_path + "#{GlobalSetting.relative_url_root}/" + end + + def stylesheet_relpath + "#{root_path}stylesheets/#{stylesheet_filename}" + end + + def stylesheet_relpath_no_digest + "#{root_path}stylesheets/#{stylesheet_filename_no_digest}" + end + + def qualified_target + if is_theme? + "#{@target}_#{theme.id}" + else + scheme_string = theme && theme.color_scheme ? "_#{theme.color_scheme.id}" : "" + "#{@target}#{scheme_string}" + end + end + + def stylesheet_filename(with_digest = true) + digest_string = "_#{self.digest}" if with_digest + "#{qualified_target}#{digest_string}.css" + end + + def stylesheet_filename_no_digest + stylesheet_filename(_with_digest=false) + end + + def is_theme? + !!(@target.to_s =~ /_theme$/) + end + + # digest encodes the things that trigger a recompile + def digest + @digest ||= begin + if is_theme? + theme_digest + else + color_scheme_digest + end + end + end + + def theme + @theme ||= (Theme.find_by(key: @theme_key) || :nil) + @theme == :nil ? nil : @theme + end + + def theme_digest + scss = "" + + if [:mobile_theme, :desktop_theme].include?(@target) + scss = theme.resolve_baked_field(:common, :scss) + scss += theme.resolve_baked_field(@target.to_s.sub("_theme", ""), :scss) + elsif @target == :embedded_theme + scss = theme.resolve_baked_field(:common, :embedded_scss) + else + raise "attempting to look up theme digest for invalid field" + end + + Digest::SHA1.hexdigest scss.to_s + end + + def color_scheme_digest + + cs = theme&.color_scheme + category_updated = Category.where("uploaded_background_id IS NOT NULL").last_updated_at + + if cs || category_updated > 0 + Digest::SHA1.hexdigest "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{Stylesheet::Manager.last_file_updated}-#{category_updated}" + else + digest_string = "defaults-#{Stylesheet::Manager.last_file_updated}" + + if cdn_url = GlobalSetting.cdn_url + digest_string = "#{digest_string}-#{cdn_url}" + end + + Digest::SHA1.hexdigest digest_string + end + end +end diff --git a/lib/stylesheet/watcher.rb b/lib/stylesheet/watcher.rb new file mode 100644 index 0000000000..8f2005e74b --- /dev/null +++ b/lib/stylesheet/watcher.rb @@ -0,0 +1,70 @@ +require 'listen' + +module Stylesheet + class Watcher + + def self.watch(paths=nil) + watcher = new(paths) + watcher.start + watcher + end + + def initialize(paths) + @paths = paths || ["app/assets/stylesheets", "plugins"] + @queue = Queue.new + end + + def start + + Thread.new do + begin + while true + worker_loop + end + rescue => e + STDERR.puts "CSS change notifier crashed #{e}" + end + end + + + root = Rails.root.to_s + @paths.each do |watch| + Thread.new do + begin + Listen.to("#{root}/#{watch}") do |modified, added, _| + paths = [modified, added].flatten + paths.compact! + paths.map!{|long| long[(root.length+1)..-1]} + process_change(paths) + end + rescue => e + STDERR.puts "Failed to listen for CSS changes at: #{watch}\n#{e}" + end + end + end + end + + def worker_loop + @queue.pop + while @queue.length > 0 + @queue.pop + end + + message = ["desktop", "mobile", "admin"].map do |name| + {hash: SecureRandom.hex, name: "/stylesheets/#{name}.css"} + end + + Stylesheet::Manager.cache.clear + MessageBus.publish '/file-change', message + end + + def process_change(paths) + paths.each do |path| + if path =~ /\.(css|scss)$/ + @queue.push path + end + end + end + + end +end diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake index 23e036f57c..16a6e403ae 100644 --- a/lib/tasks/assets.rake +++ b/lib/tasks/assets.rake @@ -50,10 +50,8 @@ task 'assets:precompile:css' => 'environment' do # css will get precompiled during first request instead in that case. if ActiveRecord::Base.connection.table_exists?(ColorScheme.table_name) - STDERR.puts "Compiling css for #{db}" - [:desktop, :mobile, :desktop_rtl, :mobile_rtl].each do |target| - STDERR.puts "target: #{target} #{DiscourseStylesheets.compile(target)}" - end + STDERR.puts "Compiling css for #{db} #{Time.zone.now}" + Stylesheet::Manager.precompile_css end end diff --git a/lib/wizard/builder.rb b/lib/wizard/builder.rb index 17de742274..3fc48b8313 100644 --- a/lib/wizard/builder.rb +++ b/lib/wizard/builder.rb @@ -114,28 +114,31 @@ class Wizard end @wizard.append_step('colors') do |step| - theme_id = ColorScheme.where(via_wizard: true).pluck(:theme_id) - theme_id = theme_id.present? ? theme_id[0] : 'default' + scheme_id = ColorScheme.where(via_wizard: true).pluck(:base_scheme_id)&.first + scheme_id ||= 'default' - themes = step.add_field(id: 'theme_id', type: 'dropdown', required: true, value: theme_id) - ColorScheme.themes.each {|t| themes.add_choice(t[:id], data: t) } + themes = step.add_field(id: 'base_scheme_id', type: 'dropdown', required: true, value: scheme_id) + ColorScheme.base_color_scheme_colors.each do |t| + with_hash = t[:colors].dup + with_hash.map{|k,v| with_hash[k] = "##{v}"} + themes.add_choice(t[:id], data: {colors: with_hash}) + end step.add_field(id: 'theme_preview', type: 'component') step.on_update do |updater| - scheme_name = updater.fields[:theme_id] + scheme_name = updater.fields[:base_scheme_id] - theme = ColorScheme.themes.find {|s| s[:id] == scheme_name } + theme = ColorScheme.base_color_schemes.find{|s| s.base_scheme_id == scheme_name} colors = [] - theme[:colors].each do |name, hex| - colors << {name: name, hex: hex[1..-1] } + theme.colors.each do |color| + colors << {name: color.name, hex: color.hex } end attrs = { - enabled: true, name: I18n.t("wizard.step.colors.fields.theme_id.choices.#{scheme_name}.label"), colors: colors, - theme_id: scheme_name + base_scheme_id: scheme_name } scheme = ColorScheme.where(via_wizard: true).first @@ -148,6 +151,14 @@ class Wizard scheme = ColorScheme.new(attrs) scheme.save! end + + default_theme = Theme.find_by(key: SiteSetting.default_theme_key) + unless default_theme + default_theme = Theme.new(name: "Default Theme", user_id: -1) + end + default_theme.color_scheme_id = scheme.id + default_theme.save! + SiteSetting.default_theme_key = default_theme.key end end diff --git a/public/javascripts/spectrum.css b/public/javascripts/spectrum.css new file mode 100644 index 0000000000..a8ad9e4f82 --- /dev/null +++ b/public/javascripts/spectrum.css @@ -0,0 +1,507 @@ +/*** +Spectrum Colorpicker v1.8.0 +https://github.com/bgrins/spectrum +Author: Brian Grinstead +License: MIT +***/ + +.sp-container { + position:absolute; + top:0; + left:0; + display:inline-block; + *display: inline; + *zoom: 1; + /* https://github.com/bgrins/spectrum/issues/40 */ + z-index: 9999994; + overflow: hidden; +} +.sp-container.sp-flat { + position: relative; +} + +/* Fix for * { box-sizing: border-box; } */ +.sp-container, +.sp-container * { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +/* http://ansciath.tumblr.com/post/7347495869/css-aspect-ratio */ +.sp-top { + position:relative; + width: 100%; + display:inline-block; +} +.sp-top-inner { + position:absolute; + top:0; + left:0; + bottom:0; + right:0; +} +.sp-color { + position: absolute; + top:0; + left:0; + bottom:0; + right:20%; +} +.sp-hue { + position: absolute; + top:0; + right:0; + bottom:0; + left:84%; + height: 100%; +} + +.sp-clear-enabled .sp-hue { + top:33px; + height: 77.5%; +} + +.sp-fill { + padding-top: 80%; +} +.sp-sat, .sp-val { + position: absolute; + top:0; + left:0; + right:0; + bottom:0; +} + +.sp-alpha-enabled .sp-top { + margin-bottom: 18px; +} +.sp-alpha-enabled .sp-alpha { + display: block; +} +.sp-alpha-handle { + position:absolute; + top:-4px; + bottom: -4px; + width: 6px; + left: 50%; + cursor: pointer; + border: 1px solid black; + background: white; + opacity: .8; +} +.sp-alpha { + display: none; + position: absolute; + bottom: -14px; + right: 0; + left: 0; + height: 8px; +} +.sp-alpha-inner { + border: solid 1px #333; +} + +.sp-clear { + display: none; +} + +.sp-clear.sp-clear-display { + background-position: center; +} + +.sp-clear-enabled .sp-clear { + display: block; + position:absolute; + top:0px; + right:0; + bottom:0; + left:84%; + height: 28px; +} + +/* Don't allow text selection */ +.sp-container, .sp-replacer, .sp-preview, .sp-dragger, .sp-slider, .sp-alpha, .sp-clear, .sp-alpha-handle, .sp-container.sp-dragging .sp-input, .sp-container button { + -webkit-user-select:none; + -moz-user-select: -moz-none; + -o-user-select:none; + user-select: none; +} + +.sp-container.sp-input-disabled .sp-input-container { + display: none; +} +.sp-container.sp-buttons-disabled .sp-button-container { + display: none; +} +.sp-container.sp-palette-buttons-disabled .sp-palette-button-container { + display: none; +} +.sp-palette-only .sp-picker-container { + display: none; +} +.sp-palette-disabled .sp-palette-container { + display: none; +} + +.sp-initial-disabled .sp-initial { + display: none; +} + + +/* Gradients for hue, saturation and value instead of images. Not pretty... but it works */ +.sp-sat { + background-image: -webkit-gradient(linear, 0 0, 100% 0, from(#FFF), to(rgba(204, 154, 129, 0))); + background-image: -webkit-linear-gradient(left, #FFF, rgba(204, 154, 129, 0)); + background-image: -moz-linear-gradient(left, #fff, rgba(204, 154, 129, 0)); + background-image: -o-linear-gradient(left, #fff, rgba(204, 154, 129, 0)); + background-image: -ms-linear-gradient(left, #fff, rgba(204, 154, 129, 0)); + background-image: linear-gradient(to right, #fff, rgba(204, 154, 129, 0)); + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#FFFFFFFF, endColorstr=#00CC9A81)"; + filter : progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr='#FFFFFFFF', endColorstr='#00CC9A81'); +} +.sp-val { + background-image: -webkit-gradient(linear, 0 100%, 0 0, from(#000000), to(rgba(204, 154, 129, 0))); + background-image: -webkit-linear-gradient(bottom, #000000, rgba(204, 154, 129, 0)); + background-image: -moz-linear-gradient(bottom, #000, rgba(204, 154, 129, 0)); + background-image: -o-linear-gradient(bottom, #000, rgba(204, 154, 129, 0)); + background-image: -ms-linear-gradient(bottom, #000, rgba(204, 154, 129, 0)); + background-image: linear-gradient(to top, #000, rgba(204, 154, 129, 0)); + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#00CC9A81, endColorstr=#FF000000)"; + filter : progid:DXImageTransform.Microsoft.gradient(startColorstr='#00CC9A81', endColorstr='#FF000000'); +} + +.sp-hue { + background: -moz-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); + background: -ms-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); + background: -o-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); + background: -webkit-gradient(linear, left top, left bottom, from(#ff0000), color-stop(0.17, #ffff00), color-stop(0.33, #00ff00), color-stop(0.5, #00ffff), color-stop(0.67, #0000ff), color-stop(0.83, #ff00ff), to(#ff0000)); + background: -webkit-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); + background: linear-gradient(to bottom, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); +} + +/* IE filters do not support multiple color stops. + Generate 6 divs, line them up, and do two color gradients for each. + Yes, really. + */ +.sp-1 { + height:17%; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0000', endColorstr='#ffff00'); +} +.sp-2 { + height:16%; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffff00', endColorstr='#00ff00'); +} +.sp-3 { + height:17%; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ff00', endColorstr='#00ffff'); +} +.sp-4 { + height:17%; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffff', endColorstr='#0000ff'); +} +.sp-5 { + height:16%; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0000ff', endColorstr='#ff00ff'); +} +.sp-6 { + height:17%; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff00ff', endColorstr='#ff0000'); +} + +.sp-hidden { + display: none !important; +} + +/* Clearfix hack */ +.sp-cf:before, .sp-cf:after { content: ""; display: table; } +.sp-cf:after { clear: both; } +.sp-cf { *zoom: 1; } + +/* Mobile devices, make hue slider bigger so it is easier to slide */ +@media (max-device-width: 480px) { + .sp-color { right: 40%; } + .sp-hue { left: 63%; } + .sp-fill { padding-top: 60%; } +} +.sp-dragger { + border-radius: 5px; + height: 5px; + width: 5px; + border: 1px solid #fff; + background: #000; + cursor: pointer; + position:absolute; + top:0; + left: 0; +} +.sp-slider { + position: absolute; + top:0; + cursor:pointer; + height: 3px; + left: -1px; + right: -1px; + border: 1px solid #000; + background: white; + opacity: .8; +} + +/* +Theme authors: +Here are the basic themeable display options (colors, fonts, global widths). +See http://bgrins.github.io/spectrum/themes/ for instructions. +*/ + +.sp-container { + border-radius: 0; + background-color: #ECECEC; + border: solid 1px #f0c49B; + padding: 0; +} +.sp-container, .sp-container button, .sp-container input, .sp-color, .sp-hue, .sp-clear { + font: normal 12px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; +} +.sp-top { + margin-bottom: 3px; +} +.sp-color, .sp-hue, .sp-clear { + border: solid 1px #666; +} + +/* Input */ +.sp-input-container { + float:right; + width: 100px; + margin-bottom: 4px; +} +.sp-initial-disabled .sp-input-container { + width: 100%; +} +.sp-input { + font-size: 12px !important; + border: 1px inset; + padding: 4px 5px; + margin: 0; + width: 100%; + background:transparent; + border-radius: 3px; + color: #222; +} +.sp-input:focus { + border: 1px solid orange; +} +.sp-input.sp-validation-error { + border: 1px solid red; + background: #fdd; +} +.sp-picker-container , .sp-palette-container { + float:left; + position: relative; + padding: 10px; + padding-bottom: 300px; + margin-bottom: -290px; +} +.sp-picker-container { + width: 172px; + border-left: solid 1px #fff; +} + +/* Palettes */ +.sp-palette-container { + border-right: solid 1px #ccc; +} + +.sp-palette-only .sp-palette-container { + border: 0; +} + +.sp-palette .sp-thumb-el { + display: block; + position:relative; + float:left; + width: 24px; + height: 15px; + margin: 3px; + cursor: pointer; + border:solid 2px transparent; +} +.sp-palette .sp-thumb-el:hover, .sp-palette .sp-thumb-el.sp-thumb-active { + border-color: orange; +} +.sp-thumb-el { + position:relative; +} + +/* Initial */ +.sp-initial { + float: left; + border: solid 1px #333; +} +.sp-initial span { + width: 30px; + height: 25px; + border:none; + display:block; + float:left; + margin:0; +} + +.sp-initial .sp-clear-display { + background-position: center; +} + +/* Buttons */ +.sp-palette-button-container, +.sp-button-container { + float: right; +} + +/* Replacer (the little preview div that shows up instead of the ) */ +.sp-replacer { + margin:0; + overflow:hidden; + cursor:pointer; + padding: 4px; + display:inline-block; + *zoom: 1; + *display: inline; + border: solid 1px #91765d; + background: #eee; + color: #333; + vertical-align: middle; +} +.sp-replacer:hover, .sp-replacer.sp-active { + border-color: #F0C49B; + color: #111; +} +.sp-replacer.sp-disabled { + cursor:default; + border-color: silver; + color: silver; +} +.sp-dd { + padding: 2px 0; + height: 16px; + line-height: 16px; + float:left; + font-size:10px; +} +.sp-preview { + position:relative; + width:25px; + height: 20px; + border: solid 1px #222; + margin-right: 5px; + float:left; + z-index: 0; +} + +.sp-palette { + *width: 220px; + max-width: 220px; +} +.sp-palette .sp-thumb-el { + width:16px; + height: 16px; + margin:2px 1px; + border: solid 1px #d0d0d0; +} + +.sp-container { + padding-bottom:0; +} + + +/* Buttons: http://hellohappy.org/css3-buttons/ */ +.sp-container button { + background-color: #eeeeee; + background-image: -webkit-linear-gradient(top, #eeeeee, #cccccc); + background-image: -moz-linear-gradient(top, #eeeeee, #cccccc); + background-image: -ms-linear-gradient(top, #eeeeee, #cccccc); + background-image: -o-linear-gradient(top, #eeeeee, #cccccc); + background-image: linear-gradient(to bottom, #eeeeee, #cccccc); + border: 1px solid #ccc; + border-bottom: 1px solid #bbb; + border-radius: 3px; + color: #333; + font-size: 14px; + line-height: 1; + padding: 5px 4px; + text-align: center; + text-shadow: 0 1px 0 #eee; + vertical-align: middle; +} +.sp-container button:hover { + background-color: #dddddd; + background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb); + background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb); + background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb); + background-image: -o-linear-gradient(top, #dddddd, #bbbbbb); + background-image: linear-gradient(to bottom, #dddddd, #bbbbbb); + border: 1px solid #bbb; + border-bottom: 1px solid #999; + cursor: pointer; + text-shadow: 0 1px 0 #ddd; +} +.sp-container button:active { + border: 1px solid #aaa; + border-bottom: 1px solid #888; + -webkit-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; + -moz-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; + -ms-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; + -o-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; + box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; +} +.sp-cancel { + font-size: 11px; + color: #d93f3f !important; + margin:0; + padding:2px; + margin-right: 5px; + vertical-align: middle; + text-decoration:none; + +} +.sp-cancel:hover { + color: #d93f3f !important; + text-decoration: underline; +} + + +.sp-palette span:hover, .sp-palette span.sp-thumb-active { + border-color: #000; +} + +.sp-preview, .sp-alpha, .sp-thumb-el { + position:relative; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==); +} +.sp-preview-inner, .sp-alpha-inner, .sp-thumb-inner { + display:block; + position:absolute; + top:0;left:0;bottom:0;right:0; +} + +.sp-palette .sp-thumb-inner { + background-position: 50% 50%; + background-repeat: no-repeat; +} + +.sp-palette .sp-thumb-light.sp-thumb-active .sp-thumb-inner { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIVJREFUeNpiYBhsgJFMffxAXABlN5JruT4Q3wfi/0DsT64h8UD8HmpIPCWG/KemIfOJCUB+Aoacx6EGBZyHBqI+WsDCwuQ9mhxeg2A210Ntfo8klk9sOMijaURm7yc1UP2RNCMbKE9ODK1HM6iegYLkfx8pligC9lCD7KmRof0ZhjQACDAAceovrtpVBRkAAAAASUVORK5CYII=); +} + +.sp-palette .sp-thumb-dark.sp-thumb-active .sp-thumb-inner { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAMdJREFUOE+tkgsNwzAMRMugEAahEAahEAZhEAqlEAZhEAohEAYh81X2dIm8fKpEspLGvudPOsUYpxE2BIJCroJmEW9qJ+MKaBFhEMNabSy9oIcIPwrB+afvAUFoK4H0tMaQ3XtlrggDhOVVMuT4E5MMG0FBbCEYzjYT7OxLEvIHQLY2zWwQ3D+9luyOQTfKDiFD3iUIfPk8VqrKjgAiSfGFPecrg6HN6m/iBcwiDAo7WiBeawa+Kwh7tZoSCGLMqwlSAzVDhoK+6vH4G0P5wdkAAAAASUVORK5CYII=); +} + +.sp-clear-display { + background-repeat:no-repeat; + background-position: center; + background-image: url(data:image/gif;base64,R0lGODlhFAAUAPcAAAAAAJmZmZ2dnZ6enqKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq/Hx8fLy8vT09PX19ff39/j4+Pn5+fr6+vv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAAUABQAAAihAP9FoPCvoMGDBy08+EdhQAIJCCMybCDAAYUEARBAlFiQQoMABQhKUJBxY0SPICEYHBnggEmDKAuoPMjS5cGYMxHW3IiT478JJA8M/CjTZ0GgLRekNGpwAsYABHIypcAgQMsITDtWJYBR6NSqMico9cqR6tKfY7GeBCuVwlipDNmefAtTrkSzB1RaIAoXodsABiZAEFB06gIBWC1mLVgBa0AAOw==); +} diff --git a/public/javascripts/spectrum.js b/public/javascripts/spectrum.js new file mode 100644 index 0000000000..2b31452841 --- /dev/null +++ b/public/javascripts/spectrum.js @@ -0,0 +1,2 @@ +(function(factory){"use strict";if(typeof define==="function"&&define.amd){define(["jquery"],factory)}else if(typeof exports=="object"&&typeof module=="object"){module.exports=factory(require("jquery"))}else{factory(jQuery)}})(function($,undefined){"use strict";var defaultOpts={beforeShow:noop,move:noop,change:noop,show:noop,hide:noop,color:false,flat:false,showInput:false,allowEmpty:false,showButtons:true,clickoutFiresChange:true,showInitial:false,showPalette:false,showPaletteOnly:false,hideAfterPaletteSelect:false,togglePaletteOnly:false,showSelectionPalette:true,localStorageKey:false,appendTo:"body",maxSelectionSize:7,cancelText:"cancel",chooseText:"choose",togglePaletteMoreText:"more",togglePaletteLessText:"less",clearText:"Clear Color Selection",noColorSelectedText:"No Color Selected",preferredFormat:false,className:"",containerClassName:"",replacerClassName:"",showAlpha:false,theme:"sp-light",palette:[["#ffffff","#000000","#ff0000","#ff8000","#ffff00","#008000","#0000ff","#4b0082","#9400d3"]],selectionPalette:[],disabled:false,offset:null},spectrums=[],IE=!!/msie/i.exec(window.navigator.userAgent),rgbaSupport=function(){function contains(str,substr){return!!~(""+str).indexOf(substr)}var elem=document.createElement("div");var style=elem.style;style.cssText="background-color:rgba(0,0,0,.5)";return contains(style.backgroundColor,"rgba")||contains(style.backgroundColor,"hsla")}(),replaceInput=["
    ","
    ","
    ","
    "].join(""),markup=function(){var gradientFix="";if(IE){for(var i=1;i<=6;i++){gradientFix+="
    "}}return["
    ","
    ","
    ","
    ","","
    ","
    ","
    ","
    ","
    ","
    ","
    ","
    ","
    ","
    ","
    ","
    ","
    ","
    ","
    ","
    ","
    ",gradientFix,"
    ","
    ","
    ","
    ","
    ","","
    ","
    ","
    ","","","
    ","
    ","
    "].join("")}();function paletteTemplate(p,color,className,opts){var html=[];for(var i=0;i')}else{var cls="sp-clear-display";html.push($("
    ").append($('').attr("title",opts.noColorSelectedText)).html())}}return"
    "+html.join("")+"
    "}function hideAll(){for(var i=0;iMath.abs(dragY-oldDragY);shiftMovementDirection=furtherFromX?"x":"y"}var setSaturation=!shiftMovementDirection||shiftMovementDirection==="x";var setValue=!shiftMovementDirection||shiftMovementDirection==="y";if(setSaturation){currentSaturation=parseFloat(dragX/dragWidth)}if(setValue){currentValue=parseFloat((dragHeight-dragY)/dragHeight)}isEmpty=false;if(!opts.showAlpha){currentAlpha=1}move()},dragStart,dragStop);if(!!initialColor){set(initialColor);updateUI();currentPreferredFormat=opts.preferredFormat||tinycolor(initialColor).format;addColorToSelectionPalette(initialColor)}else{updateUI()}if(flat){show()}function paletteElementClick(e){if(e.data&&e.data.ignore){set($(e.target).closest(".sp-thumb-el").data("color"));move()}else{set($(e.target).closest(".sp-thumb-el").data("color"));move();updateOriginalInput(true);if(opts.hideAfterPaletteSelect){hide()}}return false}var paletteEvent=IE?"mousedown.spectrum":"click.spectrum touchstart.spectrum";paletteContainer.delegate(".sp-thumb-el",paletteEvent,paletteElementClick);initialColorContainer.delegate(".sp-thumb-el:nth-child(1)",paletteEvent,{ignore:true},paletteElementClick)}function updateSelectionPaletteFromStorage(){if(localStorageKey&&window.localStorage){try{var oldPalette=window.localStorage[localStorageKey].split(",#");if(oldPalette.length>1){delete window.localStorage[localStorageKey];$.each(oldPalette,function(i,c){addColorToSelectionPalette(c)})}}catch(e){}try{selectionPalette=window.localStorage[localStorageKey].split(";")}catch(e){}}}function addColorToSelectionPalette(color){if(showSelectionPalette){var rgb=tinycolor(color).toRgbString();if(!paletteLookup[rgb]&&$.inArray(rgb,selectionPalette)===-1){selectionPalette.push(rgb);while(selectionPalette.length>maxSelectionSize){selectionPalette.shift()}}if(localStorageKey&&window.localStorage){try{window.localStorage[localStorageKey]=selectionPalette.join(";")}catch(e){}}}}function getUniqueSelectionPalette(){var unique=[];if(opts.showPalette){for(var i=0;iviewWidth&&viewWidth>dpWidth?Math.abs(offset.left+dpWidth-viewWidth):0);offset.top-=Math.min(offset.top,offset.top+dpHeight>viewHeight&&viewHeight>dpHeight?Math.abs(dpHeight+inputHeight-extraY):extraY);return offset}function noop(){}function stopPropagation(e){e.stopPropagation()}function bind(func,obj){var slice=Array.prototype.slice;var args=slice.call(arguments,2);return function(){return func.apply(obj,args.concat(slice.call(arguments)))}}function draggable(element,onmove,onstart,onstop){onmove=onmove||function(){};onstart=onstart||function(){};onstop=onstop||function(){};var doc=document;var dragging=false;var offset={};var maxHeight=0;var maxWidth=0;var hasTouch="ontouchstart"in window;var duringDragEvents={};duringDragEvents["selectstart"]=prevent;duringDragEvents["dragstart"]=prevent;duringDragEvents["touchmove mousemove"]=move;duringDragEvents["touchend mouseup"]=stop;function prevent(e){if(e.stopPropagation){e.stopPropagation()}if(e.preventDefault){e.preventDefault()}e.returnValue=false}function move(e){if(dragging){if(IE&&doc.documentMode<9&&!e.button){return stop()}var t0=e.originalEvent&&e.originalEvent.touches&&e.originalEvent.touches[0];var pageX=t0&&t0.pageX||e.pageX;var pageY=t0&&t0.pageY||e.pageY;var dragX=Math.max(0,Math.min(pageX-offset.left,maxWidth));var dragY=Math.max(0,Math.min(pageY-offset.top,maxHeight));if(hasTouch){prevent(e)}onmove.apply(element,[dragX,dragY,e])}}function start(e){var rightclick=e.which?e.which==3:e.button==2;if(!rightclick&&!dragging){if(onstart.apply(element,arguments)!==false){dragging=true;maxHeight=$(element).height();maxWidth=$(element).width();offset=$(element).offset();$(doc).bind(duringDragEvents);$(doc.body).addClass("sp-dragging");move(e);prevent(e)}}}function stop(){if(dragging){$(doc).unbind(duringDragEvents);$(doc.body).removeClass("sp-dragging");setTimeout(function(){onstop.apply(element,arguments)},0)}dragging=false}$(element).bind("touchstart mousedown",start)}function throttle(func,wait,debounce){var timeout;return function(){var context=this,args=arguments;var throttler=function(){timeout=null;func.apply(context,args)};if(debounce)clearTimeout(timeout);if(debounce||!timeout)timeout=setTimeout(throttler,wait)}}function inputTypeColorSupport(){return $.fn.spectrum.inputTypeColorSupport()}var dataID="spectrum.id";$.fn.spectrum=function(opts,extra){if(typeof opts=="string"){var returnValue=this;var args=Array.prototype.slice.call(arguments,1);this.each(function(){var spect=spectrums[$(this).data(dataID)];if(spect){var method=spect[opts];if(!method){throw new Error("Spectrum: no such method: '"+opts+"'")}if(opts=="get"){returnValue=spect.get()}else if(opts=="container"){returnValue=spect.container}else if(opts=="option"){returnValue=spect.option.apply(spect,args)}else if(opts=="destroy"){spect.destroy();$(this).removeData(dataID)}else{method.apply(spect,args)}}});return returnValue}return this.spectrum("destroy").each(function(){var options=$.extend({},opts,$(this).data());var spect=spectrum(this,options);$(this).data(dataID,spect.id)})};$.fn.spectrum.load=true;$.fn.spectrum.loadOpts={};$.fn.spectrum.draggable=draggable;$.fn.spectrum.defaults=defaultOpts;$.fn.spectrum.inputTypeColorSupport=function inputTypeColorSupport(){if(typeof inputTypeColorSupport._cachedResult==="undefined"){var colorInput=$("")[0];inputTypeColorSupport._cachedResult=colorInput.type==="color"&&colorInput.value!==""}return inputTypeColorSupport._cachedResult};$.spectrum={};$.spectrum.localization={};$.spectrum.palettes={};$.fn.spectrum.processNativeColorInputs=function(){var colorInputs=$("input[type=color]");if(colorInputs.length&&!inputTypeColorSupport()){colorInputs.spectrum({preferredFormat:"hex6"})}};(function(){var trimLeft=/^[\s,#]+/,trimRight=/\s+$/,tinyCounter=0,math=Math,mathRound=math.round,mathMin=math.min,mathMax=math.max,mathRandom=math.random;var tinycolor=function(color,opts){color=color?color:"";opts=opts||{};if(color instanceof tinycolor){return color}if(!(this instanceof tinycolor)){return new tinycolor(color,opts)}var rgb=inputToRGB(color);this._originalInput=color,this._r=rgb.r,this._g=rgb.g,this._b=rgb.b,this._a=rgb.a,this._roundA=mathRound(100*this._a)/100,this._format=opts.format||rgb.format;this._gradientType=opts.gradientType;if(this._r<1){this._r=mathRound(this._r)}if(this._g<1){this._g=mathRound(this._g)}if(this._b<1){this._b=mathRound(this._b)}this._ok=rgb.ok;this._tc_id=tinyCounter++};tinycolor.prototype={isDark:function(){return this.getBrightness()<128},isLight:function(){return!this.isDark()},isValid:function(){return this._ok},getOriginalInput:function(){return this._originalInput},getFormat:function(){return this._format},getAlpha:function(){return this._a},getBrightness:function(){var rgb=this.toRgb();return(rgb.r*299+rgb.g*587+rgb.b*114)/1e3},setAlpha:function(value){this._a=boundAlpha(value);this._roundA=mathRound(100*this._a)/100;return this},toHsv:function(){var hsv=rgbToHsv(this._r,this._g,this._b);return{h:hsv.h*360,s:hsv.s,v:hsv.v,a:this._a}},toHsvString:function(){var hsv=rgbToHsv(this._r,this._g,this._b);var h=mathRound(hsv.h*360),s=mathRound(hsv.s*100),v=mathRound(hsv.v*100);return this._a==1?"hsv("+h+", "+s+"%, "+v+"%)":"hsva("+h+", "+s+"%, "+v+"%, "+this._roundA+")"},toHsl:function(){var hsl=rgbToHsl(this._r,this._g,this._b);return{h:hsl.h*360,s:hsl.s,l:hsl.l,a:this._a}},toHslString:function(){var hsl=rgbToHsl(this._r,this._g,this._b);var h=mathRound(hsl.h*360),s=mathRound(hsl.s*100),l=mathRound(hsl.l*100);return this._a==1?"hsl("+h+", "+s+"%, "+l+"%)":"hsla("+h+", "+s+"%, "+l+"%, "+this._roundA+")"},toHex:function(allow3Char){return rgbToHex(this._r,this._g,this._b,allow3Char)},toHexString:function(allow3Char){return"#"+this.toHex(allow3Char)},toHex8:function(){return rgbaToHex(this._r,this._g,this._b,this._a)},toHex8String:function(){return"#"+this.toHex8()},toRgb:function(){return{r:mathRound(this._r),g:mathRound(this._g),b:mathRound(this._b),a:this._a}},toRgbString:function(){return this._a==1?"rgb("+mathRound(this._r)+", "+mathRound(this._g)+", "+mathRound(this._b)+")":"rgba("+mathRound(this._r)+", "+mathRound(this._g)+", "+mathRound(this._b)+", "+this._roundA+")"},toPercentageRgb:function(){return{r:mathRound(bound01(this._r,255)*100)+"%",g:mathRound(bound01(this._g,255)*100)+"%",b:mathRound(bound01(this._b,255)*100)+"%",a:this._a}},toPercentageRgbString:function(){return this._a==1?"rgb("+mathRound(bound01(this._r,255)*100)+"%, "+mathRound(bound01(this._g,255)*100)+"%, "+mathRound(bound01(this._b,255)*100)+"%)":"rgba("+mathRound(bound01(this._r,255)*100)+"%, "+mathRound(bound01(this._g,255)*100)+"%, "+mathRound(bound01(this._b,255)*100)+"%, "+this._roundA+")"},toName:function(){if(this._a===0){return"transparent"}if(this._a<1){return false}return hexNames[rgbToHex(this._r,this._g,this._b,true)]||false},toFilter:function(secondColor){var hex8String="#"+rgbaToHex(this._r,this._g,this._b,this._a);var secondHex8String=hex8String;var gradientType=this._gradientType?"GradientType = 1, ":"";if(secondColor){var s=tinycolor(secondColor);secondHex8String=s.toHex8String()}return"progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr="+hex8String+",endColorstr="+secondHex8String+")"},toString:function(format){var formatSet=!!format;format=format||this._format;var formattedString=false;var hasAlpha=this._a<1&&this._a>=0;var needsAlphaFormat=!formatSet&&hasAlpha&&(format==="hex"||format==="hex6"||format==="hex3"||format==="name");if(needsAlphaFormat){if(format==="name"&&this._a===0){return this.toName()}return this.toRgbString()}if(format==="rgb"){formattedString=this.toRgbString()}if(format==="prgb"){formattedString=this.toPercentageRgbString()}if(format==="hex"||format==="hex6"){formattedString=this.toHexString()}if(format==="hex3"){formattedString=this.toHexString(true)}if(format==="hex8"){formattedString=this.toHex8String()}if(format==="name"){formattedString=this.toName()}if(format==="hsl"){formattedString=this.toHslString()}if(format==="hsv"){formattedString=this.toHsvString()}return formattedString||this.toHexString()},_applyModification:function(fn,args){var color=fn.apply(null,[this].concat([].slice.call(args)));this._r=color._r;this._g=color._g;this._b=color._b;this.setAlpha(color._a);return this},lighten:function(){return this._applyModification(lighten,arguments)},brighten:function(){return this._applyModification(brighten,arguments)},darken:function(){return this._applyModification(darken,arguments)},desaturate:function(){return this._applyModification(desaturate,arguments)},saturate:function(){return this._applyModification(saturate,arguments)},greyscale:function(){return this._applyModification(greyscale,arguments)},spin:function(){return this._applyModification(spin,arguments)},_applyCombination:function(fn,args){return fn.apply(null,[this].concat([].slice.call(args)))},analogous:function(){return this._applyCombination(analogous,arguments)},complement:function(){return this._applyCombination(complement,arguments)},monochromatic:function(){return this._applyCombination(monochromatic,arguments)},splitcomplement:function(){return this._applyCombination(splitcomplement,arguments)},triad:function(){return this._applyCombination(triad,arguments)},tetrad:function(){return this._applyCombination(tetrad,arguments)}};tinycolor.fromRatio=function(color,opts){if(typeof color=="object"){var newColor={};for(var i in color){if(color.hasOwnProperty(i)){if(i==="a"){newColor[i]=color[i]}else{newColor[i]=convertToPercentage(color[i])}}}color=newColor}return tinycolor(color,opts)};function inputToRGB(color){var rgb={r:0,g:0,b:0};var a=1;var ok=false;var format=false;if(typeof color=="string"){color=stringInputToObject(color)}if(typeof color=="object"){if(color.hasOwnProperty("r")&&color.hasOwnProperty("g")&&color.hasOwnProperty("b")){rgb=rgbToRgb(color.r,color.g,color.b);ok=true;format=String(color.r).substr(-1)==="%"?"prgb":"rgb"}else if(color.hasOwnProperty("h")&&color.hasOwnProperty("s")&&color.hasOwnProperty("v")){color.s=convertToPercentage(color.s);color.v=convertToPercentage(color.v);rgb=hsvToRgb(color.h,color.s,color.v);ok=true;format="hsv"}else if(color.hasOwnProperty("h")&&color.hasOwnProperty("s")&&color.hasOwnProperty("l")){color.s=convertToPercentage(color.s);color.l=convertToPercentage(color.l);rgb=hslToRgb(color.h,color.s,color.l);ok=true;format="hsl"}if(color.hasOwnProperty("a")){a=color.a}}a=boundAlpha(a);return{ok:ok,format:color.format||format,r:mathMin(255,mathMax(rgb.r,0)),g:mathMin(255,mathMax(rgb.g,0)),b:mathMin(255,mathMax(rgb.b,0)),a:a}}function rgbToRgb(r,g,b){return{r:bound01(r,255)*255,g:bound01(g,255)*255,b:bound01(b,255)*255}}function rgbToHsl(r,g,b){r=bound01(r,255);g=bound01(g,255);b=bound01(b,255);var max=mathMax(r,g,b),min=mathMin(r,g,b);var h,s,l=(max+min)/2;if(max==min){h=s=0}else{var d=max-min;s=l>.5?d/(2-max-min):d/(max+min);switch(max){case r:h=(g-b)/d+(g1)t-=1;if(t<1/6)return p+(q-p)*6*t;if(t<1/2)return q;if(t<2/3)return p+(q-p)*(2/3-t)*6;return p}if(s===0){r=g=b=l}else{var q=l<.5?l*(1+s):l+s-l*s;var p=2*l-q;r=hue2rgb(p,q,h+1/3);g=hue2rgb(p,q,h);b=hue2rgb(p,q,h-1/3)}return{r:r*255,g:g*255,b:b*255}}function rgbToHsv(r,g,b){r=bound01(r,255);g=bound01(g,255);b=bound01(b,255);var max=mathMax(r,g,b),min=mathMin(r,g,b);var h,s,v=max;var d=max-min;s=max===0?0:d/max;if(max==min){h=0}else{switch(max){case r:h=(g-b)/d+(g>1)+720)%360;--results;){hsl.h=(hsl.h+part)%360;ret.push(tinycolor(hsl))}return ret}function monochromatic(color,results){results=results||6;var hsv=tinycolor(color).toHsv();var h=hsv.h,s=hsv.s,v=hsv.v;var ret=[];var modification=1/results;while(results--){ret.push(tinycolor({h:h,s:s,v:v}));v=(v+modification)%1}return ret}tinycolor.mix=function(color1,color2,amount){amount=amount===0?0:amount||50;var rgb1=tinycolor(color1).toRgb();var rgb2=tinycolor(color2).toRgb();var p=amount/100;var w=p*2-1;var a=rgb2.a-rgb1.a;var w1;if(w*a==-1){w1=w}else{w1=(w+a)/(1+w*a)}w1=(w1+1)/2;var w2=1-w1;var rgba={r:rgb2.r*w1+rgb1.r*w2,g:rgb2.g*w1+rgb1.g*w2,b:rgb2.b*w1+rgb1.b*w2,a:rgb2.a*p+rgb1.a*(1-p)};return tinycolor(rgba)};tinycolor.readability=function(color1,color2){var c1=tinycolor(color1);var c2=tinycolor(color2);var rgb1=c1.toRgb();var rgb2=c2.toRgb();var brightnessA=c1.getBrightness();var brightnessB=c2.getBrightness();var colorDiff=Math.max(rgb1.r,rgb2.r)-Math.min(rgb1.r,rgb2.r)+Math.max(rgb1.g,rgb2.g)-Math.min(rgb1.g,rgb2.g)+Math.max(rgb1.b,rgb2.b)-Math.min(rgb1.b,rgb2.b);return{brightness:Math.abs(brightnessA-brightnessB),color:colorDiff}};tinycolor.isReadable=function(color1,color2){var readability=tinycolor.readability(color1,color2);return readability.brightness>125&&readability.color>500};tinycolor.mostReadable=function(baseColor,colorList){var bestColor=null;var bestScore=0;var bestIsReadable=false;for(var i=0;i125&&readability.color>500;var score=3*(readability.brightness/125)+readability.color/500;if(readable&&!bestIsReadable||readable&&bestIsReadable&&score>bestScore||!readable&&!bestIsReadable&&score>bestScore){bestIsReadable=readable;bestScore=score;bestColor=tinycolor(colorList[i])}}return bestColor};var names=tinycolor.names={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"0ff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000",blanchedalmond:"ffebcd",blue:"00f",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",burntsienna:"ea7e5d",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"0ff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkgrey:"a9a9a9",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkslategrey:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dimgrey:"696969",dodgerblue:"1e90ff",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"f0f",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",grey:"808080",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgray:"d3d3d3",lightgreen:"90ee90",lightgrey:"d3d3d3",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslategray:"789",lightslategrey:"789",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"0f0",limegreen:"32cd32",linen:"faf0e6",magenta:"f0f",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370db",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"db7093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",rebeccapurple:"663399",red:"f00",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",slategrey:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",wheat:"f5deb3",white:"fff",whitesmoke:"f5f5f5",yellow:"ff0",yellowgreen:"9acd32"};var hexNames=tinycolor.hexNames=flip(names);function flip(o){var flipped={};for(var i in o){if(o.hasOwnProperty(i)){flipped[o[i]]=i}}return flipped}function boundAlpha(a){a=parseFloat(a);if(isNaN(a)||a<0||a>1){a=1}return a}function bound01(n,max){if(isOnePointZero(n)){n="100%"}var processPercent=isPercentage(n);n=mathMin(max,mathMax(0,parseFloat(n)));if(processPercent){n=parseInt(n*max,10)/100}if(math.abs(n-max)<1e-6){return 1}return n%max/parseFloat(max)}function clamp01(val){return mathMin(1,mathMax(0,val))}function parseIntFromHex(val){return parseInt(val,16)}function isOnePointZero(n){return typeof n=="string"&&n.indexOf(".")!=-1&&parseFloat(n)===1}function isPercentage(n){return typeof n==="string"&&n.indexOf("%")!=-1}function pad2(c){return c.length==1?"0"+c:""+c}function convertToPercentage(n){if(n<=1){n=n*100+"%"}return n}function convertDecimalToHex(d){return Math.round(parseFloat(d)*255).toString(16)}function convertHexToDecimal(h){return parseIntFromHex(h)/255}var matchers=function(){var CSS_INTEGER="[-\\+]?\\d+%?";var CSS_NUMBER="[-\\+]?\\d*\\.\\d+%?";var CSS_UNIT="(?:"+CSS_NUMBER+")|(?:"+CSS_INTEGER+")";var PERMISSIVE_MATCH3="[\\s|\\(]+("+CSS_UNIT+")[,|\\s]+("+CSS_UNIT+")[,|\\s]+("+CSS_UNIT+")\\s*\\)?";var PERMISSIVE_MATCH4="[\\s|\\(]+("+CSS_UNIT+")[,|\\s]+("+CSS_UNIT+")[,|\\s]+("+CSS_UNIT+")[,|\\s]+("+CSS_UNIT+")\\s*\\)?";return{rgb:new RegExp("rgb"+PERMISSIVE_MATCH3),rgba:new RegExp("rgba"+PERMISSIVE_MATCH4),hsl:new RegExp("hsl"+PERMISSIVE_MATCH3),hsla:new RegExp("hsla"+PERMISSIVE_MATCH4),hsv:new RegExp("hsv"+PERMISSIVE_MATCH3),hsva:new RegExp("hsva"+PERMISSIVE_MATCH4),hex3:/^([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,hex6:/^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,hex8:/^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/}}();function stringInputToObject(color){color=color.replace(trimLeft,"").replace(trimRight,"").toLowerCase();var named=false;if(names[color]){color=names[color];named=true}else if(color=="transparent"){return{r:0,g:0,b:0,a:0,format:"name"}}var match;if(match=matchers.rgb.exec(color)){return{r:match[1],g:match[2],b:match[3]}}if(match=matchers.rgba.exec(color)){return{r:match[1],g:match[2],b:match[3],a:match[4]}}if(match=matchers.hsl.exec(color)){return{h:match[1],s:match[2],l:match[3]}}if(match=matchers.hsla.exec(color)){return{h:match[1],s:match[2],l:match[3],a:match[4]}}if(match=matchers.hsv.exec(color)){return{h:match[1],s:match[2],v:match[3]}}if(match=matchers.hsva.exec(color)){return{h:match[1],s:match[2],v:match[3],a:match[4]}}if(match=matchers.hex8.exec(color)){return{a:convertHexToDecimal(match[1]),r:parseIntFromHex(match[2]),g:parseIntFromHex(match[3]),b:parseIntFromHex(match[4]),format:named?"name":"hex8"}}if(match=matchers.hex6.exec(color)){return{r:parseIntFromHex(match[1]),g:parseIntFromHex(match[2]),b:parseIntFromHex(match[3]),format:named?"name":"hex"}}if(match=matchers.hex3.exec(color)){return{r:parseIntFromHex(match[1]+""+match[1]),g:parseIntFromHex(match[2]+""+match[2]),b:parseIntFromHex(match[3]+""+match[3]),format:named?"name":"hex"}}return false}window.tinycolor=tinycolor})();$(function(){if($.fn.spectrum.load){$.fn.spectrum.processNativeColorInputs()}})}); \ No newline at end of file diff --git a/spec/components/discourse_sass_compiler_spec.rb b/spec/components/discourse_sass_compiler_spec.rb deleted file mode 100644 index baa998cf3a..0000000000 --- a/spec/components/discourse_sass_compiler_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'rails_helper' -require_dependency 'sass/discourse_sass_compiler' - -describe DiscourseSassCompiler do - - let(:test_scss) { "body { p {color: blue;} }\n@import 'common/foundation/variables';\n@import 'plugins';" } - - describe '#compile' do - it "compiles scss" do - DiscoursePluginRegistry.stubs(:stylesheets).returns(["#{Rails.root}/spec/fixtures/scss/my_plugin.scss"]) - css = described_class.compile(test_scss, "test") - expect(css).to include("color") - expect(css).to include('my-plugin-thing') - end - - it "raises error for invalid scss" do - expect { - described_class.compile("this isn't valid scss", "test") - }.to raise_error(Sass::SyntaxError) - end - - it "doesn't load theme or plugins in safe mode" do - ColorScheme.expects(:enabled).never - DiscoursePluginRegistry.stubs(:stylesheets).returns(["#{Rails.root}/spec/fixtures/scss/my_plugin.scss"]) - css = described_class.compile(test_scss, "test", safe: true) - expect(css).not_to include('my-plugin-thing') - end - end - -end diff --git a/spec/components/discourse_stylesheets_spec.rb b/spec/components/discourse_stylesheets_spec.rb deleted file mode 100644 index 30562a5844..0000000000 --- a/spec/components/discourse_stylesheets_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'rails_helper' -require_dependency 'sass/discourse_stylesheets' - -describe DiscourseStylesheets do - - describe "compile" do - it "can compile desktop bundle" do - DiscoursePluginRegistry.stubs(:stylesheets).returns(["#{Rails.root}/spec/fixtures/scss/my_plugin.scss"]) - builder = described_class.new(:desktop) - expect(builder.compile(force: true)).to include('my-plugin-thing') - FileUtils.rm builder.stylesheet_fullpath - end - - it "can compile mobile bundle" do - DiscoursePluginRegistry.stubs(:mobile_stylesheets).returns(["#{Rails.root}/spec/fixtures/scss/my_plugin.scss"]) - builder = described_class.new(:mobile) - expect(builder.compile(force: true)).to include('my-plugin-thing') - FileUtils.rm builder.stylesheet_fullpath - end - - it "can fallback when css is bad" do - DiscoursePluginRegistry.stubs(:stylesheets).returns([ - "#{Rails.root}/spec/fixtures/scss/my_plugin.scss", - "#{Rails.root}/spec/fixtures/scss/broken.scss" - ]) - builder = described_class.new(:desktop) - expect(builder.compile(force: true)).not_to include('my-plugin-thing') - FileUtils.rm builder.stylesheet_fullpath - end - end - - describe "#digest" do - before do - described_class.expects(:max_file_mtime).returns(Time.new(2016, 06, 05, 12, 30, 0, 0)) - end - - it "should return a digest" do - expect(described_class.new.digest).to eq('0e6c2e957cfc92ed60661c90ec3345198ccef887') - end - - it "should include the cdn url when generating the digest" do - GlobalSetting.expects(:cdn_url).returns('https://fastly.maxcdn.org') - expect(described_class.new.digest).to eq('4995163b1232c54c8ed3b44200d803a90bc47613') - end - end -end diff --git a/spec/components/step_updater_spec.rb b/spec/components/step_updater_spec.rb index eed8e7f7f5..783d2a1d58 100644 --- a/spec/components/step_updater_spec.rb +++ b/spec/components/step_updater_spec.rb @@ -151,27 +151,31 @@ describe Wizard::StepUpdater do let!(:color_scheme) { Fabricate(:color_scheme, name: 'existing', via_wizard: true) } it "updates the scheme" do - updater = wizard.create_updater('colors', theme_id: 'dark') + updater = wizard.create_updater('colors', base_scheme_id: 'dark') updater.update expect(updater.success?).to eq(true) expect(wizard.completed_steps?('colors')).to eq(true) - color_scheme.reload - expect(color_scheme).to be_enabled + + theme = Theme.find_by(key: SiteSetting.default_theme_key) + expect(theme.color_scheme_id).to eq(color_scheme.id) + end end context "without an existing scheme" do it "creates the scheme" do - updater = wizard.create_updater('colors', theme_id: 'dark') + updater = wizard.create_updater('colors', base_scheme_id: 'dark') updater.update expect(updater.success?).to eq(true) expect(wizard.completed_steps?('colors')).to eq(true) color_scheme = ColorScheme.where(via_wizard: true).first expect(color_scheme).to be_present - expect(color_scheme).to be_enabled expect(color_scheme.colors).to be_present + + theme = Theme.find_by(key: SiteSetting.default_theme_key) + expect(theme.color_scheme_id).to eq(color_scheme.id) end end end diff --git a/spec/components/stylesheet/compiler_spec.rb b/spec/components/stylesheet/compiler_spec.rb new file mode 100644 index 0000000000..5bd19ce32d --- /dev/null +++ b/spec/components/stylesheet/compiler_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' +require 'stylesheet/compiler' + +describe Stylesheet::Compiler do + it "can compile desktop mobile and desktop css" do + css,_map = Stylesheet::Compiler.compile_asset("desktop") + expect(css.length).to be > 1000 + + css,_map = Stylesheet::Compiler.compile_asset("mobile") + expect(css.length).to be > 1000 + end + + it "supports asset-url" do + css,_map = Stylesheet::Compiler.compile(".body{background-image: asset-url('foo.png');}","test.scss") + + expect(css).to include("url('/foo.png')") + expect(css).not_to include('asset-url') + end +end + + diff --git a/spec/components/stylesheet/manager_spec.rb b/spec/components/stylesheet/manager_spec.rb new file mode 100644 index 0000000000..3098e6000c --- /dev/null +++ b/spec/components/stylesheet/manager_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' +require 'stylesheet/compiler' + +describe Stylesheet::Manager do + it 'can correctly compile theme css' do + theme = Theme.new( + name: 'parent', + user_id: -1 + ) + + theme.set_field(:common, "scss", ".common{.scss{color: red;}}") + theme.set_field(:desktop, "scss", ".desktop{.scss{color: red;}}") + theme.set_field(:mobile, "scss", ".mobile{.scss{color: red;}}") + theme.set_field(:common, "embedded_scss", ".embedded{.scss{color: red;}}") + + theme.save! + + + child_theme = Theme.new( + name: 'parent', + user_id: -1, + ) + + child_theme.set_field(:common, "scss", ".child_common{.scss{color: red;}}") + child_theme.set_field(:desktop, "scss", ".child_desktop{.scss{color: red;}}") + child_theme.set_field(:mobile, "scss", ".child_mobile{.scss{color: red;}}") + child_theme.set_field(:common, "embedded_scss", ".child_embedded{.scss{color: red;}}") + child_theme.save! + + theme.add_child_theme!(child_theme) + + old_link = Stylesheet::Manager.stylesheet_link_tag(:desktop_theme, 'all', theme.key) + + manager = Stylesheet::Manager.new(:desktop_theme, theme.key) + manager.compile(force: true) + + css = File.read(manager.stylesheet_fullpath) + _source_map = File.read(manager.source_map_fullpath) + + expect(css).to match(/child_common/) + expect(css).to match(/child_desktop/) + expect(css).to match(/\.common/) + expect(css).to match(/\.desktop/) + + + child_theme.set_field(:desktop, :scss, ".nothing{color: green;}") + child_theme.save! + + new_link = Stylesheet::Manager.stylesheet_link_tag(:desktop_theme, 'all', theme.key) + + expect(new_link).not_to eq(old_link) + + # our theme better have a name with the theme_id as part of it + expect(new_link).to include("/stylesheets/desktop_theme_#{theme.id}_") + end +end + diff --git a/spec/controllers/admin/color_schemes_controller_spec.rb b/spec/controllers/admin/color_schemes_controller_spec.rb index 9059330b44..c1eb2c9abb 100644 --- a/spec/controllers/admin/color_schemes_controller_spec.rb +++ b/spec/controllers/admin/color_schemes_controller_spec.rb @@ -9,7 +9,6 @@ describe Admin::ColorSchemesController do let!(:user) { log_in(:admin) } let(:valid_params) { { color_scheme: { name: 'Such Design', - enabled: true, colors: [ {name: 'primary', hex: 'FFBB00'}, {name: 'secondary', hex: '888888'} diff --git a/spec/controllers/admin/site_customizations_controller_spec.rb b/spec/controllers/admin/site_customizations_controller_spec.rb deleted file mode 100644 index 2695f17c7e..0000000000 --- a/spec/controllers/admin/site_customizations_controller_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'rails_helper' - -describe Admin::SiteCustomizationsController do - - it "is a subclass of AdminController" do - expect(Admin::UsersController < Admin::AdminController).to eq(true) - end - - context 'while logged in as an admin' do - before do - @user = log_in(:admin) - end - - context ' .index' do - it 'returns success' do - SiteCustomization.create!(name: 'my name', user_id: Fabricate(:user).id, header: "my awesome header", stylesheet: "my awesome css") - xhr :get, :index - expect(response).to be_success - end - - it 'returns JSON' do - xhr :get, :index - expect(::JSON.parse(response.body)).to be_present - end - end - - context ' .create' do - it 'returns success' do - xhr :post, :create, site_customization: {name: 'my test name'} - expect(response).to be_success - end - - it 'returns json' do - xhr :post, :create, site_customization: {name: 'my test name'} - expect(::JSON.parse(response.body)).to be_present - end - - it 'logs the change' do - StaffActionLogger.any_instance.expects(:log_site_customization_change).once - xhr :post, :create, site_customization: {name: 'my test name'} - end - end - - end - - - -end diff --git a/spec/controllers/admin/staff_action_logs_controller_spec.rb b/spec/controllers/admin/staff_action_logs_controller_spec.rb index a566571708..6f8b64feca 100644 --- a/spec/controllers/admin/staff_action_logs_controller_spec.rb +++ b/spec/controllers/admin/staff_action_logs_controller_spec.rb @@ -8,15 +8,35 @@ describe Admin::StaffActionLogsController do let!(:user) { log_in(:admin) } context '.index' do - before do + + it 'works' do xhr :get, :index + expect(response).to be_success + expect(::JSON.parse(response.body)).to be_a(Array) end + end - subject { response } - it { is_expected.to be_success } + context '.diff' do + it 'can generate diffs for theme changes' do + theme = Theme.new(user_id: -1, name: 'bob') + theme.set_field(:mobile, :scss, 'body {.up}') + theme.set_field(:common, :scss, 'omit-dupe') - it 'returns JSON' do - expect(::JSON.parse(subject.body)).to be_a(Array) + original_json = ThemeSerializer.new(theme, root: false).to_json + + theme.set_field(:mobile, :scss, 'body {.down}') + + record = StaffActionLogger.new(Discourse.system_user) + .log_theme_change(original_json, theme) + + xhr :get, :diff, id: record.id + expect(response).to be_success + + parsed = JSON.parse(response.body) + expect(parsed["side_by_side"]).to include("up") + expect(parsed["side_by_side"]).to include("down") + + expect(parsed["side_by_side"]).not_to include("omit-dupe") end end end diff --git a/spec/controllers/admin/themes_controller_spec.rb b/spec/controllers/admin/themes_controller_spec.rb new file mode 100644 index 0000000000..7595b54652 --- /dev/null +++ b/spec/controllers/admin/themes_controller_spec.rb @@ -0,0 +1,101 @@ +require 'rails_helper' + +describe Admin::ThemesController do + + it "is a subclass of AdminController" do + expect(Admin::UsersController < Admin::AdminController).to eq(true) + end + + context 'while logged in as an admin' do + before do + @user = log_in(:admin) + end + + context ' .index' do + it 'returns success' do + theme = Theme.new(name: 'my name', user_id: -1) + theme.set_field(:common, :scss, '.body{color: black;}') + theme.set_field(:desktop, :after_header, 'test') + + theme.remote_theme = RemoteTheme.new( + remote_url: 'awesome.git', + remote_version: '7', + local_version: '8', + remote_updated_at: Time.zone.now + ) + + theme.save! + + # this will get serialized as well + ColorScheme.create_from_base(name: "test", colors: []) + + xhr :get, :index + + expect(response).to be_success + + json = ::JSON.parse(response.body) + + expect(json["extras"]["color_schemes"].length).to eq(2) + theme_json = json["themes"].find{|t| t["id"] == theme.id} + expect(theme_json["theme_fields"].length).to eq(2) + expect(theme_json["remote_theme"]["remote_version"]).to eq("7") + end + end + + context ' .create' do + it 'creates a theme' do + xhr :post, :create, theme: {name: 'my test name', theme_fields: [name: 'scss', target: 'common', value: 'body{color: red;}']} + expect(response).to be_success + + json = ::JSON.parse(response.body) + + expect(json["theme"]["theme_fields"].length).to eq(1) + expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) + end + end + + context ' .update' do + it 'can change default theme' do + theme = Theme.create(name: 'my name', user_id: -1) + xhr :put, :update, id: theme.id, theme: { default: true } + expect(SiteSetting.default_theme_key).to eq(theme.key) + end + + it 'can unset default theme' do + theme = Theme.create(name: 'my name', user_id: -1) + SiteSetting.default_theme_key = theme.key + xhr :put, :update, id: theme.id, theme: { default: false} + expect(SiteSetting.default_theme_key).to be_blank + end + + it 'updates a theme' do + + theme = Theme.new(name: 'my name', user_id: -1) + theme.set_field(:common, :scss, '.body{color: black;}') + theme.save + + child_theme = Theme.create(name: 'my name', user_id: -1) + + xhr :put, :update, id: theme.id, + theme: { + child_theme_ids: [child_theme.id], + name: 'my test name', + theme_fields: [name: 'scss', target: 'common', value: 'body{color: red;}'] + } + expect(response).to be_success + + json = ::JSON.parse(response.body) + + fields = json["theme"]["theme_fields"] + + expect(fields.length).to eq(1) + expect(fields.first["value"]).to eq('body{color: red;}') + + expect(json["theme"]["child_themes"].length).to eq(1) + + expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) + end + end + end + +end diff --git a/spec/controllers/site_customizations_controller_spec.rb b/spec/controllers/site_customizations_controller_spec.rb deleted file mode 100644 index d3a0f17451..0000000000 --- a/spec/controllers/site_customizations_controller_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'rails_helper' - -describe SiteCustomizationsController do - - before do - SiteCustomization.clear_cache! - end - - it 'can deliver enabled css' do - SiteCustomization.create!(name: '1', - user_id: -1, - enabled: true, - mobile_stylesheet: '.a1{margin: 1px;}', - stylesheet: '.b1{margin: 1px;}' - ) - - SiteCustomization.create!(name: '2', - user_id: -1, - enabled: true, - mobile_stylesheet: '.a2{margin: 1px;}', - stylesheet: '.b2{margin: 1px;}' - ) - - get :show, key: SiteCustomization::ENABLED_KEY, format: :css, target: 'mobile' - expect(response.body).to match(/\.a1.*\.a2/m) - - get :show, key: SiteCustomization::ENABLED_KEY, format: :css - expect(response.body).to match(/\.b1.*\.b2/m) - end - - it 'can deliver specific css' do - c = SiteCustomization.create!(name: '1', - user_id: -1, - enabled: true, - mobile_stylesheet: '.a1{margin: 1px;}', - stylesheet: '.b1{margin: 1px;}' - ) - - get :show, key: c.key, format: :css, target: 'mobile' - expect(response.body).to match(/\.a1/) - - get :show, key: c.key, format: :css - expect(response.body).to match(/\.b1/) - end -end diff --git a/spec/controllers/stylesheets_controller_spec.rb b/spec/controllers/stylesheets_controller_spec.rb index 5a5f9cf05f..7351091f79 100644 --- a/spec/controllers/stylesheets_controller_spec.rb +++ b/spec/controllers/stylesheets_controller_spec.rb @@ -5,7 +5,7 @@ describe StylesheetsController do it 'can survive cache miss' do StylesheetCache.destroy_all - builder = DiscourseStylesheets.new('desktop_rtl') + builder = Stylesheet::Manager.new('desktop_rtl', nil) builder.compile builder.ensure_digestless_file @@ -26,7 +26,7 @@ describe StylesheetsController do expect(cached.digest).to eq digest # tmp folder destruction and cached - `rm #{DiscourseStylesheets.cache_fullpath}/*` + `rm #{Stylesheet::Manager.cache_fullpath}/*` get :show, name: 'desktop_rtl' expect(response).to be_success @@ -38,4 +38,31 @@ describe StylesheetsController do end + it 'can lookup theme specific css' do + scheme = ColorScheme.create_from_base({name: "testing", colors: []}) + theme = Theme.create!(name: "test", color_scheme_id: scheme.id, user_id: -1) + + builder = Stylesheet::Manager.new(:desktop, theme.key) + builder.compile + + `rm #{Stylesheet::Manager.cache_fullpath}/*` + + get :show, name: builder.stylesheet_filename.sub(".css", "") + expect(response).to be_success + + get :show, name: builder.stylesheet_filename_no_digest.sub(".css", "") + expect(response).to be_success + + builder = Stylesheet::Manager.new(:desktop_theme, theme.key) + builder.compile + + `rm #{Stylesheet::Manager.cache_fullpath}/*` + + get :show, name: builder.stylesheet_filename.sub(".css", "") + expect(response).to be_success + + get :show, name: builder.stylesheet_filename_no_digest.sub(".css", "") + expect(response).to be_success + end + end diff --git a/spec/fabricators/color_scheme_fabricator.rb b/spec/fabricators/color_scheme_fabricator.rb index 09bde58ef2..111964527a 100644 --- a/spec/fabricators/color_scheme_fabricator.rb +++ b/spec/fabricators/color_scheme_fabricator.rb @@ -1,5 +1,4 @@ Fabricator(:color_scheme) do name { sequence(:name) {|i| "Palette #{i}" } } - enabled false color_scheme_colors(count: 2) { |attrs, i| Fabricate.build(:color_scheme_color, color_scheme: nil) } end diff --git a/spec/models/color_scheme_spec.rb b/spec/models/color_scheme_spec.rb index 34f75e4d94..4d07dac97d 100644 --- a/spec/models/color_scheme_spec.rb +++ b/spec/models/color_scheme_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' describe ColorScheme do - let(:valid_params) { {name: "Best Colors Evar", enabled: true, colors: valid_colors} } + let(:valid_params) { {name: "Best Colors Evar", colors: valid_colors} } let(:valid_colors) { [ {name: '$primary_background_color', hex: 'FFBB00'}, {name: '$secondary_background_color', hex: '888888'} @@ -10,7 +10,7 @@ describe ColorScheme do describe "new" do it "can take colors" do - c = described_class.new(valid_params) + c = ColorScheme.new(valid_params) expect(c.colors.size).to eq valid_colors.size expect(c.colors.first).to be_a(ColorSchemeColor) expect { @@ -55,29 +55,4 @@ describe ColorScheme do end end end - - describe "destroy" do - it "also destroys old versions" do - c1 = described_class.create(valid_params.merge(version: 2)) - _c2 = described_class.create(valid_params.merge(versioned_id: c1.id, version: 1)) - _other = described_class.create(valid_params) - expect { - c1.destroy - }.to change { described_class.count }.by(-2) - end - end - - describe "#enabled" do - it "returns nil when there is no enabled record" do - expect(described_class.enabled).to eq nil - end - - it "returns the enabled color scheme" do - ColorScheme.hex_cache.clear - expect(described_class.hex_for_name('$primary_background_color')).to eq nil - c = described_class.create(valid_params.merge(enabled: true)) - expect(described_class.enabled.id).to eq c.id - expect(described_class.hex_for_name('$primary_background_color')).to eq "FFBB00" - end - end end diff --git a/spec/models/remote_theme_spec.rb b/spec/models/remote_theme_spec.rb new file mode 100644 index 0000000000..2cdda9cd5b --- /dev/null +++ b/spec/models/remote_theme_spec.rb @@ -0,0 +1,84 @@ +require 'rails_helper' + +describe RemoteTheme do + context '#import_remote' do + def setup_git_repo(files) + dir = Dir.tmpdir + repo_dir = "#{dir}/#{SecureRandom.hex}" + `mkdir #{repo_dir}` + `cd #{repo_dir} && git init .` + `cd #{repo_dir} && mkdir desktop mobile common` + files.each do |name, data| + File.write("#{repo_dir}/#{name}", data) + `cd #{repo_dir} && git add #{name}` + end + `cd #{repo_dir} && git commit -am 'first commit'` + repo_dir + end + + let :initial_repo do + setup_git_repo( + "about.json" => '{ + "name": "awesome theme", + "about_url": "https://www.site.com/about", + "license_url": "https://www.site.com/license" + }', + "desktop/desktop.scss" => "body {color: red;}", + "common/header.html" => "I AM HEADER", + "common/random.html" => "I AM SILLY", + ) + end + + after do + `rm -fr #{initial_repo}` + end + + it 'can correctly import a remote theme' do + + time = Time.new('2000') + freeze_time time + + @theme = RemoteTheme.import_theme(initial_repo) + remote = @theme.remote_theme + + expect(@theme.name).to eq('awesome theme') + expect(remote.remote_url).to eq(initial_repo) + expect(remote.remote_version).to eq(`cd #{initial_repo} && git rev-parse HEAD`.strip) + expect(remote.local_version).to eq(`cd #{initial_repo} && git rev-parse HEAD`.strip) + + 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(2) + + mapped = Hash[*@theme.theme_fields.map{|f| ["#{f.target}-#{f.name}", f.value]}.flatten] + + expect(mapped["0-header"]).to eq("I AM HEADER") + expect(mapped["1-scss"]).to eq("body {color: red;}") + + expect(remote.remote_updated_at).to eq(time) + + File.write("#{initial_repo}/common/header.html", "I AM UPDATED") + `cd #{initial_repo} && git commit -am "update"` + + time = Time.new('2001') + freeze_time time + + remote.update_remote_version + expect(remote.commits_behind).to eq(1) + expect(remote.remote_version).to eq(`cd #{initial_repo} && git rev-parse HEAD`.strip) + + + remote.update_from_remote + @theme.save + @theme.reload + + mapped = Hash[*@theme.theme_fields.map{|f| ["#{f.target}-#{f.name}", f.value]}.flatten] + + expect(mapped["0-header"]).to eq("I AM UPDATED") + expect(mapped["1-scss"]).to eq("body {color: red;}") + expect(remote.remote_updated_at).to eq(time) + + end + end +end diff --git a/spec/models/site_customization_spec.rb b/spec/models/site_customization_spec.rb deleted file mode 100644 index e762c78d54..0000000000 --- a/spec/models/site_customization_spec.rb +++ /dev/null @@ -1,155 +0,0 @@ -require 'rails_helper' - -describe SiteCustomization do - - before do - SiteCustomization.clear_cache! - end - - let :user do - Fabricate(:user) - end - - let :customization_params do - {name: 'my name', user_id: user.id, header: "my awesome header", stylesheet: "my awesome css", mobile_stylesheet: nil, mobile_header: nil} - end - - let :customization do - SiteCustomization.create!(customization_params) - end - - let :customization_with_mobile do - SiteCustomization.create!(customization_params.merge(mobile_stylesheet: ".mobile {better: true;}", mobile_header: "fancy mobile stuff")) - end - - it 'should set default key when creating a new customization' do - s = SiteCustomization.create!(name: 'my name', user_id: user.id) - expect(s.key).not_to eq(nil) - end - - it 'can enable more than one style at once' do - c1 = SiteCustomization.create!(name: '2', user_id: user.id, header: 'World', - enabled: true, mobile_header: 'hi', footer: 'footer', - stylesheet: '.hello{.world {color: blue;}}') - - SiteCustomization.create!(name: '1', user_id: user.id, header: 'Hello', - enabled: true, mobile_footer: 'mfooter', - mobile_stylesheet: '.hello{margin: 1px;}', - stylesheet: 'p{width: 1px;}' - ) - - expect(SiteCustomization.custom_header).to eq("Hello\nWorld") - expect(SiteCustomization.custom_header(nil, :mobile)).to eq("hi") - expect(SiteCustomization.custom_footer(nil, :mobile)).to eq("mfooter") - expect(SiteCustomization.custom_footer).to eq("footer") - - desktop_css = SiteCustomization.custom_stylesheet - expect(desktop_css).to match(Regexp.new("#{SiteCustomization::ENABLED_KEY}.css\\?target=desktop")) - - mobile_css = SiteCustomization.custom_stylesheet(nil, :mobile) - expect(mobile_css).to match(Regexp.new("#{SiteCustomization::ENABLED_KEY}.css\\?target=mobile")) - - expect(SiteCustomization.enabled_stylesheet_contents).to match(/\.hello \.world/) - - # cache expiry - c1.enabled = false - c1.save - - expect(SiteCustomization.custom_stylesheet).not_to eq(desktop_css) - expect(SiteCustomization.enabled_stylesheet_contents).not_to match(/\.hello \.world/) - end - - it 'should be able to look up stylesheets by key' do - c = SiteCustomization.create!(name: '2', user_id: user.id, - enabled: true, - stylesheet: '.hello{.world {color: blue;}}', - mobile_stylesheet: '.world{.hello{color: black;}}') - - expect(SiteCustomization.custom_stylesheet(c.key, :mobile)).to match(Regexp.new("#{c.key}.css\\?target=mobile")) - expect(SiteCustomization.custom_stylesheet(c.key)).to match(Regexp.new("#{c.key}.css\\?target=desktop")) - - end - - - it 'should allow including discourse styles' do - c = SiteCustomization.create!(user_id: user.id, name: "test", stylesheet: '@import "desktop";', mobile_stylesheet: '@import "mobile";') - expect(c.stylesheet_baked).not_to match(/Syntax error/) - expect(c.stylesheet_baked.length).to be > 1000 - expect(c.mobile_stylesheet_baked).not_to match(/Syntax error/) - expect(c.mobile_stylesheet_baked.length).to be > 1000 - end - - it 'should provide an awesome error on failure' do - c = SiteCustomization.create!(user_id: user.id, name: "test", stylesheet: "$black: #000; #a { color: $black; }\n\n\nboom", header: '') - expect(c.stylesheet_baked).to match(/Syntax error/) - expect(c.mobile_stylesheet_baked).not_to be_present - end - - it 'should provide an awesome error on failure for mobile too' do - c = SiteCustomization.create!(user_id: user.id, name: "test", stylesheet: '', header: '', mobile_stylesheet: "$black: #000; #a { color: $black; }\n\n\nboom", mobile_header: '') - expect(c.mobile_stylesheet_baked).to match(/Syntax error/) - expect(c.stylesheet_baked).not_to be_present - end - - it 'should correct bad html in body_tag_baked and head_tag_baked' do - c = SiteCustomization.create!(user_id: -1, name: "test", head_tag: "I am bold", body_tag: "I am bold") - expect(c.head_tag_baked).to eq("I am bold") - expect(c.body_tag_baked).to eq("I am bold") - end - - it 'should precompile fragments in body and head tags' do - with_template = < - {{hello}} - - -HTML - c = SiteCustomization.create!(user_id: -1, name: "test", head_tag: with_template, body_tag: with_template) - expect(c.head_tag_baked).to match(/HTMLBars/) - expect(c.body_tag_baked).to match(/HTMLBars/) - expect(c.body_tag_baked).to match(/raw-handlebars/) - expect(c.head_tag_baked).to match(/raw-handlebars/) - end - - it 'should create body_tag_baked on demand if needed' do - c = SiteCustomization.create!(user_id: -1, name: "test", head_tag: "test", enabled: true) - c.update_columns(head_tag_baked: nil) - expect(SiteCustomization.custom_head_tag).to match(/test<\/b>/) - end - - context "plugin api" do - def transpile(html) - c = SiteCustomization.create!(user_id: -1, name: "test", head_tag: html, body_tag: html) - c.head_tag_baked - end - - it "transpiles ES6 code" do - html = < - const x = 1; - -HTML - - transpiled = transpile(html) - expect(transpiled).to match(/\/) - expect(transpiled).to match(/var x = 1;/) - expect(transpiled).to match(/_registerPluginCode\('0.1'/) - end - - it "converts errors to a script type that is not evaluated" do - html = < - const x = 1; - x = 2; - -HTML - - transpiled = transpile(html) - expect(transpiled).to match(/text\/discourse-js-error/) - expect(transpiled).to match(/read-only/) - end - end - -end diff --git a/spec/models/site_spec.rb b/spec/models/site_spec.rb index d8bb189b34..8899269c80 100644 --- a/spec/models/site_spec.rb +++ b/spec/models/site_spec.rb @@ -2,6 +2,44 @@ require 'rails_helper' require_dependency 'site' describe Site do + + def expect_correct_themes(guardian) + json = Site.json_for(guardian) + parsed = JSON.parse(json) + + expected = Theme.where('key = :default OR user_selectable', + default: SiteSetting.default_theme_key) + .order(:name) + .pluck(:key, :name) + .map{|k,n| {"theme_key" => k, "name" => n, "default" => k == SiteSetting.default_theme_key}} + + expect(parsed["user_themes"]).to eq(expected) + end + + it "includes user themes and expires them as needed" do + default_theme = Theme.create!(user_id: -1, name: 'default') + SiteSetting.default_theme_key = default_theme.key + user_theme = Theme.create!(user_id: -1, name: 'user theme', user_selectable: true) + + anon_guardian = Guardian.new + user_guardian = Guardian.new(Fabricate(:user)) + + expect_correct_themes(anon_guardian) + expect_correct_themes(user_guardian) + + Theme.clear_default! + + expect_correct_themes(anon_guardian) + expect_correct_themes(user_guardian) + + user_theme.user_selectable = false + user_theme.save! + + expect_correct_themes(anon_guardian) + expect_correct_themes(user_guardian) + + end + it "omits categories users can not write to from the category list" do category = Fabricate(:category) user = Fabricate(:user) diff --git a/spec/models/stylesheet_cache_spec.rb b/spec/models/stylesheet_cache_spec.rb index eb52d07bcc..2c49fc36ba 100644 --- a/spec/models/stylesheet_cache_spec.rb +++ b/spec/models/stylesheet_cache_spec.rb @@ -5,7 +5,7 @@ describe StylesheetCache do describe "add" do it "correctly cycles once MAX_TO_KEEP is hit" do (StylesheetCache::MAX_TO_KEEP + 1).times do |i| - StylesheetCache.add("a", "d" + i.to_s, "c" + i.to_s) + StylesheetCache.add("a", "d" + i.to_s, "c" + i.to_s, "map") end expect(StylesheetCache.count).to eq StylesheetCache::MAX_TO_KEEP @@ -13,8 +13,8 @@ describe StylesheetCache do end it "does nothing if digest is set and already exists" do - StylesheetCache.add("a", "b", "c") - StylesheetCache.add("a", "b", "cc") + StylesheetCache.add("a", "b", "c", "map") + StylesheetCache.add("a", "b", "cc", "map") expect(StylesheetCache.count).to eq 1 expect(StylesheetCache.first.content).to eq "c" diff --git a/spec/models/theme_spec.rb b/spec/models/theme_spec.rb new file mode 100644 index 0000000000..6e11d7a5a5 --- /dev/null +++ b/spec/models/theme_spec.rb @@ -0,0 +1,141 @@ +require 'rails_helper' + +describe Theme do + + before do + Theme.clear_cache! + end + + let :user do + Fabricate(:user) + end + + let :customization_params do + {name: 'my name', user_id: user.id, header: "my awesome header"} + end + + let :customization do + Theme.create!(customization_params) + end + + it 'should set default key when creating a new customization' do + s = Theme.create!(name: 'my name', user_id: user.id) + expect(s.key).not_to eq(nil) + end + + it 'can support child themes' do + child = Theme.new(name: '2', user_id: user.id) + + child.set_field(:common, "header", "World") + child.set_field(:desktop, "header", "Desktop") + child.set_field(:mobile, "header", "Mobile") + + child.save! + + expect(Theme.lookup_field(child.key, :desktop, "header")).to eq("World\nDesktop") + expect(Theme.lookup_field(child.key, "mobile", :header)).to eq("World\nMobile") + + + child.set_field(:common, "header", "Worldie") + child.save! + + expect(Theme.lookup_field(child.key, :mobile, :header)).to eq("Worldie\nMobile") + + parent = Theme.new(name: '1', user_id: user.id) + + parent.set_field(:common, "header", "Common Parent") + parent.set_field(:mobile, "header", "Mobile Parent") + + parent.save! + + parent.add_child_theme!(child) + + expect(Theme.lookup_field(parent.key, :mobile, "header")).to eq("Common Parent\nMobile Parent\nWorldie\nMobile") + + end + + it 'can correctly find parent themes' do + grandchild = Theme.create!(name: 'grandchild', user_id: user.id) + child = Theme.create!(name: 'child', user_id: user.id) + theme = Theme.create!(name: 'theme', user_id: user.id) + + theme.add_child_theme!(child) + child.add_child_theme!(grandchild) + + expect(grandchild.dependant_themes.length).to eq(2) + end + + + 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(:common, "head_tag", "I am bold") + theme.save! + + expect(Theme.lookup_field(theme.key, :desktop, "head_tag")).to eq("I am bold") + end + + it 'should precompile fragments in body and head tags' do + with_template = < + {{hello}} + + +HTML + theme = Theme.new(user_id: -1, name: "test") + theme.set_field(:common, "header", with_template) + theme.save! + + baked = Theme.lookup_field(theme.key, :mobile, "header") + + expect(baked).to match(/HTMLBars/) + expect(baked).to match(/raw-handlebars/) + end + + it 'should create body_tag_baked on demand if needed' do + + theme = Theme.new(user_id: -1, name: "test") + theme.set_field(:common, :body_tag, "test") + theme.save + + ThemeField.update_all(value_baked: nil) + + expect(Theme.lookup_field(theme.key, :desktop, :body_tag)).to match(/test<\/b>/) + end + + context "plugin api" do + def transpile(html) + f = ThemeField.create!(target: Theme.targets[:mobile], theme_id: -1, name: "after_header", value: html) + f.value_baked + end + + it "transpiles ES6 code" do + html = < + const x = 1; + +HTML + + transpiled = transpile(html) + expect(transpiled).to match(/\/) + expect(transpiled).to match(/var x = 1;/) + expect(transpiled).to match(/_registerPluginCode\('0.1'/) + end + + it "converts errors to a script type that is not evaluated" do + html = < + const x = 1; + x = 2; + +HTML + + transpiled = transpile(html) + expect(transpiled).to match(/text\/discourse-js-error/) + expect(transpiled).to match(/read-only/) + end + end + + +end diff --git a/spec/services/color_scheme_revisor_spec.rb b/spec/services/color_scheme_revisor_spec.rb index d9f8a3519a..c2dc09d2c7 100644 --- a/spec/services/color_scheme_revisor_spec.rb +++ b/spec/services/color_scheme_revisor_spec.rb @@ -3,62 +3,42 @@ require 'rails_helper' describe ColorSchemeRevisor do let(:color) { Fabricate.build(:color_scheme_color, hex: 'FFFFFF', color_scheme: nil) } - let(:color_scheme) { Fabricate(:color_scheme, enabled: false, created_at: 1.day.ago, updated_at: 1.day.ago, color_scheme_colors: [color]) } - let(:valid_params) { { name: color_scheme.name, enabled: color_scheme.enabled, colors: nil } } + let(:color_scheme) { Fabricate(:color_scheme, created_at: 1.day.ago, updated_at: 1.day.ago, color_scheme_colors: [color]) } + let(:valid_params) { { name: color_scheme.name, colors: nil } } describe "revise" do it "does nothing if there are no changes" do expect { - described_class.revise(color_scheme, valid_params.merge(colors: nil)) + ColorSchemeRevisor.revise(color_scheme, valid_params.merge(colors: nil)) }.to_not change { color_scheme.reload.updated_at } end it "can change the name" do - described_class.revise(color_scheme, valid_params.merge(name: "Changed Name")) + ColorSchemeRevisor.revise(color_scheme, valid_params.merge(name: "Changed Name")) expect(color_scheme.reload.name).to eq("Changed Name") end - it "can update the theme_id" do - described_class.revise(color_scheme, valid_params.merge(theme_id: 'test')) - expect(color_scheme.reload.theme_id).to eq('test') + it "can update the base_scheme_id" do + ColorSchemeRevisor.revise(color_scheme, valid_params.merge(base_scheme_id: 'test')) + expect(color_scheme.reload.base_scheme_id).to eq('test') end - it "can enable and disable" do - described_class.revise(color_scheme, valid_params.merge(enabled: true)) - expect(color_scheme.reload).to be_enabled - described_class.revise(color_scheme, valid_params.merge(enabled: false)) - expect(color_scheme.reload).not_to be_enabled - end - - def test_color_change(color_scheme_arg, expected_enabled) - described_class.revise(color_scheme_arg, valid_params.merge(colors: [ - {name: color.name, hex: 'BEEF99'} + it 'can change colors' do + ColorSchemeRevisor.revise(color_scheme, valid_params.merge(colors: [ + {name: color.name, hex: 'BEEF99'}, + {name: 'bob', hex: 'AAAAAA'} ])) - color_scheme_arg.reload - expect(color_scheme_arg.enabled).to eq(expected_enabled) - expect(color_scheme_arg.colors.size).to eq(1) - expect(color_scheme_arg.colors.first.hex).to eq('BEEF99') - end + color_scheme.reload - it "can change colors of a color scheme that's not enabled" do - test_color_change(color_scheme, false) - end - - it "can change colors of the enabled color scheme" do - color_scheme.update_attribute(:enabled, true) - test_color_change(color_scheme, true) - end - - it "disables other color scheme before enabling" do - prev_enabled = Fabricate(:color_scheme, enabled: true) - described_class.revise(color_scheme, valid_params.merge(enabled: true)) - expect(prev_enabled.reload.enabled).to eq(false) - expect(color_scheme.reload.enabled).to eq(true) + expect(color_scheme.version).to eq(2) + expect(color_scheme.colors.size).to eq(2) + expect(color_scheme.colors.find_by(name: color.name).hex).to eq('BEEF99') + expect(color_scheme.colors.find_by(name: 'bob').hex).to eq('AAAAAA') end it "doesn't make changes when a color is invalid" do expect { - cs = described_class.revise(color_scheme, valid_params.merge(colors: [ + cs = ColorSchemeRevisor.revise(color_scheme, valid_params.merge(colors: [ {name: color.name, hex: 'OOPS'} ])) expect(cs).not_to be_valid @@ -66,72 +46,6 @@ describe ColorSchemeRevisor do }.to_not change { color_scheme.reload.version } expect(color_scheme.colors.first.hex).to eq(color.hex) end - - describe "versions" do - it "doesn't create a new version if colors is not given" do - expect { - described_class.revise(color_scheme, valid_params.merge(name: "Changed Name")) - }.to_not change { color_scheme.reload.version } - end - - it "creates a new version if colors have changed" do - old_hex = color.hex - expect { - described_class.revise(color_scheme, valid_params.merge(colors: [ - {name: color.name, hex: 'BEEF99'} - ])) - }.to change { color_scheme.reload.version }.by(1) - old_version = ColorScheme.find_by(versioned_id: color_scheme.id, version: (color_scheme.version - 1)) - expect(old_version).not_to eq(nil) - expect(old_version.colors.count).to eq(color_scheme.colors.count) - expect(old_version.colors_by_name[color.name].hex).to eq(old_hex) - expect(color_scheme.colors_by_name[color.name].hex).to eq('BEEF99') - end - - it "doesn't create a new version if colors have not changed" do - expect { - described_class.revise(color_scheme, valid_params.merge(colors: [ - {name: color.name, hex: color.hex} - ])) - }.to_not change { color_scheme.reload.version } - end - end - end - - describe "revert" do - context "when there are no previous versions" do - it "does nothing" do - expect { - expect(described_class.revert(color_scheme)).to eq(color_scheme) - }.to_not change { color_scheme.reload.version } - end - end - - context 'when there are previous versions' do - let(:new_color_params) { {name: color.name, hex: 'BEEF99'} } - - before do - @prev_hex = color.hex - described_class.revise(color_scheme, valid_params.merge(colors: [ new_color_params ])) - end - - it "reverts the colors to the previous version" do - expect(color_scheme.colors_by_name[new_color_params[:name]].hex).to eq(new_color_params[:hex]) - expect { - described_class.revert(color_scheme) - }.to change { color_scheme.reload.version }.by(-1) - expect(color_scheme.colors.size).to eq(1) - expect(color_scheme.colors.first.hex).to eq(@prev_hex) - expect(color_scheme.colors_by_name[new_color_params[:name]].hex).to eq(@prev_hex) - end - - it "destroys the old version's record" do - expect { - described_class.revert(color_scheme) - }.to change { ColorScheme.count }.by(-1) - expect(color_scheme.reload.previous_version).to eq(nil) - end - end end end diff --git a/spec/services/staff_action_logger_spec.rb b/spec/services/staff_action_logger_spec.rb index 26d7402c2a..2be67fbfbf 100644 --- a/spec/services/staff_action_logger_spec.rb +++ b/spec/services/staff_action_logger_spec.rb @@ -129,46 +129,56 @@ describe StaffActionLogger do end end - describe "log_site_customization_change" do - let(:valid_params) { {name: 'Cool Theme', stylesheet: "body {\n background-color: blue;\n}\n", header: "h1 {color: white;}"} } + describe "log_theme_change" do it "raises an error when params are invalid" do - expect { logger.log_site_customization_change(nil, nil) }.to raise_error(Discourse::InvalidParameters) + expect { logger.log_theme_change(nil, nil) }.to raise_error(Discourse::InvalidParameters) + end + + let :theme do + Theme.new(name: 'bob', user_id: -1) end it "logs new site customizations" do - log_record = logger.log_site_customization_change(nil, valid_params) - expect(log_record.subject).to eq(valid_params[:name]) + + log_record = logger.log_theme_change(nil, theme) + expect(log_record.subject).to eq(theme.name) expect(log_record.previous_value).to eq(nil) expect(log_record.new_value).to be_present + json = ::JSON.parse(log_record.new_value) - expect(json['stylesheet']).to be_present - expect(json['header']).to be_present + expect(json['name']).to eq(theme.name) end it "logs updated site customizations" do - existing = SiteCustomization.new(name: 'Banana', stylesheet: "body {color: yellow;}", header: "h1 {color: brown;}") - log_record = logger.log_site_customization_change(existing, valid_params) + old_json = ThemeSerializer.new(theme, root:false).to_json + + theme.set_field(:common, :scss, "body{margin: 10px;}") + + log_record = logger.log_theme_change(old_json, theme) + expect(log_record.previous_value).to be_present - json = ::JSON.parse(log_record.previous_value) - expect(json['stylesheet']).to eq(existing.stylesheet) - expect(json['header']).to eq(existing.header) + + json = ::JSON.parse(log_record.new_value) + expect(json['theme_fields']).to eq([{"name" => "scss", "target" => "common", "value" => "body{margin: 10px;}"}]) end end - describe "log_site_customization_destroy" do + describe "log_theme_destroy" do it "raises an error when params are invalid" do - expect { logger.log_site_customization_destroy(nil) }.to raise_error(Discourse::InvalidParameters) + expect { logger.log_theme_destroy(nil) }.to raise_error(Discourse::InvalidParameters) end it "creates a new UserHistory record" do - site_customization = SiteCustomization.new(name: 'Banana', stylesheet: "body {color: yellow;}", header: "h1 {color: brown;}") - log_record = logger.log_site_customization_destroy(site_customization) + theme = Theme.new(name: 'Banana') + theme.set_field(:common, :scss, "body{margin: 10px;}") + + log_record = logger.log_theme_destroy(theme) expect(log_record.previous_value).to be_present expect(log_record.new_value).to eq(nil) json = ::JSON.parse(log_record.previous_value) - expect(json['stylesheet']).to eq(site_customization.stylesheet) - expect(json['header']).to eq(site_customization.header) + + expect(json['theme_fields']).to eq([{"name" => "scss", "target" => "common", "value" => "body{margin: 10px;}"}]) end end diff --git a/test/stylesheets/test_helper.css b/test/stylesheets/test_helper.css index f0e5ac700c..f9eddae24d 100644 --- a/test/stylesheets/test_helper.css +++ b/test/stylesheets/test_helper.css @@ -1,7 +1,5 @@ -/* - *= require desktop - *= require_tree . -*/ +@import '/stylesheets/desktop.css'; + .modal-backdrop { display: none; } From 95d095c97db92f7f90d50624be376639c5bc5467 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 12 Apr 2017 11:19:47 -0400 Subject: [PATCH 029/221] test should be configuring git info --- spec/models/remote_theme_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/models/remote_theme_spec.rb b/spec/models/remote_theme_spec.rb index 2cdda9cd5b..dcc6289c9d 100644 --- a/spec/models/remote_theme_spec.rb +++ b/spec/models/remote_theme_spec.rb @@ -6,7 +6,9 @@ describe RemoteTheme do dir = Dir.tmpdir repo_dir = "#{dir}/#{SecureRandom.hex}" `mkdir #{repo_dir}` - `cd #{repo_dir} && git init .` + `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` files.each do |name, data| File.write("#{repo_dir}/#{name}", data) From a7ed8a0310acbf768ffcb76f98bce838a2113dac Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 12 Apr 2017 11:30:16 -0400 Subject: [PATCH 030/221] correct theme importer to support embedded.scss --- app/models/remote_theme.rb | 11 ++++++++++- app/models/theme.rb | 2 +- spec/models/remote_theme_spec.rb | 4 +++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/models/remote_theme.rb b/app/models/remote_theme.rb index 34df065799..7ddc34d733 100644 --- a/app/models/remote_theme.rb +++ b/app/models/remote_theme.rb @@ -44,7 +44,16 @@ class RemoteTheme < ActiveRecord::Base Theme.targets.keys.each do |target| Theme::ALLOWED_FIELDS.each do |field| - value = importer["#{target}/#{field=="scss"?"#{target}.scss":"#{field}.html"}"] + lookup = + if field == "scss" + "#{target}.scss" + elsif field == "embedded_scss" && target == :common + "embedded.scss" + else + "#{field}.html" + end + + value = importer["#{target}/#{lookup}"] theme.set_field(target.to_sym, field, value) end end diff --git a/app/models/theme.rb b/app/models/theme.rb index 32308c662f..f04cace13a 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -4,7 +4,7 @@ require_dependency 'stylesheet/manager' class Theme < ActiveRecord::Base - ALLOWED_FIELDS = %w{scss head_tag header after_header body_tag footer} + ALLOWED_FIELDS = %w{scss embedded_scss head_tag header after_header body_tag footer} @cache = DistributedCache.new('theme') diff --git a/spec/models/remote_theme_spec.rb b/spec/models/remote_theme_spec.rb index dcc6289c9d..e039216e26 100644 --- a/spec/models/remote_theme_spec.rb +++ b/spec/models/remote_theme_spec.rb @@ -28,6 +28,7 @@ describe RemoteTheme do "desktop/desktop.scss" => "body {color: red;}", "common/header.html" => "I AM HEADER", "common/random.html" => "I AM SILLY", + "common/embedded.scss" => "EMBED", ) end @@ -51,12 +52,13 @@ describe RemoteTheme do 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(2) + expect(@theme.theme_fields.length).to eq(3) mapped = Hash[*@theme.theme_fields.map{|f| ["#{f.target}-#{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["0-embedded_scss"]).to eq("EMBED") expect(remote.remote_updated_at).to eq(time) From 7f2a80bbc8cd089d0d7c0f36142e04312faa4155 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 12 Apr 2017 11:59:47 -0400 Subject: [PATCH 031/221] correct theme selector expiry selection --- app/assets/javascripts/discourse/lib/theme-selector.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/theme-selector.js.es6 b/app/assets/javascripts/discourse/lib/theme-selector.js.es6 index de7a81daf9..295f8664dd 100644 --- a/app/assets/javascripts/discourse/lib/theme-selector.js.es6 +++ b/app/assets/javascripts/discourse/lib/theme-selector.js.es6 @@ -13,9 +13,9 @@ function currentThemeKey() { export function selectDefaultTheme(key) { if (key) { - $.cookie('preview_style', key); + $.cookie('preview_style', key, {path: '/', expires: 9999}); } else { - $.cookie('preview_style', null); + $.cookie('preview_style', null, {path: '/', expires: 1}); } } From 8cd9afcfadbaa47d47e694ccd54041a3b52dd345 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 12 Apr 2017 12:30:24 -0400 Subject: [PATCH 032/221] move CSS precompilation to last step --- lib/stylesheet/manager.rb | 9 ++++----- lib/tasks/assets.rake | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/stylesheet/manager.rb b/lib/stylesheet/manager.rb index f0f549cabb..ff4791bb77 100644 --- a/lib/stylesheet/manager.rb +++ b/lib/stylesheet/manager.rb @@ -71,13 +71,12 @@ class Stylesheet::Manager end def self.max_file_mtime - globs = ["#{Rails.root}/app/assets/stylesheets/**/*.*css"] + globs = ["#{Rails.root}/app/assets/stylesheets/**/*.*css", + "#{Rails.root}/app/assets/images/**/*.*"] Discourse.plugins.map { |plugin| File.dirname(plugin.path) }.each do |path| - globs += [ - "#{path}/plugin.rb", - "#{path}/**/*.*css", - ] + globs << "#{path}/plugin.rb" + globs << "#{path}/**/*.*css" end globs.map do |pattern| diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake index 16a6e403ae..2183d79098 100644 --- a/lib/tasks/assets.rake +++ b/lib/tasks/assets.rake @@ -132,8 +132,6 @@ def concurrent? end task 'assets:precompile' => 'assets:precompile:before' do - # Run after assets:precompile - Rake::Task["assets:precompile:css"].invoke if $bypass_sprockets_uglify puts "Compressing Javascript and Generating Source Maps" @@ -184,6 +182,8 @@ task 'assets:precompile' => 'assets:precompile:before' do end + Rake::Task["assets:precompile:css"].invoke + end end From 513f1f065a085029379835e6e1f4c4d521f63329 Mon Sep 17 00:00:00 2001 From: ckeboss Date: Wed, 12 Apr 2017 09:41:23 -0700 Subject: [PATCH 033/221] Wrap directory names in quotes When there are spaces in any of the directories referenced, bash will error out. This wraps those items in quotes to allow bash to parse the path names properly. --- bin/docker/boot_dev | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/docker/boot_dev b/bin/docker/boot_dev index be31b320fd..93d1fbc415 100755 --- a/bin/docker/boot_dev +++ b/bin/docker/boot_dev @@ -1,7 +1,7 @@ #!/bin/bash SCRIPTPATH=$(cd "$(dirname "$0")"; pwd -P) SOURCE_DIR=$(cd "$SCRIPTPATH" && cd ../.. && pwd -P) -DATA_DIR=$SOURCE_DIR/tmp/postgres +DATA_DIR="$SOURCE_DIR/tmp/postgres" show_help() { cat < Date: Wed, 12 Apr 2017 12:46:03 -0400 Subject: [PATCH 034/221] force recompilation of css assets --- lib/stylesheet/manager.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/stylesheet/manager.rb b/lib/stylesheet/manager.rb index ff4791bb77..15ccaee1f2 100644 --- a/lib/stylesheet/manager.rb +++ b/lib/stylesheet/manager.rb @@ -48,8 +48,13 @@ class Stylesheet::Manager themes << nil themes.each do |key,name| [:desktop, :mobile, :desktop_rtl, :mobile_rtl].each do |target| + theme_key = key || SiteSetting.default_theme_key + cache_key = "#{target}_#{theme_key}" + STDERR.puts "precompile target: #{target} #{name}" - stylesheet_link_tag(target, nil, key) + builder = self.new(target, theme_key) + builder.compile(force: true) + cache[cache_key] = nil end end nil From d0fa8cda7658dda72fc20de2a73c7e4313221e2d Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 12 Apr 2017 13:00:25 -0400 Subject: [PATCH 035/221] correct issue where no selected theme would force incorrect selection --- app/assets/javascripts/discourse/lib/theme-selector.js.es6 | 2 +- app/assets/javascripts/discourse/routes/preferences.js.es6 | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/theme-selector.js.es6 b/app/assets/javascripts/discourse/lib/theme-selector.js.es6 index 295f8664dd..40cb9eabe5 100644 --- a/app/assets/javascripts/discourse/lib/theme-selector.js.es6 +++ b/app/assets/javascripts/discourse/lib/theme-selector.js.es6 @@ -2,7 +2,7 @@ import { ajax } from 'discourse/lib/ajax'; import { refreshCSS } from 'discourse/initializers/live-development'; const keySelector = 'meta[name=discourse_theme_key]'; -function currentThemeKey() { +export function currentThemeKey() { let themeKey = null; let elem = _.first($(keySelector)); if (elem) { diff --git a/app/assets/javascripts/discourse/routes/preferences.js.es6 b/app/assets/javascripts/discourse/routes/preferences.js.es6 index 6de4f04767..9bb0d645b1 100644 --- a/app/assets/javascripts/discourse/routes/preferences.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences.js.es6 @@ -1,6 +1,7 @@ import RestrictedUserRoute from "discourse/routes/restricted-user"; import showModal from 'discourse/lib/show-modal'; import { popupAjaxError } from 'discourse/lib/ajax-error'; +import { currentThemeKey } from 'discourse/lib/theme-selector'; export default RestrictedUserRoute.extend({ model() { @@ -12,7 +13,7 @@ export default RestrictedUserRoute.extend({ controller.setProperties({ model: user, newNameInput: user.get('name'), - selectedTheme: $.cookie('preview_style') + selectedTheme: $.cookie('preview_style') || currentThemeKey() }); }, From 6bfe92e19e827686f292a679fd8dcd2e0d90f9c7 Mon Sep 17 00:00:00 2001 From: ckeboss Date: Wed, 12 Apr 2017 10:33:56 -0700 Subject: [PATCH 036/221] Update boot_dev --- bin/docker/boot_dev | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/docker/boot_dev b/bin/docker/boot_dev index 93d1fbc415..66a038a5e7 100755 --- a/bin/docker/boot_dev +++ b/bin/docker/boot_dev @@ -44,7 +44,7 @@ if [ "${initialize}" = "initialize" ]; then echo "Migrating database..." "${SCRIPTPATH}/rake" db:migrate - "RAILS_ENV=test ${SCRIPTPATH}/rake" db:migrate + RAILS_ENV=test "${SCRIPTPATH}/rake" db:migrate echo "Creating admin user..." "${SCRIPTPATH}/rake" admin:create From db9a44d4b5b365a14677dd95c1484129d02684ad Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 12 Apr 2017 13:37:27 -0400 Subject: [PATCH 037/221] we need theme vars when building theme css --- lib/stylesheet/compiler.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/stylesheet/compiler.rb b/lib/stylesheet/compiler.rb index 7ddf106689..b82c5c9d40 100644 --- a/lib/stylesheet/compiler.rb +++ b/lib/stylesheet/compiler.rb @@ -19,7 +19,7 @@ module Stylesheet if Importer.special_imports[asset.to_s] filename = "theme.scss" - file = "@import \"#{asset}\";" + file = "@import \"theme_variables\"; @import \"#{asset}\";" else filename = "#{asset}.scss" path = "#{ASSET_ROOT}/#{filename}" From a018eed6118a4fe7887ffd2e08c0a5e305891095 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 12 Apr 2017 14:47:37 -0400 Subject: [PATCH 038/221] expand tmp for osx --- lib/git_importer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/git_importer.rb b/lib/git_importer.rb index 9ad1b15c6f..e7c175e267 100644 --- a/lib/git_importer.rb +++ b/lib/git_importer.rb @@ -7,7 +7,7 @@ class GitImporter if @url.start_with?("https://github.com") && !@url.end_with?(".git") @url += ".git" end - @temp_folder = "#{Dir.tmpdir}/discourse_theme_#{SecureRandom.hex}" + @temp_folder = "#{Pathname.new(Dir.tmpdir).realpath}/discourse_theme_#{SecureRandom.hex}" end def import! From 8395569b0511891a0a287df5b4a0816ad37bdfb7 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 12 Apr 2017 14:38:58 -0400 Subject: [PATCH 039/221] FIX: Topic.similar_to error when Search is not loaded --- app/models/topic.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/topic.rb b/app/models/topic.rb index 4f1367fa07..ed3844c121 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -7,6 +7,7 @@ require_dependency 'text_cleaner' require_dependency 'archetype' require_dependency 'html_prettify' require_dependency 'discourse_tagging' +require_dependency 'search' class Topic < ActiveRecord::Base include ActionView::Helpers::SanitizeHelper From d34dd4ed4f351394cff49a3b482b05b176a4d81c Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 12 Apr 2017 14:39:37 -0400 Subject: [PATCH 040/221] FIX: pasting link into composer with a topic template will onebox the link after the template --- .../discourse/components/composer-title.js.es6 | 14 ++++++++++---- .../javascripts/discourse/models/composer.js.es6 | 5 +++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/components/composer-title.js.es6 b/app/assets/javascripts/discourse/components/composer-title.js.es6 index 91cdf7186c..e9f1790ba5 100644 --- a/app/assets/javascripts/discourse/components/composer-title.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-title.js.es6 @@ -45,7 +45,7 @@ export default Ember.Component.extend({ @observes('composer.replyLength') _clearFeaturedLink() { - if (this.get('watchForLink') && this.get('composer.replyLength') === 0) { + if (this.get('watchForLink') && this.bodyIsDefault()) { this.set('composer.featuredLink', null); } }, @@ -53,7 +53,7 @@ export default Ember.Component.extend({ _checkForUrl() { if (!this.element || this.isDestroying || this.isDestroyed) { return; } - if (this.get('isAbsoluteUrl') && (this.get('composer.reply')||"").length === 0) { + if (this.get('isAbsoluteUrl') && this.bodyIsDefault()) { // only feature links to external sites if (this.get('composer.title').match(new RegExp("^https?:\\/\\/" + window.location.hostname, "i"))) { return; } @@ -88,9 +88,10 @@ export default Ember.Component.extend({ this.set('composer.featuredLink', this.get('composer.title')); const $h = $(html), - heading = $h.find('h3').length > 0 ? $h.find('h3') : $h.find('h4'); + heading = $h.find('h3').length > 0 ? $h.find('h3') : $h.find('h4'), + composer = this.get('composer'); - this.set('composer.reply', this.get('composer.title')); + composer.appendText(this.get('composer.title'), null, {block: true}); if (heading.length > 0 && heading.text().length > 0) { this.changeTitle(heading.text()); @@ -112,5 +113,10 @@ export default Ember.Component.extend({ @computed('composer.title') isAbsoluteUrl() { return this.get('composer.titleLength') > 0 && /^(https?:)?\/\/[\w\.\-]+/i.test(this.get('composer.title')); + }, + + bodyIsDefault() { + const reply = this.get('composer.reply')||""; + return (reply.length === 0 || (reply === (this.get("composer.category.topic_template")||""))); } }); diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 68ec25cfdd..c81e89e6ed 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -73,6 +73,11 @@ const Composer = RestModel.extend({ } }, + @computed('categoryId') + category(categoryId) { + return categoryId ? this.site.categories.findBy('id', categoryId) : null; + }, + creatingTopic: Em.computed.equal('action', CREATE_TOPIC), creatingPrivateMessage: Em.computed.equal('action', PRIVATE_MESSAGE), notCreatingPrivateMessage: Em.computed.not('creatingPrivateMessage'), From 2916b4566645d5cf3e09d06b9b432ccc1979e14d Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 12 Apr 2017 14:39:59 -0400 Subject: [PATCH 041/221] multiplier should not have been changed --- app/mailers/user_notifications.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index acf82ae2dc..586c3a7163 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -118,7 +118,7 @@ class UserNotifications < ActionMailer::Base .for_mailing_list(user, min_date) .where('posts.post_type = ?', Post.types[:regular]) .where('posts.deleted_at IS NULL AND posts.hidden = false AND posts.user_deleted = false') - .where("posts.post_number > ? AND posts.score > ?", 1, ScoreCalculator.default_score_weights[:like_score] * 1.0) + .where("posts.post_number > ? AND posts.score > ?", 1, ScoreCalculator.default_score_weights[:like_score] * 5.0) .limit(SiteSetting.digest_posts) else [] From ff6a98183faa04098c0de63e7da7af0ee5812193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 12 Apr 2017 19:16:45 +0200 Subject: [PATCH 042/221] WIP: JIVE importer using the API --- script/import_scripts/jive_api.rb | 184 ++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 script/import_scripts/jive_api.rb diff --git a/script/import_scripts/jive_api.rb b/script/import_scripts/jive_api.rb new file mode 100644 index 0000000000..eec8dac761 --- /dev/null +++ b/script/import_scripts/jive_api.rb @@ -0,0 +1,184 @@ +require "htmlentities" +require File.expand_path(File.dirname(__FILE__) + "/base.rb") + +class ImportScripts::JiveApi < ImportScripts::Base + + COUNT ||= 100 + STAFF_GUARDIAN ||= Guardian.new(Discourse.system_user) + + def initialize + super + @base_uri = ENV["BASE_URI"] + @username = ENV["USERNAME"] + @password = ENV["PASSWORD"] + @htmlentities = HTMLEntities.new + end + + def execute + import_users + import_discussions + import_posts + + mark_topics_as_solved + end + + def import_users + puts "", "importing users..." + + imported_users = 0 + start_index = [0, Math.floor(PostCustomField.where(name: "import_id").count / COUNT.to_f) - COUNT].max + + loop do + users = get("people/@all?fields=initialLogin,emails,displayName,mentionName,thumbnailUrl,-resources&count=#{COUNT}&startIndex=#{start_index}", true) + create_users(users["list"], offset: imported_users) do |user| + { + id: user["id"], + created_at: user["initialLogin"], + email: user["emails"].find { |email| email["primary"] }["value"], + username: user["mentionName"], + name: user["displayName"], + avatar_url: user["thumbnailUrl"], + } + end + + break if users["list"].size < COUNT || users["links"].blank? || users["links"]["next"].blank? + imported_users += users["list"].size + break unless start_index = users["links"]["next"][/startIndex=(\d+)/, 1] + end + end + + def import_discussions + puts "", "importing discussions & questions..." + + start_index = 0 + fields = "fields=published,tags,contentID,author.id,content.text,subject,viewCount,question,-resources,-author.resources" + filter = "&filter=creationDate(null,2017-01-01T00:00:00Z)" + + loop do + discussions = get("contents?#{fields}&filter=status(published)&filter=type(discussion)#{filter}&sort=dateCreatedAsc&count=#{COUNT}&startIndex=#{start_index}") + discussions["list"].each do |discussion| + topic = { + id: discussion["contentID"], + created_at: discussion["published"], + title: @htmlentities.decode(discussion["subject"]), + raw: discussion["content"]["text"], + user_id: user_id_from_imported_user_id(discussion["author"]["id"]) || Discourse::SYSTEM_USER_ID, + # category: discussion["question"] ? 26 : 21, + views: discussion["viewCount"], + cook_method: Post.cook_methods[:raw_html], + custom_fields: { import_id: discussion["contentID"] }, + post_create_action: proc do |post| + tags = discussion["tags"].compact.map(&:strip).select(&:present?) + DiscourseTagging.tag_topic_by_names(post.topic, STAFF_GUARDIAN, tags) unless tags.empty? + end + } + + post_id = post_id_from_imported_post_id(topic[:id]) + parent_post = post_id ? Post.unscoped.find_by(id: post_id) : create_post(topic, topic[:id]) + + import_comments(discussion["contentID"], parent_post.topic_id) if parent_post + end + + break if discussions["list"].size < COUNT || discussions["links"].blank? || discussions["links"]["next"].blank? + break unless start_index = discussions["links"]["next"][/startIndex=(\d+)/, 1] + end + end + + def import_comments(discussion_id, topic_id) + start_index = 0 + fields = "fields=published,author.id,content.text,parent,answer,-resources,-author.resources" + + loop do + comments = get("messages/contents/#{discussion_id}?#{fields}&count=#{COUNT}&startIndex=#{start_index}") + comments["list"].each do |comment| + next if post_id_from_imported_post_id(comment["id"]) + + post = { + id: comment["id"], + created_at: comment["published"], + topic_id: topic_id, + user_id: user_id_from_imported_user_id(comment["author"]["id"]) || Discourse::SYSTEM_USER_ID, + raw: comment["content"]["text"], + cook_method: Post.cook_methods[:raw_html], + custom_fields: { import_id: comment["id"] }, + } + post[:custom_fields][:is_accepted_answer] = true if comment["answer"] + + if parent_post_id = comment["parent"][/\/messages\/(\d+)/, 1] + if parent = topic_lookup_from_imported_post_id(parent_post_id) + post[:reply_to_post_number] = parent[:post_number] + end + end + + create_post(post, post[:id]) + end + + break if comments["list"].size < COUNT || comments["links"].blank? || comments["links"]["next"].blank? + break unless start_index = comments["links"]["next"][/startIndex=(\d+)/, 1] + end + end + + def import_posts + puts "", "importing blog posts..." + + start_index = 0 + fields = "fields=published,tags,contentID,author.id,content.text,subject,viewCount,permalink,-resources,-author.resources" + filter = "&filter=creationDate(null,2016-05-01T00:00:00Z)" + + loop do + posts = get("contents?#{fields}&filter=status(published)&filter=type(post)#{filter}&sort=dateCreatedAsc&count=#{COUNT}&startIndex=#{start_index}") + posts["list"].each do |post| + next if post_id_from_imported_post_id(post["contentID"]) + pp = { + id: post["contentID"], + created_at: post["published"], + title: @htmlentities.decode(post["subject"]), + raw: post["content"]["text"], + user_id: user_id_from_imported_user_id(post["author"]["id"]) || Discourse::SYSTEM_USER_ID, + # category: 7, + views: post["viewCount"], + cook_method: Post.cook_methods[:raw_html], + custom_fields: { import_id: post["contentID"], import_permalink: post["permalink"] }, + post_create_action: proc do |p| + tags = post["tags"].compact.map(&:strip).select(&:present?) + DiscourseTagging.tag_topic_by_names(p.topic, STAFF_GUARDIAN, tags) unless tags.empty? + end + } + + create_post(pp, pp[:id]) + end + + break if posts["list"].size < COUNT || posts["links"].blank? || posts["links"]["next"].blank? + break unless start_index = posts["links"]["next"][/startIndex=(\d+)/, 1] + end + end + + def mark_topics_as_solved + puts "", "Marking topics as solved..." + + PostAction.exec_sql <<-SQL + INSERT INTO topic_custom_fields (name, value, topic_id, created_at, updated_at) + SELECT 'accepted_answer_post_id', pcf.post_id, p.topic_id, p.created_at, p.created_at + FROM post_custom_fields pcf + JOIN posts p ON p.id = pcf.post_id + WHERE pcf.name = 'is_accepted_answer' + SQL + end + + def get(query, authenticated=false) + tries ||= 3 + + command = ["curl", "--silent"] + command << "--user \"#{@username}:#{@password}\"" if authenticated + command << "\"#{@base_uri}/api/core/v3/#{query}\"" + + puts command.join(" ") + + JSON.parse `#{command.join(" ")}` + rescue => e + retry if (tries -= 1) >= 0 + end + +end + +ImportScripts::JiveApi.new.perform From 07d596ea056390fb45440263517bf2ec592041a3 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 12 Apr 2017 16:44:16 -0400 Subject: [PATCH 043/221] FIX: handle expanding quotes from deleted posts by rendering a trash icon --- app/assets/javascripts/discourse/widgets/post-cooked.js.es6 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/javascripts/discourse/widgets/post-cooked.js.es6 b/app/assets/javascripts/discourse/widgets/post-cooked.js.es6 index 3879e3739a..38784a3941 100644 --- a/app/assets/javascripts/discourse/widgets/post-cooked.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-cooked.js.es6 @@ -136,6 +136,10 @@ export default class PostCooked { div.html(result.cooked); div.highlight(originalText, {caseSensitive: true, element: 'span', className: 'highlighted'}); $blockQuote.showHtml(div, 'fast', finished); + }).catch((e) => { + if (e.jqXHR.status === 404) { + $blockQuote.showHtml($("
    "), 'fast', finished); + } }); } else { // Hide expanded quote From 0165b22b761411d670fc1d1fd147e8d91bbd720c Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Apr 2017 06:31:25 +0800 Subject: [PATCH 044/221] Bump onebox. --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a0f8c1af3d..9546da3467 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -164,7 +164,7 @@ GEM multi_json (1.12.1) multi_xml (0.6.0) multipart-post (2.0.0) - mustache (1.0.3) + mustache (1.0.5) netrc (0.11.0) nokogiri (1.6.8.1) mini_portile2 (~> 2.1.0) @@ -206,7 +206,7 @@ GEM omniauth-twitter (1.3.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.3) + onebox (1.8.2) fast_blank (>= 1.0.0) htmlentities (~> 4.3.4) moneta (~> 0.8) From a5f1c57a70bce540718a4c4705c70ee867d5c14d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 13 Apr 2017 00:49:22 +0200 Subject: [PATCH 045/221] bump onebox --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9546da3467..07edef68a8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -206,7 +206,7 @@ GEM omniauth-twitter (1.3.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.2) + onebox (1.8.4) fast_blank (>= 1.0.0) htmlentities (~> 4.3.4) moneta (~> 0.8) From f48022b674ebc137620d177243f218acccc78c7d Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Apr 2017 10:17:09 +0800 Subject: [PATCH 046/221] Upgrade Rails to 4.2.8. --- Gemfile.lock | 86 +++++++++++++++++++++++++--------------------------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 07edef68a8..cdd13244cd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,47 +1,46 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (4.2.7.1) - actionpack (= 4.2.7.1) - actionview (= 4.2.7.1) - activejob (= 4.2.7.1) + actionmailer (4.2.8) + actionpack (= 4.2.8) + actionview (= 4.2.8) + activejob (= 4.2.8) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.7.1) - actionview (= 4.2.7.1) - activesupport (= 4.2.7.1) + actionpack (4.2.8) + actionview (= 4.2.8) + activesupport (= 4.2.8) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.7.1) - activesupport (= 4.2.7.1) + actionview (4.2.8) + activesupport (= 4.2.8) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) - rails-html-sanitizer (~> 1.0, >= 1.0.2) + rails-html-sanitizer (~> 1.0, >= 1.0.3) active_model_serializers (0.8.3) activemodel (>= 3.0) - activejob (4.2.7.1) - activesupport (= 4.2.7.1) + activejob (4.2.8) + activesupport (= 4.2.8) globalid (>= 0.3.0) - activemodel (4.2.7.1) - activesupport (= 4.2.7.1) + activemodel (4.2.8) + activesupport (= 4.2.8) builder (~> 3.1) - activerecord (4.2.7.1) - activemodel (= 4.2.7.1) - activesupport (= 4.2.7.1) + activerecord (4.2.8) + activemodel (= 4.2.8) + activesupport (= 4.2.8) arel (~> 6.0) - activesupport (4.2.7.1) + activesupport (4.2.8) i18n (~> 0.7) - json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) annotate (2.7.1) activerecord (>= 3.2, < 6.0) rake (>= 10.4, < 12.0) - arel (6.0.3) + arel (6.0.4) aws-sdk (2.5.3) aws-sdk-resources (= 2.5.3) aws-sdk-core (2.5.3) @@ -61,14 +60,14 @@ GEM rack (>= 0.9.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - builder (3.2.2) + builder (3.2.3) bullet (5.4.2) activesupport (>= 3.0.0) uniform_notifier (~> 1.10.0) byebug (9.0.6) certified (1.0.0) coderay (1.1.1) - concurrent-ruby (1.0.2) + concurrent-ruby (1.0.5) connection_pool (2.2.0) crass (1.0.2) debug_inspector (0.0.2) @@ -121,7 +120,7 @@ GEM http-cookie (1.0.2) domain_name (~> 0.5) http_accept_language (2.0.5) - i18n (0.7.0) + i18n (0.8.1) image_optim (0.24.2) exifr (~> 1.2, >= 1.2.2) fspath (~> 3.0) @@ -135,7 +134,6 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (1.8.6) jwt (1.5.6) kgio (2.10.0) libv8 (5.3.332.38.5) @@ -151,11 +149,11 @@ GEM rack (>= 1.1.3) metaclass (0.0.4) method_source (0.8.2) - mime-types (2.99.2) + mime-types (2.99.3) mini_portile2 (2.1.0) mini_racer (0.1.9) libv8 (~> 5.3) - minitest (5.9.1) + minitest (5.10.1) mocha (1.1.0) metaclass (~> 0.0.1) mock_redis (0.15.4) @@ -239,34 +237,34 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (4.2.7.1) - actionmailer (= 4.2.7.1) - actionpack (= 4.2.7.1) - actionview (= 4.2.7.1) - activejob (= 4.2.7.1) - activemodel (= 4.2.7.1) - activerecord (= 4.2.7.1) - activesupport (= 4.2.7.1) + rails (4.2.8) + actionmailer (= 4.2.8) + actionpack (= 4.2.8) + actionview (= 4.2.8) + activejob (= 4.2.8) + activemodel (= 4.2.8) + activerecord (= 4.2.8) + activesupport (= 4.2.8) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.7.1) + railties (= 4.2.8) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.7) + rails-dom-testing (1.0.8) activesupport (>= 4.2.0.beta, < 5.0) - nokogiri (~> 1.6.0) + nokogiri (~> 1.6) rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) rails_multisite (1.0.6) rails (> 4.2, < 5) - railties (4.2.7.1) - actionpack (= 4.2.7.1) - activesupport (= 4.2.7.1) + railties (4.2.8) + actionpack (= 4.2.8) + activesupport (= 4.2.8) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) raindrops (0.17.0) - rake (11.2.2) + rake (11.3.0) rake-compiler (0.9.9) rake rb-fsevent (0.9.7) @@ -359,12 +357,12 @@ GEM stackprof (0.2.10) test_after_commit (1.1.0) activerecord (>= 3.2) - thor (0.19.1) - thread_safe (0.3.5) + thor (0.19.4) + thread_safe (0.3.6) tilt (2.0.5) timecop (0.8.1) trollop (2.1.2) - tzinfo (1.2.2) + tzinfo (1.2.3) thread_safe (~> 0.1) uglifier (3.0.2) execjs (>= 0.3.0, < 3) From 57788200ec966446605450cb33cae07b0e269d5d Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Apr 2017 10:44:26 +0800 Subject: [PATCH 047/221] REFACTOR: Add `User.reserved_username?`. --- app/controllers/users_controller.rb | 2 +- app/models/user.rb | 10 +++++++-- spec/models/user_spec.rb | 34 ++++++++++++++++++----------- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index eb69ce7d3c..a9eaa127e1 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -307,7 +307,7 @@ class UsersController < ApplicationController return fail_with("login.email_too_long") end - if SiteSetting.reserved_usernames.split("|").include? params[:username].downcase + if User.reserved_username?(params[:username]) return fail_with("login.reserved_username") end diff --git a/app/models/user.rb b/app/models/user.rb index 8c4a0a130a..8b1d540a1e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -162,9 +162,15 @@ class User < ActiveRecord::Base def self.username_available?(username) lower = username.downcase + !User.where(username_lower: lower).exists? && !reserved_username?(lower) + end - User.where(username_lower: lower).blank? && - SiteSetting.reserved_usernames.split("|").all? { |reserved| !lower.match('^' + Regexp.escape(reserved).gsub('\*', '.*') + '$') } + def self.reserved_username?(username) + lower = username.downcase + + SiteSetting.reserved_usernames.split("|").any? do |reserved| + !!lower.match("^#{Regexp.escape(reserved).gsub('\*', '.*')}$") + end end def self.plugin_staff_user_custom_fields diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 9304b1279a..9409f66ae2 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -459,7 +459,7 @@ describe User do end end - context '.username_available?' do + describe '.username_available?' do it "returns true for a username that is available" do expect(User.username_available?('BruceWayne')).to eq(true) end @@ -471,25 +471,33 @@ describe User do it 'returns false when a username is reserved' do SiteSetting.reserved_usernames = 'test|donkey' - expect(User.username_available?('donkey')).to eq(false) - expect(User.username_available?('DonKey')).to eq(false) - expect(User.username_available?('test')).to eq(false) + expect(User.username_available?('tESt')).to eq(false) + end + end + + describe '.reserved_username?' do + it 'returns true when a username is reserved' do + SiteSetting.reserved_usernames = 'test|donkey' + + expect(User.reserved_username?('donkey')).to eq(true) + expect(User.reserved_username?('DonKey')).to eq(true) + expect(User.reserved_username?('test')).to eq(true) end it 'should not allow usernames matched against an expession' do SiteSetting.reserved_usernames = 'test)|*admin*|foo*|*bar|abc.def' - expect(User.username_available?('test')).to eq(true) - expect(User.username_available?('abc9def')).to eq(true) + expect(User.reserved_username?('test')).to eq(false) + expect(User.reserved_username?('abc9def')).to eq(false) - expect(User.username_available?('admin')).to eq(false) - expect(User.username_available?('foo')).to eq(false) - expect(User.username_available?('bar')).to eq(false) + expect(User.reserved_username?('admin')).to eq(true) + expect(User.reserved_username?('foo')).to eq(true) + expect(User.reserved_username?('bar')).to eq(true) - expect(User.username_available?('admi')).to eq(true) - expect(User.username_available?('bar.foo')).to eq(true) - expect(User.username_available?('foo.bar')).to eq(false) - expect(User.username_available?('baz.bar')).to eq(false) + expect(User.reserved_username?('admi')).to eq(false) + expect(User.reserved_username?('bar.foo')).to eq(false) + expect(User.reserved_username?('foo.bar')).to eq(true) + expect(User.reserved_username?('baz.bar')).to eq(true) end end From 72c16967e61afaa59516f8fe94554af5d6eb2c87 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Apr 2017 11:32:20 +0800 Subject: [PATCH 048/221] FIX: Check for reserved usernames before hitting DB. --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 8b1d540a1e..6ce645d43d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -162,7 +162,7 @@ class User < ActiveRecord::Base def self.username_available?(username) lower = username.downcase - !User.where(username_lower: lower).exists? && !reserved_username?(lower) + !reserved_username?(lower) && !User.where(username_lower: lower).exists? end def self.reserved_username?(username) From ee449b0dd5be9a85eed598cddedcc9875de2214a Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Apr 2017 11:39:26 +0800 Subject: [PATCH 049/221] Improve SSO verbose log when user record is invalid. --- app/controllers/session_controller.rb | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index 49e7639974..96259d430d 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -137,15 +137,18 @@ class SessionController < ApplicationController rescue ActiveRecord::RecordInvalid => e if SiteSetting.verbose_sso_logging - Rails.logger.warn(<<-EOF) - Verbose SSO log: Record was invalid: #{e.record.class.name} #{e.record.id}\n - #{e.record.errors.to_h}\n - \n - #{sso.diagnostics} + Rails.logger.warn(<<~EOF) + Verbose SSO log: Record was invalid: #{e.record.class.name} #{e.record.id} + #{e.record.errors.to_h} + + Attributes: + #{e.record.attributes.slice(*SingleSignOn::ACCESSORS.map(&:to_s))} + + SSO Diagnostics: + #{sso.diagnostics} EOF end - text = nil # If there's a problem with the email we can explain that From bda20cc44a52a4c80d169cc6c8b3b71dbb98aaf3 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Apr 2017 12:02:35 +0800 Subject: [PATCH 050/221] FIX: Don't enqueue topic status update job if topic is deleted. --- app/models/topic_status_update.rb | 8 ++++++-- spec/models/topic_status_update_spec.rb | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/models/topic_status_update.rb b/app/models/topic_status_update.rb index c8461bd2b7..d5e59c6737 100644 --- a/app/models/topic_status_update.rb +++ b/app/models/topic_status_update.rb @@ -23,7 +23,7 @@ class TopicStatusUpdate < ActiveRecord::Base end after_save do - if (execute_at_changed? || user_id_changed?) && topic + if (execute_at_changed? || user_id_changed?) now = Time.zone.now time = execute_at < now ? now : execute_at @@ -40,7 +40,9 @@ class TopicStatusUpdate < ActiveRecord::Base end def self.ensure_consistency! - TopicStatusUpdate.where("execute_at < ?", Time.zone.now).find_each do |topic_status_update| + TopicStatusUpdate.where("topic_status_updates.execute_at < ?", Time.zone.now) + .find_each do |topic_status_update| + topic_status_update.send( "schedule_auto_#{self.types[topic_status_update.status_type]}_job", topic_status_update.execute_at @@ -76,6 +78,7 @@ class TopicStatusUpdate < ActiveRecord::Base end def schedule_auto_open_job(time) + return unless topic topic.update_status('closed', true, user) if !topic.closed Jobs.enqueue_at(time, :toggle_topic_closed, @@ -85,6 +88,7 @@ class TopicStatusUpdate < ActiveRecord::Base end def schedule_auto_close_job(time) + return unless topic topic.update_status('closed', false, user) if topic.closed Jobs.enqueue_at(time, :toggle_topic_closed, diff --git a/spec/models/topic_status_update_spec.rb b/spec/models/topic_status_update_spec.rb index 6880ddc2ec..7b246b8819 100644 --- a/spec/models/topic_status_update_spec.rb +++ b/spec/models/topic_status_update_spec.rb @@ -226,6 +226,11 @@ RSpec.describe TopicStatusUpdate, type: :model do Fabricate(:topic_status_update) + Fabricate(:topic_status_update, + execute_at: Time.zone.now - 1.hour, + created_at: Time.zone.now - 2.hour + ).topic.trash! + expect { described_class.ensure_consistency! } .to change { Jobs::ToggleTopicClosed.jobs.count }.by(2) From 7fb17b83c49578a019329a0123f780175166cf42 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 12 Apr 2017 23:59:32 +0530 Subject: [PATCH 051/221] FIX: confirm email token for user created via social login --- app/controllers/users/omniauth_callbacks_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 190c22924f..d7097eb434 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -112,7 +112,8 @@ class Users::OmniauthCallbacksController < ApplicationController def user_found(user) # automatically activate/unstage any account if a provider marked the email valid if @auth_result.email_valid && @auth_result.email == user.email - user.update!(staged: false, active: true) + user.update!(staged: false) + user.activate end if ScreenedIpAddress.should_block?(request.remote_ip) From 3d76fb9c2c400fb91d5f77247e9b776ea5c8912a Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Apr 2017 17:10:55 +0800 Subject: [PATCH 052/221] FIX: Don't show category options for reports that can't be scoped to a category. --- .../admin/controllers/admin-reports.js.es6 | 20 ++++++--- .../admin/routes/admin-reports.js.es6 | 4 +- app/controllers/admin/reports_controller.rb | 2 +- app/models/report.rb | 1 - app/models/user.rb | 2 +- .../admin/reports_controller_spec.rb | 41 ++++++++++++++++++- 6 files changed, 58 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/admin-reports.js.es6 b/app/assets/javascripts/admin/controllers/admin-reports.js.es6 index 1d4dac7e64..83fde41c7e 100644 --- a/app/assets/javascripts/admin/controllers/admin-reports.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-reports.js.es6 @@ -4,7 +4,7 @@ import Report from 'admin/models/report'; import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Controller.extend({ - queryParams: ["mode", "start-date", "end-date", "category-id", "group-id"], + queryParams: ["mode", "start_date", "end_date", "category_id", "group_id"], viewMode: 'graph', viewingTable: Em.computed.equal('viewMode', 'table'), viewingGraph: Em.computed.equal('viewMode', 'graph'), @@ -28,7 +28,15 @@ export default Ember.Controller.extend({ @computed('model.type') showCategoryOptions(modelType) { - return !modelType.match(/_private_messages$/) && !modelType.match(/^page_view_/); + return [ + 'topics', + 'posts', + 'time_to_first_response_total', + 'topics_with_no_response', + 'flags', + 'likes', + 'bookmarks' + ].includes(modelType); }, @computed('model.type') @@ -42,13 +50,13 @@ export default Ember.Controller.extend({ this.set("refreshing", true); this.setProperties({ - 'start-date': this.get('startDate'), - 'end-date': this.get('endDate'), - 'category-id': this.get('categoryId'), + 'start_date': this.get('startDate'), + 'end_date': this.get('endDate'), + 'category_id': this.get('categoryId'), }); if (this.get('groupId')){ - this.set('group-id', this.get('groupId')); + this.set('group_id', this.get('groupId')); } q = Report.find(this.get("model.type"), this.get("startDate"), this.get("endDate"), this.get("categoryId"), this.get("groupId")); diff --git a/app/assets/javascripts/admin/routes/admin-reports.js.es6 b/app/assets/javascripts/admin/routes/admin-reports.js.es6 index eb89a3306e..47ece70580 100644 --- a/app/assets/javascripts/admin/routes/admin-reports.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-reports.js.es6 @@ -7,11 +7,11 @@ @module Discourse **/ export default Discourse.Route.extend({ - queryParams: { mode: {}, "start-date": {}, "end-date": {}, "category-id": {}, "group-id": {}}, + queryParams: { mode: {}, "start_date": {}, "end_date": {}, "category_id": {}, "group_id": {} }, model: function(params) { const Report = require('admin/models/report').default; - return Report.find(params.type, params['start-date'], params['end-date'], params['category-id'], params['group-id']); + return Report.find(params.type, params['start_date'], params['end_date'], params['category_id'], params['group_id']); }, setupController: function(controller, model) { diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 8a47b8a9d1..6490c96d73 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -9,7 +9,7 @@ class Admin::ReportsController < Admin::AdminController start_date = params[:start_date].present? ? Time.parse(params[:start_date]) : 30.days.ago end_date = params[:end_date].present? ? Time.parse(params[:end_date]) : start_date + 30.days - + if params.has_key?(:category_id) && params[:category_id].to_i > 0 category_id = params[:category_id].to_i else diff --git a/app/models/report.rb b/app/models/report.rb index b568aec1ea..76c162ffb1 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -82,7 +82,6 @@ class Report .sum(:count) end - def self.report_visits(report) basic_report_about report, UserVisit, :by_day, report.start_date, report.end_date, report.group_id diff --git a/app/models/user.rb b/app/models/user.rb index 6ce645d43d..3c41e4c1e2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -740,7 +740,7 @@ class User < ActiveRecord::Base (tl_badge + other_badges).take(limit) end - def self.count_by_signup_date(start_date, end_date, group_id=nil) + def self.count_by_signup_date(start_date, end_date, group_id = nil) result = where('users.created_at >= ? AND users.created_at <= ?', start_date, end_date) if group_id diff --git a/spec/controllers/admin/reports_controller_spec.rb b/spec/controllers/admin/reports_controller_spec.rb index 92da083f32..9d34ebbe1c 100644 --- a/spec/controllers/admin/reports_controller_spec.rb +++ b/spec/controllers/admin/reports_controller_spec.rb @@ -1,7 +1,6 @@ require 'rails_helper' describe Admin::ReportsController do - it "is a subclass of AdminController" do expect(Admin::ReportsController < Admin::AdminController).to eq(true) end @@ -58,6 +57,46 @@ describe Admin::ReportsController do end + describe 'when report is scoped to a category' do + let(:category) { Fabricate(:category) } + let(:topic) { Fabricate(:topic, category: category) } + let(:other_topic) { Fabricate(:topic) } + + it 'should render the report as JSON' do + topic + other_topic + + xhr :get, :show, type: 'topics', category_id: category.id + + expect(response).to be_success + + report = JSON.parse(response.body)["report"] + + expect(report["type"]).to eq('topics') + expect(report["data"].count).to eq(1) + end + end + + describe 'when report is scoped to a group' do + let(:user) { Fabricate(:user) } + let(:other_user) { Fabricate(:user) } + let(:group) { Fabricate(:group) } + + it 'should render the report as JSON' do + other_user + group.add(user) + + xhr :get, :show, type: 'signups', group_id: group.id + + expect(response).to be_success + + report = JSON.parse(response.body)["report"] + + expect(report["type"]).to eq('signups') + expect(report["data"].count).to eq(1) + end + end + end end From 22214f5e5c48a34640d899151ea39a9a50f1fe14 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 13 Apr 2017 10:26:07 -0400 Subject: [PATCH 053/221] Upgrade the Listen gem --- Gemfile | 2 +- Gemfile.lock | 8 ++++++-- lib/autospec/manager.rb | 4 +++- lib/stylesheet/watcher.rb | 4 +++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 57e86b7817..0ae1f8ff4c 100644 --- a/Gemfile +++ b/Gemfile @@ -122,7 +122,7 @@ end group :test, :development do gem 'rspec' gem 'mock_redis' - gem 'listen', '0.7.3', require: false + gem 'listen', require: false gem 'certified', require: false # later appears to break Fabricate(:topic, category: category) gem 'fabrication', '2.9.8', require: false diff --git a/Gemfile.lock b/Gemfile.lock index cdd13244cd..a5a89ca661 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -137,7 +137,10 @@ GEM jwt (1.5.6) kgio (2.10.0) libv8 (5.3.332.38.5) - listen (0.7.3) + listen (3.1.5) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + ruby_dep (~> 1.2) logster (1.2.7) loofah (2.0.3) nokogiri (>= 1.5.9) @@ -312,6 +315,7 @@ GEM ruby-readability (0.7.0) guess_html_encoding (>= 0.0.4) nokogiri (>= 1.6.0) + ruby_dep (1.5.0) sanitize (4.4.0) crass (~> 1.0.2) nokogiri (>= 1.4.4) @@ -409,7 +413,7 @@ DEPENDENCIES htmlentities http_accept_language (~> 2.0.5) image_optim - listen (= 0.7.3) + listen logster lru_redux mail diff --git a/lib/autospec/manager.rb b/lib/autospec/manager.rb index 4833f22be3..450b215855 100644 --- a/lib/autospec/manager.rb +++ b/lib/autospec/manager.rb @@ -179,12 +179,14 @@ class Autospec::Manager puts "@@@@@@@@@ Listen to #{path}/#{watch} #{options}" if @debug Thread.new do begin - Listen.to("#{path}/#{watch}", options) do |modified, added, _| + listener = Listen.to("#{path}/#{watch}", options) do |modified, added, _| paths = [modified, added].flatten paths.compact! paths.map!{|long| long[(path.length+1)..-1]} process_change(paths) end + listener.start + sleep rescue => e puts "FAILED to listen on changes to #{path}/#{watch}" puts e diff --git a/lib/stylesheet/watcher.rb b/lib/stylesheet/watcher.rb index 8f2005e74b..c41e981c4c 100644 --- a/lib/stylesheet/watcher.rb +++ b/lib/stylesheet/watcher.rb @@ -31,7 +31,7 @@ module Stylesheet @paths.each do |watch| Thread.new do begin - Listen.to("#{root}/#{watch}") do |modified, added, _| + listener = Listen.to("#{root}/#{watch}", ignore: /xxxx/) do |modified, added, _| paths = [modified, added].flatten paths.compact! paths.map!{|long| long[(root.length+1)..-1]} @@ -40,6 +40,8 @@ module Stylesheet rescue => e STDERR.puts "Failed to listen for CSS changes at: #{watch}\n#{e}" end + listener.start + sleep end end end From e61be7af13ef1fa19c5872a07f3bf55b4d7666ae Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 13 Apr 2017 10:39:26 -0400 Subject: [PATCH 054/221] FIX: improve dark theme styling in header for tags --- app/assets/stylesheets/common/base/tagging.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/assets/stylesheets/common/base/tagging.scss b/app/assets/stylesheets/common/base/tagging.scss index 7fb5f4813e..f691175120 100644 --- a/app/assets/stylesheets/common/base/tagging.scss +++ b/app/assets/stylesheets/common/base/tagging.scss @@ -74,10 +74,18 @@ $tag-color: scale-color($primary, $lightness: 40%); color: $tag-color; } + .extra-info-wrapper & { + color: $header-primary !important; + } + &.box { background-color: scale-color($primary, $lightness: 90%); color: scale-color($primary, $lightness: 30%); padding: 2px 8px; + .extra-info-wrapper & { + background-color: scale-color($header-primary, $lightness: 90%); + color: scale-color($header-primary, $lightness: 30%); + } } &.simple, &.simple:visited, &.simple:hover { From 679b6548e66af5aec94e23ae6bf83ac4c74aed7d Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 13 Apr 2017 10:51:44 -0400 Subject: [PATCH 055/221] FIX: order child themes by name --- app/serializers/theme_serializer.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/serializers/theme_serializer.rb b/app/serializers/theme_serializer.rb index 051cf5d2f6..c7077b5294 100644 --- a/app/serializers/theme_serializer.rb +++ b/app/serializers/theme_serializer.rb @@ -39,4 +39,8 @@ class ThemeSerializer < ChildThemeSerializer has_many :theme_fields, serializer: ThemeFieldSerializer, embed: :objects has_many :child_themes, serializer: ChildThemeSerializer, embed: :objects has_one :remote_theme, serializer: RemoteThemeSerializer, embed: :objects + + def child_themes + object.child_themes.order(:name) + end end From ee950b419f2673d0de4bfdd8c81fdf9cd7478222 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 13 Apr 2017 11:09:31 -0400 Subject: [PATCH 056/221] correct transitioning to theme CSS editing --- .../admin/controllers/admin-customize-themes-show.js.es6 | 2 +- .../admin/routes/admin-customize-themes-edit.js.es6 | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 index 7ca3501241..4346495458 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 @@ -106,7 +106,7 @@ export default Ember.Controller.extend({ }, editTheme() { - let edit = ()=>this.transitionToRoute('adminCustomizeThemes.edit', {model: this.get('model')}); + let edit = ()=>this.transitionToRoute('adminCustomizeThemes.edit', this.get('model.id'), 'common', 'scss'); if (this.get("model.remote_theme")) { bootbox.confirm(I18n.t("admin.customize.theme.edit_confirm"), result => { diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 index aed0709e55..bd1e2c0dd4 100644 --- a/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 @@ -2,7 +2,10 @@ export default Ember.Route.extend({ model(params) { const all = this.modelFor('adminCustomizeThemes'); const model = all.findBy('id', parseInt(params.theme_id)); - return model ? { model, target: params.target, field_name: params.field_name} : this.replaceWith('adminCustomizeThemes.index'); + return model ? { model, + target: params.target, + field_name: params.field_name + } : this.replaceWith('adminCustomizeThemes.index'); }, serialize(wrapper) { From 37d4dd4a4bddb201111e541888a1f61aa6789f89 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 13 Apr 2017 12:05:27 -0400 Subject: [PATCH 057/221] use scss mode instead of css mode --- .../admin/controllers/admin-customize-themes-edit.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 index 507ea7136b..0c52b3c8dd 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 @@ -35,7 +35,7 @@ export default Ember.Controller.extend({ @computed("fieldName") activeSectionMode(fieldName) { - return fieldName && fieldName.indexOf("scss") > -1 ? "css" : "html"; + return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html"; }, @computed("fieldName", "currentTargetName", "model") From 3f4f0b32a9e94c5a2cc6548adbb46be9dbb855d4 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 13 Apr 2017 15:22:39 -0400 Subject: [PATCH 058/221] FIX: path wizard showing with no style --- app/views/layouts/embed.html.erb | 2 +- app/views/layouts/finish_installation.html.erb | 4 ++-- app/views/wizard/index.html.erb | 2 +- app/views/wizard/qunit.html.erb | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/layouts/embed.html.erb b/app/views/layouts/embed.html.erb index 61fa3ff86d..4dfa215bf6 100644 --- a/app/views/layouts/embed.html.erb +++ b/app/views/layouts/embed.html.erb @@ -3,7 +3,7 @@ - <%= stylesheet_link_tag 'embed' %> + <%= discourse_stylesheet_link_tag 'embed', theme_key: nil %> <%- unless customization_disabled? %> <%= Theme.custom_stylesheet(session[:preview_style], :embedded) %> <%- end %> diff --git a/app/views/layouts/finish_installation.html.erb b/app/views/layouts/finish_installation.html.erb index 31ebd26fa7..597822c805 100644 --- a/app/views/layouts/finish_installation.html.erb +++ b/app/views/layouts/finish_installation.html.erb @@ -1,8 +1,8 @@ - <%= stylesheet_link_tag 'wizard' %> + <%= discourse_stylesheet_link_tag 'wizard', theme_key: nil %> <%= render partial: "common/special_font_face" %> - <%= script 'jquery' %> + <%= script 'ember_jquery' %> <%= script 'wizard-vendor' %> <%= render partial: "layouts/head" %> <%= t 'wizard.title' %> diff --git a/app/views/wizard/index.html.erb b/app/views/wizard/index.html.erb index f0a0c0d46f..cb136601bc 100644 --- a/app/views/wizard/index.html.erb +++ b/app/views/wizard/index.html.erb @@ -1,6 +1,6 @@ - <%= discourse_stylesheet_link_tag 'wizard', theme_key: nil %> + <%= discourse_stylesheet_link_tag :wizard, theme_key: nil %> <%= script 'ember_jquery' %> <%= script 'wizard-vendor' %> <%= script 'wizard-application' %> diff --git a/app/views/wizard/qunit.html.erb b/app/views/wizard/qunit.html.erb index 2c4fa085e7..4e6e7a3a59 100644 --- a/app/views/wizard/qunit.html.erb +++ b/app/views/wizard/qunit.html.erb @@ -4,7 +4,7 @@ QUnit Test Runner <%= stylesheet_link_tag "qunit" %> <%= stylesheet_link_tag "test_helper" %> - <%= stylesheet_link_tag "wizard" %> + <%= discourse_stylesheet_link_tag :wizard %> <%= javascript_include_tag "qunit" %> <%= javascript_include_tag "wizard/test/test_helper" %> <%= csrf_meta_tags %> From 43e4fc03ef60ea8731f00ecd43e45894bca4d234 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 13 Apr 2017 15:49:46 -0400 Subject: [PATCH 059/221] FIX: clicking on themes while editing left a blank screen --- .../admin/routes/admin-customize-themes-index.js.es6 | 5 +++++ .../javascripts/admin/routes/admin-customize-themes.js.es6 | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/admin/routes/admin-customize-themes-index.js.es6 diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes-index.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes-index.js.es6 new file mode 100644 index 0000000000..b5fc281c1e --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-customize-themes-index.js.es6 @@ -0,0 +1,5 @@ +export default Ember.Route.extend({ + setupController() { + this.controllerFor("adminCustomizeThemes").set("editingTheme", false); + }, +}); diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 index 6e5e19b7f5..8291378b05 100644 --- a/app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 @@ -8,7 +8,6 @@ export default Ember.Route.extend({ setupController(controller, model) { this._super(controller, model); - // TODO ColorScheme to model controller.set("editingTheme", false); }, From 9927489f4e318ad197b75f377a5642d22ed2fb54 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 13 Apr 2017 16:21:46 -0400 Subject: [PATCH 060/221] FEATURE: auto focus text editor when editing themes --- .../admin/components/ace-editor.js.es6 | 20 +++++++++++++++++++ .../admin-customize-themes-edit.js.es6 | 5 +++++ .../routes/admin-customize-themes-edit.js.es6 | 1 - .../admin/templates/customize-themes-edit.hbs | 2 +- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/components/ace-editor.js.es6 b/app/assets/javascripts/admin/components/ace-editor.js.es6 index 3c1d12ffe9..b54f7b389c 100644 --- a/app/assets/javascripts/admin/components/ace-editor.js.es6 +++ b/app/assets/javascripts/admin/components/ace-editor.js.es6 @@ -7,6 +7,13 @@ export default Ember.Component.extend({ _editor: null, _skipContentChangeEvent: null, + @observes('editorId') + editorIdChanged() { + if (this.get('autofocus')) { + this.send('focus'); + } + }, + @observes('content') contentChanged() { if (this._editor && !this._skipContentChangeEvent) { @@ -63,7 +70,20 @@ export default Ember.Component.extend({ // xxx: don't run during qunit tests this.appEvents.on('ace:resize', self, self.resize); } + + if (this.get("autofocus")) { + this.send("focus"); + } }); }); + }, + + actions: { + focus() { + if (this._editor) { + this._editor.focus(); + this._editor.navigateFileEnd(); + } + } } }); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 index 0c52b3c8dd..075eba3e94 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 @@ -38,6 +38,11 @@ export default Ember.Controller.extend({ return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html"; }, + @computed("fieldName", "currentTargetName") + editorId(fieldName, currentTarget) { + return fieldName + "|" + currentTarget; + }, + @computed("fieldName", "currentTargetName", "model") activeSection: { get(fieldName, target, model) { diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 index bd1e2c0dd4..c1d3b225ff 100644 --- a/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 @@ -17,7 +17,6 @@ export default Ember.Route.extend({ }; }, - setupController(controller, wrapper) { controller.set("model", wrapper.model); controller.setTargetName(wrapper.target || "common"); diff --git a/app/assets/javascripts/admin/templates/customize-themes-edit.hbs b/app/assets/javascripts/admin/templates/customize-themes-edit.hbs index fbdafdd413..be9ef682f4 100644 --- a/app/assets/javascripts/admin/templates/customize-themes-edit.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes-edit.hbs @@ -42,7 +42,7 @@
    - {{ace-editor content=activeSection mode=activeSectionMode}} + {{ace-editor content=activeSection editorId=editorId mode=activeSectionMode autofocus="true"}}
    {{/unless}} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9f864c1be3..7d3341fc56 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -465,7 +465,7 @@ class ApplicationController < ActionController::Base # type - a machine-readable description of the error # status - HTTP status code to return def render_json_error(obj, opts={}) - opts = { status: opts } if opts.is_a?(Fixnum) + opts = { status: opts } if opts.is_a?(Integer) render json: MultiJson.dump(create_errors_json(obj, opts[:type])), status: opts[:status] || 422 end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a9eaa127e1..d630da7442 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -744,7 +744,7 @@ class UsersController < ApplicationController result = {} - %W{number_of_deleted_posts number_of_flagged_posts number_of_flags_given number_of_suspensions number_of_warnings}.each do |info| + %W{number_of_deleted_posts number_of_flagged_posts number_of_flags_given number_of_suspensions warnings_received_count}.each do |info| result[info] = @user.send(info) end diff --git a/app/models/category.rb b/app/models/category.rb index a533a3863f..0a50c69d7d 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -388,8 +388,8 @@ SQL group = group.id if group.is_a?(Group) # subtle, using Group[] ensures the group exists in the DB - group = Group[group.to_sym].id unless group.is_a?(Fixnum) - permission = CategoryGroup.permission_types[permission] unless permission.is_a?(Fixnum) + group = Group[group.to_sym].id unless group.is_a?(Integer) + permission = CategoryGroup.permission_types[permission] unless permission.is_a?(Integer) [group, permission] end diff --git a/app/models/category_featured_user.rb b/app/models/category_featured_user.rb index db524db0b8..fb75cca87b 100644 --- a/app/models/category_featured_user.rb +++ b/app/models/category_featured_user.rb @@ -8,7 +8,7 @@ class CategoryFeaturedUser < ActiveRecord::Base def self.feature_users_in(category_or_category_id) category_id = - if category_or_category_id.is_a?(Fixnum) + if category_or_category_id.is_a?(Integer) category_or_category_id else category_or_category_id.id diff --git a/app/models/draft_sequence.rb b/app/models/draft_sequence.rb index 0e1c44359e..af4eb647c9 100644 --- a/app/models/draft_sequence.rb +++ b/app/models/draft_sequence.rb @@ -1,7 +1,7 @@ class DraftSequence < ActiveRecord::Base def self.next!(user,key) user_id = user - user_id = user.id unless user.class == Fixnum + user_id = user.id unless user.is_a?(Integer) return 0 if user_id < 0 @@ -19,7 +19,7 @@ class DraftSequence < ActiveRecord::Base return nil unless user user_id = user - user_id = user.id unless user.class == Fixnum + user_id = user.id unless user.is_a?(Integer) # perf critical path r = exec_sql('select sequence from draft_sequences where user_id = ? and draft_key = ?', user_id, key).values diff --git a/app/models/plugin_store.rb b/app/models/plugin_store.rb index ecf8d9c73e..5a7675344e 100644 --- a/app/models/plugin_store.rb +++ b/app/models/plugin_store.rb @@ -42,7 +42,7 @@ class PluginStore def self.cast_value(type, value) case type - when "Fixnum" then value.to_i + when "Integer", "Fixnum" then value.to_i when "TrueClass", "FalseClass" then value == "true" when "JSON" then map_json(::JSON.parse(value)) else value diff --git a/app/models/topic.rb b/app/models/topic.rb index ed3844c121..604c491284 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -118,7 +118,7 @@ class Topic < ActiveRecord::Base has_many :invites, through: :topic_invites, source: :invite has_many :topic_status_updates, dependent: :destroy - has_one :warning + has_one :user_warning has_one :first_post, -> {where post_number: 1}, class_name: Post # When we want to temporarily attach some data to a forum topic (usually before serialization) diff --git a/app/models/user.rb b/app/models/user.rb index 3c41e4c1e2..d47f6749ca 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -37,7 +37,7 @@ class User < ActiveRecord::Base has_many :invites, dependent: :destroy has_many :topic_links, dependent: :destroy has_many :uploads - has_many :warnings + has_many :user_warnings has_many :user_archived_messages, dependent: :destroy has_many :email_change_requests, dependent: :destroy has_many :directory_items, dependent: :delete_all @@ -276,7 +276,7 @@ class User < ActiveRecord::Base def approve(approved_by, send_mail=true) self.approved = true - if approved_by.is_a?(Fixnum) + if approved_by.is_a?(Integer) self.approved_by_id = approved_by else self.approved_by = approved_by @@ -614,7 +614,7 @@ class User < ActiveRecord::Base end def warnings_received_count - warnings.count + user_warnings.count end def flags_received_count @@ -889,10 +889,6 @@ class User < ActiveRecord::Base .count end - def number_of_warnings - self.warnings.count - end - def number_of_suspensions UserHistory.for(self, :suspend_user).count end diff --git a/app/models/warning.rb b/app/models/user_warning.rb similarity index 92% rename from app/models/warning.rb rename to app/models/user_warning.rb index 0d177fb0c6..6bc201a7bc 100644 --- a/app/models/warning.rb +++ b/app/models/user_warning.rb @@ -1,4 +1,4 @@ -class Warning < ActiveRecord::Base +class UserWarning < ActiveRecord::Base belongs_to :user belongs_to :topic belongs_to :created_by, class_name: 'User' diff --git a/db/migrate/20170413043152_rename_warnings.rb b/db/migrate/20170413043152_rename_warnings.rb new file mode 100644 index 0000000000..d4397726be --- /dev/null +++ b/db/migrate/20170413043152_rename_warnings.rb @@ -0,0 +1,9 @@ +class RenameWarnings < ActiveRecord::Migration + def up + rename_table :warnings, :user_warnings + end + + def down + rename_table :user_warnings, :warnings + end +end diff --git a/lib/pretty_text/helpers.rb b/lib/pretty_text/helpers.rb index 1c0c2f0e1a..78378c5cbf 100644 --- a/lib/pretty_text/helpers.rb +++ b/lib/pretty_text/helpers.rb @@ -45,7 +45,7 @@ module PrettyText end def get_topic_info(topic_id) - return unless Fixnum === topic_id + return unless topic_id.is_a?(Integer) # TODO this only handles public topics, secured one do not get this topic = Topic.find_by(id: topic_id) if topic && Guardian.new.can_see?(topic) @@ -76,4 +76,3 @@ module PrettyText end end end - diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb index 70e576f08d..20dd2818af 100644 --- a/lib/site_setting_extension.rb +++ b/lib/site_setting_extension.rb @@ -23,7 +23,7 @@ module SiteSettingExtension def types @types ||= Enum.new(string: 1, time: 2, - fixnum: 3, + integer: 3, float: 4, bool: 5, null: 6, @@ -286,7 +286,7 @@ module SiteSettingExtension val = (val == "t" || val == "true") ? 't' : 'f' end - if type == types[:fixnum] && !val.is_a?(Fixnum) + if type == types[:integer] && !val.is_a?(Integer) val = val.to_i end @@ -295,7 +295,7 @@ module SiteSettingExtension end if type == types[:enum] - val = val.to_i if defaults[name.to_sym].is_a?(Fixnum) + val = val.to_i if defaults[name.to_sym].is_a?(Integer) if enum_class(name) raise Discourse::InvalidParameters.new(:value) unless enum_class(name).valid_value?(val) else @@ -340,9 +340,9 @@ module SiteSettingExtension valid = true type = get_data_type(name, defaults[name.to_sym]) - if type == types[:fixnum] - # validate fixnum - valid = false unless value.to_i.is_a?(Fixnum) + if type == types[:integer] + # validate integer + valid = false unless value.to_i.is_a?(Integer) end valid @@ -407,8 +407,8 @@ module SiteSettingExtension case val when String types[:string] - when Fixnum - types[:fixnum] + when Integer + types[:integer] when Float types[:float] when TrueClass, FalseClass @@ -422,14 +422,14 @@ module SiteSettingExtension case type when types[:float] value.to_f - when types[:fixnum] + when types[:integer] value.to_i when types[:bool] value == true || value == "t" || value == "true" when types[:null] nil when types[:enum] - defaults[name.to_sym].is_a?(Fixnum) ? value.to_i : value + defaults[name.to_sym].is_a?(Integer) ? value.to_i : value else return value if types[type] # Otherwise it's a type error @@ -441,7 +441,7 @@ module SiteSettingExtension @validator_mapping ||= { 'email' => EmailSettingValidator, 'username' => UsernameSettingValidator, - types[:fixnum] => IntegerSettingValidator, + types[:integer] => IntegerSettingValidator, types[:string] => StringSettingValidator, 'list' => StringSettingValidator, 'enum' => StringSettingValidator, diff --git a/lib/topic_creator.rb b/lib/topic_creator.rb index 8c0621d507..ecd35fd116 100644 --- a/lib/topic_creator.rb +++ b/lib/topic_creator.rb @@ -64,7 +64,7 @@ class TopicCreator rollback_with!(topic, :too_many_users) if @added_users.size != 1 # Create a warning record - Warning.create(topic: topic, user: @added_users.first, created_by: @user) + UserWarning.create(topic: topic, user: @added_users.first, created_by: @user) end def watch_topic(topic) diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 3019c5a780..5f0ea2fcf6 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -297,6 +297,8 @@ describe Email::Receiver do end it "supports attached images" do + SiteSetting.queue_jobs = true + expect { process(:no_body_with_image) }.to change { topic.posts.count } expect(topic.posts.last.raw).to match(/https://www.eviltrout.com
    ") end diff --git a/spec/components/onebox/engine/discourse_local_onebox_spec.rb b/spec/components/onebox/engine/discourse_local_onebox_spec.rb index 752eebc6e8..089c07a68b 100644 --- a/spec/components/onebox/engine/discourse_local_onebox_spec.rb +++ b/spec/components/onebox/engine/discourse_local_onebox_spec.rb @@ -71,7 +71,7 @@ describe Onebox::Engine::DiscourseLocalOnebox do it "returns nil if file type is not audio or video" do url = "#{Discourse.base_url}#{path}.pdf" - FakeWeb.register_uri(:get, url, body: "") + stub_request(:get, url).to_return(body: '') expect(Onebox.preview(url).to_s).to eq("") end diff --git a/spec/components/oneboxer_spec.rb b/spec/components/oneboxer_spec.rb index 1df8b30027..a01449f21e 100644 --- a/spec/components/oneboxer_spec.rb +++ b/spec/components/oneboxer_spec.rb @@ -4,9 +4,10 @@ require_dependency 'oneboxer' describe Oneboxer do it "returns blank string for an invalid onebox" do + stub_request(:get, "http://boom.com").to_return(body: "") + expect(Oneboxer.preview("http://boom.com")).to eq("") expect(Oneboxer.onebox("http://boom.com")).to eq("") end end - diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index 9c9f915f52..d2ee5299cf 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -255,6 +255,8 @@ describe PostCreator do it 'creates a post with featured link' do SiteSetting.topic_featured_link_enabled = true SiteSetting.min_first_post_length = 100 + SiteSetting.queue_jobs = true + post = creator_with_featured_link.create expect(post.topic.featured_link).to eq('http://www.discourse.org') expect(post.valid?).to eq(true) @@ -585,7 +587,7 @@ describe PostCreator do it 'acts correctly' do # It's not a warning - expect(post.topic.warning).to be_blank + expect(post.topic.user_warning).to be_blank expect(post.topic.archetype).to eq(Archetype.private_message) expect(post.topic.subtype).to eq(TopicSubtype.user_to_user) @@ -657,11 +659,11 @@ describe PostCreator do topic = post.topic expect(topic).to be_present - expect(topic.warning).to be_present + expect(topic.user_warning).to be_present expect(topic.subtype).to eq(TopicSubtype.moderator_warning) - expect(topic.warning.user).to eq(target_user1) - expect(topic.warning.created_by).to eq(user) - expect(target_user1.warnings.count).to eq(1) + expect(topic.user_warning.user).to eq(target_user1) + expect(topic.user_warning.created_by).to eq(user) + expect(target_user1.user_warnings.count).to eq(1) end end diff --git a/spec/components/site_setting_extension_spec.rb b/spec/components/site_setting_extension_spec.rb index e981f4fbf7..353c04bd4c 100644 --- a/spec/components/site_setting_extension_spec.rb +++ b/spec/components/site_setting_extension_spec.rb @@ -58,7 +58,7 @@ describe SiteSettingExtension do settings.hello = 100 expect(settings.hello).to eq(100) - settings.provider.save(:hello, 99, SiteSetting.types[:fixnum] ) + settings.provider.save(:hello, 99, SiteSetting.types[:integer] ) settings.refresh! expect(settings.hello).to eq(99) @@ -386,16 +386,6 @@ describe SiteSettingExtension do end end - describe "set for an invalid fixnum value" do - it "raises an error" do - settings.setting(:test_setting, 80) - settings.refresh! - expect { - settings.set("test_setting", 9999999999999999999) - }.to raise_error(ArgumentError) - end - end - describe "filter domain name" do before do settings.setting(:white_listed_spam_host_domains, "www.example.com") diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 3dd88514a0..dc1b11c603 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -75,12 +75,12 @@ describe UploadsController do end it 'is successful with synchronous api' do - SiteSetting.stubs(:authorized_extensions).returns("*") + SiteSetting.authorized_extensions = "*" controller.stubs(:is_api?).returns(true) Jobs.expects(:enqueue).with(:create_thumbnails, anything) - FakeWeb.register_uri(:get, "http://example.com/image.png", :body => File.read('spec/fixtures/images/logo.png')) + stub_request(:get, "http://example.com/image.png").to_return(body: File.read('spec/fixtures/images/logo.png')) xhr :post, :create, url: 'http://example.com/image.png', type: "avatar", synchronous: true diff --git a/spec/controllers/user_avatars_controller_spec.rb b/spec/controllers/user_avatars_controller_spec.rb index d09bc36568..603a60a441 100644 --- a/spec/controllers/user_avatars_controller_spec.rb +++ b/spec/controllers/user_avatars_controller_spec.rb @@ -24,7 +24,7 @@ describe UserAvatarsController do SiteSetting.s3_upload_bucket = "test" SiteSetting.s3_cdn_url = "http://cdn.com" - FakeWeb.register_uri(:get, "http://cdn.com/something/else", :body => 'image') + stub_request(:get, "http://cdn.com/something/else").to_return(body: 'image') GlobalSetting.expects(:cdn_url).returns("http://awesome.com/boom") diff --git a/spec/jobs/pull_hotlinked_images_spec.rb b/spec/jobs/pull_hotlinked_images_spec.rb index aa9473a478..bdae4fd3d4 100644 --- a/spec/jobs/pull_hotlinked_images_spec.rb +++ b/spec/jobs/pull_hotlinked_images_spec.rb @@ -5,8 +5,9 @@ describe Jobs::PullHotlinkedImages do before do png = Base64.decode64("R0lGODlhAQABALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//wBiZCH5BAEAAA8ALAAAAAABAAEAAAQC8EUAOw==") - FakeWeb.register_uri(:get, "http://wiki.mozilla.org/images/2/2e/Longcat1.png", body: png) + stub_request(:get, "http://wiki.mozilla.org/images/2/2e/Longcat1.png").to_return(body: png) SiteSetting.download_remote_images_to_local = true + FastImage.expects(:size).returns([100, 100]).at_least_once end it 'replaces image src' do diff --git a/spec/jobs/update_gravatar_spec.rb b/spec/jobs/update_gravatar_spec.rb index 0c80a3afcf..bba53c724a 100644 --- a/spec/jobs/update_gravatar_spec.rb +++ b/spec/jobs/update_gravatar_spec.rb @@ -8,7 +8,7 @@ describe Jobs::UpdateGravatar do expect(user.user_avatar.gravatar_upload_id).to eq(nil) png = Base64.decode64("R0lGODlhAQABALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//wBiZCH5BAEAAA8ALAAAAAABAAEAAAQC8EUAOw==") - FakeWeb.register_uri(:get, "http://www.gravatar.com/avatar/d10ca8d11301c2f4993ac2279ce4b930.png?s=500&d=404", body: png) + stub_request(:get, "http://www.gravatar.com/avatar/d10ca8d11301c2f4993ac2279ce4b930.png?s=360&d=404").to_return(body: png) SiteSetting.automatically_download_gravatars = true diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 9409f66ae2..8100e92782 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1155,6 +1155,7 @@ describe User do describe "refresh_avatar" do it "enqueues the update_gravatar job when automatically downloading gravatars" do SiteSetting.automatically_download_gravatars = true + SiteSetting.queue_jobs = true user = Fabricate(:user) diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index cc2ac9b91b..985bea4fb6 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -9,17 +9,14 @@ require 'rbtrace' #uncomment the following line to use spork with the debugger #require 'spork/ext/ruby-debug' -require 'fakeweb' -FakeWeb.allow_net_connect = false - Spork.prefork do # Loading more in this block will cause your tests to run faster. However, # if you change any configuration or code from libraries loaded here, you'll # need to restart spork for it take effect. require 'fabrication' require 'mocha/api' - require 'fakeweb' require 'certified' + require 'webmock/rspec' ENV["RAILS_ENV"] ||= 'test' require File.expand_path("../../config/environment", __FILE__) From afe586329f70f7b9cf9751d2bd270b22fa151081 Mon Sep 17 00:00:00 2001 From: jomaxro Date: Sat, 15 Apr 2017 02:52:20 -0400 Subject: [PATCH 079/221] Use HTTPS for links to *.discourse.org --- config/locales/server.en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index e042e5742b..d48ca5662c 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -47,7 +47,7 @@ en: topics: "Topics" posts: "posts" loading: "Loading" - powered_by_html: 'Powered by Discourse, best viewed with JavaScript enabled' + powered_by_html: 'Powered by Discourse, best viewed with JavaScript enabled' log_in: "Log In" purge_reason: "Automatically deleted as abandoned, deactivated account" @@ -1911,7 +1911,7 @@ en: usage_tips: text_body_template: | - For a few quick tips on getting started as a new user, [check out this blog post](http://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/). + For a few quick tips on getting started as a new user, [check out this blog post](https://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/). As you participate here, we’ll get to know you, and temporary new user limitations will be lifted. Over time you’ll gain [trust levels](https://meta.discourse.org/t/what-do-user-trust-levels-do/4924) that include special abilities to help us manage our community together. From 0954367bf42fc09cb40a2bf1b6b5a3776c3d7439 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Sat, 15 Apr 2017 14:48:05 +0530 Subject: [PATCH 080/221] FIX: send activation email when accepting invite if password is set --- app/controllers/invites_controller.rb | 20 +++++++++++++------- app/models/invite_redeemer.rb | 8 -------- app/models/user.rb | 2 +- spec/controllers/invites_controller_spec.rb | 9 ++++++++- spec/models/invite_redeemer_spec.rb | 1 - 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index e4990e0af1..176b067c54 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -36,9 +36,7 @@ class InvitesController < ApplicationController user = invite.redeem(username: params[:username], password: params[:password]) if user.present? log_on_user(user) - - # Send a welcome message if required - user.enqueue_welcome_message('welcome_invite') if user.send_welcome_message + post_process_invite(user) end topic = user.present? ? invite.topics.first : nil @@ -128,10 +126,7 @@ class InvitesController < ApplicationController user = Invite.redeem_from_token(params[:token], params[:email], params[:username], params[:name], params[:topic].to_i) if user.present? log_on_user(user) - - # Send a welcome message if required - user.enqueue_welcome_message('welcome_invite') if user.send_welcome_message - + post_process_invite(user) topic = invite.topics.first if topic.present? redirect_to path("#{topic.relative_url}") @@ -223,4 +218,15 @@ class InvitesController < ApplicationController false end end + + private + + def post_process_invite(user) + user.enqueue_welcome_message('welcome_invite') if user.send_welcome_message + if user.has_password? + email_token = user.email_tokens.create(email: user.email) + Jobs.enqueue(:critical_user_email, type: :signup, user_id: user.id, email_token: email_token.token) + end + end + end diff --git a/app/models/invite_redeemer.rb b/app/models/invite_redeemer.rb index 6f1406b06a..d12f0c57bb 100644 --- a/app/models/invite_redeemer.rb +++ b/app/models/invite_redeemer.rb @@ -62,7 +62,6 @@ InviteRedeemer = Struct.new(:invite, :username, :name, :password) do send_welcome_message notify_invitee send_password_instructions - enqueue_activation_mail delete_duplicate_invites end @@ -127,13 +126,6 @@ InviteRedeemer = Struct.new(:invite, :username, :name, :password) do end end - def enqueue_activation_mail - if invited_user.has_password? - email_token = invited_user.email_tokens.create(email: invited_user.email) - Jobs.enqueue(:critical_user_email, type: :signup, user_id: invited_user.id, email_token: email_token.token) - end - end - def notify_invitee if inviter = invite.invited_by inviter.notifications.create(notification_type: Notification.types[:invitee_accepted], diff --git a/app/models/user.rb b/app/models/user.rb index d47f6749ca..f3d7e325c6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -797,7 +797,7 @@ class User < ActiveRecord::Base end def find_email - last_sent_email_address || email + last_sent_email_address.present? && EmailValidator.email_regex =~ last_sent_email_address ? last_sent_email_address : email end def tl3_requirements diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb index 146a007753..741d9a35ff 100644 --- a/spec/controllers/invites_controller_spec.rb +++ b/spec/controllers/invites_controller_spec.rb @@ -223,10 +223,11 @@ describe InvitesController do end end - context 'welcome message' do + context 'welcome message and activation email' do before do Invite.any_instance.stubs(:redeem).returns(user) Jobs.expects(:enqueue).with(:invite_email, has_key(:invite_id)) + user.password_hash = nil end it 'sends a welcome message if set' do @@ -239,6 +240,12 @@ describe InvitesController do user.expects(:enqueue_welcome_message).with('welcome_invite').never xhr :put, :perform_accept_invitation, id: invite.invite_key, format: :json end + + it 'sends an activation email if password is set' do + user.password_hash = 'qaw3ni3h2wyr63lakw7pea1nrtr44pls' + Jobs.expects(:enqueue).with(:critical_user_email, has_entries(type: :signup, user_id: user.id)) + xhr :put, :perform_accept_invitation, id: invite.invite_key, format: :json + end end end end diff --git a/spec/models/invite_redeemer_spec.rb b/spec/models/invite_redeemer_spec.rb index 39d718508e..c8e3be646a 100644 --- a/spec/models/invite_redeemer_spec.rb +++ b/spec/models/invite_redeemer_spec.rb @@ -89,7 +89,6 @@ describe InviteRedeemer do it "can set password" do inviter = invite.invited_by - Jobs.expects(:enqueue).with(:critical_user_email, has_entries(type: :signup)) user = InviteRedeemer.new(invite, username, name, password).redeem expect(user).to have_password expect(user.confirm_password?(password)).to eq(true) From 73898319615303b31a79fcc81fc5100b469b3f83 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Sat, 15 Apr 2017 15:52:35 +0530 Subject: [PATCH 081/221] Fix the build --- spec/models/invite_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb index 0080d9267e..48cac4af4d 100644 --- a/spec/models/invite_spec.rb +++ b/spec/models/invite_spec.rb @@ -230,7 +230,6 @@ describe Invite do it 'does not enqueue an email if the user has already set password' do Fabricate(:user, email: invite.email, password_hash: "7af7805c9ee3697ed1a83d5e3cb5a3a431d140933a87fdcdc5a42aeef9337f81") Jobs.expects(:enqueue).with(:invite_password_instructions_email, has_key(:username)).never - Jobs.expects(:enqueue).with(:critical_user_email, has_entries(type: :signup)) # should enqueue an account activation email invite.redeem end From 5f47603b0766ef8c32d475ab5a9e99082fad25a0 Mon Sep 17 00:00:00 2001 From: jloh Date: Sat, 15 Apr 2017 21:06:51 +1000 Subject: [PATCH 082/221] FIX: increase host_must_be_valid regex for .technology TLD .technology is 11 chars long and wasn't being matched previously --- app/models/embeddable_host.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/embeddable_host.rb b/app/models/embeddable_host.rb index f94ce56b1b..1bcd29af02 100644 --- a/app/models/embeddable_host.rb +++ b/app/models/embeddable_host.rb @@ -39,7 +39,7 @@ class EmbeddableHost < ActiveRecord::Base private def host_must_be_valid - if host !~ /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,7}(:[0-9]{1,5})?(\/.*)?\Z/i && + if host !~ /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,10}(:[0-9]{1,5})?(\/.*)?\Z/i && host !~ /\A(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(:[0-9]{1,5})?(\/.*)?\Z/ && host !~ /\A([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.)?localhost(\:[0-9]{1,5})?(\/.*)?\Z/i errors.add(:host, I18n.t('errors.messages.invalid')) From 6e5296a510b31b01804105f57d3998a9a96d655e Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 17 Apr 2017 10:11:51 -0400 Subject: [PATCH 083/221] FEATURE: upgrade sprockets to latest stable This unlocks Rails upgrades, so we can now upgrade to latest Rails --- Gemfile.lock | 4 ++-- lib/es6_module_transpiler/sprockets.rb | 3 ++- .../tilt/es6_module_transpiler_template.rb | 9 +++++++++ lib/source_url.rb | 9 +++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 64a64c3dc6..e7357029e2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -358,10 +358,10 @@ GEM spork-rails (4.0.0) rails (>= 3.0.0, < 5) spork (>= 1.0rc0) - sprockets (3.6.3) + sprockets (3.7.1) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.1.1) + sprockets-rails (3.2.0) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) diff --git a/lib/es6_module_transpiler/sprockets.rb b/lib/es6_module_transpiler/sprockets.rb index 1a62c5cbfb..9da657f03c 100644 --- a/lib/es6_module_transpiler/sprockets.rb +++ b/lib/es6_module_transpiler/sprockets.rb @@ -1,3 +1,4 @@ require 'sprockets' -Sprockets.register_engine '.es6', Tilt::ES6ModuleTranspilerTemplate +Sprockets.register_mime_type 'application/ecmascript6', extensions: ['.es6', '.js.es6'], charset: :unicode +Sprockets.register_transformer 'application/ecmascript6', 'application/javascript', Tilt::ES6ModuleTranspilerTemplate diff --git a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb index d18afbf3c9..f093a0a30e 100644 --- a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb +++ b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb @@ -10,6 +10,15 @@ module Tilt @mutex = Mutex.new @ctx_init = Mutex.new + def self.call(input) + filename = input[:filename] + source = input[:data] + context = input[:environment].context_class.new(input) + + result = new(filename){source}.render(context) + context.metadata.merge(data: result) + end + def prepare # intentionally left empty # Tilt requires this method to be defined diff --git a/lib/source_url.rb b/lib/source_url.rb index c089d18eb6..41d4fe5dec 100644 --- a/lib/source_url.rb +++ b/lib/source_url.rb @@ -1,6 +1,15 @@ class SourceURL < Tilt::Template self.default_mime_type = 'application/javascript' + def self.call(input) + filename = input[:filename] + source = input[:data] + context = input[:environment].context_class.new(input) + + result = new(filename){source}.render(context) + context.metadata.merge(data: result) + end + def prepare end From cfef100ed704361d86dcdbaeda76dd8649e321ef Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 17 Apr 2017 11:04:00 -0400 Subject: [PATCH 084/221] FIX: ignore loose .es6 files during precompile also corrects precompile statement to include preload store --- config/application.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/application.rb b/config/application.rb index 2e82a8ee65..4ef778ab19 100644 --- a/config/application.rb +++ b/config/application.rb @@ -79,7 +79,7 @@ module Discourse end] config.assets.precompile += %w{ - vendor.js admin.js preload-store.js.es6 + vendor.js admin.js preload-store.js browser-update.js break_string.js ember_jquery.js pretty-text-bundle.js wizard-application.js wizard-vendor.js plugin.js plugin-third-party.js @@ -95,7 +95,7 @@ module Discourse initializer :fix_sprockets_loose_file_searcher, after: :set_default_precompile do |app| app.config.assets.precompile.delete(Sprockets::Railtie::LOOSE_APP_ASSETS) start_path = ::Rails.root.join("app/assets").to_s - exclude = ['.hbs', '.js', '.css', ''] + exclude = ['.es6', '.hbs', '.js', '.css', ''] app.config.assets.precompile << lambda do |logical_path, filename| filename.start_with?(start_path) && !exclude.include?(File.extname(logical_path)) From 5dd752877e0a23c258d9df018ba10d9c6eda97c3 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 17 Apr 2017 11:52:43 -0400 Subject: [PATCH 085/221] FEATURE: try adding some preload hints for chrome --- app/helpers/application_helper.rb | 12 ++++++------ .../common/_discourse_javascript.html.erb | 2 +- app/views/common/_special_font_face.html.erb | 1 + app/views/layouts/application.html.erb | 18 +++++++++--------- app/views/layouts/embed.html.erb | 2 +- app/views/layouts/finish_installation.html.erb | 4 ++-- lib/stylesheet/manager.rb | 2 +- 7 files changed, 21 insertions(+), 20 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 67651cc0a5..e534aa50da 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -45,17 +45,17 @@ module ApplicationHelper end end - def script(*args) + def preload_script(script) + path = asset_path("#{script}.js") + if GlobalSetting.cdn_url && GlobalSetting.cdn_url.start_with?("https") && ENV["COMPRESS_BROTLI"] == "1" && request.env["HTTP_ACCEPT_ENCODING"] =~ /br/ - tags = javascript_include_tag(*args) - tags.gsub!("#{GlobalSetting.cdn_url}/assets/", "#{GlobalSetting.cdn_url}/brotli_asset/") - tags.html_safe - else - javascript_include_tag(*args) + path.gsub!("#{GlobalSetting.cdn_url}/assets/", "#{GlobalSetting.cdn_url}/brotli_asset/") end +" +".html_safe end def discourse_csrf_tags diff --git a/app/views/common/_discourse_javascript.html.erb b/app/views/common/_discourse_javascript.html.erb index 9e2da0a900..05e0a95f94 100644 --- a/app/views/common/_discourse_javascript.html.erb +++ b/app/views/common/_discourse_javascript.html.erb @@ -65,4 +65,4 @@ })(); -<%= script 'browser-update' %> +<%= preload_script 'browser-update' %> diff --git a/app/views/common/_special_font_face.html.erb b/app/views/common/_special_font_face.html.erb index 8e95e5ece8..2462461c24 100644 --- a/app/views/common/_special_font_face.html.erb +++ b/app/views/common/_special_font_face.html.erb @@ -10,6 +10,7 @@ <% font_domain = "#{request.protocol}#{request.host_with_port}&2" %> +" as="font" type="font/woff2" crossorigin /> From 81c08fe451a5ae1d747d09418975cc4fb92b68fe Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 20 Apr 2017 11:52:06 -0400 Subject: [PATCH 132/221] missing badge count in dark theme --- app/assets/stylesheets/common/base/user-badges.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/user-badges.scss b/app/assets/stylesheets/common/base/user-badges.scss index 3c87530f5b..7c25c2652a 100644 --- a/app/assets/stylesheets/common/base/user-badges.scss +++ b/app/assets/stylesheets/common/base/user-badges.scss @@ -142,7 +142,7 @@ right: 5px; top: 5px; font-weight: bold; - color: dark-light-diff($primary, $secondary, 50%, -65%); + color: dark-light-diff($primary, $secondary, 50%, -15%); font-size: 1.2em; } From 3d3215889326a256e571dfc4754fcc3df5475811 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 20 Apr 2017 11:53:31 -0400 Subject: [PATCH 133/221] remove important from extra-info-wrapper color --- app/assets/stylesheets/common/base/tagging.scss | 2 +- app/assets/stylesheets/common/components/badges.scss | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/common/base/tagging.scss b/app/assets/stylesheets/common/base/tagging.scss index f691175120..5d6ad795ec 100644 --- a/app/assets/stylesheets/common/base/tagging.scss +++ b/app/assets/stylesheets/common/base/tagging.scss @@ -75,7 +75,7 @@ $tag-color: scale-color($primary, $lightness: 40%); } .extra-info-wrapper & { - color: $header-primary !important; + color: $header-primary; } &.box { diff --git a/app/assets/stylesheets/common/components/badges.scss b/app/assets/stylesheets/common/components/badges.scss index 8273d97c8b..d699ac77ee 100644 --- a/app/assets/stylesheets/common/components/badges.scss +++ b/app/assets/stylesheets/common/components/badges.scss @@ -37,7 +37,7 @@ text-overflow: ellipsis; .extra-info-wrapper & { - color: $header-primary !important; + color: $header-primary; } } @@ -69,7 +69,7 @@ } .extra-info-wrapper & { - color: $header-primary !important; + color: $header-primary; } } From dfe3c162ccb2473ae8c1acf47f593d92bb573c87 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 20 Apr 2017 12:34:40 -0400 Subject: [PATCH 134/221] move stylesheet after js --- app/views/layouts/application.html.erb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 9e94ba7695..984b0e198a 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -6,8 +6,6 @@ <%= render partial: "layouts/head" %> - <%= render partial: "common/special_font_face" %> - <%= render partial: "common/discourse_stylesheet" %> <%= discourse_csrf_tags %> <%- if SiteSetting.enable_escaped_fragments? %> @@ -52,6 +50,9 @@ <%- end %> + <%= render partial: "common/discourse_stylesheet" %> + <%= render partial: "common/special_font_face" %> + <%= yield :head %> <%= raw build_plugin_html 'before-head-close' %> From 5d9d2cf2871ce127c207f2efa192c2eb2eeb35f2 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Thu, 20 Apr 2017 21:45:52 +0530 Subject: [PATCH 135/221] FIX: do not explicitly show email of flagger / flagged user --- app/serializers/flagged_user_serializer.rb | 1 - lib/guardian.rb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/serializers/flagged_user_serializer.rb b/app/serializers/flagged_user_serializer.rb index 899602b9fe..c9ec0aa6e8 100644 --- a/app/serializers/flagged_user_serializer.rb +++ b/app/serializers/flagged_user_serializer.rb @@ -3,7 +3,6 @@ class FlaggedUserSerializer < BasicUserSerializer :can_be_deleted, :post_count, :topic_count, - :email, :ip_address def can_delete_all_posts diff --git a/lib/guardian.rb b/lib/guardian.rb index 9bd211e498..48af49e63d 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -159,7 +159,7 @@ class Guardian end def can_view_action_logs?(target) - is_staff? && target + target.present? && is_staff? end # Can we approve it? From 05efa7ce68809381172b03dbfeed331efd8c03dc Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 20 Apr 2017 15:26:20 -0400 Subject: [PATCH 136/221] simplify last_notified_id --- app/jobs/scheduled/pending_flags_reminder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/scheduled/pending_flags_reminder.rb b/app/jobs/scheduled/pending_flags_reminder.rb index 1b8080a049..895c6c2492 100644 --- a/app/jobs/scheduled/pending_flags_reminder.rb +++ b/app/jobs/scheduled/pending_flags_reminder.rb @@ -36,7 +36,7 @@ module Jobs end def last_notified_id - (i = $redis.get(self.class.last_notified_key)) && i.to_i + $redis.get(self.class.last_notified_key)&.to_i end def last_notified_id=(arg) From 7a9eee1b71cf298210ee9a258e9f17ae1a8a9af7 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 20 Apr 2017 15:47:25 -0400 Subject: [PATCH 137/221] FEATURE: default notification level for group messages also fixes it so staff can amend other user's group notification level --- .../admin/controllers/admin-group.js.es6 | 4 +-- .../javascripts/admin/templates/group.hbs | 6 ++++ .../group-notifications-button.js.es6 | 2 +- .../components/notifications-button.js.es6 | 4 +-- .../javascripts/discourse/models/group.js.es6 | 7 ++-- .../discourse/templates/user/messages.hbs | 2 +- app/controllers/admin/groups_controller.rb | 7 +++- app/controllers/groups_controller.rb | 7 +++- app/models/color_scheme.rb | 1 + app/models/group.rb | 1 + app/models/group_user.rb | 5 +++ app/models/theme_field.rb | 1 + app/models/user_warning.rb | 6 ++-- app/serializers/basic_group_serializer.rb | 3 +- config/locales/client.en.yml | 1 + ...add_default_notification_level_to_group.rb | 11 +++++++ spec/models/group_user_spec.rb | 33 +++++++++++++++++++ 17 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 db/migrate/20170420163628_add_default_notification_level_to_group.rb create mode 100644 spec/models/group_user_spec.rb diff --git a/app/assets/javascripts/admin/controllers/admin-group.js.es6 b/app/assets/javascripts/admin/controllers/admin-group.js.es6 index 6c3da11d7f..a0f4961b62 100644 --- a/app/assets/javascripts/admin/controllers/admin-group.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-group.js.es6 @@ -22,9 +22,9 @@ export default Ember.Controller.extend({ ]; }.property(), - @computed('model.visible', 'model.public', 'model.alias_level') + @computed('model.visible', 'model.public') disableMembershipRequestSetting(visible, publicGroup) { - return !visible || publicGroup || !this.get('model.canEveryoneMention'); + return !visible || publicGroup; }, @computed('model.visible', 'model.allow_membership_requests') diff --git a/app/assets/javascripts/admin/templates/group.hbs b/app/assets/javascripts/admin/templates/group.hbs index 095b3f5f4b..79dbcfc3b6 100644 --- a/app/assets/javascripts/admin/templates/group.hbs +++ b/app/assets/javascripts/admin/templates/group.hbs @@ -83,6 +83,12 @@ {{combo-box name="alias" valueAttribute="value" value=model.alias_level content=aliasLevelOptions}}
    +
    + + {{notifications-button i18nPrefix='groups.notifications' notificationLevel=model.default_notification_level}} +
    +
    + {{#unless model.automatic}}
    diff --git a/app/assets/javascripts/discourse/components/group-notifications-button.js.es6 b/app/assets/javascripts/discourse/components/group-notifications-button.js.es6 index 8f70ca102a..ef2386086f 100644 --- a/app/assets/javascripts/discourse/components/group-notifications-button.js.es6 +++ b/app/assets/javascripts/discourse/components/group-notifications-button.js.es6 @@ -6,6 +6,6 @@ export default NotificationsButton.extend({ i18nPrefix: 'groups.notifications', clicked(id) { - this.get('group').setNotification(id); + this.get('group').setNotification(id, this.get('user.id')); } }); diff --git a/app/assets/javascripts/discourse/components/notifications-button.js.es6 b/app/assets/javascripts/discourse/components/notifications-button.js.es6 index 10f5d54f26..d01f17e43d 100644 --- a/app/assets/javascripts/discourse/components/notifications-button.js.es6 +++ b/app/assets/javascripts/discourse/components/notifications-button.js.es6 @@ -43,7 +43,7 @@ export default DropdownButton.extend({ } }, - clicked(/* id */) { - // sub-class needs to implement this + clicked(id) { + this.set("notificationLevel", id); } }); diff --git a/app/assets/javascripts/discourse/models/group.js.es6 b/app/assets/javascripts/discourse/models/group.js.es6 index 7d1ab55e70..2af528f818 100644 --- a/app/assets/javascripts/discourse/models/group.js.es6 +++ b/app/assets/javascripts/discourse/models/group.js.es6 @@ -139,7 +139,8 @@ const Group = RestModel.extend({ bio_raw: this.get('bio_raw'), public: this.get('public'), allow_membership_requests: this.get('allow_membership_requests'), - full_name: this.get('full_name') + full_name: this.get('full_name'), + default_notification_level: this.get('default_notification_level') }; }, @@ -191,10 +192,10 @@ const Group = RestModel.extend({ }); }, - setNotification(notification_level) { + setNotification(notification_level, userId) { this.set("group_user.notification_level", notification_level); return ajax(`/groups/${this.get("name")}/notifications`, { - data: { notification_level }, + data: { notification_level, user_id: userId }, type: "POST" }); } diff --git a/app/assets/javascripts/discourse/templates/user/messages.hbs b/app/assets/javascripts/discourse/templates/user/messages.hbs index d9be88452e..ece0c407b7 100644 --- a/app/assets/javascripts/discourse/templates/user/messages.hbs +++ b/app/assets/javascripts/discourse/templates/user/messages.hbs @@ -73,7 +73,7 @@ {{/if}} {{#if isGroup}} - {{group-notifications-button group=group}} + {{group-notifications-button group=group user=model}} {{/if}}
    diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index d2a93629fc..e963e506ed 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -71,6 +71,10 @@ class Admin::GroupsController < Admin::AdminController group.bio_raw = group_params[:bio_raw] if group_params[:bio_raw] group.full_name = group_params[:full_name] if group_params[:full_name] + if group_params.key?(:default_notification_level) + group.default_notification_level = group_params[:default_notification_level] + end + if group_params[:allow_membership_requests] group.allow_membership_requests = group_params[:allow_membership_requests] end @@ -150,7 +154,8 @@ class Admin::GroupsController < Admin::AdminController :name, :alias_level, :visible, :automatic_membership_email_domains, :automatic_membership_retroactive, :title, :primary_group, :grant_trust_level, :incoming_email, :flair_url, :flair_bg_color, - :flair_color, :bio_raw, :public, :allow_membership_requests, :full_name + :flair_color, :bio_raw, :public, :allow_membership_requests, :full_name, + :default_notification_level ) end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 1314a763e2..1a027838d7 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -245,8 +245,13 @@ class GroupsController < ApplicationController group = find_group(:id) notification_level = params.require(:notification_level) + user_id = current_user.id + if guardian.is_staff? + user_id = params[:user_id] || user_id + end + GroupUser.where(group_id: group.id) - .where(user_id: current_user.id) + .where(user_id: user_id) .update_all(notification_level: notification_level) render json: success_json diff --git a/app/models/color_scheme.rb b/app/models/color_scheme.rb index 4c4690e162..6701f4cdf6 100644 --- a/app/models/color_scheme.rb +++ b/app/models/color_scheme.rb @@ -194,4 +194,5 @@ end # updated_at :datetime not null # via_wizard :boolean default(FALSE), not null # base_scheme_id :string +# theme_id :integer # diff --git a/app/models/group.rb b/app/models/group.rb index 74a4126b21..6d6a17d634 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -552,6 +552,7 @@ end # public :boolean default(FALSE), not null # allow_membership_requests :boolean default(FALSE), not null # full_name :string +# default_notification_level :integer default(3), not null # # Indexes # diff --git a/app/models/group_user.rb b/app/models/group_user.rb index 8d1e3beba5..b559580390 100644 --- a/app/models/group_user.rb +++ b/app/models/group_user.rb @@ -10,6 +10,7 @@ class GroupUser < ActiveRecord::Base after_save :set_primary_group after_destroy :remove_primary_group + before_create :set_notification_level after_save :grant_trust_level def self.notification_levels @@ -18,6 +19,10 @@ class GroupUser < ActiveRecord::Base protected + def set_notification_level + self.notification_level = group&.default_notification_level || 3 + end + def set_primary_group if group.primary_group self.class.exec_sql("UPDATE users diff --git a/app/models/theme_field.rb b/app/models/theme_field.rb index 3c67c58945..9f80dd1c56 100644 --- a/app/models/theme_field.rb +++ b/app/models/theme_field.rb @@ -137,6 +137,7 @@ end # created_at :datetime # updated_at :datetime # compiler_version :integer default(0), not null +# error :string # # Indexes # diff --git a/app/models/user_warning.rb b/app/models/user_warning.rb index 6bc201a7bc..dd89c7f995 100644 --- a/app/models/user_warning.rb +++ b/app/models/user_warning.rb @@ -6,7 +6,7 @@ end # == Schema Information # -# Table name: warnings +# Table name: user_warnings # # id :integer not null, primary key # topic_id :integer not null @@ -17,6 +17,6 @@ end # # Indexes # -# index_warnings_on_topic_id (topic_id) UNIQUE -# index_warnings_on_user_id (user_id) +# index_user_warnings_on_topic_id (topic_id) UNIQUE +# index_user_warnings_on_user_id (user_id) # diff --git a/app/serializers/basic_group_serializer.rb b/app/serializers/basic_group_serializer.rb index b37184e723..ea3df8e08a 100644 --- a/app/serializers/basic_group_serializer.rb +++ b/app/serializers/basic_group_serializer.rb @@ -19,7 +19,8 @@ class BasicGroupSerializer < ApplicationSerializer :bio_cooked, :public, :allow_membership_requests, - :full_name + :full_name, + :default_notification_level def include_incoming_email? staff? diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 8c9540977d..98265b0275 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -446,6 +446,7 @@ en: posts: "Posts" mentions: "Mentions" messages: "Messages" + notification_level: "Default notification level for group messages" alias_levels: title: "Who can message and @mention this group?" nobody: "Nobody" diff --git a/db/migrate/20170420163628_add_default_notification_level_to_group.rb b/db/migrate/20170420163628_add_default_notification_level_to_group.rb new file mode 100644 index 0000000000..dee6ae771e --- /dev/null +++ b/db/migrate/20170420163628_add_default_notification_level_to_group.rb @@ -0,0 +1,11 @@ +class AddDefaultNotificationLevelToGroup < ActiveRecord::Migration + def up + add_column :groups, :default_notification_level, :integer, default: 3, null: false + # don't auto watch 'moderators' it is just way too loud + execute 'UPDATE groups SET default_notification_level = 2 WHERE id = 2' + end + + def down + remove_column :groups, :default_notification_level + end +end diff --git a/spec/models/group_user_spec.rb b/spec/models/group_user_spec.rb new file mode 100644 index 0000000000..c7675984cf --- /dev/null +++ b/spec/models/group_user_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' + +describe GroupUser do + + it 'correctly sets notification level' do + moderator = Fabricate(:moderator) + + Group.refresh_automatic_groups!(:moderators) + gu = GroupUser.find_by(user_id: moderator.id, group_id: Group::AUTO_GROUPS[:moderators]) + + expect(gu.notification_level).to eq(NotificationLevels.all[:tracking]) + + group = Group.create!(name: 'bob') + group.add(moderator) + group.save + + gu = GroupUser.find_by(user_id: moderator.id, group_id: group.id) + expect(gu.notification_level).to eq(NotificationLevels.all[:watching]) + + group.remove(moderator) + group.save + + group.default_notification_level = 1 + group.save + + group.add(moderator) + group.save + + gu = GroupUser.find_by(user_id: moderator.id, group_id: group.id) + expect(gu.notification_level).to eq(NotificationLevels.all[:regular]) + end + +end From b3eef0513f541872ab01b7a07503896cc00bb804 Mon Sep 17 00:00:00 2001 From: jomaxro Date: Thu, 20 Apr 2017 16:08:16 -0400 Subject: [PATCH 138/221] Remove console logging --- app/assets/javascripts/admin/models/theme.js.es6 | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/admin/models/theme.js.es6 b/app/assets/javascripts/admin/models/theme.js.es6 index 8beaa9cbf0..be31a50777 100644 --- a/app/assets/javascripts/admin/models/theme.js.es6 +++ b/app/assets/javascripts/admin/models/theme.js.es6 @@ -32,8 +32,6 @@ const Theme = RestModel.extend({ getError(target, name) { let themeFields = this.get("themeFields"); let key = target + " " + name; - console.log(themeFields); - console.log(key); let field = themeFields[key]; return field ? field.error : ""; }, From 703e6faf5419304433c647c12ae8be5af335e312 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 20 Apr 2017 16:27:10 -0400 Subject: [PATCH 139/221] Revert "remove important from extra-info-wrapper color" --- app/assets/stylesheets/common/base/tagging.scss | 2 +- app/assets/stylesheets/common/components/badges.scss | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/common/base/tagging.scss b/app/assets/stylesheets/common/base/tagging.scss index 5d6ad795ec..f691175120 100644 --- a/app/assets/stylesheets/common/base/tagging.scss +++ b/app/assets/stylesheets/common/base/tagging.scss @@ -75,7 +75,7 @@ $tag-color: scale-color($primary, $lightness: 40%); } .extra-info-wrapper & { - color: $header-primary; + color: $header-primary !important; } &.box { diff --git a/app/assets/stylesheets/common/components/badges.scss b/app/assets/stylesheets/common/components/badges.scss index d699ac77ee..8273d97c8b 100644 --- a/app/assets/stylesheets/common/components/badges.scss +++ b/app/assets/stylesheets/common/components/badges.scss @@ -37,7 +37,7 @@ text-overflow: ellipsis; .extra-info-wrapper & { - color: $header-primary; + color: $header-primary !important; } } @@ -69,7 +69,7 @@ } .extra-info-wrapper & { - color: $header-primary; + color: $header-primary !important; } } From 5365973c0ac6c00c8a554e0e23682e9f9493ef05 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 20 Apr 2017 16:55:09 -0400 Subject: [PATCH 140/221] FIX: detection of error changed, so you can clear errors --- app/models/theme_field.rb | 8 +++++--- spec/models/theme_field_spec.rb | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/models/theme_field.rb b/app/models/theme_field.rb index 9f80dd1c56..65b89db514 100644 --- a/app/models/theme_field.rb +++ b/app/models/theme_field.rb @@ -95,10 +95,12 @@ COMPILED self.error = nil unless error.nil? rescue SassC::SyntaxError => e self.error = e.message - if error_changed? - update_columns(error: self.error) - end end + + if error_changed? + update_columns(error: self.error) + end + end end diff --git a/spec/models/theme_field_spec.rb b/spec/models/theme_field_spec.rb index abd341f07d..f120526e1c 100644 --- a/spec/models/theme_field_spec.rb +++ b/spec/models/theme_field_spec.rb @@ -21,8 +21,10 @@ HTML field = ThemeField.create!(theme_id: 1, target: 0, name: "scss", value: css) field.reload expect(field.error).not_to eq(nil) - field.value = "" + field.value = "body {color: blue};" field.save! + field.reload + expect(field.error).to eq(nil) end end From d4111c8676b4c7cc85d570ab7d74246506e63fcc Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 20 Apr 2017 17:24:21 -0400 Subject: [PATCH 141/221] correct spec --- spec/controllers/admin/groups_controller_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb index 64fd305e0e..e818241514 100644 --- a/spec/controllers/admin/groups_controller_spec.rb +++ b/spec/controllers/admin/groups_controller_spec.rb @@ -42,7 +42,8 @@ describe Admin::GroupsController do "bio_cooked"=>nil, "public"=>false, "allow_membership_requests"=>false, - "full_name"=>group.full_name + "full_name"=>group.full_name, + "default_notification_level"=>3 }]) end From dcf9459b7c57dadb4306d5ddf64380995c6068cb Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 20 Apr 2017 17:32:05 -0400 Subject: [PATCH 142/221] staff should track --- app/models/group.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/group.rb b/app/models/group.rb index 6d6a17d634..4bb6b1483c 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -171,6 +171,7 @@ class Group < ActiveRecord::Base unless group = self.lookup_group(name) group = Group.new(name: name.to_s, automatic: true) + group.default_notification_level = 2 if AUTO_GROUPS[:staff] == id group.id = id group.save! end From a9b953ac5a305602a0633814290c7f00d9ee3423 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 20 Apr 2017 17:37:13 -0400 Subject: [PATCH 143/221] FIX: on create you could not add a theme component --- app/assets/javascripts/admin/models/theme.js.es6 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/javascripts/admin/models/theme.js.es6 b/app/assets/javascripts/admin/models/theme.js.es6 index be31a50777..229d7ee0c0 100644 --- a/app/assets/javascripts/admin/models/theme.js.es6 +++ b/app/assets/javascripts/admin/models/theme.js.es6 @@ -73,6 +73,10 @@ const Theme = RestModel.extend({ addChildTheme(theme){ let childThemes = this.get("childThemes"); + if (!childThemes) { + childThemes = []; + this.set('childThemes', childThemes); + } childThemes.removeObject(theme); childThemes.pushObject(theme); return this.saveChanges("child_theme_ids"); From 47b99e09223e850962f42c8caebb1140fc743959 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 20 Apr 2017 17:38:46 -0400 Subject: [PATCH 144/221] group is now default watched --- spec/models/post_action_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/post_action_spec.rb b/spec/models/post_action_spec.rb index 9ed0e70499..5d61df639b 100644 --- a/spec/models/post_action_spec.rb +++ b/spec/models/post_action_spec.rb @@ -66,7 +66,7 @@ describe PostAction do expect(topic_user_ids).to include(mod.id) expect(topic.topic_users.where(user_id: mod.id) - .pluck(:notification_level).first).to eq(TopicUser.notification_levels[:tracking]) + .pluck(:notification_level).first).to eq(TopicUser.notification_levels[:watching]) expect(topic.topic_users.where(user_id: codinghorror.id) .pluck(:notification_level).first).to eq(TopicUser.notification_levels[:watching]) From aec73d40036440beb2281460fd213eb2340f3212 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 20 Apr 2017 17:41:35 -0400 Subject: [PATCH 145/221] correct the spec again --- app/models/group.rb | 2 +- spec/models/post_action_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/group.rb b/app/models/group.rb index 4bb6b1483c..0bb4f9c4ce 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -171,7 +171,7 @@ class Group < ActiveRecord::Base unless group = self.lookup_group(name) group = Group.new(name: name.to_s, automatic: true) - group.default_notification_level = 2 if AUTO_GROUPS[:staff] == id + group.default_notification_level = 2 if AUTO_GROUPS[:moderators] == id group.id = id group.save! end diff --git a/spec/models/post_action_spec.rb b/spec/models/post_action_spec.rb index 5d61df639b..9ed0e70499 100644 --- a/spec/models/post_action_spec.rb +++ b/spec/models/post_action_spec.rb @@ -66,7 +66,7 @@ describe PostAction do expect(topic_user_ids).to include(mod.id) expect(topic.topic_users.where(user_id: mod.id) - .pluck(:notification_level).first).to eq(TopicUser.notification_levels[:watching]) + .pluck(:notification_level).first).to eq(TopicUser.notification_levels[:tracking]) expect(topic.topic_users.where(user_id: codinghorror.id) .pluck(:notification_level).first).to eq(TopicUser.notification_levels[:watching]) From 9eff4f080777ebc82e27bbd2279cb331e37e188a Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Thu, 20 Apr 2017 13:19:50 +0530 Subject: [PATCH 146/221] FIX: all basic integer settings should have max value validation --- config/site_settings.yml | 63 +++++-------------- lib/validators/integer_setting_validator.rb | 2 + .../integer_setting_validator_spec.rb | 16 ++++- 3 files changed, 30 insertions(+), 51 deletions(-) diff --git a/config/site_settings.yml b/config/site_settings.yml index 28c1f2892e..a7e30e2683 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -82,10 +82,11 @@ basic: categories_topics: default: 20 min: 5 + max: 2000 suggested_topics: client: true default: 5 - min: 0 + max: 2000 limit_suggested_to_category: default: false suggested_topics_max_days_old: @@ -189,7 +190,6 @@ basic: relative_date_duration: client: true default: 30 - min: 0 topics_per_period_in_top_summary: default: 20 min: 1 @@ -332,6 +332,7 @@ users: client: true default: 3 min: 1 + max: 60 max_username_length: client: true default: 20 @@ -344,13 +345,16 @@ users: client: true default: 10 min: 8 + max: 2000 min_admin_password_length: client: true default: 15 min: 8 + max: 2000 password_unique_characters: default: 6 min: 1 + max: 2000 block_common_passwords: true enforce_global_nicknames: default: false @@ -371,8 +375,6 @@ users: default: true invite_expiry_days: default: 30 - min: 0 - max: 20000 invite_passthrough_hours: 0 invites_per_page: client: true @@ -384,6 +386,7 @@ users: delete_all_posts_max: client: true default: 15 + min: 1 redirect_users_to_top_page: true show_email_on_profile: client: true @@ -396,8 +399,6 @@ users: min: 1 purge_unactivated_users_grace_period_days: default: 14 - min: 0 - max: 20000 public_user_custom_fields: type: list default: '' @@ -416,6 +417,7 @@ users: client: true anonymous_account_duration_minutes: default: 10080 + max: 99000 hide_user_profiles_from_public: default: false client: true @@ -450,6 +452,7 @@ posting: max_post_length: client: true default: 32000 + max: 99000 topic_featured_link_enabled: client: true default: true @@ -491,7 +494,6 @@ posting: delete_removed_posts_after: client: true default: 24 - min: 0 traditional_markdown_linebreaks: client: true default: false @@ -534,12 +536,11 @@ posting: default: false show_time_gap_days: default: 7 - min: 0 - max: 20000 client: true short_progress_text_threshold: client: true default: 10000 + max: 99000 default_code_lang: client: true default: "auto" @@ -582,7 +583,6 @@ posting: enum: 'TrustLevelSetting' notify_about_queued_posts_after: default: 24 - min: 0 auto_close_messages_post_count: 500 auto_close_topics_post_count: 10000 code_formatting_style: @@ -613,8 +613,6 @@ email: digest_other_topics: 5 suppress_digest_email_after_days: default: 365 - min: 0 - max: 20000 digest_suppress_categories: type: category_list default: '' @@ -671,8 +669,6 @@ email: default: false delete_email_logs_after_days: default: 365 - min: 0 - max: 20000 max_emails_per_day_per_user: 100 enable_staged_users: true maximum_staged_users_per_email: 10 @@ -699,8 +695,6 @@ email: min: 2 reset_bounce_score_after_days: default: 30 - min: 0 - max: 20000 attachment_content_type_blacklist: type: list default: "pkcs7|x-vcard" @@ -753,8 +747,6 @@ files: clean_orphan_uploads_grace_period_hours: 48 purge_deleted_uploads_grace_period_days: default: 30 - min: 0 - max: 20000 prevent_anons_from_downloading_files: default: false client: true @@ -839,8 +831,6 @@ trust: tl2_requires_time_spent_mins: 60 tl2_requires_days_visited: default: 15 - min: 0 - max: 20000 tl2_requires_likes_received: 1 tl2_requires_likes_given: 1 tl2_requires_topic_reply_count: 3 @@ -850,44 +840,31 @@ trust: max: 1000000 tl3_requires_days_visited: default: 50 - min: 0 - max: 20000 tl3_requires_topics_replied_to: default: 10 - min: 0 tl3_requires_topics_viewed: default: 25 - min: 0 max: 100 tl3_requires_topics_viewed_cap: default: 500 - min: 0 tl3_requires_posts_read: default: 25 - min: 0 max: 100 tl3_requires_posts_read_cap: default: 20000 - min: 0 + max: 99000 tl3_requires_topics_viewed_all_time: default: 200 - min: 0 tl3_requires_posts_read_all_time: default: 500 - min: 0 tl3_requires_max_flagged: default: 5 - min: 0 tl3_promotion_min_duration: default: 14 - min: 0 - max: 20000 tl3_requires_likes_given: default: 30 - min: 0 tl3_requires_likes_received: default: 20 - min: 0 tl3_links_no_follow: default: false client: true @@ -940,7 +917,6 @@ spam: type: list levenshtein_distance_spammer_emails: default: 2 - min: 0 max: 3 max_new_accounts_per_registration_ip: 3 min_ban_entries_for_roll_up: 5 @@ -992,11 +968,9 @@ rate_limits: client: true max_logins_per_ip_per_hour: min: 1 - max: 20000 default: 30 max_logins_per_ip_per_minute: min: 1 - max: 20000 default: 6 developer: @@ -1029,12 +1003,15 @@ developer: background_polling_interval: client: true default: 60000 + max: 99000 polling_interval: client: true default: 3000 + max: 99000 anon_polling_interval: client: true default: 15000 + max: 99000 flush_timings_secs: client: true default: 20 @@ -1044,13 +1021,10 @@ developer: client: true top_topics_formula_log_views_multiplier: default: 2 - min: 0 top_topics_formula_first_post_likes_multiplier: default: 0.5 - min: 0 top_topics_formula_least_likes_per_post_multiplier: default: 3 - min: 0 rebake_old_posts_count: default: 250 min: 1 @@ -1216,18 +1190,12 @@ uncategorized: # Cold map thresholds cold_age_days_low: default: 14 - min: 0 - max: 20000 client: true cold_age_days_medium: default: 90 - min: 0 - max: 20000 client: true cold_age_days_high: default: 180 - min: 0 - max: 20000 client: true # Warnings @@ -1278,8 +1246,6 @@ uncategorized: delete_drafts_older_than_n_days: default: 180 - min: 0 - max: 20000 tos_topic_id: default: -1 @@ -1294,7 +1260,6 @@ uncategorized: bootstrap_mode_min_users: default: 50 client: true - min: 0 max: 5000 bootstrap_mode_enabled: diff --git a/lib/validators/integer_setting_validator.rb b/lib/validators/integer_setting_validator.rb index 2c92f3c20b..2af3dafb20 100644 --- a/lib/validators/integer_setting_validator.rb +++ b/lib/validators/integer_setting_validator.rb @@ -1,6 +1,8 @@ class IntegerSettingValidator def initialize(opts={}) @opts = opts + @opts[:min] = 0 unless @opts[:min].present? + @opts[:max] = 20000 unless @opts[:max].present? end def valid_value?(val) diff --git a/spec/components/validators/integer_setting_validator_spec.rb b/spec/components/validators/integer_setting_validator_spec.rb index d681f2bc63..53da301754 100644 --- a/spec/components/validators/integer_setting_validator_spec.rb +++ b/spec/components/validators/integer_setting_validator_spec.rb @@ -21,9 +21,21 @@ describe IntegerSettingValidator do it "returns true if value is a valid integer" do expect(validator.valid_value?(1)).to eq(true) - expect(validator.valid_value?(-1)).to eq(true) expect(validator.valid_value?('1')).to eq(true) - expect(validator.valid_value?('-1')).to eq(true) + end + + it "defaults min to 0" do + expect(validator.valid_value?(-1)).to eq(false) + expect(validator.valid_value?('-1')).to eq(false) + expect(validator.valid_value?(0)).to eq(true) + expect(validator.valid_value?('0')).to eq(true) + end + + it "defaults max to 20000" do + expect(validator.valid_value?(20001)).to eq(false) + expect(validator.valid_value?('20001')).to eq(false) + expect(validator.valid_value?(20000)).to eq(true) + expect(validator.valid_value?('20000')).to eq(true) end end From 8b2e3bf5f1124f26f2c617e88e78852afd005847 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 21 Apr 2017 10:04:21 +0800 Subject: [PATCH 147/221] FIX: Topic title should be included in post webhook payload. --- app/serializers/web_hook_post_serializer.rb | 24 +++++++++---------- .../web_hook_post_serializer_spec.rb | 24 +++++++++++++++++++ 2 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 spec/serializers/web_hook_post_serializer_spec.rb diff --git a/app/serializers/web_hook_post_serializer.rb b/app/serializers/web_hook_post_serializer.rb index 221c398fa7..5d79ef5f48 100644 --- a/app/serializers/web_hook_post_serializer.rb +++ b/app/serializers/web_hook_post_serializer.rb @@ -1,17 +1,17 @@ class WebHookPostSerializer < PostSerializer - def include_can_edit? - false + def include_topic_title? + true end - def can_delete - false - end - - def can_recover - false - end - - def can_wiki - false + %i{ + can_view + can_edit + can_delete + can_recover + can_wiki + }.each do |attr| + define_method("include_#{attr}?") do + false + end end end diff --git a/spec/serializers/web_hook_post_serializer_spec.rb b/spec/serializers/web_hook_post_serializer_spec.rb new file mode 100644 index 0000000000..2f1f487060 --- /dev/null +++ b/spec/serializers/web_hook_post_serializer_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +RSpec.describe WebHookPostSerializer do + let(:admin) { Fabricate(:admin) } + let(:post) { Fabricate(:post) } + let(:serializer) { WebHookPostSerializer.new(post, scope: Guardian.new(admin), root: false) } + + it 'should only include the required keys' do + count = serializer.as_json.keys.count + difference = count - 40 + + expect(difference).to eq(0), lambda { + message = "" + + if difference < 0 + message << "#{difference * -1} key(s) have been removed from this serializer." + else + message << "#{difference} key(s) have been added to this serializer." + end + + message << "\nPlease verify if those key(s) are required as part of the web hook's payload." + } + end +end From e922623da885810eca5b8ce49a6380b0cbdeb3ed Mon Sep 17 00:00:00 2001 From: Rafael dos Santos Silva Date: Thu, 20 Apr 2017 23:18:01 -0300 Subject: [PATCH 148/221] FIX: User Admin screen on mobile style was off --- app/assets/stylesheets/common/admin/admin_base.scss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index fc3044045f..93878dadb6 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -553,6 +553,9 @@ section.details { .display-row.associations .value { width: 750px; + @media (max-width: $mobile-breakpoint) { + width: 100%; + } } .display-row { @@ -598,6 +601,9 @@ section.details { width: 480px; float: left; margin-left: 12px; + @media (max-width: $mobile-breakpoint) { + width: 100%; + } .btn { margin-right: 5px; } @@ -1819,7 +1825,7 @@ table#user-badges { // Mobile specific style for Admin IP Lookup box .mobile-view .admin-contents .ip-lookup .location-box { width: 300px; - left: 20px; + left: -100%; } .cboxcontainer { From 20c2c66dd4c407d00fc57f0ce364813a08614dac Mon Sep 17 00:00:00 2001 From: cpradio Date: Thu, 20 Apr 2017 22:33:10 -0400 Subject: [PATCH 149/221] FEATURE: Add normal as a preference for topic subscription state when replying to a topic --- .../discourse/controllers/preferences.js.es6 | 3 ++- ...fication_level_when_replying_site_setting.rb | 3 ++- lib/post_creator.rb | 2 ++ spec/components/post_creator_spec.rb | 17 +++++++++++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/preferences.js.es6 b/app/assets/javascripts/discourse/controllers/preferences.js.es6 index 9173a77153..ac848d3ea2 100644 --- a/app/assets/javascripts/discourse/controllers/preferences.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences.js.es6 @@ -124,7 +124,8 @@ export default Ember.Controller.extend(CanCheckEmails, { { name: I18n.t('user.auto_track_options.after_10_minutes'), value: 600000 }], notificationLevelsForReplying: [{ name: I18n.t('topic.notifications.watching.title'), value: NotificationLevels.WATCHING }, - { name: I18n.t('topic.notifications.tracking.title'), value: NotificationLevels.TRACKING }], + { name: I18n.t('topic.notifications.tracking.title'), value: NotificationLevels.TRACKING }, + { name: I18n.t('topic.notifications.regular.title'), value: NotificationLevels.REGULAR }], considerNewTopicOptions: [{ name: I18n.t('user.new_topic_duration.not_viewed'), value: -1 }, diff --git a/app/models/notification_level_when_replying_site_setting.rb b/app/models/notification_level_when_replying_site_setting.rb index 66b07cb545..296c7c066f 100644 --- a/app/models/notification_level_when_replying_site_setting.rb +++ b/app/models/notification_level_when_replying_site_setting.rb @@ -15,7 +15,8 @@ class NotificationLevelWhenReplyingSiteSetting < EnumSiteSetting def self.values @values ||= [ { name: 'topic.notifications.watching.title', value: notification_levels[:watching] }, - { name: 'topic.notifications.tracking.title', value: notification_levels[:tracking] } + { name: 'topic.notifications.tracking.title', value: notification_levels[:tracking] }, + { name: 'topic.notifications.regular.title', value: notification_levels[:regular] } ] end diff --git a/lib/post_creator.rb b/lib/post_creator.rb index 864ea578a7..9da222b5cd 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -494,6 +494,8 @@ class PostCreator TopicUser.auto_notification_for_staging(@user.id, @topic.id, TopicUser.notification_reasons[:auto_watch]) elsif @user.user_option.notification_level_when_replying === NotificationLevels.topic_levels[:watching] TopicUser.auto_notification(@user.id, @topic.id, TopicUser.notification_reasons[:created_post], NotificationLevels.topic_levels[:watching]) + elsif @user.user_option.notification_level_when_replying === NotificationLevels.topic_levels[:regular] + TopicUser.auto_notification(@user.id, @topic.id, TopicUser.notification_reasons[:created_post], NotificationLevels.topic_levels[:regular]) else TopicUser.auto_notification(@user.id, @topic.id, TopicUser.notification_reasons[:created_post], NotificationLevels.topic_levels[:tracking]) end diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index d2ee5299cf..7240aeaf80 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -899,6 +899,23 @@ describe PostCreator do topic_user = TopicUser.find_by(user_id: user.id, topic_id: post.topic_id) expect(topic_user.notification_level).to eq(TopicUser.notification_levels[:tracking]) end + + it "topic notification level is normal based on preference" do + user.user_option.notification_level_when_replying = 1 + + admin = Fabricate(:admin) + topic = PostCreator.create(admin, + title: "this is the title of a topic created by an admin for tracking notification", + raw: "this is the content of a topic created by an admin for keeping a tracking notification state on a topic ;)" + ) + + post = PostCreator.create(user, + topic_id: topic.topic_id, + raw: "this is a reply to set the tracking state to normal ;)" + ) + topic_user = TopicUser.find_by(user_id: user.id, topic_id: post.topic_id) + expect(topic_user.notification_level).to eq(TopicUser.notification_levels[:regular]) + end end describe '#create!' do From 15e2f5565594f263bb7d2a3f14ed0c4cc50c53fe Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 21 Apr 2017 17:31:19 +0800 Subject: [PATCH 150/221] Add rake task to gather `GC.stat` for Sidekiq. --- lib/tasks/gc.rake | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 lib/tasks/gc.rake diff --git a/lib/tasks/gc.rake b/lib/tasks/gc.rake new file mode 100644 index 0000000000..ebc49e3528 --- /dev/null +++ b/lib/tasks/gc.rake @@ -0,0 +1,21 @@ +desc "Returns `GC.stat` for each Sidekiq process in JSON" +task "sidekiq:gc_stat" do + pids = `ps -eo pid,args | grep ' [s]idekiq ' | awk '{print $1}'`.split("\n").map(&:to_i) + results = [] + hostname = `hostname`.chomp + + pids.each do |pid| + tmp_path = Tempfile.new.path + + system( + "bundle exec rbtrace -p #{pid} -e \"o = GC.stat; f = File.open('#{tmp_path}', 'w'); f.write(o.to_json); f.close\"", + out: "/dev/null", err: "/dev/null" + ) + + result = JSON.parse(File.read(tmp_path)) + result["hostname"] = hostname + results << result + end + + puts results.to_json +end From 52306c393a1cfc7d4bb37a2b8f69745941de8a48 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 21 Apr 2017 10:24:43 -0400 Subject: [PATCH 151/221] FEATURE: basic implementation of stats socket --- lib/stats_socket.rb | 63 ++++++++++++++++++++++++++++ spec/components/stats_socket_spec.rb | 37 ++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 lib/stats_socket.rb create mode 100644 spec/components/stats_socket_spec.rb diff --git a/lib/stats_socket.rb b/lib/stats_socket.rb new file mode 100644 index 0000000000..c2d11ef292 --- /dev/null +++ b/lib/stats_socket.rb @@ -0,0 +1,63 @@ +require 'socket' + +class StatsSocket + + def initialize(socket_path) + @socket_path = socket_path + @server = nil + end + + def start + @server = UNIXServer.new(@socket_path) + @accept_thread = new_accept_thread + end + + def stop + @server.close if @server + @server = nil + end + + protected + + def new_accept_thread + server = @server + Thread.new do + done = false + while !done + done = !accept_connection(server) + end + end + end + + def accept_connection(server) + socket = nil + begin + socket = server.accept + rescue IOError + # socket was shut down or something catastrophic like that happened + return false + end + + line = socket.readline + socket.write get_response(line.strip) + socket.close + true + rescue IOError + # nothing to do here, case its normal on shutdown + rescue => e + Rails.logger.warn("Failed to handle connection in stats socket #{e}") + end + + def get_response(command) + result = + case command + when "gc_stat" + GC.stat.to_json + else + "[\"UNKNOWN COMMAND\"]" + end + + result << "\n" + end + +end diff --git a/spec/components/stats_socket_spec.rb b/spec/components/stats_socket_spec.rb new file mode 100644 index 0000000000..9352a6da53 --- /dev/null +++ b/spec/components/stats_socket_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' +require_dependency 'stats_socket' + +describe StatsSocket do + let :socket_path do + "#{Dir.tmpdir}/#{SecureRandom.hex}" + end + + let :stats_socket do + StatsSocket.new(socket_path) + end + + before do + stats_socket.start + end + + after do + stats_socket.stop + end + + it "can respond to various stats commands" do + line = nil + + # ensure this works more than once :) + 2.times do + socket = UNIXSocket.new(socket_path) + socket.send "gc_stat\n", 0 + line = socket.readline + socket.close + end + + parsed = JSON.parse(line) + + expect(parsed.keys.sort).to eq(GC.stat.keys.map(&:to_s).sort) + end + +end From 1f5089e474a346325a04a54198e6f2aa0a281a64 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Fri, 21 Apr 2017 19:07:24 +0530 Subject: [PATCH 152/221] FIX: handle invite error in wizard --- .../wizard/templates/components/wizard-field.hbs | 2 +- lib/wizard/builder.rb | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/wizard/templates/components/wizard-field.hbs b/app/assets/javascripts/wizard/templates/components/wizard-field.hbs index 37c216baae..8f804940fb 100644 --- a/app/assets/javascripts/wizard/templates/components/wizard-field.hbs +++ b/app/assets/javascripts/wizard/templates/components/wizard-field.hbs @@ -11,5 +11,5 @@
    {{#if field.errorDescription}} -
    {{field.errorDescription}}
    +
    {{{field.errorDescription}}}
    {{/if}} diff --git a/lib/wizard/builder.rb b/lib/wizard/builder.rb index 3fc48b8313..9600a9ab57 100644 --- a/lib/wizard/builder.rb +++ b/lib/wizard/builder.rb @@ -235,7 +235,11 @@ class Wizard users.each do |u| args = {} args[:moderator] = true if u['role'] == 'moderator' - Invite.create_invite_by_email(u['email'], @wizard.user, args) + begin + Invite.create_invite_by_email(u['email'], @wizard.user, args) + rescue => e + updater.errors.add(:invite_list, e.message.concat("
    ")) + end end end end @@ -261,4 +265,3 @@ class Wizard end end end - From 0b3aec9c945cdcf6d7b11afd99ed9a17608b9890 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 21 Apr 2017 11:36:51 -0400 Subject: [PATCH 153/221] FEATURE: set UNICORN_STATS_SOCKET_DIR for status socket eg: sam@ubuntu stats_sockets % socat - UNIX-CONNECT:9622.sock gc_stat {"count":46,"heap_allocated_pages":2459,"heap_sorted_length":2460,"heap_allocatable_pages":0,"heap_available_slots":1002267,"heap_live_slots":647293,"heap_free_slots":354974,"heap_final_slots":0,"heap_marked_slots":503494,"heap_swept_slots":498773,"heap_eden_pages":2459,"heap_tomb_pages":0,"total_allocated_pages":2459,"total_freed_pages":0,"total_allocated_objects":4337014,"total_freed_objects":3689721,"malloc_increase_bytes":6448248,"malloc_increase_bytes_limit":29188387,"minor_gc_count":36,"major_gc_count":10,"remembered_wb_unprotected_objects":19958,"remembered_wb_unprotected_objects_limit":39842,"old_objects":462019,"old_objects_limit":895782,"oldmalloc_increase_bytes":6448696,"oldmalloc_increase_bytes_limit":19350882} --- Gemfile.lock | 6 +++--- config/unicorn.conf.rb | 42 +++++++++++++++++++++++++++++++++++++++++- lib/demon/sidekiq.rb | 6 ++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3dda61b6d9..3ccbe4a87d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -140,7 +140,7 @@ GEM railties (>= 4.2.0) thor (>= 0.14, < 2.0) jwt (1.5.6) - kgio (2.10.0) + kgio (2.11.0) libv8 (5.3.332.38.5) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) @@ -272,7 +272,7 @@ GEM activesupport (= 4.2.8) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - raindrops (0.17.0) + raindrops (0.18.0) rake (11.3.0) rake-compiler (0.9.9) rake @@ -380,7 +380,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.1) - unicorn (5.2.0) + unicorn (5.3.0) kgio (~> 2.6) raindrops (~> 0.7) uniform_notifier (1.10.0) diff --git a/config/unicorn.conf.rb b/config/unicorn.conf.rb index 5f51df510a..0fc36af3c0 100644 --- a/config/unicorn.conf.rb +++ b/config/unicorn.conf.rb @@ -36,6 +36,24 @@ preload_app true # fast LAN. check_client_connection false +@stats_socket_dir = ENV["UNICORN_STATS_SOCKET_DIR"] + +def clean_up_stats_socket(server, pid) + if @stats_socket_dir.present? + name = "#{@stats_socket_dir}/#{pid}.sock" + FileUtils.rm_f(name) + server.logger.info "Cleaned up stats socket at #{name}" + end +end + +def start_stats_socket(server) + if @stats_socket_dir.present? + name = "#{@stats_socket_dir}/#{Process.pid}.sock" + StatsSocket.new(name).start + server.logger.info "Started stats socket at #{name}" + end +end + initialized = false before_fork do |server, worker| @@ -51,6 +69,18 @@ before_fork do |server, worker| # router warm up Rails.application.routes.recognize_path('abc') rescue nil + if @stats_socket_dir.present? + server.logger.info "Initializing stats socket at #{@stats_socket_dir}" + begin + FileUtils.mkdir_p @stats_socket_dir + FileUtils.rm_f Dir.glob("#{@stats_socket_dir}/*.sock") + require 'stats_socket' + start_stats_socket(server) + rescue => e + server.logger.info "Failed to initialize stats socket dir #{e}" + end + end + # get rid of rubbish so we don't share it GC.start @@ -75,6 +105,11 @@ before_fork do |server, worker| require 'demon/sidekiq' + if @stats_socket_dir + Demon::Sidekiq.after_fork do + start_stats_socket(server) + end + end Demon::Sidekiq.start(sidekiqs) Signal.trap("SIGTSTP") do @@ -168,12 +203,17 @@ before_fork do |server, worker| sleep 1 end +after_worker_exit do |server, worker, status| + clean_up_stats_socket(server, status.pid) +end + after_fork do |server, worker| + start_stats_socket(server) + # warm up v8 after fork, that way we do not fork a v8 context # it may cause issues if bg threads in a v8 isolate randomly stop # working due to fork Discourse.after_fork - begin PrettyText.cook("warm up **pretty text**") rescue => e diff --git a/lib/demon/sidekiq.rb b/lib/demon/sidekiq.rb index 4e984bc781..b095cb951a 100644 --- a/lib/demon/sidekiq.rb +++ b/lib/demon/sidekiq.rb @@ -6,6 +6,10 @@ class Demon::Sidekiq < Demon::Base "sidekiq" end + def self.after_fork(&blk) + blk ? (@blk=blk) : @blk + end + private def suppress_stdout @@ -17,6 +21,8 @@ class Demon::Sidekiq < Demon::Base end def after_fork + Demon::Sidekiq.after_fork&.call + STDERR.puts "Loading Sidekiq in process id #{Process.pid}" require 'sidekiq/cli' # CLI will close the logger, if we have one set we can be in big From 20778fbf588339d895ff2641fbf49cc7e968ae9a Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 21 Apr 2017 12:37:28 -0400 Subject: [PATCH 154/221] fix handling of stats socket --- config/unicorn.conf.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config/unicorn.conf.rb b/config/unicorn.conf.rb index 0fc36af3c0..2e142c1b0d 100644 --- a/config/unicorn.conf.rb +++ b/config/unicorn.conf.rb @@ -44,6 +44,8 @@ def clean_up_stats_socket(server, pid) FileUtils.rm_f(name) server.logger.info "Cleaned up stats socket at #{name}" end +rescue => e + server.logger.warn "Failed to clean up stats socket #{e}" end def start_stats_socket(server) @@ -52,6 +54,8 @@ def start_stats_socket(server) StatsSocket.new(name).start server.logger.info "Started stats socket at #{name}" end +rescue => e + server.logger.warn "Failed to start stats socket #{e}" end initialized = false @@ -72,9 +76,9 @@ before_fork do |server, worker| if @stats_socket_dir.present? server.logger.info "Initializing stats socket at #{@stats_socket_dir}" begin + require 'stats_socket' FileUtils.mkdir_p @stats_socket_dir FileUtils.rm_f Dir.glob("#{@stats_socket_dir}/*.sock") - require 'stats_socket' start_stats_socket(server) rescue => e server.logger.info "Failed to initialize stats socket dir #{e}" From e189ec2def24b6ba14f96416631e1b3c5186555f Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 21 Apr 2017 16:54:18 -0400 Subject: [PATCH 155/221] correct it so stats socket times out after 10 seconds --- lib/stats_socket.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/stats_socket.rb b/lib/stats_socket.rb index c2d11ef292..23f6ebc2e0 100644 --- a/lib/stats_socket.rb +++ b/lib/stats_socket.rb @@ -38,8 +38,10 @@ class StatsSocket return false end - line = socket.readline - socket.write get_response(line.strip) + if IO.select(nil, [socket], nil, 10) + line = socket.read_nonblock(1000) + socket.write get_response(line.strip) + end socket.close true rescue IOError From b077335a305278475e3e6386e78afc4d013be9c8 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 21 Apr 2017 17:12:29 -0400 Subject: [PATCH 156/221] make stat socket much more robust --- lib/stats_socket.rb | 23 +++++++++++++++++++---- spec/components/stats_socket_spec.rb | 8 ++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/stats_socket.rb b/lib/stats_socket.rb index 23f6ebc2e0..f4367c12c9 100644 --- a/lib/stats_socket.rb +++ b/lib/stats_socket.rb @@ -38,16 +38,31 @@ class StatsSocket return false end - if IO.select(nil, [socket], nil, 10) - line = socket.read_nonblock(1000) + start = Time.now + line = "" + + while Time.now - start < 10 + if IO.select(nil, [socket], nil, 10) + begin + line << socket.read_nonblock(1000) + rescue IO::WaitReadable + sleep 0.001 + end + end + break if line.include?("\n") + end + + if line.include?("\n") socket.write get_response(line.strip) end - socket.close + true - rescue IOError + rescue IOError => e # nothing to do here, case its normal on shutdown rescue => e Rails.logger.warn("Failed to handle connection in stats socket #{e}") + ensure + socket&.close rescue nil end def get_response(command) diff --git a/spec/components/stats_socket_spec.rb b/spec/components/stats_socket_spec.rb index 9352a6da53..cac144439e 100644 --- a/spec/components/stats_socket_spec.rb +++ b/spec/components/stats_socket_spec.rb @@ -29,6 +29,14 @@ describe StatsSocket do socket.close end + socket = UNIXSocket.new(socket_path) + socket.send "gc_st", 0 + socket.flush + sleep 0.001 + socket.send "at\n", 0 + line = socket.readline + socket.close + parsed = JSON.parse(line) expect(parsed.keys.sort).to eq(GC.stat.keys.map(&:to_s).sort) From b74c61777a75465ce0218885b1ebbc92e8f2bafe Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 21 Apr 2017 17:29:38 -0400 Subject: [PATCH 157/221] waiting on the wrong array --- lib/stats_socket.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/stats_socket.rb b/lib/stats_socket.rb index f4367c12c9..ad39d12a47 100644 --- a/lib/stats_socket.rb +++ b/lib/stats_socket.rb @@ -42,7 +42,7 @@ class StatsSocket line = "" while Time.now - start < 10 - if IO.select(nil, [socket], nil, 10) + if IO.select([socket], nil, nil, 10) begin line << socket.read_nonblock(1000) rescue IO::WaitReadable From 5fab2042f545ef9999e79d18a57c8910c5713f38 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Sat, 22 Apr 2017 08:23:24 +0800 Subject: [PATCH 158/221] Revert "Add rake task to gather `GC.stat` for Sidekiq." This reverts commit 15e2f5565594f263bb7d2a3f14ed0c4cc50c53fe. --- lib/tasks/gc.rake | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 lib/tasks/gc.rake diff --git a/lib/tasks/gc.rake b/lib/tasks/gc.rake deleted file mode 100644 index ebc49e3528..0000000000 --- a/lib/tasks/gc.rake +++ /dev/null @@ -1,21 +0,0 @@ -desc "Returns `GC.stat` for each Sidekiq process in JSON" -task "sidekiq:gc_stat" do - pids = `ps -eo pid,args | grep ' [s]idekiq ' | awk '{print $1}'`.split("\n").map(&:to_i) - results = [] - hostname = `hostname`.chomp - - pids.each do |pid| - tmp_path = Tempfile.new.path - - system( - "bundle exec rbtrace -p #{pid} -e \"o = GC.stat; f = File.open('#{tmp_path}', 'w'); f.write(o.to_json); f.close\"", - out: "/dev/null", err: "/dev/null" - ) - - result = JSON.parse(File.read(tmp_path)) - result["hostname"] = hostname - results << result - end - - puts results.to_json -end From dad20240946ac2af90847c4518268b89b4cf02c8 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Sat, 22 Apr 2017 12:06:02 +0530 Subject: [PATCH 159/221] FIX: do not impose default min/max validation on hidden site setting --- lib/validators/integer_setting_validator.rb | 4 ++-- .../validators/integer_setting_validator_spec.rb | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/validators/integer_setting_validator.rb b/lib/validators/integer_setting_validator.rb index 2af3dafb20..185eb0a799 100644 --- a/lib/validators/integer_setting_validator.rb +++ b/lib/validators/integer_setting_validator.rb @@ -1,8 +1,8 @@ class IntegerSettingValidator def initialize(opts={}) @opts = opts - @opts[:min] = 0 unless @opts[:min].present? - @opts[:max] = 20000 unless @opts[:max].present? + @opts[:min] = 0 unless @opts[:min].present? || @opts[:hidden] + @opts[:max] = 20000 unless @opts[:max].present? || @opts[:hidden] end def valid_value?(val) diff --git a/spec/components/validators/integer_setting_validator_spec.rb b/spec/components/validators/integer_setting_validator_spec.rb index 53da301754..db0b1cd281 100644 --- a/spec/components/validators/integer_setting_validator_spec.rb +++ b/spec/components/validators/integer_setting_validator_spec.rb @@ -97,5 +97,14 @@ describe IntegerSettingValidator do expect(validator.valid_value?(-2)).to eq(false) end end + + context "when setting is hidden" do + subject(:validator) { described_class.new(hidden: true) } + + it "does not impose default validations" do + expect(validator.valid_value?(-1)).to eq(true) + expect(validator.valid_value?(20001)).to eq(true) + end + end end end From 423f2ab2287b05e93626adbd228c7d73eec55d01 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 24 Apr 2017 12:06:28 +0800 Subject: [PATCH 160/221] FIX: Processing incoming email should be done in a background job. --- app/controllers/admin/email_controller.rb | 4 ++-- app/jobs/regular/process_email.rb | 2 +- lib/email/receiver.rb | 2 +- spec/controllers/admin/email_controller_spec.rb | 12 ++++++++++++ spec/controllers/email_controller_spec.rb | 2 -- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/controllers/admin/email_controller.rb b/app/controllers/admin/email_controller.rb index 74ce0901ec..df43323926 100644 --- a/app/controllers/admin/email_controller.rb +++ b/app/controllers/admin/email_controller.rb @@ -84,8 +84,8 @@ class Admin::EmailController < Admin::AdminController def handle_mail params.require(:email) - Email::Processor.process!(params[:email]) - render plain: "email was processed" + Jobs.enqueue(:process_email, mail: params[:email], retry_on_rate_limit: true) + render plain: "email has been received and is queued for processing" end def raw_email diff --git a/app/jobs/regular/process_email.rb b/app/jobs/regular/process_email.rb index a01e3bc07d..81acb4f8af 100644 --- a/app/jobs/regular/process_email.rb +++ b/app/jobs/regular/process_email.rb @@ -4,7 +4,7 @@ module Jobs sidekiq_options retry: 3 def execute(args) - Email::Processor.process!(args[:mail], false) + Email::Processor.process!(args[:mail], args[:retry_on_rate_limit] || false) end sidekiq_retries_exhausted do |msg| diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 2322970bae..b629fdb558 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -553,7 +553,7 @@ module Email def create_post_with_attachments(options={}) # deal with attachments attachments.each do |attachment| - tmp = Tempfile.new("discourse-email-attachment") + tmp = Tempfile.new(["discourse-email-attachment", File.extname(attachment.filename)]) begin # read attachment File.open(tmp.path, "w+b") { |f| f.write attachment.body.decoded } diff --git a/spec/controllers/admin/email_controller_spec.rb b/spec/controllers/admin/email_controller_spec.rb index 86d2b42e9a..b14b6ae979 100644 --- a/spec/controllers/admin/email_controller_spec.rb +++ b/spec/controllers/admin/email_controller_spec.rb @@ -71,4 +71,16 @@ describe Admin::EmailController do end end + context '#handle_mail' do + before do + log_in_user(Fabricate(:admin)) + SiteSetting.queue_jobs = true + end + + it 'should enqueue the right job' do + expect { xhr :post, :handle_mail, email: email('cc') } + .to change { Jobs::ProcessEmail.jobs.count }.by(1) + end + end + end diff --git a/spec/controllers/email_controller_spec.rb b/spec/controllers/email_controller_spec.rb index 63fd6865ad..996d88cf3e 100644 --- a/spec/controllers/email_controller_spec.rb +++ b/spec/controllers/email_controller_spec.rb @@ -232,6 +232,4 @@ describe EmailController do end end - - end From df77a827e1c513dd194cfefbbf74397d456e45e3 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Mon, 24 Apr 2017 18:12:40 +0530 Subject: [PATCH 161/221] Update Translations --- config/locales/client.ar.yml | 27 +---- config/locales/client.bs_BA.yml | 16 --- config/locales/client.cs.yml | 37 +----- config/locales/client.da.yml | 26 ---- config/locales/client.de.yml | 26 ---- config/locales/client.el.yml | 26 ---- config/locales/client.es.yml | 65 ++++++---- config/locales/client.et.yml | 26 ---- config/locales/client.fa_IR.yml | 121 +++++++++++++++---- config/locales/client.fi.yml | 74 ++++++++---- config/locales/client.fr.yml | 26 ---- config/locales/client.gl.yml | 26 ---- config/locales/client.he.yml | 93 ++++++++++---- config/locales/client.it.yml | 26 ---- config/locales/client.ja.yml | 25 ---- config/locales/client.ko.yml | 26 ---- config/locales/client.nb_NO.yml | 93 ++++++++++---- config/locales/client.nl.yml | 95 ++++++++++----- config/locales/client.pl_PL.yml | 26 ---- config/locales/client.pt.yml | 26 ---- config/locales/client.pt_BR.yml | 26 ---- config/locales/client.ro.yml | 40 +++--- config/locales/client.ru.yml | 113 ++++++++++++----- config/locales/client.sk.yml | 26 ---- config/locales/client.sq.yml | 24 ---- config/locales/client.sv.yml | 26 ---- config/locales/client.te.yml | 24 ---- config/locales/client.tr_TR.yml | 26 ---- config/locales/client.uk.yml | 12 -- config/locales/client.ur.yml | 26 ---- config/locales/client.vi.yml | 50 ++++---- config/locales/client.zh_CN.yml | 26 ---- config/locales/client.zh_TW.yml | 26 ---- config/locales/server.ar.yml | 1 - config/locales/server.bs_BA.yml | 1 - config/locales/server.cs.yml | 5 +- config/locales/server.da.yml | 1 - config/locales/server.de.yml | 7 -- config/locales/server.el.yml | 7 -- config/locales/server.es.yml | 7 -- config/locales/server.et.yml | 2 - config/locales/server.fa_IR.yml | 3 +- config/locales/server.fi.yml | 12 +- config/locales/server.fr.yml | 2 - config/locales/server.he.yml | 10 +- config/locales/server.id.yml | 1 - config/locales/server.it.yml | 1 - config/locales/server.ja.yml | 1 - config/locales/server.ko.yml | 1 - config/locales/server.nb_NO.yml | 15 ++- config/locales/server.nl.yml | 3 +- config/locales/server.pl_PL.yml | 2 - config/locales/server.pt.yml | 2 - config/locales/server.pt_BR.yml | 1 - config/locales/server.ro.yml | 42 +++++-- config/locales/server.ru.yml | 2 - config/locales/server.sk.yml | 1 - config/locales/server.sq.yml | 1 - config/locales/server.sv.yml | 2 - config/locales/server.te.yml | 1 - config/locales/server.tr_TR.yml | 2 - config/locales/server.uk.yml | 1 - config/locales/server.vi.yml | 1 - config/locales/server.zh_CN.yml | 4 - config/locales/server.zh_TW.yml | 5 +- plugins/poll/config/locales/client.fa_IR.yml | 19 +++ plugins/poll/config/locales/server.fa_IR.yml | 2 + public/500.fa_IR.html | 4 +- 68 files changed, 603 insertions(+), 920 deletions(-) diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index ee1efd8071..f1fe79d99d 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -819,8 +819,8 @@ ar: after_2_weeks: "أُنشئت في الأسبوعين الماضيين" auto_track_topics: "تابع آليا المواضيع التي أدخلها" auto_track_options: - never: "ابداً" - immediately: "حالاً" + never: "أبدًا" + immediately: "حالًا" after_30_seconds: "بعد 30 ثانية" after_1_minute: "بعد دقيقة واحدة" after_2_minutes: "بعد دقيقتين" @@ -2799,32 +2799,12 @@ ar: customize: title: "تخصيص" long_title: "تخصيص الموقع" - css: "CSS" - header: "Header" - top: "Top" - footer: "تذييل " - embedded_css: "تضمين CSS" - head_tag: - text: "" - title: "رقْم HTML الذي سيُدرج قبل وسم ‎‎" - body_tag: - text: "" - title: "رقْم HTML الذي سيُدرج قبل وسم ‎‎" - override_default: "لا تضمّن ورقة الأنماط القياسيّة" - enabled: "مفعّل؟" preview: "معاينة" - undo_preview: "ازالة المعاينة" - rescue_preview: "الشكل الافتراضي" - explain_preview: "مشاهدة الموقع بهذا الشكل المخصص" - explain_undo_preview: "الرجوع الى الشكل السابق" - explain_rescue_preview: "مشاهدة الموقع بالشكل الافتراضي" save: "حفظ" new: "جديد" new_style: "تصميم جديد" import: "استيراد" - import_title: "حدد ملف او انسخ نص" delete: "حذف" - delete_confirm: "أأحذف هذا التّخصيص؟" copy: "نسخ" email_templates: title: "قالب البريد الالكتروني " @@ -2837,7 +2817,6 @@ ar: colors: title: "الألوان" long_title: "مخططات الألوان" - about: "عدّل الألوان المستخدمة في الموقع دون كتابة CSS. أضف مخطّطًا للبدء." new_name: "مخطط ألوان جديد" copy_name_prefix: "نسخة من" delete_confirm: "أأحذف مخطط الألوان هذا؟" @@ -2976,8 +2955,6 @@ ar: change_trust_level: "تغيير مستوى الثقة" change_username: "تغيير اسم المستخدم" change_site_setting: "تغيير اعدادات الموقع" - change_site_customization: "تخصيص الموقع" - delete_site_customization: "حذف هذا التخصيص؟" change_site_text: "تغيير نص الموقع." suspend_user: "حظر المستخدم" unsuspend_user: "رفع الحظر " diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index 67fbd03b47..99c236b4b0 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -1857,32 +1857,18 @@ bs_BA: customize: title: "Customize" long_title: "Site Customizations" - css: "Stylesheet" - header: "Header" - override_default: "Do not include standard style sheet" - enabled: "Enabled?" preview: "preview" - undo_preview: "remove preview" - rescue_preview: "default style" - explain_preview: "See the site with this custom stylesheet" - explain_undo_preview: "Go back to the currently enabled custom stylesheet" - explain_rescue_preview: "See the site with the default stylesheet" save: "Save" new: "New" new_style: "New Style" delete: "Delete" - delete_confirm: "Delete this customization?" about: "Modify CSS stylesheets and HTML headers on the site. Add a customization to start." color: "Color" opacity: "Opacity" copy: "Copy" - css_html: - title: "CSS/HTML" - long_title: "CSS and HTML Customizations" colors: title: "Colors" long_title: "Color Schemes" - about: "Modify the colors used on the site without writing CSS. Add a scheme to start." new_name: "New Color Scheme" copy_name_prefix: "Copy of" delete_confirm: "Delete this color scheme?" @@ -2016,8 +2002,6 @@ bs_BA: change_trust_level: "change trust level" change_username: "zamjeni korisničko ime" change_site_setting: "change site setting" - change_site_customization: "change site customization" - delete_site_customization: "delete site customization" suspend_user: "suspend user" unsuspend_user: "unsuspend user" grant_badge: "grant badge" diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml index 08b2609346..3ae2f256c7 100644 --- a/config/locales/client.cs.yml +++ b/config/locales/client.cs.yml @@ -185,7 +185,7 @@ cs: admin_title: "Administrace" flags_title: "Nahlášení" show_more: "zobrazit více" - show_help: "volby" + show_help: "možnosti" links: "Odkazy" links_lowercase: one: "odkaz" @@ -195,7 +195,7 @@ cs: guidelines: "Pokyny" privacy_policy: "Ochrana soukromí" privacy: "Soukromí" - terms_of_service: "Podmínky služby" + terms_of_service: "Podmínky používání" mobile_view: "Mobilní verze" desktop_view: "Plná verze" you: "Vy" @@ -223,7 +223,7 @@ cs: about: simple_title: "O fóru" title: "O %{title}" - stats: "Statistiky Webu" + stats: "Statistiky webu" our_admins: "Naši Admini" our_moderators: "Naši Moderátoři" stat: @@ -1198,7 +1198,7 @@ cs: dismiss: "Odbýt" dismiss_read: "Odbýt všechna nepřečtená" dismiss_button: "Odbýt..." - dismiss_tooltip: "Odbýt jen nové příspěvka nebo přestat sledovat témata" + dismiss_tooltip: "Odbýt jen nové příspěvky nebo přestat sledovat témata" also_dismiss_topics: "Přestat sledovat tyto témata, takže se mi znovu nezobrazí jako nepřečtená" dismiss_new: "Odbýt nová" toggle: "hromadný výběr témat" @@ -1304,7 +1304,7 @@ cs: read_more: "Chcete si přečíst další informace? {{catLink}} nebo {{latestLink}}." read_more_MF: "{ UNREAD, plural, =0 {} one { Je tu 1 nepřečtené } other { Je tu # nepřečtených } } { NEW, plural, =0 {} one { {BOTH, select, true{and } false {is } other{}} 1 nové téma} other { {BOTH, select, true{and } false {are } other{}} # nových témat} } remaining, nebo {CATEGORY, select, true {si projděte ostatní témata v kategorii {catLink}} false {{latestLink}} other {}}" browse_all_categories: Projděte všechny kategorie - view_latest_topics: si zobrazte populární témata + view_latest_topics: zobrazte si populární témata suggest_create_topic: Co takhle založit nové téma? jump_reply_up: přejít na předchozí odpověď jump_reply_down: přejít na následující odpověď @@ -1952,7 +1952,7 @@ cs: one: "Nepřečtená (1)" few: "Nepřečtená ({{count}})" other: "Nepřečtená ({{count}})" - help: "témata. která sledujete nebo hlídáte, s nepřečtenými příspěvky" + help: "témata, která sledujete nebo hlídáte, s nepřečtenými příspěvky" lower_title_with_count: one: "{{count}} nepřečtené" few: "{{count}} nepřečtených" @@ -2446,31 +2446,12 @@ cs: customize: title: "Přizpůsobení" long_title: "Přizpůsobení webu" - css: "CSS" - header: "header" - top: "Vršek" - footer: "Patička" - head_tag: - text: "" - title: "HTML které bude vloženo před HTML tag" - body_tag: - text: "" - title: "HTML které bude vloženo před HTML tag" - override_default: "Přetížit výchozí?" - enabled: "Zapnuto?" preview: "náhled" - undo_preview: "odstranit náhled" - rescue_preview: "výchozí styl" - explain_preview: "Náhled stránky s vlastním stylesheetem." - explain_undo_preview: "Vrátit se k aktuálnímu použitému vlastnímu stylesheetu." - explain_rescue_preview: "Zobrazit web s výchozím stylesheetem." save: "Uložit" new: "Nové" new_style: "Nový styl" import: "Import" - import_title: "Vyberte soubor nebo vložte text" delete: "Smazat" - delete_confirm: "Smazat toto přizpůsobení?" about: "Změn CSS styly a hlavičky HTML na stránkách. Přidej přizpůsobení na začátek." color: "Barva" opacity: "Neprůhlednost" @@ -2483,13 +2464,9 @@ cs: none_selected: "Pro začátek editace zvolte šablonu emailu." revert: "Vrátit změny" revert_confirm: "Opravdu chcete vrátit změny?" - css_html: - title: "CSS/HTML" - long_title: "Přizpůsobení CSS a HTML" colors: title: "Barvy" long_title: "Barevná schémata" - about: "Změn barvy použité na stránkách bez psaní CSS. Přidej schéma na začátek." new_name: "Nové barevné schéma" copy_name_prefix: "Kopie" delete_confirm: "Chcete smazat toto barevné schéma?" @@ -2623,8 +2600,6 @@ cs: change_trust_level: "z. důvěryhodnosti" change_username: "Změnit uživatelské jméno" change_site_setting: "změna nastavení" - change_site_customization: "změna přizpůsobení" - delete_site_customization: "odstranit přizpůsobení" change_site_text: "změnit text webu" suspend_user: "suspendovat uživatele" unsuspend_user: "zrušit suspendování" diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml index 54a6e9c620..12e1992e80 100644 --- a/config/locales/client.da.yml +++ b/config/locales/client.da.yml @@ -2430,32 +2430,12 @@ da: customize: title: "Tilpasning" long_title: "Tilpasning af site" - css: "CSS" - header: "Header" - top: "Top" - footer: "Bund" - embedded_css: "Indlejret CSS" - head_tag: - text: "" - title: "HTML som indsættes før -tagget" - body_tag: - text: "" - title: "HTML som indsættes før -tagget" - override_default: "Inkludér ikke standard stylesheet" - enabled: "Aktiveret?" preview: "forhåndsvisning" - undo_preview: "skjul forhåndsvisning" - rescue_preview: "standard stil" - explain_preview: "Se sitet med dette stylesheet" - explain_undo_preview: "Gå tilbage til det aktuelle ændrede stylesheet" - explain_rescue_preview: "Se sitet med standard stylesheet" save: "Gem" new: "Ny" new_style: "Ny style" import: "Importer" - import_title: "Vælg en fil eller indsæt tekst" delete: "Slet" - delete_confirm: "Slet denne tilpasning?" about: "Modificer CSS stylesheets og HTML headere på sitet. Tilføj en tilpasning for at starte." color: "Farve" opacity: "Gennemsigtighed" @@ -2468,13 +2448,9 @@ da: none_selected: "Vælg en email-skabelon for at begynde at redigere." revert: "Rul ændringer tilbage" revert_confirm: "Er du sikker på, at du vil rulle ændringerne tilbage?" - css_html: - title: "CSS, HTML" - long_title: "CSS og HTML tilpasninger" colors: title: "Farver" long_title: "Farve temaer" - about: "Modificer farverne der bliver brugt på sitet uden at skrive CSS. Tilføj et tema for at begynde." new_name: "Nyt farve tema" copy_name_prefix: "Kopi af" delete_confirm: "Slet dette farvetema?" @@ -2610,8 +2586,6 @@ da: change_trust_level: "skift tillidsniveau" change_username: "skift brugernavn" change_site_setting: "skift indstillinger for site" - change_site_customization: "skift tilpasning af site" - delete_site_customization: "slet tilpasning af site" change_site_text: "skift tekst for site" suspend_user: "suspendér user" unsuspend_user: "ophæv suspendering af bruger" diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index 79807efa0b..58dd2818ad 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -2502,32 +2502,12 @@ de: customize: title: "Anpassen" long_title: "Anpassungen" - css: "CSS" - header: "Kopfbereich" - top: "Anfang" - footer: "Fußzeile" - embedded_css: "Eingebettetes CSS" - head_tag: - text: "" - title: "HTML das vor dem Tag eingefügt wird." - body_tag: - text: "" - title: "HTML das vor dem Tag eingefügt wird." - override_default: "Das Standard-Stylesheet nicht verwenden" - enabled: "Aktiviert?" preview: "Vorschau" - undo_preview: "Vorschau entfernen" - rescue_preview: "Standard-Style" - explain_preview: "Zeige die Website mit benutzerdefiniertem Stylesheet an" - explain_undo_preview: "Gehe zurück zum aktuell aktivierten, benutzerdefinierten Stylesheet" - explain_rescue_preview: "Zeige die Website mit dem Standard-Stylesheet an" save: "Speichern" new: "Neu" new_style: "Neuer Style" import: "Importieren" - import_title: "Datei auswählen oder Text einfügen" delete: "Löschen" - delete_confirm: "Diese Anpassung löschen?" about: "Ändere die Stylesheets (CSS) und den HTML-Header auf der Website. Füge eine Anpassung hinzu, um zu starten." color: "Farbe" opacity: "Transparenz" @@ -2540,13 +2520,9 @@ de: none_selected: "Wähle eine E-Mail-Vorlage aus, um diese zu bearbeiten." revert: "Änderungen verwerfen" revert_confirm: "Möchtest du wirklich deine Änderungen verwerfen?" - css_html: - title: "CSS/HTML" - long_title: "CSS und HTML Anpassungen" colors: title: "Farben" long_title: "Farbschemata" - about: "Farbschemen erlauben dir die auf der Seite benutzen Farben zu ändern ohne CSS schreiben zu müssen. Füge ein Schema hinzu, um zu beginnen." new_name: "Neues Farbschema" copy_name_prefix: "Kopie von" delete_confirm: "Dieses Farbschema löschen?" @@ -2685,8 +2661,6 @@ de: change_trust_level: "Vertrauensstufe ändern" change_username: "Benutzernamen ändern" change_site_setting: "Einstellungen ändern" - change_site_customization: "Anpassungen ändern" - delete_site_customization: "Anpassungen löschen" change_site_text: "Text ändern" suspend_user: "Benutzer sperren" unsuspend_user: "Benutzer entsperren" diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml index 087a2c6e03..c337115e03 100644 --- a/config/locales/client.el.yml +++ b/config/locales/client.el.yml @@ -2506,32 +2506,12 @@ el: customize: title: "Προσαρμογή" long_title: "Προσαρμογές ιστότοπου" - css: "CSS" - header: "Κεφαλίδα" - top: "Κορυφή" - footer: "Υποσέλιδο" - embedded_css: "Ενσωματωμένο CSS" - head_tag: - text: "" - title: "HTML που θα μπει πριν από την ετικέτα " - body_tag: - text: "" - title: "HTML που θα μπει πριν από την ετικέτα " - override_default: "Να μη χρησιμοποιηθεί το προκαθορισμένο αρχείο στυλ" - enabled: "Ενεργοποιημένο;" preview: "προεπισκόπηση " - undo_preview: "αφαίρεση προεπισκόπησης" - rescue_preview: "Προκαθορισμένο στυλ" - explain_preview: "Δείξε τον ιστότοπο με αυτό το προσαρμοσμένο αρχείο στυλ" - explain_undo_preview: "Επιστροφή στο τρέχον προσαρμοσμένο αρχείο στυλ" - explain_rescue_preview: "Δείξε τον ιστότοπο με το προκαθορισμένο αρχείο στυλ" save: "Αποθήκευση" new: "Νέο" new_style: "Νέο στυλ" import: "Εισαγωγή" - import_title: "Επίλεξε ένα φάκελο ή κάνε επικόλληση κείμενο" delete: "Σβήσιμο" - delete_confirm: "Να σβηστεί αυτή η προσαρμογή;" about: "Προσάρμοσε τα αρχεία στυλ CSS και την κεφαλίδα HTML του ιστότοπου. Πρόσθεσε μια προσαρμογή για να ξεκινήσεις." color: "Χρώμα" opacity: "Αδιαφάνεια" @@ -2544,13 +2524,9 @@ el: none_selected: "Επίλεξε ένα πρότυπο ηλεκτρονικού μηνύματος να ξεκινήσεις την επεξεργασία." revert: "Επαναφορά αλλαγών" revert_confirm: "Είσαι βέβαιος πως θέλεις να επαναφέρεις τις αλλαγές;" - css_html: - title: "CSS/HTML" - long_title: "Προσαρμογές CSS και HTML" colors: title: "Χρώματα" long_title: "Χρωματικά σύνολα" - about: "Προσάρμοσε τα χρώματα του ιστότοπου χωρίς να γράψεις CSS. Πρόσθεσε ένα χρωματικό σύνολο για να ξεκινήσεις." new_name: "Νέο χρωματικό σύνολο" copy_name_prefix: "Αντίγραφο του" delete_confirm: "Να διαγραφεί αυτό το χρωματικό σύνολο;" @@ -2689,8 +2665,6 @@ el: change_trust_level: "αλλαγή επιπέδου εμπιστοσύνης" change_username: "αλλαγή χρηστώνυμου" change_site_setting: "αλλαγή ρυθμίσεων ιστότοπου" - change_site_customization: "αλλαγή των προσαρμογών του ιστότοπου" - delete_site_customization: "σβήσιμο των προσαρμογών του ιστότοπου" change_site_text: "αλλαγή θέσης κειμένου" suspend_user: "Αποκλεισμός του χρήστη" unsuspend_user: "Αναίρεση αποκλεισμού χρήστη" diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index 1c56e6c2ea..24ed89f13e 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -140,6 +140,8 @@ es: emails_are_disabled: "Todos los emails salientes han sido desactivados por un administrador. No se enviará ninguna notificación por email." bootstrap_mode_enabled: "Para lanzar tu nuevo sitio más fácilmente, estás en modo de arranque. A todos los nuevos usuarios se les concederá el nivel 1 de confianza y recibirán resúmenes diarios por email. Esto se desactivará automáticamente cuando el número total de usuarios exceda los %{min_users}." bootstrap_mode_disabled: "El modo de arranque se desactivará en las próximas 24 horas." + themes: + default_description: "Por defecto" s3: regions: us_east_1: "EEUU Este (Virginia del Norte)" @@ -147,6 +149,7 @@ es: us_west_2: "EEUU Oeste (Oregon)" us_gov_west_1: "AWS GovCloud (US)" eu_west_1: "UE (Irlanda)" + eu_west_2: "EU (Londres)" eu_central_1: "UE (Frankfurt)" ap_southeast_1: "Asia Pacific (Singapur)" ap_southeast_2: "Asia Pacific (Sydney)" @@ -565,6 +568,7 @@ es: revoke_access: "Revocar acceso" undo_revoke_access: "Deshacer revocación de acceso" api_approved: "Aprobado:" + theme: "Tema" staff_counters: flags_given: "reportes útiles" flagged_posts: "posts reportados" @@ -934,6 +938,7 @@ es: not_allowed_from_ip_address: "No puedes iniciar sesión desde esa dirección IP." admin_not_allowed_from_ip_address: "No puedes iniciar sesión como admin desde esta dirección IP." resend_activation_email: "Has clic aquí para enviar el email de activación nuevamente." + resend_title: "Enviar de nuevo el email de activación" change_email: "Cambiar dirección de email" provide_new_email: "Escribe una nueva dirección y le reenviaremos el correo de confirmación." submit_new_email: "Actualizar dirección de email" @@ -1325,6 +1330,7 @@ es: deleted: "El tema ha sido borrado" topic_status_update: time: "Tiempo:" + publish_to: "Publicar a:" auto_update_input: limited: units: "(número de horas)" @@ -2530,32 +2536,12 @@ es: customize: title: "Personalizar" long_title: "Personalizaciones del sitio" - css: "CSS" - header: "Encabezado" - top: "Top" - footer: "Pie de página" - embedded_css: "CSS embebido" - head_tag: - text: "" - title: "HTML insertado antes de la etiqueta " - body_tag: - text: "" - title: "HTML insertado antes de la etiqueta " - override_default: "No incluir hoja de estilo estándar" - enabled: "¿Activado?" preview: "vista previa" - undo_preview: "eliminar vista previa" - rescue_preview: "estilo por defecto" - explain_preview: "Ver el sitio con esta hoja de estilo" - explain_undo_preview: "Volver a la hoja de estilo personalizada activada actualmente" - explain_rescue_preview: "Ver el sitio con la hoja de estilo por defecto" save: "Guardar" new: "Nuevo" new_style: "Nuevo Estilo" import: "Importar" - import_title: "Selecciona un archivo o pega texto" delete: "Eliminar" - delete_confirm: "¿Eliminar esta personalización?" about: "Modifica hojas de estilo CSS y cabeceras HTML en el sitio. Añade una personalización para empezar." color: "Color" opacity: "Opacidad" @@ -2568,13 +2554,40 @@ es: none_selected: "Selecciona un 'diseño de email' para comenzar a editar" revert: "Revertir los cambios" revert_confirm: "¿Estás seguro de querer revertir los cambios?" - css_html: - title: "CSS/HTML" - long_title: "Personalizaciones CSS y HTML" + theme: + edit: "Editar" + common: "Común" + desktop: "Ordenador" + mobile: "Móvil" + preview: "Previsualizar" + is_default: "Tema activado por defecto" + user_selectable: "Tema seleccionable por los usuarios" + color_scheme: "Paleta de color" + color_scheme_select: "Colores a utilizar por el tema:" + included_themes: "Temas incluidos" + child_themes_check: "El tema incluye otros temas" + css_html: "CSS/HTML personalizado" + edit_css_html: "Editar CSS/HTML" + edit_css_html_help: "No se ha editado ningún CSS ni HTML" + import_web_tip: "Repositorio que contiene el tema" + about_theme: "Sobre el tem" + license: "Licencia" + update_to_latest: "Actualizar a la última versión" + check_for_updates: "Comprobar actualizaciones" + updating: "Actualizando..." + up_to_date: "El tema está actualizado, última comprobación:" + add: "Añadir" + commits_behind: + one: "¡El tema está 1 commit atrasado!" + other: "¡El tema está {{count}} commits atrasado!" + scss: + text: "CSS" + title: "Introducir CSS personalizado, se acepta tanto CSS como SCSS" + header: + text: "Header" colors: title: "Colores" long_title: "Esquemas de color" - about: "Modifica los colores utilizados en el sitio sin editar el CSS. Añade un esquema de color para empezar." new_name: "Nuevo esquema de color" copy_name_prefix: "Copia de" delete_confirm: "¿Eliminar este esquema de color?" @@ -2713,8 +2726,8 @@ es: change_trust_level: "cambiar nivel de confianza" change_username: "cambiar nombre de usuario" change_site_setting: "cambiar configuración del sitio" - change_site_customization: "cambiar customización del sitio" - delete_site_customization: "borrar customización del sitio" + change_theme: "cambiar tem" + delete_theme: "borrar tema" change_site_text: "cambiar textos" suspend_user: "suspender usuario" unsuspend_user: "desbloquear usuario" diff --git a/config/locales/client.et.yml b/config/locales/client.et.yml index 5d1f67d640..6c7ccc40ae 100644 --- a/config/locales/client.et.yml +++ b/config/locales/client.et.yml @@ -2474,32 +2474,12 @@ et: customize: title: "Kohanda" long_title: "Saidi kohandused" - css: "CSS" - header: "Päis" - top: "Ülemine" - footer: "Jalus" - embedded_css: "Sängitatud CSS" - head_tag: - text: "" - title: "HTML, mida lisatakse enne silti" - body_tag: - text: "" - title: "HTML, mida lisatakse enne silti" - override_default: "Ära standardset laadilehte kaasa" - enabled: "Sisse lülitatud?" preview: "eelvaade" - undo_preview: "eemalda eelvaade" - rescue_preview: "vaikelaad" - explain_preview: "Vaata saiti selle kohandatud laadilehega" - explain_undo_preview: "Pöördu tagasi hetkel aktiivse kohandatud laadilehe juurde" - explain_rescue_preview: "Vaata saiti vaikimisi laadilehega" save: "Salvesta" new: "Uus" new_style: "Uus stiil" import: "Impordi" - import_title: "Vali fail või kleebi tekst" delete: "Kustuta" - delete_confirm: "Kustutan selle kohanduse?" about: "Muuda saidi CSS laadilehed ja HTML päised. Alustamiseks lisa kohandus." color: "Värv" opacity: "Läbipaistvus" @@ -2512,13 +2492,9 @@ et: none_selected: "Redigeerimise alustamiseks vali meili mall." revert: "Loobu muudatustest" revert_confirm: "Oled kindel, et soovid oma muudatustest loobuda?" - css_html: - title: "CSS/HTML" - long_title: "CSS and HTML modifikatsioonid" colors: title: "Värvid" long_title: "Värvistikud" - about: "Muuda saidil kasutatavaid värve ilma CSS-i kirjutamata. Alustamiseks lisa uus värvistik." new_name: "Uus värvistik" copy_name_prefix: "Koopia sellest" delete_confirm: "Kustutan selle värvistiku?" @@ -2657,8 +2633,6 @@ et: change_trust_level: "muuda usaldustaset" change_username: "muuda kasutajanime" change_site_setting: "muuda saidi sätet" - change_site_customization: "muuda saidi kohandusi" - delete_site_customization: "kustuta saidi kohandused" change_site_text: "muuda saidi teksti" suspend_user: "peata kasutaja" unsuspend_user: "taasluba kasutaja" diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml index 00a42956a1..4a8fce5d58 100644 --- a/config/locales/client.fa_IR.yml +++ b/config/locales/client.fa_IR.yml @@ -92,6 +92,8 @@ fa_IR: google+: 'این پیوند را در Google+‎ به اشتراک بگذارید.' email: 'این پیوند را با ایمیل بفرستید' action_codes: + public_topic: "عمومی کردن این مبحث در %{when} " + private_topic: "خصوصی کردن این مبحث در %{when}" split_topic: "این مبحث را جدا کنید %{when}" autoclosed: enabled: 'بسته شد %{when}' @@ -144,6 +146,8 @@ fa_IR: more: "بیشتر" less: "کمتر" never: "هرگز" + every_30_minutes: "هر 30 دقیقه" + every_hour: "هر ساعت" daily: "روزانه" weekly: "هفتگی" every_two_weeks: "هر دو هفته" @@ -154,6 +158,7 @@ fa_IR: other: "{{count}} نویسه" suggested_topics: title: "موضوعات پیشنهادی" + pm_title: "پیامهای پیشنهادی" about: simple_title: "درباره" title: "درباره %{title}" @@ -167,6 +172,7 @@ fa_IR: like_count: "لایک ها " topic_count: "موضوعات" post_count: "پست ها" + user_count: "کاربران" active_user_count: "کاربران فعال" contact: "ارتباط با ما" contact_info: "در شرایط حساس و مسائل اضطراری مربوط به سایت٬‌ لطفا با تماس بگیرید از طریق %{contact_info}." @@ -204,6 +210,8 @@ fa_IR: undo: "برگردانی" revert: "برگشت" failed: "ناموفق" + switch_to_anon: "ورود به حالت ناشناس" + switch_from_anon: "خروج از حالت ناشناس" banner: close: "این سردر را رد بده." edit: "این بنر را ویرایش کنید >>" @@ -225,6 +233,7 @@ fa_IR: has_pending_posts: other: "این عنوان دارای {{count}} نوشته‌ی در انتظار تایید است" confirm: "ذخیره سازی تغییرها" + delete_prompt: "آیا از حذف %{username} اطمینان دارید؟ این عمل باعث حذف شدن تمامی نوشته های او و همینطور باعث مسدود شدن IP و ایمیل او نیز می شود." approval: title: "نوشته نیاز به تایید دارد" description: "ما نوشته شما را دریافت کرده ایم ولی نیاز به تایید آن توسط یکی از مدیران است قبل از اینکه نمایش داده شود. لطفا صبر داشته باشید." @@ -250,6 +259,8 @@ fa_IR: title: "کاربران" likes_given: "داده" likes_received: "دریافت" + topics_entered: "بازدید" + topics_entered_long: "بازدید مباحث" time_read: "زمان خوانده‌ شده" topic_count: "موضوعات" topic_count_long: "موضوعات ساخته شده" @@ -262,17 +273,66 @@ fa_IR: posts_read_long: "خواندن نوشته ها" total_rows: other: "%{count} کاربران" + group_histories: + actions: + change_group_setting: "تغییر تنظیمات گروه" + add_user_to_group: "افزودن کاربر" + remove_user_from_group: "حذف کاربر" + make_user_group_owner: "مدیر کردن" + remove_user_as_group_owner: "ابطال مدیریت" groups: + logs: + title: "گزارش ها" + when: "چه زمانی" + action: "عمل" + acting_user: "کاربر اقدام کننده" + target_user: "کاربر مقصد" + subject: "موضوع" + details: "جزییات" + from: "از" + to: "به" + edit: + title: 'ویرایش گروه' + full_name: 'نام کامل' + add_members: "افزودن اعضا" + delete_member_confirm: "کاربر '%{username}' از گروه '%{group}' حذف گردد؟" + request_membership_pm: + title: "درخواست عضویت" + body: "من تقاضای عضویت در @%{groupName} را دارم." + name_placeholder: "در نام گروه نمی توان از فاصله استفاده کرد، مانند قوانین نامگذاری نام کاربری" + empty: + posts: "در این گروه هیچ نوشته ای توسط کاربران ارسال نشده." + members: "این گروه هیچ عضوی ندارد" + mentions: "هیچ اشاره ای به این گروه وجود ندارد." + messages: "هیچ پیامی برای این گروه وجود ندارد." + topics: "در این گروه هیچ مبحثی توسط اعضای آن ایجاد نشده." + logs: "هیچ گزارشی برای این گروه موجود نیست." add: "افزودن" + join: "ورود به گروه" + leave: "ترک گروه" + request: "درخواست ورود به گروه" + automatic_group: گروه خودکار + closed_group: گروه بسته شده + is_group_user: "شما عضو این گروه هستید" + allow_membership_requests: "اجازه ارسال درخواست عضویت در گروه به کاربران" + membership: "عضویت" + name: "نام" + user_count: "تعداد اعضا" + bio: "درباره گروه" selector_placeholder: "افزودن عضو" owner: "مالک" visible: "همهٔ کاربران گروه را می‌بینند" index: title: "گروه‌ها" + empty: "هیچ گروه قابل نمایشی وجود ندارد." title: other: "گروه‌ها" + activity: "فعالیت" members: "اعضا" + topics: "مباحث" posts: "نوشته ها" + mentions: "نام برده شده ها" + messages: "پیامها" alias_levels: title: "چه کسی میتواند پیام بفرستد و به این گروه @اشاره کند؟" nobody: "هیچ‌کس" @@ -287,6 +347,9 @@ fa_IR: watching: title: "در حال مشاهده" description: "در صورت ارسال شدن پست جدید در هر پیام یک اعلان برای شما ارسال می‌شود و تعداد پاسخ‌های جدید نمایش داده می‌شود." + watching_first_post: + title: "درحال مشاهده نوشته اول" + description: "تنها برای اولین نوشته در هر مبحث جدید به شما اطلاع رسانی خواهد شد." tracking: title: "ردگیری" description: "در صورت اشاره شدن به @نام شما توسط اشخاص دیگر و یا دریافت پاسخ، اعلانی برای شما ارسال می‌شود و تعداد پاسخ‌های جدید نمایش داده می‌شود." @@ -296,6 +359,14 @@ fa_IR: muted: title: "بی صدا شد" description: "با ارسال شدن موضوعات جدید در این گروه شما اعلانی دریافت نمی‌کنید." + flair_url: "تصویر آواتار" + flair_url_placeholder: "(اختیاری) URL تصویر و یا کلاس فونت Awesome " + flair_bg_color: "رنگ پس زمینه آواتار" + flair_bg_color_placeholder: "(اختیاری) کد هگز رنگ" + flair_color: "رنگ آواتار" + flair_color_placeholder: "(اختیاری) کد هگز رنگ" + flair_preview_icon: "پیشنمایش آیکون" + flair_preview_image: "پیش‌نمایش تصویر" user_action_groups: '1': "پسندهای داده شده" '2': "پسندهای دریافت شده" @@ -329,6 +400,8 @@ fa_IR: latest_by: "آخرین توسط" toggle_ordering: "ضامن کنترل مرتب سازی" subcategories: "زیر دسته‌ بندی ها" + topic_sentence: + other: "%{count} مبحث" topic_stat_sentence: other: "%{count} موضوعات تازه در %{unit} گذشته." ip_lookup: @@ -353,6 +426,11 @@ fa_IR: profile: "نمایه" mute: "بی صدا" edit: "ویرایش تنظیمات" + download_archive: + button_text: "دانلود نوشته های من" + confirm: "آیا مطمئنید که می‌خواهید نوشته‌هایتان را دانلود کنید؟" + success: "شروع فرایند دانلود، وقتی این فرایند تکمیل شود به شما از طریق پیام، اطلاع رسانی خواهد شد." + rate_limit_error: "نوشته ها را می توانید فقط روزی یک بار دانلود کنید. لطفا فردا دوباره امتحان کنید." new_private_message: "پیام های جدید" private_message: "پیام" private_messages: "پیام‌ها" @@ -370,10 +448,13 @@ fa_IR: not_supported: "اعلانات بر روی این مرورگر پشتیبانی نمیشوند. با عرض پوزش." perm_default: "فعال کردن اعلانات" perm_denied_btn: "دسترسی رد شد" + perm_denied_expl: "شما دسترسی دریافت پیام را بسته اید. در تنظیمات مرورگر خود آنرا فعال کنید." disable: "غیرفعال کردن اعلانات" enable: "فعال کردن اعلانات" each_browser_note: "نکته: شما باید این تنظیمات را در هر مرورگری که استفاده میکنید تغییر دهید." + dismiss_notifications: "پنهان کردن همه" dismiss_notifications_tooltip: "علامت گذاری همه اطلاعیه های خوانده نشده به عنوان خوانده شده" + first_notification: "اولین پیام اطلاع رسانی شما! برای شروع آنرا انتخاب کنید." disable_jump_reply: "بعد از پاسخ من به پست من پرش نکن" dynamic_favicon: " تعداد موضوعات جدید یا بروز شده را روی آیکون مرورگر نمایش بده" external_links_in_new_tab: "همهٔ پیوندهای برون‌رو را در یک تب جدید باز کن" @@ -387,6 +468,7 @@ fa_IR: suspended_notice: "این کاربر تا {{date}} در وضعیت معلق است." suspended_reason: "دلیل: " github_profile: "Github" + email_activity_summary: "چکیدهٔ فعالیت" watched_categories: "تماشا شده" tracked_categories: "پی‌گیری شده" muted_categories: "بی صدا شد" @@ -1714,32 +1796,12 @@ fa_IR: customize: title: "شخصی‌سازی" long_title: "شخصی‌سازی سایت" - css: "CSS" - header: "سردر" - top: "بالا" - footer: "پانوشته " - embedded_css: "CSS جاساز شده" - head_tag: - text: "" - title: "HTML هایی که قرار داده شده قبل از تگ " - body_tag: - text: "" - title: "HTML هایی که قرار داده شده قبل از تگ " - override_default: "شامل شیوه نامه استاندارد نکن" - enabled: "فعال شد؟" preview: "پیش‌نمایش" - undo_preview: "حذف پیش نمایش" - rescue_preview: "به سبک پیش فرض" - explain_preview: "مشاهده‌ی سایت با این قالب سفارشی" - explain_undo_preview: "بازگشت به شیوه نامه های فعال شخصی" - explain_rescue_preview: "دیدن سایت با شیوه نامه پیش فرض" save: "ذخیره سازی" new: "تازه" new_style: "سبک جدید" import: "ورود داده‌ها" - import_title: "فایلی را انتخاب یا متنی را پیست کنید" delete: "پاک کردن" - delete_confirm: "پاک کردن این شخصی‌سازی؟" about: "اطلاح شیوه نامه CSS و هدر HTML در سایت، اضافه کردنیک سفارشی سازی برای شروع." color: "رنگ" opacity: "تاری" @@ -1752,13 +1814,9 @@ fa_IR: none_selected: "یک قالب ایمیل برای شروع ویرایش انتخاب کنید." revert: "باطل کردن تغییرات" revert_confirm: "آیا مطمئن هستید که میخواهید تنظیمات را باطل کنید؟" - css_html: - title: "CSS/HTML" - long_title: "شخصی‌سازی CSS و HTML" colors: title: "رنگ‌ها" long_title: "طرح‌های رنگی" - about: "تغییر رنگ استفاده شده در انجمن بدون نوشتن کد CSS.با اضافه کردن یک طرح شروع کنید." new_name: "طرح رنگ جدید" copy_name_prefix: "نمونه سازی از" delete_confirm: "این طرح رنگ پاک شود؟" @@ -1874,8 +1932,6 @@ fa_IR: change_trust_level: "تغییر دادن سطح اعتماد" change_username: "تغییر نام کاربری" change_site_setting: "تغییر تنظیمات سایت" - change_site_customization: "تغییر سفارشی‌سازی سایت" - delete_site_customization: "پاک‌کردن سفارشی‌سازی سایت" change_site_text: "تغییر نوشته سایت" suspend_user: "کاربر تعلیق شده" unsuspend_user: "کابر تعلیق نشده" @@ -2205,6 +2261,7 @@ fa_IR: post_revision: "هنگامی که یک کاربر نوشته ای ویرایش می‌کند یا فرستد" trust_level_change: "هنگامی که کاربری سطح اعتماد را تغییر می‌دهد" user_change: "هنگامی که کاربری ویرایش یا ساخته می‌شود" + post_processed: "پس از پردازش یک پست" preview: link_text: "پیش نمایش مدال های اعطایی" plan_text: "پیشنمایش با طرح پرسش" @@ -2236,6 +2293,7 @@ fa_IR: sample: "از این کد HTML در سایت خود استفاده کنید تا بتوانید مبحث های Discourse ایجاد کنید یا جاساز کنید, این کد REPLACE_ME را به URL استاندارد صفحه ای که بر روی آن جاسازی میکنید تغییر دهید." title: "جاسازی" host: "میزبان های مجاز" + path_whitelist: "مسیر لیست سفید" edit: "ویرایش" category: "ارسال به دسته بندی" add_host: "اضافه کردن میزبان" @@ -2247,9 +2305,11 @@ fa_IR: embed_by_username: "نام کاربری برای ساخت مبحث" embed_post_limit: "حداکثر تعداد پست هایی که میتوان جاساز کرد" embed_username_key_from_feed: "کلیدی برای کشیدن نام کاربری Discourse از فید" + embed_title_scrubber: "عبارت منظم استفاده میشه برای ورز دادن عنوان پست" embed_truncate: "کوتاه کردن نوشته های جاسازی شده" embed_whitelist_selector: "CSS انتخاب کننده برای المان هایی که اجازه دارند جاسازی شوند" embed_blacklist_selector: "انتخاب کننده CSS برای المان هایی که از جاسازی پاک شده اند" + embed_classname_whitelist: " دسترسی به کلاس های سی اس اس" feed_polling_enabled: "وارد کردن پست ها توسط RSS/ATOM" feed_polling_url: " لینک RSS/ATOM فید برای خزیدن" save: "ذخیره تنظیمات کدهای جاساز" @@ -2268,3 +2328,12 @@ fa_IR: label: "جدید:" add: "افزودن" filter: "جستجو (آدرس یا آدرس خارجی)" + wizard_js: + wizard: + back: "قبلی" + next: "بعدی" + upload: "بارگذاری شد!" + uploading: "در حال بار گذاری ..." + quit: "شاید بعدا" + invites: + add_user: "اضافه کردن" diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index ba4b3c2582..4d29934d4e 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -143,6 +143,8 @@ fi: emails_are_disabled: "Ylläpitäjä on estänyt kaiken lähtevän sähköpostiliikenteen. Mitään sähköposti-ilmoituksia ei lähetetä." bootstrap_mode_enabled: "Jotta uuden palsta käynnistäminen olisi helpompaa, on sivusto asetettu aloitustilaan. Kaikki uudet käyttäjät siirretään automaattisesti luottamustasolle 1 ja heille lähetetään sähköpostitiivistelmät päivittäin. Tämä tila poistetaan automaattisesti, kun käyttäjien määrä ylittää %{min_users}." bootstrap_mode_disabled: "Aloitustila poistetaan seuraavan 24 tunnin aikana." + themes: + default_description: "Oletus" s3: regions: us_east_1: "itäinen USA (Pohjois-Virginia)" @@ -150,6 +152,7 @@ fi: us_west_2: "Läntinen USA (Oregon)" us_gov_west_1: "AWS GovCloud (USA)" eu_west_1: "EU (Irlanti)" + eu_west_2: "EU (Lontoo)" eu_central_1: "EU (Frankfurt)" ap_southeast_1: "Aasia ja Tyynimeri (Singapore)" ap_southeast_2: "Aasia ja Tyynimeri (Sydney)" @@ -571,6 +574,7 @@ fi: revoke_access: "Peru käyttöoikeus" undo_revoke_access: "Peru käyttöoikeuden peruminen" api_approved: "Sallittu:" + theme: "Teema" staff_counters: flags_given: "hyödyllistä liputusta" flagged_posts: "liputettua viestiä" @@ -2546,32 +2550,14 @@ fi: customize: title: "Mukauta" long_title: "Sivuston mukautukset" - css: "CSS" - header: "Header" - top: "Alku" - footer: "Footer" - embedded_css: "Upotuksen CSS" - head_tag: - text: "" - title: "HTML, joka lisätään ennen elementtiä" - body_tag: - text: "" - title: "HTML, joka lisätään ennen elementtiä" - override_default: "Älä sisällytä oletus-tyylitiedostoa" - enabled: "Otettu käyttöön?" preview: "esikatselu" - undo_preview: "poista esikatselu" - rescue_preview: "oletustyyli" - explain_preview: "Esikatsele sivustoa käyttäen tätä tyylitiedostoa" - explain_undo_preview: "Siirry takaisin nykyisin käytössä olevaan tyylitiedostoon" - explain_rescue_preview: "Esikatsele sivustoa käyttäen oletustyylitiedostoa" + explain_preview: "Näe miltä sivusto näyttää tällä teemalla" save: "Tallenna" new: "Uusi" new_style: "Uusi tyyli" import: "Tuo" - import_title: "Valitse tiedosto tai liitä tekstiä" delete: "Poista" - delete_confirm: "Poista tämä mukautus?" + delete_confirm: "Poista tämä teema?" about: "Muokkaa sivuston CSS tyylitiedostoja ja HTML headeria. Lisää mukautus aloittaaksesi." color: "Väri" opacity: "Läpinäkyvyys" @@ -2584,13 +2570,49 @@ fi: none_selected: "Aloita muokkaaminen valitsemalla sähköpostiluonnos." revert: "Peru muutokset" revert_confirm: "Haluatko varmasti peruuttaa muutokset?" - css_html: - title: "CSS/HTML" - long_title: "CSS ja HTML Kustomoinnit" + theme: + import_theme: "Tuo teema" + customize_desc: "Mukauta:" + title: "Teemat" + long_title: "Muuta sivustosi värejä, CSS:ää ja HTML-sisältöä" + edit: "Muokkaa" + edit_confirm: "Tämä on ulkoinen teema. Jos muokkaat CSS:ää tai HTML:ää, muutokset kumoutuvat kun päivität teeman." + common: "Yhteiset" + desktop: "Työpöytä" + mobile: "Mobiili" + preview: "Esikatselu" + is_default: "Teema on käytössä oletuksena" + user_selectable: "Teema on tarjolla käyttäjille" + color_scheme: "Värimalli" + color_scheme_select: "Valitse värit, joita teema käyttä" + custom_sections: "Mukautetut osiot:" + included_themes: "Sisältyvät teemat" + child_themes_check: "Teemaan kuuluu tytärteemoja" + css_html: "Mukautettu CSS/HTML" + edit_css_html: "Muokkaa CSS/HTML" + edit_css_html_help: "Et ole muokannut CSS:ää tai HTML:ää" + import_web_tip: "Tietovarasto, jossa teema on" + import_file_tip: ".dcstyle.json-tiedosto, jossa teema on" + about_theme: "Tietoa teemasta" + license: "Lisenssi" + update_to_latest: "Päivitä tuoreimpaan" + check_for_updates: "Hae päivityksiä" + updating: "Päivitetään..." + up_to_date: "Teema on ajan tasalla, viimeksi tarkastettu:" + add: "Lisää" + commits_behind: + one: "Teema on yhden muutoksen perässä!" + other: "Teema on {{count}} muutosta perässä!" + scss: + text: "CSS" + title: "Lisää mukautettua CSS:ää, hyväksymme käyvät CSS- ja SCSS-tyylit" + header: + text: "Ylätunniste" colors: title: "Värit" + edit: "Muokkaa värimalleja" long_title: "Värimallit" - about: "Muokkaa sivuston värejä kirjoittamatta CSS-koodia. Lisää värimallia aloittaaksesi." + about: "Muuta teemojesi värejä. Aloita luomalla uusi värimalli." new_name: "Uusi värimalli" copy_name_prefix: "Kopio" delete_confirm: "Poista tämä värimalli?" @@ -2729,8 +2751,8 @@ fi: change_trust_level: "vaihda luottamustasoa" change_username: "vaihda käyttäjätunnus" change_site_setting: "muuta sivuston asetusta" - change_site_customization: "vaihda sivuston mukautusta" - delete_site_customization: "poista sivuston mukautus" + change_theme: "muuta teemaa" + delete_theme: "poista teema" change_site_text: "muutos sivuston teksteissä" suspend_user: "hyllytä käyttäjä" unsuspend_user: "poista hyllytys" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index cc9c7cadfc..e58152cf9c 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -2545,32 +2545,12 @@ fr: customize: title: "Personnaliser" long_title: "Personnalisations du site" - css: "CSS" - header: "Entête" - top: "Top" - footer: "Pied de page" - embedded_css: "CSS intégré" - head_tag: - text: "" - title: "HTML qui sera inséré avant la balise " - body_tag: - text: "" - title: "HTML qui sera inséré avant la balise " - override_default: "Ne pas inclure la feuille de style par défaut" - enabled: "Activé ?" preview: "prévisualiser" - undo_preview: "supprimer l'aperçu" - rescue_preview: "style par défaut" - explain_preview: "Voir le site avec la feuille de style personnalisé" - explain_undo_preview: "Revenir à la feuille de style personnalisé actuellement activée" - explain_rescue_preview: "Voir le site avec la feuille de style par défaut" save: "Sauvegarder" new: "Nouveau" new_style: "Nouveau style" import: "Importer" - import_title: "Sélectionnez un fichier ou collez du texte" delete: "Supprimer" - delete_confirm: "Supprimer cette personnalisation" about: "Modification des feuilles de styles et en-têtes de votre site. Ajouter un style personnalisé pour commencer." color: "Couleur" opacity: "Opacité" @@ -2583,13 +2563,9 @@ fr: none_selected: "Choisissez un modèle de courriel pour commencer l'édition." revert: "Annuler les changements" revert_confirm: "Êtes-vous sûr de vouloir annuler vos changements ?" - css_html: - title: "CSS/HTML" - long_title: "Personnalisations du CSS et HTML" colors: title: "Couleurs" long_title: "Palettes de couleurs" - about: "Modification des couleurs utilisés par le site sans écrire du CSS. Ajouter une palette pour commencer." new_name: "Nouvelle palette de couleurs" copy_name_prefix: "Copie de" delete_confirm: "Supprimer cette palette de couleurs ?" @@ -2728,8 +2704,6 @@ fr: change_trust_level: "modifier le niveau de confiance" change_username: "modifier pseudo" change_site_setting: "modifier les paramètres du site" - change_site_customization: "modifier la personnalisation du site" - delete_site_customization: "supprimer la personnalisation du site" change_site_text: "modifier le texte du site" suspend_user: "suspendre l'utilisateur" unsuspend_user: "annuler la suspension de l'utilisateur" diff --git a/config/locales/client.gl.yml b/config/locales/client.gl.yml index 8f05fff052..7ca9300598 100644 --- a/config/locales/client.gl.yml +++ b/config/locales/client.gl.yml @@ -1862,32 +1862,12 @@ gl: customize: title: "Personalizar" long_title: "Personalización do sitio" - css: "CSS" - header: "Cabeceira" - top: "Destacados" - footer: "Pé de páxina" - embedded_css: "CSS encaixado" - head_tag: - text: "" - title: "HTML que se inserirá antes da etiqueta " - body_tag: - text: "" - title: "HTML que se inserirá antes da etiqueta " - override_default: "Non incluír folla de estilo estándar" - enabled: "Activado?" preview: "previsualizar" - undo_preview: "eliminar previsualización" - rescue_preview: "estilo predeterminado" - explain_preview: "Ver o sitio con esta folla de estilo personalizada" - explain_undo_preview: "Volver á folla de estilo personalizada activada actualmente" - explain_rescue_preview: "Ver o sitio coa folla de estilo predeterminada" save: "Gardar" new: "Novo" new_style: "Novo estilo" import: "Importar" - import_title: "Seleccionar un ficheiro ou pegar un texto" delete: "Eliminar" - delete_confirm: "Queres eliminar esta personalización?" about: "Modificar as follas de estilo CSS e as cabeceiras HTML do sitio. Engadir unha personalización para comezar." color: "Cor" opacity: "Opacidade" @@ -1900,13 +1880,9 @@ gl: none_selected: "Selecciona un modelo de correo-e para comezar a edición." revert: "Reverter os cambios" revert_confirm: "Confirmas a reversión dos cambios?" - css_html: - title: "CSS/HTML" - long_title: "Personalizacións do CSS e do HTML" colors: title: "Cores" long_title: "Esquemas de cor" - about: "Modifica as cores usadas no sitio sen ter que escribir en CSS. Engade un esquema para comezar." new_name: "Novo esquema de cores" copy_name_prefix: "Copiar de" delete_confirm: "Queres eliminar este esquema de cor?" @@ -2039,8 +2015,6 @@ gl: change_trust_level: "cambiar nivel de confianza" change_username: "cambiar nome do usuario" change_site_setting: "cambiar axuste do sitio" - change_site_customization: "cambiar a personalización do sitio" - delete_site_customization: "eliminar a personalización do sitio" change_site_text: "cambiar o texto do sitio" suspend_user: "suspender usuario" unsuspend_user: "non suspender usuario" diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index d7ae28eeb6..a384a939f9 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -143,6 +143,8 @@ he: emails_are_disabled: "כל הדוא\"ל היוצא נוטרל באופן גורף על ידי מנהל אתר. שום הודעת דוא\"ל, מכל סוג שהוא, לא תשלח." bootstrap_mode_enabled: "כדי להקל על הקמת האתר החדש שלכם, אתם במצב איתחול-ראשוני. כל המשתמשים החדשים יקבלו רמת אמון 1 ויקבלו תמצות יומי במייל. אפשרות זו תכובה אוטומטית כאשר יהיו יותר מ %{min_users} משתמשים." bootstrap_mode_disabled: "מצב איתחול-ראשוני יכובה ב 24 השעות הקרובות." + themes: + default_description: "ברירת מחדל" s3: regions: us_east_1: "מזרח ארה\"ב (צפון וירג'יניה)" @@ -150,6 +152,7 @@ he: us_west_2: "מערב ארה\"ב (ארגון)" us_gov_west_1: "AWS GovCloud (ארה״ב)" eu_west_1: "האיחוד האירופי (אירלנד)" + eu_west_2: "אירופה (לונדון)" eu_central_1: "האיחוד האירופי (פרנקפורט)" ap_southeast_1: "אסיה הפסיפית (סינגפור)" ap_southeast_2: "אסיה הפסיפית (סידני)" @@ -570,6 +573,7 @@ he: revoke_access: "שלילת גישה" undo_revoke_access: "ביטול שלילת גישה" api_approved: "אושרו:" + theme: "תמה" staff_counters: flags_given: "דגלים שעוזרים" flagged_posts: "פסטים מדוגלים" @@ -2547,32 +2551,14 @@ he: customize: title: "התאמה אישית" long_title: "התאמה של האתר" - css: "CSS" - header: "כותרת" - top: "למעלה" - footer: "כותרת תחתית" - embedded_css: "Embedded CSS" - head_tag: - text: "" - title: "קוד HTML שיוכנס לפני התגית " - body_tag: - text: "" - title: "קוד HTML שיוכנס לפני התגית " - override_default: "אל תכלול את ה-Stylesheet הסטנדרטי" - enabled: "מאופשר?" preview: "תצוגה מקדימה" - undo_preview: "הסרת התצוגה המקדימה" - rescue_preview: "ברירת מחדל סגנונית" - explain_preview: "הצג את האתר על פי גיליון הסגנונות המותאם הזה" - explain_undo_preview: "חזרה לגיליון הסגנונות המותאם המופעל כרגע" - explain_rescue_preview: "צפיה באתר עם גליון הסגנונות העיצובי של ברירת המחדל" + explain_preview: "צפו באתר עם סכמה זו" save: "שמור" new: "חדש" new_style: "סגנון חדש" import: "יבוא" - import_title: "בחרו קובץ או הדביקו טקסט" delete: "מחק" - delete_confirm: "מחק את ההתאמה הזו?" + delete_confirm: "למחוק תמה זאת?" about: "שינוי סגנונות CSS וכותרות HTML באתר. הוספת התאמות כדי להתחיל לערוך." color: "צבע" opacity: "שקיפות" @@ -2585,13 +2571,68 @@ he: none_selected: "בחרו תבנית דואר אלקטרוני לעריכה." revert: "ביטול שינויים" revert_confirm: "האם ברצונכם לבטל את השינויים?" - css_html: - title: "CSS/HTML" - long_title: "התאמת CSS ו-HTML" + theme: + import_theme: "ייבוא תמה" + customize_desc: "התאמה אישית:" + title: "תמות" + long_title: "התאמת צבעית, CSS ותכני HTML של האתר שלכם" + edit: "עריכה" + edit_confirm: "זוהי תמה מרוחקת, אם אתם עורכים CSS/HTML השינויים שלכם יימחקו בפעם הבאה שתעדכנו את התמה." + common: "משותף" + desktop: "מחשב-שולחני" + mobile: "נייד" + preview: "תצוגה מקדימה" + is_default: "תמה מאופשרת כברירת מחדל" + user_selectable: "תמה ניתנת לבחירה על ידי משתמשים" + color_scheme: "סכמת צבעים" + color_scheme_select: "בחירת צבעים לשימוש על ידי תמה" + custom_sections: "אזורים מותאמים אישית:" + included_themes: "תמות כלולות" + child_themes_check: "תמה כוללת תמות בנות אחרות" + css_html: "CSS/HTML מותאמים" + edit_css_html: "עריכת CSS/HTML" + edit_css_html_help: "לא ערכתם אף CSS או HTML" + import_web_tip: "מאגר שמכיל תמה" + import_file_tip: "קובץ .dcstyle.json שמכיל תמה" + about_theme: "אודות התמה" + license: "רישיון" + update_to_latest: "עדכון לגרסה אחרונה" + check_for_updates: "בדיקת עדכונים" + updating: "מעדכנים..." + up_to_date: "התמה מעודכנת, נבדקה לאחרונה ב:" + add: "הוספה" + commits_behind: + one: "התמה נמצאת 1 עדכונים מאחור!" + other: "התמה נמצאת {{count}} עדכונים מאחור!" + scss: + text: "CSS" + title: "עריכת CSS מותאם, אנחנו מקבלים כל סגנון CSS ו SCSS תקני" + header: + text: "כותרת" + title: "הכניסו HTML להצגה מעל הכותרת" + after_header: + text: "אחרי הכותרת" + title: "הכניסו HTML להצגה בכל הדפים אחרי הכותרת" + footer: + text: "תחתית" + title: "הכניסו HTML להצגה בתחתית הדף" + embedded_scss: + text: "CSS משולב" + title: "הכניסו CSS מותאם לשליחה עם הגרסה המשולבת של ההערות" + head_tag: + text: "" + title: "HTML שיוכנס לפני התג " + body_tag: + text: "" + title: "HTML שיוכנס לפני התג " colors: + select_base: + title: "בחרו סכמת צבעים בסיסית" + description: "סכמת בסיס:" title: "צבעים" + edit: "עריכת סכמות צבעים" long_title: "סכמת צבעים" - about: "סכמת צבעים מאפשרת לך לשנות את הצבעים שבשימוש האתר ללא כתיבת קוד CSS. בחרו או הוסיפו סכימה אחת כדי להתחיל." + about: "שינוי הצבעים בהם נעשה שימוש על ידי התמות שלכם. יצירת סכמת צבעים חדשה כדי להתחיל." new_name: "סכמת צבעים חדשה" copy_name_prefix: "העתק של" delete_confirm: "מחק את סכמת הצבעים הזאת?" @@ -2730,8 +2771,8 @@ he: change_trust_level: "שנוי רמת אמון" change_username: "שינוי שם משתמש/ת" change_site_setting: "שנוי הגדרות אתר" - change_site_customization: "שנוי התאמת אתר" - delete_site_customization: "מחק התאמת אתר" + change_theme: "שינוי תמה" + delete_theme: "מחיקת תמה" change_site_text: "שינוי טקסט אתר" suspend_user: "השעה משתמש" unsuspend_user: "בטל השהיית משתמש" diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index dc3ffd9454..b289cf0e3a 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -2446,32 +2446,12 @@ it: customize: title: "Personalizza" long_title: "Personalizzazioni Sito" - css: "CSS" - header: "Intestazione" - top: "Alto" - footer: "Fondo pagina" - embedded_css: "CSS incorporato" - head_tag: - text: "" - title: "HTML da inserire prima del tag " - body_tag: - text: "" - title: "HTML da inserire prima del tag " - override_default: "Non includere fogli di stile standard" - enabled: "Attivo?" preview: "anteprima" - undo_preview: "rimuovi anteprima" - rescue_preview: "stile default" - explain_preview: "Visualizza il sito con questo foglio di stile personalizzato" - explain_undo_preview: "Torna al foglio di stile personalizzato attualmente attivo." - explain_rescue_preview: "Visualizza il sito con il foglio di stile predefinito" save: "Salva" new: "Nuovo" new_style: "Nuovo Stile" import: "Importare" - import_title: "Seleziona un file o incolla del testo" delete: "Cancella" - delete_confirm: "Cancella questa personalizzazione?" about: "Modifica i fogli di stile CSS e le intestazioni HTML del sito. Aggiungi una personalizzazione per iniziare." color: "Colore" opacity: "Opacità" @@ -2484,13 +2464,9 @@ it: none_selected: "Scegli un modello di e-mail per iniziare la modifica." revert: "Annulla Cambiamenti" revert_confirm: "Sei sicuro di voler annullare i cambiamenti?" - css_html: - title: "CSS/HTML" - long_title: "Personalizzazioni CSS e HTML" colors: title: "Colori" long_title: "Combinazioni Colori" - about: "Modifica i colori utilizzati sul sito senza scrivere CSS. Aggiungi una combinazione per iniziare." new_name: "Nuova Combinazione Colori" copy_name_prefix: "Copia di" delete_confirm: "Eliminare questa combinazione di colori?" @@ -2629,8 +2605,6 @@ it: change_trust_level: "cambia livello esperienza" change_username: "cambia nome utente" change_site_setting: "modifica le impostazioni del sito" - change_site_customization: "modifica la personalizzazione del sito" - delete_site_customization: "cancella la personalizzazione del sito" change_site_text: "cambia il testo del sito" suspend_user: "utente sospeso" unsuspend_user: "utente riattivato" diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index 9bd11dce6e..aaa6607e29 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -1826,31 +1826,12 @@ ja: customize: title: "カスタマイズ" long_title: "サイトのカスタマイズ" - css: "CSS" - header: "ヘッダ" - top: "トップ" - footer: "フッター" - head_tag: - text: "" - title: "タグの前に挿入されるHTML" - body_tag: - text: "" - title: "タグの前に挿入されるHTML" - override_default: "標準のスタイルシートを読み込まない" - enabled: "有効にする" preview: "プレビュー" - undo_preview: "プレビューを削除" - rescue_preview: "既定スタイル" - explain_preview: "カスタムスタイルシートでサイトを表示する" - explain_undo_preview: "有効中のカスタムスタイルシートへ戻る" - explain_rescue_preview: "既定スタイルシートでサイトを表示する" save: "保存" new: "新規" new_style: "新しいスタイル" import: "インポート" - import_title: "ファイルを選択するかテキストをペースト" delete: "削除" - delete_confirm: "このカスタマイズ設定を削除しますか?" about: "サイトカスタマイズ設定により、サイトのヘッダとスタイルシートを変更できます。設定を選択するか、編集を開始して新たな設定を追加してください。" color: "カラー" opacity: "透明度" @@ -1860,13 +1841,9 @@ ja: subject: "件名" multiple_subjects: "このメールのテンプレートは複数の件名があります。" none_selected: "編集するメールテンプレートを選択してください。" - css_html: - title: "CSS, HTML" - long_title: "CSS と HTML のカスタマイズ" colors: title: "カラー" long_title: "カラースキーム" - about: "CSSを記述することなくサイトのカラーを変更できます。スキームを追加して始めてください" new_name: "カラースキームを作成" copy_name_prefix: "のコピー" delete_confirm: "このカラースキームを削除してもよろしいですか?" @@ -1989,8 +1966,6 @@ ja: change_trust_level: "トラストレベルを変更" change_username: "ユーザ名変更" change_site_setting: "サイトの設定を変更" - change_site_customization: "サイトのカスタマイズ設定を変更" - delete_site_customization: "サイトのカスタマイズ設定を削除" suspend_user: "ユーザを凍結する" unsuspend_user: "ユーザの凍結を解除する" grant_badge: "バッジを付与" diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index d57052e183..393d35df4b 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -1846,32 +1846,12 @@ ko: customize: title: "사용자 지정" long_title: "사이트 사용자 지정" - css: "CSS" - header: "헤더" - top: "Top" - footer: "푸터(하단영역)" - embedded_css: "Embedded CSS" - head_tag: - text: "" - title: " 태그 전에 들어갈 HTML" - body_tag: - text: "" - title: " 태그 전에 들어갈 HTML" - override_default: "표준 스타일 시트를 포함하지 마십시오" - enabled: "사용가능?" preview: "미리 보기" - undo_preview: "미리보기 삭제" - rescue_preview: "기본 스타일" - explain_preview: "이 커스텀 스타일시트를 적용한 상태로 사이트를 봅니다." - explain_undo_preview: "현재 적용되어 있는 커스톰 스타일시트로 돌아갑니다." - explain_rescue_preview: "기본 스타일시트를 적용한 상태로 사이트를 봅니다." save: "저장" new: "새 사용자 지정" new_style: "새로운 스타일" import: "가져오기" - import_title: "파일을 선택하거나 텍스트를 붙여넣으세요" delete: "삭제" - delete_confirm: "이 정의를 삭제 하시겠습니까?" about: "사이트 Customization은 사이트의 스타일시트와 해더를 수정할 수 있게 해줍니다. 새로운 것을 추가하거나 기존 것을 선택해서 편집하세요." color: "색" opacity: "투명도" @@ -1884,13 +1864,9 @@ ko: none_selected: "편집하려는 이메일 템플릿을 선택하세요." revert: "변경사항 취소" revert_confirm: "정말로 변경사항을 되돌리시겠습니까?" - css_html: - title: "CSS/HTML" - long_title: "CSS, HTML 사용자 정의" colors: title: "색상" long_title: "색상 Schemes" - about: "CSS 작성 없이 사이트에 사용되는 색을 수정합니다. 시작하려면 Scheme을 추가하세요." new_name: "새로운 색 조합" copy_name_prefix: "복사본" delete_confirm: "이 컬러 스키마를 제거합니까?" @@ -2021,8 +1997,6 @@ ko: change_trust_level: "회원등급 변경" change_username: "아이디 변경" change_site_setting: "사이트 설정 변경" - change_site_customization: "사이트 커스텀화 변경" - delete_site_customization: "사이트 커스텀화 삭제" change_site_text: "site text 변경" suspend_user: "suspend user" unsuspend_user: "unsuspend user" diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index f9c72ce44a..78e0df50cf 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -143,6 +143,8 @@ nb_NO: emails_are_disabled: "All utgående e-post har blitt deaktivert globalt av en administrator. Ingen e-postvarslinger vil bli sendt." bootstrap_mode_enabled: "For å gjøre det enklere å lansere det nye nettstedet ditt er det i bootstrap-modus. Alle nye brukere vil få tillitsnivå 1 og få daglige oppdateringer på e-post aktivert. Dette vil automatisk bli slått av når antallet brukere overstiger %{min_users}." bootstrap_mode_disabled: "Bootstrap modus vil bli deaktivert i løpet av de neste 24 timene." + themes: + default_description: "Forvalg" s3: regions: us_east_1: "USA øst (N. Virginia)" @@ -150,6 +152,7 @@ nb_NO: us_west_2: "USA vest (Oregon)" us_gov_west_1: "AWS GovCloud (USA)" eu_west_1: "EU (Irland)" + eu_west_2: "EU (London)" eu_central_1: "EU (Frankfurt)" ap_southeast_1: "Asia/Stillehavsregionen (Singapore)" ap_southeast_2: "Asia/Stillehavsregionen (Sydney)" @@ -570,6 +573,7 @@ nb_NO: revoke_access: "Trekk tilbake tilgang" undo_revoke_access: "Angre \"trekk tilbake tilgang\"" api_approved: "Godkjent:" + theme: "Drakt" staff_counters: flags_given: "nyttige rapporteringer" flagged_posts: "rapporterte innlegg" @@ -2545,32 +2549,14 @@ nb_NO: customize: title: "Tilpasse" long_title: "Nettstedstilpasninger" - css: "CSS" - header: "Header" - top: "Topp" - footer: "Footer" - embedded_css: "Innebygd CSS" - head_tag: - text: "" - title: "HTML som settes inn før taggen." - body_tag: - text: "" - title: "HTML som settes inn før taggen." - override_default: "Ikke inkluder forvalgt stilark" - enabled: "Aktivert?" preview: "forhåndsvisning" - undo_preview: "avbryt forhåndsvisning" - rescue_preview: "forvalgt stil" - explain_preview: "Se nettstedet med dette skreddersydde stilarket" - explain_undo_preview: "Gå tilbake til nåværende aktivert tilpasset stilark" - explain_rescue_preview: "Se nettstedet med forvalgt stilark" + explain_preview: "Se siden iført denne drakten" save: "Lagre" new: "Ny" new_style: "Ny stil" import: "Importer" - import_title: "Velg en fil eller lim inn tekst" delete: "Slett" - delete_confirm: "Slett denne tilpasningen?" + delete_confirm: "Slett denne drakten?" about: "Endre CSS og HTML-headere på nettstedet. Legg til en tilpasning for å starte." color: "Farge" opacity: "Opacity" @@ -2583,13 +2569,68 @@ nb_NO: none_selected: "Velg en e-postmal for å begynne å redigere." revert: "Tilbakestill endringer" revert_confirm: "Er du sikker på at du vil tilbakestille dine endringer?" - css_html: - title: "CSS/HTML" - long_title: "CSS og HTML-tilpasninger" + theme: + import_theme: "Importer drakt" + customize_desc: "Skreddersy:" + title: "Drakter" + long_title: "Gå over farger, CSS- og HTML-innhold på siden din" + edit: "Rediger" + edit_confirm: "Dette er en drakt annensteds hen, hvis du endrer CSS/HTML vil dine endringer gå tapt neste gang du oppdaterer drakten." + common: "Vanlig" + desktop: "Skrivebord" + mobile: "Mobil" + preview: "Forhåndsvis" + is_default: "Drakt påskrudd som forvalg" + user_selectable: "Drakten kan velges av brukerne" + color_scheme: "Fargepalett" + color_scheme_select: "Velg farger å bruke i drakten" + custom_sections: "Egendefinerte valg:" + included_themes: "Inkluderte drakter" + child_themes_check: "Drakten inneholder andre underdrakter" + css_html: "Egendefinert CSS/HTML" + edit_css_html: "Rediger CSS/HTML" + edit_css_html_help: "Du har ikke redigert noe CSS eller HTML" + import_web_tip: "Pakkebrønn inneholdende drakt" + import_file_tip: ".dcstyle.json-fil inneholdende drakt" + about_theme: "Om drakt" + license: "Lisens" + update_to_latest: "Oppdater til seneste" + check_for_updates: "Se etter oppdateringer" + updating: "Oppdaterer…" + up_to_date: "Drakten er oppdatert, sist sjekket:" + add: "Legg til" + commits_behind: + one: "Drakten én utgave foreldet!" + other: "Drakten er {{count}} utgaver foreldet!" + scss: + text: "CSS" + title: "Skriv inn egendefinert CSS, alle gyldige CSS- og SCSS-stiler godtas" + header: + text: "Hode" + title: "Skriv inn HTML å vise over sidehodet" + after_header: + text: "Etter hode" + title: "Skriv inn HTML å vise på alle sider etter hode" + footer: + text: "Fot" + title: "Skriv inn HTML å vise på sidens fot" + embedded_scss: + text: "Innebygd CSS" + title: "Skriv inn egendefinert CSS for å levere med innebygde versjoner av kommentarer" + head_tag: + text: "" + title: "HTML som har blitt smettet inn før -taggen" + body_tag: + text: "" + title: "HTML som vil bli smettet inn før -taggen" colors: + select_base: + title: "Velg hovedpalett" + description: "Hovedpalett:" title: "Farger" + edit: "Rediger fargepaletter" long_title: "Fargepanel" - about: "Endre farger som brukes på nettstedet uten å skrive CSS. Legg til et skjema for å starte." + about: "Endre fargene brukt i draktene dine. Opprett en ny fargepalett for å komme i gang." new_name: "Ny fargepalett" copy_name_prefix: "Kopi av" delete_confirm: "Slett denne fargepaletten?" @@ -2728,8 +2769,8 @@ nb_NO: change_trust_level: "endre tillitsnivå" change_username: "endre brukernavn" change_site_setting: "endre nettstedsinnstilling" - change_site_customization: "endre tilpasninger for nettstedet" - delete_site_customization: "slett tilpasninger for nettstedet" + change_theme: "bytt drakt" + delete_theme: "slett drakt" change_site_text: "endre sidens tekst" suspend_user: "bannlys bruker" unsuspend_user: "gjeninnsett bruker" diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index 24d6bfcb4a..e7bfc960ff 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -143,6 +143,8 @@ nl: emails_are_disabled: "Alle uitgaande e-mail is uitgeschakeld door een beheerder. Er zal geen enkele e-mailmelding worden verstuurd." bootstrap_mode_enabled: "Om het lanceren van uw website makkelijker te maken, bevindt u zich in bootstrapmodus. Aan alle nieuwe gebruikers wordt vertrouwensniveau 1 toegekend, en dagelijkse e-mailsamenvattingen zijn voor hen ingeschakeld. Dit wordt automatisch uitgeschakeld wanneer het totale gebruikersaantal %{min_users} gebruikers overschrijdt." bootstrap_mode_disabled: "De bootstrapmodus zal in de komende 24 uur worden uitgeschakeld." + themes: + default_description: "Standaard" s3: regions: us_east_1: "VS Oost (N. Virginia)" @@ -150,6 +152,7 @@ nl: us_west_2: "VS West (Oregon)" us_gov_west_1: "AWS GovCloud (VS)" eu_west_1: "EU (Ierland)" + eu_west_2: "EU (Londen)" eu_central_1: "EU (Frankfurt)" ap_southeast_1: "Azië Pacifisch (Singapore)" ap_southeast_2: "Azië Pacifisch (Sydney)" @@ -570,6 +573,7 @@ nl: revoke_access: "Toegang intrekken" undo_revoke_access: "Toegang intrekken ongedaan maken" api_approved: "Goedgekeurd:" + theme: "Thema" staff_counters: flags_given: "behulpzame markeringen" flagged_posts: "gemarkeerde berichten" @@ -1201,7 +1205,7 @@ nl: closed: gesloten zijn archived: gearchiveerd zijn noreplies: geen antwoorden bevatten - single_user: een enkele gebruiker bevatten + single_user: maar één gebruiker bevatten post: count: label: Minimaal berichtaantal @@ -2545,32 +2549,14 @@ nl: customize: title: "Aanpassen" long_title: "Websiteaanpassingen" - css: "CSS" - header: "Koptekst" - top: "Top" - footer: "Voettekst" - embedded_css: "Embedded CSS" - head_tag: - text: "" - title: "HTML die voor de -tag wordt ingevoegd" - body_tag: - text: "" - title: "HTML die voor de -tag wordt ingevoegd" - override_default: "Standaardstylesheet uitsluiten" - enabled: "Ingeschakeld?" preview: "voorbeeld" - undo_preview: "voorbeeld verwijderen" - rescue_preview: "standaardstijl" - explain_preview: "De website bekijken met deze aangepaste stylesheet" - explain_undo_preview: "Terug naar de momenteel ingeschakelde aangepaste stylesheet" - explain_rescue_preview: "De website bekijken met de standaardstylesheet" + explain_preview: "De website bekijken met dit thema ingeschakeld" save: "Opslaan" new: "Nieuw" new_style: "Nieuwe stijl" import: "Importeren" - import_title: "Selecteer een bestand of plak tekst" delete: "Verwijderen" - delete_confirm: "Deze aanpassing verwijderen?" + delete_confirm: "Dit thema verwijderen?" about: "Pas CSS-stylesheets en HTML-kopteksten aan op de website. Voeg een aanpassing toe om te beginnen." color: "Kleur" opacity: "Ondoorzichtigheid" @@ -2583,13 +2569,68 @@ nl: none_selected: "Selecteer een e-mailsjabloon om met bewerken te beginnen." revert: "Wijzigingen ongedaan maken" revert_confirm: "Weet u zeker dat u uw wijzigingen ongedaan wilt maken?" - css_html: - title: "CSS/HTML" - long_title: "CSS- en HTML-aanpassingen" + theme: + import_theme: "Thema importeren" + customize_desc: "Aanpassen:" + title: "Thema's" + long_title: "Kleuren, CSS en HTML-inhoud van uw website aanpassen" + edit: "Bewerken" + edit_confirm: "Dit is een extern thema; als u CSS/HTML bewerkt, worden uw wijzigingen de volgende keer dat u het thema bijwerkt gewist." + common: "Algemeen" + desktop: "Desktop" + mobile: "Mobiel" + preview: "Voorbeeld" + is_default: "Thema is standaard ingeschakeld" + user_selectable: "Thema kan door gebruikers worden geselecteerd" + color_scheme: "Kleurenschema" + color_scheme_select: "Kleuren die door thema worden gebruikt selecteren" + custom_sections: "Aangepaste secties:" + included_themes: "Inbegrepen thema's" + child_themes_check: "Thema bevat andere onderliggende thema's" + css_html: "Aangepaste CSS/HTML" + edit_css_html: "CSS/HTML bewerken" + edit_css_html_help: "U hebt geen CSS of HTML bewerkt" + import_web_tip: "Repository die thema bevat" + import_file_tip: ".dcstyle.json-bestand dat thema bevat" + about_theme: "Over thema" + license: "Licentie" + update_to_latest: "Bijwerken naar nieuwste" + check_for_updates: "Controleren op updates" + updating: "Bijwerken..." + up_to_date: "Thema is up-to-date, laatst gecontroleerd:" + add: "Toevoegen" + commits_behind: + one: "Thema loopt 1 doorvoering achter!" + other: "Thema loopt {{count}} doorvoeringen achter!" + scss: + text: "CSS" + title: "Voer aangepaste CSS in; we accepteren alle geldige CSS- en SCSS-stijlen" + header: + text: "Koptekst" + title: "Voer HTML in voor weergave boven websitekoptekst" + after_header: + text: "Na koptekst" + title: "Voer HTML in voor weergave op alle pagina's na koptekst" + footer: + text: "Voettekst" + title: "Voer HTML in voor weergave op paginavoettekst" + embedded_scss: + text: "Embedded CSS" + title: "Voer aangepaste CSS in om met embedded versie van opmerkingen te leveren" + head_tag: + text: "" + title: "HTML die voor de -tag wordt ingevoegd" + body_tag: + text: "" + title: "HTML die voor de -tag wordt ingevoegd" colors: + select_base: + title: "Basiskleurenschema selecteren" + description: "Basisschema:" title: "Kleuren" + edit: "Kleurenschema's bewerken" long_title: "Kleurenschema's" - about: "Met kleurenschema's kunt u de kleuren op de website aanpassen zonder CSS te hoeven gebruiken. Voeg een schema toe om te beginnen." + about: "De door uw thema's gebruikte kleuren aanpassen. Maak een nieuw kleurenschema aan om te beginnen." new_name: "Nieuw kleurenschema" copy_name_prefix: "Kopie van" delete_confirm: "Dit kleurenschema verwijderen?" @@ -2728,8 +2769,8 @@ nl: change_trust_level: "vertrouwensniveau wijzigen" change_username: "gebruikersnaam wijzigen" change_site_setting: "website-instelling wijzigen" - change_site_customization: "website-aanpassing wijzigen" - delete_site_customization: "website-aanpassing verwijderen" + change_theme: "thema wijzigen" + delete_theme: "thema verwijderen" change_site_text: "tekst van website wijzigen" suspend_user: "gebruiker schorsen" unsuspend_user: "schorsing opheffen" diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index a3fa41e5dc..73fcf4c008 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -2706,32 +2706,12 @@ pl_PL: customize: title: "Wygląd" long_title: "Personalizacja strony" - css: "CSS" - header: "Nagłówki" - top: "Nagłówek" - footer: "Stopka" - embedded_css: "Osadzony CSS" - head_tag: - text: "" - title: "Kod HTML, który zostanie umieszczony przed tagiem " - body_tag: - text: "" - title: "Kod HTML, który zostanie umieszczony przed tagiem ." - override_default: "Nie dołączaj standardowego arkusza stylów" - enabled: "Włączone?" preview: "podgląd" - undo_preview: "usuń podgląd" - rescue_preview: " domyślny styl" - explain_preview: "Podejrzyj witrynę z użyciem tego sylesheet'u" - explain_undo_preview: "Wróć do aktualnie aktywnego schematu styli" - explain_rescue_preview: "Zobacz stronę z domyślnym stylem" save: "Zapisz" new: "Nowy" new_style: "Nowy styl" import: "Import" - import_title: "Wybierz plik lub wklej tekst" delete: "Usuń" - delete_confirm: "Usunąć tę personalizację?" about: "Zmień arkusze stylów CSS i nagłówki HTML w witrynie. Dodaj własne ustawienie aby rozpocząć." color: "Kolor" opacity: "Widoczność" @@ -2744,13 +2724,9 @@ pl_PL: none_selected: "Aby rozpocząć edycję, wybierz szablon wiadomości e-mail. " revert: "Cofnij zmiany" revert_confirm: "Czy na pewno chcesz wycofać swoje zmiany?" - css_html: - title: "CSS, HTML" - long_title: "Personalizacja kodu CSS i HTML" colors: title: "Kolory" long_title: "Schematy kolorów" - about: "Zmień kolory strony bez modyfikacji CSS. Dodaj nowy schemat kolorów, aby rozpocząć." new_name: "Nowy schemat kolorów" copy_name_prefix: "Kopia" delete_confirm: "Usunąć ten schemat kolorów?" @@ -2889,8 +2865,6 @@ pl_PL: change_trust_level: "zmiana poziomu zaufania" change_username: "zmień nazwę użytkownika" change_site_setting: "zmiana ustawień serwisu" - change_site_customization: "modyfikacja personalizacji serwisu" - delete_site_customization: "usunięcie personalizacji strony" change_site_text: "zmiana tekstu serwisu" suspend_user: "zawieszenie użytkownika" unsuspend_user: "odwieszenie użytkownika" diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml index 094daaae26..831bf83210 100644 --- a/config/locales/client.pt.yml +++ b/config/locales/client.pt.yml @@ -2424,32 +2424,12 @@ pt: customize: title: "Personalizar" long_title: "Personalizações do Sítio" - css: "CSS" - header: "Cabeçalho" - top: "Os Melhores" - footer: "Rodapé" - embedded_css: "CSS incorporado" - head_tag: - text: "" - title: "HTML que será introduzido antes da tag " - body_tag: - text: "" - title: "HTML que será introduzido antes da tag " - override_default: "Não incluir a folha de estilo por defeito" - enabled: "Ativado?" preview: "pré-visualização" - undo_preview: "remover pré-visualização" - rescue_preview: "estilo por defeito" - explain_preview: "Ver o sítio com esta folha de estilo personalizada" - explain_undo_preview: "Voltar atrás para a atual folha de estilo personalizada ativa" - explain_rescue_preview: "Ver o sítio com a folha de estilo por defeito" save: "Guardar" new: "Novo" new_style: "Novo Estilo" import: "Importar" - import_title: "Selecione um ficheiro ou cole texto" delete: "Eliminar" - delete_confirm: "Remover esta personalização?" about: "Modificar folha de estilo CSS e cabeçalhos HTML no sítio. Adicionar personalização para iniciar." color: "Cor" opacity: "Opacidade" @@ -2462,13 +2442,9 @@ pt: none_selected: "Selecione um modelo de email para começar a editar." revert: "Reverter Alterações" revert_confirm: "Tem a certeza que quer reverter as suas alterações?" - css_html: - title: "CSS/HTML" - long_title: "Personalizações CSS e HTML" colors: title: "Cores" long_title: "Esquemas de Cores" - about: "Modificar as cores usadas no sítio sem escrever CSS. Adicionar um esquema para iniciar." new_name: "Novo Esquema de Cores" copy_name_prefix: "Cópia de" delete_confirm: "Apagar este esquema de cor?" @@ -2607,8 +2583,6 @@ pt: change_trust_level: "modificar Nível de Confiança" change_username: "alterar nome de utilizador" change_site_setting: "alterar configurações do sítio" - change_site_customization: "alterar personalização do sítio" - delete_site_customization: "remover personalização do sítio" change_site_text: "alterar texto do sítio" suspend_user: "utilizador suspenso" unsuspend_user: "utilizador não suspenso" diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index f30de6dbdb..c81f603a86 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -2508,32 +2508,12 @@ pt_BR: customize: title: "Personalizar" long_title: "Personalizações do Site" - css: "CSS" - header: "Cabeçalho" - top: "Superior" - footer: "Rodapé" - embedded_css: "CSS Incorporada" - head_tag: - text: "" - title: "HTML que será inserido antes da tag " - body_tag: - text: "" - title: "HTML que será inserido antes da tag " - override_default: "Sobrepor padrão?" - enabled: "Habilitado?" preview: "pré-visualização" - undo_preview: "remover preview" - rescue_preview: "estilo padrão" - explain_preview: "Ver o site com o estilo personalizado" - explain_undo_preview: "Voltar para o estilo personalizado atual" - explain_rescue_preview: "Ver o site com o estilo padrão" save: "Guardar" new: "Novo" new_style: "Novo Estilo" import: "Importar" - import_title: "Selecione um arquivo ou cole texto" delete: "Apagar" - delete_confirm: "Apagar esta personalização?" about: "Modificar o CSS e HTML do cabeçalho do site. Adicione uma customização para começar." color: "Cor" opacity: "Opacidade" @@ -2546,13 +2526,9 @@ pt_BR: none_selected: "Selecione um modelo de e-mail para iniciar a edição." revert: "Reverter Alterações" revert_confirm: "Tem certeza de que deseja reverter as alterações?" - css_html: - title: "CSS/HTML" - long_title: "Customizações CSS e HTML" colors: title: "Cores" long_title: "Esquema de Cores" - about: "Modifique as cores usadas no site sem escrever CSS. Adicione um esquema para começar." new_name: "Novo Esquema de Cor" copy_name_prefix: "Copiar de" delete_confirm: "Apagar esse esquema de cor?" @@ -2691,8 +2667,6 @@ pt_BR: change_trust_level: "modificou nível de confiança" change_username: "mudar nome de usuário" change_site_setting: "alterar configurações do site" - change_site_customization: "alterar personalização do site" - delete_site_customization: "remover personalização do site" change_site_text: "alterar texto do site" suspend_user: "suspender usuário" unsuspend_user: "readmitir usuário" diff --git a/config/locales/client.ro.yml b/config/locales/client.ro.yml index 9bca1981cd..f461ccfbd8 100644 --- a/config/locales/client.ro.yml +++ b/config/locales/client.ro.yml @@ -157,6 +157,8 @@ ro: emails_are_disabled: "Trimiterea de emailuri a fost dezactivată global de către un administrator. Nu vor fi trimise notificări email de nici un fel." bootstrap_mode_enabled: "Pentru a ușura lansarea site-ului tău ești în modul bootstrap. Toți noii utilizatori vor primi nivelul de încredere 1 și vor avea activată primirea zilnică a unui email-rezumat. Această setare va fi dezactivată automat de îndată ce numărul total de utilizatori depășește %{min_users}." bootstrap_mode_disabled: "Modul bootstrap va fi dezactivat în următoarele 24 de ore." + themes: + default_description: "Implicit" s3: regions: us_east_1: "US East (N. Virginia)" @@ -233,6 +235,7 @@ ro: like_count: "Aprecieri" topic_count: "Subiecte" post_count: "Postări" + user_count: "Utilizatori" active_user_count: "Utilizatori activi" contact: "Contactează-ne" contact_info: "În cazul în care o problemă critică sau alt aspect urgent afectează site-ul, contactează-ne la %{contact_info}." @@ -401,6 +404,7 @@ ro: one: "Grup" few: "Grupuri" other: "de grupuri" + activity: "Activitate" members: "Membri" topics: "Subiecte" posts: "Postări" @@ -614,6 +618,8 @@ ro: error: "(eroare)" action: "Trimite email pentru resetare parolă" set_password: "Introdu parolă" + choose_new: "Alege o parolă nouă" + choose: "Alege o parolă" change_about: title: "Schimbare date personale" error: "A apărut o eroare la schimbarea acestei valori." @@ -658,6 +664,7 @@ ro: other: "Vom trimite un email doar dacă nu te-am văzut în ultimele {{count}} de minute." name: title: "Nume" + instructions: "Numele tău complet (opțional)" instructions_required: "Numele tău complet" too_short: "Numele este prea scurt." ok: "Numele tău este OK." @@ -666,6 +673,7 @@ ro: short_instructions: "Ceilalți te pot menționa ca @{{username}}." available: "Numele de utilizator este disponibil." not_available: "Nu este disponibil. Încerci {{suggestion}}?" + not_available_no_suggestion: "Indisponibil" too_short: "Numele de utilizator este prea scurt." too_long: "Numele de utilizator este prea lung." checking: "Verifică disponibilitatea numelui de utilizator..." @@ -776,6 +784,7 @@ ro: same_as_username: "Parolă identică cu numele de utilizator" same_as_email: "Parolă identică cu adresa de email" ok: "Parola dumneavoastră este OK." + instructions: "minimum %{count} caractere" summary: title: "Sumar" stats: "Statistici" @@ -961,6 +970,8 @@ ro: not_allowed_from_ip_address: "Nu te poți conecta cu această adresă IP." admin_not_allowed_from_ip_address: "Nu te poți conecta ca administrator cu această adresă IP." resend_activation_email: "Click aici pentru a retrimite emailul de activare." + change_email: "Schimbă adresa de email" + submit_new_email: "Actualizează adresa de email" sent_activation_email_again: "Am trimis un alt email de activare pentru tine la {{currentEmail}}. Poate dura câteva minute până ajunge; vezi și în secțiunea de spam a mailului." to_continue: "Te rog să te autentifici." preferences: "Trebuie să fii autentificat pentru a schimba preferințele." @@ -1341,6 +1352,9 @@ ro: jump_reply_up: sări la un răspuns mai vechi jump_reply_down: sări la un răspuns mai nou deleted: "Subiectul a fost șters" + status_update_notice: + auto_close: "Acest subiect va fi automat închis în %{timeLeft}." + auto_close_based_on_last_post: "Acest subiect va fi automat închis după %{duration} de la ultimul răspuns." auto_close_title: 'Setări de închidere automată' auto_close_immediate: one: "Ultima postare din subiect este deja veche de o oră, așa că subiectul va fi închis imediat." @@ -2535,32 +2549,12 @@ ro: customize: title: "Personalizare" long_title: "Personalizările site-ului" - css: "CSS" - header: "Antet" - top: "Sus" - footer: "Subsol" - embedded_css: "Embedded CSS" - head_tag: - text: "" - title: "HTML care va fi inserat înaintea de tag-ul " - body_tag: - text: "" - title: "HTML care va fi inserat înaintea de tag-ul " - override_default: "Nu include foaia de stil standard" - enabled: "Activat?" preview: "previzualizare" - undo_preview: "șterge previzualizarea" - rescue_preview: "stil implicit" - explain_preview: "Vizualizează site-ul cu această foaie de stil personalizată" - explain_undo_preview: "Înapoi la foaia de stil personalizată activă acum" - explain_rescue_preview: "Vizualizează site-ul cu foaia de stil implicită" save: "Salvare" new: "Nou" new_style: "Stil nou" import: "Importă" - import_title: "Selectează un fișier sau lipește un text" delete: "Șterge" - delete_confirm: "Șterge această personalizare?" about: "Modifică foaia de stil CSS și header-ele HTML din site. Adaugă o personalizare pentru a începe." color: "Culoare" opacity: "Opacitate" @@ -2573,13 +2567,9 @@ ro: none_selected: "Selectează un șablon pentru a începe editarea" revert: "Revocă schimbările" revert_confirm: "Ești sigur că vrei să revoci schimbările?" - css_html: - title: "CSS/HTML" - long_title: "Personalizări CSS și HTML" colors: title: "Culori" long_title: "Scheme de culori" - about: "Modifică culorile folosite pe site fără a scrie CSS. Pentru a începe, adăugă o nouă schemă." new_name: "Nouă schemă de culori" copy_name_prefix: "Copie a" delete_confirm: "Ștergi această schemă de culori?" @@ -2718,8 +2708,6 @@ ro: change_trust_level: "schimbă nivelul de încredere" change_username: "schimbă numele utilizatorului" change_site_setting: "schimbă setările site-ului" - change_site_customization: "schimbă preferințele site-ului" - delete_site_customization: "șterge preferințele site-ului" change_site_text: "schimbă textul site-ului" suspend_user: "suspendă utilizatorul" unsuspend_user: "reactivează utilizator" diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index 3a223c638c..52b5566d12 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -176,6 +176,8 @@ ru: emails_are_disabled: "Все исходящие письма были глобально отключены администратором. Уведомления любого вида не будут отправляться на почту." bootstrap_mode_enabled: "Чтобы облегчить развитие вашего нового сайта в самом начале, был включен режим запуска. В этом режиме, всем новым пользователям будет автоматически присвоен 1й уровень доверия при регистрации и включена ежедневная почтовая рассылка сводки новостей. Режим запуска будет выключен автоматически, как только количество зарегистрированных пользователей достигнет %{min_users}." bootstrap_mode_disabled: "Режим запуска будет отключен в течение 24 часов." + themes: + default_description: "По умолчанию" s3: regions: us_east_1: "US East (N. Virginia)" @@ -183,6 +185,7 @@ ru: us_west_2: "US West (Oregon)" us_gov_west_1: "AWS GovCloud (US)" eu_west_1: "EU (Ireland)" + eu_west_2: "EU (London)" eu_central_1: "EU (Frankfurt)" ap_southeast_1: "Asia Pacific (Singapore)" ap_southeast_2: "Asia Pacific (Sydney)" @@ -622,6 +625,7 @@ ru: revoke_access: "Лишить прав доступа" undo_revoke_access: "Отменить Лишение прав доступа" api_approved: "Подтверждено" + theme: "Стиль" staff_counters: flags_given: "полезные жалобы" flagged_posts: "сообщения с жалобами" @@ -997,22 +1001,27 @@ ru: password: "Пароль" email_placeholder: "E-mail или псевдоним" caps_lock_warning: "Caps Lock включен" - error: "Непредвиденная ошибка" - rate_limit: "Пожалуйста, сделайте перерыв перед очередной попыткой войти." + error: "Неизвестная ошибка" + rate_limit: "Сделайте перерыв перед очередной попыткой войти." blank_username_or_password: "Введите ваш e-mail (или псевдоним) и пароль." reset_password: 'Сброс пароля' logging_in: "Проверка..." or: "или" authenticating: "Проверка..." - awaiting_approval: "Ваша учетная запись еще не одобрена. Вы получите письмо, как только это случится." + awaiting_activation: "Ваша учетная запись ожидает активации через ссылку из письма. Чтобы повторно выслать активационное письмо, используйте кнопку сброса пароля." + awaiting_approval: "Ваша учетная запись еще не одобрена персоналом. Мы вышлем вам письмо, как только это произойдет." requires_invite: "К сожалению, доступ к этому форуму только по приглашениям." - not_activated: "Прежде, чем вы сможете войти на форум, вам необходимо активировать свою учетную запись. Мы отправили на почту {{sentTo}} подробные инструкции, как это cделать." + not_activated: "Прежде, чем вы сможете войти на форум, понадобиться активировать свою учетную запись. Мы отправили на почту {{sentTo}} подробные инструкции, как это cделать." not_allowed_from_ip_address: "С этого IP адреса вход запрещен." admin_not_allowed_from_ip_address: "Вы не можете войти в качестве администратора с этого IP адреса." - resend_activation_email: "Щелкните здесь, чтобы мы повторно выслали вам письмо для активации учетной записи." + resend_activation_email: "Щелкните здесь, чтобы повторно выслать письмо для активации учетной записи." + resend_title: "Заново выслать активационное письмо" + change_email: "Изменить электронную почту" + provide_new_email: "Укажите новый адрес электронной почты, чтобы задействовать его и заново выслать активационное письмо." + submit_new_email: "Обновить электронную почту" sent_activation_email_again: "По адресу {{currentEmail}} повторно отправлено письмо с инструкциями по активации вашей учетной записи. Доставка сообщения может занять несколько минут. Имейте в виду, что иногда по ошибке письмо может попасть в папку Спам." to_continue: "Пожалуйста, войдите" - preferences: "Вам необходимо войти на сайт для редактирования настроек пользователя" + preferences: "Необходимо войти на сайт для редактирования настроек профиля." forgot: "Я не помню данные моей учетной записи" not_approved: "Ваша учетная запись еще не прошла проверку. После успешной проверки мы отправим вам письмо с уведомлением, и вы сможете входить в свою учетную запись." google: @@ -2706,32 +2715,14 @@ ru: customize: title: "Оформление" long_title: "Стили и заголовки" - css: "CSS" - header: "Заголовок" - top: "Топ" - footer: "нижний колонтитул" - embedded_css: "Встроенные CSS" - head_tag: - text: "" - title: "HTML код, который будет добавлен перед тегом ." - body_tag: - text: "" - title: "HTML код, который будет добавлен перед тегом ." - override_default: "Не использовать стандартную таблицу стилей" - enabled: "Разрешить?" preview: "как будет" - undo_preview: "удалить предпросмотр" - rescue_preview: "стиль по умолчанию" - explain_preview: "Посмотреть сайт с этой таблицей стилей" - explain_undo_preview: "Вернуться к текущей таблице стилей" - explain_rescue_preview: "Посмотреть сайт со стандартной таблицей стилей" + explain_preview: "Предпросмотр сайта с активированным стилем" save: "Сохранить" new: "Новое" new_style: "Новый стиль" import: "Импорт" - import_title: "Выберите файл или вставьте текст" delete: "Удалить" - delete_confirm: "Удалить настройки?" + delete_confirm: "Удалить этот стиль?" about: "Измените CSS стили и HTML заголовки на сайте. Чтобы начать, внесите правки." color: "Цвет" opacity: "Прозрачность" @@ -2744,13 +2735,70 @@ ru: none_selected: "Выберите шаблон письма, чтобы начать редактирование." revert: "Отменить изменения" revert_confirm: "Вы уверены, что хотите отменить Ваши изменения?" - css_html: - title: "CSS/HTML" - long_title: "Настройка CSS и HTML" + theme: + import_theme: "Импортировать стиль" + customize_desc: "Настроить:" + title: "Стили" + long_title: "Стилизация сайта: цвета, CSS и HTML" + edit: "Редактировать" + edit_confirm: "Это импортированный стиль. Изменения CSS/HTML потеряются после очередного обновления стиля." + common: "Общее" + desktop: "Настольный" + mobile: "Мобильный" + preview: "Предпросмотр" + is_default: "Активировать по умолчанию" + user_selectable: "Пользователи могут выбирать стиль" + color_scheme: "Цветовая схема" + color_scheme_select: "Выберите цвета для стиля" + custom_sections: "Настройка секций:" + included_themes: "Включенные стили" + child_themes_check: "Стиль включает дочерние стили" + css_html: "Настройка CSS/HTML" + edit_css_html: "Редактировать CSS/HTML" + edit_css_html_help: "Вы не внесли никаких изменений в CSS или HTML" + import_web_tip: "Репозиторий стиля" + import_file_tip: "файл стиля .dcstyle.json" + about_theme: "О стиле" + license: "Лицензия" + update_to_latest: "Обновить" + check_for_updates: "Проверить обновления" + updating: "Обновляю..." + up_to_date: "Стиль текущей версии, последняя проверка:" + add: "Добавить" + commits_behind: + one: "Есть 1 обновление стиля!" + few: "Есть {{count}} обновления стиля!" + many: "Есть {{count}} обновлений стиля!" + other: "Есть {{count}} обновлений стиля!" + scss: + text: "CSS" + title: "Введите CSS; допускаются все стили CSS и SCSS" + header: + text: "Шапка" + title: "Введите HTML для размещения над шапкой сайта" + after_header: + text: "Под шапкой" + title: "Введите HTML для размещения на всех страницах под шапкой" + footer: + text: "Подвал" + title: "Введите HTML для отображения в подвале сайта (внизу)" + embedded_scss: + text: "Встроенный CSS" + title: "Введите CSS для встраивания комментариев" + head_tag: + text: "" + title: "HTML для размещения перед тегом " + body_tag: + text: "" + title: "HTML для размещения перед тегом " colors: + select_base: + title: "Выберите базовую цветовую гамму" + description: "Базовая схема" title: "Цвета" + edit: "Редактировать цветовые схемы" long_title: "Цветовые схемы" - about: "Изменить цвета, используемые на этом сайте, без редактирования CSS. Добавьте новую схему для начала." + about: "Настройте цвета, используемые вашими стилями. Начните с создания новой цветовой схемы." new_name: "Новая цветовая схема" copy_name_prefix: "Копия" delete_confirm: "Удалить эту цветовую схему?" @@ -2889,8 +2937,8 @@ ru: change_trust_level: "изменен уровень доверия" change_username: "изменен псевдоним" change_site_setting: "изменена настройка сайта" - change_site_customization: "изменена настройка сайта" - delete_site_customization: "удалена настройка сайта" + change_theme: "Изменить стиль" + delete_theme: "Удалить стиль" change_site_text: "изменен текст" suspend_user: "пользователь заморожен" unsuspend_user: "пользователь разморожен" @@ -3043,6 +3091,7 @@ ru: logged_out: "Пользователь вышел с сайта на всех устройствах" revoke_admin: 'Лишить прав Администратора' grant_admin: 'Выдать права Администратора' + grant_admin_confirm: "Мы отправили вам письмо с инструкциями для активации нового администратора." revoke_moderation: 'Лишить прав Модератора' grant_moderation: 'Выдать права Модератора' unblock: 'Разблокировать' diff --git a/config/locales/client.sk.yml b/config/locales/client.sk.yml index 083a892372..6588a2203f 100644 --- a/config/locales/client.sk.yml +++ b/config/locales/client.sk.yml @@ -2318,32 +2318,12 @@ sk: customize: title: "Upraviť" long_title: "Úpravy webu" - css: "CSS" - header: "Hlavička" - top: "Vrch" - footer: "Päta" - embedded_css: "Vnorené CSS" - head_tag: - text: "" - title: "HTML, ktoré bude vložené pred tag" - body_tag: - text: "" - title: "HTML, ktoré bude vložené pred tag" - override_default: "Nevkladať štandardné štýly" - enabled: "Povolené?" preview: "náhľad" - undo_preview: "zmazať náhľad" - rescue_preview: "predvolený štýl" - explain_preview: "Nastaviť na stránke vlastné štýly" - explain_undo_preview: "Vrátiť sa k akruálnym vlastným štýlom" - explain_rescue_preview: "Nastavit na stránke štandardné štýly" save: "Uložiť" new: "Nový" new_style: "Nový štýl" import: "Import" - import_title: "Vyberte súbor alebo vložte text" delete: "Odstrániť" - delete_confirm: "Zmazať túto úpravu?" about: "Upraviť CSS štýly a HTML hlavičky na stránke. Začnite pridaním úpravy." color: "Farba" opacity: "Nepriesvitnosť" @@ -2356,13 +2336,9 @@ sk: none_selected: "Vyberte šablénu emailu pre začatie úpravy." revert: "Vrátiť zmeny" revert_confirm: "Ste si istý, že chcete vrátiť vykonané zmeny späť?" - css_html: - title: "CSS/HTML" - long_title: "Úpravy CSS a HTML" colors: title: "Farby" long_title: "Farebné schémy" - about: "Upravte farby použité na stránke bez použitia CSS. Začnite pridaním schémy." new_name: "Nová farebná schéma" copy_name_prefix: "Kópia" delete_confirm: "Zmazať túto farebnú schému?" @@ -2493,8 +2469,6 @@ sk: change_trust_level: "zmeniť stupeň dôvery" change_username: "zmeniť používateľské meno" change_site_setting: "zmeniť nastavenia webu" - change_site_customization: "zmeniť úpravy webu" - delete_site_customization: "zmazať úpravy webu" change_site_text: "zmeniť text stránky" suspend_user: "zruš práva používateľovi" unsuspend_user: "obnov práva používateľovi" diff --git a/config/locales/client.sq.yml b/config/locales/client.sq.yml index 31977d8ca0..978c03dd71 100644 --- a/config/locales/client.sq.yml +++ b/config/locales/client.sq.yml @@ -2167,31 +2167,12 @@ sq: customize: title: "Personalizo" long_title: "Personalizimet" - css: "CSS" - header: "Ballina" - top: "Popullore" - footer: "Fundi i faqes" - embedded_css: "CSS e ngjitur" - head_tag: - text: "" - title: "HTML që do të vendoset para mbylles së tagut " - body_tag: - text: "" - title: "HTML që do të vendoset para mbylljes së tagut " - override_default: "Mos përfshi faqen e stilit CSS standarte." - enabled: "Aktivizuar?" preview: "parashiko" - undo_preview: "hiqe parashikimin" - rescue_preview: "stili normal" - explain_preview: "Shife faqen me këtë faqe stili" - explain_undo_preview: "Rithehu tek faqja e stilit që është e aktivizuar tani" - explain_rescue_preview: "Shikoje faqen me stilin normal." save: "Ruaj" new: "E Re" new_style: "Veshje e Re" import: "Importo" delete: "Fshij" - delete_confirm: "Fshije këtë personalizim?" about: "Modifiko faqet e stilit CSS dhe HTML në këtë faqe. Shto një personalizim për të filluar. " color: "Ngjyra" opacity: "Opaciteti" @@ -2204,9 +2185,6 @@ sq: none_selected: "Zgjidhni një shabllon emaili për të filluar redaktimin. " revert: "Rikthe ndryshimet" revert_confirm: "A jeni i sigurtë që doni të riktheni ndryshimet?" - css_html: - title: "CSS/HTML" - long_title: "Personalizime CSS dhe HTML" colors: title: "Ngjyrat" long_title: "Skemat e ngjyrave" @@ -2326,8 +2304,6 @@ sq: change_trust_level: "ndrysho nivelin e besimit" change_username: "ndrysho emrin e përdoruesit" change_site_setting: "ndrysho parametrin e faqes" - change_site_customization: "ndrysho personalizimin e faqes" - delete_site_customization: "fshi personalizimin e faqes" change_site_text: "ndrysho tekstin e faqes" suspend_user: "pezullo anëtarin" unsuspend_user: "çpezullo anëtarin" diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml index d7a890cb23..35d5d2db6a 100644 --- a/config/locales/client.sv.yml +++ b/config/locales/client.sv.yml @@ -2530,32 +2530,12 @@ sv: customize: title: "Anpassa" long_title: "Sidanpassningar" - css: "CSS" - header: "Sidhuvud" - top: "Toppen" - footer: "Sidfot" - embedded_css: "Inbäddad CSS" - head_tag: - text: "" - title: "HTML som kommer att sättas in före taggen" - body_tag: - text: "" - title: "HTML som kommer att sättas in före taggen" - override_default: "Inkludera inte standard-stilmallen" - enabled: "Aktiverad?" preview: "förhandsgranska" - undo_preview: "ta bort förhandsgranskning" - rescue_preview: "standard stil" - explain_preview: "Se sidan med skräddarsydd stilmall" - explain_undo_preview: "Gå tillbaka till den nuvarande aktiva stilmallen" - explain_rescue_preview: "Se sidan med standard-stilmallen" save: "Spara" new: "Ny" new_style: "Ny stil" import: "Importera" - import_title: "Välj en fil eller klistra in text" delete: "Radera" - delete_confirm: "Radera denna anpassning?" about: "Modifiera CSS stilmallar och HTML sidhuvuden på sidan. Lägg till en anpassning för att börja." color: "Färg" opacity: "Opacitet" @@ -2568,13 +2548,9 @@ sv: none_selected: "Välj en e-postmall för att börja redigera." revert: "Ångra ändringar" revert_confirm: "Är du säker på att du vill ångra dina ändringar?" - css_html: - title: "CSS/HTML" - long_title: "CSS- och HTML-anpassningar" colors: title: "Färger" long_title: "Färgscheman" - about: "Modifiera färgerna som används utan att skriva CSS. Lägg till ett schema för att börja." new_name: "Nytt färgschema" copy_name_prefix: "Kopia av" delete_confirm: "Ta bort det här färgschemat?" @@ -2713,8 +2689,6 @@ sv: change_trust_level: "ändra förtroendenivå" change_username: "ändra användarnamn" change_site_setting: "ändra sidinställning" - change_site_customization: "ändra webbplatsanpassning" - delete_site_customization: "radera webbplatsanpassning" change_site_text: "ändra webbplatsens text" suspend_user: "stäng av användare" unsuspend_user: "häv avstängning av användare" diff --git a/config/locales/client.te.yml b/config/locales/client.te.yml index bb92c2d306..95794fbe4e 100644 --- a/config/locales/client.te.yml +++ b/config/locales/client.te.yml @@ -1292,40 +1292,18 @@ te: customize: title: "కస్టమైజ్" long_title: "సైట్ కస్టమైజేషనులు" - css: "సీయస్ యస్" - header: "హెడర్" - top: "అగ్ర" - footer: "ఫుటరు" - head_tag: - text: "" - title: " కొస ముందు ఉంచే హెచ్ టీ యం యల్" - body_tag: - text: "" - title: " కొస ముందు ఉంచే హెచ్ టీ యం యల్" - override_default: "స్టాండర్డ్ సైల్ షీట్ ఉంచకు" - enabled: "చేతమైందా?" preview: "మునుజూపు" - undo_preview: "మునుజూపు తొలగించు" - rescue_preview: "అప్రమేయ స్టైలు" - explain_preview: "సైటును అనురూప స్టైల్షీటుతో దర్శించు" - explain_undo_preview: "ప్రస్తుతం చేతనం చేసిఉన్న కస్టమ్ స్టైల్ షీటుకు మరలు" - explain_rescue_preview: "సైటును అప్రమేయ స్టైల్ షీటుతో చూడు" save: "భద్రపరుచు" new: "కొత్త" new_style: "కొత్త స్టైలు" delete: "తొలగించు" - delete_confirm: "ఈ కస్టమైజేషనులు తొలగించు? " about: "సైట్లో CSS స్టైల్‌షీట్స్ and HTML హెడర్స్ మార్చండి.స్టార్ట్‌కి కస్టమైజేషన్ కలపండి." color: "రంగు" opacity: "అపారదర్శకత" copy: "నకలు" - css_html: - title: "సీయస్ యస్ / హెచ్ టీ యం యల్" - long_title: "సీ యస్ యస్ మరియు హెచ్ టీ యం యల్ కస్టమైజేషనులు" colors: title: "రంగులు" long_title: "రంగు స్కీములు" - about: "సైట్లో వాడే రంగులు CSS వ్రాయకుండా మార్చండి.స్కీమ్ ను స్టార్ట్ కు కలపండి." new_name: "కొత్త రంగు స్కీము" copy_name_prefix: "దీనికి నకలు" delete_confirm: "ఈ రంగు స్కీము తొలగించు?" @@ -1433,8 +1411,6 @@ te: change_trust_level: "నమ్మకపు స్థాయి మార్చు" change_username: "సభ్యనామం మార్చు" change_site_setting: "సైటు అమరిక మార్చు" - change_site_customization: "సైట్ కస్టమైజేషను మార్చు" - delete_site_customization: "సైటు కస్టమైజేషను తొలగించు" suspend_user: "సభ్యుడిని సస్పెండు చేయి" unsuspend_user: "సస్పెండు కాని సభ్యుడు" grant_badge: "బ్యాడ్జ్ ఇవ్వు" diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index 749297e85c..3abd53a5d3 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -2350,32 +2350,12 @@ tr_TR: customize: title: "Özelleştir" long_title: "Site Özelleştirmeleri" - css: "CSS" - header: "Başlık" - top: "En Kısım" - footer: "Alt Kısım" - embedded_css: "Gömülü CSS" - head_tag: - text: "" - title: " etiketinden önce eklenecek HTML" - body_tag: - text: "" - title: " etiketinden önce eklenecek HTML" - override_default: "Standart biçim sayfasını eklemeyin" - enabled: "Etkin mi?" preview: "önizleme" - undo_preview: "önizlemeyi kaldır" - rescue_preview: "öntanımlı biçim" - explain_preview: "Websitesine bu özelleştirilmiş biçim sayfası ile bak" - explain_undo_preview: "Şu an etkin olan özelleştirilmiş biçim sayfasına geri dön" - explain_rescue_preview: "Websitesine varsayılan biçim sayfası ile bak" save: "Kaydet" new: "Yeni" new_style: "Yeni Biçim" import: "İçeri Aktar" - import_title: "Bir dosya seçin ya da kopyalayıp yapıştırın" delete: "Sil" - delete_confirm: "Bu özelleştirmeyi sil?" about: "Sitedeki CSS biçim sayfalarını ve HTML başlıklarını değiştir. Özelleştirme ekleyerek başla." color: "Renk" opacity: "Opaklık" @@ -2388,13 +2368,9 @@ tr_TR: none_selected: "Düzenlemeye başlamak için bir e-posta şablonu seçin. " revert: "Değişiklikleri Geri Al" revert_confirm: "Değişikliklerinizi geri almak istediğinize emin misiniz?" - css_html: - title: "CSS/HTML" - long_title: "CSS ve HTML Özelleştirmeleri" colors: title: "Renkler" long_title: "Renk Düzenleri" - about: "Sitede kullanılan renkleri CSS yazmadan değiştir. Renk düzeni ekleyerek başla." new_name: "Yeni Renk Düzeni" copy_name_prefix: "Kopyası" delete_confirm: "Bu renk düzenini sil?" @@ -2533,8 +2509,6 @@ tr_TR: change_trust_level: "güven seviyesini değiştir" change_username: "kullanıcı adını değiştir" change_site_setting: "site ayarlarını değiştir" - change_site_customization: "websitesinin özelleştirmesini değiştir" - delete_site_customization: "websitesinin özelleştirmesini sil" change_site_text: "site metnini değiştir" suspend_user: "kullanıcıyı uzaklaştır" unsuspend_user: "kullanıcıyı uzaklaştırma" diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index 0e7fbdb3c2..b1b9d3cde0 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -1316,18 +1316,12 @@ uk: customize: title: "Customize" long_title: "Site Customizations" - css: "CSS" - header: "Header" - override_default: "Do not include standard style sheet" - enabled: "Enabled?" preview: "preview" - rescue_preview: "стиль по замовчанню" save: "Save" new: "New" new_style: "New Style" import: "Імпорт" delete: "Delete" - delete_confirm: "Delete this customization?" color: "Колір" opacity: "Непрозорість" copy: "Копіювати" @@ -1337,13 +1331,9 @@ uk: body: "Тіло" revert: "Повернути зміни" revert_confirm: "Чи впевнені ви, що хочете повернути ваші зміни?" - css_html: - title: "CSS/HTML" - long_title: "Налаштування CSS та HTML" colors: title: "Кольори" long_title: "Схеми Кольорів" - about: "Змінюйте кольори, що використовуються на сайті без написання CSS. Додати схему, щоб почати. " new_name: "Нова Схема Кольорів" copy_name_prefix: "Копіювати з" delete_confirm: "Видалити цю кольорову схему?" @@ -1456,8 +1446,6 @@ uk: change_trust_level: "зміна рівня довіри" change_username: "зміна імені користувача" change_site_setting: "зміна налаштування сайта" - change_site_customization: "change site customization" - delete_site_customization: "delete site customization" change_site_text: "змінити текст сайту" suspend_user: "призупинення користувача" unsuspend_user: "скасування призупинення" diff --git a/config/locales/client.ur.yml b/config/locales/client.ur.yml index aa3c735d93..d7f915c2fa 100644 --- a/config/locales/client.ur.yml +++ b/config/locales/client.ur.yml @@ -2488,32 +2488,12 @@ ur: customize: title: "مرضی کے مطابق بنائیں" long_title: "ویب سائٹ کو اپنی مرضی کے حساب سے بنانے کیلئے اختیارات" - css: "CSS" - header: "ہیڈر" - top: "ٹاپ" - footer: "فُوٹَر" - embedded_css: "اَیمبَیڈ کی ہوئی CSS" - head_tag: - text: "" - title: "HTML جو ٹیگ سے پہلے داخل کی جائے گی" - body_tag: - text: "" - title: "HTML جو ٹیگ سے پہلے داخل کی جائے گی" - override_default: "سٹینڈرڈ سٹائل شیٹ شامل نہ کریں" - enabled: "فعال؟" preview: "پیشگی دیکھیں" - undo_preview: "پیشگی وِیو ہٹائیں" - rescue_preview: "پہلے سے طے شدہ سٹائل" - explain_preview: "اپنی مرضی کی سٹائل شیٹ کے ساتھ سائٹ ملاحظہ کریں" - explain_undo_preview: "فی الحال فعال اپنی مرضی کی سٹائل شیٹ پر واپس جائیں" - explain_rescue_preview: "پہلے سے طے شدہ سٹائل شیٹ کے ساتھ سائٹ ملاحظہ کریں" save: "محفوظ کریں" new: "نیا" new_style: "نیا سٹائل" import: "اِمپورٹ" - import_title: "ایک فائل منتخب کریں یا ٹَیکسٹ پَیسٹ کریں" delete: "حذف کریں" - delete_confirm: "اس اِصلاح کو حذف کریں؟" about: "سائٹ پر CSS سٹائل شیٹس اور HTML ہیڈرز میں ترمیم کریں۔ شروع کرنے کیلئے ایک اِصلاح شامل کریں۔" color: "رنگ" opacity: "دھندلاپن" @@ -2526,13 +2506,9 @@ ur: none_selected: "ترمیم شروع کرنے کیلئے ایک اِیمیل ٹَیمپلیٹ منتخب کریں۔" revert: "تبدیلیاں لوٹائیں" revert_confirm: "کیا آپ واقعی تبدیلیاں لوٹانا چاہتے ہیں؟" - css_html: - title: "CSS/HTML" - long_title: "CSS اور HTML اصلاحات" colors: title: "رنگ" long_title: "رنگ سکیمیں" - about: "CSS لکھے بغیر ویب سائٹ پر استعمال رنگوں میں ترمیم کریں۔ شروع کرنے کے لئے ایک نئی سکیم بنائیں۔" new_name: "نئی رنگ سکیم" copy_name_prefix: "نقل از" delete_confirm: "اِس رنگ سکیم کو حذف کریں؟" @@ -2671,8 +2647,6 @@ ur: change_trust_level: "ٹرسٹ لَیول تبدیل کریں" change_username: "صارف نام تبدیل کریں" change_site_setting: "ویب سائٹ کی سیٹِنگ تبدیل کریں" - change_site_customization: "سائٹ اصلاحات تبدیل کریں" - delete_site_customization: "سائٹ اصلاحات حذف کریں" change_site_text: "سائٹ ٹَیکسٹ تبدیل کریں" suspend_user: "صارف معطل کریں" unsuspend_user: "صارف کی معطلی ختم کریں" diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml index bb852949d0..9c9a8dbe61 100644 --- a/config/locales/client.vi.yml +++ b/config/locales/client.vi.yml @@ -118,9 +118,12 @@ vi: enabled: 'được hiển thị lúc %{when}' disabled: 'bỏ hiển thị lúc %{when}' topic_admin_menu: "Lựa chọn quản lí chủ đề." + wizard_required: "Chào mừng bạn đến với Discourse! Hãy bắt đầu với hướng dẫn cài đặt ✨" emails_are_disabled: "Ban quản trị đã tắt mọi email gửi đi. Sẽ không có bất kỳ thông báo nào qua email được gửi đi." bootstrap_mode_enabled: "Để đơn giản hoá quá trình triển khai, bạn đang ở trong chế độ bootstrap. Mọi người dùng mới đều có trust level 1 và sẽ nhận được email cập nhật thông tin mỗi ngày. Chế độ này sẽ tự động tắt khi số người dùng vượt qua %{min_users}." bootstrap_mode_disabled: "Chế độ bootstrap sẽ bị tắt trong 24 giờ tới." + themes: + default_description: "Mặc định" s3: regions: us_east_1: "US East (N. Virginia)" @@ -511,7 +514,9 @@ vi: watched_categories: "Xem" tracked_categories: "Theo dõi" watched_first_post_categories: "Xem bài viết đầu tiên" + watched_first_post_categories_instructions: "Bạn sẽ nhận được thông báo khi có ai đó đăng chủ đề mới trong thư mục này." watched_first_post_tags: "Xem bài viết đầu tiên" + watched_first_post_tags_instructions: "Bạn sẽ nhận được thông báo khi có ai đó đăng chủ đề mới có chứa thẻ này." muted_categories: "Im lặng" muted_categories_instructions: "Bạn sẽ không bao giờ được thông báo về bất cứ điều gì về các chủ đề mới trong các chuyên mục này, và chúng sẽ không hiển thị mới nhất" delete_account: "Xoá Tài khoản của tôi" @@ -530,6 +535,7 @@ vi: revoke_access: "Lấy lại quyền" undo_revoke_access: "Cấp lại quyền" api_approved: "Chấp thuận:" + theme: "Giao diện" staff_counters: flags_given: "cờ hữu ích" flagged_posts: "bài viết gắn cờ" @@ -553,11 +559,14 @@ vi: error: "(lỗi)" action: "Gửi lại mật khẩu tới email" set_password: "Nhập Mật khẩu" + choose_new: "Chọn một mật khẩu mới" + choose: "Chọn một mật khẩu" change_about: title: "Thay đổi thông tin về tôi" error: "Có lỗi xảy ra khi thay đổi giá trị này." change_username: title: "Thay Username" + confirm: "Nếu bạn thay đổi tên đăng nhập, tất cả những liên kết người khác nhắc đến bạn @name sẽ bị lỗi. Bạn chắc chắn muốn thay đổi chứ?" taken: "Xin lỗi, đã có username này." error: "Có lỗi trong khi thay đổi username của bạn." invalid: "Username này không thích hợp. Nó chỉ chứa các ký tự là chữ cái và chữ số. " @@ -605,6 +614,7 @@ vi: short_instructions: "Mọi người có thể nhắc tới bạn bằng @{{username}}" available: "Tên đăng nhập của bạn có sẵn" not_available: "Chưa có sẵn. Thử {{suggestion}}?" + not_available_no_suggestion: "Không sẵn có" too_short: "Tên đăng nhập của bạn quá ngắn" too_long: "Tên đăng nhập của bạn quá dài" checking: "Đang kiểm tra username sẵn sàng để sử dụng...." @@ -638,12 +648,14 @@ vi: always: "luôn luôn" never: "không" email_digests: + title: "Khi tôi không ghé thăm, hãy gửi cho tôi email tóm tắt về các chủ đề nhiều người quan tâm nhất và các câu trả lời." every_30_minutes: "mỗi 30 phút" every_hour: "hàng giờ" daily: "hàng ngày" every_three_days: "ba ngày một" weekly: "hàng tuần" every_two_weeks: "hai tuần một" + include_tl0_in_digests: "Bao gồm nội dung của những thành viên mới trong email tóm tắt." email_in_reply_to: "Kèm theo đoạn dẫn trích trả lời bài viết trong email" email_direct: "Gửi cho tôi một email khi có người trích dẫn, trả lời cho bài viết của tôi, đề cập đến @username của tôi, hoặc mời tôi đến một chủ đề" email_private_messages: "Gửi cho tôi email khi có ai đó nhắn tin cho tôi" @@ -889,12 +901,23 @@ vi: github: title: "với GitHub" message: "Chứng thực với GitHub (chắc chắn chặn popup không bật)" + invites: + accept_title: "Lời mời" + welcome_to: "Chào mừng bạn đến với %{site_name}!" + invited_by: "Bạn đã được mời bởi:" + your_email: "Địa chỉ email của bạn là %{email}." + accept_invite: "Chấp nhận lời mời" + password_label: "Thiết lập mật khẩu (Không bắt buộc)" + password_reset: + continue: "Tiếp tục truy cập %{site_name}" emoji_set: google: "Google" twitter: "Twitter" win10: "Win10" category_page_style: categories_only: "Chỉ chuyên mục" + categories_with_featured_topics: "Các chuyên mục và chủ đề nổi bật" + categories_and_latest_topics: "Các chuyên mục và chủ đề mới" shortcut_modifier_key: shift: 'Shift' ctrl: 'Ctrl' @@ -1039,6 +1062,7 @@ vi: sort_by: "Sắp xếp theo" relevance: "Độ phù hợp" latest_post: "Bài viết mới nhất" + latest_topic: "Chủ đề mới" most_viewed: "Xem nhiều nhất" most_liked: "Like nhiều nhất" select_all: "Chọn tất cả" @@ -2069,32 +2093,12 @@ vi: customize: title: "Tùy biến" long_title: "Tùy biến trang" - css: "CSS" - header: "Header" - top: "Trên" - footer: "Footer" - embedded_css: "Nhúng CSS" - head_tag: - text: "" - title: "HTML sẻ thêm trước thẻ " - body_tag: - text: "" - title: "HTML sẽ thêm trước thẻ " - override_default: "Không bao gồm style sheet chuẩn" - enabled: "Cho phép?" preview: "xem trước" - undo_preview: "xóa xem trước" - rescue_preview: "default style" - explain_preview: "Xem website với stylesheet tùy chỉnh" - explain_undo_preview: "Quay trở lại với kiểu tùy chỉnh stylesheet hiện tại" - explain_rescue_preview: "Xem website với stylesheet mặc định" save: "Lưu" new: "Mới" new_style: "Style mới" import: "Nhập" - import_title: "Chọn một file hoặc paste chữ." delete: "Xóa" - delete_confirm: "Xóa tùy biến này?" about: "Chỉnh sửa CSS và HTML header trên trang. Thêm tùy biến để bắt đầu." color: "Màu sắc" opacity: "Độ mờ" @@ -2107,13 +2111,9 @@ vi: none_selected: "Chọn email template để bắt đầu chỉnh sửa." revert: "Hoàn nguyên thay đổi" revert_confirm: "Bạn có chắc chắn muốn hoàn nguyên các thay đổi?" - css_html: - title: "CSS/HTML" - long_title: "Tùy biến CSS và HTML" colors: title: "Màu sắc" long_title: "Bảng màu" - about: "Chỉnh " new_name: "Bản màu mới" copy_name_prefix: "Bản sao của" delete_confirm: "Xóa bảng màu này?" @@ -2247,8 +2247,6 @@ vi: change_trust_level: "thay đổi cấp tin cậy" change_username: "thay đổi username" change_site_setting: "thay đổi cấu hình trang" - change_site_customization: "thay đổi tùy biến trang" - delete_site_customization: "xóa tùy biến trang" change_site_text: "thay đổi chữ trên website" suspend_user: "tạm khóa thành viên" unsuspend_user: "hủy tạm khóa thành viên" diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index 808d36b076..e66bc4b35b 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -2359,32 +2359,12 @@ zh_CN: customize: title: "定制" long_title: "站点定制" - css: "CSS" - header: "头部" - top: "顶部" - footer: "底部" - embedded_css: "嵌入的 CSS" - head_tag: - text: "" - title: "将在 标签前插入的 HTML" - body_tag: - text: "" - title: "将在 标签前插入的 HTML" - override_default: "覆盖缺省值?" - enabled: "启用?" preview: "预览" - undo_preview: "移除预览" - rescue_preview: "默认样式" - explain_preview: "以自定义样式浏览此网页" - explain_undo_preview: "返回目前使用中的自定义样式" - explain_rescue_preview: "以默认样式浏览此网页" save: "保存" new: "新建" new_style: "新样式" import: "导入" - import_title: "选择一个文件或者粘贴文本" delete: "删除" - delete_confirm: "删除本定制内容?" about: "修改站点的 CSS 样式表和 HTML 头部。添加一个自定义方案开始。" color: "颜色" opacity: "透明度" @@ -2397,13 +2377,9 @@ zh_CN: none_selected: "选择一个邮件模板开始编辑。" revert: "撤销更变" revert_confirm: "你确定要撤销你的更变吗?" - css_html: - title: "CSS/HTML" - long_title: "自定义 CSS 和 HTML" colors: title: "颜色" long_title: "颜色方案" - about: "颜色方案让你能够让你在不写 CSS 的情况下更改色彩。添加一种颜色以开始。" new_name: "新的颜色方案" copy_name_prefix: "复制于" delete_confirm: "删除这个颜色方案?" @@ -2542,8 +2518,6 @@ zh_CN: change_trust_level: "更改信任等级" change_username: "修改用户名" change_site_setting: "更改站点设置" - change_site_customization: "更改站点自定义" - delete_site_customization: "删除站点自定义" change_site_text: "更改站点文字" suspend_user: "封禁用户" unsuspend_user: "解禁用户" diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml index 4d3811e275..a3a326b2ad 100644 --- a/config/locales/client.zh_TW.yml +++ b/config/locales/client.zh_TW.yml @@ -2360,32 +2360,12 @@ zh_TW: customize: title: "客製化" long_title: "網站客製化" - css: "CSS" - header: "標頭" - top: "精選" - footer: "頁尾" - embedded_css: "內嵌 CSS" - head_tag: - text: "" - title: "HTML 將會置於 之前" - body_tag: - text: "" - title: "HTML 將會置於 之前" - override_default: "不要保含標準樣式" - enabled: "已啟用?" preview: "預覽" - undo_preview: "移除預覽" - rescue_preview: "預設風格" - explain_preview: "以此自訂樣式預覽網頁" - explain_undo_preview: "還原現時的自訂樣式" - explain_rescue_preview: "以預設樣式預覽網頁" save: "儲存" new: "新增" new_style: "新增樣式" import: "匯入" - import_title: "選取檔案或貼上文本" delete: "刪除" - delete_confirm: "刪除此樣式?" about: "修改網站的 CSS 和 HTML headers。請新增一個自定樣式來開始使用。" color: "顏色" opacity: "不透明度" @@ -2398,13 +2378,9 @@ zh_TW: none_selected: "選擇一個電子郵件範本開始編輯" revert: "恢復變更" revert_confirm: "你確定要恢復這個變更?" - css_html: - title: "CSS/HTML" - long_title: "CSS 與 HTML 客製化" colors: title: "顏色" long_title: "顏色樣式" - about: "不需撰寫 CSS 即可修改網站的顏色。請新增一項方案來開始使用。" new_name: "新的配色樣式" copy_name_prefix: "複製於" delete_confirm: "刪除此顏色樣式?" @@ -2543,8 +2519,6 @@ zh_TW: change_trust_level: "修改信任等級" change_username: "修改用戶名稱" change_site_setting: "修改網站設定" - change_site_customization: "修改網站客製化" - delete_site_customization: "刪除網站客製化" change_site_text: "更改站點文字" suspend_user: "將用戶停權" unsuspend_user: "恢復用戶權限" diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index 31b0ff4c28..5fc265524a 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -27,7 +27,6 @@ ar: topics: "المواضيع" posts: "المشاركات" loading: "يحمّل" - powered_by_html: 'تدعمه دسكورس ، يفضّل عرضه وجافاسكربت مفعّل' log_in: "لِج" purge_reason: "حُذف آليًّا باعتباره حسابًا إمّا مهجورًا أو غير مفعّل" disable_remote_images_download_reason: "عُطّل تنزيل الصور عن بعد بسبب نفاذ مساحة القرص الحرّة." diff --git a/config/locales/server.bs_BA.yml b/config/locales/server.bs_BA.yml index 7524f81d16..f9e96e443d 100644 --- a/config/locales/server.bs_BA.yml +++ b/config/locales/server.bs_BA.yml @@ -27,7 +27,6 @@ bs_BA: topics: "Topics" posts: "posts" loading: "Loading" - powered_by_html: 'Powered by Discourse, best viewed with JavaScript enabled' log_in: "Log In" purge_reason: "Automatski obrisan kao napušten, deaktiviran račun" disable_remote_images_download_reason: "Remote images download was disabled because there wasn't enough disk space available." diff --git a/config/locales/server.cs.yml b/config/locales/server.cs.yml index 35a6acd328..4c372e8e78 100644 --- a/config/locales/server.cs.yml +++ b/config/locales/server.cs.yml @@ -27,7 +27,6 @@ cs: topics: "Témata" posts: "příspěvky" loading: "Nahrávám" - powered_by_html: 'Systém běží na Discourse, nejlépe funguje se zapnutým JavaScriptem' log_in: "Přihlásit se" purge_reason: "Tento účet byl automaticky smazán a deaktivován jako opuštěný" disable_remote_images_download_reason: "Stahování obrázků z cizích serverů bylo vypnuto protože na disku není dostatek místa." @@ -620,7 +619,7 @@ cs: title_fancy_entities: "Převádět běžné ASCII znaky na HTML entity v názvech témat, ve stylu SmartyPants http://daringfireball.net/projects/smartypants/" title_prettify: "Zabrání běžným překlepům a chybám v názvu tématu, včetně psaní velkými písmeny, malé písmeno na začátku, vícenásobné vykřičníky a otazníky, atd." faq_url: "Pokud máte dokument FAQ hostovaný samostatně, napište sem jeho plnou URL." - tos_url: "Pokud máte dokument 'Podmínky Používání' hostovaný samostatně, napište sem jeho plnou URL." + tos_url: "Pokud máte dokument 'Podmínky používání' hostovaný samostatně, napište sem jeho plnou URL." privacy_policy_url: "Pokud máte dokument 'Ochrana Soukromí' hostovaný samostatně, napište sem jeho plnou URL." reply_by_email_address: "Šablona emailé adresy pro odpověď emailem, např. %{reply_key}@reply.myforum.com" delete_all_posts_max: "Maximální počet příspěvků, které mohou být smazány najednou tlačítkem 'Odstranit všechny příspěvky'. Pokud má uživatel více příspěvků než je zde nastaveno, nemohou být jeho příspěvky smazány najednou a uživatele nelze odstranit." @@ -719,7 +718,7 @@ cs: #[Vítejte na %{title}](#welcome) Je vyžadován uživatelský účet. Prosím vytvořte si nový účet nebo se přihlaste. terms_of_service: - title: "Podmínky používání." + title: "Podmínky používání" signup_form_message: 'I have read and accept the Terms of Service.' deleted: 'smazáno' upload: diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml index 73ad95b60f..da593c43b9 100644 --- a/config/locales/server.da.yml +++ b/config/locales/server.da.yml @@ -27,7 +27,6 @@ da: topics: "Emner" posts: "indlæg" loading: "Indlæser" - powered_by_html: 'Siden er lavet med Discourse, opleves bedst med JavaScript slået til' log_in: "Log ind" purge_reason: "Automatisk slettet som forladt, deaktiveret konto" disable_remote_images_download_reason: "Fjerndownload af billeder deaktiveret grundet utilstrækkelig diskplads" diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 27394c71a3..bdb6ebb192 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -27,7 +27,6 @@ de: topics: "Themen" posts: "Beiträge" loading: "Wird geladen" - powered_by_html: 'Basiert auf Discourse, funktioniert am besten mit aktiviertem JavaScript' log_in: "Anmelden" purge_reason: "Verlassenes, deaktiviertes Konto automatisch gelöscht" disable_remote_images_download_reason: "Der Download von Bildern wurde deaktiviert, weil nicht mehr genug Plattenplatz vorhanden war." @@ -1655,11 +1654,6 @@ de: Wenn der Beitrag jedoch von der Community erneut ausgeblendet wurde, wird er ausgeblendet bleiben, bis sich das Team darum kümmert – und möglicherweise gibt es weitere Maßnahmen, einschließlich einer möglichen Sperrung deines Kontos. Weitre Hinweise findest du in unseren [Community-Richtlinien](%{base_url}/guidelines). - usage_tips: - text_body_template: | - Ein paar Tipps für die ersten Schritte als neuer Benutzer [findest du in diesem Blog-Eintrag](http://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/). - - Während du hier teilnimmst, werden wir dich kennenlernen und die vorübergehenden Einschränkungen für neue Benutzer werden aufgehoben. Über die Zeit, erreichst du [Vertrauensstufen](https://meta.discourse.org/t/what-do-user-trust-levels-do/4924), die spezielle Fähigkeiten beinhalten, um die Community gemeinsam zu verwalten. welcome_user: title: "Willkommen: Benutzer" subject_template: "Willkommen bei %{site_name}!" @@ -2866,7 +2860,6 @@ de: safe_mode: title: "Abgesicherten Modus betreten" description: "Der abgesicherte Modus ermöglicht es dir, deine Seite zu testen, ohne Plugins oder Seiten-Anpassungen zu laden." - no_customizations: "Alle Seiten-Anpassungen deaktivieren" only_official: "Inoffizielle Plugins deaktivieren" no_plugins: "Alle Plugins deaktivieren" enter: "Abgesicherten Modus betreten" diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml index fcd63c10b9..397b742506 100644 --- a/config/locales/server.el.yml +++ b/config/locales/server.el.yml @@ -27,7 +27,6 @@ el: topics: "Νήματα" posts: "αναρτήσεις" loading: "Φόρτωση" - powered_by_html: 'Βασίζεται στο Discourse, λειτουργεί καλύτερα με ενεργοποιημένη τη Javascript' log_in: "Σύνδεση" purge_reason: "Διεγράφη αυτόματα ως εγκαταλελειμμένος, μη ενεργοποιημένος λογαριασμός." disable_remote_images_download_reason: "Το κατέβασμα εικόνων απενεργοποιήθηκε επειδή δεν υπάρχει αρκετός χώρος στο δίσκο." @@ -1646,11 +1645,6 @@ el: Όμως, εάν η ανάρτηση αποκρυφθεί από την κοινότητα για δεύτερη φορά, θα παραμείνει κρυμμένη έως ότου διαχειριστεί απο συνεργάτη, και ίσως υπάρξουν περαιτέρω ενέργειες συμπεριλαμβανομένης την πιθανής διακοπής του λογαριασμού σου. Για επιπλεον καθοδήγηση, παρακαλούμε ανάτρεξε στις [οδηγίες κοινότητας](%{base_url}/guidelines). - usage_tips: - text_body_template: | - Για να βρεις μερικά μυστικά για να αρχίσεις ως νέος χρήστης, [τσέκαρε αυτή την ανάρτηση στο ιστολόγιο](http://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/). - - Καθώς συμμετέχεις εδώ, θα σε γνωρίσουμε και οι προσωρινοί περιορισμοί νέου χρήστη θα αρθούν. Με τον καιρό θα αποκτάς [επίπεδα εμπιστοσύνης](https://meta.discourse.org/t/what-do-user-trust-levels-do/4924) που περιλαμβάνουν ειδικές δυνατότητες και μας βοηθούν να διαχειριζόμαστε την κοινότητα μαζί. welcome_user: title: "Καλώς ήλθες χρήστη" subject_template: "Καλώς ήλθατε στο %{site_name}!" @@ -2773,7 +2767,6 @@ el: safe_mode: title: "Εισαγωγή λειτουργίας ασφαλείας" description: "Η ασφαλής λειτουργία σου επιτρέπει να εξετάσεις τον ιστότοπό σου χωρίς να φορτώσεις πρόσθετα ή εξηατομικεύσεις ιστοτόπου." - no_customizations: "Απενεργοποίησε όλες τις προσαρμογές του ιστότοπου. " only_official: "Απενεργοποίησε ανεπίσημα πρόσθετα" no_plugins: "Απενεργοποίησε όλα τα πρόσθετα" enter: "Εισαγωγή λειτουργίας ασφαλείας" diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index 8bdaefea22..26005e77ba 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -27,7 +27,6 @@ es: topics: "Temas" posts: "posts" loading: "Cargando" - powered_by_html: 'Funciona gracias a Discourse, se ve mejor con JavaScript activado' log_in: "Iniciar sesión" purge_reason: "Cuenta eliminada automáticamente al detectarse como abandonada, desactivada" disable_remote_images_download_reason: "La descarga de imágenes remotas se desactivó porque no había suficiente espacio disponible en disco." @@ -1724,11 +1723,6 @@ es: Sin embargo, si el mensaje es ocultado por la comunidad una segunda vez, seguirá ocultado hasta que el staff lo revise – y se podrían tomar medidas incluida la posible suspensión de tu cuenta. Para más información sobre lo que consideramos adecuado, lee las [directrices de la comunidad](%{base_url}/guidelines). - usage_tips: - text_body_template: | - Si quieres unos consejos para empezar, [echa un vistazo a esta entrada de blog](http://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/). - - Conforme vayas participando por aquí, te iremos conociendo más, y tus limitaciones temporales como nuevo usuario serán levantadas. Con el tiempo ganarás [niveles de confianza](https://meta.discourse.org/t/what-do-user-trust-levels-do/4924) que incluyen habilidades especiales para ayudarnos a gestionar la comunidad juntos. welcome_user: title: "Bienvenido Usuario" subject_template: "¡Bienvenido a %{site_name}!" @@ -2930,7 +2924,6 @@ es: safe_mode: title: "Activar modo seguro" description: "El modo seguro permite probar tu página sin cargar plugins o customizaciones." - no_customizations: "Desactivar todas las customizaciones" only_official: "Desactivar plugins no oficiale" no_plugins: "Desactivar todos los plugins" enter: "Activar modo seguro" diff --git a/config/locales/server.et.yml b/config/locales/server.et.yml index deca03da1c..c54f8cc7df 100644 --- a/config/locales/server.et.yml +++ b/config/locales/server.et.yml @@ -27,7 +27,6 @@ et: topics: "Teemad" posts: "postitused" loading: "Laetakse" - powered_by_html: 'Ajamiks on Discourse... vaadata JavaScript lubatuna' log_in: "Logi sisse" purge_reason: "Kustutatud automaatselt kui mahajäetud, deaktiveeritud konto" disable_remote_images_download_reason: "Väliste piltide allalaadimine keelati, kuna polnud polnud piisavalt vaba ruumi." @@ -915,7 +914,6 @@ et: title: "Saada aktiveerimismeil uuesti" safe_mode: title: "Sisene turvalisse režiimi" - no_customizations: "Keela kõik saidi kohandused" only_official: "Keela kõik mitteametlikud pluginad" no_plugins: "Keela kõik pluginad" enter: "Sisene turvalisse režiimi" diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index db9daae38f..610ac8d0d6 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -27,7 +27,6 @@ fa_IR: topics: "موضوعات" posts: "نوشته ها" loading: "بارگذاری..." - powered_by_html: 'قدرت گرفته ازدیسکورسفارسی سازی دیسکورس فارسی, برای نمایش بهتر جاوا اسکریپت را فعال کنید.' log_in: "ورود" purge_reason: "حساب کاربری غیرفعال. حذف شده به صورت خودکار به علت عدم استفاده." disable_remote_images_download_reason: "عکس های ریموت دانلود شده غیرفعال شدند زیرا آنجا فضای کافی در دیسک وجود نداشت" @@ -207,6 +206,8 @@ fa_IR: attributes: category: name: "نام دسته" + topic: + title: 'عنوان' post: raw: "متن" user_profile: diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index efb214f84a..c2d463e6e4 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -27,7 +27,7 @@ fi: topics: "Ketjut" posts: "viestit" loading: "Lataa" - powered_by_html: 'Voimanlähteenä Discourse, toimii parhaiten, kun JavaScript on käytössä' + powered_by_html: 'Voimanlähteenä Discourse, toimii parhaiten, kun JavaScript on käytössä' log_in: "Kirjaudu" purge_reason: "Hylätty, aktivoimaton tili poistettiin automaattisesti" disable_remote_images_download_reason: "Linkattujen kuvien lataaminen poistettiin käytöstä vähäisen tallennustilan vuoksi." @@ -826,7 +826,7 @@ fi: poll_pop3_auth_error: "Yhteys POP3-palvelimelle epäonnistuu autentikaatiovirheen vuoksi. Tarkista POP3-asetukset." site_settings: censored_words: "Sanat, jotka korvataan automaattisesti merkeillä ■■■■" - censored_pattern: "Merkkijono, joka korvataan automaattisesti ■■■■" + censored_pattern: "Säännöllinen lauseke, joka korvataan automaattisesti ■■■■" delete_old_hidden_posts: "Poista automaattisesti kaikki yli 30 päivää piilotettuna olleet viestit." default_locale: "Sivuston oletuskieli (ISO 639-1 koodi)" allow_user_locale: "Salli käyttäjien vaihtaa käyttöliittymän kieli omista asetuksista" @@ -1713,9 +1713,9 @@ fi: Saadaksesi lisätietoja, lue [yhteisön säännöt](%{base_url}/guidelines). usage_tips: text_body_template: | - [Tässä blogikirjoituksessa](http://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/) on joitakin käteviä vinkkejä uudelle käyttäjälle. + [Tässä blogikirjoituksessa](https://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/) on joitakin käteviä vinkkejä uudelle käyttäjälle. - Sitä mukaa kun toimit täällä, opimme tuntemaan sinut ja väliaikaisia uuden käyttäjän rajoitteita poistetaan automaattisesti. Ajan myötä nouset ylemmille [luottamustasoille](https://meta.discourse.org/t/what-do-user-trust-levels-do/4924) ja saat valtuuksia joiden avulla voit osallistua yhteisömme ylläpitoon. + Sitä mukaa kun toimit täällä, opimme tuntemaan sinut ja väliaikaisia uuden käyttäjän rajoitteita poistetaan automaattisesti. Ajan myötä nouset ylemmille [luottamustasoille](https://meta.discourse.org/t/what-do-user-trust-levels-do/4924) ja saat valtuuksia, joiden avulla voit osallistua yhteisömme ylläpitoon. welcome_user: title: "Tervetuloa käyttäjä" subject_template: "Tervetuloa sivustolle %{site_name}!" @@ -2421,6 +2421,8 @@ fi: no_echo_mailing_list_mode: "Postitustilassa ei sähköposti-ilmoituksia käyttäjän omista viesteistä" color_schemes: base_theme_name: "Pohja" + default: "Vaalea värimalli" + dark: "Tumma värimalli" about: "Tietoja" guidelines: "Ohjeet" privacy: "Yksityisyys" @@ -2869,7 +2871,7 @@ fi: safe_mode: title: "Siirry vikasietotilaan" description: "Vikasietotilassa voit testata sivustoasi ilman lisäosia ja sivuston mukautuksia." - no_customizations: "Poista käytöstä sivuston mukautukset" + no_customizations: "Poista nykyinen teema käytöstä" only_official: "Poista käytöstä epäviralliset lisäosat" no_plugins: "Poista käytöstä kaikki lisäosat" enter: "Siirry vikasietotilaan" diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index a5e46ad5ac..b8a7257d6b 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -27,7 +27,6 @@ fr: topics: "Sujets" posts: "messages" loading: "Chargement" - powered_by_html: 'Propulsé par Discourse, le rendu est meilleur avec le JavaScript activé' log_in: "Se connecter" purge_reason: "Supprimé automatiquement comme compte abandonné et non activé" disable_remote_images_download_reason: "Le téléchargement des images externes a été désactivé faute de place suffisante sur le disque." @@ -2429,7 +2428,6 @@ fr: safe_mode: title: "Activer le mode sans échec" description: "Le mode sans échec vous permet de tester votre site sans charger les extensions ou personnalisations." - no_customizations: "Désactiver toutes les personnalisations" only_official: "Désactiver les extensions non officielles" no_plugins: "Désactiver toutes les extensions" enter: "Activer le mode sans échec" diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index a17d4d7a3d..9cb0cc262e 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -27,7 +27,7 @@ he: topics: "נושאים" posts: "פוסטים" loading: "טוען" - powered_by_html: 'מונע ע"י Discourse, פועל מיטבית עם Javascript מאופשר' + powered_by_html: 'מופעל על ידי דיסקורס, לצפיה מיטבית יש להפעיל JavaScript' log_in: "התחברות" purge_reason: "נמחק אוטומטית כחשבון נטוש ולא פעיל" disable_remote_images_download_reason: "הורדת תמונות מרחוק נחסמה בשל היעדר מספיק שטח אחסון פנוי." @@ -1714,9 +1714,9 @@ he: להנחיות נוספות, אנא פנו ל[הנחיות הקהילה](%{base_url}/guidelines). usage_tips: text_body_template: | - כמה טיפים לגבי איך להתחיל כמשתמשים חדשים, [ראו את הפוסט הזה](http://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/). + לכמה טיפים זריזים בנוגע להתחלה כמשתמשים חדשים, [קיראו פוסט זה](https://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/). - ככל שתשתתפו כאן, נכיר אתכם, ומגבלות זמניות על משתמשים חדשים יוסרו. במהלך הזמן תצבעו [דרגות אמון](https://meta.discourse.org/t/what-do-user-trust-levels-do/4924) שכוללות יכולות לסייע לנו לנהל את הקהילה שלנו ביחד. + ככל שתשתתפו כאן, נכיר אתכם, והגבלות זמניות של משתמשים חדשים יוסרו. במשך הזמן תצברו [רמות אמון](https://meta.discourse.org/t/what-do-user-trust-levels-do/4924) שיכללו יכולות מיוחדות שיעזרו לנו לנהל את הקהילה שלנו יחד. welcome_user: title: "ברוכים הבאים למשתמש/ת" subject_template: "ברוכים הבאים ל %{site_name}!" @@ -2422,6 +2422,8 @@ he: no_echo_mailing_list_mode: "התראות רשימת תפוצה ינוטרלו עבור הפוסטים של המשתמש עצמו" color_schemes: base_theme_name: "בסיס" + default: "סכמה בהירה" + dark: "סכמה כהה" about: "אודות" guidelines: "קווים מנחים" privacy: "פרטיות" @@ -2912,7 +2914,7 @@ he: safe_mode: title: "כנסו למצב בטוח" description: "מצב בטוח מאפשר לכם לבחון את האתר שלכם מבלי לטעון תוספים או התאמות אתר." - no_customizations: "נטרלו את כל התאמות האתר" + no_customizations: "ניטרול של תמה נוכחית" only_official: "נטרלו תוספים לא רשמיים" no_plugins: "נטרלו את כל התוספים" enter: "כנסו למצב בטוח" diff --git a/config/locales/server.id.yml b/config/locales/server.id.yml index 08cb3f4465..1ca7b879aa 100644 --- a/config/locales/server.id.yml +++ b/config/locales/server.id.yml @@ -23,7 +23,6 @@ id: topics: "Topik" posts: "post" loading: "Memuat" - powered_by_html: 'Didayakan oleh Discourse, aktifkan JavaScript untuk tampilan terbaik' log_in: "Log In" purge_reason: "Secara otomatis dihapus sebagai akun yang ditinggalkan, dinon-aktifkan." disable_remote_images_download_reason: "Unduh gambar jarak jauh dimatikan karena ruang penyimpanan di server tidak cukup." diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index 8b80968c28..500781b23d 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -27,7 +27,6 @@ it: topics: "Argomenti" posts: "messaggi" loading: "Caricamento" - powered_by_html: 'Powered by Discourse, è consigliato abilitare JavaScript ' log_in: "Connetti" purge_reason: "Account cancellato automaticamente perché abbandonato o disattivato" disable_remote_images_download_reason: "Lo scaricamento delle immagini remote è stato disabilitato perché non c'è abbastanza spazio disco disponibile." diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml index ea1d8aac64..71969ce4c7 100644 --- a/config/locales/server.ja.yml +++ b/config/locales/server.ja.yml @@ -25,7 +25,6 @@ ja: topics: "トピック" posts: "投稿" loading: "読み込み中" - powered_by_html: 'Powered by Discourse, best viewed with JavaScript enabled' log_in: "ログイン" purge_reason: "放棄されていたため自動的に削除、アカウントを停止しました。" disable_remote_images_download_reason: "ディスク容量が不足しているため、リモートでの画像ダウンロードは無効になっています。" diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index 1ff3ef67f3..15edab89dc 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -26,7 +26,6 @@ ko: topics: "글타래" posts: "글" loading: "로딩중" - powered_by_html: 'Powered by Discourse, best viewed with JavaScript enabled' log_in: "로그인" disable_remote_images_download_reason: "디스크저장공간이 부족하여 원격 이미지 다운로드 기능이 비활성화 되었습니다." anonymous: "익명" diff --git a/config/locales/server.nb_NO.yml b/config/locales/server.nb_NO.yml index 58df044ee3..75dee21328 100644 --- a/config/locales/server.nb_NO.yml +++ b/config/locales/server.nb_NO.yml @@ -27,7 +27,6 @@ nb_NO: topics: "Emner" posts: "innlegg" loading: "Laster" - powered_by_html: 'Drevet av Discourse, best nyttet med JavaScript påslått' log_in: "Logg inn" purge_reason: "Automatisk slettet som som forlatt, uaktivert konto" disable_remote_images_download_reason: "Nedlasting av bilder ble deaktivert grunnet mangel på tilgjengelig diskplass." @@ -1046,11 +1045,25 @@ nb_NO: description: "Sikkerhetsmodus lar deg teste nettstedet ditt uten at tillegg eller tilpasninger lastes." wizard: step: + forum_title: + title: "Navn" contact: fields: contact_email: label: "E-post" placeholder: "navn@example.com" + contact_url: + label: "Nettside" + placeholder: "http://www.eksempel.no/kontakt-oss" + site_contact: + label: "Automatiserte meldinger" + corporate: + title: "Organisasjon" + fields: + company_short_name: + label: "Bedriftsnavn (kort)" + colors: + title: "Drakt" logos: title: "Logoer" fields: diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index 60606ff3b0..cd4554c3ab 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -27,7 +27,7 @@ nl: topics: "Topics" posts: "berichten" loading: "Laden" - powered_by_html: 'Mogelijk gemaakt door Discourse, best bekeken met JavaScript ingeschakeld' + powered_by_html: 'Mogelijk gemaakt door Discourse, best bekeken met JavaScript ingeschakeld' log_in: "Aanmelden" purge_reason: "Automatisch verwijderd als verlaten, gedeactiveerde account." disable_remote_images_download_reason: "Het downloaden van externe afbeeldingen is uitgeschakeld, omdat er niet genoeg schijfruimte beschikbaar was." @@ -1552,7 +1552,6 @@ nl: finish_installation: congratulations: "Gefeliciteerd, u hebt Discourse geïnstalleerd!" safe_mode: - no_customizations: "Alle website-aanpassingen uitschakelen" only_official: "Niet-officiële plug-ins uitschakelen" no_plugins: "Alle plug-ins uitschakelen" wizard: diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index a9a3447f6c..856eca8074 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -27,7 +27,6 @@ pl_PL: topics: "Tematy" posts: "posty" loading: "Ładowanie" - powered_by_html: 'Zasilane przez Discourse, najlepiej oglądać z włączonym JavaScriptem' log_in: "Logowanie" purge_reason: "Automatycznie usunięte jako porzucone, dezaktywowane konto." disable_remote_images_download_reason: "Pobieranie zewnętrznych grafik zostało wyłączone z uwagi na niską ilość wolnego miejsca na dysku." @@ -2078,7 +2077,6 @@ pl_PL: message: "

    Powtórnie przesłaliśmy maila aktywacyjnego do %{email}" safe_mode: title: "Wejście w tryb bezpieczny" - no_customizations: "Wyłącz własną personalizację strony" only_official: "Wyłącz nieoficjalne wtyczki" no_plugins: "Wyłącz wszystkie wtyczki" enter: "Włącz Tryb Bezpieczny" diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index a59689fdc6..3e589073ca 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -27,7 +27,6 @@ pt: topics: "Tópicos" posts: "mensagens" loading: "A carregar" - powered_by_html: 'Desenvolvido por Discourse, e melhor visualizado com o JavaScript ativo' log_in: "Iniciar Sessão" purge_reason: "Conta removida automaticamente como conta abandonada, desactivada." disable_remote_images_download_reason: "O download remoto de imagens foi desativado por não haver espaço disponível no disco." @@ -2435,7 +2434,6 @@ pt: safe_mode: title: "Entrar em modo de segurança" description: "O modo de segurança permite-lhe testar o seu sítio sem carregar plugins ou alterações." - no_customizations: "Desativar todas as alterações do sítio" only_official: "Desativar plugins não-oficiais" no_plugins: "Desativar todos os plugins" enter: "Entrar em Modo de Segurança" diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index dad3faea36..283d4883a1 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -27,7 +27,6 @@ pt_BR: topics: "Tópicos" posts: "mensagens" loading: "Carregando" - powered_by_html: 'Movido a Discourse, melhor visualizado com JavaScript ativado' log_in: "Entrar" purge_reason: "Automaticamente eliminada por ser uma conta abandonada, não ativada" disable_remote_images_download_reason: "Download de imagens remotas foi desativado porque não havia espaço suficiente em disco disponível." diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index 3e0e757cb4..96642b7b30 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -27,13 +27,14 @@ ro: topics: "Subiecte" posts: "Postări" loading: "Se încarcă" - powered_by_html: 'Propulsat de Discourse, Vizualizare optimă cu JavaScript activat' log_in: "Autentificare" purge_reason: "Cont șters automat, ca fiind abandonat, dezactivat." disable_remote_images_download_reason: "Descărcarea de imagini la distanță a fost dezactivată deoarece nu mai era spațiu suficient pe disc." anonymous: "Anonime" + remove_posts_deleted_by_author: "Șters de către autor" emails: incoming: + default_subject: "Acest subiect are nevoie de un titlu" show_trimmed_content: "Arată conținut restrâns." maximum_staged_user_per_email_reached: "Număr maxim de utilizatori în așteptare, pe email." errors: @@ -176,6 +177,7 @@ ro: spamming_host: "Nu poți adăuga un link pentru acest domeniu." user_is_suspended: "Utilizatorii suspendați nu au dreptul de a posta." topic_not_found: "Ceva n-a mers bine. Poate că acest subiect a fost închis sau șters în timp ce te uitai la el?" + not_accepting_pms: "%{username} nu acceptă mesaje în acest moment." just_posted_that: "este prea asemănător cu ceea ce ați postat recent" invalid_characters: "conține caractere invalide" is_invalid: "pare neclar, e completă propoziția?" @@ -250,6 +252,20 @@ ro: - Critica constructivă e binevenită, critică *ideea*, nu oamenii. Pentru detalii, [verifică regulile comune](/reguli). Acest panel va apărea doar pentru prima %{education_posts_text}. + avatar: | + ### Ce zici de o poză atașată contului tău? + + Ai adăugat câteva comentarii și subiecte, dar poza ta de profil nu este unică, cum ești tu - este doar o literă. + + Ai luat în considerare să **[îți actualizezi profilul](%{profile_path})** cu o imagine care te reprezintă? + + Este mai ușor să urmărești o discuție și să găsești oameni interesanți într-o conversație atunci când fiecare are o imagine unică! + reviving_old_topic: | + ### Reînvii această discuție? + + Ultimul răspuns în această discuție a fost în urmă cu **%{days}** zile. Răspunsul tău va pune discuția pe prima pagină și îi va notifica pe toți cei ce au fost implicați în discuție. + + Ești sigur că vrei să continui această discuție veche? activerecord: attributes: category: @@ -280,6 +296,7 @@ ro: same_as_username: "este identică cu nume utilizator. Te rugăm să folosești o parolă mai sigură." same_as_email: "este identică cu emailul. Te rugăm să folosești o parolă mai sigură." same_as_current: "este identică cu parola curentă." + unique_characters: "are prea multe caractere repetate. Te rugăm să folosești o parolă mai sigură." ip_address: signup_not_allowed: "Înscrierea de pe acest cont nu este permisă." color_scheme_color: @@ -290,6 +307,14 @@ ro: attributes: payload_url: invalid: "URL-ul este invalid. URL-ul trebuie să includă http:// sau https:// , iar caracterul spațiu nu este permis." + custom_emoji: + attributes: + name: + taken: este deja folosit de un alt emoji + topic_status_update: + attributes: + execute_at: + in_the_past: "trebuie să fie în viitor." <<: *errors user_profile: no_info_other: "

    %{name} încă nu a introdus nimic în câmpul Despre mine din profilul său
    " @@ -479,6 +504,8 @@ ro: other: "cu aproape %{count} de ani în urmă" password_reset: no_token: "Ne pare rău, dar link-ul de schimbare a parolei este prea vechi. Selectează butonul Înregistrare și folosește 'Mi-am uitat parola' pentru a obține un nou link." + choose_new: "Alege o parolă nouă" + choose: "Alege o parolă" update: 'Actualizează parola' save: 'Setează parola' title: 'Resetează parola' @@ -501,6 +528,13 @@ ro: welcome_to: "Bine ai venit pe %{site_name}!" approval_required: "Un moderator trebuie să aprobe manual contul înainte să poți accesa forumul. Vei primi un email imediat ce contul ți-a fost aprobat!" missing_session: "Nu putem detecta dacă contul tău a fost deja creat. Te rugăm să verifici că ai cookie-urile activate. " + activated: "Acest cont a fost deja activat." + admin_confirm: + title: "Confirmă contul de Administrator" + description: "Eșți sigur că vrei ca %{target_username} să fie administrator?" + grant: "Acordă titlu de admin" + complete: "%{target_username} este administrator." + back_to: "Întoarce-te la %{title}" post_action_types: off_topic: title: 'În afara subiectului' @@ -1519,11 +1553,6 @@ ro: Totuși, dacă postarea este ascunsă de comunitate pentru a doua oară, va rămâne așa până ce va fi gestionată de un membru al echipei -- și asta ar putea atrage și alte consecințe, inclusiv posibilitatea suspendării contului tău.. Pentru informații suplimentare, te rugăm să citești [ghidul comunității](%{base_url}/guidelines). - usage_tips: - text_body_template: | - Citește câteva sfaturi rapide destinate noilor utilizatori, [urmărind această postare pe blog](http://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/). - - Pe măsură ce participi aici, vom ajunge să te cunoaștem iar limitările temporare aferente unui utilizator nou, îți vor fi ridicate. Cu timpul vei câștiga [niveluri de încredere](https://meta.discourse.org/t/what-do-user-trust-levels-do/4924) care includ abilități speciale și care îți vor da posibilitatea să ne ajuți să ne gestionăm împreuna comunitatea. welcome_user: subject_template: "Bine ai venit pe %{site_name}!" text_body_template: | @@ -2497,7 +2526,6 @@ ro: safe_mode: title: "Activează modul sigur" description: "Modul sigur îți permite să îți testezi site-ul fără să încarci plugin-uri și personalizări." - no_customizations: "Dezactivează toate personalizările site-ului" only_official: "Dezactivează plugin-urile neoficiale" no_plugins: "Dezactivează toate plugin-urile" enter: "Activează modul sigur" diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index eaa26683b0..6219c616f8 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -27,7 +27,6 @@ ru: topics: "Темы" posts: "сообщения" loading: "Загрузка..." - powered_by_html: 'При поддержке Discourse, лучше всего использовать с включенным JavaScript' log_in: "Войти" purge_reason: "Деактивированная учетная запись будет автоматически удалена как заброшенная" disable_remote_images_download_reason: "Загрузка картинок была отключена из-за недостаточности места на диске." @@ -1677,7 +1676,6 @@ ru: safe_mode: title: "Войти в Безопасный Режим" description: "Безопасный режим позволяет Вам проверить работу Вашего сайта без загрузки плагинов и настроек сайта." - no_customizations: "Отключить все настройки сайта" only_official: "Отключить неофициальные плагины" no_plugins: "Отключить все плагины" enter: "Войти в Безопасный Режим" diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml index 5c3f44ec89..69b1ed8750 100644 --- a/config/locales/server.sk.yml +++ b/config/locales/server.sk.yml @@ -26,7 +26,6 @@ sk: topics: "Témy" posts: "príspevky" loading: "Načítava sa" - powered_by_html: 'Systém beží na Discourse, najlepšie funguje so zapnutým JavaScriptom' log_in: "Prihlásenie" purge_reason: "Automaticky zmazaný ako opustený, dezaktivovaný účet" disable_remote_images_download_reason: "Sťahovanie vzdialených obrázkov je vypnuté kvôli nedostatku diskového priestoru." diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml index 38919670cb..339010a344 100644 --- a/config/locales/server.sq.yml +++ b/config/locales/server.sq.yml @@ -27,7 +27,6 @@ sq: topics: "Tema" posts: "postime" loading: "Loading" - powered_by_html: 'Mundësuar nga Discourse, për një eksperience më të mirë aktivizoni JavaScript' log_in: "Identifikohu" purge_reason: "Automatically deleted as abandoned, deactivated account" disable_remote_images_download_reason: "Remote images download was disabled because there wasn't enough disk space available." diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index 339eba2314..e192c2047c 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -27,7 +27,6 @@ sv: topics: "Ämnen" posts: "inlägg" loading: "Laddar" - powered_by_html: 'Drivs av Discourse, visas bäst med JavaScript aktiverat' log_in: "Logga in" purge_reason: "Automatiskt borttaget som övergett, avaktiverat konto" disable_remote_images_download_reason: "Nedladdning av externa bilder är inaktiverat eftersom det inte fanns tillräckligt mycket lagringsutrymme tillgängligt." @@ -2178,7 +2177,6 @@ sv: safe_mode: title: "ange felsäkert läge" description: "I felsäkert läge kan du testa din webplats utan att ladda ner några plugins eller anpassade webplatser." - no_customizations: "Inaktivera alla webplats inställningar" only_official: "Inaktivera alla inofficiella plugins" no_plugins: "Inaktivera alla plugins" enter: "Starta felsäkert läge" diff --git a/config/locales/server.te.yml b/config/locales/server.te.yml index 995795f619..5d6a8fe2d4 100644 --- a/config/locales/server.te.yml +++ b/config/locales/server.te.yml @@ -14,7 +14,6 @@ te: topics: "విషయాలు" posts: "టపాలు" loading: "లోడవుతోంది" - powered_by_html: ' డిస్కౌర్స్ చేత శక్తివంతం చేయబడింది, జావాస్క్రిప్ట్ చేతనం చేస్తే బాగా కనిపిస్తుంది. ' log_in: "లాగిన్" disable_remote_images_download_reason: "సుదూర బొమ్మల దిగుమతి అచేతనమైంది ఎందుకంటే డిస్క్ జాగా తక్కువగా ఉంది." errors: &errors diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index 972c15aa00..6ea92de572 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -27,7 +27,6 @@ tr_TR: topics: "Konular" posts: "gönderiler" loading: "Yükleniyor" - powered_by_html: 'Gücünü Discourse''tan alır, en iyi görünüm için JavaScript etkinleştirmeniz gerekir' log_in: "Giriş Yap" purge_reason: "terkedilmiş, edilgen hesap olarak otomatik olarak silindi" disable_remote_images_download_reason: "Yeterli disk alanı kalmaması sebebiyle uzaktan görüntü indirme devre dışı bırakıldı." @@ -1817,7 +1816,6 @@ tr_TR: message: "

    %{email} adresine etkinleştirme e-postasını tekrar gönderdik" safe_mode: title: "Güvenli moda gir" - no_customizations: "Tüm site özelleştirmelerini devre dışı bırak" no_plugins: "Tüm eklentileri devre dışı bırak" enter: "Güvenli Moda Gir" wizard: diff --git a/config/locales/server.uk.yml b/config/locales/server.uk.yml index d906b08592..4210ce8eb8 100644 --- a/config/locales/server.uk.yml +++ b/config/locales/server.uk.yml @@ -14,7 +14,6 @@ uk: topics: "Теми" posts: "дописи" loading: "Завантаження" - powered_by_html: 'Створено за допомогою технології Discourse, бажано переглядати з увімкненим JavaScript' log_in: "Вхід" emails: incoming: diff --git a/config/locales/server.vi.yml b/config/locales/server.vi.yml index 22406bb7f2..e7eb9ef580 100644 --- a/config/locales/server.vi.yml +++ b/config/locales/server.vi.yml @@ -26,7 +26,6 @@ vi: topics: "Chủ đề" posts: "bài viết" loading: "Đang tải" - powered_by_html: 'Được hỗ trợ bởi Discourse, xem tốt nhất khi JavaScript được kích hoạt' log_in: "Đăng nhập" disable_remote_images_download_reason: "Không thể tải ảnh về máy chủ vì thiếu dung lượng." anonymous: "Ẩn danh" diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index b8a6fadf9b..21219b2cbe 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -27,7 +27,6 @@ zh_CN: topics: "主题列表" posts: "帖子" loading: "载入中" - powered_by_html: 'Powered by Discourse,启用JavaScript获致最佳浏览' log_in: "登录" purge_reason: "自动删除废弃,停用账户" disable_remote_images_download_reason: "磁盘空间不足,远程图像下载已经被停用。" @@ -1486,8 +1485,6 @@ zh_CN: 然而,如果帖子再次被社群成员标记并隐藏,它将被隐藏至版主处理后——并且可能导致进一步的措施,如封禁帐号。 想了解更多,请查看我们的[社群指引](%{base_url}/guidelines)。 - usage_tips: - text_body_template: "要查看给新用户的简要技巧,[(英文)看看这篇博客文章](http://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/)。\n\n只要你参与,我们将更了解你,并且新用户的临时限制将被移除。\b随着时间,你将获得[信任等级](https://meta.discourse.org/t/what-do-user-trust-levels-do/4924),这将提供一些特殊功能来帮助我们更好地管理社群。\n" welcome_user: title: "欢迎用户" subject_template: "欢迎来到 %{site_name}!" @@ -2504,7 +2501,6 @@ zh_CN: safe_mode: title: "进入安全模式" description: "安全模式让你的站点不载入插件或站点自定义设置。" - no_customizations: "禁止所以站点自定义" only_official: "禁用非官方插件" no_plugins: "禁用所以插件" enter: "进入安全模式" diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index d68c2f5f74..5fb8cca8ca 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -27,7 +27,7 @@ zh_TW: topics: "討論話題" posts: "文章" loading: "載入中" - powered_by_html: 'Powered by Discourse, 請啟用 Javascript 以獲得最佳瀏覽效果' + powered_by_html: 'Powered by Discourse, 建議開啟 JavaScript 瀏覽' log_in: "登入" purge_reason: "自動當成遺棄、未啟動的帳戶刪除" disable_remote_images_download_reason: "磁盤空間不足,圖像下載已被禁用。" @@ -1561,8 +1561,6 @@ zh_TW: 然而,如果帖子再次被社群成員標記並隱藏,它將被隱藏至版主處理後——並且可能導致進一步的措施,如封禁帳號。 想瞭解更多,請查看我們的[社群指引](%{base_url}/guidelines)。 - usage_tips: - text_body_template: "要查看給新用戶的簡要技巧,[(英文)看看這篇博客文章](http://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/)。\n\n只要你參與,我們將更瞭解你,並且新用戶的臨時限制將被移除。\b隨着時間,你將獲得[信任等級](https://meta.discourse.org/t/what-do-user-trust-levels-do/4924),這將提供一些特殊功能來幫助我們更好地管理社群。\n" welcome_user: title: "歡迎使用者" subject_template: "歡迎來到 %{site_name}!" @@ -2625,7 +2623,6 @@ zh_TW: safe_mode: title: "進入安全模式" description: "安全模式讓你的站點不載入插件或站點自定義設置。" - no_customizations: "禁止所以站點自定義" only_official: "禁用非官方插件" no_plugins: "禁用所以插件" enter: "進入安全模式" diff --git a/plugins/poll/config/locales/client.fa_IR.yml b/plugins/poll/config/locales/client.fa_IR.yml index 064c381502..37f31d3787 100644 --- a/plugins/poll/config/locales/client.fa_IR.yml +++ b/plugins/poll/config/locales/client.fa_IR.yml @@ -15,6 +15,15 @@ fa_IR: average_rating: "میانگین امتیاز: %{average}." public: title: "آرا عمومی هستند" + multiple: + help: + at_least_min_options: + other: "حداقل تعداد %{count} گزینه انتخاب کنید." + up_to_max_options: + other: "حداکثر %{count} گزینه انتخاب کنید" + x_options: + other: "تعداد %{count} گزینه انتخاب کنید" + between_min_and_max_options: "بین %{min} تا %{max} گزینه انتخاب کنید" cast-votes: title: "انداختن رأی شما" label: "رای بدهید!" @@ -32,15 +41,25 @@ fa_IR: title: "نظرسنجی را ببند" label: "بسته " confirm: "آیا از بستن این نظرسنجی اطمینان دارید ؟ " + error_while_toggling_status: "متاسفانه در تغییر وضعیت این نظرسنجی، خطایی رخ داده است." + error_while_casting_votes: "متاسفانه در ثبت رای شما خطایی رخ داده است." + error_while_fetching_voters: "متاسفانه در نمایش رای دهندگان خطایی رخ داده است." ui_builder: title: ساخت نظرسنجی + insert: درج نظر سنجی + help: + options_count: حداقل 2 گزینه وارد کنید + invalid_values: مقدار حداقل می بایست کمتر از مقدار حداکثر باشد. poll_type: label: نوع regular: یک انتخاب multiple: انتخاب چندگانه + number: تعداد امتیاز poll_config: max: بیشترین min: کمترین step: مرحله poll_public: label: نشان دادن رای دهندگان + poll_options: + label: در هر خط یک گزینه نظرسنجی را وارد کنید diff --git a/plugins/poll/config/locales/server.fa_IR.yml b/plugins/poll/config/locales/server.fa_IR.yml index a13b8de965..48858b5199 100644 --- a/plugins/poll/config/locales/server.fa_IR.yml +++ b/plugins/poll/config/locales/server.fa_IR.yml @@ -9,6 +9,7 @@ fa_IR: site_settings: poll_enabled: "به کاربران اجازه ساخت نظرسنجی ها را بده ؟ " poll_maximum_options: "Maximum number of options allowed in a poll." + poll_edit_window_mins: "چند دقیقه بعد از ارسال نظر می توان آن را ویرایش کرد" poll: multiple_polls_without_name: "چند نظرسنجی متفاوت بدون اسم است. استفاده کن از 'اسم/code>' ویژگی ٬ تا نظرسنجی منحصر به فرد را تشخیص دهد." multiple_polls_with_same_name: "چند نظرسنجی با اسم برابر وجود دارند: %{name}.استفاده کن از 'اسم/code>' ویژگی ٬ تا نظرسنجی منحصر به فرد را تشخیص دهد." @@ -23,6 +24,7 @@ fa_IR: default_poll_with_multiple_choices_has_invalid_parameters: "در نظرسنجی چند گزینه‌ای پارامترهای نامعتبری وجود دارد." named_poll_with_multiple_choices_has_invalid_parameters: "در نظرسنجی چند گزینه‌ای با نام %{name} پارامترهای نامعتبری وجود دارد." requires_at_least_1_valid_option: "You must select at least 1 valid option." + default_cannot_be_made_public: "نظر سنجی با رای را نمی توان عمومی کرد" no_polls_associated_with_this_post: "هیچ نظرسنجی با این نوشته در تماس نیستند. " no_poll_with_this_name: "هیچ نظرسنجی اسم گزاری نشده %{name} در تماس به این نوشته نیست. " post_is_deleted: "این کار را نمی‌توان روی یک نوشته حذف شده انجام داد." diff --git a/public/500.fa_IR.html b/public/500.fa_IR.html index cb0254098b..e38f27ec62 100644 --- a/public/500.fa_IR.html +++ b/public/500.fa_IR.html @@ -7,6 +7,8 @@

    اِی بابا

    نرم‌افزاری که ا به ین انجمن گفتگو را قدرت می‌دهد، با مشکلی غیرمنتظره روبرو شده است. از اینکه به زحمت افتادید پوزش میخواهیم.

    اطلاعات کامل در مورد این خطا، به سیستم ارسال شد و اطلاع رسانی به صورت خودکار تولید شد. ما آن را برسی می نماییم.

    -

    No further action is necessary. However, if the error condition persists, you can provide additional detail, including steps to reproduce the error, by posting a discussion topic in the site's feedback category.

    +

    هیچ اقدام دیگری لازم نیست انجام دهید . با این وجود اگر خطا + همچنان باقی است , شما می توانید جزئیات آن را به همراه گام +های لازم برای تولید خطایی مثل این ,در یک موضوع جدید در دسته بازخوردهای سایت ارسال نمایید .

    From 4e1f8ec72b791006fca7c6c3347c8347d27e4dc6 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 24 Apr 2017 13:21:50 -0400 Subject: [PATCH 162/221] FIX: Without suggested topics jumping to a post had the wrong offset --- app/assets/javascripts/discourse/lib/offset-calculator.js.es6 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/offset-calculator.js.es6 b/app/assets/javascripts/discourse/lib/offset-calculator.js.es6 index 92dd68dea5..3c2fc1fc71 100644 --- a/app/assets/javascripts/discourse/lib/offset-calculator.js.es6 +++ b/app/assets/javascripts/discourse/lib/offset-calculator.js.es6 @@ -33,12 +33,11 @@ export default function offsetCalculator(y) { } - if (inter > ideal) { const bottom = $('#topic-bottom').offset().top; const switchPos = bottom - rawWinHeight; if (scrollTop > switchPos) { - const p = Math.max(Math.min((scrollTop + inter - switchPos) / rawWinHeight, 1.0), 0.0); + const p = Math.max(Math.min((scrollTop + inter - switchPos) / bottom, 1.0), 0.0); return ((1 - p) * ideal) + (p * inter); } else { return ideal; From 62966b102338a9e54a231d1b3ab4a70b45e23efc Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 24 Apr 2017 13:36:05 -0400 Subject: [PATCH 163/221] FIX: Remove duplicate route warning, it's just a noop now --- .../javascripts/discourse/mapping-router.js.es6 | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/app/assets/javascripts/discourse/mapping-router.js.es6 b/app/assets/javascripts/discourse/mapping-router.js.es6 index 6d30be749c..140ea94610 100644 --- a/app/assets/javascripts/discourse/mapping-router.js.es6 +++ b/app/assets/javascripts/discourse/mapping-router.js.es6 @@ -72,21 +72,10 @@ class RouteNode { if (this.name === 'root') { children.forEach(c => c.mapRoutes(router)); } else { - const builder = (children.length === 0) ? undefined : function() { children.forEach(c => c.mapRoutes(this)); }; router.route(this.name, this.opts, builder); - - // We can have multiple paths to the same route - const paths = Object.keys(this.paths); - if (paths.length > 1) { - paths.filter(p => p !== this.opts.path).forEach(path => { - const newOpts = jQuery.extend({}, this.opts, { path }); - console.log(`warning: we can't have duplicate route names anymore`, newOpts); - // router.route(this.name, newOpts, builder); - }); - } } } From d5630d616094f6034816ada2e64125729da5ebd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 24 Apr 2017 22:01:41 +0200 Subject: [PATCH 164/221] HtmlToMarkdown library Small library to transform HTML to Discourse-flavored markdown (mostly used for imports) --- lib/html_to_markdown.rb | 197 +++++++++++++++++++++++ spec/components/html_to_markdown_spec.rb | 169 +++++++++++++++++++ 2 files changed, 366 insertions(+) create mode 100644 lib/html_to_markdown.rb create mode 100644 spec/components/html_to_markdown_spec.rb diff --git a/lib/html_to_markdown.rb b/lib/html_to_markdown.rb new file mode 100644 index 0000000000..820ebd0209 --- /dev/null +++ b/lib/html_to_markdown.rb @@ -0,0 +1,197 @@ +require "nokogiri" + +class HtmlToMarkdown + + class Block < Struct.new(:name, :head, :body, :opened, :markdown) + def initialize(name, head="", body="", opened=false, markdown=""); super; end + end + + def initialize(html) + @doc = Nokogiri::HTML.fragment(html) + remove_whitespaces! + puts @doc.to_html + end + + def remove_whitespaces! + @doc.traverse do |node| + if node.is_a? Nokogiri::XML::Text + node.content = node.content.lstrip if node.previous_element&.description&.block? + node.content = node.content.lstrip if node.previous_element.nil? && node.parent.description&.block? + node.content = node.content.rstrip if node.next_element&.description&.block? + node.content = node.content.rstrip if node.next_element.nil? && node.parent.description&.block? + node.remove if node.content.empty? + end + end + end + + def to_markdown + @stack = [Block.new("root")] + @markdown = "" + traverse(@doc) + @markdown << format_block + @markdown.gsub(/\n{3,}/, "\n\n").strip + end + + def traverse(node) + node.children.each { |node| visit(node) } + end + + def visit(node) + if node.description&.block? && node.parent&.description&.block? && @stack[-1].markdown.size > 0 + block = @stack[-1].dup + @markdown << format_block + block.markdown = "" + block.opened = true + @stack << block + end + + visitor = "visit_#{node.name}" + respond_to?(visitor) ? send(visitor, node) : traverse(node) + end + + BLACKLISTED ||= %w{button datalist fieldset form input label legend meter optgroup option output progress select textarea style script} + BLACKLISTED.each do |tag| + class_eval <<-RUBY + def visit_#{tag}(node) + "" + end + RUBY + end + + def visit_pre(node) + code = node.children.find { |c| c.name == "code" } + code_class = code ? code["class"] : "" + lang = code_class ? code_class[/lang-(\w+)/, 1] : "" + @stack << Block.new("pre") + @markdown << "```#{lang}\n" + traverse(node) + @markdown << format_block + @markdown << "```\n" + end + + def visit_blockquote(node) + @stack << Block.new("blockquote", "> ", "> ") + traverse(node) + @markdown << format_block + end + + BLOCK_WITH_NEWLINE ||= %w{div p} + BLOCK_WITH_NEWLINE.each do |tag| + class_eval <<-RUBY + def visit_#{tag}(node) + @stack << Block.new("#{tag}") + traverse(node) + @markdown << format_block + @markdown << "\n" + end + RUBY + end + + BLOCK_LIST ||= %w{menu ol ul} + BLOCK_LIST.each do |tag| + class_eval <<-RUBY + def visit_#{tag}(node) + @stack << Block.new("#{tag}") + traverse(node) + @markdown << format_block + end + RUBY + end + + def visit_li(node) + parent = @stack.reverse.find { |n| n.name[/ul|ol|menu/] } + prefix = parent.name == "ol" ? "1. " : "- " + @stack << Block.new("li", prefix, " ") + traverse(node) + @markdown << format_block + end + + (1..6).each do |n| + class_eval <<-RUBY + def visit_h#{n}(node) + @stack << Block.new("h#{n}", "#" * #{n} + " ") + traverse(node) + @markdown << format_block + end + RUBY + end + + WHITELISTED ||= %w{del ins kbd s small strike sub sup table tbody td tfoot th thead tr} + WHITELISTED.each do |tag| + class_eval <<-RUBY + def visit_#{tag}(node) + @stack[-1].markdown << "<#{tag}>" + traverse(node) + @stack[-1].markdown << "" + end + RUBY + end + + def visit_abbr(node) + @stack[-1].markdown << (node["title"].present? ? %Q[] : "") + traverse(node) + @stack[-1].markdown << "" + end + + def visit_img(node) + title = node["alt"].presence || node["title"].presence + @stack[-1].markdown << "![#{title}](#{node["src"]})" + end + + def visit_a(node) + @stack[-1].markdown << "[" + traverse(node) + @stack[-1].markdown << "](#{node["href"]})" + end + + def visit_tt(node) + @stack[-1].markdown << "`" + traverse(node) + @stack[-1].markdown << "`" + end + + def visit_code(node) + @stack.reverse.find { |n| n.name["pre"] } ? traverse(node) : visit_tt(node) + end + + def visit_br(node) + @stack[-1].markdown << "\n" + end + + def visit_hr(node) + @stack[-1].markdown << "\n\n---\n\n" + end + + def visit_strong(node) + delimiter = node.text["*"] ? "__" : "**" + @stack[-1].markdown << delimiter + traverse(node) + @stack[-1].markdown << delimiter + end + + alias :visit_b :visit_strong + + def visit_em(node) + delimiter = node.text["*"] ? "_" : "*" + @stack[-1].markdown << delimiter + traverse(node) + @stack[-1].markdown << delimiter + end + + alias :visit_i :visit_em + + def visit_text(node) + @stack[-1].markdown << node.text.gsub(/\s{2,}/, " ") + end + + def format_block + lines = @stack[-1].markdown.each_line.map do |line| + prefix = @stack.map { |b| b.opened ? b.body : b.head }.join + @stack.each { |b| b.opened = true } + prefix + line.rstrip + end + @stack.pop + (lines + [""]).join("\n") + end + +end diff --git a/spec/components/html_to_markdown_spec.rb b/spec/components/html_to_markdown_spec.rb new file mode 100644 index 0000000000..f71c0e0efc --- /dev/null +++ b/spec/components/html_to_markdown_spec.rb @@ -0,0 +1,169 @@ +require 'rails_helper' +require 'html_to_markdown' + +describe HtmlToMarkdown do + + def html_to_markdown(html) + HtmlToMarkdown.new(html).to_markdown + end + + it "converts " do + expect(html_to_markdown("Strong")).to eq("**Strong**") + expect(html_to_markdown("Str*ng")).to eq("__Str*ng__") + end + + it "converts " do + expect(html_to_markdown("Bold")).to eq("**Bold**") + expect(html_to_markdown("B*ld")).to eq("__B*ld__") + end + + it "converts " do + expect(html_to_markdown("Emphasis")).to eq("*Emphasis*") + expect(html_to_markdown("Emph*sis")).to eq("_Emph*sis_") + end + + it "converts " do + expect(html_to_markdown("Italic")).to eq("*Italic*") + expect(html_to_markdown("It*lic")).to eq("_It*lic_") + end + + it "converts " do + expect(html_to_markdown(%Q{Discourse})).to eq("[Discourse](https://www.discourse.org)") + end + + it "converts " do + expect(html_to_markdown(%Q{Discourse Logo})).to eq("![Discourse Logo](https://www.discourse.org/logo.svg)") + end + + (1..6).each do |n| + it "converts " do + expect(html_to_markdown("Header #{n}")).to eq("#" * n + " Header #{n}") + end + end + + it "converts
    " do + expect(html_to_markdown("Before
    Inside
    After")).to eq("Before\nInside\nAfter") + end + + it "converts
    " do + expect(html_to_markdown("Before
    Inside
    After")).to eq("Before\n\n---\n\nInside\n\n---\n\nAfter") + end + + it "converts " do + expect(html_to_markdown("Teletype")).to eq("`Teletype`") + end + + it "converts " do + expect(html_to_markdown("Code")).to eq("`Code`") + end + + it "supports " do + expect(html_to_markdown("This is an insertion")).to eq("This is an insertion") + end + + it "supports " do + expect(html_to_markdown("This is a deletion")).to eq("This is a deletion") + end + + it "supports " do + expect(html_to_markdown("H2O")).to eq("H2O") + end + + it "supports " do + expect(html_to_markdown("Super Script!")).to eq("Super Script!") + end + + it "supports " do + expect(html_to_markdown("Small")).to eq("Small") + end + + it "supports " do + expect(html_to_markdown("CTRL+C")).to eq("CTRL+C") + end + + it "supports " do + expect(html_to_markdown(%Q{CDCK})).to eq(%Q{CDCK}) + end + + it "supports " do + expect(html_to_markdown("Strike Through")).to eq("Strike Through") + end + + it "supports " do + expect(html_to_markdown("Strike Through")).to eq("Strike Through") + end + + it "supports
    " do + expect(html_to_markdown("
    Quote
    ")).to eq("> Quote") + end + + it "supports