diff --git a/app/assets/javascripts/discourse/templates/components/date-picker.hbs b/app/assets/javascripts/discourse/templates/components/date-picker.hbs
index a2c89401ab..d49379d954 100644
--- a/app/assets/javascripts/discourse/templates/components/date-picker.hbs
+++ b/app/assets/javascripts/discourse/templates/components/date-picker.hbs
@@ -1 +1 @@
-
+{{input type="text" class="date-picker" placeholder=placeholder}}
diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs
index e1fec09080..ddfb1ce11b 100644
--- a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs
+++ b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs
@@ -1,10 +1,15 @@
diff --git a/app/assets/javascripts/discourse/templates/components/featured-topic.hbs b/app/assets/javascripts/discourse/templates/components/featured-topic.hbs
index e84b8bf4e7..7061778b8e 100644
--- a/app/assets/javascripts/discourse/templates/components/featured-topic.hbs
+++ b/app/assets/javascripts/discourse/templates/components/featured-topic.hbs
@@ -1,4 +1,4 @@
-{{topic-status topic=topic}}
+{{raw "topic-status" topic=topic}}
{{{unbound topic.fancyTitle}}}
{{topic-post-badges newPosts=topic.totalUnread unseen=topic.unseen url=topic.lastUnreadUrl}}
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/components/mobile-category-topic.hbs b/app/assets/javascripts/discourse/templates/components/mobile-category-topic.hbs
index c3827841dc..6cc45d2f1e 100644
--- a/app/assets/javascripts/discourse/templates/components/mobile-category-topic.hbs
+++ b/app/assets/javascripts/discourse/templates/components/mobile-category-topic.hbs
@@ -1,6 +1,6 @@
- {{topic-status topic=topic}}
+ {{raw "topic-status" topic=topic}}
{{topic-link topic}}
{{#if topic.unseen}}
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 @@
{{/d-modal-body}}
diff --git a/app/assets/javascripts/discourse/templates/modal/edit-topic-status-update.hbs b/app/assets/javascripts/discourse/templates/modal/edit-topic-status-update.hbs
index 8fca98f413..c1039fd9b4 100644
--- a/app/assets/javascripts/discourse/templates/modal/edit-topic-status-update.hbs
+++ b/app/assets/javascripts/discourse/templates/modal/edit-topic-status-update.hbs
@@ -1,59 +1,14 @@
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}}
diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6
index 3e9979cad1..890a7a7b4a 100644
--- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6
+++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6
@@ -163,22 +163,22 @@ export default createWidget('hamburger-menu', {
const prioritizeFaq = this.currentUser && !this.currentUser.read_faq;
if (prioritizeFaq) {
- results.push(this.attach('menu-links', { heading: true, contents: () => {
+ results.push(this.attach('menu-links', { name: 'faq-link', heading: true, contents: () => {
return this.attach('priority-faq-link', { href: faqUrl });
}}));
}
if (currentUser && currentUser.staff) {
- results.push(this.attach('menu-links', { contents: () => {
+ results.push(this.attach('menu-links', { name: 'admin-links', contents: () => {
const extraLinks = flatten(applyDecorators(this, 'admin-links', this.attrs, this.state));
return this.adminLinks().concat(extraLinks);
}}));
}
- results.push(this.attach('menu-links', { contents: () => this.generalLinks() }));
+ results.push(this.attach('menu-links', {name: 'general-links', contents: () => this.generalLinks() }));
results.push(this.listCategories());
results.push(h('hr'));
- results.push(this.attach('menu-links', { omitRule: true, contents: () => this.footerLinks(prioritizeFaq, faqUrl) }));
+ results.push(this.attach('menu-links', {name: 'footer-links', omitRule: true, contents: () => this.footerLinks(prioritizeFaq, faqUrl) }));
return results;
},
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
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/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/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/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss
index 0fba7c199b..93878dadb6 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
@@ -551,6 +553,9 @@ section.details {
.display-row.associations .value {
width: 750px;
+ @media (max-width: $mobile-breakpoint) {
+ width: 100%;
+ }
}
.display-row {
@@ -596,6 +601,9 @@ section.details {
width: 480px;
float: left;
margin-left: 12px;
+ @media (max-width: $mobile-breakpoint) {
+ width: 100%;
+ }
.btn {
margin-right: 5px;
}
@@ -724,138 +732,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 {
@@ -1949,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 {
@@ -1970,6 +1846,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..e03ecb6b60
--- /dev/null
+++ b/app/assets/stylesheets/common/admin/customize.scss
@@ -0,0 +1,191 @@
+// Customise area
+.customize {
+ h1 {
+ margin-bottom: 10px;
+ input {
+ margin-bottom: 0;
+ }
+ }
+
+ .field-error {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ background-color: dark-light-diff($quaternary, $secondary, 70%, -70%);
+ padding: 5px;
+ }
+
+ .edit-main-nav {
+ .nav-pills:after, .nav-pills:before {
+ display: inline;
+ content: "";
+ }
+ .show-overidden {
+ float: right;
+ }
+ margin-bottom: 10px;
+ }
+
+ .admin-container {
+ padding-left: 10px;
+ padding-right: 10px;
+ }
+ .admin-footer {
+ margin-top: 20px;
+ }
+ .select2-chosen, .color-schemes li {
+ .fa {
+ margin-right: 6px;
+ color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%));
+ }
+ }
+ .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 {
+ li {
+ position: relative;
+ }
+ margin-top: 15px;
+ .fa {
+ margin-left: 3px;
+ }
+ li.mobile a {
+ padding-right: 25px;
+ }
+ .fa-mobile {
+ position: absolute;
+ right: 10px;
+ top: 3px;
+ font-size: 1.5em;
+ }
+ }
+
+ .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%;
+ }
+ }
+
+ .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/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%);
}
diff --git a/app/assets/stylesheets/common/base/edit-topic-status-update-modal.scss b/app/assets/stylesheets/common/base/edit-topic-status-update-modal.scss
new file mode 100644
index 0000000000..65b06ed8f4
--- /dev/null
+++ b/app/assets/stylesheets/common/base/edit-topic-status-update-modal.scss
@@ -0,0 +1,42 @@
+.edit-topic-status-update-modal {
+ .modal-body {
+ max-height: none;
+ }
+
+ input.date-picker, input[type="time"] {
+ width: 150px;
+ text-align: left;
+ }
+
+ label {
+ display: inline-block;
+ }
+
+ .btn.pull-right {
+ margin-right: 10px;
+ }
+
+ .auto-update-input {
+ input {
+ margin: 0;
+ }
+
+ .alert-info {
+ margin: 0 -15px -15px -15px;
+ }
+
+ .pika-single {
+ position: relative !important;
+ }
+
+ .topic-status-info {
+ border: none;
+ padding: 0;
+
+ h3 {
+ font-weight: normal;
+ font-size: 15px;
+ }
+ }
+ }
+}
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 {
diff --git a/app/assets/stylesheets/common/base/topic-close-modal.scss b/app/assets/stylesheets/common/base/topic-close-modal.scss
deleted file mode 100644
index 0a638b6f81..0000000000
--- a/app/assets/stylesheets/common/base/topic-close-modal.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-.topic-close-modal {
- label {
- display: inline-block;
- }
-
- .radios {
- padding-bottom: 20px;
- display: inline-block;
-
- input[type='radio'] {
- vertical-align: middle;
- margin: 0px;
- }
-
- label {
- padding: 0 10px 0px 5px;
- }
- }
-
- .btn.pull-right {
- margin-right: 10px;
- }
-
- .auto-update-input {
- input {
- margin: 0;
- }
- }
-}
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;
}
diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss
index 6c7fbfbb51..911c461f15 100644
--- a/app/assets/stylesheets/common/base/user.scss
+++ b/app/assets/stylesheets/common/base/user.scss
@@ -1,4 +1,3 @@
-
// styling of bottom section
.user-stream .child-actions {
margin-top: 8px;
diff --git a/app/assets/stylesheets/common/components/auto-update-input-selector.scss b/app/assets/stylesheets/common/components/auto-update-input-selector.scss
new file mode 100644
index 0000000000..65207f3c1c
--- /dev/null
+++ b/app/assets/stylesheets/common/components/auto-update-input-selector.scss
@@ -0,0 +1,9 @@
+.auto-update-input-selector-datetime {
+ float: right;
+ color: lighten($primary, 40%);
+ font-size: 13px;
+}
+
+.auto-update-input-selector-icons {
+ margin-right: 10px;
+}
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 96%
rename from app/assets/stylesheets/common/components/banner.css.scss
rename to app/assets/stylesheets/common/components/banner.scss
index 7faca616cb..5fe427df04 100644
--- a/app/assets/stylesheets/common/components/banner.css.scss
+++ b/app/assets/stylesheets/common/components/banner.scss
@@ -20,6 +20,7 @@
margin-top: -5px;
color: scale-color($tertiary, $lightness: 70%);
padding-left: 5px;
+ float: right;
}
.meta {
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/common/printer-friendly.scss b/app/assets/stylesheets/common/printer-friendly.scss
index 6d62eb40d8..d640737b47 100644
--- a/app/assets/stylesheets/common/printer-friendly.scss
+++ b/app/assets/stylesheets/common/printer-friendly.scss
@@ -16,7 +16,7 @@
.show-topic-admin,
#topic-progress,
.quote-controls,
- #topic-status-info,
+ .topic-status-info,
div.lazyYT,
.post-info.edits,
.post-action,
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/desktop/topic.scss b/app/assets/stylesheets/desktop/topic.scss
index 59d297ac47..bd4b0c9f5f 100644
--- a/app/assets/stylesheets/desktop/topic.scss
+++ b/app/assets/stylesheets/desktop/topic.scss
@@ -79,7 +79,7 @@
}
}
-#topic-status-info {
+.topic-status-info {
border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -75%);
padding-top: 10px;
height: 20px;
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/mobile/topic.scss b/app/assets/stylesheets/mobile/topic.scss
index 4365009a6a..7e0fc0cac6 100644
--- a/app/assets/stylesheets/mobile/topic.scss
+++ b/app/assets/stylesheets/mobile/topic.scss
@@ -43,7 +43,7 @@
clear: both;
}
-#topic-status-info {
+.topic-status-info {
margin-left: 10px;
}
diff --git a/app/assets/stylesheets/vendor/font_awesome/_icons.scss b/app/assets/stylesheets/vendor/font_awesome/_icons.scss
index 6f9375989a..e63e702c4d 100644
--- a/app/assets/stylesheets/vendor/font_awesome/_icons.scss
+++ b/app/assets/stylesheets/vendor/font_awesome/_icons.scss
@@ -438,7 +438,7 @@
.#{$fa-css-prefix}-stumbleupon:before { content: $fa-var-stumbleupon; }
.#{$fa-css-prefix}-delicious:before { content: $fa-var-delicious; }
.#{$fa-css-prefix}-digg:before { content: $fa-var-digg; }
-.#{$fa-css-prefix}-pied-piper:before { content: $fa-var-pied-piper; }
+.#{$fa-css-prefix}-pied-piper-pp:before { content: $fa-var-pied-piper-pp; }
.#{$fa-css-prefix}-pied-piper-alt:before { content: $fa-var-pied-piper-alt; }
.#{$fa-css-prefix}-drupal:before { content: $fa-var-drupal; }
.#{$fa-css-prefix}-joomla:before { content: $fa-var-joomla; }
@@ -488,6 +488,7 @@
.#{$fa-css-prefix}-life-ring:before { content: $fa-var-life-ring; }
.#{$fa-css-prefix}-circle-o-notch:before { content: $fa-var-circle-o-notch; }
.#{$fa-css-prefix}-ra:before,
+.#{$fa-css-prefix}-resistance:before,
.#{$fa-css-prefix}-rebel:before { content: $fa-var-rebel; }
.#{$fa-css-prefix}-ge:before,
.#{$fa-css-prefix}-empire:before { content: $fa-var-empire; }
@@ -604,6 +605,7 @@
.#{$fa-css-prefix}-opencart:before { content: $fa-var-opencart; }
.#{$fa-css-prefix}-expeditedssl:before { content: $fa-var-expeditedssl; }
.#{$fa-css-prefix}-battery-4:before,
+.#{$fa-css-prefix}-battery:before,
.#{$fa-css-prefix}-battery-full:before { content: $fa-var-battery-full; }
.#{$fa-css-prefix}-battery-3:before,
.#{$fa-css-prefix}-battery-three-quarters:before { content: $fa-var-battery-three-quarters; }
@@ -695,3 +697,93 @@
.#{$fa-css-prefix}-bluetooth:before { content: $fa-var-bluetooth; }
.#{$fa-css-prefix}-bluetooth-b:before { content: $fa-var-bluetooth-b; }
.#{$fa-css-prefix}-percent:before { content: $fa-var-percent; }
+.#{$fa-css-prefix}-gitlab:before { content: $fa-var-gitlab; }
+.#{$fa-css-prefix}-wpbeginner:before { content: $fa-var-wpbeginner; }
+.#{$fa-css-prefix}-wpforms:before { content: $fa-var-wpforms; }
+.#{$fa-css-prefix}-envira:before { content: $fa-var-envira; }
+.#{$fa-css-prefix}-universal-access:before { content: $fa-var-universal-access; }
+.#{$fa-css-prefix}-wheelchair-alt:before { content: $fa-var-wheelchair-alt; }
+.#{$fa-css-prefix}-question-circle-o:before { content: $fa-var-question-circle-o; }
+.#{$fa-css-prefix}-blind:before { content: $fa-var-blind; }
+.#{$fa-css-prefix}-audio-description:before { content: $fa-var-audio-description; }
+.#{$fa-css-prefix}-volume-control-phone:before { content: $fa-var-volume-control-phone; }
+.#{$fa-css-prefix}-braille:before { content: $fa-var-braille; }
+.#{$fa-css-prefix}-assistive-listening-systems:before { content: $fa-var-assistive-listening-systems; }
+.#{$fa-css-prefix}-asl-interpreting:before,
+.#{$fa-css-prefix}-american-sign-language-interpreting:before { content: $fa-var-american-sign-language-interpreting; }
+.#{$fa-css-prefix}-deafness:before,
+.#{$fa-css-prefix}-hard-of-hearing:before,
+.#{$fa-css-prefix}-deaf:before { content: $fa-var-deaf; }
+.#{$fa-css-prefix}-glide:before { content: $fa-var-glide; }
+.#{$fa-css-prefix}-glide-g:before { content: $fa-var-glide-g; }
+.#{$fa-css-prefix}-signing:before,
+.#{$fa-css-prefix}-sign-language:before { content: $fa-var-sign-language; }
+.#{$fa-css-prefix}-low-vision:before { content: $fa-var-low-vision; }
+.#{$fa-css-prefix}-viadeo:before { content: $fa-var-viadeo; }
+.#{$fa-css-prefix}-viadeo-square:before { content: $fa-var-viadeo-square; }
+.#{$fa-css-prefix}-snapchat:before { content: $fa-var-snapchat; }
+.#{$fa-css-prefix}-snapchat-ghost:before { content: $fa-var-snapchat-ghost; }
+.#{$fa-css-prefix}-snapchat-square:before { content: $fa-var-snapchat-square; }
+.#{$fa-css-prefix}-pied-piper:before { content: $fa-var-pied-piper; }
+.#{$fa-css-prefix}-first-order:before { content: $fa-var-first-order; }
+.#{$fa-css-prefix}-yoast:before { content: $fa-var-yoast; }
+.#{$fa-css-prefix}-themeisle:before { content: $fa-var-themeisle; }
+.#{$fa-css-prefix}-google-plus-circle:before,
+.#{$fa-css-prefix}-google-plus-official:before { content: $fa-var-google-plus-official; }
+.#{$fa-css-prefix}-fa:before,
+.#{$fa-css-prefix}-font-awesome:before { content: $fa-var-font-awesome; }
+.#{$fa-css-prefix}-handshake-o:before { content: $fa-var-handshake-o; }
+.#{$fa-css-prefix}-envelope-open:before { content: $fa-var-envelope-open; }
+.#{$fa-css-prefix}-envelope-open-o:before { content: $fa-var-envelope-open-o; }
+.#{$fa-css-prefix}-linode:before { content: $fa-var-linode; }
+.#{$fa-css-prefix}-address-book:before { content: $fa-var-address-book; }
+.#{$fa-css-prefix}-address-book-o:before { content: $fa-var-address-book-o; }
+.#{$fa-css-prefix}-vcard:before,
+.#{$fa-css-prefix}-address-card:before { content: $fa-var-address-card; }
+.#{$fa-css-prefix}-vcard-o:before,
+.#{$fa-css-prefix}-address-card-o:before { content: $fa-var-address-card-o; }
+.#{$fa-css-prefix}-user-circle:before { content: $fa-var-user-circle; }
+.#{$fa-css-prefix}-user-circle-o:before { content: $fa-var-user-circle-o; }
+.#{$fa-css-prefix}-user-o:before { content: $fa-var-user-o; }
+.#{$fa-css-prefix}-id-badge:before { content: $fa-var-id-badge; }
+.#{$fa-css-prefix}-drivers-license:before,
+.#{$fa-css-prefix}-id-card:before { content: $fa-var-id-card; }
+.#{$fa-css-prefix}-drivers-license-o:before,
+.#{$fa-css-prefix}-id-card-o:before { content: $fa-var-id-card-o; }
+.#{$fa-css-prefix}-quora:before { content: $fa-var-quora; }
+.#{$fa-css-prefix}-free-code-camp:before { content: $fa-var-free-code-camp; }
+.#{$fa-css-prefix}-telegram:before { content: $fa-var-telegram; }
+.#{$fa-css-prefix}-thermometer-4:before,
+.#{$fa-css-prefix}-thermometer:before,
+.#{$fa-css-prefix}-thermometer-full:before { content: $fa-var-thermometer-full; }
+.#{$fa-css-prefix}-thermometer-3:before,
+.#{$fa-css-prefix}-thermometer-three-quarters:before { content: $fa-var-thermometer-three-quarters; }
+.#{$fa-css-prefix}-thermometer-2:before,
+.#{$fa-css-prefix}-thermometer-half:before { content: $fa-var-thermometer-half; }
+.#{$fa-css-prefix}-thermometer-1:before,
+.#{$fa-css-prefix}-thermometer-quarter:before { content: $fa-var-thermometer-quarter; }
+.#{$fa-css-prefix}-thermometer-0:before,
+.#{$fa-css-prefix}-thermometer-empty:before { content: $fa-var-thermometer-empty; }
+.#{$fa-css-prefix}-shower:before { content: $fa-var-shower; }
+.#{$fa-css-prefix}-bathtub:before,
+.#{$fa-css-prefix}-s15:before,
+.#{$fa-css-prefix}-bath:before { content: $fa-var-bath; }
+.#{$fa-css-prefix}-podcast:before { content: $fa-var-podcast; }
+.#{$fa-css-prefix}-window-maximize:before { content: $fa-var-window-maximize; }
+.#{$fa-css-prefix}-window-minimize:before { content: $fa-var-window-minimize; }
+.#{$fa-css-prefix}-window-restore:before { content: $fa-var-window-restore; }
+.#{$fa-css-prefix}-times-rectangle:before,
+.#{$fa-css-prefix}-window-close:before { content: $fa-var-window-close; }
+.#{$fa-css-prefix}-times-rectangle-o:before,
+.#{$fa-css-prefix}-window-close-o:before { content: $fa-var-window-close-o; }
+.#{$fa-css-prefix}-bandcamp:before { content: $fa-var-bandcamp; }
+.#{$fa-css-prefix}-grav:before { content: $fa-var-grav; }
+.#{$fa-css-prefix}-etsy:before { content: $fa-var-etsy; }
+.#{$fa-css-prefix}-imdb:before { content: $fa-var-imdb; }
+.#{$fa-css-prefix}-ravelry:before { content: $fa-var-ravelry; }
+.#{$fa-css-prefix}-eercast:before { content: $fa-var-eercast; }
+.#{$fa-css-prefix}-microchip:before { content: $fa-var-microchip; }
+.#{$fa-css-prefix}-snowflake-o:before { content: $fa-var-snowflake-o; }
+.#{$fa-css-prefix}-superpowers:before { content: $fa-var-superpowers; }
+.#{$fa-css-prefix}-wpexplorer:before { content: $fa-var-wpexplorer; }
+.#{$fa-css-prefix}-meetup:before { content: $fa-var-meetup; }
diff --git a/app/assets/stylesheets/vendor/font_awesome/_mixins.scss b/app/assets/stylesheets/vendor/font_awesome/_mixins.scss
index 02deee1818..c3bbd5745d 100644
--- a/app/assets/stylesheets/vendor/font_awesome/_mixins.scss
+++ b/app/assets/stylesheets/vendor/font_awesome/_mixins.scss
@@ -12,15 +12,49 @@
}
@mixin fa-icon-rotate($degrees, $rotation) {
- filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})";
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})";
-webkit-transform: rotate($degrees);
-ms-transform: rotate($degrees);
transform: rotate($degrees);
}
@mixin fa-icon-flip($horiz, $vert, $rotation) {
- filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})";
+ -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)";
-webkit-transform: scale($horiz, $vert);
-ms-transform: scale($horiz, $vert);
transform: scale($horiz, $vert);
}
+
+
+// Only display content to screen readers. A la Bootstrap 4.
+//
+// See: http://a11yproject.com/posts/how-to-hide-content/
+
+@mixin sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0,0,0,0);
+ border: 0;
+}
+
+// Use in conjunction with .sr-only to only display content when it's focused.
+//
+// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
+//
+// Credit: HTML5 Boilerplate
+
+@mixin sr-only-focusable {
+ &:active,
+ &:focus {
+ position: static;
+ width: auto;
+ height: auto;
+ margin: 0;
+ overflow: visible;
+ clip: auto;
+ }
+}
diff --git a/app/assets/stylesheets/vendor/font_awesome/_path.scss b/app/assets/stylesheets/vendor/font_awesome/_path.scss
index 4ec8fc7333..bb457c23a8 100644
--- a/app/assets/stylesheets/vendor/font_awesome/_path.scss
+++ b/app/assets/stylesheets/vendor/font_awesome/_path.scss
@@ -3,11 +3,13 @@
@font-face {
font-family: 'FontAwesome';
- src: url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'),
- url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff');
- // the below are for really ancient browsers such as IE9 and earlier, and I think can be removed
- //url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'),
- //url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'),
- //url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'),
+ src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}');
+ src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'),
+ url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'),
+ url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'),
+ url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'),
+ url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg');
// src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts
+ font-weight: normal;
+ font-style: normal;
}
diff --git a/app/assets/stylesheets/vendor/font_awesome/_screen-reader.scss b/app/assets/stylesheets/vendor/font_awesome/_screen-reader.scss
new file mode 100644
index 0000000000..637426f0da
--- /dev/null
+++ b/app/assets/stylesheets/vendor/font_awesome/_screen-reader.scss
@@ -0,0 +1,5 @@
+// Screen Readers
+// -------------------------
+
+.sr-only { @include sr-only(); }
+.sr-only-focusable { @include sr-only-focusable(); }
diff --git a/app/assets/stylesheets/vendor/font_awesome/_variables.scss b/app/assets/stylesheets/vendor/font_awesome/_variables.scss
index 0a471102c4..498fc4a087 100644
--- a/app/assets/stylesheets/vendor/font_awesome/_variables.scss
+++ b/app/assets/stylesheets/vendor/font_awesome/_variables.scss
@@ -4,14 +4,18 @@
$fa-font-path: "../fonts" !default;
$fa-font-size-base: 14px !default;
$fa-line-height-base: 1 !default;
-//$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.5.0/fonts" !default; // for referencing Bootstrap CDN font files directly
+//$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.7.0/fonts" !default; // for referencing Bootstrap CDN font files directly
$fa-css-prefix: fa !default;
-$fa-version: "4.5.0" !default;
+$fa-version: "4.7.0" !default;
$fa-border-color: #eee !default;
$fa-inverse: #fff !default;
$fa-li-width: (30em / 14) !default;
$fa-var-500px: "\f26e";
+$fa-var-address-book: "\f2b9";
+$fa-var-address-book-o: "\f2ba";
+$fa-var-address-card: "\f2bb";
+$fa-var-address-card-o: "\f2bc";
$fa-var-adjust: "\f042";
$fa-var-adn: "\f170";
$fa-var-align-center: "\f037";
@@ -20,6 +24,7 @@ $fa-var-align-left: "\f036";
$fa-var-align-right: "\f038";
$fa-var-amazon: "\f270";
$fa-var-ambulance: "\f0f9";
+$fa-var-american-sign-language-interpreting: "\f2a3";
$fa-var-anchor: "\f13d";
$fa-var-android: "\f17b";
$fa-var-angellist: "\f209";
@@ -50,17 +55,24 @@ $fa-var-arrows: "\f047";
$fa-var-arrows-alt: "\f0b2";
$fa-var-arrows-h: "\f07e";
$fa-var-arrows-v: "\f07d";
+$fa-var-asl-interpreting: "\f2a3";
+$fa-var-assistive-listening-systems: "\f2a2";
$fa-var-asterisk: "\f069";
$fa-var-at: "\f1fa";
+$fa-var-audio-description: "\f29e";
$fa-var-automobile: "\f1b9";
$fa-var-backward: "\f04a";
$fa-var-balance-scale: "\f24e";
$fa-var-ban: "\f05e";
+$fa-var-bandcamp: "\f2d5";
$fa-var-bank: "\f19c";
$fa-var-bar-chart: "\f080";
$fa-var-bar-chart-o: "\f080";
$fa-var-barcode: "\f02a";
$fa-var-bars: "\f0c9";
+$fa-var-bath: "\f2cd";
+$fa-var-bathtub: "\f2cd";
+$fa-var-battery: "\f240";
$fa-var-battery-0: "\f244";
$fa-var-battery-1: "\f243";
$fa-var-battery-2: "\f242";
@@ -86,6 +98,7 @@ $fa-var-bitbucket: "\f171";
$fa-var-bitbucket-square: "\f172";
$fa-var-bitcoin: "\f15a";
$fa-var-black-tie: "\f27e";
+$fa-var-blind: "\f29d";
$fa-var-bluetooth: "\f293";
$fa-var-bluetooth-b: "\f294";
$fa-var-bold: "\f032";
@@ -94,6 +107,7 @@ $fa-var-bomb: "\f1e2";
$fa-var-book: "\f02d";
$fa-var-bookmark: "\f02e";
$fa-var-bookmark-o: "\f097";
+$fa-var-braille: "\f2a1";
$fa-var-briefcase: "\f0b1";
$fa-var-btc: "\f15a";
$fa-var-bug: "\f188";
@@ -196,6 +210,8 @@ $fa-var-cutlery: "\f0f5";
$fa-var-dashboard: "\f0e4";
$fa-var-dashcube: "\f210";
$fa-var-database: "\f1c0";
+$fa-var-deaf: "\f2a4";
+$fa-var-deafness: "\f2a4";
$fa-var-dedent: "\f03b";
$fa-var-delicious: "\f1a5";
$fa-var-desktop: "\f108";
@@ -206,18 +222,25 @@ $fa-var-dollar: "\f155";
$fa-var-dot-circle-o: "\f192";
$fa-var-download: "\f019";
$fa-var-dribbble: "\f17d";
+$fa-var-drivers-license: "\f2c2";
+$fa-var-drivers-license-o: "\f2c3";
$fa-var-dropbox: "\f16b";
$fa-var-drupal: "\f1a9";
$fa-var-edge: "\f282";
$fa-var-edit: "\f044";
+$fa-var-eercast: "\f2da";
$fa-var-eject: "\f052";
$fa-var-ellipsis-h: "\f141";
$fa-var-ellipsis-v: "\f142";
$fa-var-empire: "\f1d1";
$fa-var-envelope: "\f0e0";
$fa-var-envelope-o: "\f003";
+$fa-var-envelope-open: "\f2b6";
+$fa-var-envelope-open-o: "\f2b7";
$fa-var-envelope-square: "\f199";
+$fa-var-envira: "\f299";
$fa-var-eraser: "\f12d";
+$fa-var-etsy: "\f2d7";
$fa-var-eur: "\f153";
$fa-var-euro: "\f153";
$fa-var-exchange: "\f0ec";
@@ -231,6 +254,7 @@ $fa-var-external-link-square: "\f14c";
$fa-var-eye: "\f06e";
$fa-var-eye-slash: "\f070";
$fa-var-eyedropper: "\f1fb";
+$fa-var-fa: "\f2b4";
$fa-var-facebook: "\f09a";
$fa-var-facebook-f: "\f09a";
$fa-var-facebook-official: "\f230";
@@ -265,6 +289,7 @@ $fa-var-filter: "\f0b0";
$fa-var-fire: "\f06d";
$fa-var-fire-extinguisher: "\f134";
$fa-var-firefox: "\f269";
+$fa-var-first-order: "\f2b0";
$fa-var-flag: "\f024";
$fa-var-flag-checkered: "\f11e";
$fa-var-flag-o: "\f11d";
@@ -277,11 +302,13 @@ $fa-var-folder-o: "\f114";
$fa-var-folder-open: "\f07c";
$fa-var-folder-open-o: "\f115";
$fa-var-font: "\f031";
+$fa-var-font-awesome: "\f2b4";
$fa-var-fonticons: "\f280";
$fa-var-fort-awesome: "\f286";
$fa-var-forumbee: "\f211";
$fa-var-forward: "\f04e";
$fa-var-foursquare: "\f180";
+$fa-var-free-code-camp: "\f2c5";
$fa-var-frown-o: "\f119";
$fa-var-futbol-o: "\f1e3";
$fa-var-gamepad: "\f11b";
@@ -300,15 +327,21 @@ $fa-var-git-square: "\f1d2";
$fa-var-github: "\f09b";
$fa-var-github-alt: "\f113";
$fa-var-github-square: "\f092";
+$fa-var-gitlab: "\f296";
$fa-var-gittip: "\f184";
$fa-var-glass: "\f000";
+$fa-var-glide: "\f2a5";
+$fa-var-glide-g: "\f2a6";
$fa-var-globe: "\f0ac";
$fa-var-google: "\f1a0";
$fa-var-google-plus: "\f0d5";
+$fa-var-google-plus-circle: "\f2b3";
+$fa-var-google-plus-official: "\f2b3";
$fa-var-google-plus-square: "\f0d4";
$fa-var-google-wallet: "\f1ee";
$fa-var-graduation-cap: "\f19d";
$fa-var-gratipay: "\f184";
+$fa-var-grav: "\f2d6";
$fa-var-group: "\f0c0";
$fa-var-h-square: "\f0fd";
$fa-var-hacker-news: "\f1d4";
@@ -325,6 +358,8 @@ $fa-var-hand-rock-o: "\f255";
$fa-var-hand-scissors-o: "\f257";
$fa-var-hand-spock-o: "\f259";
$fa-var-hand-stop-o: "\f256";
+$fa-var-handshake-o: "\f2b5";
+$fa-var-hard-of-hearing: "\f2a4";
$fa-var-hashtag: "\f292";
$fa-var-hdd-o: "\f0a0";
$fa-var-header: "\f1dc";
@@ -347,8 +382,12 @@ $fa-var-hourglass-start: "\f251";
$fa-var-houzz: "\f27c";
$fa-var-html5: "\f13b";
$fa-var-i-cursor: "\f246";
+$fa-var-id-badge: "\f2c1";
+$fa-var-id-card: "\f2c2";
+$fa-var-id-card-o: "\f2c3";
$fa-var-ils: "\f20b";
$fa-var-image: "\f03e";
+$fa-var-imdb: "\f2d8";
$fa-var-inbox: "\f01c";
$fa-var-indent: "\f03c";
$fa-var-industry: "\f275";
@@ -386,6 +425,7 @@ $fa-var-line-chart: "\f201";
$fa-var-link: "\f0c1";
$fa-var-linkedin: "\f0e1";
$fa-var-linkedin-square: "\f08c";
+$fa-var-linode: "\f2b8";
$fa-var-linux: "\f17c";
$fa-var-list: "\f03a";
$fa-var-list-alt: "\f022";
@@ -397,6 +437,7 @@ $fa-var-long-arrow-down: "\f175";
$fa-var-long-arrow-left: "\f177";
$fa-var-long-arrow-right: "\f178";
$fa-var-long-arrow-up: "\f176";
+$fa-var-low-vision: "\f2a8";
$fa-var-magic: "\f0d0";
$fa-var-magnet: "\f076";
$fa-var-mail-forward: "\f064";
@@ -417,8 +458,10 @@ $fa-var-maxcdn: "\f136";
$fa-var-meanpath: "\f20c";
$fa-var-medium: "\f23a";
$fa-var-medkit: "\f0fa";
+$fa-var-meetup: "\f2e0";
$fa-var-meh-o: "\f11a";
$fa-var-mercury: "\f223";
+$fa-var-microchip: "\f2db";
$fa-var-microphone: "\f130";
$fa-var-microphone-slash: "\f131";
$fa-var-minus: "\f068";
@@ -468,8 +511,9 @@ $fa-var-phone-square: "\f098";
$fa-var-photo: "\f03e";
$fa-var-picture-o: "\f03e";
$fa-var-pie-chart: "\f200";
-$fa-var-pied-piper: "\f1a7";
+$fa-var-pied-piper: "\f2ae";
$fa-var-pied-piper-alt: "\f1a8";
+$fa-var-pied-piper-pp: "\f1a7";
$fa-var-pinterest: "\f0d2";
$fa-var-pinterest-p: "\f231";
$fa-var-pinterest-square: "\f0d3";
@@ -482,6 +526,7 @@ $fa-var-plus: "\f067";
$fa-var-plus-circle: "\f055";
$fa-var-plus-square: "\f0fe";
$fa-var-plus-square-o: "\f196";
+$fa-var-podcast: "\f2ce";
$fa-var-power-off: "\f011";
$fa-var-print: "\f02f";
$fa-var-product-hunt: "\f288";
@@ -490,10 +535,13 @@ $fa-var-qq: "\f1d6";
$fa-var-qrcode: "\f029";
$fa-var-question: "\f128";
$fa-var-question-circle: "\f059";
+$fa-var-question-circle-o: "\f29c";
+$fa-var-quora: "\f2c4";
$fa-var-quote-left: "\f10d";
$fa-var-quote-right: "\f10e";
$fa-var-ra: "\f1d0";
$fa-var-random: "\f074";
+$fa-var-ravelry: "\f2d9";
$fa-var-rebel: "\f1d0";
$fa-var-recycle: "\f1b8";
$fa-var-reddit: "\f1a1";
@@ -507,6 +555,7 @@ $fa-var-reorder: "\f0c9";
$fa-var-repeat: "\f01e";
$fa-var-reply: "\f112";
$fa-var-reply-all: "\f122";
+$fa-var-resistance: "\f1d0";
$fa-var-retweet: "\f079";
$fa-var-rmb: "\f157";
$fa-var-road: "\f018";
@@ -519,6 +568,7 @@ $fa-var-rss-square: "\f143";
$fa-var-rub: "\f158";
$fa-var-ruble: "\f158";
$fa-var-rupee: "\f156";
+$fa-var-s15: "\f2cd";
$fa-var-safari: "\f267";
$fa-var-save: "\f0c7";
$fa-var-scissors: "\f0c4";
@@ -543,9 +593,12 @@ $fa-var-shirtsinbulk: "\f214";
$fa-var-shopping-bag: "\f290";
$fa-var-shopping-basket: "\f291";
$fa-var-shopping-cart: "\f07a";
+$fa-var-shower: "\f2cc";
$fa-var-sign-in: "\f090";
+$fa-var-sign-language: "\f2a7";
$fa-var-sign-out: "\f08b";
$fa-var-signal: "\f012";
+$fa-var-signing: "\f2a7";
$fa-var-simplybuilt: "\f215";
$fa-var-sitemap: "\f0e8";
$fa-var-skyatlas: "\f216";
@@ -554,6 +607,10 @@ $fa-var-slack: "\f198";
$fa-var-sliders: "\f1de";
$fa-var-slideshare: "\f1e7";
$fa-var-smile-o: "\f118";
+$fa-var-snapchat: "\f2ab";
+$fa-var-snapchat-ghost: "\f2ac";
+$fa-var-snapchat-square: "\f2ad";
+$fa-var-snowflake-o: "\f2dc";
$fa-var-soccer-ball-o: "\f1e3";
$fa-var-sort: "\f0dc";
$fa-var-sort-alpha-asc: "\f15d";
@@ -599,6 +656,7 @@ $fa-var-subscript: "\f12c";
$fa-var-subway: "\f239";
$fa-var-suitcase: "\f0f2";
$fa-var-sun-o: "\f185";
+$fa-var-superpowers: "\f2dd";
$fa-var-superscript: "\f12b";
$fa-var-support: "\f1cd";
$fa-var-table: "\f0ce";
@@ -608,6 +666,7 @@ $fa-var-tag: "\f02b";
$fa-var-tags: "\f02c";
$fa-var-tasks: "\f0ae";
$fa-var-taxi: "\f1ba";
+$fa-var-telegram: "\f2c6";
$fa-var-television: "\f26c";
$fa-var-tencent-weibo: "\f1d5";
$fa-var-terminal: "\f120";
@@ -616,6 +675,18 @@ $fa-var-text-width: "\f035";
$fa-var-th: "\f00a";
$fa-var-th-large: "\f009";
$fa-var-th-list: "\f00b";
+$fa-var-themeisle: "\f2b2";
+$fa-var-thermometer: "\f2c7";
+$fa-var-thermometer-0: "\f2cb";
+$fa-var-thermometer-1: "\f2ca";
+$fa-var-thermometer-2: "\f2c9";
+$fa-var-thermometer-3: "\f2c8";
+$fa-var-thermometer-4: "\f2c7";
+$fa-var-thermometer-empty: "\f2cb";
+$fa-var-thermometer-full: "\f2c7";
+$fa-var-thermometer-half: "\f2c9";
+$fa-var-thermometer-quarter: "\f2ca";
+$fa-var-thermometer-three-quarters: "\f2c8";
$fa-var-thumb-tack: "\f08d";
$fa-var-thumbs-down: "\f165";
$fa-var-thumbs-o-down: "\f088";
@@ -625,6 +696,8 @@ $fa-var-ticket: "\f145";
$fa-var-times: "\f00d";
$fa-var-times-circle: "\f057";
$fa-var-times-circle-o: "\f05c";
+$fa-var-times-rectangle: "\f2d3";
+$fa-var-times-rectangle-o: "\f2d4";
$fa-var-tint: "\f043";
$fa-var-toggle-down: "\f150";
$fa-var-toggle-left: "\f191";
@@ -655,6 +728,7 @@ $fa-var-twitter-square: "\f081";
$fa-var-umbrella: "\f0e9";
$fa-var-underline: "\f0cd";
$fa-var-undo: "\f0e2";
+$fa-var-universal-access: "\f29a";
$fa-var-university: "\f19c";
$fa-var-unlink: "\f127";
$fa-var-unlock: "\f09c";
@@ -664,20 +738,28 @@ $fa-var-upload: "\f093";
$fa-var-usb: "\f287";
$fa-var-usd: "\f155";
$fa-var-user: "\f007";
+$fa-var-user-circle: "\f2bd";
+$fa-var-user-circle-o: "\f2be";
$fa-var-user-md: "\f0f0";
+$fa-var-user-o: "\f2c0";
$fa-var-user-plus: "\f234";
$fa-var-user-secret: "\f21b";
$fa-var-user-times: "\f235";
$fa-var-users: "\f0c0";
+$fa-var-vcard: "\f2bb";
+$fa-var-vcard-o: "\f2bc";
$fa-var-venus: "\f221";
$fa-var-venus-double: "\f226";
$fa-var-venus-mars: "\f228";
$fa-var-viacoin: "\f237";
+$fa-var-viadeo: "\f2a9";
+$fa-var-viadeo-square: "\f2aa";
$fa-var-video-camera: "\f03d";
$fa-var-vimeo: "\f27d";
$fa-var-vimeo-square: "\f194";
$fa-var-vine: "\f1ca";
$fa-var-vk: "\f189";
+$fa-var-volume-control-phone: "\f2a0";
$fa-var-volume-down: "\f027";
$fa-var-volume-off: "\f026";
$fa-var-volume-up: "\f028";
@@ -687,11 +769,20 @@ $fa-var-weibo: "\f18a";
$fa-var-weixin: "\f1d7";
$fa-var-whatsapp: "\f232";
$fa-var-wheelchair: "\f193";
+$fa-var-wheelchair-alt: "\f29b";
$fa-var-wifi: "\f1eb";
$fa-var-wikipedia-w: "\f266";
+$fa-var-window-close: "\f2d3";
+$fa-var-window-close-o: "\f2d4";
+$fa-var-window-maximize: "\f2d0";
+$fa-var-window-minimize: "\f2d1";
+$fa-var-window-restore: "\f2d2";
$fa-var-windows: "\f17a";
$fa-var-won: "\f159";
$fa-var-wordpress: "\f19a";
+$fa-var-wpbeginner: "\f297";
+$fa-var-wpexplorer: "\f2de";
+$fa-var-wpforms: "\f298";
$fa-var-wrench: "\f0ad";
$fa-var-xing: "\f168";
$fa-var-xing-square: "\f169";
@@ -702,6 +793,7 @@ $fa-var-yc: "\f23b";
$fa-var-yc-square: "\f1d4";
$fa-var-yelp: "\f1e9";
$fa-var-yen: "\f157";
+$fa-var-yoast: "\f2b1";
$fa-var-youtube: "\f167";
$fa-var-youtube-play: "\f16a";
$fa-var-youtube-square: "\f166";
diff --git a/app/assets/stylesheets/vendor/font_awesome/font-awesome.scss b/app/assets/stylesheets/vendor/font_awesome/font-awesome.scss
index 4cd98d36f4..abab424ee5 100644
--- a/app/assets/stylesheets/vendor/font_awesome/font-awesome.scss
+++ b/app/assets/stylesheets/vendor/font_awesome/font-awesome.scss
@@ -1,5 +1,5 @@
/*!
- * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome
+ * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
*/
@@ -15,3 +15,4 @@
@import "rotated-flipped";
@import "stacked";
@import "icons";
+@import "screen-reader";
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/email_controller.rb b/app/controllers/admin/email_controller.rb
index 74ce0901ec..455a22151e 100644
--- a/app/controllers/admin/email_controller.rb
+++ b/app/controllers/admin/email_controller.rb
@@ -84,8 +84,21 @@ class Admin::EmailController < Admin::AdminController
def handle_mail
params.require(:email)
- Email::Processor.process!(params[:email])
- render plain: "email was processed"
+ retry_count = 0
+
+ begin
+ Jobs.enqueue(:process_email, mail: params[:email], retry_on_rate_limit: true)
+ rescue JSON::GeneratorError => e
+ if retry_count == 0
+ params[:email] = params[:email].force_encoding('iso-8859-1').encode("UTF-8")
+ retry_count += 1
+ retry
+ else
+ raise e
+ end
+ end
+
+ render plain: "email has been received and is queued for processing"
end
def raw_email
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index d2a93629fc..c93e287cee 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -24,13 +24,22 @@ class Admin::GroupsController < Admin::AdminController
def bulk_perform
group = Group.find(params[:group_id].to_i)
+ users_added = 0
if group.present?
users = (params[:users] || []).map {|u| u.downcase}
- user_ids = User.where("username_lower in (:users) OR email IN (:users)", users: users).pluck(:id)
- group.bulk_add(user_ids) if user_ids.present?
+ valid_emails = {}
+ valid_usernames = {}
+ valid_users = User.where("username_lower IN (:users) OR email IN (:users)", users: users).pluck(:id, :username_lower, :email)
+ valid_users.map! do |id, username_lower, email|
+ valid_emails[email] = valid_usernames[username_lower] = id
+ id
+ end
+ invalid_users = users.reject! { |u| valid_emails[u] || valid_usernames[u] }
+ group.bulk_add(valid_users) if valid_users.present?
+ users_added = valid_users.count
end
- render json: success_json
+ render json: { success: true, message: I18n.t('groups.success.bulk_add', users_added: users_added), users_not_added: invalid_users }
end
def create
@@ -71,6 +80,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 +163,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/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/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..700df88b65
--- /dev/null
+++ b/app/controllers/admin/themes_controller.rb
@@ -0,0 +1,205 @@
+class Admin::ThemesController < Admin::AdminController
+
+ skip_before_filter :check_xhr, only: [:show, :preview]
+
+ def preview
+ @theme = Theme.find(params[:id])
+
+ redirect_to path("/"), flash: {preview_theme_key: @theme.key}
+ end
+
+ 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
+
+ save_remote = false
+ if params[:theme][:remote_check]
+ @theme.remote_theme.update_remote_version
+ save_remote = true
+ end
+
+ if params[:theme][:remote_update]
+ @theme.remote_theme.update_from_remote
+ save_remote = true
+ end
+
+ respond_to do |format|
+ if @theme.save
+
+ @theme.remote_theme.save! if save_remote
+
+ update_default_theme
+
+ log_theme_change(original_json, @theme)
+ format.json { render json: @theme, status: :created}
+ else
+ format.json {
+
+ error = @theme.errors[:color_scheme] ? I18n.t("themes.bad_color_scheme") : I18n.t("themes.other_error")
+ render json: {errors: [ error ]}, 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..48b38ed88f 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -33,12 +33,11 @@ class ApplicationController < ActionController::Base
end
end
+ before_filter :handle_theme
before_filter :set_current_user_for_logs
before_filter :clear_notifications
before_filter :set_locale
before_filter :set_mobile_view
- before_filter :inject_preview_style
- before_filter :disable_customization
before_filter :block_if_readonly_mode
before_filter :authorize_mini_profiler
before_filter :preload_json
@@ -243,28 +242,40 @@ class ApplicationController < ActionController::Base
session[:mobile_view] = params[:mobile_view] if params.has_key?(:mobile_view)
end
- def inject_preview_style
- style = request['preview-style']
+ NO_CUSTOM = "no_custom".freeze
+ NO_PLUGINS = "no_plugins".freeze
+ ONLY_OFFICIAL = "only_official".freeze
+ SAFE_MODE = "safe_mode".freeze
- if style.nil?
- session[:preview_style] = cookies[:preview_style]
- else
- cookies.delete(:preview_style)
-
- if style.blank? || style == 'default'
- session[:preview_style] = nil
- else
- session[:preview_style] = style
- if request['sticky']
- cookies[:preview_style] = style
- end
- end
+ def resolve_safe_mode
+ safe_mode = params[SAFE_MODE]
+ if safe_mode
+ request.env[NO_CUSTOM] = !!safe_mode.include?(NO_CUSTOM)
+ request.env[NO_PLUGINS] = !!safe_mode.include?(NO_PLUGINS)
+ request.env[ONLY_OFFICIAL] = !!safe_mode.include?(ONLY_OFFICIAL)
end
-
end
- def disable_customization
- session[:disable_customization] = params[:customization] == "0" if params.has_key?(:customization)
+ def handle_theme
+
+ return if request.xhr? || request.format.json?
+ return if request.method != "GET"
+
+ resolve_safe_mode
+ return if request.env[NO_CUSTOM]
+
+ theme_key = flash[:preview_theme_key] || cookies[:theme_key] || session[:theme_key]
+
+ if theme_key && !guardian.allow_theme?(theme_key)
+ theme_key = nil
+ cookies[:theme_key] = nil
+ session[:theme_key] = nil
+ end
+
+ theme_key ||= SiteSetting.default_theme_key
+ theme_key = nil if theme_key.blank?
+
+ @theme_key = request.env[:resolved_theme_key] = theme_key
end
def guardian
@@ -410,15 +421,23 @@ 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)
- }
+ data = if @theme_key
+ {
+ top: Theme.lookup_field(@theme_key, target, "after_header"),
+ footer: Theme.lookup_field(@theme_key, target, "footer")
+ }
+ else
+ {}
+ end
if DiscoursePluginRegistry.custom_html
data.merge! DiscoursePluginRegistry.custom_html
end
+ DiscoursePluginRegistry.html_builders.each do |name, blk|
+ data[name] = blk.call(self)
+ end
+
MultiJson.dump(data)
end
@@ -450,7 +469,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/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/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index e4990e0af1..dd151b0ba2 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,17 @@ 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)
+ elsif !SiteSetting.enable_sso && SiteSetting.enable_local_logins
+ Jobs.enqueue(:invite_password_instructions_email, username: user.username)
+ end
+ end
+
end
diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb
index e6c82e01ec..1415f78f57 100644
--- a/app/controllers/notifications_controller.rb
+++ b/app/controllers/notifications_controller.rb
@@ -8,7 +8,7 @@ class NotificationsController < ApplicationController
user =
if params[:username] && !params[:recent]
user_record = User.find_by(username: params[:username].to_s)
- raise Discourse::InvalidParameters.new(:username) if !user_record
+ raise Discourse::NotFound if !user_record
user_record
else
current_user
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index 9feab0fe82..6ed83c91eb 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -383,8 +383,8 @@ class PostsController < ApplicationController
PostAction.act(current_user, post, PostActionType.types[:bookmark])
else
post_action = PostAction.find_by(post_id: params[:post_id], user_id: current_user.id)
- post = post_action.post
- raise Discourse::InvalidParameters unless post_action
+ post = post_action&.post
+ raise Discourse::NotFound unless post_action
PostAction.remove_act(current_user, post, PostActionType.types[:bookmark])
end
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
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/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)
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index eb69ce7d3c..401d9ed623 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -36,7 +36,7 @@ class UsersController < ApplicationController
end
def show
- raise Discourse::InvalidAccess if SiteSetting.hide_user_profiles_from_public && !current_user
+ return redirect_to path('/login') if SiteSetting.hide_user_profiles_from_public && !current_user
@user = fetch_user_from_params(
{ include_inactive: current_user.try(:staff?) },
@@ -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
@@ -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/helpers/application_helper.rb b/app/helpers/application_helper.rb
index a9e4ed40e8..404ed92bf4 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
@@ -248,32 +248,23 @@ module ApplicationHelper
MobileDetection.mobile_device?(request.user_agent)
end
- NO_CUSTOM = "no_custom".freeze
- NO_PLUGINS = "no_plugins".freeze
- ONLY_OFFICIAL = "only_official".freeze
- SAFE_MODE = "safe_mode".freeze
-
def customization_disabled?
- safe_mode = params[SAFE_MODE]
- session[:disable_customization] || (safe_mode && safe_mode.include?(NO_CUSTOM))
+ request.env[ApplicationController::NO_CUSTOM]
end
def allow_plugins?
- safe_mode = params[SAFE_MODE]
- !(safe_mode && safe_mode.include?(NO_PLUGINS))
+ !request.env[ApplicationController::NO_PLUGINS]
end
def allow_third_party_plugins?
- safe_mode = params[SAFE_MODE]
- !(safe_mode && (safe_mode.include?(NO_PLUGINS) || safe_mode.include?(ONLY_OFFICIAL)))
+ allow_plugins? && !request.env[ApplicationController::ONLY_OFFICIAL]
end
def normalized_safe_mode
- mode_string = params["safe_mode"]
safe_mode = nil
- (safe_mode ||= []) << NO_CUSTOM if mode_string.include?(NO_CUSTOM)
- (safe_mode ||= []) << NO_PLUGINS if mode_string.include?(NO_PLUGINS)
- (safe_mode ||= []) << ONLY_OFFICIAL if mode_string.include?(ONLY_OFFICIAL)
+ (safe_mode ||= []) << ApplicationController::NO_CUSTOM if customization_disabled?
+ (safe_mode ||= []) << ApplicationController::NO_PLUGINS if !allow_plugins?
+ (safe_mode ||= []) << ApplicationController::ONLY_OFFICIAL if !allow_third_party_plugins?
if safe_mode
safe_mode.join(",").html_safe
end
@@ -316,4 +307,32 @@ module ApplicationHelper
''
end
end
+
+ def theme_key
+ if customization_disabled?
+ nil
+ else
+ request.env[:resolved_theme_key]
+ end
+ end
+
+ def build_plugin_html(name)
+ return "" unless allow_plugins?
+ DiscoursePluginRegistry.build_html(name, controller) || ""
+ 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/helpers/email_helper.rb b/app/helpers/email_helper.rb
new file mode 100644
index 0000000000..620f1fe94c
--- /dev/null
+++ b/app/helpers/email_helper.rb
@@ -0,0 +1,38 @@
+module EmailHelper
+
+ def mailing_list_topic(topic, post_count)
+ render(
+ partial: partial_for("mailing_list_post"),
+ locals: { topic: topic, post_count: post_count }
+ )
+ end
+
+ def mailing_list_topic_text(topic)
+ url, title = extract_details(topic)
+ raw(@markdown_linker.create(title, url))
+ end
+
+ def private_topic_title(topic)
+ I18n.t("system_messages.private_topic_title", id: topic.id)
+ end
+
+ def email_topic_link(topic)
+ url, title = extract_details(topic)
+ raw "#{title}"
+ end
+
+ protected
+
+ def extract_details(topic)
+ if SiteSetting.private_email?
+ [topic.slugless_url, private_topic_title(topic)]
+ else
+ [topic.relative_url, format_topic_title(topic.title)]
+ end
+ end
+
+ def partial_for(name)
+ SiteSetting.private_email? ? "email/secure_#{name}" : "email/#{name}"
+ end
+
+end
diff --git a/app/jobs/onceoff/clean_up_sidekiq_statistic.rb b/app/jobs/onceoff/clean_up_sidekiq_statistic.rb
new file mode 100644
index 0000000000..69614a8827
--- /dev/null
+++ b/app/jobs/onceoff/clean_up_sidekiq_statistic.rb
@@ -0,0 +1,7 @@
+module Jobs
+ class CleanUpSidekiqStatistic < Jobs::Onceoff
+ def execute_onceoff(args)
+ $redis.without_namespace.del('sidekiq:sidekiq:statistic')
+ end
+ end
+end
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
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/app/jobs/regular/publish_topic_to_category.rb b/app/jobs/regular/publish_topic_to_category.rb
index 11424dd32f..c67adde491 100644
--- a/app/jobs/regular/publish_topic_to_category.rb
+++ b/app/jobs/regular/publish_topic_to_category.rb
@@ -8,8 +8,15 @@ 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
MessageBus.publish("/topic/#{topic.id}", reload_topic: true, refresh_stream: true)
diff --git a/app/jobs/scheduled/clean_up_unused_staged_users.rb b/app/jobs/scheduled/clean_up_unused_staged_users.rb
index f11ec67c1e..074b09e978 100644
--- a/app/jobs/scheduled/clean_up_unused_staged_users.rb
+++ b/app/jobs/scheduled/clean_up_unused_staged_users.rb
@@ -6,11 +6,21 @@ module Jobs
def execute(args)
destroyer = UserDestroyer.new(Discourse.system_user)
- User.joins(:user_stat)
- .where(staged: true)
- .where("users.created_at < ?", 1.year.ago)
- .where("user_stats.post_count = 0")
- .find_each { |user| destroyer.destroy(user) }
+ User.joins("LEFT JOIN posts ON posts.user_id = users.id")
+ .where("posts.user_id IS NULL")
+ .where(staged: true)
+ .where("users.created_at < ?", 1.year.ago)
+ .find_each do |user|
+
+ begin
+ destroyer.destroy(user)
+ rescue => e
+ Discourse.handle_job_exception(e,
+ message: "Cleaning up unused staged user",
+ extra: { user_id: user.id }
+ )
+ end
+ end
end
end
diff --git a/app/jobs/scheduled/enqueue_digest_emails.rb b/app/jobs/scheduled/enqueue_digest_emails.rb
index bab41fb142..1bd4ed4fdf 100644
--- a/app/jobs/scheduled/enqueue_digest_emails.rb
+++ b/app/jobs/scheduled/enqueue_digest_emails.rb
@@ -5,10 +5,9 @@ module Jobs
every 30.minutes
def execute(args)
- unless SiteSetting.disable_digest_emails?
- target_user_ids.each do |user_id|
- Jobs.enqueue(:user_email, type: :digest, user_id: user_id)
- end
+ return if SiteSetting.disable_digest_emails? || SiteSetting.private_email?
+ target_user_ids.each do |user_id|
+ Jobs.enqueue(:user_email, type: :digest, user_id: user_id)
end
end
diff --git a/app/jobs/scheduled/pending_flags_reminder.rb b/app/jobs/scheduled/pending_flags_reminder.rb
index 94fb480ef1..895c6c2492 100644
--- a/app/jobs/scheduled/pending_flags_reminder.rb
+++ b/app/jobs/scheduled/pending_flags_reminder.rb
@@ -4,12 +4,15 @@ module Jobs
class PendingFlagsReminder < Jobs::Scheduled
- every 1.day
+ every 1.hour
def execute(args)
if SiteSetting.notify_about_flags_after > 0 &&
- PostAction.flagged_posts_count > 0 &&
- FlagQuery.flagged_post_actions('active').where('post_actions.created_at < ?', SiteSetting.notify_about_flags_after.to_i.hours.ago).pluck(:id).count > 0
+ PostAction.flagged_posts_count > 0 &&
+ flag_ids.size > 0 && last_notified_id.to_i < flag_ids.max
+
+ mentions = active_moderator_usernames.size > 0 ?
+ "@#{active_moderator_usernames.join(', @')} " : ""
PostCreator.create(
Discourse.system_user,
@@ -17,9 +20,40 @@ module Jobs
archetype: Archetype.private_message,
subtype: TopicSubtype.system_message,
title: I18n.t('flags_reminder.subject_template', { count: PostAction.flagged_posts_count }),
- raw: I18n.t('flags_reminder.flags_were_submitted', { count: SiteSetting.notify_about_flags_after })
+ raw: mentions + I18n.t('flags_reminder.flags_were_submitted', { count: SiteSetting.notify_about_flags_after })
)
+
+ self.last_notified_id = flag_ids.max
end
+
+ true
+ end
+
+ def flag_ids
+ @_flag_ids ||= FlagQuery.flagged_post_actions('active')
+ .where('post_actions.created_at < ?', SiteSetting.notify_about_flags_after.to_i.hours.ago)
+ .pluck(:id)
+ end
+
+ def last_notified_id
+ $redis.get(self.class.last_notified_key)&.to_i
+ end
+
+ def last_notified_id=(arg)
+ $redis.set(self.class.last_notified_key, arg)
+ end
+
+ def self.last_notified_key
+ "last_notified_pending_flag_id"
+ end
+
+ def active_moderator_usernames
+ @_active_moderator_usernames ||=
+ User.where(moderator: true)
+ .human_users
+ .order('last_seen_at DESC')
+ .limit(3)
+ .pluck(:username)
end
end
diff --git a/app/mailers/invite_mailer.rb b/app/mailers/invite_mailer.rb
index 346507326f..3614123716 100644
--- a/app/mailers/invite_mailer.rb
+++ b/app/mailers/invite_mailer.rb
@@ -5,6 +5,7 @@ class InviteMailer < ActionMailer::Base
class UserNotificationRenderer < ActionView::Base
include UserNotificationsHelper
+ include EmailHelper
end
def send_invite(invite, custom_message=nil)
@@ -30,12 +31,18 @@ class InviteMailer < ActionMailer::Base
template = 'custom_invite_mailer'
end
+ topic_title = first_topic.try(:title)
+ if SiteSetting.private_email?
+ topic_title = I18n.t("system_messages.private_topic_title", id: first_topic.id)
+ topic_excerpt = ""
+ end
+
build_email(invite.email,
template: template,
invitee_name: invitee_name,
site_domain_name: Discourse.current_hostname,
invite_link: "#{Discourse.base_url}/invites/#{invite.invite_key}",
- topic_title: first_topic.try(:title),
+ topic_title: topic_title,
topic_excerpt: topic_excerpt,
site_description: SiteSetting.site_description,
site_title: SiteSetting.title,
diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb
index acf82ae2dc..dff3331aa0 100644
--- a/app/mailers/user_notifications.rb
+++ b/app/mailers/user_notifications.rb
@@ -5,7 +5,7 @@ require_dependency 'age_words'
class UserNotifications < ActionMailer::Base
include UserNotificationsHelper
include ApplicationHelper
- helper :application
+ helper :application, :email
default charset: 'UTF-8'
include Email::BuildEmailHelper
@@ -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
[]
@@ -284,10 +284,12 @@ class UserNotifications < ActionMailer::Base
class UserNotificationRenderer < ActionView::Base
include UserNotificationsHelper
+ include EmailHelper
end
def self.get_context_posts(post, topic_user, user)
- if user.user_option.email_previous_replies == UserOption.previous_replies_type[:never]
+ if (user.user_option.email_previous_replies == UserOption.previous_replies_type[:never]) ||
+ SiteSetting.private_email?
return []
end
@@ -349,6 +351,7 @@ class UserNotifications < ActionMailer::Base
def send_notification_email(opts)
post = opts[:post]
title = opts[:title]
+
allow_reply_by_email = opts[:allow_reply_by_email]
use_site_subject = opts[:use_site_subject]
add_re_to_subject = opts[:add_re_to_subject] && post.post_number > 1
@@ -377,6 +380,10 @@ class UserNotifications < ActionMailer::Base
show_category_in_subject = nil
end
+ if SiteSetting.private_email?
+ title = I18n.t("system_messages.private_topic_title", id: post.topic_id)
+ end
+
context = ""
tu = TopicUser.get(post.topic_id, user)
context_posts = self.class.get_context_posts(post, tu, user)
@@ -400,6 +407,7 @@ class UserNotifications < ActionMailer::Base
invite_template = "user_notifications.invited_to_topic_body"
end
topic_excerpt = post.excerpt.tr("\n", " ") if post.is_first_post? && post.excerpt
+ topic_excerpt = "" if SiteSetting.private_email?
message = I18n.t(invite_template, username: username, topic_title: gsub_emoji_to_unicode(title), topic_excerpt: topic_excerpt, site_title: SiteSetting.title, site_description: SiteSetting.site_description)
unless translation_override_exists
@@ -418,7 +426,12 @@ class UserNotifications < ActionMailer::Base
.count) >= (SiteSetting.max_emails_per_day_per_user-1)
in_reply_to_post = post.reply_to_post if user.user_option.email_in_reply_to
- message = email_post_markdown(post) + (reached_limit ? "\n\n#{I18n.t "user_notifications.reached_limit", count: SiteSetting.max_emails_per_day_per_user}" : "");
+ if SiteSetting.private_email?
+ message = I18n.t('system_messages.contents_hidden')
+ else
+ message = email_post_markdown(post) + (reached_limit ? "\n\n#{I18n.t "user_notifications.reached_limit", count: SiteSetting.max_emails_per_day_per_user}" : "");
+ end
+
unless translation_override_exists
html = UserNotificationRenderer.new(Rails.configuration.paths["app/views"]).render(
@@ -438,7 +451,7 @@ class UserNotifications < ActionMailer::Base
topic_title: gsub_emoji_to_unicode(title),
topic_title_url_encoded: title ? URI.encode(title) : title,
message: message,
- url: post.url,
+ url: post.url(without_slug: SiteSetting.private_email?),
post_id: post.id,
topic_id: post.topic_id,
context: context,
diff --git a/app/models/badge.rb b/app/models/badge.rb
index 5ffde4726b..25b2a38105 100644
--- a/app/models/badge.rb
+++ b/app/models/badge.rb
@@ -132,12 +132,24 @@ class Badge < ActiveRecord::Base
exec_sql <<-SQL.squish
DELETE FROM user_badges
USING user_badges ub
- LEFT JOIN users u ON u.id = ub.user_id
- WHERE u.id IS NULL
- AND user_badges.id = ub.id
+ LEFT JOIN users u ON u.id = ub.user_id
+ WHERE u.id IS NULL
+ AND user_badges.id = ub.id
SQL
- Badge.find_each(&:reset_grant_count!)
+ exec_sql <<-SQL.squish
+ WITH X AS (
+ SELECT badge_id
+ , COUNT(user_id) users
+ FROM user_badges
+ GROUP BY badge_id
+ )
+ UPDATE badges
+ SET grant_count = X.users
+ FROM X
+ WHERE id = X.badge_id
+ AND grant_count <> X.users
+ SQL
end
def awarded_for_trust_level?
diff --git a/app/models/category.rb b/app/models/category.rb
index 2a48c6a53a..0a50c69d7d 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
@@ -389,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
@@ -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/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/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..6701f4cdf6 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,12 +43,16 @@ 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
+ belongs_to :theme
validates_associated :color_scheme_colors
@@ -64,13 +72,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 +114,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 +142,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 +187,12 @@ 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
+# theme_id :integer
#
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/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'))
diff --git a/app/models/group.rb b/app/models/group.rb
index 74a4126b21..1c185cc791 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -171,16 +171,17 @@ 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[:moderators] == id
group.id = id
group.save!
end
# don't allow shoddy localization to break this
- localized_name = I18n.t("groups.default_names.#{name}")
+ localized_name = I18n.t("groups.default_names.#{name}").downcase
validator = UsernameValidator.new(localized_name)
group.name =
- if !Group.where("lower(name) = ?", localized_name).exists? && validator.valid_format?
+ if !Group.where("LOWER(name) = ?", localized_name).exists? && validator.valid_format?
localized_name
else
name
@@ -195,54 +196,45 @@ class Group < ActiveRecord::Base
end
# Remove people from groups they don't belong in.
- #
- # BEWARE: any of these subqueries could match ALL the user records,
- # so they can't be used in IN clauses.
- remove_user_subquery = case name
- when :admins
- "SELECT u.id FROM users u WHERE NOT u.admin"
- when :moderators
- "SELECT u.id FROM users u WHERE NOT u.moderator"
- when :staff
- "SELECT u.id FROM users u WHERE NOT u.admin AND NOT u.moderator"
- when :trust_level_0, :trust_level_1, :trust_level_2, :trust_level_3, :trust_level_4
- "SELECT u.id FROM users u WHERE u.trust_level < #{id - 10}"
- end
+ remove_subquery = case name
+ when :admins
+ "SELECT id FROM users WHERE NOT admin"
+ when :moderators
+ "SELECT id FROM users WHERE NOT moderator"
+ when :staff
+ "SELECT id FROM users WHERE NOT admin AND NOT moderator"
+ when :trust_level_0, :trust_level_1, :trust_level_2, :trust_level_3, :trust_level_4
+ "SELECT id FROM users WHERE trust_level < #{id - 10}"
+ end
- remove_ids = exec_sql("SELECT gu.id id
- FROM group_users gu,
- (#{remove_user_subquery}) u
- WHERE gu.group_id = #{group.id}
- AND gu.user_id = u.id").map {|x| x['id']}
-
- if remove_ids.length > 0
- remove_ids.each_slice(100) do |ids|
- GroupUser.where(id: ids).delete_all
- end
- end
+ exec_sql <<-SQL
+ DELETE FROM group_users
+ USING (#{remove_subquery}) X
+ WHERE group_id = #{group.id}
+ AND user_id = X.id
+ SQL
# Add people to groups
- real_ids = case name
- when :admins
- "SELECT u.id FROM users u WHERE u.admin"
- when :moderators
- "SELECT u.id FROM users u WHERE u.moderator"
- when :staff
- "SELECT u.id FROM users u WHERE u.moderator OR u.admin"
- when :trust_level_1, :trust_level_2, :trust_level_3, :trust_level_4
- "SELECT u.id FROM users u WHERE u.trust_level >= #{id-10}"
- when :trust_level_0
- "SELECT u.id FROM users u"
- end
+ insert_subquery = case name
+ when :admins
+ "SELECT id FROM users WHERE admin"
+ when :moderators
+ "SELECT id FROM users WHERE moderator"
+ when :staff
+ "SELECT id FROM users WHERE moderator OR admin"
+ when :trust_level_1, :trust_level_2, :trust_level_3, :trust_level_4
+ "SELECT id FROM users WHERE trust_level >= #{id - 10}"
+ when :trust_level_0
+ "SELECT id FROM users"
+ end
- missing_users = GroupUser
- .joins("RIGHT JOIN (#{real_ids}) X ON X.id = user_id AND group_id = #{group.id}")
- .where("user_id IS NULL")
- .select("X.id")
-
- missing_users.each do |u|
- group.group_users.build(user_id: u.id)
- end
+ exec_sql <<-SQL
+ INSERT INTO group_users (group_id, user_id, created_at, updated_at)
+ SELECT #{group.id}, X.id, now(), now()
+ FROM group_users
+ RIGHT JOIN (#{insert_subquery}) X ON X.id = user_id AND group_id = #{group.id}
+ WHERE user_id IS NULL
+ SQL
group.save!
@@ -258,18 +250,24 @@ class Group < ActiveRecord::Base
end
def self.reset_all_counters!
- Group.pluck(:id).each do |group_id|
- Group.reset_counters(group_id, :group_users)
- end
+ exec_sql <<-SQL
+ WITH X AS (
+ SELECT group_id
+ , COUNT(user_id) users
+ FROM group_users
+ GROUP BY group_id
+ )
+ UPDATE groups
+ SET user_count = X.users
+ FROM X
+ WHERE id = X.group_id
+ AND user_count <> X.users
+ SQL
end
def self.refresh_automatic_groups!(*args)
- if args.length == 0
- args = AUTO_GROUPS.keys
- end
- args.each do |group|
- refresh_automatic_group!(group)
- end
+ args = AUTO_GROUPS.keys if args.empty?
+ args.each { |group| refresh_automatic_group!(group) }
end
def self.ensure_automatic_groups!
@@ -465,24 +463,15 @@ class Group < ActiveRecord::Base
return if new_record? && !self.title.present?
if self.title_changed?
- sql = < COALESCE(:title,'') AND
- id IN (
- SELECT user_id
- FROM group_users
- WHERE group_id = :id
- )
-SQL
+ sql = <<-SQL.squish
+ UPDATE users
+ SET title = :title
+ WHERE (title = :title_was OR title = '' OR title IS NULL)
+ AND COALESCE(title,'') <> COALESCE(:title,'')
+ AND id IN (SELECT user_id FROM group_users WHERE group_id = :id)
+ SQL
- self.class.exec_sql(sql,
- title: title,
- title_was: title_was,
- id: id
- )
+ self.class.exec_sql(sql, title: title, title_was: title_was, id: id)
end
end
@@ -552,6 +541,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/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/app/models/invite_redeemer.rb b/app/models/invite_redeemer.rb
index 6f1406b06a..a6212f9169 100644
--- a/app/models/invite_redeemer.rb
+++ b/app/models/invite_redeemer.rb
@@ -61,8 +61,6 @@ InviteRedeemer = Struct.new(:invite, :username, :name, :password) do
add_user_to_groups
send_welcome_message
notify_invitee
- send_password_instructions
- enqueue_activation_mail
delete_duplicate_invites
end
@@ -121,19 +119,6 @@ InviteRedeemer = Struct.new(:invite, :username, :name, :password) do
end
end
- def send_password_instructions
- if !SiteSetting.enable_sso && SiteSetting.enable_local_logins && !invited_user.has_password?
- Jobs.enqueue(:invite_password_instructions_email, username: invited_user.username)
- 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/notification.rb b/app/models/notification.rb
index 96e3e43f9e..a09451e4ee 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -19,14 +19,20 @@ class Notification < ActiveRecord::Base
after_commit :refresh_notification_count, on: [:create, :update]
def self.ensure_consistency!
- Notification.exec_sql("
- DELETE FROM Notifications n WHERE notification_type = :id AND
- NOT EXISTS(
- SELECT 1 FROM posts p
- JOIN topics t ON t.id = p.topic_id
- WHERE p.deleted_at is null AND t.deleted_at IS NULL
- AND p.post_number = n.post_number AND t.id = n.topic_id
- )" , id: Notification.types[:private_message])
+ Notification.exec_sql <<-SQL
+ DELETE
+ FROM notifications n
+ WHERE notification_type = #{Notification.types[:private_message]}
+ AND NOT EXISTS (
+ SELECT 1
+ FROM posts p
+ JOIN topics t ON t.id = p.topic_id
+ WHERE p.deleted_at IS NULL
+ AND t.deleted_at IS NULL
+ AND p.post_number = n.post_number
+ AND t.id = n.topic_id
+ )
+ SQL
end
def self.types
@@ -66,13 +72,12 @@ class Notification < ActiveRecord::Base
end
def self.read(user, notification_ids)
- count = Notification.where(user_id: user.id,
- id: notification_ids,
- read: false).update_all(read: true)
+ count = Notification.where(user_id: user.id)
+ .where(id: notification_ids)
+ .where(read: false)
+ .update_all(read: true)
- if count > 0
- user.publish_notifications_state
- end
+ user.publish_notifications_state if count > 0
end
def self.interesting_after(min_date)
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/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/post.rb b/app/models/post.rb
index 2ed6d3e065..13d88aec1b 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -400,9 +400,11 @@ class Post < ActiveRecord::Base
"#{Discourse.base_url}#{url}"
end
- def url
+ def url(opts=nil)
+ opts ||= {}
+
if topic
- Post.url(topic.slug, topic.id, post_number)
+ Post.url(topic.slug, topic.id, post_number, opts)
else
"/404"
end
@@ -412,8 +414,13 @@ class Post < ActiveRecord::Base
"#{Discourse.base_url}/email/unsubscribe/#{UnsubscribeKey.create_key_for(user, self)}"
end
- def self.url(slug, topic_id, post_number)
- "/t/#{slug}/#{topic_id}/#{post_number}"
+ def self.url(slug, topic_id, post_number, opts=nil)
+ opts ||= {}
+
+ result = "/t/"
+ result << "#{slug}/" unless !!opts[:without_slug]
+
+ "#{result}#{topic_id}/#{post_number}"
end
def self.urls(post_ids)
diff --git a/app/models/post_mover.rb b/app/models/post_mover.rb
index 793bfcc59a..9dff1988fb 100644
--- a/app/models/post_mover.rb
+++ b/app/models/post_mover.rb
@@ -77,7 +77,7 @@ class PostMover
post.is_first_post? ? create_first_post(post) : move(post)
end
- PostReply.where("reply_id in (:post_ids) OR post_id in (:post_ids)", post_ids: post_ids).each do |post_reply|
+ PostReply.where("reply_id IN (:post_ids) OR post_id IN (:post_ids)", post_ids: post_ids).each do |post_reply|
if post_reply.post && post_reply.reply && post_reply.reply.topic_id != post_reply.post.topic_id
PostReply.delete_all(reply_id: post_reply.reply.id, post_id: post_reply.post.id)
end
diff --git a/app/models/remote_theme.rb b/app/models/remote_theme.rb
new file mode 100644
index 0000000000..d32a7705af
--- /dev/null
+++ b/app/models/remote_theme.rb
@@ -0,0 +1,129 @@
+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|
+ 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
+
+ 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
+
+ update_theme_color_schemes(theme, theme_info["color_schemes"])
+
+ self
+ ensure
+ begin
+ importer.cleanup! if cleanup
+ rescue => e
+ Rails.logger.warn("Failed cleanup remote git #{e}")
+ end
+ end
+
+ def normalize_override(hex)
+ return unless hex
+
+ override = hex.downcase
+ if override !~ /\A[0-9a-f]{6}\z/
+ override = nil
+ end
+ override
+ end
+
+ def update_theme_color_schemes(theme, schemes)
+ return if schemes.blank?
+
+ schemes.each do |name, colors|
+ existing = theme.color_schemes.find_by(name: name)
+ if existing
+ existing.colors.each do |c|
+ override = normalize_override(colors[c.name])
+ if override && c.hex != override
+ c.hex = override
+ theme.notify_color_change(c)
+ end
+ end
+ else
+ scheme = theme.color_schemes.build(name: name)
+ ColorScheme.base.colors_hashes.each do |color|
+ override = normalize_override(colors[color[:name]])
+ scheme.color_scheme_colors << ColorSchemeColor.new(name: color[:name], hex: override || color[:hex])
+ end
+ end
+ 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/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/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/app/models/scheduler_stat.rb b/app/models/scheduler_stat.rb
index 55dc8fda6a..258a437f64 100644
--- a/app/models/scheduler_stat.rb
+++ b/app/models/scheduler_stat.rb
@@ -17,4 +17,5 @@ end
# live_slots_finish :integer
# started_at :datetime not null
# success :boolean
+# error :text
#
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..1ea01b2f1e
--- /dev/null
+++ b/app/models/theme.rb
@@ -0,0 +1,281 @@
+require_dependency 'distributed_cache'
+require_dependency 'stylesheet/compiler'
+require_dependency 'stylesheet/manager'
+
+class Theme < ActiveRecord::Base
+
+ ALLOWED_FIELDS = %w{scss embedded_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
+ has_many :color_schemes
+ belongs_to :remote_theme
+
+ before_create do
+ self.key ||= SecureRandom.uuid
+ true
+ end
+
+ def notify_color_change(color)
+ changed_colors << color
+ end
+
+ after_save do
+ changed_colors.each(&:save!)
+ changed_colors.clear
+ 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.theme_keys
+ if keys = @cache["theme_keys"]
+ return keys
+ end
+ @cache["theme_keys"] = Set.new(Theme.pluck(:key))
+ end
+
+ def self.user_theme_keys
+ if keys = @cache["user_theme_keys"]
+ return keys
+ end
+ @cache["theme_keys"] = Set.new(
+ Theme
+ .where('user_selectable OR key = ?', SiteSetting.default_theme_key)
+ .pluck(:key)
+ )
+ end
+
+ 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)
+ 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)
+ end.compact.flatten
+ MessageBus.publish('/file-change', message)
+ end
+
+ def refresh_message_for_targets(targets, theme)
+ targets.map do |target|
+ href = Stylesheet::Manager.stylesheet_href(target.to_sym, theme.key)
+ if href
+ {
+ target: target,
+ new_href: href,
+ theme_key: theme.key
+ }
+ 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 changed_colors
+ @changed_colors ||= []
+ 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?
+ theme_fields.delete 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..65b89db514
--- /dev/null
+++ b/app/models/theme_field.rb
@@ -0,0 +1,147 @@
+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)
+ errors = nil
+
+ 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("")
+ errors ||= []
+ errors << ex.message
+ end
+ end
+ end
+
+ [doc.to_s, errors&.join("\n")]
+ end
+
+
+ def self.html_fields
+ %w(body_tag head_tag header footer after_header)
+ end
+
+ def self.scss_fields
+ %w(scss embedded_scss)
+ end
+
+
+ def ensure_baked!
+ if ThemeField.html_fields.include?(self.name)
+ if !self.value_baked || compiler_version != COMPILER_VERSION
+
+ self.value_baked, self.error = process_html(self.value)
+ self.compiler_version = COMPILER_VERSION
+
+ if self.value_baked_changed? || compiler_version.changed? || self.error_changed?
+ self.update_columns(value_baked: value_baked,
+ compiler_version: compiler_version,
+ error: error)
+ end
+ end
+ end
+ end
+
+ def ensure_scss_compiles!
+ if ThemeField.scss_fields.include?(self.name)
+ begin
+ Stylesheet::Compiler.compile("@import \"theme_variables\"; @import \"theme_field\";",
+ "theme.scss",
+ theme_field: self.value.dup)
+ self.error = nil unless error.nil?
+ rescue SassC::SyntaxError => e
+ self.error = e.message
+ end
+
+ if error_changed?
+ update_columns(error: self.error)
+ 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!
+ ensure_scss_compiles!
+
+ 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 theme && self.name == "header"
+ MessageBus.publish "/footer-change/#{theme.key}", self.value if theme && 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
+# error :string
+#
+# Indexes
+#
+# index_theme_fields_on_theme_id_and_target_and_name (theme_id,target,name) UNIQUE
+#
diff --git a/app/models/topic.rb b/app/models/topic.rb
index 4f1367fa07..2dd0b9b835 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
@@ -39,12 +40,16 @@ class Topic < ActiveRecord::Base
update_category_topic_count_by(-1) if deleted_at.nil?
super(trashed_by)
update_flagged_posts_count
+ self.topic_embed.trash! if has_topic_embed?
end
def recover!
update_category_topic_count_by(1) unless deleted_at.nil?
super
update_flagged_posts_count
+ unless (topic_embed = TopicEmbed.with_deleted.find_by_topic_id(id)).nil?
+ topic_embed.recover!
+ end
end
rate_limit :default_rate_limiter
@@ -117,9 +122,11 @@ 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
+ has_one :topic_embed, dependent: :destroy
+
# When we want to temporarily attach some data to a forum topic (usually before serialization)
attr_accessor :user_data
@@ -883,11 +890,17 @@ SQL
end
def self.relative_url(id, slug, post_number=nil)
- url = "#{Discourse.base_uri}/t/#{slug}/#{id}"
+ url = "#{Discourse.base_uri}/t/"
+ url << "#{slug}/" if slug.present?
+ url << id.to_s
url << "/#{post_number}" if post_number.to_i > 1
url
end
+ def slugless_url(post_number=nil)
+ Topic.relative_url(id, nil, post_number)
+ end
+
def relative_url(post_number=nil)
Topic.relative_url(id, slug, post_number)
end
@@ -1174,6 +1187,15 @@ SQL
private_topic
end
+ def pm_with_non_human_user?
+ Topic.private_messages
+ .joins("LEFT JOIN topic_allowed_groups ON topics.id = topic_allowed_groups.topic_id")
+ .where("topic_allowed_groups.topic_id IS NULL")
+ .where("topics.id = ?", self.id)
+ .where("(SELECT COUNT(*) FROM topic_allowed_users WHERE topic_allowed_users.topic_id = ? AND topic_allowed_users.user_id > 0) = 1", self.id)
+ .exists?
+ end
+
private
def update_category_topic_count_by(num)
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/app/models/topic_embed.rb b/app/models/topic_embed.rb
index 8e45c1d60e..15ca33e40f 100644
--- a/app/models/topic_embed.rb
+++ b/app/models/topic_embed.rb
@@ -1,11 +1,19 @@
require_dependency 'nokogiri'
class TopicEmbed < ActiveRecord::Base
+ include Trashable
+
belongs_to :topic
belongs_to :post
validates_presence_of :embed_url
validates_uniqueness_of :embed_url
+ before_validation(on: :create) do
+ unless (topic_embed = TopicEmbed.with_deleted.where('deleted_at IS NOT NULL AND embed_url = ?', embed_url).first).nil?
+ topic_embed.destroy!
+ end
+ end
+
class FetchResponse
attr_accessor :title, :body, :author
end
@@ -203,13 +211,15 @@ end
#
# Table name: topic_embeds
#
-# id :integer not null, primary key
-# topic_id :integer not null
-# post_id :integer not null
-# embed_url :string(1000) not null
-# content_sha1 :string(40)
-# created_at :datetime not null
-# updated_at :datetime not null
+# id :integer not null, primary key
+# topic_id :integer not null
+# post_id :integer not null
+# embed_url :string(1000) not null
+# content_sha1 :string(40)
+# created_at :datetime not null
+# updated_at :datetime not null
+# deleted_at :datetime
+# deleted_by_id :integer
#
# Indexes
#
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/app/models/topic_user.rb b/app/models/topic_user.rb
index 49c43e7b4e..7dd30be2df 100644
--- a/app/models/topic_user.rb
+++ b/app/models/topic_user.rb
@@ -38,7 +38,10 @@ class TopicUser < ActiveRecord::Base
end
def auto_notification(user_id, topic_id, reason, notification_level)
- if TopicUser.where(user_id: user_id, topic_id: topic_id, notifications_reason_id: nil).exists?
+ if TopicUser.where("user_id = :user_id AND topic_id = :topic_id AND (notifications_reason_id IS NULL OR
+ (notification_level < :notification_level AND notification_level > :normal_notification_level))",
+ user_id: user_id, topic_id: topic_id, notification_level: notification_level,
+ normal_notification_level: notification_levels[:regular]).exists?
change(user_id, topic_id,
notification_level: notification_level,
notifications_reason_id: reason
@@ -136,7 +139,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/app/models/upload.rb b/app/models/upload.rb
index d9aa27eaf3..1ea66d12d2 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -105,6 +105,9 @@ class Upload < ActiveRecord::Base
DistributedMutex.synchronize("upload_#{user_id}_#{filename}") do
# do some work on images
if FileHelper.is_image?(filename) && is_actual_image?(file)
+ # retrieve image info
+ w, h = FastImage.size(file) || [0, 0]
+
if filename[/\.svg$/i]
# whitelist svg elements
doc = Nokogiri::XML(file)
@@ -112,20 +115,15 @@ class Upload < ActiveRecord::Base
File.write(file.path, doc.to_s)
file.rewind
else
- # ensure image isn't huge
- w, h = FastImage.size(file) || [0, 0]
if w * h >= SiteSetting.max_image_megapixels * 1_000_000
upload.errors.add(:base, I18n.t("upload.images.larger_than_x_megapixels", max_image_megapixels: SiteSetting.max_image_megapixels))
return upload
end
# fix orientation first
- fix_image_orientation(file.path) if should_optimize?(file.path)
+ fix_image_orientation(file.path) if should_optimize?(file.path, [w, h])
end
- # retrieve image info
- w, h = FastImage.size(file) || [0, 0]
-
# default size
width, height = ImageSizer.resize(w, h)
@@ -156,8 +154,13 @@ class Upload < ActiveRecord::Base
end
# optimize image (except GIFs, SVGs and large PNGs)
- if should_optimize?(file.path)
- ImageOptim.new.optimize_image!(file.path) rescue nil
+ if should_optimize?(file.path, [w, h])
+ begin
+ ImageOptim.new.optimize_image!(file.path)
+ rescue ImageOptim::Worker::TimeoutExceeded
+ # Don't optimize if it takes too long
+ Rails.logger.warn("ImageOptim timed out while optimizing #{filename}")
+ end
# update the file size
filesize = File.size(file.path)
end
@@ -225,11 +228,13 @@ class Upload < ActiveRecord::Base
LARGE_PNG_SIZE ||= 3.megabytes
- def self.should_optimize?(path)
+ def self.should_optimize?(path, dimensions = nil)
# don't optimize GIFs or SVGs
return false if path =~ /\.(gif|svg)$/i
return true if path !~ /\.png$/i
- w, h = FastImage.size(path) || [0, 0]
+
+ dimensions ||= (FastImage.size(path) || [0, 0])
+ w, h = dimensions
# don't optimize large PNGs
w > 0 && h > 0 && w * h < LARGE_PNG_SIZE
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 8c4a0a130a..f3d7e325c6 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
@@ -162,9 +162,15 @@ class User < ActiveRecord::Base
def self.username_available?(username)
lower = username.downcase
+ !reserved_username?(lower) && !User.where(username_lower: lower).exists?
+ 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
@@ -270,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
@@ -608,7 +614,7 @@ class User < ActiveRecord::Base
end
def warnings_received_count
- warnings.count
+ user_warnings.count
end
def flags_received_count
@@ -734,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
@@ -791,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
@@ -883,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/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/models/warning.rb b/app/models/user_warning.rb
similarity index 69%
rename from app/models/warning.rb
rename to app/models/user_warning.rb
index 0d177fb0c6..dd89c7f995 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'
@@ -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/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/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/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..f119d79a75 100644
--- a/app/serializers/color_scheme_serializer.rb
+++ b/app/serializers/color_scheme_serializer.rb
@@ -1,8 +1,8 @@
class ColorSchemeSerializer < ApplicationSerializer
- attributes :id, :name, :enabled, :is_base
+ attributes :id, :name, :is_base, :base_scheme_id, :theme_id, :theme_name
has_many :colors, serializer: ColorSchemeColorSerializer, embed: :objects
- def base
- object.is_base || false
+ def theme_name
+ object.theme&.name
end
end
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/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..ab9af520a0
--- /dev/null
+++ b/app/serializers/theme_serializer.rb
@@ -0,0 +1,50 @@
+class ThemeFieldSerializer < ApplicationSerializer
+ attributes :name, :target, :value, :error
+
+ def target
+ case object.target
+ when 0 then "common"
+ when 1 then "desktop"
+ when 2 then "mobile"
+ end
+ end
+
+ def include_error?
+ object.error.present?
+ 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
+
+ def child_themes
+ object.child_themes.order(:name)
+ end
+end
diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb
index 848c3e3f02..c438bacb6f 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|
@@ -33,7 +34,8 @@ class TopicViewSerializer < ApplicationSerializer
:word_count,
:deleted_at,
:pending_posts_count,
- :user_id
+ :user_id,
+ :pm_with_non_human_user?
attributes :draft,
:draft_key,
@@ -58,7 +60,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
@@ -69,7 +72,7 @@ class TopicViewSerializer < ApplicationSerializer
last_poster: BasicUserSerializer.new(topic.last_poster, scope: scope, root: false)
}
- if object.topic.private_message?
+ if private_message?(topic)
allowed_user_ids = Set.new
result[:allowed_groups] = object.topic.allowed_groups.map do |group|
@@ -127,7 +130,7 @@ class TopicViewSerializer < ApplicationSerializer
end
def is_warning
- object.topic.private_message? && object.topic.subtype == TopicSubtype.moderator_warning
+ private_message?(object.topic) && object.topic.subtype == TopicSubtype.moderator_warning
end
def include_is_warning?
@@ -147,7 +150,7 @@ class TopicViewSerializer < ApplicationSerializer
end
def include_message_archived?
- object.topic.private_message?
+ private_message?(object.topic)
end
def message_archived
@@ -264,4 +267,22 @@ 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
+
+ def include_pm_with_non_human_user?
+ private_message?(object.topic)
+ end
+
+ private
+
+ def private_message?(topic)
+ @private_message ||= topic.private_message?
+ end
+
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/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/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_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/_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/common/_special_font_face.html.erb b/app/views/common/_special_font_face.html.erb
index 8e95e5ece8..6436768afd 100644
--- a/app/views/common/_special_font_face.html.erb
+++ b/app/views/common/_special_font_face.html.erb
@@ -9,11 +9,13 @@
%>
<% font_domain = "#{request.protocol}#{request.host_with_port}&2" %>
+<% woff2_url = "#{asset_path("fontawesome-webfont.woff2")}?#{font_domain}&v=4.7.0".html_safe %>
+
diff --git a/app/views/email/_mailing_list_post.html.erb b/app/views/email/_mailing_list_post.html.erb
new file mode 100644
index 0000000000..43d09820e2
--- /dev/null
+++ b/app/views/email/_mailing_list_post.html.erb
@@ -0,0 +1,5 @@
+
+ <%= raw format_topic_title(topic.title) %>
+ <%= post_count %>
+ <%= category_badge(topic.category, inline_style: true, absolute_url: true) %>
+
diff --git a/app/views/email/_secure_mailing_list_post.html.erb b/app/views/email/_secure_mailing_list_post.html.erb
new file mode 100644
index 0000000000..418a6b79fa
--- /dev/null
+++ b/app/views/email/_secure_mailing_list_post.html.erb
@@ -0,0 +1,4 @@
+
+ <%= email_topic_link(topic) %>
+ <%= post_count %>
+
diff --git a/app/views/email/notification.html.erb b/app/views/email/notification.html.erb
index 5e33b4625e..5809973d56 100644
--- a/app/views/email/notification.html.erb
+++ b/app/views/email/notification.html.erb
@@ -2,28 +2,32 @@
- <%= render partial: 'email/post', locals: { post: post, use_excerpt: false } %>
+ <%- if SiteSetting.private_email? %>
+ <%= t('system_messages.contents_hidden') %>
+ <% else %>
+ <%= render partial: 'email/post', locals: { post: post, use_excerpt: false } %>
- <% if in_reply_to_post.present? || context_posts.present? %>
-
-
- <% end %>
-
- <% if in_reply_to_post.present? %>
- <%= t "user_notifications.in_reply_to" %>
- <%= render partial: 'email/post', locals: { post: in_reply_to_post, use_excerpt: true} %>
- <% end %>
-
- <% if context_posts.present? %>
- <%= t "user_notifications.previous_discussion" %>
- <% context_posts.each do |p| %>
- <%= render partial: 'email/post', locals: { post: p, use_excerpt: false } %>
+ <% if in_reply_to_post.present? || context_posts.present? %>
+
+
<% end %>
- <% end %>
- <% if reached_limit %>
-
-
+ <% if in_reply_to_post.present? %>
+ <%= t "user_notifications.in_reply_to" %>
+ <%= render partial: 'email/post', locals: { post: in_reply_to_post, use_excerpt: true} %>
+ <% end %>
+
+ <% if context_posts.present? %>
+ <%= t "user_notifications.previous_discussion" %>
+ <% context_posts.each do |p| %>
+ <%= render partial: 'email/post', locals: { post: p, use_excerpt: false } %>
+ <% end %>
+ <% end %>
+
+ <% if reached_limit %>
+
+
+ <% end %>
<% end %>
@@ -33,7 +37,7 @@
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 41b321f22d..984b0e198a 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -4,9 +4,8 @@
<%= content_for?(:title) ? yield(:title) + ' - ' + SiteSetting.title : SiteSetting.title %>
+
<%= render partial: "layouts/head" %>
- <%= render partial: "common/special_font_face" %>
- <%= render partial: "common/discourse_stylesheet" %>
<%= discourse_csrf_tags %>
<%- if SiteSetting.enable_escaped_fragments? %>
@@ -22,26 +21,26 @@
window.EmberENV['FORCE_JQUERY'] = true;
- <%= script "locales/#{I18n.locale}" %>
- <%= script "ember_jquery" %>
- <%= script "preload-store" %>
- <%= script "vendor" %>
- <%= script "pretty-text-bundle" %>
- <%= script "application" %>
+ <%= preload_script "locales/#{I18n.locale}" %>
+ <%= preload_script "ember_jquery" %>
+ <%= preload_script "preload-store" %>
+ <%= preload_script "vendor" %>
+ <%= preload_script "pretty-text-bundle" %>
+ <%= preload_script "application" %>
<%- if allow_plugins? %>
- <%= script "plugin" %>
+ <%= preload_script "plugin" %>
<%- end %>
<%- if allow_third_party_plugins? %>
- <%= script "plugin-third-party" %>
+ <%= preload_script "plugin-third-party" %>
<%- end %>
<%- if staff? %>
- <%= script "admin" %>
+ <%= preload_script "admin" %>
<%- end %>
<%- unless customization_disabled? %>
- <%= raw SiteCustomization.custom_head_tag(session[:preview_style]) %>
+ <%= raw theme_lookup("head_tag") %>
<%- end %>
<%= render_google_universal_analytics_code %>
@@ -51,7 +50,12 @@
<%- end %>
+ <%= render partial: "common/discourse_stylesheet" %>
+ <%= render partial: "common/special_font_face" %>
+
<%= yield :head %>
+
+ <%= raw build_plugin_html 'before-head-close' %>
@@ -82,7 +86,7 @@
<%- unless customization_disabled? || loading_admin? %>
- <%= SiteCustomization.custom_header(session[:preview_style], mobile_view? ? :mobile : :desktop) %>
+ <%= theme_lookup("header") %>
<%- end %>
@@ -118,7 +122,8 @@
<%= render_google_analytics_code %>
<%- unless customization_disabled? %>
- <%= raw SiteCustomization.custom_body_tag(session[:preview_style]) %>
+ <%= raw theme_lookup("body_tag") %>
<%- end %>
+ <%= raw build_plugin_html 'before-body-close' %>
|