-
-
- {{#each getTabs as |tab|}}
- -
- {{#link-to tab.location model title=tab.message}}
- {{tab.message}}
- {{#if tab.count}}({{tab.count}}){{/if}}
- {{/link-to}}
-
- {{/each}}
-
-
+
+
+
+ {{#if model.flair_url}}
+
+ {{avatar-flair
+ flairURL=model.flair_url
+ flairBgColor=model.flair_bg_color
+ flairColor=model.flair_color
+ groupName=model.name}}
+
+ {{/if}}
-
-
-
+
+
+
+ {{#if model.title}}
+ @{{model.name}}
+ {{/if}}
+
+
+ {{#if canEditGroup}}
+
+ {{d-button action="showGroupEditor" label="group.edit.title" class="group-edit-btn" icon="pencil"}}
+
+ {{/if}}
+
+
+ {{#if model.bio_cooked}}
+
+
+
+
{{{model.bio_cooked}}}
+
+ {{/if}}
+
+
+ {{#mobile-nav class='group-nav' desktopClass="pull-left nav nav-stacked" currentPath=currentPath}}
+ {{#each getTabs as |tab|}}
+
+ {{#link-to tab.location model title=tab.message}}
+ {{tab.message}}
+ {{#if tab.count}}({{tab.count}}){{/if}}
+ {{/link-to}}
+
+ {{/each}}
+ {{/mobile-nav}}
+
+
diff --git a/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs b/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs
index bc21dfd6a7..da825c4774 100644
--- a/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs
+++ b/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs
@@ -7,6 +7,9 @@
{{raw "topic-status" topic=topic}}
{{topic-link topic}}
+ {{#if topic.featured_link}}
+ {{topic-featured-link topic}}
+ {{/if}}
{{plugin-outlet "topic-list-after-title"}}
{{#if showTopicPostBadges}}
{{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl}}
diff --git a/app/assets/javascripts/discourse/templates/modal/edit-group.hbs b/app/assets/javascripts/discourse/templates/modal/edit-group.hbs
new file mode 100644
index 0000000000..60a762eb7e
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/modal/edit-group.hbs
@@ -0,0 +1,16 @@
+{{#d-modal-body title="group.edit.title" class="edit-group groups"}}
+
+{{/d-modal-body}}
+
+
diff --git a/app/assets/javascripts/discourse/templates/modal/history.hbs b/app/assets/javascripts/discourse/templates/modal/history.hbs
index eac25480d5..ce860fefcb 100644
--- a/app/assets/javascripts/discourse/templates/modal/history.hbs
+++ b/app/assets/javascripts/discourse/templates/modal/history.hbs
@@ -86,6 +86,13 @@
{{/each}}
{{/if}}
+ {{#if model.featured_link_changes}}
+
+ {{model.featured_link_changes.previous}}
+ →
+ {{model.featured_link_changes.current}}
+
+ {{/if}}
{{plugin-outlet "post-revisions"}}
diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs
index 65c0f3f6a8..aed888a39f 100644
--- a/app/assets/javascripts/discourse/templates/topic.hbs
+++ b/app/assets/javascripts/discourse/templates/topic.hbs
@@ -25,6 +25,9 @@
{{category-chooser valueAttribute="id" value=buffered.category_id}}
{{/if}}
+ {{#if canEditTopicFeaturedLink}}
+ {{text-field type="url" value=buffered.featured_link id='topic-featured-link' placeholderKey="composer.topic_featured_link_placeholder"}}
+ {{/if}}
{{#if canEditTags}}
{{tag-chooser tags=buffered.tags categoryId=buffered.category_id}}
diff --git a/app/assets/javascripts/discourse/templates/user-invited-show.hbs b/app/assets/javascripts/discourse/templates/user-invited-show.hbs
index 6588a53e01..fc8657b0d4 100644
--- a/app/assets/javascripts/discourse/templates/user-invited-show.hbs
+++ b/app/assets/javascripts/discourse/templates/user-invited-show.hbs
@@ -16,7 +16,7 @@
{{d-button icon="plus" action="showInvite" label="user.invited.create" class="btn"}}
{{#if canBulkInvite}}
- {{resumable-upload target="/invites/upload" success="uploadSuccess" error="uploadError" uploadText=uploadText}}
+ {{csv-uploader uploading=uploading}}
{{/if}}
{{#if showReinviteAllButton}}
{{#if reinvitedAll}}
diff --git a/app/assets/javascripts/discourse/templates/user/activity.hbs b/app/assets/javascripts/discourse/templates/user/activity.hbs
index c5243f1ffb..f1818f2ec9 100644
--- a/app/assets/javascripts/discourse/templates/user/activity.hbs
+++ b/app/assets/javascripts/discourse/templates/user/activity.hbs
@@ -23,12 +23,12 @@
{{/link-to}}
{{/if}}
- {{plugin-outlet "user-activity-bottom"}}
+ {{plugin-outlet "user-activity-bottom" tagName='li'}}
{{/mobile-nav}}
{{#if viewingSelf}}
- {{d-button action="exportUserArchive" label="user.download_archive" icon="download"}}
+ {{d-button action="exportUserArchive" label="user.download_archive.button_text" icon="download"}}
{{/if}}
{{/d-section}}
diff --git a/app/assets/javascripts/discourse/templates/user/stream.hbs b/app/assets/javascripts/discourse/templates/user/stream.hbs
index 0a06e728c8..220ae57ba8 100644
--- a/app/assets/javascripts/discourse/templates/user/stream.hbs
+++ b/app/assets/javascripts/discourse/templates/user/stream.hbs
@@ -1,3 +1,8 @@
+{{#if model.noContent}}
+
+ {{{model.noContentHelp}}}
+
+{{/if}}
{{#user-stream stream=model}}
{{#each model.content as |item|}}
{{stream-item item=item removeBookmark="removeBookmark"}}
diff --git a/app/assets/javascripts/discourse/templates/user/summary.hbs b/app/assets/javascripts/discourse/templates/user/summary.hbs
index ffb578edcb..e36928a986 100644
--- a/app/assets/javascripts/discourse/templates/user/summary.hbs
+++ b/app/assets/javascripts/discourse/templates/user/summary.hbs
@@ -37,6 +37,7 @@
{{user-stat value=model.likes_received label="user.summary.likes_received"}}
+ {{plugin-outlet "user-summary-stat" tagName="li"}}
diff --git a/app/assets/javascripts/discourse/widgets/avatar-flair.js.es6 b/app/assets/javascripts/discourse/widgets/avatar-flair.js.es6
index 55beefc8cd..265d6a69b5 100644
--- a/app/assets/javascripts/discourse/widgets/avatar-flair.js.es6
+++ b/app/assets/javascripts/discourse/widgets/avatar-flair.js.es6
@@ -37,4 +37,4 @@ createWidget('avatar-flair', {
return [];
}
}
-});
\ No newline at end of file
+});
diff --git a/app/assets/javascripts/discourse/widgets/header-topic-info.js.es6 b/app/assets/javascripts/discourse/widgets/header-topic-info.js.es6
index 8704830bbd..a3d5ad585d 100644
--- a/app/assets/javascripts/discourse/widgets/header-topic-info.js.es6
+++ b/app/assets/javascripts/discourse/widgets/header-topic-info.js.es6
@@ -4,6 +4,7 @@ import { iconNode } from 'discourse/helpers/fa-icon-node';
import DiscourseURL from 'discourse/lib/url';
import RawHtml from 'discourse/widgets/raw-html';
import { tagNode } from 'discourse/lib/render-tag';
+import { topicFeaturedLinkNode } from 'discourse/lib/render-topic-featured-link';
export default createWidget('header-topic-info', {
tagName: 'div.extra-info-wrapper',
@@ -44,12 +45,19 @@ export default createWidget('header-topic-info', {
title.push(this.attach('category-link', { category }));
}
+ const extra = [];
if (this.siteSettings.tagging_enabled) {
const tags = topic.get('tags') || [];
if (tags.length) {
- title.push(h('div.list-tags', tags.map(tagNode)));
+ extra.push(h('div.list-tags', tags.map(tagNode)));
}
}
+ if (this.siteSettings.topic_featured_link_enabled) {
+ extra.push(topicFeaturedLinkNode(attrs.topic));
+ }
+ if (extra) {
+ title.push(h('div.topic-header-extra', extra));
+ }
}
const contents = h('div.title-wrapper', title);
diff --git a/app/assets/javascripts/discourse/widgets/widget.js.es6 b/app/assets/javascripts/discourse/widgets/widget.js.es6
index bb35076a5e..1b6b436e80 100644
--- a/app/assets/javascripts/discourse/widgets/widget.js.es6
+++ b/app/assets/javascripts/discourse/widgets/widget.js.es6
@@ -5,7 +5,6 @@ import { WidgetClickHook,
WidgetDragHook } from 'discourse/widgets/hooks';
import { h } from 'virtual-dom';
import DecoratorHelper from 'discourse/widgets/decorator-helper';
-import { TARGET_NAME } from 'discourse/mixins/delegated-actions';
function emptyContent() { }
@@ -272,7 +271,7 @@ export default class Widget {
if (target) {
// TODO: Use ember closure actions
- const actions = target[TARGET_NAME] || target.actionHooks || {};
+ const actions = target.actions || target.actionHooks || {};
const method = actions[actionName];
if (method) {
promise = method.call(target, param);
diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss
index cb7bb105ce..13893d7cc2 100644
--- a/app/assets/stylesheets/common/admin/admin_base.scss
+++ b/app/assets/stylesheets/common/admin/admin_base.scss
@@ -696,38 +696,6 @@ section.details {
width: 100%;
border-color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%));
}
- .avatar-flair-preview {
- position: relative;
- width: 45px;
-
- .avatar-wrapper {
- background-color: #f4f4f4;
- }
- }
- .form-horizontal {
- .flair_inputs {
- margin-top: 30px;
- margin-bottom: 30px;
-
- .flair_left {
- float: left;
- width: 60%;
- input[name=flair_url] {
- width: 90%;
- }
- }
-
- .flair_right {
- float: left;
- margin-left: 30px;
- }
- }
- }
-}
-.row.groups {
- input[type='text'].flair_bg_color, input[type='text'].flair_color {
- width: 200px;
- }
}
// Customise area
diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss
index 34abe80e41..cb1bccb1fc 100644
--- a/app/assets/stylesheets/common/base/_topic-list.scss
+++ b/app/assets/stylesheets/common/base/_topic-list.scss
@@ -88,6 +88,10 @@ html.anon .topic-list a.title:visited:not(.badge-notification) {color: dark-ligh
}
}
+ .topic-featured-link {
+ padding-left: 5px;
+ }
+
.topic-excerpt {
font-size: 0.929em;
margin-top: 8px;
diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss
index dfadbd6fb8..4e53da0fc2 100644
--- a/app/assets/stylesheets/common/base/compose.scss
+++ b/app/assets/stylesheets/common/base/compose.scss
@@ -187,6 +187,10 @@ div.ac-wrap {
}
}
+#reply-control.topic-featured-link-only.open {
+ .wmd-controls { display: none; }
+}
+
#cancel-file-upload {
font-size: 1.6em;
}
diff --git a/app/assets/stylesheets/common/base/group.scss b/app/assets/stylesheets/common/base/group.scss
new file mode 100644
index 0000000000..2157423bd3
--- /dev/null
+++ b/app/assets/stylesheets/common/base/group.scss
@@ -0,0 +1,121 @@
+.group-header {
+ font-size: 2.142em;
+ font-weight: normal;
+}
+
+.group-name {
+ font-weight: normal;
+ margin-top: 5px;
+ color: dark-light-diff($primary, $secondary, 50%, -50%);
+}
+
+.group-details-container {
+ background: rgba(230, 230, 230, 0.3);
+ padding: 20px;
+ margin-bottom: 30px;
+}
+
+table.group-members {
+ width: 100%;
+
+ th, tr {
+ border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
+ }
+
+ th {
+ text-align: left;
+ }
+
+ tr {
+ .user-info {
+ display: block;
+ }
+
+ td {
+ color: dark-light-diff($primary, $secondary, 50%, -50%);
+ padding: 0.8em 0;
+ }
+ }
+}
+
+.group-owner-label {
+ color: $primary;
+}
+
+.group-details {
+ width: 100%;
+}
+
+.group-details {
+ span {
+ display: inline-block;
+ vertical-align: middle;
+ }
+
+ .avatar-flair {
+ $size: 50px;
+
+ background-size: $size;
+ height: $size;
+ width: $size;
+
+ i {
+ font-size: $size !important;
+ }
+ }
+}
+
+.group-edit {
+ float: right;
+}
+
+.groups.edit-group .form-horizontal {
+ textarea {
+ width: 99%;
+ }
+
+ label {
+ font-weight: bold;
+ }
+
+ input[type="text"] {
+ width: 80% !important;
+ margin-bottom: 10px;
+ }
+
+ .group-flair-inputs {
+ display: inline-block;
+ margin-top: 30px;
+ margin-bottom: 30px;
+
+ .group-flair-left {
+ float: left;
+ }
+
+ .group-flair-right {
+ float: left;
+ margin-left: 30px;
+ }
+ }
+
+ .avatar-flair-preview {
+ position: relative;
+ width: 45px;
+
+ .avatar-wrapper {
+ background-color: #f4f4f4;
+ }
+ }
+}
+
+#add-user-to-group {
+ margin: 0px;
+
+ .ac-wrap {
+ width: 100% !important;
+ }
+
+ .add {
+ margin-top: 10px;
+ }
+}
diff --git a/app/assets/stylesheets/common/base/login.scss b/app/assets/stylesheets/common/base/login.scss
index b24e1ccc5c..7024567bae 100644
--- a/app/assets/stylesheets/common/base/login.scss
+++ b/app/assets/stylesheets/common/base/login.scss
@@ -30,6 +30,7 @@ $input-width: 220px;
.disclaimer {
font-size: 0.9em;
color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%));
+ clear: both;
}
.user-field.confirm {
diff --git a/app/assets/stylesheets/common/base/tagging.scss b/app/assets/stylesheets/common/base/tagging.scss
index 332614c4ac..2a32c29d3b 100644
--- a/app/assets/stylesheets/common/base/tagging.scss
+++ b/app/assets/stylesheets/common/base/tagging.scss
@@ -27,18 +27,11 @@
}
}
-.extra-info-wrapper {
- .list-tags {
- padding-top: 5px;
- }
-
- .discourse-tag {
- -webkit-animation: fadein .7s;
- animation: fadein .7s;
- }
+.topic-header-extra .discourse-tag {
+ -webkit-animation: fadein .7s;
+ animation: fadein .7s;
}
-
.add-tags .select2 {
margin: 0;
}
@@ -136,11 +129,11 @@ $tag-color: scale-color($primary, $lightness: 40%);
top: -0.1em;
}
-header .discourse-tag {color: $tag-color !important; }
+header .discourse-tag {color: $tag-color }
.list-tags {
+ margin-right: 3px;
display: inline;
- margin: 0 0 0 5px;
font-size: 0.857em;
}
@@ -171,24 +164,6 @@ header .discourse-tag {color: $tag-color !important; }
left: auto;
}
-.bullet + .list-tags {
- display: block;
- line-height: 15px;
-}
-
-.bar + .list-tags {
- line-height: 1.25;
- .discourse-tag {
- vertical-align: middle;
- }
-}
-
-.box + .list-tags {
- display: inline-block;
- margin: 5px 0 0 5px;
- padding-top: 2px;
-}
-
.tag-sort-options {
margin-bottom: 20px;
a {
diff --git a/app/assets/stylesheets/common/base/topic.scss b/app/assets/stylesheets/common/base/topic.scss
index 38e17a48da..def557e03e 100644
--- a/app/assets/stylesheets/common/base/topic.scss
+++ b/app/assets/stylesheets/common/base/topic.scss
@@ -9,6 +9,10 @@
.badge-wrapper {
float: left;
}
+
+ a.topic-featured-link {
+ display: inline-block;
+ }
}
a.badge-category {
@@ -47,7 +51,7 @@
display: inline;
}
-#suggested-topics h3 .badge-wrapper.bullet span.badge-category, {
+#suggested-topics h3 .badge-wrapper.bullet span.badge-category {
// Override vertical-align: text-top from `badges.css.scss`
vertical-align: baseline;
line-height: 1.2;
@@ -133,3 +137,18 @@
}
}
}
+
+a.topic-featured-link {
+ display: inline-block;
+ text-transform: lowercase;
+ color: #858585;
+ font-size: 0.875rem;
+
+ &::before {
+ position: relative;
+ top: 0.1em;
+ padding-right: 3px;
+ font-family: FontAwesome;
+ content: "\f08e";
+ }
+}
diff --git a/app/assets/stylesheets/common/components/badges.css.scss b/app/assets/stylesheets/common/components/badges.css.scss
index ed14534894..8273d97c8b 100644
--- a/app/assets/stylesheets/common/components/badges.css.scss
+++ b/app/assets/stylesheets/common/components/badges.css.scss
@@ -133,6 +133,10 @@
}
}
+.extra-info-wrapper .title-wrapper .badge-wrapper.bar {
+ margin-top: 6px;
+}
+
.autocomplete, td.category {
.badge-wrapper {
max-width: 230px;
diff --git a/app/assets/stylesheets/desktop.scss b/app/assets/stylesheets/desktop.scss
index 989d6cb286..807b43c726 100644
--- a/app/assets/stylesheets/desktop.scss
+++ b/app/assets/stylesheets/desktop.scss
@@ -19,6 +19,7 @@
@import "desktop/history";
@import "desktop/queued-posts";
@import "desktop/menu-panel";
+@import "desktop/group";
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */
diff --git a/app/assets/stylesheets/desktop/category-list.scss b/app/assets/stylesheets/desktop/category-list.scss
index 151c01c2cd..d33120135f 100644
--- a/app/assets/stylesheets/desktop/category-list.scss
+++ b/app/assets/stylesheets/desktop/category-list.scss
@@ -151,6 +151,10 @@
font-size: 0.75em;
}
+ .topic-featured-link {
+ padding-left: 8px;
+ }
+
.topic-list {
.posts {
width: 100%;
diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss
index 738fb583e8..fbfcd4e874 100644
--- a/app/assets/stylesheets/desktop/compose.scss
+++ b/app/assets/stylesheets/desktop/compose.scss
@@ -298,6 +298,11 @@
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
}
}
+ #topic-featured-link {
+ padding: 7px 10px;
+ margin: 6px 10px 3px 0;
+ width: 400px;
+ }
.d-editor-input:disabled {
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
}
@@ -465,6 +470,10 @@
}
}
+#reply-control.topic-featured-link-only.open {
+ height: 200px;
+}
+
.control-row.reply-area {
padding-left: 20px;
padding-right: 20px;
diff --git a/app/assets/stylesheets/desktop/group.scss b/app/assets/stylesheets/desktop/group.scss
new file mode 100644
index 0000000000..e368da25fd
--- /dev/null
+++ b/app/assets/stylesheets/desktop/group.scss
@@ -0,0 +1,12 @@
+.group-outlet {
+ width: 75%;
+}
+
+.group-nav {
+ width: 20%;
+ margin-right: 30px;
+}
+
+.group-details {
+ margin-bottom: 20px;
+}
diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss
index a53b90f9ba..2c0c57e4fd 100644
--- a/app/assets/stylesheets/desktop/topic-post.scss
+++ b/app/assets/stylesheets/desktop/topic-post.scss
@@ -505,13 +505,13 @@ video {
.extra-info-wrapper {
overflow: hidden;
- .badge-wrapper, i, .topic-link {
+ .badge-wrapper, i, .topic-link {
-webkit-animation: fadein .7s;
animation: fadein .7s;
}
.topic-statuses {
- i { color: $header_primary; }
+ i { color: $header_primary; }
i.fa-envelope { color: $danger; }
.unpinned { color: $header_primary; }
}
@@ -523,6 +523,26 @@ video {
overflow: hidden;
text-overflow: ellipsis;
}
+
+ .topic-header-extra {
+ margin: 0 0 0 5px;
+ padding-top: 5px;
+ }
+}
+
+.bullet + .topic-header-extra {
+ display: block;
+ line-height: 12px;
+}
+
+.bar + .topic-header-extra {
+ line-height: 1.25;
+}
+
+.box + .topic-header-extra {
+ display: inline-block;
+ margin: 0 0 0 5px;
+ padding-top: 5px;
}
/* default docked header CSS for all topics, including those without categories */
diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss
index 08036e0eb5..5ec7d0ecd8 100644
--- a/app/assets/stylesheets/desktop/user.scss
+++ b/app/assets/stylesheets/desktop/user.scss
@@ -133,50 +133,6 @@
}
}
- table.group-members {
- width: 100%;
- p {
- max-width: 600px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- th {
- padding: 0.5em;
- text-align: right;
- border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
- }
- td.avatar {
- width: 60px;
- position: relative;
- .is-owner {
- position: absolute;
- right: 0;
- top: 20px;
- color: dark-light-diff($primary, $secondary, 50%, -50%);
- }
- }
- td.remove-user {
- text-align: right;
- }
- td {
- padding: 0.5em;
- border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
- img {
- margin-right: 10px;
- }
- span.text {
- float: right;
- font-size: 1.2em;
- color: dark-light-diff($primary, $secondary, 50%, -50%);
- }
- }
- }
-
- .user-right.groups {
- margin-top: 0;
- }
-
.user-right {
width: 900px;
margin-top: 20px;
diff --git a/app/assets/stylesheets/mobile.scss b/app/assets/stylesheets/mobile.scss
index 4152083286..ce220071ee 100644
--- a/app/assets/stylesheets/mobile.scss
+++ b/app/assets/stylesheets/mobile.scss
@@ -22,6 +22,7 @@
@import "mobile/search";
@import "mobile/emoji";
@import "mobile/ring";
+@import "mobile/group";
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */
diff --git a/app/assets/stylesheets/mobile/compose.scss b/app/assets/stylesheets/mobile/compose.scss
index 088aa7d0cd..f4b149a618 100644
--- a/app/assets/stylesheets/mobile/compose.scss
+++ b/app/assets/stylesheets/mobile/compose.scss
@@ -16,7 +16,10 @@ display: none !important; // can be removed if inline JS CSS is removed from com
input {
background: $secondary;
color: $primary;
- border-color: blend-primary-secondary(15%);
+ padding: 4px;
+ border-radius: 3px;
+ box-shadow: inset 0 1px 1px rgba(0,0,0, .3);
+ border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
}
#reply-control {
diff --git a/app/assets/stylesheets/mobile/group.scss b/app/assets/stylesheets/mobile/group.scss
new file mode 100644
index 0000000000..120e980d0f
--- /dev/null
+++ b/app/assets/stylesheets/mobile/group.scss
@@ -0,0 +1,49 @@
+.group {
+ margin-top: 15px;
+}
+
+.group-header {
+ margin: 0px;
+}
+
+.group-name {
+ margin: 5px 0px 0px 0px;
+}
+
+.group-nav, .group-outlet {
+ width: 100%;
+}
+
+.group-details-container {
+ margin-bottom: 15px;
+}
+
+.group-nav.mobile-nav {
+ margin-bottom: 15px;
+
+ > li {
+ a {
+ color: white;
+
+ .fa { color: white; }
+ }
+ }
+
+ background-color: $quaternary;
+}
+
+table.group-members {
+ th {
+ text-align: center;
+ }
+
+ tr {
+ .user-info {
+ width: 130px;
+ }
+
+ td {
+ padding-left: 0.5em;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/mobile/login.scss b/app/assets/stylesheets/mobile/login.scss
index bcc1a06424..9359b1c47f 100644
--- a/app/assets/stylesheets/mobile/login.scss
+++ b/app/assets/stylesheets/mobile/login.scss
@@ -19,7 +19,11 @@
color: dark-light-choose(scale-color($primary, $lightness: 35%), scale-color($secondary, $lightness: 65%));
}
label { float: left; display: block; }
- textarea, input, select {font-size: 1.143em; clear: left; margin-top: 0; }
+ textarea, input, select {
+ font-size: 1.143em;
+ clear: left;
+ margin-top: 0;
+ }
td { padding: 4px; }
}
diff --git a/app/assets/stylesheets/mobile/modal.scss b/app/assets/stylesheets/mobile/modal.scss
index bc1689b97c..13e41e2a25 100644
--- a/app/assets/stylesheets/mobile/modal.scss
+++ b/app/assets/stylesheets/mobile/modal.scss
@@ -38,15 +38,16 @@
width: 95%;
}
-// an ember metamorph is inserted between btn's sometimes, breaking this rule, but only on mobile for some reason:
-// .modal-footer .btn + .btn {
-.modal-footer .btn {
+// we need a little extra room on mobile for the
+// stuff inside the footer to fit
+.modal-footer {
+ padding-right: 0;
+}
+
+.modal-footer .btn + .btn {
margin-right: 5px;
margin-bottom: 5px;
}
-.modal-footer .btn-group .btn + .btn {
- margin-left: -1px;
-}
.modal-header {
// we need tighter spacing on mobile for header
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index daca310124..e363aff74a 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -36,7 +36,7 @@ class Admin::GroupsController < Admin::AdminController
def create
group = Group.new
- group.name = (params[:name] || '').strip
+ group.name = (group_params[:name] || '').strip
save_group(group)
end
@@ -44,29 +44,29 @@ class Admin::GroupsController < Admin::AdminController
group = Group.find(params[:id])
# group rename is ignored for automatic groups
- group.name = params[:name] if params[:name] && !group.automatic
+ group.name = group_params[:name] if group_params[:name] && !group.automatic
save_group(group)
end
def save_group(group)
- group.alias_level = params[:alias_level].to_i if params[:alias_level].present?
- group.visible = params[:visible] == "true"
- grant_trust_level = params[:grant_trust_level].to_i
+ group.alias_level = group_params[:alias_level].to_i if group_params[:alias_level].present?
+ group.visible = group_params[:visible] == "true"
+ grant_trust_level = group_params[:grant_trust_level].to_i
group.grant_trust_level = (grant_trust_level > 0 && grant_trust_level <= 4) ? grant_trust_level : nil
- group.automatic_membership_email_domains = params[:automatic_membership_email_domains] unless group.automatic
- group.automatic_membership_retroactive = params[:automatic_membership_retroactive] == "true" unless group.automatic
+ group.automatic_membership_email_domains = group_params[:automatic_membership_email_domains] unless group.automatic
+ group.automatic_membership_retroactive = group_params[:automatic_membership_retroactive] == "true" unless group.automatic
- group.primary_group = group.automatic ? false : params["primary_group"] == "true"
+ group.primary_group = group.automatic ? false : group_params["primary_group"] == "true"
- group.incoming_email = group.automatic ? nil : params[:incoming_email]
+ group.incoming_email = group.automatic ? nil : group_params[:incoming_email]
- title = params[:title] if params[:title].present?
+ title = group_params[:title] if group_params[:title].present?
group.title = group.automatic ? nil : title
- group.flair_url = params[:flair_url].presence
- group.flair_bg_color = params[:flair_bg_color].presence
- group.flair_color = params[:flair_color].presence
+ group.flair_url = group_params[:flair_url].presence
+ group.flair_bg_color = group_params[:flair_bg_color].presence
+ group.flair_color = group_params[:flair_color].presence
if group.save
Group.reset_counters(group.id, :group_users)
@@ -124,7 +124,18 @@ class Admin::GroupsController < Admin::AdminController
protected
- def can_not_modify_automatic
- render json: {errors: I18n.t('groups.errors.can_not_modify_automatic')}, status: 422
- end
+ def can_not_modify_automatic
+ render json: {errors: I18n.t('groups.errors.can_not_modify_automatic')}, status: 422
+ end
+
+ private
+
+ def group_params
+ params.require(:group).permit(
+ :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
+ )
+ end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 223189b28d..f02c9c9759 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -110,6 +110,32 @@ class ApplicationController < ActionController::Base
end
end
+ def self.last_ar_cache_reset
+ @last_ar_cache_reset
+ end
+
+ def self.last_ar_cache_reset=(val)
+ @last_ar_cache_reset
+ end
+
+ rescue_from ActiveRecord::StatementInvalid do |e|
+
+ last_cache_reset = ApplicationController.last_ar_cache_reset
+
+ if e.message =~ /UndefinedColumn/ && (last_cache_reset.nil? || last_cache_reset < 30.seconds.ago)
+ Rails.logger.warn "Clear Active Record cache cause schema appears to have changed!"
+
+ ApplicationController.last_ar_cache_reset = Time.zone.now
+
+ ActiveRecord::Base.connection.query_cache.clear
+ (ActiveRecord::Base.connection.tables - %w[schema_migrations]).each do |table|
+ table.classify.constantize.reset_column_information rescue nil
+ end
+ end
+
+ raise e
+ end
+
class PluginDisabled < StandardError; end
# Handles requests for giant IDs that throw pg exceptions
@@ -130,7 +156,7 @@ class ApplicationController < ActionController::Base
end
rescue_from Discourse::ReadOnly do
- render_json_error I18n.t('read_only_mode_enabled'), type: :read_only, status: 405
+ render_json_error I18n.t('read_only_mode_enabled'), type: :read_only, status: 503
end
def rescue_discourse_actions(type, status_code, include_ember=false)
@@ -382,7 +408,7 @@ class ApplicationController < ActionController::Base
def preload_current_user_data
store_preloaded("currentUser", MultiJson.dump(CurrentUserSerializer.new(current_user, scope: guardian, root: false)))
- report = TopicTrackingState.report(current_user.id)
+ report = TopicTrackingState.report(current_user)
serializer = ActiveModel::ArraySerializer.new(report, each_serializer: TopicTrackingStateSerializer)
store_preloaded("topicTrackingStates", MultiJson.dump(serializer))
end
@@ -465,7 +491,7 @@ class ApplicationController < ActionController::Base
end
def mini_profiler_enabled?
- defined?(Rack::MiniProfiler) && guardian.is_developer?
+ defined?(Rack::MiniProfiler) && (guardian.is_developer? || Rails.env.development?)
end
def authorize_mini_profiler
diff --git a/app/controllers/finish_installation_controller.rb b/app/controllers/finish_installation_controller.rb
index 01bbf5bfcc..c9a30724d4 100644
--- a/app/controllers/finish_installation_controller.rb
+++ b/app/controllers/finish_installation_controller.rb
@@ -59,6 +59,7 @@ class FinishInstallationController < ApplicationController
end
def ensure_no_admins
+ preload_anonymous_data
raise Discourse::InvalidAccess.new unless SiteSetting.has_login_hint?
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index a39bdb3c98..6c449113ee 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -1,27 +1,26 @@
class GroupsController < ApplicationController
- before_filter :ensure_logged_in, only: [:set_notifications, :mentionable]
+ before_filter :ensure_logged_in, only: [
+ :set_notifications,
+ :mentionable,
+ :update
+ ]
+
skip_before_filter :preload_json, :check_xhr, only: [:posts_feed, :mentions_feed]
def show
render_serialized(find_group(:id), GroupShowSerializer, root: 'basic_group')
end
- def counts
- group = find_group(:group_id)
+ def update
+ group = Group.find(params[:id])
+ guardian.ensure_can_edit!(group)
- counts = {
- posts: group.posts_for(guardian).count,
- topics: group.posts_for(guardian).where(post_number: 1).count,
- mentions: group.mentioned_posts_for(guardian).count,
- members: group.users.count,
- }
-
- if guardian.can_see_group_messages?(group)
- counts[:messages] = group.messages_for(guardian).where(post_number: 1).count
+ if group.update_attributes(group_params)
+ render json: success_json
+ else
+ render_json_error(group)
end
-
- render json: { counts: counts }
end
def posts
@@ -169,11 +168,21 @@ class GroupsController < ApplicationController
private
- def find_group(param_name)
- name = params.require(param_name)
- group = Group.find_by("lower(name) = ?", name.downcase)
- guardian.ensure_can_see!(group)
- group
- end
+ def group_params
+ params.require(:group).permit(
+ :flair_url,
+ :flair_bg_color,
+ :flair_color,
+ :bio_raw,
+ :title
+ )
+ end
+
+ def find_group(param_name)
+ name = params.require(param_name)
+ group = Group.find_by("lower(name) = ?", name.downcase)
+ guardian.ensure_can_see!(group)
+ group
+ end
end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 01936f6871..ae62b9b873 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -6,7 +6,7 @@ class InvitesController < ApplicationController
skip_before_filter :check_xhr, :preload_json
skip_before_filter :redirect_to_login_if_required
- before_filter :ensure_logged_in, only: [:destroy, :create, :create_invite_link, :resend_invite, :resend_all_invites, :check_csv_chunk, :upload_csv_chunk]
+ before_filter :ensure_logged_in, only: [:destroy, :create, :create_invite_link, :resend_invite, :resend_all_invites, :upload_csv]
before_filter :ensure_new_registrations_allowed, only: [:show, :redeem_disposable_invite]
before_filter :ensure_not_logged_in, only: [:show, :redeem_disposable_invite]
@@ -147,48 +147,29 @@ class InvitesController < ApplicationController
render nothing: true
end
- def check_csv_chunk
+ def upload_csv
guardian.ensure_can_bulk_invite_to_forum!(current_user)
- filename = params.fetch(:resumableFilename)
- identifier = params.fetch(:resumableIdentifier)
- chunk_number = params.fetch(:resumableChunkNumber)
- current_chunk_size = params.fetch(:resumableCurrentChunkSize).to_i
+ file = params[:file] || params[:files].first
+ name = params[:name] || File.basename(file.original_filename, ".*")
+ extension = File.extname(file.original_filename)
- # path to chunk file
- chunk = Invite.chunk_path(identifier, filename, chunk_number)
- # check chunk upload status
- status = HandleChunkUpload.check_chunk(chunk, current_chunk_size: current_chunk_size)
-
- render nothing: true, status: status
- end
-
- def upload_csv_chunk
- guardian.ensure_can_bulk_invite_to_forum!(current_user)
-
- filename = params.fetch(:resumableFilename)
- return render status: 415, text: I18n.t("bulk_invite.file_should_be_csv") unless (filename.to_s.end_with?(".csv") || filename.to_s.end_with?(".txt"))
-
- file = params.fetch(:file)
- identifier = params.fetch(:resumableIdentifier)
- chunk_number = params.fetch(:resumableChunkNumber).to_i
- chunk_size = params.fetch(:resumableChunkSize).to_i
- total_size = params.fetch(:resumableTotalSize).to_i
- current_chunk_size = params.fetch(:resumableCurrentChunkSize).to_i
-
- # path to chunk file
- chunk = Invite.chunk_path(identifier, filename, chunk_number)
- # upload chunk
- HandleChunkUpload.upload_chunk(chunk, file: file)
-
- uploaded_file_size = chunk_number * chunk_size
- # when all chunks are uploaded
- if uploaded_file_size + current_chunk_size >= total_size
- # handle bulk_invite processing in a background thread
- Jobs.enqueue(:bulk_invite, filename: filename, identifier: identifier, chunks: chunk_number, current_user_id: current_user.id)
+ Scheduler::Defer.later("Upload CSV") do
+ begin
+ data = if extension == ".csv"
+ path = Invite.create_csv(file, name)
+ Jobs.enqueue(:bulk_invite, filename: "#{name}.csv", current_user_id: current_user.id)
+ {url: path}
+ else
+ failed_json.merge(errors: [I18n.t("bulk_invite.file_should_be_csv")])
+ end
+ rescue
+ failed_json.merge(errors: [I18n.t("bulk_invite.error")])
+ end
+ MessageBus.publish("/uploads/csv", data.as_json, user_ids: [current_user.id])
end
- render nothing: true
+ render json: success_json
end
def fetch_username
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index b2c3c85a3f..7f68a648ef 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -574,7 +574,6 @@ class PostsController < ApplicationController
end
- params.require(:raw)
result = params.permit(*permitted).tap do |whitelisted|
whitelisted[:image_sizes] = params[:image_sizes]
# TODO this does not feel right, we should name what meta_data is allowed
diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb
index d7dee2377b..1641c01956 100644
--- a/app/controllers/static_controller.rb
+++ b/app/controllers/static_controller.rb
@@ -4,7 +4,7 @@ require_dependency 'file_helper'
class StaticController < ApplicationController
skip_before_filter :check_xhr, :redirect_to_login_if_required
- skip_before_filter :verify_authenticity_token, only: [:cdn_asset, :enter, :favicon]
+ skip_before_filter :verify_authenticity_token, only: [:brotli_asset, :cdn_asset, :enter, :favicon]
PAGES_WITH_EMAIL_PARAM = ['login', 'password_reset', 'signup']
@@ -123,7 +123,35 @@ class StaticController < ApplicationController
response.headers["Last-Modified"] = Time.new('2000-01-01').httpdate
render text: data, content_type: "image/png"
end
+ end
+ def brotli_asset
+ path = File.expand_path(Rails.root + "public/assets/" + params[:path])
+ path += ".br"
+
+ # SECURITY what if path has /../
+ raise Discourse::NotFound unless path.start_with?(Rails.root.to_s + "/public/assets")
+
+ opts = { disposition: nil }
+ opts[:type] = "application/javascript" if path =~ /\.js.br$/
+
+ response.headers["Expires"] = 1.year.from_now.httpdate
+ response.headers["Cache-Control"] = 'max-age=31557600, public'
+ response.headers["Content-Encoding"] = 'br'
+ begin
+ response.headers["Last-Modified"] = File.ctime(path).httpdate
+ response.headers["Content-Length"] = File.size(path).to_s
+ rescue Errno::ENOENT
+ raise Discourse::NotFound
+ end
+
+ expires_in 1.year, public: true, must_revalidate: false
+
+ if File.exists?(path)
+ send_file(path, opts)
+ else
+ raise Discourse::NotFound
+ end
end
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index dad7ee9512..f72c1ede90 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -8,7 +8,14 @@ class TagsController < ::ApplicationController
before_filter :ensure_tags_enabled
skip_before_filter :check_xhr, only: [:tag_feed, :show, :index]
- before_filter :ensure_logged_in, only: [:notifications, :update_notifications, :update]
+ before_filter :ensure_logged_in, except: [
+ :index,
+ :show,
+ :tag_feed,
+ :search,
+ :check_hashtag,
+ Discourse.anonymous_filters.map { |f| :"show_#{f}"}
+ ].flatten
before_filter :set_category_from_params, except: [:index, :update, :destroy, :tag_feed, :search, :notifications, :update_notifications]
def index
@@ -40,30 +47,14 @@ class TagsController < ::ApplicationController
end
end
- # TODO: move all this to ListController
Discourse.filters.each do |filter|
define_method("show_#{filter}") do
@tag_id = params[:tag_id]
@additional_tags = params[:additional_tag_ids].to_s.split('/')
- page = params[:page].to_i
list_opts = build_topic_list_options
- query = TopicQuery.new(current_user, list_opts)
-
- results = query.send("#{filter}_results")
-
- if @filter_on_category
- category_ids = [@filter_on_category.id]
-
- unless list_opts[:no_subcategories]
- category_ids += @filter_on_category.subcategories.pluck(:id)
- end
-
- results = results.where(category_id: category_ids)
- end
-
- @list = query.create_list(:by_tag, {}, results)
+ @list = TopicQuery.new(current_user, list_opts).public_send("list_#{filter}")
@list.draft_key = Draft::NEW_TOPIC
@list.draft_sequence = DraftSequence.current(current_user, Draft::NEW_TOPIC)
diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb
index 4961793ffa..ef0cd8ba69 100644
--- a/app/controllers/topics_controller.rb
+++ b/app/controllers/topics_controller.rb
@@ -574,7 +574,7 @@ class TopicsController < ApplicationController
topic_ids = params[:topic_ids].map {|t| t.to_i}
elsif params[:filter] == 'unread'
tq = TopicQuery.new(current_user)
- topics = TopicQuery.unread_filter(tq.joined_topic_user).listable_topics
+ topics = TopicQuery.unread_filter(tq.joined_topic_user, staff: guardian.is_staff?).listable_topics
topics = topics.where('category_id = ?', params[:category_id]) if params[:category_id]
topic_ids = topics.pluck(:id)
else
diff --git a/app/controllers/user_actions_controller.rb b/app/controllers/user_actions_controller.rb
index 6ff74ec995..71dd2a5a12 100644
--- a/app/controllers/user_actions_controller.rb
+++ b/app/controllers/user_actions_controller.rb
@@ -24,7 +24,21 @@ class UserActionsController < ApplicationController
UserAction.stream(opts)
end
- render_serialized(stream, UserActionSerializer, root: 'user_actions')
+ stream = stream.to_a
+ if stream.length == 0 && (help_key = params['no_results_help_key'])
+ if user.id == guardian.user.try(:id)
+ help_key += ".self"
+ else
+ help_key += ".others"
+ end
+ render json: {
+ user_action: [],
+ no_results_help: I18n.t(help_key)
+ }
+ else
+ render_serialized(stream, UserActionSerializer, root: 'user_actions')
+ end
+
end
def show
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 0c226d93b2..d67c12455e 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -152,7 +152,7 @@ class UsersController < ApplicationController
user = fetch_user_from_params
guardian.ensure_can_edit!(user)
- report = TopicTrackingState.report(user.id)
+ report = TopicTrackingState.report(user)
serializer = ActiveModel::ArraySerializer.new(report, each_serializer: TopicTrackingStateSerializer)
render json: MultiJson.dump(serializer)
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index d79ace3f34..e13539e647 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -46,10 +46,12 @@ module ApplicationHelper
end
def script(*args)
- if SiteSetting.enable_cdn_js_debugging && GlobalSetting.cdn_url
- tags = javascript_include_tag(*args, "crossorigin" => "anonymous")
- tags.gsub!("/assets/", "/cdn_asset/#{Discourse.current_hostname.tr(".","_")}/")
- tags.gsub!(".js\"", ".js?v=1&origin=#{CGI.escape request.base_url}\"")
+ 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)
@@ -280,4 +282,15 @@ module ApplicationHelper
result.html_safe
end
+ def topic_featured_link_domain(link)
+ begin
+ uri = URI.encode(link)
+ uri = URI.parse(uri)
+ uri = URI.parse("http://#{uri}") if uri.scheme.nil?
+ host = uri.host.downcase
+ host.start_with?('www.') ? host[4..-1] : host
+ rescue
+ ''
+ end
+ end
end
diff --git a/app/jobs/regular/bulk_invite.rb b/app/jobs/regular/bulk_invite.rb
index 44b14abe64..6fb1eeb863 100644
--- a/app/jobs/regular/bulk_invite.rb
+++ b/app/jobs/regular/bulk_invite.rb
@@ -14,21 +14,12 @@ module Jobs
end
def execute(args)
- filename = args[:filename]
- identifier = args[:identifier]
- chunks = args[:chunks].to_i
+ filename = args[:filename]
@current_user = User.find_by(id: args[:current_user_id])
-
- raise Discourse::InvalidParameters.new(:filename) if filename.blank?
- raise Discourse::InvalidParameters.new(:identifier) if identifier.blank?
- raise Discourse::InvalidParameters.new(:chunks) if chunks <= 0
-
- # merge chunks, and get csv path
- csv_path = get_csv_path(filename, identifier, chunks)
+ raise Discourse::InvalidParameters.new(:filename) if filename.blank?
# read csv file, and send out invitations
- read_csv_file(csv_path)
-
+ read_csv_file("#{Invite.base_directory}/#{filename}")
ensure
# send notification to user regarding progress
notify_user
@@ -37,17 +28,6 @@ module Jobs
FileUtils.rm_rf(csv_path) rescue nil
end
- def get_csv_path(filename, identifier, chunks)
- csv_path = "#{Invite.base_directory}/#{filename}"
- tmp_csv_path = "#{csv_path}.tmp"
- # path to tmp directory
- tmp_directory = File.dirname(Invite.chunk_path(identifier, filename, 0))
- # merge all chunks
- HandleChunkUpload.merge_chunks(chunks, upload_path: csv_path, tmp_upload_path: tmp_csv_path, model: Invite, identifier: identifier, filename: filename, tmp_directory: tmp_directory)
-
- return csv_path
- end
-
def read_csv_file(csv_path)
CSV.foreach(csv_path, encoding: "iso-8859-1:UTF-8") do |csv_info|
if csv_info[0]
diff --git a/app/jobs/scheduled/poll_feed.rb b/app/jobs/scheduled/poll_feed.rb
index 8cfed44571..8c0e1e75b3 100644
--- a/app/jobs/scheduled/poll_feed.rb
+++ b/app/jobs/scheduled/poll_feed.rb
@@ -55,6 +55,9 @@ module Jobs
def topics
feed_topics = []
+ rss = fetch_rss
+ return feed_topics unless rss.present?
+
rss.items.each do |i|
current_feed_topic = FeedTopic.new(i)
feed_topics << current_feed_topic if current_feed_topic.content
@@ -65,8 +68,10 @@ module Jobs
private
- def rss
+ def fetch_rss
SimpleRSS.parse open(@feed_url, allow_redirections: :all)
+ rescue OpenURI::HTTPError
+ nil
end
end
diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb
index a41e83c584..be7956973b 100644
--- a/app/mailers/user_notifications.rb
+++ b/app/mailers/user_notifications.rb
@@ -102,33 +102,45 @@ class UserNotifications < ActionMailer::Base
@preheader_text = I18n.t('user_notifications.digest.preheader', last_seen_at: @last_seen_at)
# Try to find 3 interesting stats for the top of the digest
- @counts = [{label_key: 'user_notifications.digest.new_topics', value: Topic.new_since_last_seen(user, min_date).count}]
+ @counts = [{label_key: 'user_notifications.digest.new_topics',
+ value: Topic.new_since_last_seen(user, min_date).count,
+ href: "#{Discourse.base_url}/new"}]
value = user.unread_notifications
- @counts << {label_key: 'user_notifications.digest.unread_notifications', value: value} if value > 0
+ @counts << {label_key: 'user_notifications.digest.unread_notifications', value: value, href: "#{Discourse.base_url}/my/notifications"} if value > 0
value = user.unread_private_messages
- @counts << {label_key: 'user_notifications.digest.unread_messages', value: value} if value > 0
+ @counts << {label_key: 'user_notifications.digest.unread_messages', value: value, href: "#{Discourse.base_url}/my/messages"} if value > 0
if @counts.size < 3
- @counts << {label_key: 'user_notifications.digest.new_posts', value: Post.for_mailing_list(user, min_date).where("posts.post_number > ?", 1).count}
+ @counts << {
+ label_key: 'user_notifications.digest.new_posts',
+ value: Post.for_mailing_list(user, min_date).where("posts.post_number > ?", 1).count,
+ href: "#{Discourse.base_url}/new"
+ }
end
if @counts.size < 3
value = User.real.where(active: true, staged: false).not_suspended.where("created_at > ?", min_date).count
- @counts << {label_key: 'user_notifications.digest.new_users', value: value } if value > 0
+ @counts << {
+ label_key: 'user_notifications.digest.new_users',
+ value: value,
+ href: "#{Discourse.base_url}/about"
+ } if value > 0
end
# Now fetch some topics and posts to show
- topics_for_digest = Topic.for_digest(user, min_date, limit: SiteSetting.digest_topics + 3, top_order: true).to_a
+ topics_for_digest = Topic.for_digest(user, min_date, limit: SiteSetting.digest_topics + SiteSetting.digest_other_topics, top_order: true).to_a
@popular_topics = topics_for_digest[0,SiteSetting.digest_topics]
@other_new_for_you = topics_for_digest.size > SiteSetting.digest_topics ? topics_for_digest[SiteSetting.digest_topics..-1] : []
@popular_posts = if SiteSetting.digest_posts > 0
- Post.for_mailing_list(user, min_date)
- .where("posts.post_number > ? AND posts.score > ?", 1, 5.0)
- .order("posts.score DESC")
+ Post.order("posts.score DESC")
+ .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] * 5.0)
.limit(SiteSetting.digest_posts)
else
[]
diff --git a/app/models/category.rb b/app/models/category.rb
index 2a044088c8..e2dd126701 100644
--- a/app/models/category.rb
+++ b/app/models/category.rb
@@ -213,11 +213,11 @@ SQL
end
def description_text
- return nil unless description
+ return nil unless self.description
@@cache ||= LruRedux::ThreadSafeCache.new(1000)
@@cache.getset(self.description) do
- Nokogiri::HTML(self.description).text
+ Nokogiri::HTML.fragment(self.description).text.strip
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 9d488ac762..dde84c2922 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -13,6 +13,7 @@ class Group < ActiveRecord::Base
has_and_belongs_to_many :web_hooks
before_save :downcase_incoming_email
+ before_save :cook_bio
after_save :destroy_deletions
after_save :automatic_group_membership
@@ -83,6 +84,12 @@ class Group < ActiveRecord::Base
self.incoming_email = (incoming_email || "").strip.downcase.presence
end
+ def cook_bio
+ if !self.bio_raw.blank?
+ self.bio_cooked = PrettyText.cook(self.bio_raw)
+ end
+ end
+
def incoming_email_validator
return if self.automatic || self.incoming_email.blank?
@@ -349,7 +356,11 @@ class Group < ActiveRecord::Base
end
def add_owner(user)
- self.group_users.create(user_id: user.id, owner: true)
+ if group_user = self.group_users.find_by(user: user)
+ group_user.update_attributes!(owner: true) if !group_user.owner
+ else
+ GroupUser.create!(user: user, group: self, owner: true)
+ end
end
def self.find_by_email(email)
diff --git a/app/models/invite.rb b/app/models/invite.rb
index 989631d2bd..8ecef5eda0 100644
--- a/app/models/invite.rb
+++ b/app/models/invite.rb
@@ -265,8 +265,12 @@ class Invite < ActiveRecord::Base
File.join(Rails.root, "public", "uploads", "csv", RailsMultisite::ConnectionManagement.current_db)
end
- def self.chunk_path(identifier, filename, chunk_number)
- File.join(Invite.base_directory, "tmp", identifier, "#{filename}.part#{chunk_number}")
+ def self.create_csv(file, name)
+ extension = File.extname(file.original_filename)
+ path = "#{Invite.base_directory}/#{name}#{extension}"
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
+ File.open(path, "wb") { |f| f << file.tempfile.read }
+ path
end
end
diff --git a/app/models/post.rb b/app/models/post.rb
index 84566d5a8a..e262f259ba 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -24,7 +24,7 @@ class Post < ActiveRecord::Base
rate_limit :limit_posts_per_day
belongs_to :user
- belongs_to :topic, counter_cache: :posts_count
+ belongs_to :topic
belongs_to :reply_to_user, class_name: "User"
diff --git a/app/models/post_action.rb b/app/models/post_action.rb
index 5cee56bdde..5bc031ccae 100644
--- a/app/models/post_action.rb
+++ b/app/models/post_action.rb
@@ -435,8 +435,10 @@ SQL
post_action_type: post_action_type_key)
end
- topic_count = Post.where(topic_id: topic_id).sum(column)
- Topic.where(id: topic_id).update_all ["#{column} = ?", topic_count]
+ if column == "like_count"
+ topic_count = Post.where(topic_id: topic_id).sum(column)
+ Topic.where(id: topic_id).update_all ["#{column} = ?", topic_count]
+ end
if PostActionType.notify_flag_type_ids.include?(post_action_type_id)
PostAction.update_flagged_posts_count
diff --git a/app/models/post_analyzer.rb b/app/models/post_analyzer.rb
index f5ff4117b1..09f7cd9932 100644
--- a/app/models/post_analyzer.rb
+++ b/app/models/post_analyzer.rb
@@ -19,7 +19,7 @@ class PostAnalyzer
result = Oneboxer.apply(cooked, topic_id: @topic_id) do |url, _|
@found_oneboxes = true
Oneboxer.invalidate(url) if args.last[:invalidate_oneboxes]
- Oneboxer.cached_onebox url
+ Oneboxer.cached_onebox(url)
end
cooked = result.to_html if result.changed?
@@ -30,10 +30,9 @@ class PostAnalyzer
def image_count
return 0 unless @raw.present?
- cooked_document.search("img").reject do |t|
- dom_class = t["class"]
- if dom_class
- (Post.white_listed_image_classes & dom_class.split(" ")).count > 0
+ cooked_stripped.css("img").reject do |t|
+ if dom_class = t["class"]
+ (Post.white_listed_image_classes & dom_class.split).count > 0
end
end.count
end
@@ -42,8 +41,8 @@ class PostAnalyzer
def attachment_count
return 0 unless @raw.present?
- attachments = cooked_document.css("a.attachment[href^=\"#{Discourse.store.absolute_base_url}\"]")
- attachments += cooked_document.css("a.attachment[href^=\"#{Discourse.store.relative_base_url}\"]") if Discourse.store.internal?
+ attachments = cooked_stripped.css("a.attachment[href^=\"#{Discourse.store.absolute_base_url}\"]")
+ attachments += cooked_stripped.css("a.attachment[href^=\"#{Discourse.store.relative_base_url}\"]") if Discourse.store.internal?
attachments.count
end
@@ -51,13 +50,6 @@ class PostAnalyzer
return [] if @raw.blank?
return @raw_mentions if @raw_mentions.present?
- # strip quotes, code blocks and oneboxes
- cooked_stripped = cooked_document
- cooked_stripped.css("aside.quote").remove
- cooked_stripped.css("pre").remove
- cooked_stripped.css("code").remove
- cooked_stripped.css(".onebox").remove
-
raw_mentions = cooked_stripped.css('.mention, .mention-group').map do |e|
if name = e.inner_text
name = name[1..-1]
@@ -105,11 +97,10 @@ class PostAnalyzer
@raw_links = []
- cooked_document.search("a").each do |l|
+ cooked_stripped.css("a[href]").each do |l|
# Don't include @mentions in the link count
- next if l.attributes['href'].nil? || link_is_a_mention?(l)
- url = l.attributes['href'].to_s
- @raw_links << url
+ next if l['href'].blank? || link_is_a_mention?(l)
+ @raw_links << l['href'].to_s
end
@raw_links
@@ -122,13 +113,18 @@ class PostAnalyzer
private
- def cooked_document
- @cooked_document ||= Nokogiri::HTML.fragment(cook(@raw, topic_id: @topic_id))
- end
+ def cooked_stripped
+ @cooked_stripped ||= begin
+ doc = Nokogiri::HTML.fragment(cook(@raw, topic_id: @topic_id))
+ doc.css("pre, code, aside.quote, .onebox, .elided").remove
+ doc
+ end
+ end
+
+ def link_is_a_mention?(l)
+ html_class = l['class']
+ return false if html_class.blank?
+ html_class.to_s['mention'] && l['href'].to_s[/^\/users\//]
+ end
- def link_is_a_mention?(l)
- html_class = l.attributes['class']
- return false if html_class.nil?
- html_class.to_s == 'mention' && l.attributes['href'].to_s =~ /^\/users\//
- end
end
diff --git a/app/models/quoted_post.rb b/app/models/quoted_post.rb
index 5a4ad07673..2bd06fce81 100644
--- a/app/models/quoted_post.rb
+++ b/app/models/quoted_post.rb
@@ -20,22 +20,22 @@ class QuotedPost < ActiveRecord::Base
next if uniq[[topic_id,post_number]]
uniq[[topic_id,post_number]] = true
+ begin
+ # It would be so much nicer if we used post_id in quotes
+ results = exec_sql "INSERT INTO quoted_posts(post_id, quoted_post_id, created_at, updated_at)
+ SELECT :post_id, p.id, current_timestamp, current_timestamp
+ FROM posts p
+ LEFT JOIN quoted_posts q on q.post_id = :post_id AND q.quoted_post_id = p.id
+ WHERE post_number = :post_number AND
+ topic_id = :topic_id AND
+ q.id IS NULL
+ RETURNING quoted_post_id
+ ", post_id: post.id, post_number: post_number, topic_id: topic_id
- # It would be so much nicer if we used post_id in quotes
- results = exec_sql "INSERT INTO quoted_posts(post_id, quoted_post_id, created_at, updated_at)
- SELECT :post_id, p.id, current_timestamp, current_timestamp
- FROM posts p
- LEFT JOIN quoted_posts q on q.post_id = :post_id AND q.quoted_post_id = p.id
- WHERE post_number = :post_number AND
- topic_id = :topic_id AND
- q.id IS NULL
- RETURNING quoted_post_id
- ", post_id: post.id, post_number: post_number, topic_id: topic_id
-
- results = results.to_a
-
- if results.length > 0
- ids << results[0]["quoted_post_id"].to_i
+ results = results.to_a
+ ids << results[0]["quoted_post_id"].to_i if results.length > 0
+ rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation
+ # it's fine
end
end
diff --git a/app/models/topic.rb b/app/models/topic.rb
index a8786ac781..a511711323 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 'discourse_featured_link'
class Topic < ActiveRecord::Base
include ActionView::Helpers::SanitizeHelper
@@ -73,6 +74,10 @@ class Topic < ActiveRecord::Base
(!t.user_id || !t.user.staff?)
}
+ validates :featured_link, allow_nil: true, format: URI::regexp(%w(http https))
+ validate if: :featured_link do
+ errors.add(:featured_link, :invalid_category) unless Guardian.new.can_edit_featured_link?(category_id)
+ end
before_validation do
self.title = TextCleaner.clean_title(TextSentinel.title_sentinel(title).text) if errors[:title].empty?
@@ -378,6 +383,14 @@ class Topic < ActiveRecord::Base
featured_topic_ids ? topics.where("topics.id NOT IN (?)", featured_topic_ids) : topics
end
+ def featured_link
+ custom_fields[DiscourseFeaturedLink::CUSTOM_FIELD_NAME]
+ end
+
+ def featured_link=(link)
+ custom_fields[DiscourseFeaturedLink::CUSTOM_FIELD_NAME] = link.strip
+ end
+
def meta_data=(data)
custom_fields.replace(data)
end
@@ -466,23 +479,103 @@ class Topic < ActiveRecord::Base
end
# Atomically creates the next post number
- def self.next_post_number(topic_id, reply = false)
+ def self.next_post_number(topic_id, reply = false, whisper = false)
highest = exec_sql("select coalesce(max(post_number),0) as max from posts where topic_id = ?", topic_id).first['max'].to_i
- reply_sql = reply ? ", reply_count = reply_count + 1" : ""
- result = exec_sql("UPDATE topics SET highest_post_number = ? + 1#{reply_sql}
- WHERE id = ? RETURNING highest_post_number", highest, topic_id)
- result.first['highest_post_number'].to_i
+ if whisper
+
+ result = exec_sql("UPDATE topics
+ SET highest_staff_post_number = ? + 1
+ WHERE id = ?
+ RETURNING highest_staff_post_number", highest, topic_id)
+
+ result.first['highest_staff_post_number'].to_i
+
+ else
+
+ reply_sql = reply ? ", reply_count = reply_count + 1" : ""
+
+ result = exec_sql("UPDATE topics
+ SET highest_staff_post_number = :highest + 1,
+ highest_post_number = :highest + 1#{reply_sql},
+ posts_count = posts_count + 1
+ WHERE id = :topic_id
+ RETURNING highest_post_number", highest: highest, topic_id: topic_id)
+
+ result.first['highest_post_number'].to_i
+ end
end
+
+ def self.reset_all_highest!
+ exec_sql < 4
+ GROUP BY topic_id
+)
+UPDATE topics
+SET
+ highest_staff_post_number = X.highest_post_number,
+ highest_post_number = Y.highest_post_number,
+ last_posted_at = Y.last_posted_at,
+ posts_count = Y.posts_count
+FROM X, Y
+WHERE
+ X.topic_id = topics.id AND
+ Y.topic_id = topics.id AND (
+ topics.highest_staff_post_number <> X.highest_post_number OR
+ topics.highest_post_number <> Y.highest_post_number OR
+ topics.last_posted_at <> Y.last_posted_at OR
+ topics.posts_count <> Y.posts_count
+ )
+SQL
+ end
+
+
# If a post is deleted we have to update our highest post counters
def self.reset_highest(topic_id)
result = exec_sql "UPDATE topics
- SET highest_post_number = (SELECT COALESCE(MAX(post_number), 0) FROM posts WHERE topic_id = :topic_id AND deleted_at IS NULL),
- posts_count = (SELECT count(*) FROM posts WHERE deleted_at IS NULL AND topic_id = :topic_id),
- last_posted_at = (SELECT MAX(created_at) FROM POSTS WHERE topic_id = :topic_id AND deleted_at IS NULL)
+ SET
+ highest_staff_post_number = (
+ SELECT COALESCE(MAX(post_number), 0) FROM posts
+ WHERE topic_id = :topic_id AND
+ deleted_at IS NULL
+ ),
+ highest_post_number = (
+ SELECT COALESCE(MAX(post_number), 0) FROM posts
+ WHERE topic_id = :topic_id AND
+ deleted_at IS NULL AND
+ post_type <> 4
+ ),
+ posts_count = (
+ SELECT count(*) FROM posts
+ WHERE deleted_at IS NULL AND
+ topic_id = :topic_id AND
+ post_type <> 4
+ ),
+
+ last_posted_at = (
+ SELECT MAX(created_at) FROM posts
+ WHERE topic_id = :topic_id AND
+ deleted_at IS NULL AND
+ post_type <> 4
+ )
WHERE id = :topic_id
RETURNING highest_post_number", topic_id: topic_id
+
highest_post_number = result.first['highest_post_number'].to_i
# Update the forum topic user records
@@ -724,10 +817,7 @@ class Topic < ActiveRecord::Base
end
def update_action_counts
- PostActionType.types.each_key do |type|
- count_field = "#{type}_count"
- update_column(count_field, Post.where(topic_id: id).sum(count_field))
- end
+ update_column(:like_count, Post.where(topic_id: id).sum(:like_count))
end
def posters_summary(options = {})
diff --git a/app/models/topic_link.rb b/app/models/topic_link.rb
index 182c3d64d9..12ae7dcaab 100644
--- a/app/models/topic_link.rb
+++ b/app/models/topic_link.rb
@@ -37,7 +37,7 @@ class TopicLink < ActiveRecord::Base
def self.topic_map(guardian, topic_id)
# Sam: complicated reports are really hard in AR
- builder = SqlBuilder.new < 0 && user.last_seen_at && user.last_seen_at > 1.month.ago
+
# top must be in the top_menu
return unless SiteSetting.top_menu =~ /(^|\|)top(\||$)/i
+
+
# not enough topics
return unless period = SiteSetting.min_redirected_to_top_period(1.days.ago)
diff --git a/app/models/web_hook.rb b/app/models/web_hook.rb
index 42bbfd5b1e..9884bbf52e 100644
--- a/app/models/web_hook.rb
+++ b/app/models/web_hook.rb
@@ -40,8 +40,12 @@ class WebHook < ActiveRecord::Base
end
end
- def self.enqueue_topic_hooks(event, topic, user)
- WebHook.enqueue_hooks(:topic, topic_id: topic.id, user_id: user&.id, category_id: topic&.category&.id, event_name: event.to_s)
+ def self.enqueue_topic_hooks(event, topic, user=nil)
+ WebHook.enqueue_hooks(:topic, topic_id: topic.id, user_id: user&.id, category_id: topic&.category_id, event_name: event.to_s)
+ end
+
+ def self.enqueue_post_hooks(event, post, user=nil)
+ WebHook.enqueue_hooks(:post, post_id: post.id, topic_id: post&.topic_id, user_id: user&.id, category_id: post&.topic&.category_id, event_name: event.to_s)
end
%i(topic_destroyed topic_recovered).each do |event|
@@ -57,18 +61,16 @@ class WebHook < ActiveRecord::Base
%i(post_created
post_destroyed
post_recovered).each do |event|
-
DiscourseEvent.on(event) do |post, _, user|
- WebHook.enqueue_hooks(:post,
- post_id: post.id,
- topic_id: post&.topic&.id,
- user_id: user&.id,
- category_id: post.topic&.category&.id,
- event_name: event.to_s
- )
+ 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).each do |event|
DiscourseEvent.on(event) do |user|
WebHook.enqueue_hooks(:user, user_id: user.id, event_name: event.to_s)
diff --git a/app/serializers/basic_group_serializer.rb b/app/serializers/basic_group_serializer.rb
index 783fdafb94..401a88b2ff 100644
--- a/app/serializers/basic_group_serializer.rb
+++ b/app/serializers/basic_group_serializer.rb
@@ -14,9 +14,25 @@ class BasicGroupSerializer < ApplicationSerializer
:has_messages,
:flair_url,
:flair_bg_color,
- :flair_color
+ :flair_color,
+ :bio_raw,
+ :bio_cooked
def include_incoming_email?
- scope.is_staff?
+ staff?
+ end
+
+ def include_has_messsages
+ staff?
+ end
+
+ def include_bio_raw
+ staff?
+ end
+
+ private
+
+ def staff?
+ @staff ||= scope.is_staff?
end
end
diff --git a/app/serializers/group_post_serializer.rb b/app/serializers/group_post_serializer.rb
index 77f1eb95b3..1fb6305e59 100644
--- a/app/serializers/group_post_serializer.rb
+++ b/app/serializers/group_post_serializer.rb
@@ -1,42 +1,22 @@
class GroupPostSerializer < ApplicationSerializer
attributes :id,
- :cooked,
+ :excerpt,
:created_at,
:title,
:url,
- :user_title,
- :user_long_name,
- :topic,
:category
- has_one :user, serializer: BasicUserSerializer, embed: :objects
+ has_one :user, serializer: GroupPostUserSerializer, embed: :object
+ has_one :topic, serializer: BasicTopicSerializer, embed: :object
def title
object.topic.title
end
- def user_long_name
- object.user.try(:name)
- end
-
- def user_title
- object.user.try(:title)
- end
-
def include_user_long_name?
SiteSetting.enable_names?
end
- def topic
- object.topic
- end
-
- def cooked
- fragment = Nokogiri::HTML.fragment(object.cooked)
- DiscourseEvent.trigger(:reduce_cooked, fragment, object)
- fragment.to_html
- end
-
def category
object.topic.category
end
diff --git a/app/serializers/group_post_user_serializer.rb b/app/serializers/group_post_user_serializer.rb
new file mode 100644
index 0000000000..5a69f23fbc
--- /dev/null
+++ b/app/serializers/group_post_user_serializer.rb
@@ -0,0 +1,3 @@
+class GroupPostUserSerializer < BasicUserSerializer
+ attributes :title, :name
+end
diff --git a/app/serializers/group_show_serializer.rb b/app/serializers/group_show_serializer.rb
index 58358c1bac..dc9e0c7649 100644
--- a/app/serializers/group_show_serializer.rb
+++ b/app/serializers/group_show_serializer.rb
@@ -1,11 +1,25 @@
class GroupShowSerializer < BasicGroupSerializer
- attributes :is_group_user
+ attributes :is_group_user, :is_group_owner
def include_is_group_user?
scope.authenticated?
end
def is_group_user
- object.users.include?(scope.user)
+ !!fetch_group_user
+ end
+
+ def include_is_group_owner?
+ scope.authenticated?
+ end
+
+ def is_group_owner
+ scope.is_admin? || fetch_group_user&.owner
+ end
+
+ private
+
+ def fetch_group_user
+ @group_user ||= object.group_users.find_by(user: scope.user)
end
end
diff --git a/app/serializers/listable_topic_serializer.rb b/app/serializers/listable_topic_serializer.rb
index 3589d7a231..13a87235c1 100644
--- a/app/serializers/listable_topic_serializer.rb
+++ b/app/serializers/listable_topic_serializer.rb
@@ -26,6 +26,10 @@ class ListableTopicSerializer < BasicTopicSerializer
has_one :last_poster, serializer: BasicUserSerializer, embed: :objects
+ def highest_post_number
+ (scope.is_staff? && object.highest_staff_post_number) || object.highest_post_number
+ end
+
def liked
object.user_data && object.user_data.liked
end
@@ -109,7 +113,7 @@ class ListableTopicSerializer < BasicTopicSerializer
protected
def unread_helper
- @unread_helper ||= Unread.new(object, object.user_data)
+ @unread_helper ||= Unread.new(object, object.user_data, scope)
end
end
diff --git a/app/serializers/post_revision_serializer.rb b/app/serializers/post_revision_serializer.rb
index 4df9012e8d..8ff8b1826e 100644
--- a/app/serializers/post_revision_serializer.rb
+++ b/app/serializers/post_revision_serializer.rb
@@ -193,6 +193,10 @@ class PostRevisionSerializer < ApplicationSerializer
end
end
+ if SiteSetting.topic_featured_link_enabled
+ latest_modifications["featured_link"] = [post.topic.featured_link]
+ end
+
if SiteSetting.tagging_enabled
latest_modifications["tags"] = [post.topic.tags.map(&:name)]
end
diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb
index 718cb1221a..d43c165a96 100644
--- a/app/serializers/site_serializer.rb
+++ b/app/serializers/site_serializer.rb
@@ -23,7 +23,8 @@ class SiteSerializer < ApplicationSerializer
:can_tag_topics,
:tags_filter_regexp,
:top_tags,
- :wizard_required
+ :wizard_required,
+ :topic_featured_link_allowed_category_ids
has_many :categories, serializer: BasicCategorySerializer, embed: :objects
has_many :trust_levels, embed: :objects
@@ -121,4 +122,12 @@ class SiteSerializer < ApplicationSerializer
def include_wizard_required?
Wizard.user_requires_completion?(scope.user)
end
+
+ def include_topic_featured_link_allowed_category_ids?
+ SiteSetting.topic_featured_link_enabled
+ end
+
+ def topic_featured_link_allowed_category_ids
+ scope.topic_featured_link_allowed_category_ids
+ end
end
diff --git a/app/serializers/suggested_topic_serializer.rb b/app/serializers/suggested_topic_serializer.rb
index 3d4b8f380f..a0817917f5 100644
--- a/app/serializers/suggested_topic_serializer.rb
+++ b/app/serializers/suggested_topic_serializer.rb
@@ -7,7 +7,7 @@ class SuggestedTopicSerializer < ListableTopicSerializer
has_one :user, serializer: BasicUserSerializer, embed: :objects
end
- attributes :archetype, :like_count, :views, :category_id, :tags
+ attributes :archetype, :like_count, :views, :category_id, :tags, :featured_link
has_many :posters, serializer: SuggestedPosterSerializer, embed: :objects
def posters
@@ -21,4 +21,12 @@ class SuggestedTopicSerializer < ListableTopicSerializer
def tags
object.tags.map(&:name)
end
+
+ def include_featured_link?
+ SiteSetting.topic_featured_link_enabled
+ end
+
+ def featured_link
+ object.featured_link
+ end
end
diff --git a/app/serializers/topic_list_item_serializer.rb b/app/serializers/topic_list_item_serializer.rb
index a04c1eeae3..9e1e642a15 100644
--- a/app/serializers/topic_list_item_serializer.rb
+++ b/app/serializers/topic_list_item_serializer.rb
@@ -10,7 +10,8 @@ class TopicListItemSerializer < ListableTopicSerializer
:pinned_globally,
:bookmarked_post_numbers,
:liked_post_numbers,
- :tags
+ :tags,
+ :featured_link
has_many :posters, serializer: TopicPosterSerializer, embed: :objects
has_many :participants, serializer: TopicPosterSerializer, embed: :objects
@@ -72,4 +73,12 @@ class TopicListItemSerializer < ListableTopicSerializer
object.tags.map(&:name)
end
+ def include_featured_link?
+ SiteSetting.topic_featured_link_enabled
+ end
+
+ def featured_link
+ object.featured_link
+ end
+
end
diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb
index e9e308b5df..780d2b80c3 100644
--- a/app/serializers/topic_view_serializer.rb
+++ b/app/serializers/topic_view_serializer.rb
@@ -56,7 +56,8 @@ class TopicViewSerializer < ApplicationSerializer
:chunk_size,
:bookmarked,
:message_archived,
- :tags
+ :tags,
+ :featured_link
# TODO: Split off into proper object / serializer
def details
@@ -243,8 +244,17 @@ class TopicViewSerializer < ApplicationSerializer
def include_tags?
SiteSetting.tagging_enabled
end
+
def tags
object.topic.tags.map(&:name)
end
+ def include_featured_link?
+ SiteSetting.topic_featured_link_enabled
+ end
+
+ def featured_link
+ object.topic.featured_link
+ end
+
end
diff --git a/app/views/user_notifications/digest.html.erb b/app/views/user_notifications/digest.html.erb
index d8819a9da7..1a2e03dec8 100644
--- a/app/views/user_notifications/digest.html.erb
+++ b/app/views/user_notifications/digest.html.erb
@@ -18,9 +18,18 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
<%= @preheader_text %>
+<%- if I18n.t('user_notifications.digest.custom.html.header').present? %>
+
+
+ |
+ <%= raw(t 'user_notifications.digest.custom.html.header') %>
+ |
+
+
+<%- else %>
- |
+ |
<%- if logo_url.blank? %>
@@ -33,6 +42,7 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
|
+<%- end %>
@@ -63,14 +73,14 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
<%- @counts.each do |count| -%>
|
- <%= count[:value] -%>
+ <%= count[:value] -%>
|
<%- end -%>
<%- @counts.each do |count| -%>
|
- <%=t count[:label_key] -%>
+ <%=t count[:label_key] -%>
|
<%- end -%>
@@ -117,6 +127,9 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
<%= t.title -%>
+ <%- if SiteSetting.show_topic_featured_link_in_digest && t.featured_link %>
+ <%= raw topic_featured_link_domain(t.featured_link) %>
+ <%- end %>
@@ -129,13 +142,15 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
- <%= t.user.try(:username) -%>
- <% if t.user.try(:name).present? %>
- <%= t.user.name -%>
+ <% if t.user %>
+ <%= t.user.username -%>
+ <% if SiteSetting.enable_names? && t.user.name.present? && t.user.name.downcase != t.user.username.downcase %>
+ <%= t.user.name -%>
+ <% end %>
<% end %>
|
<%- if show_image_with_url(t.image_url) -%>
-
+ |
|
<%- end -%>
@@ -147,7 +162,7 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
- |
+ |
<%= email_excerpt(t.first_post.cooked) %>
|
@@ -157,24 +172,24 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
-
- |
+ |
+
<%= t.like_count -%>
|
-
+ |
<%= t.posts_count - 1 -%>
|
-
+ |
<% t.posters_summary.each do |ps| %>
<% if ps.user %>
<% end %>
<% end %>
|
-
-
+ |
+
<%=t 'user_notifications.digest.join_the_discussion' %>
|
@@ -219,7 +234,7 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
-
+
<%=t 'user_notifications.digest.popular_posts' %>
|
@@ -261,8 +276,12 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
- <%= post.user.username -%>
- <%= post.user.name -%>
+ <% if post.user %>
+ <%= post.user.username -%>
+ <% if SiteSetting.enable_names? && post.user.name && post.user.name.downcase != post.user.username.downcase %>
+ <%= post.user.name -%>
+ <% end %>
+ <% end %>
|
@@ -279,22 +298,30 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
<% end %>
+ |
+ |
+
+
+
<% end %>
+<%= digest_custom_html("above_popular_topics") %>
<% if @other_new_for_you.present? %>
- <%=t 'user_notifications.digest.more_new' %>
+ <%=t 'user_notifications.digest.more_new' %>
-
-<%= digest_custom_html("above_popular_topics") %>
+
+
+ | |
+
@@ -312,6 +339,9 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
<%= t.title -%>
+ <%- if SiteSetting.show_topic_featured_link_in_digest && t.featured_link %>
+ <%= raw topic_featured_link_domain(t.featured_link) %>
+ <%- end %>
<%= category_badge(t.category, inline_style: true, absolute_url: true) %>
@@ -342,10 +372,15 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
-<%= digest_custom_html("below_popular_topics") %>
+ |
+ |
+
+
<% end %>
+<%= digest_custom_html("below_popular_topics") %>
+
|
diff --git a/config/initializers/100-logster.rb b/config/initializers/100-logster.rb
index c39494f6a0..d913051336 100644
--- a/config/initializers/100-logster.rb
+++ b/config/initializers/100-logster.rb
@@ -87,5 +87,6 @@ RailsMultisite::ConnectionManagement.each_connection do
end
if Rails.configuration.multisite
- Rails.logger.instance_variable_get(:@chained).first.formatter = RailsMultisite::Formatter.new
+ chained = Rails.logger.instance_variable_get(:@chained)
+ chained && chained.first.formatter = RailsMultisite::Formatter.new
end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 1ec5a52c01..4255aaa32a 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -487,8 +487,11 @@ en:
profile: "Profile"
mute: "Mute"
edit: "Edit Preferences"
- download_archive: "Download My Posts"
- download_archive_confirm: "Are you sure you want to download your posts?"
+ download_archive:
+ button_text: "Download My Posts"
+ confirm: "Are you sure you want to download your posts?"
+ success: "Download initiated, you will be notified via message when the process is complete."
+ rate_limit_error: "Posts can be downloaded once per day, please try again tomorrow."
new_private_message: "New Message"
private_message: "Message"
private_messages: "Messages"
@@ -775,11 +778,9 @@ en:
link_generated: "Invite link generated successfully!"
valid_for: "Invite link is only valid for this email address: %{email}"
bulk_invite:
- none: "You haven't invited anyone here yet. You can send individual invites, or invite a bunch of people at once by uploading a bulk invite file."
+ none: "You haven't invited anyone here yet. You can send individual invites, or invite a bunch of people at once by uploading a CSV file."
text: "Bulk Invite from File"
- uploading: "Uploading..."
success: "File uploaded successfully, you will be notified via message when the process is complete."
- error: "There was an error uploading '{{filename}}': {{message}}"
password:
title: "Password"
@@ -1073,6 +1074,7 @@ en:
title_placeholder: "What is this discussion about in one brief sentence?"
edit_reason_placeholder: "why are you editing?"
show_edit_reason: "(add edit reason)"
+ topic_featured_link_placeholder: "Enter link shown with title."
reply_placeholder: "Type here. Use Markdown, BBCode, or HTML to format. Drag or paste images."
view_new_post: "View your new post."
saving: "Saving"
@@ -1842,6 +1844,23 @@ en:
title: "Show the raw source diffs side-by-side"
button: ' Raw'
+ group:
+ edit:
+ title: 'Edit Group'
+ title: 'Title'
+ name: "Name"
+ bio: "About Group"
+ name_placeholder: "Group name, no spaces, same as username rule"
+ flair_url: "Avatar Flair Image"
+ flair_url_placeholder: "(Optional) Image URL or Font Awesome class"
+ flair_bg_color: "Avatar Flair Background Color"
+ flair_bg_color_placeholder: "(Optional) Hex color value"
+ flair_color: "Avatar Flair Color"
+ flair_color_placeholder: "(Optional) Hex color value"
+ flair_preview_icon: "Preview Icon"
+ flair_preview_image: "Preview Image"
+ flair_note: "Note: Flair will only show for a user's primary group."
+
category:
can: 'can… '
none: '(no category)'
@@ -1858,6 +1877,7 @@ en:
tags_allowed_tag_groups: "Tag groups that can only be used in this category:"
tags_placeholder: "(Optional) list of allowed tags"
tag_groups_placeholder: "(Optional) list of allowed tag groups"
+ topic_featured_link_allowed: "Restricts editing the topic featured link in this category. Require site setting topic_featured_link_enabled is checked."
delete: 'Delete Category'
create: 'New Category'
create_long: 'Create a new category'
@@ -2448,7 +2468,6 @@ en:
refresh: "Refresh"
new: "New"
selector_placeholder: "enter username"
- name_placeholder: "Group name, no spaces, same as username rule"
about: "Edit your group membership and names here"
group_members: "Group members"
delete: "Delete"
@@ -2456,7 +2475,6 @@ en:
delete_failed: "Unable to delete group. If this is an automatic group, it cannot be destroyed."
delete_member_confirm: "Remove '%{username}' from the '%{group}' group?"
delete_owner_confirm: "Remove owner privilege for '%{username}'?"
- name: "Name"
add: "Add"
add_members: "Add members"
custom: "Custom"
@@ -2473,14 +2491,6 @@ en:
add_owners: Add owners
incoming_email: "Custom incoming email address"
incoming_email_placeholder: "enter email address"
- flair_url: "Avatar Flair Image"
- flair_url_placeholder: "(Optional) Image URL or Font Awesome class"
- flair_bg_color: "Avatar Flair Background Color"
- flair_bg_color_placeholder: "(Optional) Hex color value"
- flair_color: "Avatar Flair Color"
- flair_color_placeholder: "(Optional) Hex color value"
- flair_preview: "Preview"
- flair_note: "Note: Flair will only show for a user's primary group."
api:
generate_master: "Generate Master API Key"
@@ -2636,7 +2646,6 @@ en:
export_csv:
success: "Export initiated, you will be notified via message when the process is complete."
failed: "Export failed. Please check the logs."
- rate_limit_error: "Posts can be downloaded once per day, please try again tomorrow."
button_text: "Export"
button_title:
user: "Export full user list in CSV format."
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 573685dca4..3c1d84d833 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -134,7 +134,8 @@ en:
<<: *errors
bulk_invite:
- file_should_be_csv: "The uploaded file should be of csv or txt format."
+ file_should_be_csv: "The uploaded file should be of csv format."
+ error: "There was an error uploading that file. Please try again later."
backup:
operation_already_running: "An operation is currently running. Can't start a new job right now."
@@ -319,6 +320,7 @@ en:
name: "Category Name"
topic:
title: 'Title'
+ featured_link: 'Featured Link'
post:
raw: "Body"
user_profile:
@@ -335,6 +337,9 @@ en:
too_many_users: "You can only send warnings to one user at a time."
cant_send_pm: "Sorry, you cannot send a private message to that user."
no_user_selected: "You must select a valid user."
+ featured_link:
+ invalid: "is invalid. URL should include http:// or https://."
+ invalid_category: "can't be edited in this category."
user:
attributes:
password:
@@ -599,6 +604,14 @@ en:
description: 'Vote for this post'
long_form: 'voted for this post'
+ user_activity:
+ no_bookmarks:
+ self: "You have no bookmarked posts, bookmarking posts allows you to easily access them later on."
+ others: "No bookmarks."
+ no_likes_given:
+ self: "You have not liked any posts."
+ others: "No liked posts."
+
topic_flag_types:
spam:
title: 'Spam'
@@ -837,6 +850,10 @@ en:
min_first_post_length: "Minimum allowed first post (topic body) length in characters"
min_private_message_post_length: "Minimum allowed post length in characters for messages"
max_post_length: "Maximum allowed post length in characters"
+ topic_featured_link_enabled: "Enable posting a link with topics."
+ topic_featured_link_onebox: "Show an onebox in the post body if possible and prevent editing post content."
+ open_topic_featured_link_in_external_window: "Open topic featured link in a external window."
+ show_topic_featured_link_in_digest: "Show the topic featured link in the digest email."
min_topic_title_length: "Minimum allowed topic title length in characters"
max_topic_title_length: "Maximum allowed topic title length in characters"
min_private_message_title_length: "Minimum allowed title length for a message in characters"
@@ -959,7 +976,7 @@ en:
email_token_grace_period_hours: "Forgot password / activate account tokens are still valid for a grace period of (n) hours after being redeemed."
enable_badges: "Enable the badge system"
- enable_whispers: "Allow staff private communication within topic. (experimental)"
+ enable_whispers: "Allow staff private communication within topics."
allow_index_in_robots_txt: "Specify in robots.txt that this site is allowed to be indexed by web search engines."
email_domains_blacklist: "A pipe-delimited list of email domains that users are not allowed to register accounts with. Example: mailinator.com|trashmail.net"
@@ -1278,8 +1295,9 @@ en:
allow_animated_thumbnails: "Generates animated thumbnails of animated gifs."
default_avatars: "URLs to avatars that will be used by default for new users until they change them."
automatically_download_gravatars: "Download Gravatars for users upon account creation or email change."
- digest_topics: "The maximum number of topics to display in the email summary."
+ digest_topics: "The maximum number of popular topics to display in the email summary."
digest_posts: "The maximum number of popular posts to display in the email summary."
+ digest_other_topics: "The maximum number of topics to show in the 'New in topics and categories you follow' section of the email summary."
digest_min_excerpt_length: "Minimum post excerpt in the email summary, in characters."
delete_digest_email_after_days: "Suppress summary emails for users not seen on the site for more than (n) days."
digest_suppress_categories: "Suppress these categories from summary emails."
@@ -1345,7 +1363,6 @@ en:
embed_whitelist_selector: "CSS selector for elements that are allowed in embeds."
embed_blacklist_selector: "CSS selector for elements that are removed from embeds."
notify_about_flags_after: "If there are flags that haven't been handled after this many hours, send an email to the contact_email. Set to 0 to disable."
- enable_cdn_js_debugging: "Allow /logs to display proper errors by adding crossorigin permissions on all js includes."
show_create_topics_notice: "If the site has fewer than 5 public topics, show a notice asking admins to create some topics."
delete_drafts_older_than_n_days: Delete drafts older than (n) days.
diff --git a/config/routes.rb b/config/routes.rb
index f465e56a38..5bffa05f74 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -617,12 +617,8 @@ Discourse::Application.routes.draw do
resources :queued_posts, constraints: StaffConstraint.new
get 'queued-posts' => 'queued_posts#index'
- resources :invites do
- collection do
- get "upload" => "invites#check_csv_chunk"
- post "upload" => "invites#upload_csv_chunk"
- end
- end
+ resources :invites
+ post "invites/upload_csv" => "invites#upload_csv"
post "invites/reinvite" => "invites#resend_invite"
post "invites/reinvite-all" => "invites#resend_all_invites"
post "invites/link" => "invites#create_invite_link"
@@ -650,6 +646,7 @@ Discourse::Application.routes.draw do
delete "draft" => "draft#destroy"
get "cdn_asset/:site/*path" => "static#cdn_asset", format: false
+ get "brotli_asset/*path" => "static#brotli_asset", format: false
get "favicon/proxied" => "static#favicon", format: false
diff --git a/config/site_settings.yml b/config/site_settings.yml
index 9be481803e..aae34135a6 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -433,6 +433,15 @@ posting:
max_post_length:
client: true
default: 32000
+ topic_featured_link_enabled:
+ client: true
+ default: false
+ topic_featured_link_onebox:
+ client: true
+ default: false
+ open_topic_featured_link_in_external_window:
+ client: true
+ default: true
body_min_entropy: 7
min_topic_title_length:
client: true
@@ -588,6 +597,7 @@ email:
default: 5
min: 1
digest_posts: 3
+ digest_other_topics: 5
delete_digest_email_after_days: 365
digest_suppress_categories:
type: category_list
@@ -595,6 +605,7 @@ email:
disable_digest_emails:
default: false
client: true
+ show_topic_featured_link_in_digest: true
email_custom_headers: 'Auto-Submitted: auto-generated'
email_subject: '[%{site_name}] %{optional_pm}%{optional_cat}%{topic_title}'
reply_by_email_enabled:
@@ -669,7 +680,7 @@ email:
reset_bounce_score_after_days: 30
attachment_content_type_blacklist:
type: list
- default: "pkcs7"
+ default: "pkcs7|x-vcard"
attachment_filename_blacklist:
type: list
default: "smime.p7s|signature.asc"
@@ -685,7 +696,7 @@ files:
default: 3072
authorized_extensions:
client: true
- default: 'jpg|jpeg|png|gif'
+ default: 'jpg|jpeg|png|gif|csv'
refresh: true
type: list
crawl_images:
@@ -1196,8 +1207,6 @@ uncategorized:
notify_about_flags_after: 48
- enable_cdn_js_debugging: false
-
show_create_topics_notice:
client: true
default: true
diff --git a/db/fixtures/009_users.rb b/db/fixtures/009_users.rb
index d65c8f2ef7..dd8a203e6b 100644
--- a/db/fixtures/009_users.rb
+++ b/db/fixtures/009_users.rb
@@ -32,7 +32,7 @@ duration = Rails.env.production? ? 60 : 0
if User.exec_sql("SELECT 1 FROM schema_migration_details
WHERE EXISTS(
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
- WHERE table_name = 'users' AND column_name = 'last_redirected_to_top_at'
+ WHERE table_schema = 'public' AND table_name = 'users' AND column_name = 'last_redirected_to_top_at'
) AND
name = 'MoveTrackingOptionsToUserOptions' AND
created_at < (current_timestamp at time zone 'UTC' - interval '#{duration} minutes')
diff --git a/db/fixtures/999_topics.rb b/db/fixtures/999_topics.rb
index cfe48615bc..e46419fd20 100644
--- a/db/fixtures/999_topics.rb
+++ b/db/fixtures/999_topics.rb
@@ -64,3 +64,32 @@ if seed_welcome_topics
skip_validations: true,
category: staff ? staff.name : nil)
end
+
+
+
+# run this later, cause we need to make sure new application controller resilience is in place first
+duration = Rails.env.production? ? 60 : 0
+if Topic.exec_sql("SELECT 1 FROM schema_migration_details
+ WHERE EXISTS(
+ SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE table_schema = 'public' AND table_name = 'topics' AND column_name = 'inappropriate_count'
+ ) AND
+ name = 'AddTopicColumnsBack' AND
+ created_at < (current_timestamp at time zone 'UTC' - interval '#{duration} minutes')
+ ").to_a.length > 0
+
+
+ Topic.transaction do
+ STDERR.puts "Removing superflous topic columns!"
+ %w[
+ inappropriate_count
+ bookmark_count
+ off_topic_count
+ illegal_count
+ notify_user_count
+].each do |column|
+ User.exec_sql("ALTER TABLE topics DROP COLUMN IF EXISTS #{column}")
+ end
+
+ end
+end
diff --git a/db/migrate/20161202011139_add_whisper_support_to_topics.rb b/db/migrate/20161202011139_add_whisper_support_to_topics.rb
new file mode 100644
index 0000000000..0e065889e8
--- /dev/null
+++ b/db/migrate/20161202011139_add_whisper_support_to_topics.rb
@@ -0,0 +1,16 @@
+class AddWhisperSupportToTopics < ActiveRecord::Migration
+ def up
+ remove_column :topics, :bookmark_count
+ remove_column :topics, :off_topic_count
+ remove_column :topics, :illegal_count
+ remove_column :topics, :inappropriate_count
+ remove_column :topics, :notify_user_count
+
+ add_column :topics, :highest_staff_post_number, :int, default: 0, null: false
+ execute "UPDATE topics SET highest_staff_post_number = highest_post_number"
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/db/migrate/20161205001727_add_topic_columns_back.rb b/db/migrate/20161205001727_add_topic_columns_back.rb
new file mode 100644
index 0000000000..d464c54a9e
--- /dev/null
+++ b/db/migrate/20161205001727_add_topic_columns_back.rb
@@ -0,0 +1,21 @@
+class AddTopicColumnsBack < ActiveRecord::Migration
+
+ # This really sucks big time, we have no use for these columns yet can not remove them
+ # if we remove them then sites will be down during migration
+
+ def up
+ add_column :topics, :bookmark_count, :int
+ add_column :topics, :off_topic_count, :int
+ add_column :topics, :illegal_count, :int
+ add_column :topics, :inappropriate_count, :int
+ add_column :topics, :notify_user_count, :int
+ end
+
+ def down
+ remove_column :topics, :bookmark_count
+ remove_column :topics, :off_topic_count
+ remove_column :topics, :illegal_count
+ remove_column :topics, :inappropriate_count
+ remove_column :topics, :notify_user_count
+ end
+end
diff --git a/db/migrate/20161205065743_add_bio_to_groups.rb b/db/migrate/20161205065743_add_bio_to_groups.rb
new file mode 100644
index 0000000000..846b3a1c97
--- /dev/null
+++ b/db/migrate/20161205065743_add_bio_to_groups.rb
@@ -0,0 +1,6 @@
+class AddBioToGroups < ActiveRecord::Migration
+ def change
+ add_column :groups, :bio_raw, :text
+ add_column :groups, :bio_cooked, :text
+ end
+end
diff --git a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb
index b52a6c12ac..2f2e75030c 100644
--- a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb
+++ b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb
@@ -6,23 +6,42 @@ class PostgreSQLFallbackHandler
include Singleton
def initialize
- @master = {}
- @running = {}
- @mutex = {}
- @last_check = {}
-
- setup!
+ @masters_down = {}
+ @mutex = Mutex.new
end
def verify_master
- @mutex[namespace].synchronize do
- return if running || recently_checked?
- @running[namespace] = true
- end
+ synchronize { return if @thread && @thread.alive? }
- current_namespace = namespace
- Thread.new do
- RailsMultisite::ConnectionManagement.with_connection(current_namespace) do
+ @thread = Thread.new do
+ while true do
+ begin
+ thread = Thread.new { initiate_fallback_to_master }
+ thread.join
+ break if synchronize { @masters_down.empty? }
+ sleep 10
+ ensure
+ thread.kill
+ end
+ end
+ end
+ end
+
+ def master_down?
+ synchronize { @masters_down[namespace] }
+ end
+
+ def master_down=(args)
+ synchronize { @masters_down[namespace] = args }
+ end
+
+ def master_up(namespace)
+ synchronize { @masters_down.delete(namespace) }
+ end
+
+ def initiate_fallback_to_master
+ @masters_down.keys.each do |key|
+ RailsMultisite::ConnectionManagement.with_connection(key) do
begin
logger.warn "#{log_prefix}: Checking master server..."
connection = ActiveRecord::Base.postgresql_connection(config)
@@ -32,54 +51,19 @@ class PostgreSQLFallbackHandler
ActiveRecord::Base.clear_all_connections!
logger.warn "#{log_prefix}: Master server is active. Reconnecting..."
- if namespace == RailsMultisite::ConnectionManagement::DEFAULT
- ActiveRecord::Base.establish_connection(config)
- else
- RailsMultisite::ConnectionManagement.establish_connection(db: namespace)
- end
-
+ self.master_up(key)
Discourse.disable_readonly_mode
- self.master = true
end
rescue => e
- if e.message.include?("could not connect to server")
- logger.warn "#{log_prefix}: Connection to master PostgreSQL server failed with '#{e.message}'"
- else
- raise e
- end
- ensure
- @mutex[namespace].synchronize do
- @last_check[namespace] = Time.zone.now
- @running[namespace] = false
- end
+ logger.warn "#{log_prefix}: Connection to master PostgreSQL server failed with '#{e.message}'"
end
end
end
end
- def master
- @master[namespace]
- end
-
- def master=(args)
- @master[namespace] = args
- end
-
- def running
- @running[namespace]
- end
-
+ # Use for testing
def setup!
- RailsMultisite::ConnectionManagement.all_dbs.each do |db|
- @master[db] = true
- @running[db] = false
- @mutex[db] = Mutex.new
- @last_check[db] = nil
- end
- end
-
- def verify?
- !master && !running
+ @masters_down = {}
end
private
@@ -96,17 +80,13 @@ class PostgreSQLFallbackHandler
"#{self.class} [#{namespace}]"
end
- def recently_checked?
- if @last_check[namespace]
- Time.zone.now <= (@last_check[namespace] + 5.seconds)
- else
- false
- end
- end
-
def namespace
RailsMultisite::ConnectionManagement.current_db
end
+
+ def synchronize
+ @mutex.synchronize { yield }
+ end
end
module ActiveRecord
@@ -115,7 +95,9 @@ module ActiveRecord
fallback_handler = ::PostgreSQLFallbackHandler.instance
config = config.symbolize_keys
- if fallback_handler.verify?
+ if fallback_handler.master_down?
+ fallback_handler.verify_master
+
connection = postgresql_connection(config.dup.merge({
host: config[:replica_host], port: config[:replica_port]
}))
@@ -126,7 +108,8 @@ module ActiveRecord
begin
connection = postgresql_connection(config)
rescue PG::ConnectionBad => e
- fallback_handler.master = false
+ fallback_handler.master_down = true
+ fallback_handler.verify_master
raise e
end
end
@@ -141,20 +124,4 @@ module ActiveRecord
raise "Replica database server is not in recovery mode." if value == 'f'
end
end
-
- module ConnectionAdapters
- class PostgreSQLAdapter
- set_callback :checkout, :before, :switch_back?
-
- private
-
- def fallback_handler
- @fallback_handler ||= ::PostgreSQLFallbackHandler.instance
- end
-
- def switch_back?
- fallback_handler.verify_master if fallback_handler.verify?
- end
- end
- end
end
diff --git a/lib/discourse.rb b/lib/discourse.rb
index 1ac6e1b00d..9f27827f4c 100644
--- a/lib/discourse.rb
+++ b/lib/discourse.rb
@@ -228,10 +228,12 @@ module Discourse
def self.keep_readonly_mode
# extend the expiry by 1 minute every 30 seconds
- Thread.new do
- while readonly_mode?
- $redis.expire(READONLY_MODE_KEY, READONLY_MODE_KEY_TTL)
- sleep 30.seconds
+ unless Rails.env.test?
+ Thread.new do
+ while readonly_mode?
+ $redis.expire(READONLY_MODE_KEY, READONLY_MODE_KEY_TTL)
+ sleep 30.seconds
+ end
end
end
end
@@ -369,9 +371,11 @@ module Discourse
end
end
+ SIDEKIQ_NAMESPACE ||= 'sidekiq'.freeze
+
def self.sidekiq_redis_config
conf = GlobalSetting.redis_config.dup
- conf[:namespace] = 'sidekiq'
+ conf[:namespace] = SIDEKIQ_NAMESPACE
conf
end
diff --git a/lib/discourse_featured_link.rb b/lib/discourse_featured_link.rb
new file mode 100644
index 0000000000..304383e923
--- /dev/null
+++ b/lib/discourse_featured_link.rb
@@ -0,0 +1,27 @@
+module DiscourseFeaturedLink
+ CUSTOM_FIELD_NAME = 'featured_link'.freeze
+
+ AdminDashboardData::GLOBAL_REPORTS << CUSTOM_FIELD_NAME
+
+ Report.add_report(CUSTOM_FIELD_NAME) do |report|
+ report.data = []
+ link_topics = TopicCustomField.where(name: CUSTOM_FIELD_NAME)
+ link_topics = link_topics.joins(:topic).where("topics.category_id = ?", report.category_id) if report.category_id
+ link_topics.where("topic_custom_fields.created_at >= ?", report.start_date)
+ .where("topic_custom_fields.created_at <= ?", report.end_date)
+ .group("DATE(topic_custom_fields.created_at)")
+ .order("DATE(topic_custom_fields.created_at)")
+ .count
+ .each { |date, count| report.data << { x: date, y: count } }
+ report.total = link_topics.count
+ report.prev30Days = link_topics.where("topic_custom_fields.created_at >= ?", report.start_date - 30.days)
+ .where("topic_custom_fields.created_at <= ?", report.start_date)
+ .count
+ end
+
+ def self.cache_onebox_link(link)
+ # If the link is pasted swiftly, onebox may not have time to cache it
+ Oneboxer.onebox(link, invalidate_oneboxes: false)
+ link
+ end
+end
diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb
index faaf55fe46..b64c250957 100644
--- a/lib/email/receiver.rb
+++ b/lib/email/receiver.rb
@@ -244,14 +244,21 @@ module Email
address_field.decoded
from_address = address_field.address
from_display_name = address_field.display_name.try(:to_s)
- return [from_address.downcase, from_display_name] if from_address["@"]
+ return [from_address&.downcase, from_display_name&.strip] if from_address["@"]
end
end
- from_address = mail.from[/<([^>]+)>/, 1]
- from_display_name = mail.from[/^([^<]+)/, 1]
+ if mail.from[/<[^>]+>/]
+ from_address = mail.from[/<([^>]+)>/, 1]
+ from_display_name = mail.from[/^([^<]+)/, 1]
+ end
- [from_address.downcase, from_display_name]
+ if (from_address.blank? || !from_address["@"]) && mail.from[/\[mailto:[^\]]+\]/]
+ from_address = mail.from[/\[mailto:([^\]]+)\]/, 1]
+ from_display_name = mail.from[/^([^\[]+)/, 1]
+ end
+
+ [from_address&.downcase, from_display_name&.strip]
end
def subject
@@ -376,6 +383,9 @@ module Email
def process_forwarded_email(destination, user)
embedded = Mail.new(@embedded_email_raw)
email, display_name = parse_from_field(embedded)
+
+ return false if email.blank? || !email["@"]
+
embedded_user = find_or_create_user(email, display_name)
raw = try_to_encode(embedded.decoded, "UTF-8").presence || embedded.to_s
title = embedded.subject.presence || subject
@@ -387,6 +397,7 @@ module Email
raw: raw,
title: title,
archetype: Archetype.private_message,
+ target_usernames: [user.username],
target_group_names: [group.name],
is_group_message: true,
skip_validations: true,
@@ -409,11 +420,14 @@ module Email
end
if post && post.topic && @before_embedded.present?
+ post_type = Post.types[:regular]
+ post_type = Post.types[:whisper] if post.topic.private_message? && group.usernames[user.username]
+
create_reply(user: user,
raw: @before_embedded,
post: post,
topic: post.topic,
- post_type: Post.types[:whisper])
+ post_type: post_type)
end
true
diff --git a/lib/email/styles.rb b/lib/email/styles.rb
index cb20f3dca1..39adcf88cd 100644
--- a/lib/email/styles.rb
+++ b/lib/email/styles.rb
@@ -67,6 +67,11 @@ module Email
add_styles(img, 'max-width: 100%;') if img['style'] !~ /max-width/
end
+ # topic featured link
+ @fragment.css('a.topic-featured-link').each do |e|
+ e['style'] = "color:#858585;padding:2px 8px;border:1px solid #e6e6e6;border-radius:2px;box-shadow:0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);"
+ end
+
# attachments
@fragment.css('a.attachment').each do |a|
# ensure all urls are absolute
@@ -114,12 +119,12 @@ module Email
style('blockquote > p', 'padding: 1em;')
# Oneboxes
- style('aside.onebox', "padding: 12px 25px 2px 12px; border-left: 5px solid #bebebe; background: #eee; margin-bottom: 10px;")
- style('aside.onebox img', "max-height: 80%; max-width: 25%; height: auto; float: left; margin-right: 10px; margin-bottom: 10px")
- style('aside.onebox h3', "border-bottom: 0")
- style('aside.onebox .source', "margin-bottom: 8px")
- style('aside.onebox .source a[href]', "color: #333; font-weight: normal")
- style('aside.clearfix', "clear: both")
+ style('aside.onebox', "border: 5px solid #e9e9e9; padding: 12px 25px 12px 12px;")
+ style('aside.onebox header a[href]', "color: #222222; text-decoration: none;")
+ style('aside.onebox .onebox-body', "clear: both")
+ style('aside.onebox .onebox-body img', "max-height: 80%; max-width: 20%; height: auto; float: left; margin-right: 10px;")
+ style('aside.onebox .onebox-body h3, aside.onebox .onebox-body h4', "font-size: 1.17em; margin: 10px 0;")
+ style('.onebox-metadata', "color: #919191")
# Finally, convert all `aside` tags to `div`s
@fragment.css('aside, article, header').each do |n|
diff --git a/lib/guardian/category_guardian.rb b/lib/guardian/category_guardian.rb
index dbe5f86819..ccebde7dbf 100644
--- a/lib/guardian/category_guardian.rb
+++ b/lib/guardian/category_guardian.rb
@@ -68,4 +68,9 @@ module CategoryGuardian
def topic_create_allowed_category_ids
@topic_create_allowed_category_ids ||= @user.topic_create_allowed_category_ids
end
+
+ def topic_featured_link_allowed_category_ids
+ @topic_featured_link_allowed_category_ids = CategoryCustomField.where(name: "topic_featured_link_allowed", value: "true")
+ .pluck(:category_id)
+ end
end
diff --git a/lib/guardian/topic_guardian.rb b/lib/guardian/topic_guardian.rb
index 892a8517d9..b107fc7e66 100644
--- a/lib/guardian/topic_guardian.rb
+++ b/lib/guardian/topic_guardian.rb
@@ -105,4 +105,9 @@ module TopicGuardian
records
end
+ def can_edit_featured_link?(category_id)
+ SiteSetting.topic_featured_link_enabled &&
+ (topic_featured_link_allowed_category_ids.empty? || # no per category restrictions
+ category_id && topic_featured_link_allowed_category_ids.include?(category_id.to_i)) # category restriction exists
+ end
end
diff --git a/lib/middleware/anonymous_cache.rb b/lib/middleware/anonymous_cache.rb
index a79b422566..149ac3f009 100644
--- a/lib/middleware/anonymous_cache.rb
+++ b/lib/middleware/anonymous_cache.rb
@@ -11,6 +11,7 @@ module Middleware
class Helper
USER_AGENT = "HTTP_USER_AGENT".freeze
RACK_SESSION = "rack.session".freeze
+ ACCEPT_ENCODING = "HTTP_ACCEPT_ENCODING".freeze
def initialize(env)
@env = env
@@ -35,6 +36,14 @@ module Middleware
@is_mobile == :true
end
+ def has_brotli?
+ @has_brotli ||=
+ begin
+ @env[ACCEPT_ENCODING].to_s =~ /br/ ? :true : :false
+ end
+ @has_brotli == :true
+ end
+
def is_crawler?
@is_crawler ||=
begin
@@ -45,7 +54,7 @@ module Middleware
end
def cache_key
- @cache_key ||= "ANON_CACHE_#{@env["HTTP_ACCEPT"]}_#{@env["HTTP_HOST"]}#{@env["REQUEST_URI"]}|m=#{is_mobile?}|c=#{is_crawler?}"
+ @cache_key ||= "ANON_CACHE_#{@env["HTTP_ACCEPT"]}_#{@env["HTTP_HOST"]}#{@env["REQUEST_URI"]}|m=#{is_mobile?}|c=#{is_crawler?}|b=#{has_brotli?}"
end
def cache_key_body
diff --git a/lib/post_creator.rb b/lib/post_creator.rb
index 0cd26aa33d..6a788cf5bd 100644
--- a/lib/post_creator.rb
+++ b/lib/post_creator.rb
@@ -5,6 +5,7 @@ require_dependency 'topic_creator'
require_dependency 'post_jobs_enqueuer'
require_dependency 'distributed_mutex'
require_dependency 'has_errors'
+require_dependency 'discourse_featured_link'
class PostCreator
include HasErrors
@@ -103,6 +104,11 @@ class PostCreator
end
end
+ onebox_featured_link = SiteSetting.topic_featured_link_enabled && SiteSetting.topic_featured_link_onebox && guardian.can_edit_featured_link?(find_category_id)
+ if onebox_featured_link
+ @opts[:raw] = DiscourseFeaturedLink.cache_onebox_link(@opts[:featured_link])
+ end
+
setup_post
return true if skip_validations?
@@ -116,7 +122,7 @@ class PostCreator
DiscourseEvent.trigger :before_create_post, @post
DiscourseEvent.trigger :validate_post, @post
- post_validator = Validators::PostValidator.new(skip_topic: true)
+ post_validator = Validators::PostValidator.new(skip_topic: true, skip_post_body: onebox_featured_link)
post_validator.validate(@post)
valid = @post.errors.blank?
@@ -146,6 +152,9 @@ class PostCreator
end
if @post && errors.blank?
+ # update counters etc.
+ @post.topic.reload
+
publish
track_latest_on_category
@@ -199,7 +208,9 @@ class PostCreator
set_reply_info(post)
post.word_count = post.raw.scan(/[[:word:]]+/).size
- post.post_number ||= Topic.next_post_number(post.topic_id, post.reply_to_post_number.present?)
+
+ whisper = post.post_type == Post.types[:whisper]
+ post.post_number ||= Topic.next_post_number(post.topic_id, post.reply_to_post_number.present?, whisper)
cooking_options = post.cooking_options || {}
cooking_options[:topic_id] = post.topic_id
@@ -333,6 +344,18 @@ class PostCreator
private
+ # TODO: merge the similar function in TopicCreator and fix parameter naming for `category`
+ def find_category_id
+ @opts.delete(:category) if @opts[:archetype].present? && @opts[:archetype] == Archetype.private_message
+
+ category = if (@opts[:category].is_a? Integer) || (@opts[:category] =~ /^\d+$/)
+ Category.find_by(id: @opts[:category])
+ else
+ Category.find_by(name_lower: @opts[:category].try(:downcase))
+ end
+ category&.id
+ end
+
def create_topic
return if @topic
begin
diff --git a/lib/post_jobs_enqueuer.rb b/lib/post_jobs_enqueuer.rb
index c5f1f6a9d1..0e05c565f9 100644
--- a/lib/post_jobs_enqueuer.rb
+++ b/lib/post_jobs_enqueuer.rb
@@ -35,7 +35,7 @@ class PostJobsEnqueuer
def after_post_create
TopicTrackingState.publish_unread(@post) if @post.post_number > 1
- TopicTrackingState.publish_latest(@topic)
+ TopicTrackingState.publish_latest(@topic, @post.post_type == Post.types[:whisper])
Jobs.enqueue_in(
SiteSetting.email_time_window_mins.minutes,
diff --git a/lib/post_revisor.rb b/lib/post_revisor.rb
index b9cccc9f9d..a5451affba 100644
--- a/lib/post_revisor.rb
+++ b/lib/post_revisor.rb
@@ -95,6 +95,23 @@ class PostRevisor
end
end
+ track_topic_field(:featured_link) do |topic_changes, featured_link|
+ if SiteSetting.topic_featured_link_enabled &&
+ featured_link.present? &&
+ topic_changes.guardian.can_edit_featured_link?(topic_changes.topic.category_id)
+
+ topic_changes.record_change('featured_link', topic_changes.topic.featured_link, featured_link)
+ topic_changes.topic.featured_link = featured_link
+
+ if SiteSetting.topic_featured_link_onebox
+ post = topic_changes.topic.first_post
+ post.raw = DiscourseFeaturedLink.cache_onebox_link(featured_link)
+ post.save!
+ post.rebake!
+ end
+ end
+ end
+
# AVAILABLE OPTIONS:
# - revised_at: changes the date of the revision
# - force_new_version: bypass ninja-edit window
@@ -425,18 +442,14 @@ class PostRevisor
def update_category_description
return unless category = Category.find_by(topic_id: @topic.id)
- body = @post.cooked
- matches = body.scan(/\(.*)\<\/p\>/)
+ doc = Nokogiri::HTML.fragment(@post.cooked)
+ doc.css("img").remove
- matches.each do |match|
- next if match[0] =~ /\ = ?", match.to_i)
+ end
+
advanced_filter(/in:first/) do |posts|
posts.where("posts.post_number = 1")
end
diff --git a/lib/system_message.rb b/lib/system_message.rb
index 98280b5e9c..d95cbf00be 100644
--- a/lib/system_message.rb
+++ b/lib/system_message.rb
@@ -33,7 +33,7 @@ class SystemMessage
post = creator.create
if creator.errors.present?
- raise StandardError, creator.errors.to_s
+ raise StandardError, creator.errors.full_messages.join(" ")
end
UserArchivedMessage.create!(user: Discourse.site_contact_user, topic: post.topic)
diff --git a/lib/tasks/qunit.rake b/lib/tasks/qunit.rake
index 70ff6d5e45..b4e9e22887 100644
--- a/lib/tasks/qunit.rake
+++ b/lib/tasks/qunit.rake
@@ -1,6 +1,6 @@
desc "Runs the qunit test suite"
-task "qunit:test" => :environment do
+task "qunit:test", [:timeout] => :environment do |_, args|
require "rack"
require "socket"
@@ -35,7 +35,7 @@ task "qunit:test" => :environment do
begin
success = true
test_path = "#{Rails.root}/vendor/assets/javascripts"
- cmd = "phantomjs #{test_path}/run-qunit.js http://localhost:#{port}/qunit"
+ cmd = "phantomjs #{test_path}/run-qunit.js http://localhost:#{port}/qunit #{args[:timeout]}"
options = {}
diff --git a/lib/tasks/redis.rake b/lib/tasks/redis.rake
new file mode 100644
index 0000000000..4300f0686c
--- /dev/null
+++ b/lib/tasks/redis.rake
@@ -0,0 +1,30 @@
+task 'redis:clean_up' => ['environment'] do
+ return unless Rails.configuration.multisite
+
+ dbs = RailsMultisite::ConnectionManagement.all_dbs
+ dbs << Discourse::SIDEKIQ_NAMESPACE
+
+ regexp = /((\$(?\w+)$)|(^?(?\w+):))/
+
+ cursor = 0
+ redis = $redis.without_namespace
+
+ loop do
+ cursor, keys = redis.scan(cursor)
+ cursor = cursor.to_i
+
+ redis.multi do
+ keys.each do |key|
+ if match = key.match(regexp)
+ db_name = match[:message_bus] || match[:namespace]
+
+ if !dbs.include?(db_name)
+ redis.del(key)
+ end
+ end
+ end
+ end
+
+ break if cursor == 0
+ end
+end
diff --git a/lib/topic_creator.rb b/lib/topic_creator.rb
index 9e76327a5a..8c0621d507 100644
--- a/lib/topic_creator.rb
+++ b/lib/topic_creator.rb
@@ -124,6 +124,10 @@ class TopicCreator
topic_params[:pinned_at] = Time.zone.parse(@opts[:pinned_at].to_s) if @opts[:pinned_at].present?
topic_params[:pinned_globally] = @opts[:pinned_globally] if @opts[:pinned_globally].present?
+ if SiteSetting.topic_featured_link_enabled && @opts[:featured_link].present? && @guardian.can_edit_featured_link?(topic_params[:category_id])
+ topic_params[:featured_link] = @opts[:featured_link]
+ end
+
topic_params
end
diff --git a/lib/topic_query.rb b/lib/topic_query.rb
index f13cbbd8bc..3da879c6a3 100644
--- a/lib/topic_query.rb
+++ b/lib/topic_query.rb
@@ -242,9 +242,12 @@ class TopicQuery
.where("COALESCE(tu.notification_level, :tracking) >= :tracking", tracking: TopicUser.notification_levels[:tracking])
end
- def self.unread_filter(list)
- list.where("tu.last_read_post_number < topics.highest_post_number")
- .where("COALESCE(tu.notification_level, :regular) >= :tracking", regular: TopicUser.notification_levels[:regular], tracking: TopicUser.notification_levels[:tracking])
+ def self.unread_filter(list, opts)
+ col_name = opts[:staff] ? "highest_staff_post_number" : "highest_post_number"
+
+ list.where("tu.last_read_post_number < topics.#{col_name}")
+ .where("COALESCE(tu.notification_level, :regular) >= :tracking",
+ regular: TopicUser.notification_levels[:regular], tracking: TopicUser.notification_levels[:tracking])
end
def prioritize_pinned_topics(topics, options)
@@ -320,7 +323,7 @@ class TopicQuery
end
def unread_results(options={})
- result = TopicQuery.unread_filter(default_results(options.reverse_merge(:unordered => true)))
+ result = TopicQuery.unread_filter(default_results(options.reverse_merge(:unordered => true)), staff: @user.try(:staff?))
.order('CASE WHEN topics.user_id = tu.user_id THEN 1 ELSE 2 END')
self.class.results_filter_callbacks.each do |filter_callback|
@@ -656,7 +659,7 @@ class TopicQuery
end
def unread_messages(params)
- TopicQuery.unread_filter(messages_for_groups_or_user(params[:my_group_ids]))
+ TopicQuery.unread_filter(messages_for_groups_or_user(params[:my_group_ids]), staff: @user.try(:staff?))
.limit(params[:count])
end
diff --git a/lib/unread.rb b/lib/unread.rb
index a5a062d319..f04ffcd228 100644
--- a/lib/unread.rb
+++ b/lib/unread.rb
@@ -2,7 +2,8 @@ class Unread
# This module helps us calculate unread and new post counts
- def initialize(topic, topic_user)
+ def initialize(topic, topic_user, guardian)
+ @guardian = guardian
@topic = topic
@topic_user = topic_user
end
@@ -18,9 +19,12 @@ class Unread
def new_posts
return 0 if @topic_user.highest_seen_post_number.blank?
return 0 if do_not_notify?(@topic_user.notification_level)
- return 0 if (@topic_user.last_read_post_number||0) > @topic.highest_post_number
- new_posts = (@topic.highest_post_number - @topic_user.highest_seen_post_number)
+ highest_post_number = @guardian.is_staff? ? @topic.highest_staff_post_number : @topic.highest_post_number
+
+ return 0 if (@topic_user.last_read_post_number||0) > highest_post_number
+
+ new_posts = (highest_post_number - @topic_user.highest_seen_post_number)
new_posts = 0 if new_posts < 0
return new_posts
end
diff --git a/lib/validators/post_validator.rb b/lib/validators/post_validator.rb
index 32f6063596..30e68bafad 100644
--- a/lib/validators/post_validator.rb
+++ b/lib/validators/post_validator.rb
@@ -10,8 +10,7 @@ class Validators::PostValidator < ActiveModel::Validator
return if record.acting_user.try(:staged?)
return if record.acting_user.try(:admin?) && Discourse.static_doc_topic_ids.include?(record.topic_id)
- stripped_length(record)
- raw_quality(record)
+ post_body_validator(record)
max_posts_validator(record)
max_mention_validator(record)
max_images_validator(record)
@@ -21,8 +20,6 @@ class Validators::PostValidator < ActiveModel::Validator
end
def presence(post)
- post.errors.add(:raw, :blank, options) if post.raw.blank?
-
unless options[:skip_topic]
post.errors.add(:topic_id, :blank, options) if post.topic_id.blank?
end
@@ -32,6 +29,12 @@ class Validators::PostValidator < ActiveModel::Validator
end
end
+ def post_body_validator(post)
+ return if options[:skip_post_body]
+ stripped_length(post)
+ raw_quality(post)
+ end
+
def stripped_length(post)
range = if private_message?(post)
# private message
diff --git a/lib/version.rb b/lib/version.rb
index 430e4776fb..11ffbe5033 100644
--- a/lib/version.rb
+++ b/lib/version.rb
@@ -5,7 +5,7 @@ module Discourse
MAJOR = 1
MINOR = 7
TINY = 0
- PRE = 'beta8'
+ PRE = 'beta9'
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
end
diff --git a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6
index 9631c3ad99..77a25aaf03 100644
--- a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6
+++ b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6
@@ -88,8 +88,16 @@ function initializePolls(api) {
votes[pollName]
);
+ // Destroy a poll view if we're replacing it
+ if (_pollViews && _pollViews[pollId]) {
+ _pollViews[pollId].destroy();
+ }
+
$poll.replaceWith($div);
- Em.run.schedule('afterRender', () => pollComponent.renderer.replaceIn(pollComponent, $div[0]));
+ Ember.run.scheduleOnce('afterRender', () => {
+ pollComponent.renderer.appendTo(pollComponent, $div[0]);
+ });
+
postPollViews[pollId] = pollComponent;
});
diff --git a/plugins/poll/assets/javascripts/lib/md5.js.es6 b/plugins/poll/assets/javascripts/lib/md5.js.es6
deleted file mode 100644
index 8b13789179..0000000000
--- a/plugins/poll/assets/javascripts/lib/md5.js.es6
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/plugins/poll/test/javascripts/acceptance/polls-test.js.es6 b/plugins/poll/test/javascripts/acceptance/polls-test.js.es6
new file mode 100644
index 0000000000..401838b610
--- /dev/null
+++ b/plugins/poll/test/javascripts/acceptance/polls-test.js.es6
@@ -0,0 +1,32 @@
+import { acceptance, controllerFor } from "helpers/qunit-helpers";
+import PostCooked from 'discourse/widgets/post-cooked';
+
+acceptance("Rendering polls", {
+ loggedIn: true,
+ settings: { poll_enabled: true },
+ setup() {
+ const response = object => {
+ return [
+ 200,
+ { "Content-Type": "application/json" },
+ object
+ ];
+ }
+
+ server.get('/t/13.json', () => {
+ return response({"post_stream":{"posts":[{"id":19,"name":null,"username":"tgx","avatar_template":"/letter_avatar_proxy/v2/letter/t/ecae2f/{size}.png","created_at":"2016-12-01T02:39:49.199Z","cooked":"\n\n","post_number":1,"post_type":1,"updated_at":"2016-12-01T02:47:18.317Z","reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":null,"incoming_link_count":0,"reads":1,"score":0,"yours":true,"topic_id":13,"topic_slug":"this-is-a-test-topic-for-polls","display_username":null,"primary_group_name":null,"primary_group_flair_url":null,"primary_group_flair_bg_color":null,"primary_group_flair_color":null,"version":2,"can_edit":true,"can_delete":false,"can_recover":true,"can_wiki":true,"read":true,"user_title":null,"actions_summary":[{"id":3,"can_act":true},{"id":4,"can_act":true},{"id":5,"hidden":true,"can_act":true},{"id":7,"can_act":true},{"id":8,"can_act":true}],"moderator":false,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false,"polls":{"poll":{"options":[{"id":"57ddd734344eb7436d64a7d68a0df444","html":"test","votes":0},{"id":"b5b78d79ab5b5d75d4d33d8b87f5d2aa","html":"haha","votes":0}],"voters":2,"status":"open","name":"poll"},"test":{"options":[{"id":"c26ad90783b0d80936e5fdb292b7963c","html":"donkey","votes":0},{"id":"99f2b9ac452ba73b115fcf3556e6d2d4","html":"kong","votes":0}],"voters":3,"status":"open","name":"test"}}}],"stream":[19]},"timeline_lookup":[[1,0]],"id":13,"title":"This is a test topic for polls","fancy_title":"This is a test topic for polls","posts_count":1,"created_at":"2016-12-01T02:39:48.055Z","views":1,"reply_count":0,"participant_count":1,"like_count":0,"last_posted_at":"2016-12-01T02:39:49.199Z","visible":true,"closed":false,"archived":false,"has_summary":false,"archetype":"regular","slug":"this-is-a-test-topic-for-polls","category_id":1,"word_count":10,"deleted_at":null,"user_id":1,"draft":null,"draft_key":"topic_13","draft_sequence":4,"posted":true,"unpinned":null,"pinned_globally":false,"pinned":false,"pinned_at":null,"pinned_until":null,"details":{"auto_close_at":null,"auto_close_hours":null,"auto_close_based_on_last_post":false,"created_by":{"id":1,"username":"tgx","avatar_template":"/letter_avatar_proxy/v2/letter/t/ecae2f/{size}.png"},"last_poster":{"id":1,"username":"tgx","avatar_template":"/letter_avatar_proxy/v2/letter/t/ecae2f/{size}.png"},"participants":[{"id":1,"username":"tgx","avatar_template":"/letter_avatar_proxy/v2/letter/t/ecae2f/{size}.png","post_count":1}],"suggested_topics":[{"id":8,"title":"Welcome to Discourse","fancy_title":"Welcome to Discourse","slug":"welcome-to-discourse","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2016-11-24T02:10:54.328Z","last_posted_at":"2016-11-24T02:10:54.393Z","bumped":true,"bumped_at":"2016-11-24T02:10:54.393Z","unseen":false,"pinned":true,"unpinned":null,"excerpt":"The first paragraph of this pinned topic will be visible as a welcome message to all new visitors on your homepage. It's important! \n\nEdit this into a brief description of your community: \n\n\nWho is it for?\nWhat can they …","visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":0,"views":0,"category_id":1,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user":{"id":-1,"username":"system","avatar_template":"/letter_avatar_proxy/v2/letter/s/bcef8e/{size}.png"}}]},{"id":12,"title":"Some testing topic testing","fancy_title":"Some testing topic testing","slug":"some-testing-topic-testing","posts_count":4,"reply_count":0,"highest_post_number":4,"image_url":null,"created_at":"2016-11-24T08:36:08.773Z","last_posted_at":"2016-12-01T01:15:52.008Z","bumped":true,"bumped_at":"2016-12-01T01:15:52.008Z","unseen":false,"last_read_post_number":4,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":3,"bookmarked":false,"liked":false,"archetype":"regular","like_count":0,"views":2,"category_id":1,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user":{"id":1,"username":"tgx","avatar_template":"/letter_avatar_proxy/v2/letter/t/ecae2f/{size}.png"}}]},{"id":11,"title":"Some testing topic","fancy_title":"Some testing topic","slug":"some-testing-topic","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2016-11-24T08:35:26.758Z","last_posted_at":"2016-11-24T08:35:26.894Z","bumped":true,"bumped_at":"2016-11-24T08:35:26.894Z","unseen":false,"last_read_post_number":1,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":3,"bookmarked":false,"liked":false,"archetype":"regular","like_count":0,"views":0,"category_id":1,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user":{"id":1,"username":"tgx","avatar_template":"/letter_avatar_proxy/v2/letter/t/ecae2f/{size}.png"}}]}],"notification_level":3,"notifications_reason_id":1,"can_move_posts":true,"can_edit":true,"can_delete":true,"can_recover":true,"can_remove_allowed_users":true,"can_invite_to":true,"can_create_post":true,"can_reply_as_new_topic":true,"can_flag_topic":true},"highest_post_number":1,"last_read_post_number":1,"last_read_post_id":19,"deleted_by":null,"has_deleted":false,"actions_summary":[{"id":4,"count":0,"hidden":false,"can_act":true},{"id":7,"count":0,"hidden":false,"can_act":true},{"id":8,"count":0,"hidden":false,"can_act":true}],"chunk_size":20,"bookmarked":false});
+ });
+ }
+});
+
+test("Single Poll", () => {
+ visit("/t/this-is-a-test-topic-for-polls/13");
+
+ andThen(() => {
+ const polls = find('.poll');
+
+ equal(polls.length, 2, 'it should render the polls correctly');
+ equal(find('.info-number', polls[0]).text(), '2', 'it should display the right number of votes');
+ equal(find('.info-number', polls[1]).text(), '3', 'it should display the right number of votes');
+ });
+});
diff --git a/public/images/emoji/apple/eight.png b/public/images/emoji/apple/eight.png
index e69de29bb2..ba8ed5cf21 100644
Binary files a/public/images/emoji/apple/eight.png and b/public/images/emoji/apple/eight.png differ
diff --git a/public/images/emoji/apple/five.png b/public/images/emoji/apple/five.png
index e69de29bb2..97fcc34bf7 100644
Binary files a/public/images/emoji/apple/five.png and b/public/images/emoji/apple/five.png differ
diff --git a/public/images/emoji/apple/four.png b/public/images/emoji/apple/four.png
index e69de29bb2..3ed87090ed 100644
Binary files a/public/images/emoji/apple/four.png and b/public/images/emoji/apple/four.png differ
diff --git a/public/images/emoji/apple/nine.png b/public/images/emoji/apple/nine.png
index e69de29bb2..52c6c63dcf 100644
Binary files a/public/images/emoji/apple/nine.png and b/public/images/emoji/apple/nine.png differ
diff --git a/public/images/emoji/apple/one.png b/public/images/emoji/apple/one.png
index e69de29bb2..fbd9bcf592 100644
Binary files a/public/images/emoji/apple/one.png and b/public/images/emoji/apple/one.png differ
diff --git a/public/images/emoji/apple/seven.png b/public/images/emoji/apple/seven.png
index e69de29bb2..0c675d1252 100644
Binary files a/public/images/emoji/apple/seven.png and b/public/images/emoji/apple/seven.png differ
diff --git a/public/images/emoji/apple/six.png b/public/images/emoji/apple/six.png
index e69de29bb2..0c7618b6ca 100644
Binary files a/public/images/emoji/apple/six.png and b/public/images/emoji/apple/six.png differ
diff --git a/public/images/emoji/apple/three.png b/public/images/emoji/apple/three.png
index e69de29bb2..6a0af40ae7 100644
Binary files a/public/images/emoji/apple/three.png and b/public/images/emoji/apple/three.png differ
diff --git a/public/images/emoji/apple/two.png b/public/images/emoji/apple/two.png
index e69de29bb2..17e3962e6f 100644
Binary files a/public/images/emoji/apple/two.png and b/public/images/emoji/apple/two.png differ
diff --git a/public/images/emoji/emoji_one/eight.png b/public/images/emoji/emoji_one/eight.png
index e69de29bb2..3b8024f65c 100644
Binary files a/public/images/emoji/emoji_one/eight.png and b/public/images/emoji/emoji_one/eight.png differ
diff --git a/public/images/emoji/emoji_one/five.png b/public/images/emoji/emoji_one/five.png
index e69de29bb2..a4ccd7fd73 100644
Binary files a/public/images/emoji/emoji_one/five.png and b/public/images/emoji/emoji_one/five.png differ
diff --git a/public/images/emoji/emoji_one/four.png b/public/images/emoji/emoji_one/four.png
index e69de29bb2..15f5cf1977 100644
Binary files a/public/images/emoji/emoji_one/four.png and b/public/images/emoji/emoji_one/four.png differ
diff --git a/public/images/emoji/emoji_one/nine.png b/public/images/emoji/emoji_one/nine.png
index e69de29bb2..61af7b1553 100644
Binary files a/public/images/emoji/emoji_one/nine.png and b/public/images/emoji/emoji_one/nine.png differ
diff --git a/public/images/emoji/emoji_one/one.png b/public/images/emoji/emoji_one/one.png
index e69de29bb2..2e02f0dc59 100644
Binary files a/public/images/emoji/emoji_one/one.png and b/public/images/emoji/emoji_one/one.png differ
diff --git a/public/images/emoji/emoji_one/seven.png b/public/images/emoji/emoji_one/seven.png
index e69de29bb2..082c0c853f 100644
Binary files a/public/images/emoji/emoji_one/seven.png and b/public/images/emoji/emoji_one/seven.png differ
diff --git a/public/images/emoji/emoji_one/six.png b/public/images/emoji/emoji_one/six.png
index e69de29bb2..e63c0ca97f 100644
Binary files a/public/images/emoji/emoji_one/six.png and b/public/images/emoji/emoji_one/six.png differ
diff --git a/public/images/emoji/emoji_one/three.png b/public/images/emoji/emoji_one/three.png
index e69de29bb2..f0ba2b4db5 100644
Binary files a/public/images/emoji/emoji_one/three.png and b/public/images/emoji/emoji_one/three.png differ
diff --git a/public/images/emoji/emoji_one/two.png b/public/images/emoji/emoji_one/two.png
index e69de29bb2..0f8b3c87e8 100644
Binary files a/public/images/emoji/emoji_one/two.png and b/public/images/emoji/emoji_one/two.png differ
diff --git a/public/images/emoji/google/eight.png b/public/images/emoji/google/eight.png
index e69de29bb2..af151748ca 100644
Binary files a/public/images/emoji/google/eight.png and b/public/images/emoji/google/eight.png differ
diff --git a/public/images/emoji/google/five.png b/public/images/emoji/google/five.png
index e69de29bb2..2ea1be6607 100644
Binary files a/public/images/emoji/google/five.png and b/public/images/emoji/google/five.png differ
diff --git a/public/images/emoji/google/four.png b/public/images/emoji/google/four.png
index e69de29bb2..32d830771a 100644
Binary files a/public/images/emoji/google/four.png and b/public/images/emoji/google/four.png differ
diff --git a/public/images/emoji/google/nine.png b/public/images/emoji/google/nine.png
index e69de29bb2..589d508d5a 100644
Binary files a/public/images/emoji/google/nine.png and b/public/images/emoji/google/nine.png differ
diff --git a/public/images/emoji/google/one.png b/public/images/emoji/google/one.png
index e69de29bb2..fd231c9dcc 100644
Binary files a/public/images/emoji/google/one.png and b/public/images/emoji/google/one.png differ
diff --git a/public/images/emoji/google/seven.png b/public/images/emoji/google/seven.png
index e69de29bb2..8a1fe0417c 100644
Binary files a/public/images/emoji/google/seven.png and b/public/images/emoji/google/seven.png differ
diff --git a/public/images/emoji/google/six.png b/public/images/emoji/google/six.png
index e69de29bb2..2cf9e489f3 100644
Binary files a/public/images/emoji/google/six.png and b/public/images/emoji/google/six.png differ
diff --git a/public/images/emoji/google/three.png b/public/images/emoji/google/three.png
index e69de29bb2..600c2b652e 100644
Binary files a/public/images/emoji/google/three.png and b/public/images/emoji/google/three.png differ
diff --git a/public/images/emoji/google/two.png b/public/images/emoji/google/two.png
index e69de29bb2..1de23db789 100644
Binary files a/public/images/emoji/google/two.png and b/public/images/emoji/google/two.png differ
diff --git a/public/images/emoji/twitter/eight.png b/public/images/emoji/twitter/eight.png
index e69de29bb2..90f774c392 100644
Binary files a/public/images/emoji/twitter/eight.png and b/public/images/emoji/twitter/eight.png differ
diff --git a/public/images/emoji/twitter/five.png b/public/images/emoji/twitter/five.png
index e69de29bb2..75a85c48ad 100644
Binary files a/public/images/emoji/twitter/five.png and b/public/images/emoji/twitter/five.png differ
diff --git a/public/images/emoji/twitter/four.png b/public/images/emoji/twitter/four.png
index e69de29bb2..77de1b164b 100644
Binary files a/public/images/emoji/twitter/four.png and b/public/images/emoji/twitter/four.png differ
diff --git a/public/images/emoji/twitter/nine.png b/public/images/emoji/twitter/nine.png
index e69de29bb2..1e425523f8 100644
Binary files a/public/images/emoji/twitter/nine.png and b/public/images/emoji/twitter/nine.png differ
diff --git a/public/images/emoji/twitter/one.png b/public/images/emoji/twitter/one.png
index e69de29bb2..9369fcc06b 100644
Binary files a/public/images/emoji/twitter/one.png and b/public/images/emoji/twitter/one.png differ
diff --git a/public/images/emoji/twitter/seven.png b/public/images/emoji/twitter/seven.png
index e69de29bb2..6547329774 100644
Binary files a/public/images/emoji/twitter/seven.png and b/public/images/emoji/twitter/seven.png differ
diff --git a/public/images/emoji/twitter/six.png b/public/images/emoji/twitter/six.png
index e69de29bb2..990c260628 100644
Binary files a/public/images/emoji/twitter/six.png and b/public/images/emoji/twitter/six.png differ
diff --git a/public/images/emoji/twitter/three.png b/public/images/emoji/twitter/three.png
index e69de29bb2..cdd7ca9db3 100644
Binary files a/public/images/emoji/twitter/three.png and b/public/images/emoji/twitter/three.png differ
diff --git a/public/images/emoji/twitter/two.png b/public/images/emoji/twitter/two.png
index e69de29bb2..3ca9cd0232 100644
Binary files a/public/images/emoji/twitter/two.png and b/public/images/emoji/twitter/two.png differ
diff --git a/public/images/emoji/win10/eight.png b/public/images/emoji/win10/eight.png
index e69de29bb2..941ddf7da0 100644
Binary files a/public/images/emoji/win10/eight.png and b/public/images/emoji/win10/eight.png differ
diff --git a/public/images/emoji/win10/five.png b/public/images/emoji/win10/five.png
index e69de29bb2..f8b21706da 100644
Binary files a/public/images/emoji/win10/five.png and b/public/images/emoji/win10/five.png differ
diff --git a/public/images/emoji/win10/four.png b/public/images/emoji/win10/four.png
index e69de29bb2..3a22419dc5 100644
Binary files a/public/images/emoji/win10/four.png and b/public/images/emoji/win10/four.png differ
diff --git a/public/images/emoji/win10/nine.png b/public/images/emoji/win10/nine.png
index e69de29bb2..655f80a7f9 100644
Binary files a/public/images/emoji/win10/nine.png and b/public/images/emoji/win10/nine.png differ
diff --git a/public/images/emoji/win10/one.png b/public/images/emoji/win10/one.png
index e69de29bb2..eba530a82c 100644
Binary files a/public/images/emoji/win10/one.png and b/public/images/emoji/win10/one.png differ
diff --git a/public/images/emoji/win10/seven.png b/public/images/emoji/win10/seven.png
index e69de29bb2..c1329dd773 100644
Binary files a/public/images/emoji/win10/seven.png and b/public/images/emoji/win10/seven.png differ
diff --git a/public/images/emoji/win10/six.png b/public/images/emoji/win10/six.png
index e69de29bb2..8e4b23b3ba 100644
Binary files a/public/images/emoji/win10/six.png and b/public/images/emoji/win10/six.png differ
diff --git a/public/images/emoji/win10/three.png b/public/images/emoji/win10/three.png
index e69de29bb2..bf930925c4 100644
Binary files a/public/images/emoji/win10/three.png and b/public/images/emoji/win10/three.png differ
diff --git a/public/images/emoji/win10/two.png b/public/images/emoji/win10/two.png
index e69de29bb2..5a7e481947 100644
Binary files a/public/images/emoji/win10/two.png and b/public/images/emoji/win10/two.png differ
diff --git a/script/import_scripts/vbulletin.rb b/script/import_scripts/vbulletin.rb
index 90b943b71e..8ff7c0340a 100644
--- a/script/import_scripts/vbulletin.rb
+++ b/script/import_scripts/vbulletin.rb
@@ -31,6 +31,7 @@ class ImportScripts::VBulletin < ImportScripts::Base
def execute
import_groups
import_users
+ create_groups_membership
import_categories
import_topics
import_posts
@@ -40,7 +41,7 @@ class ImportScripts::VBulletin < ImportScripts::Base
close_topics
post_process_posts
- create_permalinks
+ create_permalink_file
suspend_users
end
@@ -89,7 +90,7 @@ class ImportScripts::VBulletin < ImportScripts::Base
email: user["email"].presence || fake_email,
website: user["homepage"].strip,
title: @htmlentities.decode(user["usertitle"]).strip,
- primary_group_id: group_id_from_imported_group_id(user["usergroupid"]),
+ primary_group_id: group_id_from_imported_group_id(user["usergroupid"].to_i),
created_at: parse_timestamp(user["joindate"]),
last_seen_at: parse_timestamp(user["lastvisit"]),
post_create_action: proc do |u|
@@ -102,6 +103,32 @@ class ImportScripts::VBulletin < ImportScripts::Base
end
end
+ def create_groups_membership
+ puts "", "Creating groups membership..."
+
+ Group.find_each do |group|
+ begin
+ next if group.automatic
+ puts "\t#{group.name}"
+ next if GroupUser.where(group_id: group.id).count > 0
+ user_ids_in_group = User.where(primary_group_id: group.id).pluck(:id).to_a
+ next if user_ids_in_group.size == 0
+ values = user_ids_in_group.map { |user_id| "(#{group.id}, #{user_id}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" }.join(",")
+
+ User.exec_sql <<-SQL
+ BEGIN;
+ INSERT INTO group_users (group_id, user_id, created_at, updated_at) VALUES #{values};
+ COMMIT;
+ SQL
+
+ Group.reset_counters(group.id, :group_users)
+ rescue Exception => e
+ puts e.message
+ puts e.backtrace.join("\n")
+ end
+ end
+ end
+
def import_profile_picture(old_user, imported_user)
query = mysql_query <<-SQL
SELECT filedata, filename
@@ -163,9 +190,9 @@ class ImportScripts::VBulletin < ImportScripts::Base
categories = mysql_query("SELECT forumid, title, description, displayorder, parentid FROM #{TABLE_PREFIX}forum ORDER BY forumid").to_a
- # top_level_categories = categories.select { |c| c["parentid"] == -1 }
+ top_level_categories = categories.select { |c| c["parentid"] == -1 }
- create_categories(categories) do |category|
+ create_categories(top_level_categories) do |category|
{
id: category["forumid"],
name: @htmlentities.decode(category["title"]).strip,
@@ -174,27 +201,27 @@ class ImportScripts::VBulletin < ImportScripts::Base
}
end
- # puts "", "importing children categories..."
- #
- # children_categories = categories.select { |c| c["parentid"] != -1 }
- # top_level_category_ids = Set.new(top_level_categories.map { |c| c["forumid"] })
- #
- # # cut down the tree to only 2 levels of categories
- # children_categories.each do |cc|
- # while !top_level_category_ids.include?(cc["parentid"])
- # cc["parentid"] = categories.detect { |c| c["forumid"] == cc["parentid"] }["parentid"]
- # end
- # end
- #
- # create_categories(children_categories) do |category|
- # {
- # id: category["forumid"],
- # name: @htmlentities.decode(category["title"]).strip,
- # position: category["displayorder"],
- # description: @htmlentities.decode(category["description"]).strip,
- # parent_category_id: category_id_from_imported_category_id(category["parentid"])
- # }
- # end
+ puts "", "importing children categories..."
+
+ children_categories = categories.select { |c| c["parentid"] != -1 }
+ top_level_category_ids = Set.new(top_level_categories.map { |c| c["forumid"] })
+
+ # cut down the tree to only 2 levels of categories
+ children_categories.each do |cc|
+ while !top_level_category_ids.include?(cc["parentid"])
+ cc["parentid"] = categories.detect { |c| c["forumid"] == cc["parentid"] }["parentid"]
+ end
+ end
+
+ create_categories(children_categories) do |category|
+ {
+ id: category["forumid"],
+ name: @htmlentities.decode(category["title"]).strip,
+ position: category["displayorder"],
+ description: @htmlentities.decode(category["description"]).strip,
+ parent_category_id: category_id_from_imported_category_id(category["parentid"])
+ }
+ end
end
def import_topics
@@ -223,7 +250,7 @@ class ImportScripts::VBulletin < ImportScripts::Base
raw = preprocess_post_raw(topic["raw"]) rescue nil
next if raw.blank?
topic_id = "thread-#{topic["threadid"]}"
- @closed_topic_ids << topic_id if topic["open"] == "0"
+ @closed_topic_ids << topic_id if topic["open"] == 0
t = {
id: topic_id,
user_id: user_id_from_imported_user_id(topic["postuserid"]) || Discourse::SYSTEM_USER_ID,
@@ -237,6 +264,18 @@ class ImportScripts::VBulletin < ImportScripts::Base
t[:pinned_at] = t[:created_at] if topic["sticky"].to_i == 1
t
end
+
+ # uncomment below lines to create permalink
+ # topics.each do |thread|
+ # topic_id = "thread-#{thread["threadid"]}"
+ # topic = topic_lookup_from_imported_post_id(topic_id)
+ # if topic.present?
+ # title_slugified = thread["title"].gsub(" ","-").gsub(".","-") if thread["title"].present?
+ # url_slug = "threads/#{thread["threadid"]}-#{title_slugified}" if thread["title"].present?
+ # Permalink.create(url: url_slug, topic_id: topic[:topic_id].to_i) if url_slug.present? && topic[:topic_id].present?
+ # end
+ # end
+
end
end
@@ -244,7 +283,11 @@ class ImportScripts::VBulletin < ImportScripts::Base
puts "", "importing posts..."
# make sure `firstpostid` is indexed
- mysql_query("CREATE INDEX firstpostid_index ON #{TABLE_PREFIX}thread (firstpostid)")
+ begin
+ mysql_query("CREATE INDEX firstpostid_index ON #{TABLE_PREFIX}thread (firstpostid)")
+ rescue Mysql2::Error
+ puts 'Index already exists'
+ end
post_count = mysql_query("SELECT COUNT(postid) count FROM #{TABLE_PREFIX}post WHERE postid NOT IN (SELECT firstpostid FROM #{TABLE_PREFIX}thread)").first["count"]
@@ -469,15 +512,15 @@ class ImportScripts::VBulletin < ImportScripts::Base
sql = <<-SQL
WITH closed_topic_ids AS (
SELECT t.id AS topic_id
- FROM #{TABLE_PREFIX}post_custom_fields pcf
- JOIN #{TABLE_PREFIX}posts p ON p.id = pcf.post_id
- JOIN #{TABLE_PREFIX}topics t ON t.id = p.topic_id
+ FROM post_custom_fields pcf
+ JOIN posts p ON p.id = pcf.post_id
+ JOIN topics t ON t.id = p.topic_id
WHERE pcf.name = 'import_id'
AND pcf.value IN (?)
)
UPDATE topics
SET closed = true
- WHERE id IN (SELECT topic_id FROM #{TABLE_PREFIX}closed_topic_ids)
+ WHERE id IN (SELECT topic_id FROM closed_topic_ids)
SQL
Topic.exec_sql(sql, @closed_topic_ids)
@@ -511,39 +554,39 @@ class ImportScripts::VBulletin < ImportScripts::Base
raw = @htmlentities.decode(raw)
# fix whitespaces
- raw = raw.gsub(/(\\r)?\\n/, "\n")
- .gsub("\\t", "\t")
+ raw.gsub!(/(\\r)?\\n/, "\n")
+ raw.gsub!("\\t", "\t")
# [HTML]...[/HTML]
- raw = raw.gsub(/\[html\]/i, "\n```html\n")
- .gsub(/\[\/html\]/i, "\n```\n")
+ raw.gsub!(/\[html\]/i, "\n```html\n")
+ raw.gsub!(/\[\/html\]/i, "\n```\n")
# [PHP]...[/PHP]
- raw = raw.gsub(/\[php\]/i, "\n```php\n")
- .gsub(/\[\/php\]/i, "\n```\n")
+ raw.gsub!(/\[php\]/i, "\n```php\n")
+ raw.gsub!(/\[\/php\]/i, "\n```\n")
# [HIGHLIGHT="..."]
- raw = raw.gsub(/\[highlight="?(\w+)"?\]/i) { "\n```#{$1.downcase}\n" }
+ raw.gsub!(/\[highlight="?(\w+)"?\]/i) { "\n```#{$1.downcase}\n" }
# [CODE]...[/CODE]
# [HIGHLIGHT]...[/HIGHLIGHT]
- raw = raw.gsub(/\[\/?code\]/i, "\n```\n")
- .gsub(/\[\/?highlight\]/i, "\n```\n")
+ raw.gsub!(/\[\/?code\]/i, "\n```\n")
+ raw.gsub!(/\[\/?highlight\]/i, "\n```\n")
# [SAMP]...[/SAMP]
- raw = raw.gsub(/\[\/?samp\]/i, "`")
+ raw.gsub!(/\[\/?samp\]/i, "`")
# replace all chevrons with HTML entities
# NOTE: must be done
# - AFTER all the "code" processing
# - BEFORE the "quote" processing
- raw = raw.gsub(/`([^`]+)`/im) { "`" + $1.gsub("<", "\u2603") + "`" }
- .gsub("<", "<")
- .gsub("\u2603", "<")
+ raw.gsub!(/`([^`]+)`/im) { "`" + $1.gsub("<", "\u2603") + "`" }
+ raw.gsub!("<", "<")
+ raw.gsub!("\u2603", "<")
- raw = raw.gsub(/`([^`]+)`/im) { "`" + $1.gsub(">", "\u2603") + "`" }
- .gsub(">", ">")
- .gsub("\u2603", ">")
+ raw.gsub!(/`([^`]+)`/im) { "`" + $1.gsub(">", "\u2603") + "`" }
+ raw.gsub!(">", ">")
+ raw.gsub!("\u2603", ">")
# [URL=...]...[/URL]
raw.gsub!(/\[url="?([^"]+?)"?\](.*?)\[\/url\]/im) { "[#{$2.strip}](#{$1})" }
@@ -551,11 +594,11 @@ class ImportScripts::VBulletin < ImportScripts::Base
# [URL]...[/URL]
# [MP3]...[/MP3]
- raw = raw.gsub(/\[\/?url\]/i, "")
- .gsub(/\[\/?mp3\]/i, "")
+ raw.gsub!(/\[\/?url\]/i, "")
+ raw.gsub!(/\[\/?mp3\]/i, "")
# [MENTION][/MENTION]
- raw = raw.gsub(/\[mention\](.+?)\[\/mention\]/i) do
+ raw.gsub!(/\[mention\](.+?)\[\/mention\]/i) do
old_username = $1
if @old_username_to_new_usernames.has_key?(old_username)
old_username = @old_username_to_new_usernames[old_username]
@@ -563,6 +606,24 @@ class ImportScripts::VBulletin < ImportScripts::Base
"@#{old_username}"
end
+ # [FONT=blah] and [COLOR=blah]
+ raw.gsub! /\[FONT=.*?\](.*?)\[\/FONT\]/im, '\1'
+ raw.gsub! /\[COLOR=.*?\](.*?)\[\/COLOR\]/im, '\1'
+ raw.gsub! /\[COLOR=#.*?\](.*?)\[\/COLOR\]/im, '\1'
+
+ raw.gsub! /\[SIZE=.*?\](.*?)\[\/SIZE\]/im, '\1'
+ raw.gsub! /\[h=.*?\](.*?)\[\/h\]/im, '\1'
+
+ # [CENTER]...[/CENTER]
+ raw.gsub! /\[CENTER\](.*?)\[\/CENTER\]/im, '\1'
+
+ # [INDENT]...[/INDENT]
+ raw.gsub! /\[INDENT\](.*?)\[\/INDENT\]/im, '\1'
+ raw.gsub! /\[TABLE\](.*?)\[\/TABLE\]/im, '\1'
+ raw.gsub! /\[TR\](.*?)\[\/TR\]/im, '\1'
+ raw.gsub! /\[TD\](.*?)\[\/TD\]/im, '\1'
+ raw.gsub! /\[TD="?.*?"?\](.*?)\[\/TD\]/im, '\1'
+
# [QUOTE]...[/QUOTE]
raw.gsub!(/\[quote\](.+?)\[\/quote\]/im) { |quote|
quote.gsub!(/\[quote\](.+?)\[\/quote\]/im) { "\n#{$1}\n" }
@@ -570,7 +631,7 @@ class ImportScripts::VBulletin < ImportScripts::Base
}
# [QUOTE=]...[/QUOTE]
- raw = raw.gsub(/\[quote=([^;\]]+)\](.+?)\[\/quote\]/im) do
+ raw.gsub!(/\[quote=([^;\]]+)\](.+?)\[\/quote\]/im) do
old_username, quote = $1, $2
if @old_username_to_new_usernames.has_key?(old_username)
old_username = @old_username_to_new_usernames[old_username]
@@ -579,10 +640,10 @@ class ImportScripts::VBulletin < ImportScripts::Base
end
# [YOUTUBE][/YOUTUBE]
- raw = raw.gsub(/\[youtube\](.+?)\[\/youtube\]/i) { "\n//youtu.be/#{$1}\n" }
+ raw.gsub!(/\[youtube\](.+?)\[\/youtube\]/i) { "\n//youtu.be/#{$1}\n" }
# [VIDEO=youtube;]...[/VIDEO]
- raw = raw.gsub(/\[video=youtube;([^\]]+)\].*?\[\/video\]/i) { "\n//youtu.be/#{$1}\n" }
+ raw.gsub!(/\[video=youtube;([^\]]+)\].*?\[\/video\]/i) { "\n//youtu.be/#{$1}\n" }
# More Additions ....
@@ -603,14 +664,14 @@ class ImportScripts::VBulletin < ImportScripts::Base
raw.gsub!(/\[\*\]\n/, '')
raw.gsub!(/\[\*\](.*?)\[\/\*:m\]/, '[li]\1[/li]')
raw.gsub!(/\[\*\](.*?)\n/, '[li]\1[/li]')
-
+ raw.gsub!(/\[\*=1\]/, '')
raw
end
def postprocess_post_raw(raw)
# [QUOTE=;]...[/QUOTE]
- raw = raw.gsub(/\[quote=([^;]+);(\d+)\](.+?)\[\/quote\]/im) do
+ raw.gsub!(/\[quote=([^;]+);(\d+)\](.+?)\[\/quote\]/im) do
old_username, post_id, quote = $1, $2, $3
if @old_username_to_new_usernames.has_key?(old_username)
@@ -627,11 +688,11 @@ class ImportScripts::VBulletin < ImportScripts::Base
end
# remove attachments
- raw = raw.gsub(/\[attach[^\]]*\]\d+\[\/attach\]/i, "")
+ raw.gsub!(/\[attach[^\]]*\]\d+\[\/attach\]/i, "")
# [THREAD][/THREAD]
# ==> http://my.discourse.org/t/slug/
- raw = raw.gsub(/\[thread\](\d+)\[\/thread\]/i) do
+ raw.gsub!(/\[thread\](\d+)\[\/thread\]/i) do
thread_id = $1
if topic_lookup = topic_lookup_from_imported_post_id("thread-#{thread_id}")
topic_lookup[:url]
@@ -642,7 +703,7 @@ class ImportScripts::VBulletin < ImportScripts::Base
# [THREAD=]...[/THREAD]
# ==> [...](http://my.discourse.org/t/slug/)
- raw = raw.gsub(/\[thread=(\d+)\](.+?)\[\/thread\]/i) do
+ raw.gsub!(/\[thread=(\d+)\](.+?)\[\/thread\]/i) do
thread_id, link = $1, $2
if topic_lookup = topic_lookup_from_imported_post_id("thread-#{thread_id}")
url = topic_lookup[:url]
@@ -654,7 +715,7 @@ class ImportScripts::VBulletin < ImportScripts::Base
# [POST][/POST]
# ==> http://my.discourse.org/t/slug//
- raw = raw.gsub(/\[post\](\d+)\[\/post\]/i) do
+ raw.gsub!(/\[post\](\d+)\[\/post\]/i) do
post_id = $1
if topic_lookup = topic_lookup_from_imported_post_id(post_id)
topic_lookup[:url]
@@ -665,7 +726,7 @@ class ImportScripts::VBulletin < ImportScripts::Base
# [POST=]...[/POST]
# ==> [...](http://my.discourse.org/t///)
- raw = raw.gsub(/\[post=(\d+)\](.+?)\[\/post\]/i) do
+ raw.gsub!(/\[post=(\d+)\](.+?)\[\/post\]/i) do
post_id, link = $1, $2
if topic_lookup = topic_lookup_from_imported_post_id(post_id)
url = topic_lookup[:url]
@@ -679,8 +740,8 @@ class ImportScripts::VBulletin < ImportScripts::Base
end
- def create_permalinks
- puts '', 'Creating Permalinks...', ''
+ def create_permalink_file
+ puts '', 'Creating Permalink File...', ''
id_mapping = []
@@ -719,7 +780,7 @@ class ImportScripts::VBulletin < ImportScripts::Base
system_user = Discourse.system_user
mysql_query("SELECT userid, bandate FROM #{TABLE_PREFIX}userban").each do |b|
- user = User.find_by_id(b['userid'])
+ user = User.find_by_id(user_id_from_imported_user_id(b['userid']))
if user
user.suspended_at = parse_timestamp(user["bandate"])
user.suspended_till = 200.years.from_now
diff --git a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb
index 05e6b68cd9..a9e61dbea1 100644
--- a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb
+++ b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb
@@ -48,8 +48,9 @@ describe ActiveRecord::ConnectionHandling do
end
it 'should failover to a replica server' do
+ current_threads = Thread.list
+
RailsMultisite::ConnectionManagement.stubs(:all_dbs).returns(['default', multisite_db])
- ::PostgreSQLFallbackHandler.instance.setup!
[config, multisite_config].each do |configuration|
ActiveRecord::Base.expects(:postgresql_connection).with(configuration).raises(PG::ConnectionBad)
@@ -60,7 +61,7 @@ describe ActiveRecord::ConnectionHandling do
})).returns(@replica_connection)
end
- expect(postgresql_fallback_handler.master).to eq(true)
+ expect(postgresql_fallback_handler.master_down?).to eq(nil)
expect { ActiveRecord::Base.postgresql_fallback_connection(config) }
.to raise_error(PG::ConnectionBad)
@@ -68,10 +69,10 @@ describe ActiveRecord::ConnectionHandling do
expect{ ActiveRecord::Base.postgresql_fallback_connection(config) }
.to change{ Discourse.readonly_mode? }.from(false).to(true)
- expect(postgresql_fallback_handler.master).to eq(false)
+ expect(postgresql_fallback_handler.master_down?).to eq(true)
with_multisite_db(multisite_db) do
- expect(postgresql_fallback_handler.master).to eq(true)
+ expect(postgresql_fallback_handler.master_down?).to eq(nil)
expect { ActiveRecord::Base.postgresql_fallback_connection(multisite_config) }
.to raise_error(PG::ConnectionBad)
@@ -79,30 +80,18 @@ describe ActiveRecord::ConnectionHandling do
expect{ ActiveRecord::Base.postgresql_fallback_connection(multisite_config) }
.to change{ Discourse.readonly_mode? }.from(false).to(true)
- expect(postgresql_fallback_handler.master).to eq(false)
+ expect(postgresql_fallback_handler.master_down?).to eq(true)
end
+ postgresql_fallback_handler.master_up(multisite_db)
+
ActiveRecord::Base.unstub(:postgresql_connection)
- current_threads = Thread.list
-
- expect{ ActiveRecord::Base.connection_pool.checkout }
- .to change{ Thread.list.size }.by(1)
-
- # Ensure that we don't try to connect back to the replica when a thread
- # is running
- begin
- ActiveRecord::Base.postgresql_fallback_connection(config)
- rescue PG::ConnectionBad => e
- # This is expected if the thread finishes before the above is called.
- end
-
- # Wait for the thread to finish execution
- (Thread.list - current_threads).each(&:join)
+ postgresql_fallback_handler.initiate_fallback_to_master
expect(Discourse.readonly_mode?).to eq(false)
- expect(PostgreSQLFallbackHandler.instance.master).to eq(true)
+ expect(postgresql_fallback_handler.master_down?).to eq(nil)
expect(ActiveRecord::Base.connection_pool.connections.count).to eq(0)
diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb
index 29ba74adfc..0a7b0a1bed 100644
--- a/spec/components/email/receiver_spec.rb
+++ b/spec/components/email/receiver_spec.rb
@@ -383,6 +383,39 @@ describe Email::Receiver do
expect(Post.last.raw).to match(/discourse\.rb/)
end
+ it "handles forwarded emails" do
+ SiteSetting.enable_forwarded_emails = true
+ expect { process(:forwarded_email_1) }.to change(Topic, :count)
+
+ forwarded_post, last_post = *Post.last(2)
+
+ expect(forwarded_post.user.email).to eq("some@one.com")
+ expect(last_post.user.email).to eq("ba@bar.com")
+
+ expect(forwarded_post.raw).to match(/XoXo/)
+ expect(last_post.raw).to match(/can you have a look at this email below/)
+
+ expect(last_post.post_type).to eq(Post.types[:regular])
+ end
+
+ it "handles weirdly forwarded emails" do
+ group.add(Fabricate(:user, email: "ba@bar.com"))
+ group.save
+
+ SiteSetting.enable_forwarded_emails = true
+ expect { process(:forwarded_email_2) }.to change(Topic, :count)
+
+ forwarded_post, last_post = *Post.last(2)
+
+ expect(forwarded_post.user.email).to eq("some@one.com")
+ expect(last_post.user.email).to eq("ba@bar.com")
+
+ expect(forwarded_post.raw).to match(/XoXo/)
+ expect(last_post.raw).to match(/can you have a look at this email below/)
+
+ expect(last_post.post_type).to eq(Post.types[:whisper])
+ end
+
end
context "new topic in a category" do
diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb
index c5e637f189..bf51aadfe4 100644
--- a/spec/components/guardian_spec.rb
+++ b/spec/components/guardian_spec.rb
@@ -2280,4 +2280,27 @@ describe Guardian do
end
end
end
+
+ context 'topic featured link category restriction' do
+ before { SiteSetting.topic_featured_link_enabled = true }
+ let(:guardian) { Guardian.new }
+
+ it 'returns true if no category restricts editing link' do
+ expect(guardian.can_edit_featured_link?(nil)).to eq(true)
+ expect(guardian.can_edit_featured_link?(5)).to eq(true)
+ end
+
+ context 'when exist' do
+ let!(:category) { Fabricate(:category) }
+ let!(:link_category) { Fabricate(:link_category) }
+
+ it 'returns true if the category is listed' do
+ expect(guardian.can_edit_featured_link?(link_category.id)).to eq(true)
+ end
+
+ it 'returns false if the category is not listed' do
+ expect(guardian.can_edit_featured_link?(category.id)).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/components/middleware/anonymous_cache_spec.rb b/spec/components/middleware/anonymous_cache_spec.rb
index 8dce0e5bcc..db0c481c4a 100644
--- a/spec/components/middleware/anonymous_cache_spec.rb
+++ b/spec/components/middleware/anonymous_cache_spec.rb
@@ -45,6 +45,16 @@ describe Middleware::AnonymousCache::Helper do
crawler.clear_cache
end
+ it "handles brotli switching" do
+ helper.cache([200, {"HELLO" => "WORLD"}, ["hello ", "my world"]])
+
+ helper = new_helper("ANON_CACHE_DURATION" => 10)
+ expect(helper.cached).to eq([200, {"X-Discourse-Cached" => "true", "HELLO" => "WORLD"}, ["hello my world"]])
+
+ helper = new_helper("ANON_CACHE_DURATION" => 10, "HTTP_ACCEPT_ENCODING" => "gz, br")
+ expect(helper.cached).to eq(nil)
+ end
+
it "returns cached data for cached requests" do
helper.is_mobile = true
expect(helper.cached).to eq(nil)
diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb
index ab1bd54866..197e6be9d4 100644
--- a/spec/components/post_creator_spec.rb
+++ b/spec/components/post_creator_spec.rb
@@ -20,6 +20,7 @@ describe PostCreator do
let(:creator_with_category) { PostCreator.new(user, basic_topic_params.merge(category: category.id )) }
let(:creator_with_meta_data) { PostCreator.new(user, basic_topic_params.merge(meta_data: {hello: "world"} )) }
let(:creator_with_image_sizes) { PostCreator.new(user, basic_topic_params.merge(image_sizes: image_sizes)) }
+ let(:creator_with_featured_link) { PostCreator.new(user, title: "featured link topic", archetype_id: 1, featured_link: "http://discourse.org") }
it "can create a topic with null byte central" do
post = PostCreator.create(user, title: "hello\u0000world this is title", raw: "this is my\u0000 first topic")
@@ -243,6 +244,14 @@ describe PostCreator do
end
end
+ it 'creates a post without raw' do
+ SiteSetting.topic_featured_link_enabled = true
+ SiteSetting.topic_featured_link_onebox = true
+ post = creator_with_featured_link.create
+ expect(post.topic.featured_link).to eq('http://discourse.org')
+ expect(post.raw).to eq('http://discourse.org')
+ end
+
describe "topic's auto close" do
it "doesn't update topic's auto close when it's not based on last post" do
@@ -334,7 +343,12 @@ describe PostCreator do
context 'whisper' do
let!(:topic) { Fabricate(:topic, user: user) }
- it 'forces replies to whispers to be whispers' do
+ it 'whispers do not mess up the public view' do
+
+ first = PostCreator.new(user,
+ topic_id: topic.id,
+ raw: 'this is the first post').create
+
whisper = PostCreator.new(user,
topic_id: topic.id,
reply_to_post_number: 1,
@@ -344,6 +358,7 @@ describe PostCreator do
expect(whisper).to be_present
expect(whisper.post_type).to eq(Post.types[:whisper])
+
whisper_reply = PostCreator.new(user,
topic_id: topic.id,
reply_to_post_number: whisper.post_number,
@@ -352,6 +367,29 @@ describe PostCreator do
expect(whisper_reply).to be_present
expect(whisper_reply.post_type).to eq(Post.types[:whisper])
+
+
+ first.reload
+ # does not leak into the OP
+ expect(first.reply_count).to eq(0)
+
+ topic.reload
+
+ # cause whispers should not muck up that number
+ expect(topic.highest_post_number).to eq(1)
+ expect(topic.reply_count).to eq(0)
+ expect(topic.posts_count).to eq(1)
+ expect(topic.highest_staff_post_number).to eq(3)
+
+ topic.update_columns(highest_staff_post_number:0, highest_post_number:0, posts_count: 0, last_posted_at: 1.year.ago)
+
+ Topic.reset_highest(topic.id)
+
+ topic.reload
+ expect(topic.highest_post_number).to eq(1)
+ expect(topic.posts_count).to eq(1)
+ expect(topic.last_posted_at).to eq(first.created_at)
+ expect(topic.highest_staff_post_number).to eq(3)
end
end
@@ -624,6 +662,8 @@ describe PostCreator do
_post2 = create_post(user: post1.user, topic_id: post1.topic_id)
post1.topic.reload
+
+ expect(post1.topic.posts_count).to eq(3)
expect(post1.topic.closed).to eq(true)
end
end
diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb
index 20c2046e0b..8d054157b1 100644
--- a/spec/components/search_spec.rb
+++ b/spec/components/search_spec.rb
@@ -532,6 +532,7 @@ describe Search do
expect(Search.execute('test status:closed').posts.length).to eq(0)
expect(Search.execute('test status:open').posts.length).to eq(1)
expect(Search.execute('test posts_count:1').posts.length).to eq(1)
+ expect(Search.execute('test min_post_count:1').posts.length).to eq(1)
topic.closed = true
topic.save
diff --git a/spec/components/topic_query_spec.rb b/spec/components/topic_query_spec.rb
index c50319ff27..619a080aae 100644
--- a/spec/components/topic_query_spec.rb
+++ b/spec/components/topic_query_spec.rb
@@ -409,6 +409,29 @@ describe TopicQuery do
end
end
+ context 'with whispers' do
+
+ it 'correctly shows up in unread for staff' do
+
+ first = create_post(raw: 'this is the first post', title: 'super amazing title')
+
+ _whisper = create_post(topic_id: first.topic.id,
+ post_type: Post.types[:whisper],
+ raw: 'this is a whispered reply')
+
+ topic_id = first.topic.id
+
+ TopicUser.update_last_read(user, topic_id, first.post_number, 1)
+ TopicUser.update_last_read(admin, topic_id, first.post_number, 1)
+
+ TopicUser.change(user.id, topic_id, notification_level: TopicUser.notification_levels[:tracking])
+ TopicUser.change(admin.id, topic_id, notification_level: TopicUser.notification_levels[:tracking])
+
+ expect(TopicQuery.new(user).list_unread.topics).to eq([])
+ expect(TopicQuery.new(admin).list_unread.topics).to eq([first.topic])
+ end
+ end
+
context 'with read data' do
let!(:partially_read) { Fabricate(:post, user: creator).topic }
let!(:fully_read) { Fabricate(:post, user: creator).topic }
@@ -419,8 +442,9 @@ describe TopicQuery do
end
context 'list_unread' do
- it 'contains no topics' do
+ it 'lists topics correctly' do
expect(topic_query.list_unread.topics).to eq([])
+ expect(topic_query.list_read.topics).to match_array([fully_read, partially_read])
end
end
@@ -435,11 +459,6 @@ describe TopicQuery do
end
end
- context 'list_read' do
- it 'contain both topics ' do
- expect(topic_query.list_read.topics).to match_array([fully_read, partially_read])
- end
- end
end
end
@@ -630,7 +649,6 @@ describe TopicQuery do
related_by_group_pm = create_pm(sender, target_group_names: [group_with_user.name])
read(user, related_by_group_pm, 1)
-
expect(TopicQuery.new(user).list_suggested_for(pm_to_group).topics.map(&:id)).to(
eq([related_by_group_pm.id, related_by_user_pm.id, pm_to_user.id])
)
diff --git a/spec/components/unread_spec.rb b/spec/components/unread_spec.rb
index cfbf457053..21e4b28b59 100644
--- a/spec/components/unread_spec.rb
+++ b/spec/components/unread_spec.rb
@@ -3,62 +3,86 @@ require 'unread'
describe Unread do
-
- before do
- @topic = Fabricate(:topic, posts_count: 13, highest_post_number: 13)
- @topic.notifier.watch_topic!(@topic.user_id)
- @topic_user = TopicUser.get(@topic, @topic.user)
- @topic_user.stubs(:notification_level).returns(TopicUser.notification_levels[:tracking])
- @topic_user.notification_level = TopicUser.notification_levels[:tracking]
- @unread = Unread.new(@topic, @topic_user)
+ let (:user) { Fabricate.build(:user, id: 1) }
+ let (:topic) do
+ Fabricate.build(:topic,
+ posts_count: 13,
+ highest_staff_post_number: 15,
+ highest_post_number: 13,
+ id: 1)
end
+ let (:topic_user) do
+ Fabricate.build(:topic_user,
+ notification_level: TopicUser.notification_levels[:tracking],
+ topic_id: topic.id,
+ user_id: user.id)
+ end
+
+ def unread
+ Unread.new(topic, topic_user, Guardian.new(user))
+ end
+
+ describe 'staff counts' do
+ it 'shoule correctly return based on staff post number' do
+
+ user.admin = true
+
+ topic_user.last_read_post_number = 13
+ topic_user.highest_seen_post_number = 13
+
+ expect(unread.unread_posts).to eq(0)
+ expect(unread.new_posts).to eq(2)
+ end
+ end
+
+
describe 'unread_posts' do
it 'should have 0 unread posts if the user has seen all posts' do
- @topic_user.stubs(:last_read_post_number).returns(13)
- @topic_user.stubs(:highest_seen_post_number).returns(13)
- expect(@unread.unread_posts).to eq(0)
+ topic_user.last_read_post_number = 13
+ topic_user.highest_seen_post_number = 13
+ expect(unread.unread_posts).to eq(0)
end
it 'should have 6 unread posts if the user has seen all but 6 posts' do
- @topic_user.stubs(:last_read_post_number).returns(5)
- @topic_user.stubs(:highest_seen_post_number).returns(11)
- expect(@unread.unread_posts).to eq(6)
+ topic_user.last_read_post_number = 5
+ topic_user.highest_seen_post_number = 11
+ expect(unread.unread_posts).to eq(6)
end
it 'should have 0 unread posts if the user has seen more posts than exist (deleted)' do
- @topic_user.stubs(:last_read_post_number).returns(100)
- @topic_user.stubs(:highest_seen_post_number).returns(13)
- expect(@unread.unread_posts).to eq(0)
+ topic_user.last_read_post_number = 100
+ topic_user.highest_seen_post_number = 13
+ expect(unread.unread_posts).to eq(0)
end
end
describe 'new_posts' do
it 'should have 0 new posts if the user has read all posts' do
- @topic_user.stubs(:last_read_post_number).returns(13)
- expect(@unread.new_posts).to eq(0)
+ topic_user.last_read_post_number = 13
+ expect(unread.new_posts).to eq(0)
end
it 'returns 0 when the topic is the same length as when you last saw it' do
- @topic_user.stubs(:highest_seen_post_number).returns(13)
- expect(@unread.new_posts).to eq(0)
+ topic_user.highest_seen_post_number = 13
+ expect(unread.new_posts).to eq(0)
end
it 'has 3 new posts if the user has read 10 posts' do
- @topic_user.stubs(:highest_seen_post_number).returns(10)
- expect(@unread.new_posts).to eq(3)
+ topic_user.highest_seen_post_number = 10
+ expect(unread.new_posts).to eq(3)
end
it 'has 0 new posts if the user has read 10 posts but is not tracking' do
- @topic_user.stubs(:highest_seen_post_number).returns(10)
- @topic_user.stubs(:notification_level).returns(TopicUser.notification_levels[:regular])
- expect(@unread.new_posts).to eq(0)
+ topic_user.highest_seen_post_number = 10
+ topic_user.notification_level = TopicUser.notification_levels[:regular]
+ expect(unread.new_posts).to eq(0)
end
it 'has 0 new posts if the user read more posts than exist (deleted)' do
- @topic_user.stubs(:highest_seen_post_number).returns(16)
- expect(@unread.new_posts).to eq(0)
+ topic_user.highest_seen_post_number = 16
+ expect(unread.new_posts).to eq(0)
end
-
end
+
end
diff --git a/spec/components/validators/post_validator_spec.rb b/spec/components/validators/post_validator_spec.rb
index b497d4454c..093cff9ff5 100644
--- a/spec/components/validators/post_validator_spec.rb
+++ b/spec/components/validators/post_validator_spec.rb
@@ -5,6 +5,16 @@ describe Validators::PostValidator do
let(:post) { build(:post) }
let(:validator) { Validators::PostValidator.new({}) }
+ context "when empty raw can bypass post body validation" do
+ let(:validator) { Validators::PostValidator.new(skip_post_body: true) }
+
+ it "should be allowed for empty raw based on site setting" do
+ post.raw = ""
+ validator.post_body_validator(post)
+ expect(post.errors).to be_empty
+ end
+ end
+
context "stripped_length" do
it "adds an error for short raw" do
post.raw = "abc"
diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb
index 6971184dd4..53cb83436a 100644
--- a/spec/controllers/admin/groups_controller_spec.rb
+++ b/spec/controllers/admin/groups_controller_spec.rb
@@ -37,7 +37,9 @@ describe Admin::GroupsController do
"has_messages"=>false,
"flair_url"=>nil,
"flair_bg_color"=>nil,
- "flair_color"=>nil
+ "flair_color"=>nil,
+ "bio_raw"=>nil,
+ "bio_cooked"=>nil
}])
end
@@ -66,7 +68,7 @@ describe Admin::GroupsController do
context ".create" do
it "strip spaces on the group name" do
- xhr :post, :create, name: " bob "
+ xhr :post, :create, { group: { name: " bob " } }
expect(response.status).to eq(200)
@@ -81,7 +83,7 @@ describe Admin::GroupsController do
context ".update" do
it "ignore name change on automatic group" do
- xhr :put, :update, id: 1, name: "WAT", visible: "true"
+ xhr :put, :update, { id: 1, group: { name: "WAT", visible: "true" } }
expect(response).to be_success
group = Group.find(1)
@@ -92,14 +94,14 @@ describe Admin::GroupsController do
it "doesn't launch the 'automatic group membership' job when it's not retroactive" do
Jobs.expects(:enqueue).never
group = Fabricate(:group)
- xhr :put, :update, id: group.id, automatic_membership_retroactive: "false"
+ xhr :put, :update, { id: group.id, group: { automatic_membership_retroactive: "false" } }
expect(response).to be_success
end
it "launches the 'automatic group membership' job when it's retroactive" do
group = Fabricate(:group)
Jobs.expects(:enqueue).with(:automatic_group_membership, group_id: group.id)
- xhr :put, :update, id: group.id, automatic_membership_retroactive: "true"
+ xhr :put, :update, { id: group.id, group: { automatic_membership_retroactive: "true" } }
expect(response).to be_success
end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index b926e4d6c6..5cd791ba18 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -25,19 +25,6 @@ describe GroupsController do
end
end
- describe "counts" do
- it "returns counts if it can be seen" do
- xhr :get, :counts, group_id: group.name
- expect(response).to be_success
- end
-
- it "returns no counts if it can not be seen" do
- group.update_columns(visible: false)
- xhr :get, :counts, group_id: group.name
- expect(response).not_to be_success
- end
- end
-
describe "posts" do
it "ensures the group can be seen" do
Guardian.any_instance.expects(:can_see?).with(group).returns(false)
diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb
index 745c781f9c..be07bb7894 100644
--- a/spec/controllers/invites_controller_spec.rb
+++ b/spec/controllers/invites_controller_spec.rb
@@ -367,33 +367,10 @@ describe InvitesController do
end
- context '.check_csv_chunk' do
+ context '.upload_csv' do
it 'requires you to be logged in' do
expect {
- post :check_csv_chunk
- }.to raise_error(Discourse::NotLoggedIn)
- end
-
- context 'while logged in' do
- let(:resumableChunkNumber) { 1 }
- let(:resumableCurrentChunkSize) { 46 }
- let(:resumableIdentifier) { '46-discoursecsv' }
- let(:resumableFilename) { 'discourse.csv' }
-
- it "fails if you can't bulk invite to the forum" do
- log_in
- post :check_csv_chunk, resumableChunkNumber: resumableChunkNumber, resumableCurrentChunkSize: resumableCurrentChunkSize.to_i, resumableIdentifier: resumableIdentifier, resumableFilename: resumableFilename
- expect(response).not_to be_success
- end
-
- end
-
- end
-
- context '.upload_csv_chunk' do
- it 'requires you to be logged in' do
- expect {
- post :upload_csv_chunk
+ xhr :post, :upload_csv
}.to raise_error(Discourse::NotLoggedIn)
end
@@ -402,27 +379,19 @@ describe InvitesController do
let(:file) do
ActionDispatch::Http::UploadedFile.new({ filename: 'discourse.csv', tempfile: csv_file })
end
- let(:resumableChunkNumber) { 1 }
- let(:resumableChunkSize) { 1048576 }
- let(:resumableCurrentChunkSize) { 46 }
- let(:resumableTotalSize) { 46 }
- let(:resumableType) { 'text/csv' }
- let(:resumableIdentifier) { '46-discoursecsv' }
- let(:resumableFilename) { 'discourse.csv' }
- let(:resumableRelativePath) { 'discourse.csv' }
+ let(:filename) { 'discourse.csv' }
it "fails if you can't bulk invite to the forum" do
log_in
- post :upload_csv_chunk, file: file, resumableChunkNumber: resumableChunkNumber.to_i, resumableChunkSize: resumableChunkSize.to_i, resumableCurrentChunkSize: resumableCurrentChunkSize.to_i, resumableTotalSize: resumableTotalSize.to_i, resumableType: resumableType, resumableIdentifier: resumableIdentifier, resumableFilename: resumableFilename
+ xhr :post, :upload_csv, file: file, name: filename
expect(response).not_to be_success
end
- it "allows admins to bulk invite" do
+ it "allows admin to bulk invite" do
log_in(:admin)
- post :upload_csv_chunk, file: file, resumableChunkNumber: resumableChunkNumber.to_i, resumableChunkSize: resumableChunkSize.to_i, resumableCurrentChunkSize: resumableCurrentChunkSize.to_i, resumableTotalSize: resumableTotalSize.to_i, resumableType: resumableType, resumableIdentifier: resumableIdentifier, resumableFilename: resumableFilename
+ xhr :post, :upload_csv, file: file, name: filename
expect(response).to be_success
end
-
end
end
diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb
index 99af04c81a..9f29a45639 100644
--- a/spec/controllers/posts_controller_spec.rb
+++ b/spec/controllers/posts_controller_spec.rb
@@ -579,10 +579,6 @@ describe PostsController do
let(:moderator) { log_in(:moderator) }
let(:new_post) { Fabricate.build(:post, user: user) }
- it "raises an exception without a raw parameter" do
- expect { xhr :post, :create }.to raise_error(ActionController::ParameterMissing)
- end
-
context "fast typing" do
before do
SiteSetting.min_first_post_typing_time = 3000
@@ -771,8 +767,8 @@ describe PostsController do
end
it "passes category through" do
- xhr :post, :create, {raw: 'hello', category: 'cool'}
- expect(assigns(:manager_params)['category']).to eq('cool')
+ xhr :post, :create, {raw: 'hello', category: 1}
+ expect(assigns(:manager_params)['category']).to eq('1')
end
it "passes target_usernames through" do
diff --git a/spec/controllers/static_controller_spec.rb b/spec/controllers/static_controller_spec.rb
index b5b8eb0b74..f8ba662984 100644
--- a/spec/controllers/static_controller_spec.rb
+++ b/spec/controllers/static_controller_spec.rb
@@ -2,6 +2,26 @@ require 'rails_helper'
describe StaticController do
+ context 'brotli_asset' do
+ it 'has correct headers for brotli assets' do
+ begin
+ assets_path = Rails.root.join("public/assets")
+
+ FileUtils.mkdir_p(assets_path)
+
+ file_path = assets_path.join("test.js.br")
+ File.write(file_path, 'fake brotli file')
+
+ get :brotli_asset, path: 'test.js'
+
+ expect(response.status).to eq(200)
+ expect(response.headers["Cache-Control"]).to match(/public/)
+ ensure
+ File.delete(file_path)
+ end
+ end
+ end
+
context 'show' do
before do
post = create_post
diff --git a/spec/controllers/tags_controller_spec.rb b/spec/controllers/tags_controller_spec.rb
index 7c2e7aff06..75696f18cc 100644
--- a/spec/controllers/tags_controller_spec.rb
+++ b/spec/controllers/tags_controller_spec.rb
@@ -81,6 +81,12 @@ describe TagsController do
expect(response).to be_success
expect(assigns(:list).topics).to include(t)
end
+
+ it "can filter by bookmarked" do
+ log_in(:user)
+ xhr :get, :show_bookmarks, tag_id: tag.name
+ expect(response).to be_success
+ end
end
end
diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb
index 05a7b50477..452470abda 100644
--- a/spec/controllers/topics_controller_spec.rb
+++ b/spec/controllers/topics_controller_spec.rb
@@ -1224,6 +1224,12 @@ describe TopicsController do
expect { xhr :put, :bulk, topic_ids: topic_ids, operation: {}}.to raise_error(ActionController::ParameterMissing)
end
+ it "can find unread" do
+ # mark all unread muted
+ xhr :put, :bulk, filter: 'unread', operation: {type: :change_notification_level, notification_level_id: 0}
+ expect(response.status).to eq(200)
+ end
+
it "delegates work to `TopicsBulkAction`" do
topics_bulk_action = mock
TopicsBulkAction.expects(:new).with(user, topic_ids, operation, group: nil).returns(topics_bulk_action)
diff --git a/spec/controllers/user_actions_controller_spec.rb b/spec/controllers/user_actions_controller_spec.rb
index a161c2f90d..52583c7f86 100644
--- a/spec/controllers/user_actions_controller_spec.rb
+++ b/spec/controllers/user_actions_controller_spec.rb
@@ -24,6 +24,28 @@ describe UserActionsController do
expect(action["post_number"]).to eq(1)
end
+ it 'renders help text if provided for self' do
+ logged_in = log_in
+
+ xhr :get, :index, filter: UserAction::LIKE, username: logged_in.username, no_results_help_key: "user_activity.no_bookmarks"
+
+ expect(response.status).to eq(200)
+ parsed = JSON.parse(response.body)
+
+ expect(parsed["no_results_help"]).to eq(I18n.t("user_activity.no_bookmarks.self"))
+
+ end
+
+ it 'renders help text for others' do
+ user = Fabricate(:user)
+ xhr :get, :index, filter: UserAction::LIKE, username: user.username, no_results_help_key: "user_activity.no_bookmarks"
+
+ expect(response.status).to eq(200)
+ parsed = JSON.parse(response.body)
+
+ expect(parsed["no_results_help"]).to eq(I18n.t("user_activity.no_bookmarks.others"))
+ end
+
context "queued posts" do
context "without access" do
let(:user) { Fabricate(:user) }
diff --git a/spec/fabricators/embeddable_host_fabricator.rb b/spec/fabricators/embeddable_host_fabricator.rb
index a37f5b4c8b..9f589d389e 100644
--- a/spec/fabricators/embeddable_host_fabricator.rb
+++ b/spec/fabricators/embeddable_host_fabricator.rb
@@ -25,3 +25,7 @@ Fabricator(:private_category, from: :category) do
cat.category_groups.build(group_id: transients[:group].id, permission_type: CategoryGroup.permission_types[:full])
end
end
+
+Fabricator(:link_category, from: :category) do
+ before_validation { |category, transients| category.custom_fields['topic_featured_link_allowed'] = 'true' }
+end
diff --git a/spec/fabricators/topic_user_fabricator.rb b/spec/fabricators/topic_user_fabricator.rb
new file mode 100644
index 0000000000..b299806f70
--- /dev/null
+++ b/spec/fabricators/topic_user_fabricator.rb
@@ -0,0 +1,4 @@
+Fabricator(:topic_user) do
+ user
+ topic
+end
diff --git a/spec/fixtures/emails/forwarded_email_1.eml b/spec/fixtures/emails/forwarded_email_1.eml
new file mode 100644
index 0000000000..30fb904190
--- /dev/null
+++ b/spec/fixtures/emails/forwarded_email_1.eml
@@ -0,0 +1,18 @@
+Message-ID: <58@foo.bar.mail>
+From: Ba Bar
+To: Team
+Date: Mon, 1 Dec 2016 13:37:42 +0100
+Subject: FW: Discoursing much?
+
+@team, can you have a look at this email below?
+
+From: Some One
+To: Ba Bar
+Date: Mon, 1 Dec 2016 00:13:37 +0100
+Subject: Discoursing much?
+
+Hello Ba Bar,
+
+Discoursing much today?
+
+XoXo
diff --git a/spec/fixtures/emails/forwarded_email_2.eml b/spec/fixtures/emails/forwarded_email_2.eml
new file mode 100644
index 0000000000..d31d5c44ea
--- /dev/null
+++ b/spec/fixtures/emails/forwarded_email_2.eml
@@ -0,0 +1,18 @@
+Message-ID: <59@foo.bar.mail>
+From: Ba Bar
+To: Team
+Date: Mon, 1 Dec 2016 13:37:42 +0100
+Subject: Re: Discoursing much?
+
+@team, can you have a look at this email below?
+
+From: Some One [mailto:some@one.com]
+To: Ba Bar
+Date: Mon, 1 Dec 2016 00:13:37 +0100
+Subject: Discoursing much?
+
+Hello Ba Bar,
+
+Discoursing much today?
+
+XoXo
diff --git a/spec/integration/groups_spec.rb b/spec/integration/groups_spec.rb
index 396d7df9f4..c063e7d78d 100644
--- a/spec/integration/groups_spec.rb
+++ b/spec/integration/groups_spec.rb
@@ -1,22 +1,22 @@
require 'rails_helper'
describe "Groups" do
- describe "checking if a group can be mentioned" do
- let(:password) { 'somecomplicatedpassword' }
- let(:email_token) { Fabricate(:email_token, confirmed: true) }
- let(:user) { email_token.user }
- let(:group) { Fabricate(:group, name: 'test', users: [user]) }
+ let(:password) { 'somecomplicatedpassword' }
+ let(:email_token) { Fabricate(:email_token, confirmed: true) }
+ let(:user) { email_token.user }
- before do
- user.update_attributes!(password: password)
- end
+ before do
+ user.update_attributes!(password: password)
+ post "/session.json", { login: user.username, password: password }
+ expect(response).to be_success
+ end
+
+ describe "checking if a group can be mentioned" do
+ let(:group) { Fabricate(:group, name: 'test', users: [user]) }
it "should return the right response" do
group
- post "/session.json", { login: user.username, password: password }
- expect(response).to be_success
-
get "/groups/test/mentionable.json", { name: group.name }
expect(response).to be_success
@@ -33,4 +33,55 @@ describe "Groups" do
expect(response_body["mentionable"]).to eq(true)
end
end
+
+ describe "group can be updated" do
+ let(:group) { Fabricate(:group, name: 'test', users: [user]) }
+
+ context "when user is group owner" do
+ before do
+ group.add_owner(user)
+ end
+
+ it "should be able update the group" do
+ xhr :put, "/groups/#{group.id}", { group: {
+ flair_bg_color: 'FFF',
+ flair_color: 'BBB',
+ flair_url: 'fa-adjust',
+ bio_raw: 'testing',
+ title: 'awesome team'
+ } }
+
+ expect(response).to be_success
+
+ group.reload
+
+ expect(group.flair_bg_color).to eq('FFF')
+ expect(group.flair_color).to eq('BBB')
+ expect(group.flair_url).to eq('fa-adjust')
+ expect(group.bio_raw).to eq('testing')
+ expect(group.title).to eq('awesome team')
+ end
+ end
+
+ context "when user is group admin" do
+ before do
+ user.update_attributes!(admin: true)
+ end
+
+ it 'should be able to update the group' do
+ xhr :put, "/groups/#{group.id}", { group: { flair_color: 'BBB' } }
+
+ expect(response).to be_success
+ expect(group.reload.flair_color).to eq('BBB')
+ end
+ end
+
+ context "when user is not a group owner or admin" do
+ it 'should not be able to update the group' do
+ xhr :put, "/groups/#{group.id}", { group: { name: 'testing' } }
+
+ expect(response.status).to eq(403)
+ end
+ end
+ end
end
diff --git a/spec/jobs/bulk_invite_spec.rb b/spec/jobs/bulk_invite_spec.rb
index 77629a6a82..0739a41574 100644
--- a/spec/jobs/bulk_invite_spec.rb
+++ b/spec/jobs/bulk_invite_spec.rb
@@ -5,15 +5,8 @@ describe Jobs::BulkInvite do
context '.execute' do
it 'raises an error when the filename is missing' do
- expect { Jobs::BulkInvite.new.execute(identifier: '46-discoursecsv', chunks: '1') }.to raise_error(Discourse::InvalidParameters)
- end
-
- it 'raises an error when the identifier is missing' do
- expect { Jobs::BulkInvite.new.execute(filename: 'discourse.csv', chunks: '1') }.to raise_error(Discourse::InvalidParameters)
- end
-
- it 'raises an error when the chunks is missing' do
- expect { Jobs::BulkInvite.new.execute(filename: 'discourse.csv', identifier: '46-discoursecsv') }.to raise_error(Discourse::InvalidParameters)
+ user = Fabricate(:user)
+ expect { Jobs::BulkInvite.new.execute(current_user_id: user.id) }.to raise_error(Discourse::InvalidParameters)
end
context '.read_csv_file' do
diff --git a/spec/mailers/user_notifications_spec.rb b/spec/mailers/user_notifications_spec.rb
index 51f73a58ae..a427b6d3b0 100644
--- a/spec/mailers/user_notifications_spec.rb
+++ b/spec/mailers/user_notifications_spec.rb
@@ -155,8 +155,7 @@ describe UserNotifications do
context "with new topics" do
before do
- Topic.stubs(:for_digest).returns([Fabricate(:topic, user: Fabricate(:coding_horror))])
- Topic.stubs(:new_since_last_seen).returns(Topic.none)
+ Fabricate(:topic, user: Fabricate(:coding_horror))
end
it "works" do
@@ -184,6 +183,28 @@ describe UserNotifications do
expect(html).to_not include deleted.title
expect(html).to_not include post.raw
end
+
+ it "excludes whispers and other post types that don't belong" do
+ t = Fabricate(:topic, user: Fabricate(:user), title: "Who likes the same stuff I like?")
+ whisper = Fabricate(:post, topic: t, score: 100.0, post_number: 2, raw: "You like weird stuff", post_type: Post.types[:whisper])
+ mod_action = Fabricate(:post, topic: t, score: 100.0, post_number: 3, raw: "This topic unlisted", post_type: Post.types[:moderator_action])
+ small_action = Fabricate(:post, topic: t, score: 100.0, post_number: 4, raw: "A small action", post_type: Post.types[:small_action])
+ html = subject.html_part.body.to_s
+ expect(html).to_not include whisper.raw
+ expect(html).to_not include mod_action.raw
+ expect(html).to_not include small_action.raw
+ end
+
+ it "excludes deleted and hidden posts" do
+ t = Fabricate(:topic, user: Fabricate(:user), title: "Post objectionable stuff here")
+ deleted = Fabricate(:post, topic: t, score: 100.0, post_number: 2, raw: "This post is uncalled for", deleted_at: 5.minutes.ago)
+ hidden = Fabricate(:post, topic: t, score: 100.0, post_number: 3, raw: "Try to find this post", hidden: true, hidden_at: 5.minutes.ago, hidden_reason_id: Post.hidden_reasons[:flagged_by_tl3_user])
+ user_deleted = Fabricate(:post, topic: t, score: 100.0, post_number: 4, raw: "I regret this post", user_deleted: true)
+ html = subject.html_part.body.to_s
+ expect(html).to_not include deleted.raw
+ expect(html).to_not include hidden.raw
+ expect(html).to_not include user_deleted.raw
+ end
end
end
diff --git a/spec/models/category_spec.rb b/spec/models/category_spec.rb
index d603077ebf..efe28cb6be 100644
--- a/spec/models/category_spec.rb
+++ b/spec/models/category_spec.rb
@@ -421,14 +421,14 @@ describe Category do
describe 'latest' do
it 'should be updated correctly' do
category = Fabricate(:category)
- post = create_post(category: category.name)
+ post = create_post(category: category.id)
category.reload
expect(category.latest_post_id).to eq(post.id)
expect(category.latest_topic_id).to eq(post.topic_id)
- post2 = create_post(category: category.name)
- post3 = create_post(topic_id: post.topic_id, category: category.name)
+ post2 = create_post(category: category.id)
+ post3 = create_post(topic_id: post.topic_id, category: category.id)
category.reload
expect(category.latest_post_id).to eq(post3.id)
@@ -451,7 +451,7 @@ describe Category do
context 'with regular topics' do
before do
- create_post(user: @category.user, category: @category.name)
+ create_post(user: @category.user, category: @category.id)
Category.update_stats
@category.reload
end
@@ -491,7 +491,7 @@ describe Category do
context 'with revised post' do
before do
- post = create_post(user: @category.user, category: @category.name)
+ post = create_post(user: @category.user, category: @category.id)
SiteSetting.stubs(:editing_grace_period).returns(1.minute.to_i)
post.revise(post.user, { raw: 'updated body' }, revised_at: post.updated_at + 2.minutes)
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index dfc54ba1a8..8a6477ed73 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -322,7 +322,6 @@ describe Group do
expect(Group.desired_trust_level_groups(2).sort).to eq [10,11,12]
end
-
it "correctly handles trust level changes" do
user = Fabricate(:user, trust_level: 2)
Group.user_trust_level_change!(user.id, 2)
@@ -369,4 +368,11 @@ describe Group do
expect(u3.reload.trust_level).to eq(3)
end
+ it 'should cook the bio' do
+ group = Fabricate(:group)
+ group.update_attributes!(bio_raw: 'This is a group for :unicorn: lovers')
+
+ expect(group.bio_cooked).to include("unicorn.png")
+ end
+
end
diff --git a/spec/models/post_action_spec.rb b/spec/models/post_action_spec.rb
index 9321dedd81..fb621269df 100644
--- a/spec/models/post_action_spec.rb
+++ b/spec/models/post_action_spec.rb
@@ -199,10 +199,6 @@ describe PostAction do
expect { bookmark.save; post.reload }.to change(post, :bookmark_count).by(1)
end
- it "increases the forum topic's bookmark count when saved" do
- expect { bookmark.save; post.topic.reload }.to change(post.topic, :bookmark_count).by(1)
- end
-
describe 'when deleted' do
before do
@@ -218,9 +214,6 @@ describe PostAction do
expect { post.reload }.to change(post, :bookmark_count).by(-1)
end
- it 'reduces the bookmark count of the forum topic' do
- expect { @topic.reload }.to change(post.topic, :bookmark_count).by(-1)
- end
end
end
@@ -291,19 +284,24 @@ describe PostAction do
end
end
- describe 'when a user votes for something' do
- it 'should increase the vote counts when a user votes' do
+ describe 'when a user likes something' do
+ it 'should increase the like counts when a user votes' do
expect {
- PostAction.act(codinghorror, post, PostActionType.types[:vote])
+ PostAction.act(codinghorror, post, PostActionType.types[:like])
post.reload
- }.to change(post, :vote_count).by(1)
+ }.to change(post, :like_count).by(1)
end
it 'should increase the forum topic vote count when a user votes' do
expect {
- PostAction.act(codinghorror, post, PostActionType.types[:vote])
+ PostAction.act(codinghorror, post, PostActionType.types[:like])
post.topic.reload
- }.to change(post.topic, :vote_count).by(1)
+ }.to change(post.topic, :like_count).by(1)
+
+ expect {
+ PostAction.remove_act(codinghorror, post, PostActionType.types[:like])
+ post.topic.reload
+ }.to change(post.topic, :like_count).by(-1)
end
end
diff --git a/spec/models/post_analyzer_spec.rb b/spec/models/post_analyzer_spec.rb
index 91cefd382b..ecda5cc6a0 100644
--- a/spec/models/post_analyzer_spec.rb
+++ b/spec/models/post_analyzer_spec.rb
@@ -38,8 +38,9 @@ describe PostAnalyzer do
context "links" do
let(:raw_no_links) { "hello world my name is evil trout" }
let(:raw_one_link_md) { "[jlawr](http://www.imdb.com/name/nm2225369)" }
- let(:raw_two_links_html) { "disney reddit"}
- let(:raw_three_links) { "http://discourse.org and http://discourse.org/another_url and http://www.imdb.com/name/nm2225369"}
+ let(:raw_two_links_html) { "disney reddit" }
+ let(:raw_three_links) { "http://discourse.org and http://discourse.org/another_url and http://www.imdb.com/name/nm2225369" }
+ let(:raw_elided) { "\n···\nhttp://discourse.org\n " }
describe "raw_links" do
it "returns a blank collection for a post with no links" do
@@ -61,6 +62,12 @@ describe PostAnalyzer do
post_analyzer = PostAnalyzer.new(raw_three_links, default_topic_id)
expect(post_analyzer.raw_links).to eq(["http://discourse.org", "http://discourse.org/another_url", "http://www.imdb.com/name/nm2225369"])
end
+
+ it "doesn't extract links from elided part" do
+ post_analyzer = PostAnalyzer.new(raw_elided, default_topic_id)
+ post_analyzer.expects(:cook).returns("\n···\ndiscourse.org\n ")
+ expect(post_analyzer.raw_links).to be_blank
+ end
end
describe "linked_hosts" do
diff --git a/spec/models/quoted_post_spec.rb b/spec/models/quoted_post_spec.rb
index 77bb741339..4c61952c69 100644
--- a/spec/models/quoted_post_spec.rb
+++ b/spec/models/quoted_post_spec.rb
@@ -15,9 +15,15 @@ describe QuotedPost do
post1 = Fabricate(:post)
post2 = Fabricate(:post)
- post2.cooked = < techAPJ: When the user will v
-HTML
+ post2.cooked = <<-HTML
+
+ HTML
QuotedPost.create!(post_id: post2.id, quoted_post_id: 999)
diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb
index 6b84eff2e6..0f35c71448 100644
--- a/spec/models/topic_spec.rb
+++ b/spec/models/topic_spec.rb
@@ -1724,4 +1724,55 @@ describe Topic do
expect(@topic_status_event_triggered).to eq(true)
end
+
+ it 'allows users to normalize counts' do
+
+ topic = Fabricate(:topic, last_posted_at: 1.year.ago)
+ post1 = Fabricate(:post, topic: topic, post_number: 1)
+ post2 = Fabricate(:post, topic: topic, post_type: Post.types[:whisper], post_number: 2)
+
+ Topic.reset_all_highest!
+ topic.reload
+
+ expect(topic.posts_count).to eq(1)
+ expect(topic.highest_post_number).to eq(post1.post_number)
+ expect(topic.highest_staff_post_number).to eq(post2.post_number)
+ expect(topic.last_posted_at).to be_within(1.second).of (post1.created_at)
+ end
+
+ context 'featured link' do
+ before { SiteSetting.topic_featured_link_enabled = true }
+ let(:topic) { Fabricate(:topic) }
+
+ it 'can validate featured link' do
+ topic.featured_link = ' invalid string'
+
+ expect(topic).not_to be_valid
+ expect(topic.errors[:featured_link]).to be_present
+ end
+
+ it 'can properly save the featured link' do
+ topic.featured_link = ' https://github.com/discourse/discourse'
+
+ expect(topic.save).to be_truthy
+ expect(topic.custom_fields['featured_link']).to eq('https://github.com/discourse/discourse')
+ end
+
+ context 'when category restricts present' do
+ let!(:link_category) { Fabricate(:link_category) }
+ let(:topic) { Fabricate(:topic) }
+ let(:link_topic) { Fabricate(:topic, category: link_category) }
+
+ it 'can save the featured link if it belongs to that category' do
+ link_topic.featured_link = 'https://github.com/discourse/discourse'
+ expect(link_topic.save).to be_truthy
+ expect(link_topic.custom_fields['featured_link']).to eq('https://github.com/discourse/discourse')
+ end
+
+ it 'can not save the featured link if it belongs to that category' do
+ topic.featured_link = 'https://github.com/discourse/discourse'
+ expect(topic.save).to be_falsey
+ end
+ end
+ end
end
diff --git a/spec/models/topic_tracking_state_spec.rb b/spec/models/topic_tracking_state_spec.rb
index 5410f69eed..cd51c5ff76 100644
--- a/spec/models/topic_tracking_state_spec.rb
+++ b/spec/models/topic_tracking_state_spec.rb
@@ -20,7 +20,7 @@ describe TopicTrackingState do
user = Fabricate(:user)
post
- report = TopicTrackingState.report(user.id)
+ report = TopicTrackingState.report(user)
expect(report.length).to eq(1)
CategoryUser.create!(user_id: user.id,
@@ -30,12 +30,12 @@ describe TopicTrackingState do
create_post(topic_id: post.topic_id)
- report = TopicTrackingState.report(user.id)
+ report = TopicTrackingState.report(user)
expect(report.length).to eq(0)
TopicUser.create!(user_id: user.id, topic_id: post.topic_id, last_read_post_number: 1, notification_level: 3)
- report = TopicTrackingState.report(user.id)
+ report = TopicTrackingState.report(user)
expect(report.length).to eq(1)
end
@@ -62,18 +62,18 @@ describe TopicTrackingState do
TopicUser.change(user.id, post2.topic_id, tracking)
TopicUser.change(user.id, post3.topic_id, tracking)
- report = TopicTrackingState.report(user.id)
+ report = TopicTrackingState.report(user)
expect(report.length).to eq(3)
end
it "correctly gets the tracking state" do
- report = TopicTrackingState.report(user.id)
+ report = TopicTrackingState.report(user)
expect(report.length).to eq(0)
post.topic.notifier.watch_topic!(post.topic.user_id)
- report = TopicTrackingState.report(user.id)
+ report = TopicTrackingState.report(user)
expect(report.length).to eq(1)
row = report[0]
@@ -84,18 +84,18 @@ describe TopicTrackingState do
expect(row.user_id).to eq(user.id)
# lets not leak out random users
- expect(TopicTrackingState.report(post.user_id)).to be_empty
+ expect(TopicTrackingState.report(post.user)).to be_empty
# lets not return anything if we scope on non-existing topic
- expect(TopicTrackingState.report(user.id, post.topic_id + 1)).to be_empty
+ expect(TopicTrackingState.report(user, post.topic_id + 1)).to be_empty
# when we reply the poster should have an unread row
create_post(user: user, topic: post.topic)
- report = TopicTrackingState.report(user.id)
+ report = TopicTrackingState.report(user)
expect(report.length).to eq(0)
- report = TopicTrackingState.report(post.user_id)
+ report = TopicTrackingState.report(post.user)
expect(report.length).to eq(1)
row = report[0]
@@ -111,7 +111,7 @@ describe TopicTrackingState do
post.topic.category_id = category.id
post.topic.save
- expect(TopicTrackingState.report(post.user_id)).to be_empty
- expect(TopicTrackingState.report(user.id)).to be_empty
+ expect(TopicTrackingState.report(post.user)).to be_empty
+ expect(TopicTrackingState.report(user)).to be_empty
end
end
diff --git a/spec/models/web_hook_spec.rb b/spec/models/web_hook_spec.rb
index 88eaee6cb0..0022c0e9d6 100644
--- a/spec/models/web_hook_spec.rb
+++ b/spec/models/web_hook_spec.rb
@@ -120,14 +120,14 @@ describe WebHook do
end
it 'should enqueue the right hooks for post events' do
- user # bypass a user_created event
- WebHook.expects(:enqueue_hooks).once
+ WebHook.expects(:enqueue_post_hooks).once
PostCreator.create(user, { raw: 'post', topic_id: topic.id, reply_to_post_number: 1, skip_validations: true })
- WebHook.expects(:enqueue_hooks).once
+ # post destroy or recover triggers a moderator post
+ WebHook.expects(:enqueue_post_hooks).twice
PostDestroyer.new(user, post2).destroy
- WebHook.expects(:enqueue_hooks).once
+ WebHook.expects(:enqueue_post_hooks).twice
PostDestroyer.new(user, post2).recover
end
diff --git a/spec/serializers/group_show_serializer_spec.rb b/spec/serializers/group_show_serializer_spec.rb
new file mode 100644
index 0000000000..5641ccd8d0
--- /dev/null
+++ b/spec/serializers/group_show_serializer_spec.rb
@@ -0,0 +1,31 @@
+require 'rails_helper'
+
+describe GroupShowSerializer do
+ context 'admin user' do
+ let(:user) { Fabricate(:admin) }
+ let(:group) { Fabricate(:group, users: [user]) }
+
+ it 'should return the right attributes' do
+ json = GroupShowSerializer.new(group, scope: Guardian.new(user)).as_json
+
+ expect(json[:group_show][:is_group_owner]).to eq(true)
+ expect(json[:group_show][:is_group_user]).to eq(true)
+ end
+ end
+
+ context 'group owner' do
+ let(:user) { Fabricate(:user) }
+ let(:group) { Fabricate(:group) }
+
+ before do
+ group.add_owner(user)
+ end
+
+ it 'should return the right attributes' do
+ json = GroupShowSerializer.new(group, scope: Guardian.new(user)).as_json
+
+ expect(json[:group_show][:is_group_owner]).to eq(true)
+ expect(json[:group_show][:is_group_user]).to eq(true)
+ end
+ end
+end
diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb
index 39457073a3..36863e798d 100644
--- a/spec/support/helpers.rb
+++ b/spec/support/helpers.rb
@@ -28,7 +28,7 @@ module Helpers
args[:title] ||= "This is my title #{Helpers.next_seq}"
user = args.delete(:user) || Fabricate(:user)
guardian = Guardian.new(user)
- args[:category] = args[:category].name if args[:category].is_a?(Category)
+ args[:category] = args[:category].id if args[:category].is_a?(Category)
TopicCreator.create(user, guardian, args)
end
@@ -37,7 +37,7 @@ module Helpers
args[:raw] ||= "This is the raw body of my post, it is cool #{Helpers.next_seq}"
args[:topic_id] = args[:topic].id if args[:topic]
user = args.delete(:user) || Fabricate(:user)
- args[:category] = args[:category].name if args[:category].is_a?(Category)
+ args[:category] = args[:category].id if args[:category].is_a?(Category)
creator = PostCreator.new(user, args)
post = creator.create
diff --git a/spec/tasks/redis_spec.rb b/spec/tasks/redis_spec.rb
new file mode 100644
index 0000000000..64e0305e2f
--- /dev/null
+++ b/spec/tasks/redis_spec.rb
@@ -0,0 +1,45 @@
+require 'rails_helper'
+
+RSpec.describe "Redis rake tasks" do
+ let(:redis) { $redis.without_namespace }
+
+ before do
+ @multisite = Rails.configuration.multisite
+ Rails.configuration.multisite = true
+ Discourse::Application.load_tasks
+ end
+
+ after do
+ Rails.configuration.multisite = @multisite
+ end
+
+ describe 'clean up' do
+ it 'should clean up orphan Redis keys' do
+ active_keys = [
+ '__mb_backlog_id_n_/users/someusername$|$default',
+ 'default:user-last-seen:607',
+ 'sidekiq:something:do:something',
+ 'somekeytonotbetouched'
+ ]
+
+ orphan_keys = [
+ 'tgxworld:user-last-seen:607',
+ '__mb_backlog_id_n_/users/someusername$|$tgxworld'
+ ]
+
+ (active_keys | orphan_keys).each do |key|
+ redis.set(key, 1)
+ end
+
+ Rake::Task['redis:clean_up'].invoke
+
+ active_keys.each do |key|
+ expect(redis.get(key)).to eq('1')
+ end
+
+ orphan_keys.each do |key|
+ expect(redis.get(key)).to eq(nil)
+ end
+ end
+ end
+end
diff --git a/test/javascripts/acceptance/groups-test.js.es6 b/test/javascripts/acceptance/groups-test.js.es6
index 0fcc909999..bc6a6ee44a 100644
--- a/test/javascripts/acceptance/groups-test.js.es6
+++ b/test/javascripts/acceptance/groups-test.js.es6
@@ -4,7 +4,9 @@ acceptance("Groups");
test("Browsing Groups", () => {
visit("/groups/discourse");
+
andThen(() => {
+ ok(count('.avatar-flair .fa-adjust') === 1, "it displays the group's avatar flair");
ok(count('.group-members tr') > 0, "it lists group members");
});
@@ -25,18 +27,28 @@ test("Browsing Groups", () => {
visit("/groups/discourse/messages");
andThen(() => {
- ok($('.action-list li').length === 4, 'it should not show messages tab');
+ ok($('.nav-stacked li').length === 4, 'it should not show messages tab');
ok(count('.user-stream .item') > 0, "it lists stream items");
});
});
-test("Messages tab", () => {
+test("Admin Browsing Groups", () => {
logIn();
Discourse.reset();
visit("/groups/discourse");
andThen(() => {
- ok($('.action-list li').length === 5, 'it should show messages tab if user is admin');
+ ok(find('.nav-stacked li').length === 5, 'it should show messages tab if user is admin');
+ equal(find('.group-title').text(), 'Awesome Team', 'it should display the group title');
+ equal(find('.group-name').text(), '@discourse', 'it should display the group name');
+ });
+
+ click('.group-edit-btn');
+
+ andThen(() => {
+ ok(find('.group-flair-inputs').length === 1, 'it should display avatar flair inputs');
+ ok(find('.edit-group-bio').length === 1, 'it should display group bio input');
+ ok(find('.edit-group-title').length === 1, 'it should display group title input');
});
});
diff --git a/test/javascripts/acceptance/search-full-test.js.es6 b/test/javascripts/acceptance/search-full-test.js.es6
index 2bc511f93b..f62879adb4 100644
--- a/test/javascripts/acceptance/search-full-test.js.es6
+++ b/test/javascripts/acceptance/search-full-test.js.es6
@@ -73,7 +73,7 @@ test("open advanced search", assert => {
test("validate population of advanced search", assert => {
visit("/search");
- fillIn('.search input.full-page-search', 'test user:admin #bug group:moderators badge:Reader tags:monkey in:likes in:private in:wiki in:bookmarks status:open after:2016-10-05 posts_count:10');
+ fillIn('.search input.full-page-search', 'test user:admin #bug group:moderators badge:Reader tags:monkey in:likes in:private in:wiki in:bookmarks status:open after:2016-10-05 min_post_count:10');
click('.search-advanced-btn');
andThen(() => {
@@ -89,7 +89,7 @@ test("validate population of advanced search", assert => {
assert.ok(exists('.search-advanced-options .combobox .select2-choice .select2-chosen:contains("are open")'), 'has "are open" pre-populated');
assert.ok(exists('.search-advanced-options .combobox .select2-choice .select2-chosen:contains("after")'), 'has "after" pre-populated');
assert.equal(find('.search-advanced-options #search-post-date').val(), "2016-10-05", 'has "2016-10-05" pre-populated');
- assert.equal(find('.search-advanced-options #search-posts-count').val(), "10", 'has "10" pre-populated');
+ assert.equal(find('.search-advanced-options #search-min-post-count').val(), "10", 'has "10" pre-populated');
});
});
@@ -274,15 +274,15 @@ test("update post time through advanced search ui", assert => {
});
});
-test("update posts count through advanced search ui", assert => {
+test("update min post count through advanced search ui", assert => {
visit("/search");
fillIn('.search input.full-page-search', 'none');
click('.search-advanced-btn');
- fillIn('#search-posts-count', '5');
+ fillIn('#search-min-post-count', '5');
andThen(() => {
- assert.equal(find('.search-advanced-options #search-posts-count').val(), "5", 'has "5" populated');
- assert.equal(find('.search input.full-page-search').val(), "none posts_count:5", 'has updated search term to "none posts_count:5"');
+ assert.equal(find('.search-advanced-options #search-min-post-count').val(), "5", 'has "5" populated');
+ assert.equal(find('.search input.full-page-search').val(), "none min_post_count:5", 'has updated search term to "none min_post_count:5"');
});
});
diff --git a/test/javascripts/controllers/group-test.js.es6 b/test/javascripts/controllers/group-test.js.es6
new file mode 100644
index 0000000000..822b14dd42
--- /dev/null
+++ b/test/javascripts/controllers/group-test.js.es6
@@ -0,0 +1,19 @@
+moduleFor("controller:group");
+
+test("canEditGroup", function() {
+ const GroupController = this.subject();
+
+ GroupController.setProperties({
+ model: { is_group_owner: true, automatic: true }
+ });
+
+ equal(GroupController.get("canEditGroup"), false, "automatic groups cannot be edited");
+
+ GroupController.set("model.automatic", false);
+
+ equal(GroupController.get("canEditGroup"), true, "owners can edit groups");
+
+ GroupController.set("model.is_group_owner", false);
+
+ equal(GroupController.get("canEditGroup"), false, "normal users cannot edit groups");
+});
diff --git a/test/javascripts/fixtures/group-fixtures.js.es6 b/test/javascripts/fixtures/group-fixtures.js.es6
index 8a2d2fa252..55041ac96f 100644
--- a/test/javascripts/fixtures/group-fixtures.js.es6
+++ b/test/javascripts/fixtures/group-fixtures.js.es6
@@ -4,9 +4,12 @@ export default {
"id":47,
"automatic":false,
"name":"discourse",
+ "title":"Awesome Team",
"user_count":8,
"alias_level":0,
- "visible":true
+ "visible":true,
+ "flair_url": 'fa-adjust',
+ "is_group_owner":true
}
},
"/groups/discourse/counts.json":{
diff --git a/test/javascripts/models/composer-test.js.es6 b/test/javascripts/models/composer-test.js.es6
index d541aaeb08..1ed5a691c3 100644
--- a/test/javascripts/models/composer-test.js.es6
+++ b/test/javascripts/models/composer-test.js.es6
@@ -40,6 +40,10 @@ test('missingReplyCharacters', function() {
missingReplyCharacters('hi', false, false, Discourse.SiteSettings.min_post_length - 2, 'too short public post');
missingReplyCharacters('hi', false, true, Discourse.SiteSettings.min_first_post_length - 2, 'too short first post');
missingReplyCharacters('hi', true, false, Discourse.SiteSettings.min_private_message_post_length - 2, 'too short private message');
+
+ Discourse.SiteSettings.topic_featured_link_onebox = true;
+ const composer = createComposer({ canEditTopicFeaturedLink: true });
+ equal(composer.get('missingReplyCharacters'), 0, "don't require any post content");
});
test('missingTitleCharacters', function() {
@@ -105,7 +109,7 @@ test("prependText", function() {
composer.prependText("world ");
equal(composer.get('reply'), "world hello", "it prepends text to existing text");
-
+
composer.prependText("before new line", {new_line: true});
equal(composer.get('reply'), "before new line\n\nworld hello", "it prepends text with new line to existing text");
});
diff --git a/vendor/assets/javascripts/run-qunit.js b/vendor/assets/javascripts/run-qunit.js
index 97fce3dc94..7f0335fea5 100644
--- a/vendor/assets/javascripts/run-qunit.js
+++ b/vendor/assets/javascripts/run-qunit.js
@@ -36,7 +36,7 @@ page.open(args[0], function(status) {
} else {
page.evaluate(logQUnit);
- var timeout = parseInt(args[1] || 130000, 10),
+ var timeout = parseInt(args[1] || 200000, 10),
start = Date.now();
var interval = setInterval(function() {
|