diff --git a/Gemfile b/Gemfile index 7d39c4aff0..302d22be4c 100644 --- a/Gemfile +++ b/Gemfile @@ -55,7 +55,7 @@ gem 'fast_xor' # Forked until https://github.com/sdsykes/fastimage/pull/93 is merged gem 'discourse_fastimage', require: 'fastimage' -gem 'aws-sdk', require: false +gem 'aws-sdk-s3', require: false gem 'excon', require: false gem 'unf', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 061142dabf..64a3964df8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,37 +1,37 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (5.1.3) - actionpack (= 5.1.3) - actionview (= 5.1.3) - activejob (= 5.1.3) + actionmailer (5.1.4) + actionpack (= 5.1.4) + actionview (= 5.1.4) + activejob (= 5.1.4) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.1.3) - actionview (= 5.1.3) - activesupport (= 5.1.3) + actionpack (5.1.4) + actionview (= 5.1.4) + activesupport (= 5.1.4) rack (~> 2.0) - rack-test (~> 0.6.3) + rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.1.3) - activesupport (= 5.1.3) + actionview (5.1.4) + activesupport (= 5.1.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) active_model_serializers (0.8.3) activemodel (>= 3.0) - activejob (5.1.3) - activesupport (= 5.1.3) + activejob (5.1.4) + activesupport (= 5.1.4) globalid (>= 0.3.6) - activemodel (5.1.3) - activesupport (= 5.1.3) - activerecord (5.1.3) - activemodel (= 5.1.3) - activesupport (= 5.1.3) + activemodel (5.1.4) + activesupport (= 5.1.4) + activerecord (5.1.4) + activemodel (= 5.1.4) + activesupport (= 5.1.4) arel (~> 8.0) - activesupport (5.1.3) + activesupport (5.1.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (~> 0.7) minitest (~> 5.1) @@ -44,12 +44,19 @@ GEM ansi (1.5.0) arel (8.0.0) ast (2.3.0) - aws-sdk (2.5.3) - aws-sdk-resources (= 2.5.3) - aws-sdk-core (2.5.3) + aws-partitions (1.24.0) + aws-sdk-core (3.6.0) + aws-partitions (~> 1.0) + aws-sigv4 (~> 1.0) jmespath (~> 1.0) - aws-sdk-resources (2.5.3) - aws-sdk-core (= 2.5.3) + aws-sdk-kms (1.2.0) + aws-sdk-core (~> 3) + aws-sigv4 (~> 1.0) + aws-sdk-s3 (1.4.0) + aws-sdk-core (~> 3) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.0) + aws-sigv4 (1.0.2) barber (0.11.2) ember-source (>= 1.0, < 3) execjs (>= 1.2, < 3) @@ -144,13 +151,14 @@ GEM rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) logster (1.2.7) - loofah (2.0.3) + loofah (2.1.1) + crass (~> 1.0.2) nokogiri (>= 1.5.9) lru_redux (1.1.0) mail (2.6.6) mime-types (>= 1.16, < 4) memory_profiler (0.9.8) - message_bus (2.0.5) + message_bus (2.0.8) rack (>= 1.1.3) metaclass (0.0.4) method_source (0.8.2) @@ -250,8 +258,8 @@ GEM ruby-openid (>= 2.1.8) rack-protection (2.0.0) rack - rack-test (0.6.3) - rack (>= 1.0) + rack-test (0.7.0) + rack (>= 1.0, < 3) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) @@ -260,16 +268,16 @@ GEM rails_multisite (1.1.0.rc4) activerecord (> 4.2, < 6) railties (> 4.2, < 6) - railties (5.1.3) - actionpack (= 5.1.3) - activesupport (= 5.1.3) + railties (5.1.4) + actionpack (= 5.1.4) + activesupport (= 5.1.4) method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.2.2) rake raindrops (0.18.0) - rake (12.0.0) + rake (12.1.0) rake-compiler (1.0.4) rake rb-fsevent (0.9.8) @@ -279,7 +287,7 @@ GEM ffi (>= 1.0.6) msgpack (>= 0.4.3) trollop (>= 1.16.2) - redis (3.3.3) + redis (3.3.5) redis-namespace (1.5.3) redis (~> 3.0, >= 3.0.4) rinku (2.0.2) @@ -344,11 +352,11 @@ GEM shoulda-context (1.2.2) shoulda-matchers (2.8.0) activesupport (>= 3.0.0) - sidekiq (5.0.4) + sidekiq (5.0.5) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) - redis (~> 3.3, >= 3.3.3) + redis (>= 3.3.4, < 5) simple-rss (1.3.1) slop (3.6.0) sprockets (3.7.1) @@ -392,7 +400,7 @@ DEPENDENCIES activerecord (~> 5.1) activesupport (~> 5.1) annotate - aws-sdk + aws-sdk-s3 barber better_errors binding_of_caller diff --git a/app/assets/javascripts/admin/templates/group.hbs b/app/assets/javascripts/admin/templates/group.hbs index 03be28e034..0689bd30ce 100644 --- a/app/assets/javascripts/admin/templates/group.hbs +++ b/app/assets/javascripts/admin/templates/group.hbs @@ -103,7 +103,7 @@ {{/if}}
-
diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs index e01f05ebfb..fcc9d716f2 100644 --- a/app/assets/javascripts/admin/templates/user-index.hbs +++ b/app/assets/javascripts/admin/templates/user-index.hbs @@ -307,7 +307,9 @@
{{i18n-yes-no model.isSuspended}} {{#if model.isSuspended}} - {{i18n "admin.user.suspended_until" until=model.suspendedTillDate}} + {{#unless model.suspendedForever}} + {{i18n "admin.user.suspended_until" until=model.suspendedTillDate}} + {{/unless}} {{/if}}
diff --git a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 index da6ac057d3..0dcf4051cd 100644 --- a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 +++ b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 @@ -9,6 +9,8 @@ const REPLACEMENTS = { 'd-watching-first': 'dot-circle-o', 'd-drop-expanded': 'caret-down', 'd-drop-collapsed': 'caret-right', + 'd-unliked': 'heart', + 'd-liked': 'heart', 'notification.mentioned': "at", 'notification.group_mentioned': "at", 'notification.quoted': "quote-right", @@ -40,7 +42,7 @@ export function renderIcon(renderType, id, params) { let rendererForType = renderer[renderType]; if (rendererForType) { - let result = rendererForType(id, params || {}); + let result = rendererForType(REPLACEMENTS[id] || id, params || {}); if (result) { return result; } @@ -80,8 +82,6 @@ registerIconRenderer({ name: 'font-awesome', string(id, params) { - id = REPLACEMENTS[id] || id; - let tagName = params.tagName || 'i'; let html = `<${tagName} class='${faClasses(id, params)}'`; if (params.title) { html += ` title='${I18n.t(params.title)}'`; } @@ -94,8 +94,6 @@ registerIconRenderer({ }, node(id, params) { - id = REPLACEMENTS[id] || id; - let tagName = params.tagName || 'i'; const properties = { diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 4a5de6d738..8445b9f8a5 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -85,7 +85,11 @@ export default Ember.Component.extend({ const $input = this.$('.d-editor-input'); $input.autocomplete({ template: findRawTemplate('user-selector-autocomplete'), - dataSource: term => userSearch({ term, topicId, includeGroups: true }), + dataSource: term => userSearch({ + term, + topicId, + includeMentionableGroups: true + }), key: "@", transformComplete: v => v.username || v.name }); diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index 44f0130acb..d661b26a21 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -1,10 +1,7 @@ /*global Mousetrap:true */ import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; -import Category from 'discourse/models/category'; import { categoryHashtagTriggerRule } from 'discourse/lib/category-hashtags'; -import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags'; import { search as searchCategoryTag } from 'discourse/lib/category-tag-search'; -import { SEPARATOR } from 'discourse/lib/category-hashtags'; import { cookAsync } from 'discourse/lib/text'; import { translations } from 'pretty-text/emoji/data'; import { emojiSearch, isSkinTonableEmoji } from 'pretty-text/emoji'; @@ -322,11 +319,7 @@ export default Ember.Component.extend({ template: findRawTemplate('category-tag-autocomplete'), key: '#', transformComplete(obj) { - if (obj.model) { - return Category.slugFor(obj.model, SEPARATOR); - } else { - return `${obj.text}${TAG_HASHTAG_POSTFIX}`; - } + return obj.text; }, dataSource(term) { return searchCategoryTag(term, siteSettings); diff --git a/app/assets/javascripts/discourse/components/discourse-topic.js.es6 b/app/assets/javascripts/discourse/components/discourse-topic.js.es6 index ba81a63bf3..809d3b6f90 100644 --- a/app/assets/javascripts/discourse/components/discourse-topic.js.es6 +++ b/app/assets/javascripts/discourse/components/discourse-topic.js.es6 @@ -99,7 +99,6 @@ export default Ember.Component.extend(AddArchetypeClass, Scrolling, { // this happens after route exit, stuff could have trickled in this.appEvents.trigger('header:hide-topic'); this.appEvents.off('post:highlight'); - }, @observes('Discourse.hasFocus') diff --git a/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6 b/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6 new file mode 100644 index 0000000000..421972b834 --- /dev/null +++ b/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6 @@ -0,0 +1,50 @@ +import { default as computed, observes, on } from "ember-addons/ember-computed-decorators"; + +import { + PUBLISH_TO_CATEGORY_STATUS_TYPE, + OPEN_STATUS_TYPE, + DELETE_STATUS_TYPE, + REMINDER_TYPE, + CLOSE_STATUS_TYPE +} from 'discourse/controllers/edit-topic-timer'; + +export default Ember.Component.extend({ + selection: Ember.computed.alias('topicTimer.status_type'), + autoOpen: Ember.computed.equal('selection', OPEN_STATUS_TYPE), + autoClose: Ember.computed.equal('selection', CLOSE_STATUS_TYPE), + autoDelete: Ember.computed.equal('selection', DELETE_STATUS_TYPE), + publishToCategory: Ember.computed.equal('selection', PUBLISH_TO_CATEGORY_STATUS_TYPE), + reminder: Ember.computed.equal('selection', REMINDER_TYPE), + showTimeOnly: Ember.computed.or('autoOpen', 'autoDelete', 'reminder'), + + @computed('topicTimer.updateTime', 'loading', 'publishToCategory', 'topicTimer.category_id') + saveDisabled(updateTime, loading, publishToCategory, topicTimerCategoryId) { + return Ember.isEmpty(updateTime) || + loading || + (publishToCategory && !topicTimerCategoryId); + }, + + @computed("topic.visible") + excludeCategoryId(visible) { + if (visible) return this.get('topic.category_id'); + }, + + @on('init') + @observes("topicTimer", "topicTimer.execute_at", "topicTimer.duration") + _setUpdateTime() { + let time = null; + const executeAt = this.get('topicTimer.execute_at'); + + if (executeAt && this.get("topicTimer.based_on_last_post")) { + time = this.get("topicTimer.duration"); + } else if (executeAt) { + const closeTime = moment(executeAt); + + if (closeTime > moment()) { + time = closeTime.format("YYYY-MM-DD HH:mm"); + } + } + + this.set("topicTimer.updateTime", time); + } +}); diff --git a/app/assets/javascripts/discourse/components/future-date-input-selector.js.es6 b/app/assets/javascripts/discourse/components/future-date-input-selector.js.es6 index f4c9a00fc8..df1b2e871c 100644 --- a/app/assets/javascripts/discourse/components/future-date-input-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/future-date-input-selector.js.es6 @@ -8,7 +8,10 @@ const TOMORROW = 'tomorrow'; const LATER_THIS_WEEK = 'later_this_week'; const THIS_WEEKEND = 'this_weekend'; const NEXT_WEEK = 'next_week'; +const TWO_WEEKS = 'two_weeks'; const NEXT_MONTH = 'next_month'; +const FOREVER = 'forever'; + export const PICK_DATE_AND_TIME = 'pick_date_and_time'; export const SET_BASED_ON_LAST_POST = 'set_based_on_last_post'; @@ -44,14 +47,13 @@ export default Combobox.extend({ }); } - if (day < 5) { + if (day < 5 && this.get('includeWeekend')) { selections.push({ id: THIS_WEEKEND, name: I18n.t('topic.auto_update_input.this_weekend') }); } - if (day !== 7) { selections.push({ id: NEXT_WEEK, @@ -59,6 +61,11 @@ export default Combobox.extend({ }); } + selections.push({ + id: TWO_WEEKS, + name: I18n.t('topic.auto_update_input.two_weeks') + }); + if (moment().endOf('month').date() !== now.date()) { selections.push({ id: NEXT_MONTH, @@ -66,6 +73,13 @@ export default Combobox.extend({ }); } + if (this.get('includeForever')) { + selections.push({ + id: FOREVER, + name: I18n.t('topic.auto_update_input.forever') + }); + } + selections.push({ id: PICK_DATE_AND_TIME, name: I18n.t('topic.auto_update_input.pick_date_and_time') @@ -118,7 +132,7 @@ export default Combobox.extend({ if (time) { if (state.id === LATER_TODAY) { time = time.format('h a'); - } else if (state.id === NEXT_MONTH) { + } else if (state.id === NEXT_MONTH || state.id === TWO_WEEKS) { time = time.format('MMM D'); } else { time = time.format('ddd, h a'); @@ -133,7 +147,7 @@ export default Combobox.extend({ output += `${state.text}`; - if (time) { + if (time && state.id !== FOREVER) { output += `${time}`; } @@ -166,10 +180,18 @@ export default Combobox.extend({ time = time.add(1, 'week').day(1).hour(timeOfDay).minute(0); icon = 'briefcase'; break; + case TWO_WEEKS: + time = time.add(2, 'week').hour(timeOfDay).minute(0); + icon = 'briefcase'; + break; case NEXT_MONTH: time = time.add(1, 'month').startOf('month').hour(timeOfDay).minute(0); icon = 'briefcase'; break; + case FOREVER: + time = time.add(1000, 'year').hour(timeOfDay).minute(0); + icon = 'gavel'; + break; case PICK_DATE_AND_TIME: time = null; icon = 'calendar-plus-o'; diff --git a/app/assets/javascripts/discourse/components/user-selector.js.es6 b/app/assets/javascripts/discourse/components/user-selector.js.es6 index c583960abe..fd9bb6ba3b 100644 --- a/app/assets/javascripts/discourse/components/user-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/user-selector.js.es6 @@ -21,6 +21,7 @@ export default TextField.extend({ groups = [], currentUser = this.currentUser, includeMentionableGroups = this.get('includeMentionableGroups') === 'true', + includeMessageableGroups = this.get('includeMessageableGroups') === 'true', includeGroups = this.get('includeGroups') === 'true', allowedUsers = this.get('allowedUsers') === 'true'; @@ -52,6 +53,7 @@ export default TextField.extend({ includeGroups, allowedUsers, includeMentionableGroups, + includeMessageableGroups, group: self.get("group") }); diff --git a/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 b/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 index fdb66c1a65..e1a122a37a 100644 --- a/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 @@ -1,67 +1,51 @@ -import { default as computed, observes } from "ember-addons/ember-computed-decorators"; +import { default as computed } from "ember-addons/ember-computed-decorators"; import ModalFunctionality from 'discourse/mixins/modal-functionality'; import TopicTimer from 'discourse/models/topic-timer'; import { popupAjaxError } from 'discourse/lib/ajax-error'; export const CLOSE_STATUS_TYPE = 'close'; -const OPEN_STATUS_TYPE = 'open'; +export const OPEN_STATUS_TYPE = 'open'; export const PUBLISH_TO_CATEGORY_STATUS_TYPE = 'publish_to_category'; -const DELETE_STATUS_TYPE = 'delete'; -const REMINDER_TYPE = 'reminder'; +export const DELETE_STATUS_TYPE = 'delete'; +export const REMINDER_TYPE = 'reminder'; export default Ember.Controller.extend(ModalFunctionality, { loading: false, - updateTime: null, - topicTimer: Ember.computed.alias("model.topic_timer"), - selection: Ember.computed.alias('model.topic_timer.status_type'), - autoOpen: Ember.computed.equal('selection', OPEN_STATUS_TYPE), - autoClose: Ember.computed.equal('selection', CLOSE_STATUS_TYPE), - autoDelete: Ember.computed.equal('selection', DELETE_STATUS_TYPE), - publishToCategory: Ember.computed.equal('selection', PUBLISH_TO_CATEGORY_STATUS_TYPE), - reminder: Ember.computed.equal('selection', REMINDER_TYPE), - - showTimeOnly: Ember.computed.or('autoOpen', 'autoDelete', 'reminder'), + isPublic: "true", @computed("model.closed") - timerTypes(closed) { + publicTimerTypes(closed) { return [ { id: CLOSE_STATUS_TYPE, name: I18n.t(closed ? 'topic.temp_open.title' : 'topic.auto_close.title'), }, { id: OPEN_STATUS_TYPE, name: I18n.t(closed ? 'topic.auto_reopen.title' : 'topic.temp_close.title') }, { id: PUBLISH_TO_CATEGORY_STATUS_TYPE, name: I18n.t('topic.publish_to_category.title') }, - { id: DELETE_STATUS_TYPE, name: I18n.t('topic.auto_delete.title') }, + { id: DELETE_STATUS_TYPE, name: I18n.t('topic.auto_delete.title') } + ]; + }, + + @computed() + privateTimerTypes() { + return [ { id: REMINDER_TYPE, name: I18n.t('topic.reminder.title') } ]; }, - @computed('updateTime', 'loading', 'publishToCategory', 'topicTimer.category_id') - saveDisabled(updateTime, loading, publishToCategory, topicTimerCategoryId) { - return Ember.isEmpty(updateTime) || - loading || - (publishToCategory && !topicTimerCategoryId); - }, - - @computed("model.visible") - excludeCategoryId(visible) { - if (visible) return this.get('model.category_id'); - }, - - @observes("topicTimer.execute_at", "topicTimer.duration") - _setUpdateTime() { - if (!this.get('topicTimer.execute_at')) return; - - let time = null; - - if (this.get("topicTimer.based_on_last_post")) { - time = this.get("topicTimer.duration"); - } else if (this.get("topicTimer.execute_at")) { - const closeTime = moment(this.get('topicTimer.execute_at')); - - if (closeTime > moment()) { - time = closeTime.format("YYYY-MM-DD HH:mm"); - } + @computed("isPublic", 'publicTimerTypes', 'privateTimerTypes') + selections(isPublic, publicTimerTypes, privateTimerTypes) { + if (isPublic === 'true') { + return publicTimerTypes; + } else { + return privateTimerTypes; } + }, - this.set("updateTime", time); + @computed('isPublic', 'model.topic_timer', 'model.private_topic_timer') + topicTimer(isPublic, publicTopicTimer, privateTopicTimer) { + if (isPublic === 'true') { + return publicTopicTimer; + } else { + return privateTopicTimer; + } }, _setTimer(time, statusType) { @@ -85,10 +69,11 @@ export default Ember.Controller.extend(ModalFunctionality, { this.set('model.closed', result.closed); } else { + const topicTimer = this.get('isPublic') === 'true' ? 'topic_timer' : 'private_topic_timer'; + this.set(`model.${topicTimer}`, Ember.Object.create({})); + this.setProperties({ - topicTimer: Ember.Object.create({}), selection: null, - updateTime: null }); } }).catch(error => { @@ -98,11 +83,11 @@ export default Ember.Controller.extend(ModalFunctionality, { actions: { saveTimer() { - this._setTimer(this.get("updateTime"), this.get('selection')); + this._setTimer(this.get("topicTimer.updateTime"), this.get('topicTimer.status_type')); }, removeTimer() { - this._setTimer(null, this.get('selection')); + this._setTimer(null, this.get('topicTimer.status_type')); } } }); diff --git a/app/assets/javascripts/discourse/controllers/group.js.es6 b/app/assets/javascripts/discourse/controllers/group.js.es6 index d38f15290e..4734ac8db4 100644 --- a/app/assets/javascripts/discourse/controllers/group.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group.js.es6 @@ -48,9 +48,9 @@ export default Ember.Controller.extend({ }; }, - @computed("model.mentionable") - displayGroupMessageButton(mentionable) { - return this.currentUser && mentionable; + @computed("model.messageable") + displayGroupMessageButton(messageable) { + return this.currentUser && messageable; }, @observes('model.user_count') diff --git a/app/assets/javascripts/discourse/controllers/invites-show.js.es6 b/app/assets/javascripts/discourse/controllers/invites-show.js.es6 index 15acf96ddd..2553a01a13 100644 --- a/app/assets/javascripts/discourse/controllers/invites-show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/invites-show.js.es6 @@ -61,7 +61,7 @@ export default Ember.Controller.extend(PasswordValidation, UsernameValidation, N username: this.get('accountUsername'), name: this.get('accountName'), password: this.get('accountPassword'), - userCustomFields + user_custom_fields: userCustomFields } }).then(result => { if (result.success) { diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6 index a42f4233c4..c524939435 100644 --- a/app/assets/javascripts/discourse/controllers/login.js.es6 +++ b/app/assets/javascripts/discourse/controllers/login.js.es6 @@ -71,7 +71,7 @@ export default Ember.Controller.extend(ModalFunctionality, { type: 'POST' }).then(function (result) { // Successful login - if (result.error) { + if (result && result.error) { self.set('loggingIn', false); if (result.reason === 'not_activated') { self.send('showNotActivated', { diff --git a/app/assets/javascripts/discourse/lib/category-tag-search.js.es6 b/app/assets/javascripts/discourse/lib/category-tag-search.js.es6 index d7895ff47a..359e766041 100644 --- a/app/assets/javascripts/discourse/lib/category-tag-search.js.es6 +++ b/app/assets/javascripts/discourse/lib/category-tag-search.js.es6 @@ -1,5 +1,7 @@ import { CANCELLED_STATUS } from 'discourse/lib/autocomplete'; import Category from 'discourse/models/category'; +import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags'; +import { SEPARATOR } from 'discourse/lib/category-hashtags'; var cache = {}; var cacheTime; @@ -27,7 +29,18 @@ function searchTags(term, categories, limit) { var returnVal = CANCELLED_STATUS; oldSearch.then((r) => { - var tags = r.results.map((tag) => { return { text: tag.text, count: tag.count }; }); + const categoryNames = cats.map(c => c.model.get('name')); + + const tags = r.results.map((tag) => { + const tagName = tag.text; + + return { + name: tagName, + text: (categoryNames.includes(tagName) ? `${tagName}${TAG_HASHTAG_POSTFIX}` : tagName), + count: tag.count, + }; + }); + returnVal = cats.concat(tags); }).always(() => { oldSearch = null; @@ -55,7 +68,10 @@ export function search(term, siteSettings) { const limit = 5; var categories = Category.search(term, { limit }); var numOfCategories = categories.length; - categories = categories.map((category) => { return { model: category }; }); + + categories = categories.map((category) => { + return { model: category, text: Category.slugFor(category, SEPARATOR) }; + }); if (numOfCategories !== limit && siteSettings.tagging_enabled) { return searchTags(term, categories, limit - numOfCategories); diff --git a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 index 86a60bd835..43c70615f0 100644 --- a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 +++ b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 @@ -19,10 +19,10 @@ function calcHeight() { // iPhone shrinks header and removes footer controls ( back / forward nav ) // at 39px we are at the largest viewport - const smallViewport = (window.screen.height - window.innerHeight) > 40; + const portrait = window.innerHeight > window.innerWidth; + const smallViewport = ((portrait ? window.screen.height : window.screen.width) - window.innerHeight) > 40; - // portrait - if (window.screen.height > window.screen.width) { + if (portrait) { // iPhone SE, it is super small so just // have a bit of crop @@ -39,24 +39,22 @@ function calcHeight() { if (window.screen.height === 736) { withoutKeyboard = smallViewport ? 353 : 383; } - // iPad can use innerHeight cause it renders nothing in the footer if (window.innerHeight > 920) { withoutKeyboard -= 45; } } else { + // landscape - // // iPad, we have a bigger keyboard - if (window.innerWidth > window.innerHeight && window.innerHeight > 665) { + if (window.innerHeight > 665) { withoutKeyboard -= 128; } } // iPad portrait also has a bigger keyboard - return Math.max(withoutKeyboard, min); } diff --git a/app/assets/javascripts/discourse/lib/search.js.es6 b/app/assets/javascripts/discourse/lib/search.js.es6 index 8939c17329..266f374a65 100644 --- a/app/assets/javascripts/discourse/lib/search.js.es6 +++ b/app/assets/javascripts/discourse/lib/search.js.es6 @@ -1,7 +1,5 @@ import { ajax } from 'discourse/lib/ajax'; import { findRawTemplate } from 'discourse/lib/raw-templates'; -import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags'; -import { SEPARATOR } from 'discourse/lib/category-hashtags'; import Category from 'discourse/models/category'; import { search as searchCategoryTag } from 'discourse/lib/category-tag-search'; import userSearch from 'discourse/lib/user-search'; @@ -148,11 +146,7 @@ export function applySearchAutocomplete($input, siteSettings, appEvents, options width: '100%', treatAsTextarea: true, transformComplete(obj) { - if (obj.model) { - return Category.slugFor(obj.model, SEPARATOR); - } else { - return `${obj.text}${TAG_HASHTAG_POSTFIX}`; - } + return obj.text; }, dataSource(term) { return searchCategoryTag(term, siteSettings); diff --git a/app/assets/javascripts/discourse/lib/user-search.js.es6 b/app/assets/javascripts/discourse/lib/user-search.js.es6 index 5186779d3c..29ad228a05 100644 --- a/app/assets/javascripts/discourse/lib/user-search.js.es6 +++ b/app/assets/javascripts/discourse/lib/user-search.js.es6 @@ -7,7 +7,7 @@ var cache = {}, currentTerm, oldSearch; -function performSearch(term, topicId, includeGroups, includeMentionableGroups, allowedUsers, group, resultsFn) { +function performSearch(term, topicId, includeGroups, includeMentionableGroups, includeMessageableGroups, allowedUsers, group, resultsFn) { var cached = cache[term]; if (cached) { resultsFn(cached); @@ -20,6 +20,7 @@ function performSearch(term, topicId, includeGroups, includeMentionableGroups, a topic_id: topicId, include_groups: includeGroups, include_mentionable_groups: includeMentionableGroups, + include_messageable_groups: includeMessageableGroups, group: group, topic_allowed_users: allowedUsers } }); @@ -88,6 +89,7 @@ export default function userSearch(options) { var term = options.term || "", includeGroups = options.includeGroups, includeMentionableGroups = options.includeMentionableGroups, + includeMessageableGroups = options.includeMessageableGroups, allowedUsers = options.allowedUsers, topicId = options.topicId, group = options.group; @@ -120,6 +122,7 @@ export default function userSearch(options) { topicId, includeGroups, includeMentionableGroups, + includeMessageableGroups, allowedUsers, group, function(r) { diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index fb3e16b888..3727290485 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -29,6 +29,8 @@ export function loadTopicView(topic, args) { }); } +export const ID_CONSTRAINT = /^\d+$/; + const Topic = RestModel.extend({ message: null, errorLoading: false, diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 93dce42015..10a822a4e9 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -178,6 +178,11 @@ const User = RestModel.extend({ return suspendedTill && moment(suspendedTill).isAfter(); }, + @computed("suspended_till") + suspendedForever(suspendedTill) { + return moment().diff(suspendedTill, 'years') < -500; + }, + @computed("suspended_till") suspendedTillDate(suspendedTill) { return longDate(suspendedTill); diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 index 7b6e59a895..8ec183d8b5 100644 --- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 +++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 @@ -9,7 +9,8 @@ export default function() { this.route('fromParams', { path: '/' }); this.route('fromParamsNear', { path: '/:nearPost' }); }); - this.route('topicBySlug', { path: '/t/:slug', resetNamespace: true }); + + this.route('topicBySlugOrId', { path: '/t/:slugOrId', resetNamespace: true }); this.route('topicUnsubscribe', { path: '/t/:slug/:id/unsubscribe' }); this.route('discovery', { path: '/', resetNamespace: true }, function() { diff --git a/app/assets/javascripts/discourse/routes/topic-by-slug-or-id.js.es6 b/app/assets/javascripts/discourse/routes/topic-by-slug-or-id.js.es6 new file mode 100644 index 0000000000..6fcc92151a --- /dev/null +++ b/app/assets/javascripts/discourse/routes/topic-by-slug-or-id.js.es6 @@ -0,0 +1,16 @@ +import { default as Topic, ID_CONSTRAINT } from 'discourse/models/topic'; +import DiscourseURL from 'discourse/lib/url'; + +export default Discourse.Route.extend({ + model(params) { + if (params.slugOrId.match(ID_CONSTRAINT)) { + return { url: `/t/topic/${params.slugOrId}` }; + } else { + return Topic.idForSlug(params.slugOrId); + } + }, + + afterModel(result) { + DiscourseURL.routeTo(result.url, { replaceURL: true }); + } +}); diff --git a/app/assets/javascripts/discourse/routes/topic-by-slug.js.es6 b/app/assets/javascripts/discourse/routes/topic-by-slug.js.es6 deleted file mode 100644 index fc402e43c9..0000000000 --- a/app/assets/javascripts/discourse/routes/topic-by-slug.js.es6 +++ /dev/null @@ -1,12 +0,0 @@ -import Topic from 'discourse/models/topic'; -import DiscourseURL from 'discourse/lib/url'; - -export default Discourse.Route.extend({ - model: function(params) { - return Topic.idForSlug(params.slug); - }, - - afterModel: function(result) { - DiscourseURL.routeTo(result.url, { replaceURL: true }); - } -}); diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 7fc7fd098f..5086722534 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -1,4 +1,5 @@ import DiscourseURL from 'discourse/lib/url'; +import { ID_CONSTRAINT } from 'discourse/models/topic'; let isTransitioning = false, scheduledReplace = null, @@ -53,6 +54,7 @@ const TopicRoute = Discourse.Route.extend({ showTopicStatusUpdate() { const model = this.modelFor('topic'); model.set('topic_timer', Ember.Object.create(model.get('topic_timer'))); + model.set('private_topic_timer', Ember.Object.create(model.get('private_topic_timer'))); showModal('edit-topic-timer', { model }); this.controllerFor('modal').set('modalClass', 'edit-topic-timer-modal'); }, @@ -157,6 +159,10 @@ const TopicRoute = Discourse.Route.extend({ }, model(params, transition) { + if (params.slug.match(ID_CONSTRAINT)) { + return DiscourseURL.routeTo(`/t/topic/${params.slug}/${params.id}`, { replaceURL: true }); + }; + const queryParams = transition.queryParams; let topic = this.modelFor('topic'); diff --git a/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs b/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs index 1c429ddb39..3f4359aab5 100644 --- a/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs +++ b/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs @@ -5,7 +5,7 @@ {{#if option.model}} {{category-link option.model allowUncategorized="true" link="false"}} {{else}} - {{d-icon 'tag'}}{{option.text}} x {{option.count}} + {{d-icon 'tag'}}{{option.name}} x {{option.count}} {{/if}} {{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs b/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs index 402416a1d6..99fc0a5b97 100644 --- a/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs +++ b/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs @@ -2,7 +2,7 @@ {{user-selector topicId=topicId onChangeCallback='triggerResize' id="private-message-users" - includeMentionableGroups='true' + includeMessageableGroups='true' class="span8" placeholderKey="composer.users_placeholder" tabindex="1" diff --git a/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs b/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs new file mode 100644 index 0000000000..e231080081 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs @@ -0,0 +1,36 @@ +
+
+ {{combo-box content=timerTypes value=selection width="50%"}} +
+ +
+ {{#if showTimeOnly}} + {{future-date-input + input=topicTimer.updateTime + statusType=selection + includeWeekend=true + basedOnLastPost=false}} + {{else if publishToCategory}} +
+ + {{category-select-box + value=topicTimer.category_id + excludeCategoryId=excludeCategoryId}} +
+ + {{future-date-input + input=topicTimer.updateTime + statusType=selection + includeWeekend=true + categoryId=topicTimer.category_id + basedOnLastPost=false}} + {{else if autoClose}} + {{future-date-input + input=topicTimer.updateTime + statusType=selection + includeWeekend=true + basedOnLastPost=topicTimer.based_on_last_post + lastPostedAt=model.last_posted_at}} + {{/if}} +
+
diff --git a/app/assets/javascripts/discourse/templates/components/future-date-input.hbs b/app/assets/javascripts/discourse/templates/components/future-date-input.hbs index 3f5f984893..62dd978281 100644 --- a/app/assets/javascripts/discourse/templates/components/future-date-input.hbs +++ b/app/assets/javascripts/discourse/templates/components/future-date-input.hbs @@ -8,6 +8,8 @@ statusType=statusType value=selection input=input + includeWeekend=includeWeekend + includeForever=includeForever width="50%" none="topic.auto_update_input.none"}}
diff --git a/app/assets/javascripts/discourse/templates/modal/edit-topic-timer.hbs b/app/assets/javascripts/discourse/templates/modal/edit-topic-timer.hbs index bc01b06634..4de56698a3 100644 --- a/app/assets/javascripts/discourse/templates/modal/edit-topic-timer.hbs +++ b/app/assets/javascripts/discourse/templates/modal/edit-topic-timer.hbs @@ -1,51 +1,35 @@ -
- {{#d-modal-body title="topic.topic_status_update.title" autoFocus="false"}} -
- {{combo-box content=timerTypes value=selection width="50%"}} -
+{{#d-modal-body title="topic.topic_status_update.title" autoFocus="false"}} +
+ -
- {{#if showTimeOnly}} - {{future-date-input - input=updateTime - statusType=selection - basedOnLastPost=false}} - {{else if publishToCategory}} -
- - {{category-select-box - value=topicTimer.category_id - excludeCategoryId=excludeCategoryId}} -
- - {{future-date-input - input=updateTime - statusType=selection - categoryId=topicTimer.category_id - basedOnLastPost=false}} - {{else if autoClose}} - {{future-date-input - input=updateTime - statusType=selection - basedOnLastPost=topicTimer.based_on_last_post - lastPostedAt=model.last_posted_at}} - {{/if}} -
- {{/d-modal-body}} - - - + + {{edit-topic-timer-form + topic=model + topicTimer=topicTimer + timerTypes=selections + updateTime=updateTime + closeModal="closeModal"}} +{{/d-modal-body}} + + diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs index 4f0fcddacd..b35d45d2d6 100644 --- a/app/assets/javascripts/discourse/templates/topic.hbs +++ b/app/assets/javascripts/discourse/templates/topic.hbs @@ -199,6 +199,13 @@
{{/if}} + {{#if model.private_topic_timer.execute_at}} + {{topic-timer-info + statusType=model.private_topic_timer.status_type + executeAt=model.private_topic_timer.execute_at + duration=model.private_topic_timer.duration}} + {{/if}} + {{topic-timer-info statusType=model.topic_timer.status_type executeAt=model.topic_timer.execute_at diff --git a/app/assets/javascripts/discourse/templates/user.hbs b/app/assets/javascripts/discourse/templates/user.hbs index 7c654823ef..17677dd3d1 100644 --- a/app/assets/javascripts/discourse/templates/user.hbs +++ b/app/assets/javascripts/discourse/templates/user.hbs @@ -84,7 +84,13 @@ {{#if model.isSuspended}}
{{d-icon "ban"}} - {{i18n 'user.suspended_notice' date=model.suspendedTillDate}}
+ + {{#if model.suspendedForever}} + {{i18n 'user.suspended_permanently'}} + {{else}} + {{i18n 'user.suspended_notice' date=model.suspendedTillDate}} + {{/if}} +
{{#if model.suspend_reason}} {{i18n 'user.suspended_reason'}} {{model.suspend_reason}} {{/if}} diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index 890a7a7b4a..b5835b3c19 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -33,6 +33,11 @@ createWidget('priority-faq-link', { export default createWidget('hamburger-menu', { tagName: 'div.hamburger-panel', + settings: { + showCategories: true, + maxWidth: 300 + }, + adminLinks() { const { currentUser } = this; @@ -176,15 +181,22 @@ export default createWidget('hamburger-menu', { } results.push(this.attach('menu-links', {name: 'general-links', contents: () => this.generalLinks() })); - results.push(this.listCategories()); - results.push(h('hr')); + + if (this.settings.showCategories) { + results.push(this.listCategories()); + results.push(h('hr')); + } + results.push(this.attach('menu-links', {name: 'footer-links', omitRule: true, contents: () => this.footerLinks(prioritizeFaq, faqUrl) })); return results; }, html() { - return this.attach('menu-panel', { contents: () => this.panelContents() }); + return this.attach('menu-panel', { + contents: () => this.panelContents(), + maxWidth: this.settings.maxWidth, + }); }, clickOutside() { diff --git a/app/assets/javascripts/discourse/widgets/header-contents.js.es6 b/app/assets/javascripts/discourse/widgets/header-contents.js.es6 index 819178b6ac..e998053ff4 100644 --- a/app/assets/javascripts/discourse/widgets/header-contents.js.es6 +++ b/app/assets/javascripts/discourse/widgets/header-contents.js.es6 @@ -4,10 +4,10 @@ import hbs from 'discourse/widgets/hbs-compiler'; createWidget('header-contents', { tagName: 'div.contents.clearfix', template: hbs` - {{attach widget="home-logo"}} + {{attach widget="home-logo" attrs=attrs}}
{{yield}}
{{#if attrs.topic}} - {{attach widget="header-topic-info"}} + {{attach widget="header-topic-info" attrs=attrs}} {{/if}} `, }); diff --git a/app/assets/javascripts/discourse/widgets/link.js.es6 b/app/assets/javascripts/discourse/widgets/link.js.es6 index 5774ccc763..a762a1e74d 100644 --- a/app/assets/javascripts/discourse/widgets/link.js.es6 +++ b/app/assets/javascripts/discourse/widgets/link.js.es6 @@ -31,8 +31,10 @@ export default createWidget('link', { }, buildAttributes(attrs) { - return { href: this.href(attrs), - title: attrs.title ? I18n.t(attrs.title) : this.label(attrs) }; + return { + href: this.href(attrs), + title: attrs.title ? I18n.t(attrs.title) : this.label(attrs) + }; }, label(attrs) { diff --git a/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 index dabbdc421b..0a29b82bb2 100644 --- a/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 @@ -10,52 +10,85 @@ createWidget('post-admin-menu-button', jQuery.extend(ButtonClass, { } })); +export function buildManageButtons(attrs, currentUser) { + if (!currentUser) { + return []; + } + + let contents = []; + if (!attrs.isWhisper && currentUser.staff) { + const buttonAtts = { + action: 'togglePostType', + icon: 'shield', + className: 'toggle-post-type' + }; + + if (attrs.isModeratorAction) { + buttonAtts.label = 'post.controls.revert_to_regular'; + } else { + buttonAtts.label = 'post.controls.convert_to_moderator'; + } + contents.push(buttonAtts); + } + + if (attrs.canManage) { + contents.push({ + icon: 'cog', + label: 'post.controls.rebake', + action: 'rebakePost', + className: 'rebuild-html' + }); + + if (attrs.hidden) { + contents.push({ + icon: 'eye', + label: 'post.controls.unhide', + action: 'unhidePost', + className: 'unhide-post' + }); + } + } + + if (currentUser.admin) { + contents.push({ + icon: 'user', + label: 'post.controls.change_owner', + action: 'changePostOwner', + className: 'change-owner' + }); + } + + if (attrs.canManage || attrs.canWiki) { + if (attrs.wiki) { + contents.push({ + action: 'toggleWiki', + label: 'post.controls.unwiki', + icon: 'pencil-square-o', + className: 'wiki wikied' + }); + } else { + contents.push({ + action: 'toggleWiki', + label: 'post.controls.wiki', + icon: 'pencil-square-o', + className: 'wiki' + }); + } + } + + return contents; +} + export default createWidget('post-admin-menu', { tagName: 'div.post-admin-menu.popup-menu', - html(attrs) { + html() { const contents = []; contents.push(h('h3', I18n.t('admin_title'))); - if (!attrs.isWhisper && this.currentUser.staff) { - const buttonAtts = { action: 'togglePostType', icon: 'shield', className: 'toggle-post-type' }; - - if (attrs.isModeratorAction) { - buttonAtts.label = 'post.controls.revert_to_regular'; - } else { - buttonAtts.label = 'post.controls.convert_to_moderator'; - } - contents.push(this.attach('post-admin-menu-button', buttonAtts)); - } - - if (attrs.canManage) { - contents.push(this.attach('post-admin-menu-button', { - icon: 'cog', label: 'post.controls.rebake', action: 'rebakePost', className: 'rebuild-html' - })); - - if (attrs.hidden) { - contents.push(this.attach('post-admin-menu-button', { - icon: 'eye', label: 'post.controls.unhide', action: 'unhidePost', className: 'unhide-post' - })); - } - } - - if (this.currentUser.admin) { - contents.push(this.attach('post-admin-menu-button', { - icon: 'user', label: 'post.controls.change_owner', action: 'changePostOwner', className: 'change-owner' - })); - } - - // toggle Wiki button - if (attrs.wiki) { - contents.push(this.attach('post-admin-menu-button', { - action: 'toggleWiki', label: 'post.controls.unwiki', icon: 'pencil-square-o', className: 'wiki wikied' - })); - } else { - contents.push(this.attach('post-admin-menu-button', { - action: 'toggleWiki', label: 'post.controls.wiki', icon: 'pencil-square-o', className: 'wiki' - })); - } + buildManageButtons(this.attrs, this.currentUser).forEach(b => { + contents.push(this.attach('post-admin-menu-button', b)); + }); return contents; }, diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 index deda3b167a..80ab77838e 100644 --- a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 @@ -1,4 +1,4 @@ -import { createWidget } from 'discourse/widgets/widget'; +import { applyDecorators, createWidget } from 'discourse/widgets/widget'; import { avatarAtts } from 'discourse/widgets/actions-summary'; import { h } from 'virtual-dom'; @@ -29,16 +29,29 @@ function registerButton(name, builder) { _builders[name] = builder; } +export function buildButton(name, widget) { + let { attrs, state, siteSettings } = widget; + let builder = _builders[name]; + if (builder) { + let button = builder(attrs, state, siteSettings); + if (button && !button.id) { + button.id = name; + } + return button; + } +} + registerButton('like', attrs => { if (!attrs.showLike) { return; } const className = attrs.liked ? 'toggle-like has-like fade-out' : 'toggle-like like'; const button = { action: 'like', - icon: 'heart', + icon: attrs.liked ? 'd-liked' : 'd-unliked', className }; + if (attrs.canToggleLike) { button.title = attrs.liked ? 'post.controls.undo_like' : 'post.controls.like'; } else if (attrs.liked) { @@ -180,6 +193,7 @@ registerButton('bookmark', attrs => { } return { + id: attrs.bookmarked ? 'bookmark' : 'unbookmark', action: 'toggleBookmark', title: attrs.bookmarked ? "bookmarks.created" : "bookmarks.not_bookmarked", className, @@ -197,13 +211,13 @@ registerButton('admin', attrs => { registerButton('delete', attrs => { if (attrs.canRecoverTopic) { - return { action: 'recoverPost', title: 'topic.actions.recover', icon: 'undo', className: 'recover' }; + return { id: 'recover_topic', action: 'recoverPost', title: 'topic.actions.recover', icon: 'undo', className: 'recover' }; } else if (attrs.canDeleteTopic) { - return { action: 'deletePost', title: 'topic.actions.delete', icon: 'trash-o', className: 'delete' }; + return { id: 'delete_topic', action: 'deletePost', title: 'topic.actions.delete', icon: 'trash-o', className: 'delete' }; } else if (attrs.canRecover) { - return { action: 'recoverPost', title: 'post.controls.undelete', icon: 'undo', className: 'recover' }; + return { id: 'recover', action: 'recoverPost', title: 'post.controls.undelete', icon: 'undo', className: 'recover' }; } else if (attrs.canDelete) { - return { action: 'deletePost', title: 'post.controls.delete', icon: 'trash-o', className: 'delete' }; + return { id: 'delete', action: 'deletePost', title: 'post.controls.delete', icon: 'trash-o', className: 'delete' }; } }); @@ -228,16 +242,18 @@ export default createWidget('post-menu', { buildKey: attrs => `post-menu-${attrs.id}`, - attachButton(name, attrs) { - const builder = _builders[name]; - if (builder) { - const buttonAtts = builder(attrs, this.state, this.siteSettings); - if (buttonAtts) { - return this.attach(this.settings.buttonType, buttonAtts); - } + attachButton(name) { + let buttonAtts = buildButton(name, this); + if (buttonAtts) { + return this.attach(this.settings.buttonType, buttonAtts); } }, + menuItems() { + let result = this.siteSettings.post_menu.split('|'); + return result; + }, + html(attrs, state) { const { siteSettings } = this; @@ -249,7 +265,7 @@ export default createWidget('post-menu', { const allButtons = []; let visibleButtons = []; - const orderedButtons = siteSettings.post_menu.split('|'); + const orderedButtons = this.menuItems(); // If the post is a wiki, make Edit more prominent if (attrs.wiki) { @@ -329,7 +345,8 @@ export default createWidget('post-menu', { postControls.push(repliesButton); } - postControls.push(h('div.actions', visibleButtons)); + let extraControls = applyDecorators(this, 'extra-controls', attrs, state); + postControls.push(h('div.actions', visibleButtons.concat(extraControls))); if (state.adminVisible) { postControls.push(this.attach('post-admin-menu', attrs)); } @@ -368,7 +385,7 @@ export default createWidget('post-menu', { return this.sendWidgetAction('toggleLike'); } - const $heart = $(`[data-post-id=${attrs.id}] .d-icon-heart`); + const $heart = $(`[data-post-id=${attrs.id}] .toggle-like .d-icon`); $heart.closest('button').addClass('has-like'); if (!Ember.testing) { diff --git a/app/assets/javascripts/discourse/widgets/raw-html.js.es6 b/app/assets/javascripts/discourse/widgets/raw-html.js.es6 index e009d3caf4..c532946207 100644 --- a/app/assets/javascripts/discourse/widgets/raw-html.js.es6 +++ b/app/assets/javascripts/discourse/widgets/raw-html.js.es6 @@ -20,3 +20,8 @@ export default class RawHtml { } RawHtml.prototype.type = 'Widget'; + +// TODO: Improve how helpers are registered for vdom compliation +if (typeof Discourse !== "undefined") { + Discourse.__widget_helpers.rawHtml = RawHtml; +} diff --git a/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 b/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 index 212d549d42..003de97cea 100644 --- a/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 @@ -177,7 +177,7 @@ export default createWidget('topic-admin-menu', { icon: visible ? 'eye-slash' : 'eye', label: visible ? 'actions.invisible' : 'actions.visible' }); - if (this.currentUser.get('staff')) { + if (details.get('can_convert_topic')) { buttons.push({ className: 'topic-admin-convert', action: isPrivateMessage ? 'convertToPublicTopic' : 'convertToPrivateMessage', icon: isPrivateMessage ? 'comment' : 'envelope', diff --git a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 index db8207c688..6ba446b8be 100644 --- a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 @@ -89,6 +89,10 @@ createWidget('user-menu-links', { export default createWidget('user-menu', { tagName: 'div.user-menu', + settings: { + maxWidth: 300 + }, + panelContents() { const path = this.currentUser.get('path'); @@ -104,7 +108,10 @@ export default createWidget('user-menu', { }, html() { - return this.attach('menu-panel', { contents: () => this.panelContents() }); + return this.attach('menu-panel', { + maxWidth: this.settings.maxWidth, + contents: () => this.panelContents() + }); }, clickOutside() { diff --git a/app/assets/javascripts/discourse/widgets/widget.js.es6 b/app/assets/javascripts/discourse/widgets/widget.js.es6 index 339549c3b4..d81b2b5395 100644 --- a/app/assets/javascripts/discourse/widgets/widget.js.es6 +++ b/app/assets/javascripts/discourse/widgets/widget.js.es6 @@ -91,6 +91,8 @@ function drawWidget(builder, attrs, state) { } } + this.transformed = this.transform(this.attrs, this.state); + let contents = this.html(attrs, state); if (this.name) { const beforeContents = applyDecorators(this, 'before', attrs, state) || []; @@ -173,6 +175,10 @@ export default class Widget { } } + transform() { + return {}; + } + defaultState() { return {}; } diff --git a/app/assets/stylesheets/common/admin/flagging.scss b/app/assets/stylesheets/common/admin/flagging.scss index f16de2bd97..b2a416691b 100644 --- a/app/assets/stylesheets/common/admin/flagging.scss +++ b/app/assets/stylesheets/common/admin/flagging.scss @@ -194,21 +194,9 @@ .mobile-view { .flagged-posts { - .flagged-post-details { - flex-wrap: wrap; - justify-content: flex-start; - - .flagged-post-avatar { - margin-right: 10px; - } - - .flagged-post-excerpt { - width: 70%; - } - - .flaggers { - margin-left: 4em; - margin-bottom: 1em; + .flagged-post { + .flag-user-lists { + display: block; } } } diff --git a/app/assets/stylesheets/common/base/edit-topic-status-update-modal.scss b/app/assets/stylesheets/common/base/edit-topic-status-update-modal.scss index 1c17981d7b..3a9424ca47 100644 --- a/app/assets/stylesheets/common/base/edit-topic-status-update-modal.scss +++ b/app/assets/stylesheets/common/base/edit-topic-status-update-modal.scss @@ -8,8 +8,18 @@ text-align: left; } + .radios { + margin-bottom: 10px; + } + label { + vertical-align: middle; display: inline-block; + padding-right: 5px; + + input { + vertical-align: middle; + } } .btn.pull-right { diff --git a/app/assets/stylesheets/desktop/header.scss b/app/assets/stylesheets/desktop/header.scss index 4c4d3da302..c401a76594 100644 --- a/app/assets/stylesheets/desktop/header.scss +++ b/app/assets/stylesheets/desktop/header.scss @@ -4,7 +4,6 @@ .d-header { left: 0; - z-index: 1000; padding-top: 3px; height: 60px; .d-icon-home { diff --git a/app/assets/stylesheets/desktop/topic.scss b/app/assets/stylesheets/desktop/topic.scss index 8448400be7..98f7ec65fd 100644 --- a/app/assets/stylesheets/desktop/topic.scss +++ b/app/assets/stylesheets/desktop/topic.scss @@ -69,8 +69,8 @@ } .topic-status-info { - border-top: 1px solid $primary-low; - padding-top: 10px; + border-top: 1px solid $primary-low; + padding: 10px 0px; height: 20px; max-width: 757px; } diff --git a/app/controllers/admin/email_templates_controller.rb b/app/controllers/admin/email_templates_controller.rb index 4c1df06239..a29aa0a5ed 100644 --- a/app/controllers/admin/email_templates_controller.rb +++ b/app/controllers/admin/email_templates_controller.rb @@ -47,11 +47,9 @@ class Admin::EmailTemplatesController < Admin::AdminController error_messages = [] if subject_result[:error_messages].present? - TranslationOverride.upsert!(I18n.locale, "#{key}.subject_template", subject_result[:old_value]) error_messages << format_error_message(subject_result, "subject") end if body_result[:error_messages].present? - TranslationOverride.upsert!(I18n.locale, "#{key}.text_body_template", body_result[:old_value]) error_messages << format_error_message(body_result, "body") end @@ -61,6 +59,9 @@ class Admin::EmailTemplatesController < Admin::AdminController render_serialized(key, AdminEmailTemplateSerializer, root: 'email_template', rest_serializer: true) else + TranslationOverride.upsert!(I18n.locale, "#{key}.subject_template", subject_result[:old_value]) + TranslationOverride.upsert!(I18n.locale, "#{key}.text_body_template", body_result[:old_value]) + render_json_error(error_messages) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5f5537ae3b..cce9eb95fd 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -167,6 +167,12 @@ class ApplicationController < ActionController::Base render_json_error I18n.t(opts[:custom_message] || type), type: type, status: status_code else + begin + current_user + rescue Discourse::InvalidAccess + return render plain: I18n.t(type), status: status_code + end + render html: build_not_found_page(status_code, opts[:include_ember] ? 'application' : 'no_ember') end end diff --git a/app/controllers/export_csv_controller.rb b/app/controllers/export_csv_controller.rb index 0053837c00..4b74781dbd 100644 --- a/app/controllers/export_csv_controller.rb +++ b/app/controllers/export_csv_controller.rb @@ -29,7 +29,7 @@ class ExportCsvController < ApplicationController def export_params @_export_params ||= begin params.require(:entity) - params.permit(:entity, args: [:name, :start_date, :end_date, :category_id, :group_id, :trust_level]) + params.permit(:entity, args: [:name, :start_date, :end_date, :category_id, :group_id, :trust_level]).to_h end end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 7691f99cab..2785e69312 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -125,15 +125,17 @@ class GroupsController < ApplicationController order = "#{params[:order]} #{dir} NULLS LAST" end - total = group.users.count - members = group.users + users = group.users.human_users + + total = users.count + members = users .order('NOT group_users.owner') .order(order) .order(username_lower: dir) .limit(limit) .offset(offset) - owners = group.users + owners = users .order(order) .order(username_lower: dir) .where('group_users.owner') diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 152e23392f..2866ff2dcb 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -31,7 +31,7 @@ class InvitesController < ApplicationController def perform_accept_invitation params.require(:id) - params.permit(:username, :name, :password, :user_custom_fields) + params.permit(:username, :name, :password, user_custom_fields: {}) invite = Invite.find_by(invite_key: params[:id]) if invite.present? diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index 1a77a589e4..02b3cdd3e5 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -331,6 +331,8 @@ class ListController < ApplicationController def build_topic_list_options options = {} + params[:page] = params[:page].to_i rescue 1 + TopicQuery.public_valid_options.each do |key| options[key] = params[key] end diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index 1060c6e034..c67e6baf34 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -247,7 +247,7 @@ class SessionController < ApplicationController end json = { result: "ok" } - unless SiteSetting.forgot_password_strict + unless SiteSetting.hide_email_address_taken json[:user_found] = user_presence end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 75ce09efa7..54115945e0 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -100,8 +100,11 @@ class TagsController < ::ApplicationController def destroy guardian.ensure_can_admin_tags! tag_name = params[:tag_id] + tag = Tag.find_by_name(tag_name) + raise Discourse::NotFound if tag.nil? + TopicCustomField.transaction do - Tag.find_by_name(tag_name).destroy + tag.destroy StaffActionLogger.new(current_user).log_custom('deleted_tag', subject: tag_name) end render json: success_json diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 87f6332ab5..ca3dba2426 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -372,6 +372,19 @@ class UsersController < ApplicationController message: activation.message, user_id: user.id } + elsif SiteSetting.hide_email_address_taken && user.errors[:primary_email]&.include?(I18n.t('errors.messages.taken')) + session["user_created_message"] = activation.success_message + + if existing_user = User.find_by_email(user.primary_email&.email) + Jobs.enqueue(:critical_user_email, type: :account_exists, user_id: existing_user.id) + end + + render json: { + success: true, + active: user.active?, + message: activation.success_message, + user_id: user.id + } else errors = user.errors.to_hash errors[:email] = errors.delete(:primary_email) if errors[:primary_email] @@ -698,11 +711,17 @@ class UsersController < ApplicationController end end - if params[:include_mentionable_groups] == "true" && current_user - to_render[:groups] = Group.mentionable(current_user) - .where("name ILIKE :term_like", term_like: "#{term}%") - .map do |m| - { name: m.name, full_name: m.full_name } + if current_user + groups = + if params[:include_mentionable_groups] == 'true' + Group.mentionable(current_user) + elsif params[:include_messageable_groups] == 'true' + Group.messageable(current_user) + end + + if groups + to_render[:groups] = groups.where("name ILIKE :term_like", term_like: "#{term}%") + .map { |m| { name: m.name, full_name: m.full_name } } end end diff --git a/app/jobs/onceoff/fix_primary_emails_for_staged_users.rb b/app/jobs/onceoff/fix_primary_emails_for_staged_users.rb index 15fe43a14b..36a55c64d5 100644 --- a/app/jobs/onceoff/fix_primary_emails_for_staged_users.rb +++ b/app/jobs/onceoff/fix_primary_emails_for_staged_users.rb @@ -1,3 +1,5 @@ +require_dependency 'user_destroyer' + module Jobs class FixPrimaryEmailsForStagedUsers < Jobs::Onceoff def execute_onceoff(args) diff --git a/app/jobs/regular/crawl_topic_link.rb b/app/jobs/regular/crawl_topic_link.rb index b5077aff85..15c0c01ad0 100644 --- a/app/jobs/regular/crawl_topic_link.rb +++ b/app/jobs/regular/crawl_topic_link.rb @@ -2,6 +2,7 @@ require 'open-uri' require 'nokogiri' require 'excon' require_dependency 'retrieve_title' +require_dependency 'topic_link' module Jobs class CrawlTopicLink < Jobs::Base diff --git a/app/jobs/regular/create_avatar_thumbnails.rb b/app/jobs/regular/create_avatar_thumbnails.rb index 046f50bfe1..ef081cee95 100644 --- a/app/jobs/regular/create_avatar_thumbnails.rb +++ b/app/jobs/regular/create_avatar_thumbnails.rb @@ -9,7 +9,7 @@ module Jobs raise Discourse::InvalidParameters.new(:upload_id) if upload_id.blank? return unless upload = Upload.find_by(id: upload_id) - return unless user = User.find(args[:user_id] || upload.user_id) + return unless user = User.find_by(id: args[:user_id] || upload.user_id) Discourse.avatar_sizes.each do |size| OptimizedImage.create_for(upload, size, size, filename: upload.original_filename, allow_animation: SiteSetting.allow_animated_avatars) diff --git a/app/jobs/regular/download_avatar_from_url.rb b/app/jobs/regular/download_avatar_from_url.rb index 83e687fed9..f4eabdf951 100644 --- a/app/jobs/regular/download_avatar_from_url.rb +++ b/app/jobs/regular/download_avatar_from_url.rb @@ -12,7 +12,15 @@ module Jobs return unless user = User.find_by(id: user_id) - UserAvatar.import_url_for_user(url, user, override_gravatar: args[:override_gravatar]) + begin + UserAvatar.import_url_for_user( + '/assets/vorablesen/placeholder-user-ed74bdf68223d030da1b7ddc44f59faf9c5a184388c94aff91632d5bf166a9e5.png', + user, + override_gravatar: args[:override_gravatar] + ) + rescue Discourse::InvalidParameters => e + raise e unless e.message == 'url' + end end end diff --git a/app/jobs/regular/emit_web_hook_event.rb b/app/jobs/regular/emit_web_hook_event.rb index 341702904a..305c10d7e0 100644 --- a/app/jobs/regular/emit_web_hook_event.rb +++ b/app/jobs/regular/emit_web_hook_event.rb @@ -8,7 +8,7 @@ module Jobs end web_hook = WebHook.find_by(id: args[:web_hook_id]) - raise Discourse::InvalidParameters(:web_hook_id) if web_hook.blank? + raise Discourse::InvalidParameters.new(:web_hook_id) if web_hook.blank? unless ping_event?(args[:event_type]) return unless web_hook.active? diff --git a/app/jobs/regular/notify_mailing_list_subscribers.rb b/app/jobs/regular/notify_mailing_list_subscribers.rb index 6b8647a36b..015cb65fdc 100644 --- a/app/jobs/regular/notify_mailing_list_subscribers.rb +++ b/app/jobs/regular/notify_mailing_list_subscribers.rb @@ -1,3 +1,5 @@ +require_dependency 'post' + module Jobs class NotifyMailingListSubscribers < Jobs::Base diff --git a/app/jobs/regular/update_top_redirection.rb b/app/jobs/regular/update_top_redirection.rb index 776cdfca7d..592dc6b840 100644 --- a/app/jobs/regular/update_top_redirection.rb +++ b/app/jobs/regular/update_top_redirection.rb @@ -3,9 +3,12 @@ module Jobs class UpdateTopRedirection < Jobs::Base def execute(args) - if user = User.find_by(id: args[:user_id]) - user.user_option.update_column(:last_redirected_to_top_at, args[:redirected_at]) - end + return if args[:user_id].blank? || args[:redirected_at].blank? + + UserOption + .where(user_id: args[:user_id]) + .limit(1) + .update_all(last_redirected_to_top_at: args[:redirected_at]) end end diff --git a/app/jobs/scheduled/dashboard_stats.rb b/app/jobs/scheduled/dashboard_stats.rb index 75977e7acb..9556bd7ea1 100644 --- a/app/jobs/scheduled/dashboard_stats.rb +++ b/app/jobs/scheduled/dashboard_stats.rb @@ -1,3 +1,7 @@ +require_dependency 'admin_dashboard_data' +require_dependency 'group' +require_dependency 'group_message' + module Jobs class DashboardStats < Jobs::Scheduled every 30.minutes diff --git a/app/jobs/scheduled/poll_feed.rb b/app/jobs/scheduled/poll_feed.rb index fcd0243aae..798cbff789 100644 --- a/app/jobs/scheduled/poll_feed.rb +++ b/app/jobs/scheduled/poll_feed.rb @@ -101,7 +101,9 @@ module Jobs end def content - @article_rss_item.content.try(:force_encoding, "UTF-8").try(:scrub) || @article_rss_item.description.try(:force_encoding, "UTF-8").try(:scrub) + @article_rss_item.content_encoded&.force_encoding("UTF-8")&.scrub || + @article_rss_item.content&.force_encoding("UTF-8")&.scrub || + @article_rss_item.description&.force_encoding("UTF-8")&.scrub end def title diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 83ba60ad08..e1d24b37c6 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -83,6 +83,15 @@ class UserNotifications < ActionMailer::Base ) end + def account_exists(user, opts = {}) + build_email( + user.email, + template: 'user_notifications.account_exists', + locale: user_locale(user), + email: user.email + ) + end + def short_date(dt) if dt.year == Time.now.year I18n.l(dt, format: :short_no_year) diff --git a/app/models/category.rb b/app/models/category.rb index 1b9032b8d4..54f59af722 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -108,10 +108,6 @@ class Category < ActiveRecord::Base Category.reset_topic_ids_cache end - def self.last_updated_at - order('updated_at desc').limit(1).pluck(:updated_at).first.to_i - end - def self.scoped_to_permissions(guardian, permission_types) if guardian.try(:is_admin?) all @@ -564,5 +560,5 @@ end # # index_categories_on_email_in (email_in) UNIQUE # index_categories_on_topic_count (topic_count) -# unique_index_categories_on_name (name) UNIQUE +# unique_index_categories_on_name ((COALESCE(parent_category_id, '-1'::integer)), name) UNIQUE # diff --git a/app/models/color_scheme.rb b/app/models/color_scheme.rb index c0a01cc5d5..0e1f09c12c 100644 --- a/app/models/color_scheme.rb +++ b/app/models/color_scheme.rb @@ -9,7 +9,7 @@ class ColorScheme < ActiveRecord::Base "tertiary" => '0f82af', "quaternary" => 'c14924', "header_background" => '111111', - "header_primary" => '333333', + "header_primary" => 'dddddd', "highlight" => 'a87137', "danger" => 'e45735', "success" => '1ca551', diff --git a/app/models/global_setting.rb b/app/models/global_setting.rb index e141e151e1..9c5e67cccf 100644 --- a/app/models/global_setting.rb +++ b/app/models/global_setting.rb @@ -118,13 +118,6 @@ class GlobalSetting c[:db] = redis_db if redis_db != 0 c[:db] = 1 if Rails.env == "test" - if redis_sentinels.present? - c[:sentinels] = redis_sentinels.split(",").map do |address| - host, port = address.split(":") - { host: host, port: port } - end.to_a - end - c.freeze end end diff --git a/app/models/group.rb b/app/models/group.rb index 491d9d3daa..4083346854 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -307,6 +307,7 @@ class Group < ActiveRecord::Base def self.ensure_consistency! reset_all_counters! refresh_automatic_groups! + refresh_has_messages! end def self.reset_all_counters! @@ -330,6 +331,18 @@ class Group < ActiveRecord::Base args.each { |group| refresh_automatic_group!(group) } end + def self.refresh_has_messages! + exec_sql <<-SQL + UPDATE groups g SET has_messages = false + WHERE NOT EXISTS (SELECT tg.id + FROM topic_allowed_groups tg + INNER JOIN topics t ON t.id = tg.topic_id + WHERE tg.group_id = g.id + AND t.deleted_at IS NULL) + AND g.has_messages = true + SQL + end + def self.ensure_automatic_groups! AUTO_GROUPS.each_key do |name| refresh_automatic_group!(name) unless lookup_group(name) diff --git a/app/models/post.rb b/app/models/post.rb index 992e3a83a9..a70c69a915 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -91,6 +91,16 @@ class Post < ActiveRecord::Base q.order('posts.created_at ASC') } + scope :raw_match, -> (pattern, type = 'string') { + type = type&.downcase + + case type + when 'string' + where('raw ILIKE ?', "%#{pattern}%") + when 'regex' + where('raw ~ ?', pattern) + end + } delegate :username, to: :user diff --git a/app/models/post_custom_field.rb b/app/models/post_custom_field.rb index 713d0affbc..999500de52 100644 --- a/app/models/post_custom_field.rb +++ b/app/models/post_custom_field.rb @@ -15,6 +15,6 @@ end # # Indexes # -# index_post_custom_fields_on_name_and_value (name) +# index_post_custom_fields_on_name_and_value (name, "left"(value, 200)) # index_post_custom_fields_on_post_id_and_name (post_id,name) # diff --git a/app/models/topic.rb b/app/models/topic.rb index 86feb633a3..f4093cb8f5 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -411,6 +411,7 @@ class Topic < ActiveRecord::Base def reload(options = nil) @post_numbers = nil @public_topic_timer = nil + @private_topic_timer = nil super(options) end @@ -1002,6 +1003,10 @@ SQL @public_topic_timer ||= topic_timers.find_by(deleted_at: nil, public_type: true) end + def private_topic_timer(user) + @private_topic_Timer ||= topic_timers.find_by(deleted_at: nil, public_type: false, user_id: user.id) + end + def delete_topic_timer(status_type, by_user: Discourse.system_user) options = { status_type: status_type } options.merge!(user: by_user) unless TopicTimer.public_types[status_type] @@ -1022,8 +1027,9 @@ SQL def set_or_create_timer(status_type, time, by_user: nil, timezone_offset: 0, based_on_last_post: false, category_id: SiteSetting.uncategorized_category_id) return delete_topic_timer(status_type, by_user: by_user) if time.blank? - topic_timer_options = { topic: self } - topic_timer_options.merge!(user: by_user) unless TopicTimer.public_types[status_type] + public_topic_timer = !!TopicTimer.public_types[status_type] + topic_timer_options = { topic: self, public_type: public_topic_timer } + topic_timer_options.merge!(user: by_user) unless public_topic_timer topic_timer = TopicTimer.find_or_initialize_by(topic_timer_options) topic_timer.status_type = status_type @@ -1330,6 +1336,7 @@ end # index_topics_on_bumped_at (bumped_at) # index_topics_on_created_at_and_visible (created_at,visible) # index_topics_on_id_and_deleted_at (id,deleted_at) +# index_topics_on_lower_title (lower((title)::text)) # index_topics_on_pinned_at (pinned_at) # index_topics_on_pinned_globally (pinned_globally) # diff --git a/app/models/topic_user.rb b/app/models/topic_user.rb index 3404fc90d4..a141de281d 100644 --- a/app/models/topic_user.rb +++ b/app/models/topic_user.rb @@ -38,26 +38,19 @@ class TopicUser < ActiveRecord::Base end def auto_notification(user_id, topic_id, reason, notification_level) - if TopicUser.where("user_id = :user_id AND topic_id = :topic_id AND (notifications_reason_id IS NULL OR - (notification_level < :notification_level AND notification_level > :normal_notification_level))", - user_id: user_id, topic_id: topic_id, notification_level: notification_level, - normal_notification_level: notification_levels[:regular]).exists? - change(user_id, topic_id, - notification_level: notification_level, - notifications_reason_id: reason - ) - end + should_change = TopicUser + .where(user_id: user_id, topic_id: topic_id) + .where("notifications_reason_id IS NULL OR (notification_level < :min AND notification_level > :max)", min: notification_level, max: notification_levels[:regular]) + .exists? + + change(user_id, topic_id, notification_level: notification_level, notifications_reason_id: reason) if should_change end - def auto_notification_for_staging(user_id, topic_id, reason) - topic_user = TopicUser.find_or_initialize_by(user_id: user_id, topic_id: topic_id) - topic_user.notification_level = notification_levels[:watching] - topic_user.notifications_reason_id = reason - topic_user.save + def auto_notification_for_staging(user_id, topic_id, reason, notification_level = notification_levels[:watching]) + change(user_id, topic_id, notification_level: notification_level, notifications_reason_id: reason) end def unwatch_categories!(user, category_ids) - track_threshold = user.user_option.auto_track_topics_after_msecs sql = < 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 + return unless SiteSetting.top_menu[/\btop\b/i] # not enough topics - return unless period = SiteSetting.min_redirected_to_top_period(1.days.ago) + return unless period = SiteSetting.min_redirected_to_top_period(1.day.ago) if !user.seen_before? || (user.trust_level == 0 && !redirected_to_top_yet?) update_last_redirected_to_top! diff --git a/app/serializers/group_show_serializer.rb b/app/serializers/group_show_serializer.rb index 933111b71c..e97d49e9eb 100644 --- a/app/serializers/group_show_serializer.rb +++ b/app/serializers/group_show_serializer.rb @@ -1,5 +1,5 @@ class GroupShowSerializer < BasicGroupSerializer - attributes :is_group_user, :is_group_owner, :mentionable + attributes :is_group_user, :is_group_owner, :mentionable, :messageable def include_is_group_user? authenticated? @@ -21,6 +21,10 @@ class GroupShowSerializer < BasicGroupSerializer authenticated? end + def include_messageable? + authenticated? + end + def mentionable Group.mentionable(scope.user).exists?(id: object.id) end diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index ab1068e4a5..df70f3592b 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -61,6 +61,7 @@ class TopicViewSerializer < ApplicationSerializer :message_archived, :tags, :topic_timer, + :private_topic_timer, :unicode_title, :message_bus_last_id, :participant_count @@ -118,6 +119,7 @@ class TopicViewSerializer < ApplicationSerializer result[:can_create_post] = true if scope.can_create?(Post, object.topic) result[:can_reply_as_new_topic] = true if scope.can_reply_as_new_topic?(object.topic) result[:can_flag_topic] = actions_summary.any? { |a| a[:can_act] } + result[:can_convert_topic] = true if scope.can_convert_topic?(object.topic) result end @@ -241,6 +243,15 @@ class TopicViewSerializer < ApplicationSerializer TopicTimerSerializer.new(object.topic.public_topic_timer, root: false) end + def include_private_topic_timer? + scope.user + end + + def private_topic_timer + timer = object.topic.private_topic_timer(scope.user) + TopicTimerSerializer.new(timer, root: false) + end + def tags object.topic.tags.map(&:name) end diff --git a/app/services/post_alerter.rb b/app/services/post_alerter.rb index 40078708b8..56ed07fddd 100644 --- a/app/services/post_alerter.rb +++ b/app/services/post_alerter.rb @@ -93,7 +93,7 @@ class PostAlerter DiscourseEvent.trigger(:before_create_notifications_for_users, users, post) users.each do |user| notification_level = TopicUser.get(post.topic, user).try(:notification_level) - if notified.include?(user) || notification_level == TopicUser.notification_levels[:watching] + if notified.include?(user) || notification_level == TopicUser.notification_levels[:watching] || user.staged? create_notification(user, Notification.types[:private_message], post) end end diff --git a/app/services/user_activator.rb b/app/services/user_activator.rb index d0c380db95..d65fba83c2 100644 --- a/app/services/user_activator.rb +++ b/app/services/user_activator.rb @@ -16,6 +16,10 @@ class UserActivator @message = activator.activate end + def success_message + activator.success_message + end + private def activator @@ -38,6 +42,10 @@ end class ApprovalActivator < UserActivator def activate + success_message + end + + def success_message I18n.t("login.wait_approval") end end @@ -52,6 +60,11 @@ class EmailActivator < UserActivator user_id: user.id, email_token: email_token.token ) + + success_message + end + + def success_message I18n.t("login.activate_email", email: Rack::Utils.escape_html(user.email)) end end @@ -62,6 +75,10 @@ class LoginActivator < UserActivator def activate log_on_user(user) user.enqueue_welcome_message('welcome_user') + success_message + end + + def success_message I18n.t("login.active") end end diff --git a/app/views/list/list.erb b/app/views/list/list.erb index d8679ae7bd..7d1179c746 100644 --- a/app/views/list/list.erb +++ b/app/views/list/list.erb @@ -104,6 +104,6 @@ <% content_for :title do %><%= @title %><% end %> <% elsif @category %> <% content_for :title do %><%= @category.name %> - <%= SiteSetting.title %><% end %> -<% elsif params[:page] %> +<% elsif params[:page].to_i > 1 %> <% content_for :title do %><%=t 'page_num', num: params[:page].to_i + 1 %> - <%= SiteSetting.title %><% end %> <% end %> diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index c8ce6b7cd5..46e883ccb2 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -2029,25 +2029,25 @@ ar: like: "أعجبهم هذا" vote: "صوت لهذا" by_you: - off_topic: "لقد ابلغت ان المنشور خارج الموضوع" - spam: "لقد ابلغت ان المنشور سبام" - inappropriate: "لقد ابلغت ان المنشور غير لائق" - notify_moderators: "لقد ابلغت المشرفين" + off_topic: "لقد ابلغت ان هذا المنشور خارج الموضوع" + spam: "لقد ابلغت ان هذا المنشور سبام" + inappropriate: "لقد ابلغت ان هذا المنشور غير لائق" + notify_moderators: "لقد ابلغت المشرفين عن هذا المنشور" notify_user: "لقد أرسلت رسالة إلى هذا العضو" - bookmark: "لقد علّمت هذا المنشور" + bookmark: "لقد وضعت علامة مرجعية علي هذا المنشور" like: "أعجبت بهذا المنشور" vote: "لقد صوّت على هذا المنشور" by_you_and_others: off_topic: zero: "أنت أبلّغت بأن هذا المنشور خارج الموضوع." - one: "أنت وآخر أبلّغتما بأن هذا المنشور خارج الموضوع." + one: "أنت وشخص اخر أبلّغتما بأن هذا المنشور خارج الموضوع." two: "أنت و {{count}} آخرون أبلغتم بأن هذا المنشور خارج الموضوع" few: "أنت و {{count}} آخرون أبلغتم بأن هذا المنشور خارج الموضوع" many: "أنت و {{count}} آخرون أبلغتم بأن هذا المنشور خارج الموضوع" other: "أنت و {{count}} آخرون أبلغتم بأن هذا المنشور خارج الموضوع" spam: zero: "أنت أبلّغت بأن هذا هذا المنشور سبام." - one: "أنت وآخر أبلّغتما بأن هذا المنشور سبام." + one: "أنت وشخص آخر أبلّغتما بأن هذا المنشور سبام." two: "أنت و {{count}} آخرون أبلّغتم بأن هذا المنشور سبام." few: "أنت و {{count}} آخرون أبلّغتم بأن هذا المنشور سبام." many: "أنت و {{count}} آخرون أبلّغتم بأن هذا المنشور سبام." @@ -2060,12 +2060,12 @@ ar: many: "أنت و {{count}} آخرون أبلّغتم بأن هذا المنشور غير لائق." other: "أنت و {{count}} آخرون أبلّغتم بأن هذا المنشور غير لائق." notify_moderators: - zero: "أنت أبلّغت المشرفين." - one: "أنت و شخص آخر أبلّغتما المشرفين." - two: "أنت و {{count}} آخرون أبلّغتم المشرفين." - few: "أنت و {{count}} آخرون أبلّغتم المشرفين." - many: "أنت و {{count}} آخرون أبلّغتم المشرفين." - other: "أنت و {{count}} آخرون أبلّغتم المشرفين." + zero: "أنت أبلّغت المشرفين عن هذا المنشور." + one: "أنت و شخص آخر أبلّغتما المشرفين عن هذا المنشور." + two: "أنت و {{count}} آخرون أبلّغتم المشرفين عن هذا المنشور." + few: "أنت و {{count}} آخرون أبلّغتم المشرفين عن هذا المنشور." + many: "أنت و {{count}} آخرون أبلّغتم المشرفين عن هذا المنشور." + other: "أنت و {{count}} آخرون أبلّغتم المشرفين عن هذا المنشور." notify_user: zero: "أنت أرسلت رسالة لهذا العضو." one: "أنت و شخص اخر ارسلتما رسالة لهذا العضو." @@ -2074,91 +2074,99 @@ ar: many: "أنت و {{count}} آخرون ارسلتم رسالة لهذا العضو." other: "أنت و {{count}} آخرون ارسلتم رسالة لهذا العضو." bookmark: - zero: "أنت عَلَّمتَ هذه المشاركة." - one: "أنت و شخص آخر عَلَّمتُما هذه المشاركة." - two: "أنت و {{count}} آخران عَلَّمتُم هذه المشاركة." - few: "أنت و {{count}} آخرون عَلَّمتُم هذه المشاركة." - many: "أنت و {{count}} آخرون عَلَّمتُم هذه المشاركة." - other: "أنت و {{count}} آخرون عَلَّمتُم هذه المشاركة." + zero: "أنت وضعت علامة مرجعية علي هذا المنشور" + one: "أنت و شخص آخر وضعتما علامة مرجعية علي هذا المنشور" + two: "أنت و {{count}} آخرون وضعتم علامة مرجعية علي هذا المنشور" + few: "أنت و {{count}} آخرون وضعتم علامة مرجعية علي هذا المنشور" + many: "أنت و {{count}} آخرون وضعتم علامة مرجعية علي هذا المنشور" + other: "أنت و {{count}} آخرون وضعتم علامة مرجعية علي هذا المنشور" like: - zero: "أعجبك هذا" - one: "أعجب هذا شخصا واحدا غيرك" - two: "أعجب هذا شخصين غيرك" - few: "أعجب هذا {{count}} أشخاص غيرك" - many: "أعجب هذا {{count}} شخصا غيرك" - other: "أعجب هذا {{count}} شخص غيرك" + zero: "أنت اعجبت بهذا المنشور" + one: "أنت و شخص آخر اعجبتما بهذا المنشور" + two: "أنت و {{count}} آخرون اعجبتم بهذا المنشور" + few: "أنت و {{count}} آخرون اعجبتم بهذا المنشور" + many: "أنت و {{count}} آخرون اعجبتم بهذا المنشور" + other: "أنت و {{count}} آخرون اعجبتم بهذا المنشور" vote: - zero: "أنت و {{count}} أشخاص أخرين صوتو لهذا الموضوع" - one: "أنت و {{count}} أشخاص أخرين صوتو لهذا الموضوع" - two: "أنت و {{count}} أشخاص أخرين صوتو لهذا الموضوع" - few: "أنت و {{count}} أشخاص أخرين صوتو لهذا الموضوع" - many: "أنت و {{count}} أشخاص أخرين صوتو لهذا الموضوع" - other: "أنت و {{count}} أشخاص أخرين صوتو لهذا الموضوع" + zero: "أنت صوت علي هذا المنشور" + one: "أنت و شخص آخر صوتما علي هذا المنشور" + two: "أنت و {{count}} آخرون صوتم علي هذا المنشور" + few: "أنت و {{count}} آخرون صوتم علي هذا المنشور" + many: "أنت و {{count}} آخرون صوتم علي هذا المنشور" + other: "أنت و {{count}} آخرون صوتم علي هذا المنشور" by_others: off_topic: - zero: "لم يتم الاشارة لهذا كخارج عن الموضوع." - one: "شخص أشار لهذا كخارج عن الموضوع." - two: "شخصان أشارا لهذا كخارج عن الموضوع." - few: "{{count}} أشخاص أشاروا لهذا كخارج عن الموضوع." - many: "{{count}} شخص أشار لهذا كخارج عن الموضوع." - other: "{{count}} شخص أشار لهذا كخارج عن الموضوع." + zero: "لم يبلغ احد بأن هذا المنشور خارج الموضوع" + one: "عضو واحد أبلغ بأن هذا المنشور خارج الموضوع" + two: "{{count}} عضو أبلغ بأن هذا المنشور خارج الموضوع" + few: "{{count}} عضو أبلغ بأن هذا المنشور خارج الموضوع" + many: "{{count}} عضو أبلغ بأن هذا المنشور خارج الموضوع" + other: "{{count}} عضو أبلغ بأن هذا المنشور خارج الموضوع" spam: - zero: "لم يتم الاشارة لهذا كغير مفيد," - one: "شخص أشار لهذا كغير مفيد." - two: "شخصان أشارا لهذا كغير مفيد." - few: "{{count}} أشخاص أشاروا لهذا كغير مفيد." - many: "{{count}} شخص أشار لهذا كغير مفيد." - other: "{{count}} شخص أشار لهذا كغير مفيد." + zero: "لم يبلّغ احد بأن هذا المنشور سبام." + one: "عضو واحد أبلّغ بأن هذا المنشور سبام." + two: "{{count}} عضو أبلّغ بأن هذا المنشور سبام." + few: "{{count}} عضو أبلّغ بأن هذا المنشور سبام." + many: "{{count}} عضو أبلّغ بأن هذا المنشور سبام." + other: "{{count}} عضو أبلّغ بأن هذا المنشور سبام." inappropriate: - zero: "لم تتم الإشارة لهذا كغير ملائم." - one: "شخص أشار لهذا كغير ملائم." - two: "شخصان أشارا لهذا كغير ملائم." - few: "{{count}} أشخاص أشاروا لهذا كغير ملائم." - many: "{{count}} شخص أشاروا لهذا كغير ملائم." - other: "{{count}} شخص أشاروا لهذا كغير ملائم." + zero: "لم يبلغ احد بأن هذا المنشور غير لائق." + one: "عضو واحد أبلّغ بأن هذا المنشور غير لائق." + two: "{{count}} عضو أبلّغ بأن هذا المنشور غير لائق." + few: "{{count}} عضو أبلّغ بأن هذا المنشور غير لائق." + many: "{{count}} عضو أبلّغ بأن هذا المنشور غير لائق." + other: "{{count}} عضو أبلّغ بأن هذا المنشور غير لائق." notify_moderators: - zero: "1 عضو علّم هذا للمراقبين" - one: "1 عضو علّم هذا للمراقبين" - two: "{{count}} أعضاء علّمو هذا للمراقبين" - few: "{{count}} أعضاء علّمو هذا للمراقبين" - many: "{{count}} أعضاء علّمو هذا للمراقبين" - other: "{{count}} أعضاء علّمو هذا للمراقبين" + zero: "لم يبلغ احد المشرفين عن هذا المنشور" + one: "عضو واحد أبلّغ المشرفين عن هذا المنشور" + two: "{{count}} عضو أبلّغ المشرفين عن هذا المنشور" + few: "{{count}} عضو أبلّغ المشرفين عن هذا المنشور" + many: "{{count}} عضو أبلّغ المشرفين عن هذا المنشور" + other: "{{count}} عضو أبلّغ المشرفين عن هذا المنشور" notify_user: - zero: "لم يُرسل شيء لهذا المستخدم" - one: "أرسل شخص واحد رسالة لهذا المستخدم" - two: "أرسل شخصين رسالة لهذا المستخدم" - few: "أرسل {{count}} أشخاص رسالة لهذا المستخدم" - many: "أرسل {{count}} شخصا رسالة لهذا المستخدم" - other: "أرسل {{count}} شخص رسالة لهذا المستخدم" + zero: "لم يرسل احد رسالة لهذا العضو" + one: "عضو واحد ارسل رسالة لهذا العضو" + two: "{{count}} عضو ارسل رسالة لهذا العضو" + few: "{{count}} عضو ارسل رسالة لهذا العضو" + many: "{{count}} عضو ارسل رسالة لهذا العضو" + other: "{{count}} عضو ارسل رسالة لهذا العضو" bookmark: - zero: "لم يعلّم أحد هذه المشاركة" - one: "شخص واحد علّم هذه المشاركة" - two: "شخصان علّما هذه المشاركة" - few: "{{count}} أشخاص علّموا هذه المشاركة" - many: "{{count}} شخصًا علّموا هذه المشاركة" - other: "{{count}} شخص علّموا هذه المشاركة" + zero: "لم يضع احد علامة مرجعية علي هذا المنشور" + one: "عضو واحد وضع علامة مرجعية علي هذا المنشور" + two: "{{count}} عضو وضع علامة مرجعية علي هذا المنشور" + few: "{{count}} عضو وضع علامة مرجعية علي هذا المنشور" + many: "{{count}} عضو وضع علامة مرجعية علي هذا المنشور" + other: "{{count}} عضو وضع علامة مرجعية علي هذا المنشور" like: - zero: "لم يعجب هذا أحدا" - one: "أعجب هذا شخص واحد" - two: "أعجب هذا شخصان" - few: "أعجب هذا {{count}} أشخاص" - many: "أعجب هذا {{count}} شخصا" - other: "أعجب هذا {{count}} شخص" + zero: "لم يعجب احد بهذا المنشور" + one: "عضو واحد اعجب بهذا المنشور" + two: "{{count}} عضو اعجب بهذا المنشور" + few: "{{count}} عضو اعجب بهذا المنشور" + many: "{{count}} عضو اعجب بهذا المنشور" + other: "{{count}} عضو اعجب بهذا المنشور" vote: - zero: "لم يصوّت أحد على هذه المشاركة" - one: "صوّت شخص واحد على هذه المشاركة" - two: "صوّت شخصان على هذه المشاركة" - few: "صوّت {{count}} أشخاص على هذه المشاركة" - many: "صوّت {{count}} شخصا على هذه المشاركة" - other: "صوّت {{count}} شخص على هذه المشاركة" + zero: "لم يصوت احد علي هذا المنشور" + one: "عضو واحد صوت علي هذا المنشور" + two: "{{count}} عضو صوت علي هذا المنشور" + few: "{{count}} عضو صوت علي هذا المنشور" + many: "{{count}} عضو صوت علي هذا المنشور" + other: "{{count}} عضو صوت علي هذا المنشور" delete: confirm: zero: "لا شيء لحذفه." - one: "أمتأكد من حذف المشاركة؟" - two: "أمتأكد من حذف المشاركتين؟" - few: "أمتأكد من حذف المشاركات هذه؟" - many: "أمتأكد من حذف المشاركات هذه؟" - other: "أمتأكد من حذف المشاركات هذه؟" + one: "هل انت متاكد انك تريد حذف هذا المنشور؟" + two: "هل انت متاكد انك تريد حذف كل هذة المنشورات؟" + few: "هل انت متاكد انك تريد حذف كل هذة المنشورات؟" + many: "هل انت متاكد انك تريد حذف كل هذة المنشورات؟" + other: "هل انت متاكد انك تريد حذف كل هذة المنشورات؟" + merge: + confirm: + zero: "لا يوجد منشورات لدمجها" + one: "هل انت متاكد انك تريد دمج هذا المنشور؟" + two: "هل انت متاكد انك تريد دمج هذان المنشوران؟" + few: "هل انت متاكد انك تريد دمج الـ {{count}} منشور؟" + many: "هل انت متاكد انك تريد دمج الـ {{count}} منشور؟" + other: "هل انت متاكد انك تريد دمج الـ {{count}} منشور؟" revisions: controls: first: "أول مراجعة" @@ -2168,19 +2176,32 @@ ar: hide: "أخفِ المراجعة" show: "أظهر المراجعة" revert: "ارجع إلى هذه المراجعة" - edit_wiki: "حرّر الويكي" + edit_wiki: "عدل الwiki" edit_post: "عدل المنشور" comparing_previous_to_current_out_of_total: "{{previous}} {{current}} / {{total}}" displays: inline: - button: 'HTML' + title: "اعرض النسخة المنسقة في عمود واحد مع تمييز الاسطر المضافة و المحذوفة" + button: 'عمود واحد' side_by_side: - title: "أظهر فروقات الخرج المصيّر جنبًا إلى جنب" - button: 'HTML' + title: "اعرض الفروقات في النسخة المنسقة جنبا إلي جنب" + button: 'عمودين' side_by_side_markdown: - title: "أظهر فروقات المصدر الخامّ جنبًا إلى جنب" + title: "اعرض الفروقات في النسخة الخام جنبا إلي جنب" + button: 'عمودين خام' + raw_email: + displays: + raw: + title: "اعرض نص الرساله الخام" + button: 'خام' + text_part: + title: "اظهر الجزء النصي من رسالة البريد الالكتروني" + button: 'نص' + html_part: + title: "اظهر جزء الـ HTML من رسالة البريد الالكتروني" + button: 'HTML' category: - can: 'يمكنها…' + can: 'قادر علي…' none: '(غير مصنف)' all: 'كل الأقسام' choose: 'اختر قسم …' @@ -2193,7 +2214,7 @@ ar: tags: "الوسوم" tags_allowed_tags: "اسمح فقط لهذة الوسمة بالاستخدام في هذا القسم." tags_allowed_tag_groups: "اسمح فقط للأوسمة من هذة المجموعات بالاستخدام في هذا القسم." - tags_placeholder: "(اختياري) قائمة العلامات الوصفية المسموح بها" + tags_placeholder: "(اختياري) قائمة الاوسمة المسموح بها" tag_groups_placeholder: "(اختياريّ) قائمة مجموعات الوسوم المسموح بها" topic_featured_link_allowed: "اسمح بالروابط المُميزة بهذا القسم." delete: 'احذف القسم' @@ -2201,7 +2222,7 @@ ar: create_long: 'أنشئ قسم جديد' save: 'احفظ القسم' slug: 'عنوان القسم في الURL' - slug_placeholder: '(اختياريّ) كلمات مفصولة بشَرطة للعنوان' + slug_placeholder: '(اختياريّ) كلمات مفصولة-بشرطة للعنوان' creation_error: حدثت مشكلة أثناء إنشاء القسم. save_error: حدث خطأ في حفظ القسم. name: "اسم القسم" @@ -2213,7 +2234,7 @@ ar: background_color: "لون الخلفية" foreground_color: "لون المقدمة" name_placeholder: "كلمة أو كلمتين على الأكثر" - color_placeholder: "أيّ لون وبّ" + color_placeholder: "أيّ لون متوافق مع الانترنت" delete_confirm: "هل تريد فعلاً حذف هذا القسم؟" delete_error: "حدث خطأ في حذف القسم." list: "عرض الأقسام" @@ -2224,7 +2245,7 @@ ar: special_warning: "تحذير: هذا القسم هو قسم اصلي إعدادات الحماية له لا يمكن تعديلها. إذا لم تكن تريد استخدام هذا القسم، قم بحذفة بدلا من تطويعة لأغراض اخري." images: "الصور" email_in: "تعيين بريد إلكتروني خاص:" - email_in_allow_strangers: "قبول بريد إلكتروني من مستخدمين لا يملكون حسابات" + email_in_allow_strangers: "قبول بريد إلكتروني من زوار لا يملكون حسابات" email_in_disabled: "عُطّل إرسال المشاركات عبر البريد الإلكترونيّ من إعدادات الموقع. لتفعيل نشر المشاركات الجديدة عبر البريد،" email_in_disabled_click: 'قم بتفعيل خيار "email in" في الإعدادات' suppress_from_homepage: "امنع هذا القسم من الظهور في الصفحة الرئيسية." @@ -2235,8 +2256,9 @@ ar: subcategory_list_style: "أسلوب عرض قائمة الأقسام الفرعية:" sort_order: "رتب قائمة الموضوعات حسب:" default_view: "قائمة الموضوعات الإفتراضية" + default_top_period: "فترة الاكثر مشاهدة الافتراضية" allow_badges_label: "السماح بالحصول على الأوسمة في هذا القسم" - edit_permissions: "حرّر التّصاريح" + edit_permissions: "عدل التصاريح" add_permission: "أضف تصريحًا" this_year: "هذه السنة" position: "المكان" @@ -2262,53 +2284,72 @@ ar: description: "لن يتم إشعارك بأي موضوعات جديدة في هذه الأقسام ولن يتم عرضها في قائمة الموضوعات المنشورة مؤخراً." sort_options: default: "افترضى" - likes: "اعجاب" - op_likes: "الاعجابات الاساسية للمنشور" + likes: "الاعجابات" + op_likes: "الاعجابات علي المنشور الاساسي" views: "المشاهدات" posts: "المنشورات" activity: "النشاط" - posters: "البوستر" - category: "قسم" - created: "انشئ " - sort_ascending: 'تنازلى' + posters: "الإعلانات" + category: "القسم" + created: "تاريخ الإنشاء" + sort_ascending: 'تصاعدي' sort_descending: 'تنازلي' + subcategory_list_styles: + rows: "صفوف" + rows_with_featured_topics: "صفوف مع الموضوعات المميزة" + boxes: "مربعات" + boxes_with_featured_topics: "مربعات مع الموضوعات المميزة" flagging: - title: 'شكرا لمساعدتك في إبقاء مجتمعنا نظيفاً.' - action: 'التبليغ عن مشاركة' - take_action: "أجراء العمليه " + title: 'شكرا لمساعدتك في إبقاء مجتمعنا متحضرا.' + action: 'ابلغ عن المنشور' + take_action: "اتخذ اجراء" notify_action: 'رسالة' official_warning: 'تحذير رسمي' - delete_spammer: "احذف ناشر السخام" + delete_spammer: "احذف ناشر السبام" delete_confirm_MF: "{posts, plural,\n zero {ليس للمستخدم أيّة مشاركات}\n other {}\n}\n{topics, plural,\n zero {\n {posts, plural,\n zero {أو مواضيع.}\n other {ليس للمستخدم أيّة مواضيع.}\n }\n }\n other {{posts, plural, zero{.} other{}}}\n}\nأنت على وشك\n{posts, plural,\n zero {{topics, plural, zero {} other {حذف}}}\n other {حذف}\n}\n{posts, plural,\n zero {}\n one {مشاركة واحدة}\n two {مشاركتين}\n few {# مشاركات}\n many {# مشاركة}\n other {# مشاركة}\n}\n{posts, plural, zero {} other {{topics, plural, zero {} other {و}}}}\n{topics, plural,\n zero {}\n one {موضوع واحد}\n two {موضوعين}\n few {# مواضيع}\n many {# موضوعًا}\n other {# موضوع}\n}\n{posts, plural,\n zero {{topics, plural, zero {} other {له، و}}}\n other {{topics, plural, zero {له، و} other {للمستخدم، و}}}\n}\nإزالة حسابه، ومنع التّسجيل من عنوان IP هذا {ip_address}، وإضافة عنوان البريد الإلكترونيّ {email} إلى قائمة منع دائم. أمتأكّد حقًّا من أنّ هذا المستخدم ناشر سخام؟" - yes_delete_spammer: "نعم، احذف ناشر السخام" + yes_delete_spammer: "نعم، احذف ناشر السبام" ip_address_missing: "(N/A)" hidden_email_address: "(مخفي)" - submit_tooltip: "إرسال تبليغ" - take_action_tooltip: "الوصول إلى الحد الأعلى للتبليغات دون انتظار تبليغات أكثر من أعضاء الموقع." - cant: "المعذرة، لا يمكنك التبليغ عن هذه المشاركة في هذه اللحظة." - notify_staff: 'أعلِم الطّاقم بسريّة' + submit_tooltip: "إرسال البلاغ" + take_action_tooltip: "الوصول إلى الحد الأعلى للبلاغات دون انتظار بلاغات أكثر من أعضاء الموقع." + cant: "عذرا، لا يمكنك الابلاغ عن هذا المنشور في هذه اللحظة." + notify_staff: 'ابلغ طاقم العمل بسرية' formatted_name: off_topic: "خارج عن الموضوع" inappropriate: "غير لائق" spam: "هذا سبام" - custom_placeholder_notify_user: "كن محدد, استدلالي ودائما حسن الاخلاق" - custom_placeholder_notify_moderators: "ممكن تزودنا بمعلومات أكثر عن سبب عدم ارتياحك حول هذه المشاركة؟ زودنا ببعض الروابط و الأمثلة قدر الإمكان." + custom_placeholder_notify_user: "كن محدد و كن بناء و دائما كن حسن الخلق" + custom_placeholder_notify_moderators: "يمكنك تزودنا بمعلومات أكثر عن سبب عدم ارتياحك إلي هذا المنشور؟ زودنا ببعض الروابط و الأمثلة قدر الإمكان." custom_message: at_least: zero: "لا تُدخل أيّ محرف" - one: "أدخل محرفًا واحدًا على الأقلّ" - two: "أدخل محرفين على الأقلّ" - few: "أدخل {{count}} محارف على الأقلّ" - many: "أدخل {{count}} محرفًا على الأقلّ" - other: "أدخل {{count}} محرف على الأقلّ" + one: "أدخل حرف واحد على الأقل" + two: "أدخل {{count}} احرف على الأقل" + few: "أدخل {{count}} احرف على الأقل" + many: "أدخل {{count}} احرف على الأقل" + other: "أدخل {{count}} احرف على الأقل" + more: + zero: "{{count}} حرف متبقي علي الحد الادني..." + one: "حرف واحد متبقي علي الحد الادني..." + two: "{{count}} حرف متبقي علي الحد الادني..." + few: "{{count}} حرف متبقي علي الحد الادني..." + many: "{{count}} حرف متبقي علي الحد الادني..." + other: "{{count}} حرف متبقي علي الحد الادني..." + left: + zero: "{{count}} حرف متبقي علي الحد الاقصي..." + one: "حرف واحد متبقي علي الحد الاقصي..." + two: "{{count}} حرف متبقي علي الحد الاقصي..." + few: "{{count}} حرف متبقي علي الحد الاقصي..." + many: "{{count}} حرف متبقي علي الحد الاقصي..." + other: "{{count}} حرف متبقي علي الحد الاقصي..." flagging_topic: - title: "شكرا لمساعدتنا في ابقاء مجتمعنا نضيفا" + title: "شكرا لمساعدتنا في ابقاء المجمتع متحضر" action: "التبليغ عن الموضوع" notify_action: "رسالة" topic_map: title: "ملخص الموضوع" - participants_title: "المشاركون المعتادون" - links_title: "روابط شائعة" + participants_title: "الناشرون المترددون" + links_title: "روابط مشهورة" links_shown: "أظهر روابط أخرى..." clicks: zero: "لا نقرات" @@ -2330,16 +2371,16 @@ ar: warning: help: "هذا تحذير رسمي." bookmarked: - help: "لقد علّمت هذا الموضوع" + help: "لقد وضعت علامة مرجعية علي هذا الموضوع" locked: - help: "هذا الموضوع مغلق ولم يعد يستقبل ردودا" + help: "هذا الموضوع مغلق, لذا فهو لم يعد يستقبل ردودا" archived: - help: "هذا الموضوع مؤرشف، لذا فهو مجمّد ولا يمكن تعديله" + help: "هذا الموضوع مؤرشف، لذا فهو مجمد ولا يمكن تعديله" locked_and_archived: - help: "هذا الموضوع مغلق ومؤرشف، لذا لم يعد يستقبل ردودًا ولا يمكن تغييره" + help: "هذا الموضوع مغلق ومؤرشف، لذا فهو لم يعد يستقبل ردودًا ولا يمكن تغييره" unpinned: title: "غير مثبّت" - help: "هذا الموضوع غير مثبّت لك، وسيُعرض بالترتيب العاديّ" + help: "هذا الموضوع غير مثبّت لك، وسيُعرض بالترتيب العادي" pinned_globally: title: "مثبّت للعموم" help: "هذا الموضوع مثبت بشكل عام, سوف يظهر في مقدمة قائمة اخر الموضوعات وفي القسم الخاصة به." @@ -2347,16 +2388,16 @@ ar: title: "مثبّت" help: "هذا الموضوع مثبّت لك، وسيُعرض أعلى قسمة" invisible: - help: "هذا الموضوع غير مصنف لن يظهر في قائمة التصانيف ولايمكن الدخول عليه الابرابط مباشر." - posts: "مشاركات" - posts_long: "هناك {{number}} مشاركات في هذا الموضوع" + help: "هذا الموضوع غير مدرج, لن يظهر في قائمة الموضوعات ولا يمكن الوصول إلية إلا برابط مباشر" + posts: "منشورات" + posts_long: "هناك {{number}} منشور في هذا الموضوع" posts_likes_MF: | {count, plural, zero {ليس في هذا الموضوع أي رد} one {في هذا الموضوع رد واحد} two {في هذا الموضوع ردان} few {في هذا الموضوع # ردود} many {في هذا الموضوع # ردا} other {في هذا الموضوع # رد}} {ratio, select, low {ونسبة الإعجاب إلى المشاركة عالية} med {ونسبة الإعجاب إلى المشاركة عالية جدا} high {ونسبة الإعجاب إلى المشاركة مهولة} other {}} - original_post: "المشاركة الاصلية" + original_post: "المنشور الاصلي" views: "المشاهدات" views_lowercase: zero: "المشاهدات" @@ -2366,6 +2407,13 @@ ar: many: "المشاهدات" other: "المشاهدات" replies: "الردود" + views_long: + zero: "تم مشاهدة هذا الموضوع {{number}} مرة" + one: "تم مشاهدة هذا الموضوع مرة واحدة" + two: "تم مشاهدة هذا الموضوع {{number}} مرة" + few: "تم مشاهدة هذا الموضوع {{number}} مرة" + many: "تم مشاهدة هذا الموضوع {{number}} مرة" + other: "تم مشاهدة هذا الموضوع {{number}} مرة" activity: "النشاط" likes: "اعجابات" likes_lowercase: @@ -2388,10 +2436,11 @@ ar: history: "تاريخ" changed_by: "الكاتب {{author}}" raw_email: + title: "البريد الإلكتروني الوارد" not_available: "غير متوفر!" categories_list: "قائمة الأقسام" filters: - with_topics: "المواضيع %{filter}" + with_topics: "الموضوعات %{filter}" with_category: "الموضوعات%{filter} في %{category}" latest: title: "الأخيرة" @@ -2402,16 +2451,16 @@ ar: few: "الأخيرة ({{count}})" many: "الأخيرة ({{count}})" other: "الأخيرة ({{count}})" - help: "المواضيع التي فيها مشاركات حديثة" + help: "الموضوعات التي بها منشورات حديثة" hot: title: "نَشط" - help: "مختارات من مواضيع ساخنة" + help: "مختارات من انشط الموضوعات" read: title: "المقروءة" - help: "المواضيع التي قرأتها بترتيب آخر قراءة لها" + help: "المواضيع التي قرأتها بترتيب قرائتك لها" search: title: "بحث" - help: "بحث في كل المواضيع" + help: "بحث في كل الموضوعات" categories: title: "الأقسام" title_in: "قسم - {{categoryName}}" @@ -2425,7 +2474,7 @@ ar: few: "غير المقروءة ({{count}})" many: "غير المقروءة ({{count}})" other: "غير المقروءة ({{count}})" - help: "المواضيع التي تتابعها (أو تراقبها) والتي فيها مشاركات غير مقروءة" + help: "الموضوعات التي تتابعها (أو تراقبها) والتي فيها منشورات غير مقروءة" lower_title_with_count: zero: "1 غير مقررء " one: "1 غير مقروء" @@ -2442,7 +2491,7 @@ ar: many: "{{count}} جديد" other: "{{count}} جديد" lower_title: "جديد" - title: "الجديدة" + title: "جديد" title_with_count: zero: "الجديدة ({{count}})" one: "الجديدة ({{count}})" @@ -2452,11 +2501,11 @@ ar: other: "الجديدة ({{count}})" help: "المواضيع المنشأة في الأيّام القليلة الماضية" posted: - title: "مشاركاتي" - help: "مواضيع شاركت بها " + title: "منشوراتي" + help: "مواضيع نشرت بها " bookmarks: - title: "العلامات" - help: "مواضيع قمت بتفضيلها" + title: "العلامات المرجعية" + help: "موضوعات وضعت عليها علامة مرجعية" category: title: "{{categoryName}}" title_with_count: @@ -2466,30 +2515,30 @@ ar: few: "{{categoryName}} ({{count}})" many: "{{categoryName}} ({{count}})" other: "{{categoryName}} ({{count}})" - help: "آخر الموضوعات في القسم {{categoryName}}" + help: "آخر الموضوعات في قسم {{categoryName}}" top: title: "الأكثر مُشاهدة" - help: "أكثر المواضيع نشاطًا في آخر عام، أو شهر أو أسبوع أو يوم" + help: "أكثر المواضيع نشاطًا في آخر عام أو شهر أو أسبوع أو يوم" all: - title: "كلّها" + title: "كل الوقت" yearly: - title: "السّنويّة" + title: "سنة" quarterly: - title: "الرّبعيّة" + title: "ربع سنة" monthly: - title: "الشّهريّة" + title: "شهر" weekly: - title: "الأسبوعيّة" + title: "اسبوع" daily: - title: "اليوميّة" - all_time: "على مرّ الزّمن" + title: "يوم" + all_time: "كل الوقت" this_year: "سنة" this_quarter: "ربع" this_month: "شهر" this_week: "أسبوع" today: "يوم" - other_periods: "مشاهدة الأفضل" - browser_update: 'للأسف، متصفّحك قديم جدًّا ليعمل عليه هذا الموقع. من فضلك رقّه.' + other_periods: "اعرض الاكثر مشاهدة" + browser_update: 'للأسف، متصفّحك قديم جدًّا ليعمل عليه هذا الموقع. من فضلك حدث المتصفح خاصتك.' permission_types: full: "انشاء / رد / مشاهدة" create_post: "رد / مشاهدة" @@ -2499,19 +2548,19 @@ ar: keyboard_shortcuts_help: title: 'اختصارات لوحة المفاتيح' jump_to: - title: 'الانتقال' + title: 'الانتقال إلي' home: 'g، h الرّئيسيّة' latest: 'g، l الأخيرة' - new: 'g، n الجديد' + new: 'g، n الجديدة' unread: 'g، u غير المقروء' categories: 'g، c الأقسام' - top: 'g, t الأعلى' - bookmarks: 'g، b العلامات' - profile: 'g، p اللاحة' + top: 'g, t الاكثر مشاهدة' + bookmarks: 'g، b العلامات المرجعية' + profile: 'g، p الملف الشخصي' messages: 'g، m الرّسائل' navigation: title: 'التنقّل' - jump: '# الانتقال الى المشاركة #' + jump: '# الانتقال الى المنشور#' back: 'u العودة' up_down: 'k/j نقل المحدد ↑ ↓' open: 'o أو Enter فتح الموضوع المحدد' @@ -2522,26 +2571,27 @@ ar: notifications: 'n فتح الإشعارات' hamburger_menu: '= فتح القائمة الرّئيسيّة' user_profile_menu: 'pفتح قائمة المستخدم' - show_incoming_updated_topics: '. عرض المواضيع المحدثة' + show_incoming_updated_topics: '. عرض الموضوعات المحدثة' + search: '/ او ctrl+shift+s بحث' help: '? فتح مساعدة لوحة المفاتيح' - dismiss_new_posts: 'تجاهل جديد / المشاركات x, r' - dismiss_topics: 'x, t تجاهل المواضيع' - log_out: 'shift+z shift+z الخروج' + dismiss_new_posts: 'x, r تجاهل المنشورات الجديدة' + dismiss_topics: 'x, t تجاهل الموضوعات' + log_out: 'shift+z shift+z تسجيل خروج' actions: title: 'إجراءات' - bookmark_topic: 'f تبديل علامة مرجعية الموضوع' - pin_unpin_topic: 'shift+p تثبيت الموضوع أو إلغاء تثبيته' + bookmark_topic: 'f وضع/ازالة علامة مرجعية علي الموضوع' + pin_unpin_topic: 'shift+p تثبيت/إلغاء تثبيت الموضوع' share_topic: 'shift+s مشاركة الموضوع' - share_post: 's مشاركة المشاركة' - reply_as_new_topic: 'الرد في موضوع مرتبط t' + share_post: 's مشاركة المنشور' + reply_as_new_topic: 't الرد كموضوع مرتبط' reply_topic: 'shift+r الرد على الموضوع' - reply_post: 'r الرد على المشاركة' - quote_post: 'q اقتباس المشاركة' - like: 'l الإعجاب بالمشاركة' - flag: '! علم على المشاركة' - bookmark: 'b أضف مرجعية للمشاركة' - edit: 'e تعديل المشاركة' - delete: 'd حذف المشاركة' + reply_post: 'r الرد على المنشور' + quote_post: 'q اقتباس المنشور' + like: 'l الإعجاب بالمنشور' + flag: '! الإبلاغ عن المنشور' + bookmark: 'b وضع علامة مرجعية علي المنشور' + edit: 'e تعديل المنشور' + delete: 'd حذف المنشور' mark_muted: 'm، m كتم الموضوع' mark_regular: 'm, r موضوع منظم (الإفتراضي)' mark_tracking: 'm، t متابعة الموضوع' @@ -2618,7 +2668,7 @@ ar: sort_by_count: "العدد" sort_by_name: "الاسم" manage_groups: "أدر مجموعات الوسوم" - manage_groups_description: "اصنع مجموعات لتنظيم الوسوم" + manage_groups_description: "انشئ مجموعات لتنظيم الوسوم" filters: without_category: "مواضيع %{tag} %{filter}" with_category: "موضوعات %{filter}%{tag} في %{category}" @@ -2658,27 +2708,27 @@ ar: unread: "ليست هناك مواضيع غير مقروءة." new: "ليست هناك مواضيع جديدة." read: "لم تقرأ أيّ موضوع بعد." - posted: "لم تشارك في أيّ موضوع بعد." - latest: "ليست هناك مواضيع حديثة." - hot: "لا يوجد المزيد من المواضيع النشطة" - bookmarks: "لا مواضيع معلّمة بعد." - top: "لا يوجد المزيد من المواضيع العليا" - search: "لا نتائج للبحث." + posted: "لم تنشر في أيّ موضوع بعد.." + latest: "لا يوجد موضوعات حديثة." + hot: "لا يوجد موضوعات نشطة." + bookmarks: "لم تقم بوضع علامات مرجعية علي اي موضوع بعد." + top: "لا يوجد موضوعات الاكثر مشاهدة." + search: "لا يوجد نتائج للبحث." bottom: - latest: "ليست هناك مواضيع حديثة أخرى." - hot: "لا يوجد المزيد من المواضيع النشطة" - posted: "لا يوجد مواضيع أخرى." - read: "لا مواضيع مقروءة أخرى." - new: "لا مواضيع جديدة أخرى." - unread: "لا مواضيع غير مقروءة أخرى." - top: "لقد اطلعت على كل المواضيع المميزة حتى هذه اللحظة." - bookmarks: "لايوجد المزيد من المواضيع في المفضلة" + latest: "لا يوجد المزيد من الموضوعات الحديثة." + hot: "لا يوجد المزيد من الموضوعات النشطة." + posted: "لا يوجد المزيد من الموضوعات المنشورة." + read: "لا يوجد المزيد من الموضوعات المقروءة." + new: "لا يوجد المزيد من الموضوعات الجديدة." + unread: "لا يوجد المزيد من الموضوعات غير مقروءة." + top: "لا يوجد المزيد من الموضوعات الاكثر مشاهدة." + bookmarks: "لا يوجد المزيد من الموضوعات التي عليها علامة مرجعية." search: "لا نتائج بحث أخرى." invite: custom_message: "اجعل دعوتك شخصيّة أكثر بكتابة" custom_message_link: "رسالة مخصصة" custom_message_placeholder: "ادخل رسالتك المخصصة" - custom_message_template_forum: "مرحبا. عليك الانضمام إلى هذا المنتدى!" + custom_message_template_forum: "مرحبا. عليك الانضمام إلى هذا المجتمع!" custom_message_template_topic: "مرحبا. أظن أن هذا الموضوع سيسعدك!" safe_mode: enabled: "الوضع الآمن مفعّل، لتخرج منه أغلق نافذة المتصفّح هذه" @@ -2686,18 +2736,18 @@ ar: type_to_filter: "اكتب للتّرشيح..." admin: title: 'مدير المجتمع' - moderator: 'مراقب' + moderator: 'مشرف' dashboard: title: "لوحة التحكم" last_updated: "أخر تحديث للوحة التحكم:" - version: "الإصدارة" - up_to_date: "لديك أحدث إصدارة!" - critical_available: "يتوفّر تحديث لمشاكل حرجة." + version: "الإصدار" + up_to_date: "لديك أحدث إصدار!" + critical_available: "يتوفّر تحديث لمشكلات حرجة." updates_available: "التحديثات متوفرة." please_upgrade: "من فضلك رقّ البرمجية!" - no_check_performed: "لم يجرِ التماس التّحديثات. تحقّق من عمل sidekiq." - stale_data: "لم يجرِ التماس التّحديثات حديثًا. تحقّق من عمل sidekiq." - version_check_pending: "يبدو أنك رقّيت الموقع مؤخرا. مذهل!" + no_check_performed: "لم يتم التحقق من التحديثات. تأكد أن sidekiq قيد التشغيل." + stale_data: "لم يتم التحقق من التحديثات مؤخراً. تاكد أن sidekiq قيد التشغيل." + version_check_pending: "يبدو أنك قمت بتحديث الموقع مؤخرا. رائع!" installed_version: "المثبتة" latest_version: "الأخيرة" problems_found: "عُثر على مشاكل في تثبيت نسخة دسكورس هذه:" @@ -2706,12 +2756,12 @@ ar: no_problems: "لا مشاكل." moderators: 'المشرفون:' admins: 'المدراء:' - blocked: 'محظور:' - suspended: 'موقوف:' + blocked: 'محظورون:' + suspended: 'موقوفون:' private_messages_short: "الرسائل" private_messages_title: "الرسائل" mobile_title: "متنقل" - space_free: "{{size}} حرّ" + space_free: "{{size}} فارغ" uploads: "عمليات الرفع" backups: "النسخ الاحتياطية" traffic_short: "المرور" @@ -2729,38 +2779,38 @@ ar: 30_days_ago: "منذ ٣٠ يوم" all: "الكل" view_table: "جدول" - view_graph: "شكل رسومي" + view_graph: "رسم بياني" refresh_report: "تحديث التقرير " start_date: "تاريخ البدء" end_date: "تاريخ الإنتهاء" - groups: "جميع الفئات" + groups: "كل المجموعات" commits: latest_changes: "آخر تغيير: يرجى التحديث" by: "بواسطة" flags: - title: "التبليغات" - active_posts: "المشاركات المبلغ عنها " - old_posts: "المشاركات القديمة المبلغ عنها " - topics: "المواضيع المبلغ عنها " + title: "البلاغات" + active_posts: "المنشورات المبلغ عنها " + old_posts: "المنشورات القديمة المبلغ عنها " + topics: "الموشوعات المبلغ عنها " agree: "أوافق" - agree_title: "أكد هذا البلاغ لكونه صحيح وصالح" - agree_flag_modal_title: "أوافق مع ..." - agree_flag_hide_post: "اوافق (اخفاء المشاركة + ارسال ر.خ)" - agree_flag_hide_post_title: "أخفي هذه المشاركة وَ تلقائيا بإرسال رسالة للمستخدم وحثهم على تحريرها" - agree_flag_restore_post: "موافق (استعادة المشاركة)" - agree_flag_restore_post_title: "استعد هذه المشاركة." - agree_flag: "الموافقه على التبليغ" - agree_flag_title: "الموافقة مع التَعَلّيم وحفظ المشاركة دون تغيير." + agree_title: "أكد هذا البلاغ كونه صحيح وصالح" + agree_flag_modal_title: "أوافق و ..." + agree_flag_hide_post: "اوافق (اخفاء المنشور + ارسال رسالة خاصة)" + agree_flag_hide_post_title: "أخفي هذا المنشور و ارسل رسالة للعضو تلقائيا تحثه علي تعديلة" + agree_flag_restore_post: "موافق (اعد المنشور)" + agree_flag_restore_post_title: "اعد هذا المنشور." + agree_flag: "الموافقه على البلاغ" + agree_flag_title: "الموافقة علي البلاغ و حفظ المنشور دون تغيير." defer_flag: "تأجيل" - defer_flag_title: "إزالة البلاغ، لا يتطلب منك إجراء في الوقت الحالي." - delete: "حذف" - delete_title: "حذف المشاركة المرتبطة بهذا البلاغ" - delete_post_defer_flag: "حذف المشاركة مع تأجيل البلاغ" - delete_post_defer_flag_title: "حذف المشاركة. اذا كانت المشاركة الاولى, احذف الموضوع" - delete_post_agree_flag: "حذف المشاركة مع الموافقة على البلاغ" - delete_post_agree_flag_title: "حذف المشاركة. اذا كانت المشاركة الاولى, احذف الموضوع" - delete_flag_modal_title: "حذف مع ..." - delete_spammer: "احذف ناشر السخام" + defer_flag_title: "ازل هذا البلاغ، لا يتطلب منك إجراء في الوقت الحالي." + delete: "احذف" + delete_title: "احذف المنشور المرتبط بهذا البلاغ" + delete_post_defer_flag: "احذف المنشور مع تأجيل البلاغ" + delete_post_defer_flag_title: "احذف المنشور. اذا كان المنشور الاول, احذف الموضوع" + delete_post_agree_flag: "احذف المنشور مع الموافقة على البلاغ" + delete_post_agree_flag_title: "احذف المنشور. اذا كان المنشور الاول, احذف الموضوع" + delete_flag_modal_title: "احذف و..." + delete_spammer: "احذف ناشر السبام" delete_spammer_title: "احذف المستخدم مع مشاركاته ومواضيعه." disagree_flag_unhide_post: "أختلف مع البلاغ، إعادة إظهار المشاركة." disagree_flag_unhide_post_title: "حذف أي بلاغ يخص هذه المشاركة مع إظهارها مرة أخرى" @@ -3061,11 +3111,17 @@ ar: text: "بعد الـ Header" footer: text: "تذييل " + title: "ادخل نص الـ HTML ليتم عرضه في ذيل الصفحة (الفوتر)" + embedded_scss: + text: "CSS مضمن" head_tag: text: "" body_tag: text: "" colors: + select_base: + title: "اختر مخطط الالوان الاساسي" + description: "مخطط اساسي" title: "الألوان" edit: "عدل مخططات الألوان" long_title: "مخططات الألوان" @@ -3283,11 +3339,16 @@ ar: censor: 'راقب (ضلل)' require_approval: 'يحتاج لموافقة' flag: 'علم' + action_descriptions: + require_approval: 'المنشورات التي تحتوي على هذه الكلمات تحتاج الى موافقة الطاقم قبل ان يتم رؤيتهم.' + flag: 'اسمح للمنشورات التي تحتوي على هذه الكلمات, ولكن علمهم كـ غير مناسب لكي يتمكن المشرف من مراجعتهم.' form: label: 'كلمة جديدة:' + placeholder_regexp: "تعبير اعتيادي" add: 'اضف' success: 'نجاح' upload: "حمل" + upload_successful: "تم الرفع بنجاح. تم اضافة الكلمات" impersonate: title: "انتحال الشخصية" help: "استخدم هذه الأداة لانتحال شخصية حساب مستخدم لأغراض التصحيح. سيتم تسجيل خروجك عندما تنتهي." @@ -3361,10 +3422,15 @@ ar: suspend_failed: "حدث خطب ما أثناء إيقاف هذا المستخدم {{error}}" unsuspend_failed: "حدث خطب ما أثناء إلغاء إيقاف هذا المستخدم {{error}}" suspend_duration: "ما المدّة التي سيُوقف هذا المستخدم خلالها؟" - suspend_duration_units: "(أيام)" suspend_reason_label: "لماذا هل أنت عالق؟ هذا النص سيكون ظاهراً للكل على صفحة تعريف هذا العضو, وسيكون ظاهراً للعضو عندما يحاول تسجل الدخول. احفظها قصيرة." + suspend_reason_hidden_label: "لماذا انت موقوف؟ هذا النص سيظهر للعضو حين يحاول الولوج. اجعله قصيراً." suspend_reason: "سبب" + suspend_reason_placeholder: "سبب التوقيف" + suspend_message: "رسالة بريد الكتروني" + suspend_message_placeholder: "اختياري. وفر المزيد من المعلومات حول التوقيف و سيتم ارساله عبر البريد الالكتروني الى العضو." suspended_by: "محظور من قبل" + suspended_until: "(حتى %{until})" + cant_suspend: "لا يمكن ايقاف هذا العضو" delete_all_posts: "احذف كل مشاركاته" delete_all_posts_confirm_MF: "أنت على وشك {POSTS, plural, zero {عدم حذف شيء} one {حذف مشاركة واحدة} two {حذف مشاركتين} few {حذف # مشاركات} many {حذف # مشاركة} other {حذف # مشاركة}}{TOPICS, plural, zero {} one { وموضوع واحد} two { وموضوعين} few { و# مواضيع} many {و# موضوعا} other {و# موضوع}}. أمتأكد؟" suspend: "علّق" @@ -3385,6 +3451,7 @@ ar: logged_out: "أخرجنا العضو من كلّ أجهزته" revoke_admin: 'سحب الإدارة' grant_admin: 'منحة إدارية' + grant_admin_confirm: "لقد ارسلنا بريداً الكترونياً لتأكيد المدير الجديد. رجاء افتح الرسالة و اتبع التعلميات." revoke_moderation: 'سحب المراقبة' grant_moderation: 'منحة مراقبة' unblock: 'إلغاء حظر' diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml index 340b50c267..822fb56727 100644 --- a/config/locales/client.el.yml +++ b/config/locales/client.el.yml @@ -540,6 +540,7 @@ el: admin_tooltip: "Αυτός ο χρήστης είναι διαχειριστής" blocked_tooltip: "Ο χρήστης είναι μπλοκαρισμένος" suspended_notice: "Αυτός ο χρήστης είναι σε αποβολή μέχρι τις {{date}}." + suspended_permanently: "Ο χρήστης είναι αποβλημένος." suspended_reason: "Αιτιολογία:" github_profile: "Github" email_activity_summary: "Περίληψη Ενεργειών" @@ -1415,7 +1416,9 @@ el: later_this_week: "Αργότερα αυτή την εβδομάδα" this_weekend: "Αυτό το Σαββατοκύριακο" next_week: "Την άλλη εβδομάδα" + two_weeks: "Δύο Εβδομάδες" next_month: "Τον άλλο μήνα" + forever: "Για Πάντα" pick_date_and_time: "Επίλεξε ημερομηνία και ώρα" set_based_on_last_post: "Κλείσε ανάλογα με την τελευταία ανάρτηση" publish_to_category: diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 80ffb7603c..28f3c24433 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -604,6 +604,7 @@ en: admin_tooltip: "This user is an admin" blocked_tooltip: "This user is blocked" suspended_notice: "This user is suspended until {{date}}." + suspended_permanently: "This user is suspended." suspended_reason: "Reason: " github_profile: "Github" email_activity_summary: "Activity Summary" @@ -1554,12 +1555,14 @@ en: deleted: "The topic has been deleted" topic_status_update: - title: "Set Topic Timer" + title: "Topic Timer" save: "Set Timer" num_of_hours: "Number of hours:" remove: "Remove Timer" publish_to: "Publish To:" when: "When:" + public_timer_types: Topic Timers + private_timer_types: User Topic Timers auto_update_input: none: "" later_today: "Later today" @@ -1567,7 +1570,9 @@ en: later_this_week: "Later this week" this_weekend: "This weekend" next_week: "Next week" + two_weeks: "Two Weeks" next_month: "Next month" + forever: "Forever" pick_date_and_time: "Pick date and time" set_based_on_last_post: "Close based on last post" publish_to_category: diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index 647c13bf2c..d8bb0fdd29 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -540,6 +540,7 @@ fi: admin_tooltip: "Tämä käyttäjä on ylläpitäjä" blocked_tooltip: "Tämä käyttäjä on estetty" suspended_notice: "Tämä käyttäjätili on hyllytetty {{date}} asti." + suspended_permanently: "Käyttäjä on hyllytetty." suspended_reason: "Syy:" github_profile: "GitHub" email_activity_summary: "Kooste tapahtumista" @@ -1416,7 +1417,9 @@ fi: later_this_week: "Myöhemmin tällä viikolla" this_weekend: "Viikonloppuna" next_week: "Ensi viikolla" + two_weeks: "Kahden viikon kuluttua" next_month: "Ensi kuussa" + forever: "Ikuisesti" pick_date_and_time: "Valitse päivämäärä ja kellonaika" set_based_on_last_post: "Sulje viimeisimmän viestin mukaan" publish_to_category: @@ -2959,6 +2962,7 @@ fi: form: label: 'Uusi sana:' placeholder: 'kokonainen sana tai * jokerimerkkinä' + placeholder_regexp: "säännöllinen lauseke" add: 'Lisä' success: 'Onnistui' upload: "Lataa" @@ -3037,7 +3041,7 @@ fi: moderator: "Valvoja?" admin: "Ylläpitäjä?" blocked: "Estetty?" - staged: "Luotu?" + staged: "Esikäyttäjä?" show_admin_profile: "Ylläpito" refresh_browsers: "Pakota sivun uudelleen lataus" refresh_browsers_message: "Sanoma lähetettiin kaikille päätelaitteille!" @@ -3112,7 +3116,7 @@ fi: deactivate_explanation: "Käytöstä poistetun käyttäjän täytyy uudelleen vahvistaa sähköpostiosoitteensa." suspended_explanation: "Hyllytetty käyttäjä ei voi kirjautua sisään." block_explanation: "Estetty käyttäjä ei voi luoda viestejä tai ketjuja." - staged_explanation: "Automaattisesti luotu käyttäjä voi kirjoittaa vain tiettyihin ketjuihin sähköpostin välityksellä." + staged_explanation: "Esikäyttäjä voi kirjoittaa vain tiettyihin ketjuihin sähköpostin välityksellä." bounce_score_explanation: none: "Tästä sähköpostiosoitteesta ei ole tullut palautuksia viime aikoina" some: "Tästä sähköpostiosoitteesta on tullut joitakin palautuksia viime aikoina" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 8d244ee84a..0fc8215647 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -2369,8 +2369,9 @@ fr: by: "par" flags: title: "Signalements" - old: "Anciens" - active: "Actifs" + active_posts: "Messages signalés" + old_posts: "Anciens messages signalés" + topics: "Sujets signalés" agree: "Accepter" agree_title: "Confirmer que le signalement est valide et correcte" agree_flag_modal_title: "Accepter et…" @@ -2398,6 +2399,8 @@ fr: clear_topic_flags: "Terminer" clear_topic_flags_title: "Ce sujet a été étudié et les problèmes ont été résolus. Cliquez sur Terminer pour enlever les signalements." more: "(plus de réponses…)" + suspend_user: "Suspendre l'utilisateur" + suspend_user_title: "Suspendre l'utilisateur pour ce message" dispositions: agreed: "accepté" disagreed: "refusé" @@ -2408,11 +2411,18 @@ fr: system: "Système" error: "Quelque chose s'est mal passé" reply_message: "Répondre" - no_results: "Il n'y a aucun signalement." + no_results: "Il n'y a pas de messages signalés." topic_flagged: "Ce sujet a été signalé." + show_full: "afficher le message complet" visit_topic: "Consulter le sujet pour intervenir" was_edited: "Le message a été modifié après le premier signalement" previous_flags_count: "Ce message a déjà été signalé {{count}} fois." + show_details: "Afficher les détails du signalement" + flagged_topics: + topic: "Sujet" + type: "Type" + users: "Utilisateurs" + last_flagged: "Dernier signalés" summary: action_type_3: one: "hors sujet" @@ -2950,6 +2960,7 @@ fr: form: label: 'Nouveau mot :' placeholder: 'mot complet ou * comme signe générique' + placeholder_regexp: "expression régulière" add: 'Ajouter' success: 'Succès' upload: "Envoyer" @@ -3011,10 +3022,15 @@ fr: suspend_failed: "Il y a eu un problème pendant la suspension de cet utilisateur {{error}}" unsuspend_failed: "Il y a eu un problème pendant le retrait de la suspension de cet utilisateur {{error}}" suspend_duration: "Combien de temps l'utilisateur sera suspendu ?" - suspend_duration_units: "(jours)" suspend_reason_label: "Pourquoi suspendez-vous ? Ce texte sera visible par tout le monde sur la page du profil de cet utilisateur, et sera affiché à l'utilisateur quand ils essaient de se connecter. Soyez bref." + suspend_reason_hidden_label: "Pourquoi le suspendez-vous ? Ce texte sera affiché à l'utilisateur quand il essaie de se connecter. Soyez bref." suspend_reason: "Raison" + suspend_reason_placeholder: "Raison de la suspension" + suspend_message: "Message courriel" + suspend_message_placeholder: "Donner plus d'informations au sujet de la suspension qui seront envoyées à l'utilisateur par courriel." suspended_by: "Suspendu par" + suspended_until: "(jusqu'à %{until})" + cant_suspend: "Cet utilisateur ne peut pas être suspendu." delete_all_posts: "Supprimer tous les messages" delete_all_posts_confirm_MF: "Vous êtes sur le point de supprimer {POSTS, plural, one {1 message} other {# messages}} et {TOPICS, plural, one {1 sujet} other {# sujets}}. Êtes-vous sûr ?" suspend: "Suspendre" diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index b9f0e3a01b..2321fddf5b 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -540,6 +540,7 @@ it: admin_tooltip: "Questo utente è un amministratore" blocked_tooltip: "Questo utente è bloccato" suspended_notice: "Questo utente è sospeso fino al {{date}}." + suspended_permanently: "Questo utente è sospeso." suspended_reason: "Motivo: " github_profile: "Github" email_activity_summary: "Riassunto Attività" @@ -1415,7 +1416,9 @@ it: later_this_week: "Più tardi questa settimana" this_weekend: "Questo fine settimana" next_week: "La prossima settimana" + two_weeks: "Due Settimane" next_month: "Il prossimo mese" + forever: "Per sempre" pick_date_and_time: "Scegli la data e l'ora" set_based_on_last_post: "Chiudi in base all'ultimo messaggio" publish_to_category: @@ -2956,6 +2959,7 @@ it: form: label: 'Nuova Parola:' placeholder: 'parola completa o * come carattere jolly' + placeholder_regexp: "espressione regolare" add: 'Aggiungi' success: 'Successo' upload: "Carica" diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index 6f2210b094..c2906f34b6 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -44,17 +44,17 @@ ja: less_than_x_seconds: other: "%{count}秒未満" x_seconds: - other: "%{count}秒前" + other: "%{count}秒" x_minutes: - other: "%{count}分前" + other: "%{count}分" about_x_hours: - other: "%{count}時間前" + other: "%{count}時間" x_days: - other: "%{count}日前" + other: "%{count}日" about_x_years: - other: "%{count}年前" + other: "%{count}年" over_x_years: - other: "%{count}年以上前" + other: "%{count}年以上" almost_x_years: other: "約%{count}年" date_month: "MMM Do" @@ -1630,6 +1630,8 @@ ja: yes_value: "はい" via_email: "メールで投稿されました。" whisper: "この投稿はモデレーターへのプライベートメッセージです" + wiki: + about: "この投稿はWiki形式です" archetypes: save: '保存オプション' controls: @@ -2160,6 +2162,8 @@ ja: clear_topic_flags: "完了" clear_topic_flags_title: "このトピックについての問題が解決されました。「完了」をクリックして通報の対応を完了します。" more: "(more replies...)" + suspend_user: "ユーザを凍結" + suspend_user_title: "この投稿においてユーザを凍結" dispositions: agreed: "賛成しました。" disagreed: "反対する" @@ -2170,7 +2174,7 @@ ja: system: "システム" error: "何らかの理由でうまくいきませんでした" reply_message: "返信する" - no_results: "通報はありません。" + no_results: "通報された投稿はありません。" topic_flagged: "この トピック は通報されました。" visit_topic: "トピックを確認" was_edited: "最初の通報後に編集された投稿" @@ -2284,6 +2288,8 @@ ja: without_uploads: "はい(ファイルは含まない)" download: label: "ダウンロード" + title: "ダウンロードリンクをメールで送る" + alert: "バックアップのダウンロードリンクがメールで送られました。" destroy: title: "バックアップを削除" confirm: "このバックアップを削除しますか?" @@ -2329,8 +2335,25 @@ ja: subject: "件名" multiple_subjects: "このメールのテンプレートは複数の件名があります。" none_selected: "編集するメールテンプレートを選択してください。" + revert: "変更を元に戻す" + theme: + edit: "編集" + desktop: "デスクトップ" + mobile: "モバイル" + preview: "プレビュー" + upload: "アップロード" + license: "ライセンス" + check_for_updates: "アップデートを確認" + updating: "アップデート中..." + scss: + text: "CSS" + head_tag: + text: "" + body_tag: + text: "" colors: title: "カラー" + edit: "カラースキームの編集" long_title: "カラースキーム" new_name: "カラースキームを作成" copy_name_prefix: "のコピー" @@ -2416,6 +2439,9 @@ ja: subject: "件名" body: "本文" filters: + from_placeholder: "from@example.com" + to_placeholder: "to@example.com" + cc_placeholder: "cc@example.com" subject_placeholder: "件名..." error_placeholder: "エラー" logs: @@ -2488,7 +2514,12 @@ ja: revoke_admin: "管理者権限を剥奪" grant_moderation: "モデレータ権限を付与" revoke_moderation: "モデレータ権限を剥奪" + backup_create: "バックアップを生成" + deleted_tag: "タグを削除" + renamed_tag: "タグ名変更" change_readonly_mode: "閲覧専用モードに変更する" + backup_download: "バックアップをダウンロード" + backup_destroy: "バックアップを消去" screened_emails: title: "ブロック対象アドレス" description: "新規アカウント作成時、次のメールアドレスからの登録をブロックする。" @@ -2529,6 +2560,7 @@ ja: flag: 'これらの言葉を含む投稿を許可しますが、仲裁者がこれをレビューできるよう、不適切であると通報されます。' form: label: '新しい単語:' + placeholder_regexp: "正規表現" add: '追加' success: '成功' upload: "アップロード" @@ -2586,9 +2618,9 @@ ja: suspend_failed: "ユーザの凍結に失敗しました: {{error}}" unsuspend_failed: "ユーザの凍結解除に失敗しました: {{error}}" suspend_duration: "ユーザを何日間凍結しますか?" - suspend_duration_units: "(日)" suspend_reason_label: "アカウントを凍結する理由を説明してください。ここに書いた理由は、このユーザのプロファイルページにおいて全員が閲覧可能な状態で公開されます。またこのユーザがログインを試みた際にも表示されます。" suspend_reason: "理由" + suspend_reason_placeholder: "凍結理由" suspended_by: "凍結したユーザ" delete_all_posts: "全ての投稿を削除" suspend: "凍結" diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index 62cf421b13..55f9b73024 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -345,6 +345,11 @@ ko: closed_group: 닫힌 그룹 is_group_user: "당신은 이 그룹의 구성원입니다." allow_membership_requests: "사용자가 그룹 소유자에게 가입신청을 할 수 있도록 허용합니다" + membership_request_template: "가입 요청을 전송할 때 사용자에게 표시할 커스텀 템플릿" + membership_request: + submit: "요청 보내기" + title: "@%{group_name}에 가입 요청하기" + reason: "그룹 소유자에게 왜 이 그룹에 속해야하는지 알립니다." membership: "회원제" name: "이름" user_count: "멤버 수" @@ -506,7 +511,8 @@ ko: admin_tooltip: "이 회원은 관리자입니다." blocked_tooltip: "이 회원은 차단되었습니다" suspended_notice: "이 회원은 {{date}}까지 접근 금지 되었습니다." - suspended_reason: "이유: " + suspended_permanently: "이 회원은 일시정지되었습니다." + suspended_reason: "사유: " github_profile: "Github" email_activity_summary: "활동 요약" mailing_list_mode: @@ -990,6 +996,7 @@ ko: alt: 'Alt' select_box: default_header_text: 선택... + no_content: 검색 결과가 없습니다 filter_placeholder: 검색... emoji_picker: filter_placeholder: 이모지 찾기 @@ -1154,6 +1161,8 @@ ko: searching: "검색중..." post_format: "#{{post_number}} by {{username}}" results_page: "검색 결과" + search_google_button: "Google" + search_google_title: "이 사이트 검색" context: user: "@{{username}}의 글 검색" category: "#{{category}} 카테고리에서 검색" @@ -1185,6 +1194,7 @@ ko: seen: 읽은 것 unseen: 읽지 않은 것 wiki: 은(는) 위키입니다. + images: 이미지 포함 all_tags: 모든 태그를 포함 statuses: label: 토픽 조건 @@ -2220,8 +2230,7 @@ ko: by: "에 의해" flags: title: "신고" - old: "지난" - active: "활성화된" + active_posts: "신고된 포스트" agree: "동의" agree_title: "이 신고가 올바르고 타당함을 확인합니다" agree_flag_modal_title: "동의 및 ..." @@ -2249,6 +2258,7 @@ ko: clear_topic_flags: "완료" clear_topic_flags_title: "주제 조사를 끝냈고 이슈를 해결했습니다. 신고를 지우기 위해 완료를 클릭하세요" more: "(더 많은 답글...)" + suspend_user: "정지된 사용자" dispositions: agreed: "동의" disagreed: "반대" diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index c4c3d9ab62..28dad2f16d 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -540,6 +540,7 @@ nb_NO: admin_tooltip: "Denne brukeren er en administrator" blocked_tooltip: "Denne brukeren er blokkert" suspended_notice: "Denne brukeren er bannlyst til {{date}}." + suspended_permanently: "Brukeren er bannlyst." suspended_reason: "Begrunnelse:" github_profile: "Github" email_activity_summary: "Oppsummering av aktivitet" @@ -1409,13 +1410,17 @@ nb_NO: remove: "Fjern utløp" publish_to: "Publiser til:" when: "Når:" + public_timer_types: Emneutløp + private_timer_types: Brukerstyrte emneutløp auto_update_input: later_today: "Senere i dag" tomorrow: "I morgen" later_this_week: "Senere denne uken" this_weekend: "Denne uken" next_week: "Neste uke" + two_weeks: "To uker" next_month: "Neste måned" + forever: "For alltid" pick_date_and_time: "Velg dato og tid" set_based_on_last_post: "Lukk basert på siste post" publish_to_category: diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index 566e8d232e..0262442353 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -2578,8 +2578,9 @@ pl_PL: by: "przez" flags: title: "Flagi" - old: "Stare" - active: "Aktywność" + active_posts: "Ofllagowane wpisy" + old_posts: "Stare oflagowane wpisy" + topics: "Oflagowane tematy" agree: "Potwierdź" agree_title: "Potwierdź to zgłoszenie jako uzasadnione i poprawne" agree_flag_modal_title: "Potwierdź i…" @@ -2607,6 +2608,8 @@ pl_PL: clear_topic_flags: "Zrobione" clear_topic_flags_title: "Ten temat został sprawdzony i związane z nim problemy zostały rozwiązane. Kliknij Zrobione, aby usunąć flagi." more: "(więcej odpowiedzi…)" + suspend_user: "Zawieszony użytkownik" + suspend_user_title: "Zawieś użytkownika za ten wpis" dispositions: agreed: "potwierdzono" disagreed: "wycofano" @@ -2617,11 +2620,18 @@ pl_PL: system: "System" error: "Coś poszło nie tak" reply_message: "Odpowiedz" - no_results: "Nie ma flag." + no_results: "Nie ma żadnych oflagowanych wpisów." topic_flagged: "Ten temat został oflagowany." + show_full: "pokaż cały wpis" visit_topic: "Odwiedź temat by podjąć działania." was_edited: "Wpis został zmieniony po pierwszej fladze" previous_flags_count: "Ten wpis został do tej pory oznaczony flagą {{count}} razy." + show_details: "Pokaż szczegóły oflagowania" + flagged_topics: + topic: "Temat" + type: "Typ" + users: "Użytkownicy" + last_flagged: "Ostatnio Oflagowane" summary: action_type_3: one: "nie-na-temat" @@ -3177,6 +3187,7 @@ pl_PL: form: label: 'Nowe słowo:' placeholder: 'pełne słowo lub jako dzika karta' + placeholder_regexp: "wyrażenie regularne" add: 'Dodaj' success: 'Sukces' upload: "Prześlij" @@ -3246,10 +3257,15 @@ pl_PL: suspend_failed: "Coś poszło nie tak podczas zawieszania użytkownika {{error}}" unsuspend_failed: "Coś poszło nie tak podczas odwieszania użytkownika {{error}}" suspend_duration: "Jak długo użytkownik ma być zawieszony?" - suspend_duration_units: "(dni)" suspend_reason_label: "Dlaczego zawieszasz? Ten tekst będzie widoczny dla wszystkich na stronie profilu użytkownika i będzie wyświetlany użytkownikowi gdy ten będzie próbował się zalogować. Zachowaj zwięzłość." + suspend_reason_hidden_label: "Dlaczego zawieszasz użytkownika? Ten krótki tekst zostanie wyświetlony, gdy zawieszony użytkownik spróbuje się zalogować. " suspend_reason: "Powód" + suspend_reason_placeholder: "Powód zawieszenia" + suspend_message: "Wiadomość Email" + suspend_message_placeholder: "Ewentualnie podaj więcej informacji o zawieszeniu użytkownika, a zostaną wysłane do niego poprzez email." suspended_by: "Zawieszony przez" + suspended_until: "(do %{until})" + cant_suspend: "Nie można zawiesić tego użytkownika." delete_all_posts: "Usuń wszystkie wpisy" delete_all_posts_confirm_MF: "Zamierzasz usunąć {POSTS, plural, one {1 post} few {# posty} many {# postów} other {# postów}} i {TOPICS, plural, one {1 temat} few {# tematy} many {# tematów} other {# tematów}}. Czy jesteś pewien?" suspend: "Zawieś" diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index 2938f52dff..72a04a7448 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -1446,6 +1446,8 @@ uk: change_trust_level: "зміна рівня довіри" change_username: "зміна імені користувача" change_site_setting: "зміна налаштування сайта" + change_theme: "змінити тему" + delete_theme: "видалити тему" change_site_text: "змінити текст сайту" suspend_user: "призупинення користувача" unsuspend_user: "скасування призупинення" @@ -1489,6 +1491,15 @@ uk: ip_address: "IP-адреса" add: "Додати" filter: "Пошук" + watched_words: + search: "Пошук" + clear_filter: "Очистити" + actions: + block: 'Заблокувати' + form: + add: 'Додати' + success: 'Успіх' + upload: "Вивантажити" impersonate: not_found: "Цього користувача не вдалося знайти." users: @@ -1496,6 +1507,7 @@ uk: create: 'Додати адміністратора' last_emailed: "Останній ел. лист" not_found: "Даруйте, такого імені користувача немає в нашій системі." + id_not_found: "Даруйте, користувача за таким номером немає в нашій системі." active: "Активний(а)" show_emails: "Показати електронну пошту" nav: diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index eaaf22fead..0dcc76db84 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -1037,7 +1037,7 @@ ar: allow_index_in_robots_txt: "حدّد في robots.txt إمكانيّة فهرسة محرّكات البحث في الوبّ هذا الموقع." email_domains_blacklist: "قائمة pipe-delimited المجالات البريد الإلكتروني الذي لا يسمح للمستخدمين تسجيل حسابات مع. مثال: mailinator.com | trashmail.net" email_domains_whitelist: "قائمة pipe-delimited من مجالات البريد الإلكتروني التي يجب على المستخدمين تسجيل حسابات مع. تحذير: لن يسمح للمستخدمين مع مجالات البريد الإلكتروني الأخرى غير المذكورة هنا!" - forgot_password_strict: "لا تخبر المستخدمين بوجود الحساب عند استخدامهم حوار نسيان كلمة السّرّ." + hide_email_address_taken: "لا تخبر المستخدمين بوجود الحساب عند استخدامهم حوار نسيان كلمة السّرّ." log_out_strict: "عند الخروج، اخرج من كلّ جلسات المستخدم في كلّ الأجهزة" version_checks: "Ping ديسكورس مركزا لتحديثات الإصدار وإظهار الرسائل النسخة الجديدة على لوحة القيادة / مسؤول" new_version_emails: "إرسال بريد إلكتروني إلى عنوان contact_email عندما نسخة جديدة من ديسكورس هو متاح." diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 868d8a4888..aba151f27a 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -992,7 +992,7 @@ de: allow_index_in_robots_txt: "Suchmaschinen mittels der robots.txt Datei erlauben, die Site zu indizieren." email_domains_blacklist: "Eine durch senkrechte Striche getrennte Liste von E-Mail-Domains, die für die Registrierung neuer Konten nicht verwendet werden dürfen. Beispiel: mailinator.com|trashmail.net" email_domains_whitelist: "Eine durch senkrechte Striche getrennte Liste von E-Mail-Domains, die für die Registrierung neuer Konten verwendet werden können. ACHTUNG: Benutzer mit E-Mail-Adressen anderer Domains werden nicht zugelassen!" - forgot_password_strict: "Benutzer nicht informieren, ob ein Konto existiert, wenn sie den Passwort vergessen-Dialog verwenden." + hide_email_address_taken: "Benutzer nicht informieren, ob ein Konto existiert, wenn sie den Passwort vergessen-Dialog verwenden." log_out_strict: "Beim Abmelden ALLE Sitzungen des Benutzers auf allen Geräten beenden" version_checks: "Kontaktiere den Discourse Hub zur Überprüfung auf neue Versionen und zeige Benachrichtigungen über neue Versionen auf der Administratorkonsole an." new_version_emails: "Sende eine E-Mail an die contact_email Adresse wenn eine neue Version von Discourse verfügbar ist." diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml index bd947e7d5d..0fea629d80 100644 --- a/config/locales/server.el.yml +++ b/config/locales/server.el.yml @@ -57,6 +57,8 @@ el: topic_closed_error: "Συμβαίνει όταν καταχωρηθεί μία απάντηση για θέμα που έχει κλείσει. " bounced_email_error: "Το email είναι αναφορά αποτυχίας παράδοσης. " screened_email_error: "Συμβαίνει όταν η διεύθυνση email του αποστολέα ήταν ήδη ελεγχόμενη" + unsubscribe_not_allowed: "Συμβαίνει όταν η απεγγραφή μέσω email δεν επιτρέπεται για αυτόν τον χρήστη." + email_not_allowed: "Συμβαίνει όταν η διεύθυνση email δεν βρίσκεται στην λευκή ή βρίσκεται στην μαύρη λίστα." unrecognized_error: "Άγνωστο Σφάλμα" errors: &errors format: '%{attribute} %{message}' @@ -979,6 +981,7 @@ el: gtm_container_id: "Google Tag Manager container id. eg: GTM-ABCDEF" enable_escaped_fragments: "Επιλογή του Ajax-Crawling API της Google, αν δεν βρεθεί κάποιο πρόγραμμα ανίχνευσης του Ιστού. Βλέπε https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" allow_moderators_to_create_categories: "Να επιτρέπεται στους συντονιστές να δημιουργούν νέες κατηγορίες" + crawler_user_agents: "Λίστα από user agents τα οποία θεωρούνται crawlers και τους παρέχεται static HTML αντί για JavaScript payload" cors_origins: "Επιτρεπόμενες πηγές για αιτήσεις πολλαπλής προέλευσης (cross-origin requests, CORS). Η κάθε προέλευση πρέπει να περιέχει http:// or https://. Η env μεταβλητή DISCOURSE_ENABLE_CORS πρέπει να οριστεί σε αληθινή για να ενεργοποιηθεί το CORS." use_admin_ip_whitelist: "Οι διαχειριστές μπορούν να συνδέονται μόνο αν βρίσκονται σε μια διευθυνση IP καθορισμένη στη λίστα Screened IPs (Admin > Logs > Screened Ips)." blacklist_ip_blocks: "Μια λίστα από private IP blocks η οποία ποτέ δεν θα ανιχνεύεται από το Discourse" @@ -1007,7 +1010,7 @@ el: allow_index_in_robots_txt: "Καθόρισε στο robots.txt ότι για αυτή την ιστοσελίδα επιτρέπεται να δημιουργείται κατάλογος περιεχομένων από τις μηχανές αναζήτησης." email_domains_blacklist: "Μία λίστα με διευθύνσεις email τις οποίες οι χρήστες δεν μπορούν να χρησιμοποιήσουν για να δημιουργήσουν λογαριασμό. Πχ: mailinator.com|trashmail.net" email_domains_whitelist: "Μία λίστα με διευθύνσεις email τις οποίες οι χρήστες ΘΑ ΠΡΕΠΕΙ να χρησιμοποιήσουν για να δημιουργήσουν λογαριασμό. ΠΡΟΣΟΧΗ: οι χρήστες με διευθύνσεις email οι οποίες δεν βρίσκονται σε αυτή τη λίστα δεν θα μπορούν να δημιουργήσουν λογαριασμό." - forgot_password_strict: "Να μην ενημερώνονται οι χρήστες για την ύπαρξη ενός λογαριασμού όταν χρησιμοποιούν την λειτουργία ανάκτησης κωδικού πρόσβασης." + hide_email_address_taken: "Μην ενημερώνεις του χρήστες ότι υπάρχει λογαριασμός χρήστη με αυτήν την διεύθυνση email κατά την εγγραφή και στην φόρμα επαναφοράς κωδικού." log_out_strict: "Όταν αποσυνδεθείτε, ΟΛΕΣ οι δραστηριότητες σας σε ΟΛΕΣ τις συσκευές θα αποσυνδεθούν" version_checks: "Έλεγξε το Hub του Discourse για αναβαθμίσεις και δείξε μηνύματα για νέες ενημερώσεις στην σελίδα διαχείρισης. " new_version_emails: "Αποστολή email στην contact_email διεύθυνση όταν μια νέα έκδοση του Discourse είναι διαθέσιμη." @@ -1905,6 +1908,13 @@ el: Λυπούμαστε, αλλά το email σας προς %{destination} (με τίτλο %{former_title}) απέτυχε. Η απάντησή σας στάλθηκε από μία αποκλεισμένη διεύθυνση email. Δοκιμάστε να στείλετε από κάποια άλλη διεύθυνση email ή [επικοινωνήστε με την διαχείριση](%{base_url}/about). + email_reject_not_allowed_email: + title: "Email Απερρίφθη Μη Επιτρεπτό Email" + subject_template: "[%{email_prefix}] Πρόβλημα Email -- Αποκλεισμένο Email" + text_body_template: | + Λυπούμαστε, αλλά το email προς %{destination} (με τίτλο %{former_title}) δεν λειτούργησε. + + Η απάντησή σας στάλθηκε από μία αποκλεισμένη διεύθυνση email. Δοκιμάστε να στείλετε ξανά από κάποια άλλη διεύθυνση email ή [επικοινωνήστε με συνεργάτη](%{base_url}/about). email_reject_inactive_user: title: "Απόρριψη email Μη ενεργός χρήστης" subject_template: "[%{email_prefix}] Πρόβλημα Email -- Ανενεργός Χρήστης" @@ -2350,6 +2360,9 @@ el: %{reason} %{message} + account_exists: + title: "Ο λογαριασμός υπάρχει ήδη" + subject_template: "[%{email_prefix}] Ο λογαριασμός υπάρχει ήδη" digest: why: "Μια σύντομη σύνοψη του %{site_link} από την τελευταία σου επίσκεψη στις %{last_seen_at}" since_last_visit: "Από την τελευταία σου επίσκεψη" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index e289dd0208..faf758f5c9 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -79,6 +79,8 @@ en: topic_closed_error: "Happens when a reply came in but the related topic has been closed." bounced_email_error: "Email is a bounced email report." screened_email_error: "Happens when the sender's email address was already screened." + unsubscribe_not_allowed: "Happens when unsubscribing via email is not allowed for this user." + email_not_allowed: "Happens when the email address is not on the whitelist or is on the blacklist." unrecognized_error: "Unrecognized Error" errors: &errors @@ -1063,6 +1065,7 @@ en: gtm_container_id: "Google Tag Manager container id. eg: GTM-ABCDEF" enable_escaped_fragments: "Fall back to Google's Ajax-Crawling API if no webcrawler is detected. See https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" allow_moderators_to_create_categories: "Allow moderators to create new categories" + crawler_user_agents: "List of user agents that are considered crawlers and served static HTML instead of JavaScript payload" cors_origins: "Allowed origins for cross-origin requests (CORS). Each origin must include http:// or https://. The DISCOURSE_ENABLE_CORS env variable must be set to true to enable CORS." use_admin_ip_whitelist: "Admins can only log in if they are at an IP address defined in the Screened IPs list (Admin > Logs > Screened Ips)." blacklist_ip_blocks: "A list of private IP blocks that should never be crawled by Discourse" @@ -1094,7 +1097,7 @@ en: 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" email_domains_whitelist: "A pipe-delimited list of email domains that users MUST register accounts with. WARNING: Users with email domains other than those listed will not be allowed!" - forgot_password_strict: "Don't inform users of an account's existence when they use the forgot password dialog." + hide_email_address_taken: "Don't inform users that an account exists with a given email address during signup and from the forgot password form." log_out_strict: "When logging out, log out ALL sessions for the user on all devices" version_checks: "Ping the Discourse Hub for version updates and show new version messages on the /admin dashboard" new_version_emails: "Send an email to the contact_email address when a new version of Discourse is available." @@ -2149,6 +2152,14 @@ en: Your reply was sent from a blocked email address. Try sending from another email address, or [contact a staff member](%{base_url}/about). + email_reject_not_allowed_email: + title: "Email Reject Not Allowed Email" + subject_template: "[%{email_prefix}] Email issue -- Blocked Email" + text_body_template: | + We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. + + Your reply was sent from a blocked email address. Try sending from another email address, or [contact a staff member](%{base_url}/about). + email_reject_inactive_user: title: "Email Reject Inactive User" subject_template: "[%{email_prefix}] Email issue -- Inactive User" @@ -2645,6 +2656,19 @@ en: %{message} + account_exists: + title: "Account already exists" + subject_template: "[%{email_prefix}] Account already exists" + text_body_template: | + You just tried to create an account at %{site_name}, or tried to change the email of an account to %{email}. However, an account already exists for %{email}. + + If you forgot your password, [reset it now](%{base_url}/password-reset). + + If you didn’t try to create an account for %{email} or change your email address, don’t worry – you can safely ignore this message. + + If you have any questions, [contact our friendly staff](%{base_url}/about). + + digest: why: "A brief summary of %{site_link} since your last visit on %{last_seen_at}" diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index 3292aa4679..a1a7d20f2b 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -971,7 +971,7 @@ es: allow_index_in_robots_txt: "Especificar en robots.txt que este sitio puede ser indexado por los motores de búsqueda web." email_domains_blacklist: "Una lista de dominios de correo electrónico con los que los usuarios no se podrán registrar. Ejemplo: mailinator.com|trashmail.net" email_domains_whitelist: "Una lista de dominios de email con los que los usuarios DEBERÁN registrar sus cuentas. AVISO: ¡los usuarios con un email con diferente dominio a los listados no estarán permitidos!" - forgot_password_strict: "No informar a los usuarios de la existencia de una cuenta cuando utilicen el diálogo de pérdida de contraseña." + hide_email_address_taken: "No informar a los usuarios de la existencia de una cuenta cuando utilicen el diálogo de pérdida de contraseña." log_out_strict: "Al cerrar sesión, cierra TODAS las sesiones del usuario en todos los dispositivos" version_checks: "Ping el 'Discourse Hub' para actualizaciones de versión y mostrar mensajes del número de versión en el dashboard /admin" new_version_emails: "Enviar un email a la dirección contact_email cuando esté disponible una nueva versión de Discourse." diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index 037617e819..cff8e54b35 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -929,7 +929,7 @@ fa_IR: allow_index_in_robots_txt: "در robots.txt که به این سایت اجازه دهید که در موتور‌های جستجو ایندکس شود." email_domains_blacklist: "لیست pipe-delimit دامنه های ایمیل که کاربران اجازه ندارند با آن حساب کاربری ثبت‌نام کنند. برای مثال: mailinator.com|trashmail.net" email_domains_whitelist: "لیست pipe-delimit دامنه های ایمیل که کاربران اجازه باید با آن حساب کاربری ثبت نام کنند. اخطار: کاربرانی با دامنه‌های ایمیلی به غیر از آن‌هایی که در لیست هستند اجازه ندارند. " - forgot_password_strict: "آگاه نکردن کاربران از وجود حساب کاربری در صفحه فراموشی روز عبور" + hide_email_address_taken: "آگاه نکردن کاربران از وجود حساب کاربری در صفحه فراموشی روز عبور" log_out_strict: "وقتی از سیستم خارج می شود، کاربر را از تمام session‌ها بر روی تمام دستگاه‌ها خارج کن " version_checks: "Discourse Hub را پینگ کن برای نسخه بروزرسانی و پیام نسخه جدید را صفحه آمار ادمین نشان بده." new_version_emails: "ارسال ایمیل به contact_email address وقتی نسخه جدید Discourse موجود است. " diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index 38389f578f..317dfad4bf 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -40,7 +40,7 @@ fi: incoming: default_subject: "Tämä ketju tarvitsee otsikon" show_trimmed_content: "Näytä piilotettu sisältö" - maximum_staged_user_per_email_reached: "Saavutti maksimimäärän automaattisesti luotuja tunnuksia per sähköpostiosoite" + maximum_staged_user_per_email_reached: "Saavutti maksimimäärän automaattisesti luotuja esikäyttäjiä per sähköpostiosoite." errors: empty_email_error: "Näin käy, kun saapuneessa sähköpostissa ei lue mitään." no_message_id_error: "Näin käy, kun viestin otsikkotiedoista puuttuu ID-tunniste (engl. message-ID)." @@ -57,6 +57,7 @@ fi: topic_closed_error: "Näin käy, kun vastauksen saapuessa ketju, johon viesti oli tarkoitettu, on suljettu." bounced_email_error: "Sähköposti on palautetun sähköpostin raportti" screened_email_error: "Näin käy, kun lähettäjän sähköpostiosoite on jo seulottu." + email_not_allowed: "Näin käy, kun sähköpostiosoite ei ole sallittujen listalla tai on kiellettyjen listalla." unrecognized_error: "Tuntematon virhe" errors: &errors format: '%{attribute} %{message}' @@ -567,7 +568,7 @@ fi: confirmed: "Sähköpostiosoite päivitetty." please_continue: "Jatka sivustolle %{site_name}" error: "Sähköpostiosoitteen vaihdossa tapahtui virhe. Ehkäpä tämä sähköpostiosoite on jo käytössä?" - error_staged: "Sähköpostiosoitetta muutettaessa tapahtui virhe. Osoite on automaattisesti luodun käyttäjän käytössä." + error_staged: "Sähköpostiosoitetta muutettaessa tapahtui virhe. Osoite on automaattisesti luodun esikäyttäjän käytössä." already_done: "Pahoittelut, tämä varmennuslinnkki ei ole enää voimassa. Ehkäpä sähköpostiosoitteesi on jo vaihdettu?" authorizing_old: title: "Kiitos sähköpostiosoitteesi varmentamisesta" @@ -635,6 +636,9 @@ fi: short_description: 'Äänestä viestiä' long_form: 'viestiä äänestetty' user_activity: + no_default: + self: "Et ole vielä tehnyt mitään mainittavaa." + others: "Ei ole tehnyt mitään mainittavaa." no_bookmarks: self: "Kirjanmerkeissäsi ei ole viestejä. Kirjanmerkit auttavat löytämään viestejä helposti myöhemmin." others: "Ei kirjanmerkkejä." @@ -965,6 +969,7 @@ fi: gtm_container_id: "Google Tag Manager -säiliön ID. Esim: GTM-ABCDEF" enable_escaped_fragments: "Käytä Googlen Ajax-sivustoille tarkoitettua API:a, jos webcrawleria ei tunnisteta. Katso https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" allow_moderators_to_create_categories: "Salli valvojien luoda uusia alueita" + crawler_user_agents: "Lista käyttäjäagenteista, joita pidetään hakurobotteina ja joille näytetään staattinen HTML-sivu JavaScript-tietosisällön sijaan" cors_origins: "Salli lähteet CORS-pyynnöille (cross-origin request). Jokaisen lähteen pitää sisältää http:// tai https://. DISCOURSE_ENABLE_CORS asetus pitää olla valittuna ottaaksesi CORSin käyttöön." use_admin_ip_whitelist: "Ylläpitäjät voivat kirjautua vain IP-osoitteista, jotka on määritetty Seulottavien IP:iden listassa (Ylläpito > Lokit > Seulottavat IP:t)" blacklist_ip_blocks: "Lista yksityisistä IP-blokeista, joita Discoursen ei tule käydä läpi" @@ -991,7 +996,7 @@ fi: allow_index_in_robots_txt: "Määrittele robots.txt-tiedostossa, että hakukoneet saavat luetteloida sivuston." email_domains_blacklist: "Pystyviivalla eroteltu lista sähköposti-verkkotunnuksista, joista käyttäjät eivät voi luoda tiliä. Esimerkiksi mailinator.com|trashmail.net" email_domains_whitelist: "Pystyviivalla eroteltu lista sähköposti-verkkotunnuksista, joista käyttäjien pitää luoda tilinsä. VAROITUS: Käyttäjiä, joiden sähköpostiosoite on muusta verkkotunnuksesta ei sallita!" - forgot_password_strict: "Älä paljasta tilin olemassaoloa unohtuneen salasanan kyselyssä." + hide_email_address_taken: "Älä kerro käyttäjälle, että käyttäjätili annetulla sähköpostiosoitteella on jo olemassa, kun hän liittyy palstalle tai kun hän pyytää salasanan palauttamista." log_out_strict: "Kun kirjaudutaan ulos, kirjaa käyttäjä ulos kaikilta laitteilta" version_checks: "Pingaa Discourse Hubia päivityksistä ja näytä viesti /admin hallintapaneelissa kun uusi versio on saatavilla" new_version_emails: "Lähetä sähköposti contact_email osoitteeseen kun uusi versio Discoursesta on saatavilla." @@ -1093,6 +1098,7 @@ fi: allow_all_attachments_for_group_messages: "Salli kaikki sähköpostiliitteet ryhmäviesteissä." png_to_jpg_quality: "Muunnetun JPG-tiedoston laatu (1 on huonoin laatu, 99 on paras laatu, 100 ottaa pois käytöstä)." allow_staff_to_upload_any_file_in_pm: "Salli henkilökunnan ladata minkätyyppisiä liitteitä tahansa yksityisviesteihin." + strip_image_metadata: "Poista metadata kuvista." enable_flash_video_onebox: "Ota käyttöön swf- ja flv-linkkien (Adobe Flash) onebox-tuki. VAROITUS: saattaa lisätä tietoturvariskejä." default_invitee_trust_level: "Oletus luottamustaso (0-4) kutsutuille käyttäjille." default_trust_level: "Uusien käyttäjien oletusarvoinen luottamustaso (0-4). VAROITUS! Tämän muuttaminen altistaa roskapostille." @@ -1125,6 +1131,7 @@ fi: min_trust_to_edit_post: "Viestin muokkaamiseen vaadittava luottamustaso." min_trust_to_allow_self_wiki: "Minimiluottamustaso, jolla käyttäjä voi tehdä omasta viestistään wiki-viestin." min_trust_to_send_messages: "Yksityisviestien luomiseen vaadittava luottamustaso" + min_trust_to_send_email_messages: "Vähimmäisluottamustaso, jotta voi lähettää uusia yksityisviestejä sähköpostilla (esikäyttäjille)" newuser_max_links: "Kuinka monta linkkiä uusi käyttäjä voi lisätä viestiin." newuser_max_images: "Kuinka monta kuvaa uusi käyttäjä voi lisätä viestiin." newuser_max_attachments: "Kuinka monta liitettä uusi käyttäjä voi lisätä viestiin." @@ -1199,8 +1206,8 @@ fi: unsubscribe_via_email_footer: "Liitä sähköpostiviestien alaosaan mailto: linkki, jonka avulla saaja voi lakkauttaa sähköposti-ilmoitukset" delete_email_logs_after_days: "Poista sähköpostilokit (N) päivän jälkeen. Aseta 0 säilyttääksesi ikuisesti." max_emails_per_day_per_user: "Käyttäjälle päivässä lähetettävien sähköpostien enimmäismäärä. Aseta 0, jos et halua rajoittaa." - enable_staged_users: "Luo automaattisesti käyttäjiä, kun saapuvia sähköposteja käsitellään." - maximum_staged_users_per_email: "Maksimimäärä automaattisesti luotavia tilejä, kun käsitellään saapuvaa sähköpostia." + enable_staged_users: "Luo automaattisesti esikäyttäjiä, kun saapuvia sähköposteja käsitellään." + maximum_staged_users_per_email: "Enimmäismäärä automaattisesti luotuja esikäyttäjiä, kun käsitellään saapuvaa sähköpostia." auto_generated_whitelist: "Lista sähköpostiosoitteista, joiden viestejä ei tarkasteta automaattisesti luodun sisällön osalta. Esimerkki: foo@bar.com|discourse@bar.com" block_auto_generated_emails: "Estä saapuvat sähköpostit, jotka tunnistetaan automaattisesti luoduiksi." ignore_by_title: "Jätä sähköpostit huomiotta niiden otsikon perusteella." @@ -1260,6 +1267,7 @@ fi: anonymous_posting_min_trust_level: "Anonyymin tilan käyttämiseen vaadittava luottamustila" anonymous_account_duration_minutes: "Suojellaksesi anonymiteettiä, luo uusi anonyymi tili N minuutin välein jokaiselle käyttäjälle. Esimerkki: jos arvoksi asetetaan 600, kun 600 minuuttia tulee kuluneeksi edellisestä viestistä JA käyttäjä vaihtaa anonyymiin tilaan, luodaan uusi anonyymi tili." hide_user_profiles_from_public: "Älä näytä käyttäjäkortteja, käyttäjäprofiileita tai käyttäjähakemistoa kirjautumattomille käyttäjille." + hide_suspension_reasons: "Älä näytä hyllytysten syitä julkisesti käyttäjäprofiileissa." user_website_domains_whitelist: "Käyttäjän kotisivu voi olla näiden verkkotunnusten alainen. Pystyviivoin erotettu lista." allow_profile_backgrounds: "Salli käyttäjien ladata profiilin taustakuva." sequential_replies_threshold: "Kuinka monen peräkkäisen viestin jälkeen yhdessä ketjussa käyttäjää muistutetaan peräkkäisistä vastauksista." @@ -1276,6 +1284,7 @@ fi: topic_page_title_includes_category: "Ketjusivu sisältää alueen nimen." native_app_install_banner: "Tarjoaa toistuvasti vieraileville Discoursen käyttöjärjestelmäkohtaista sovellusta." share_anonymized_statistics: "Julkaise yksilöimättömät käyttötilastot." + auto_handle_queued_age: "Käsittele automaattisesti asiat, jotka ovat odottaneet käsittelyä näin monta päivää. Lippuja lykätään. Jonossa olevat viestit ja käyttäjät hylätään. Jos asetat 0:ksi, ominaisuus ei ole käytössä." max_prints_per_hour_per_user: "Tulostuspyyntöjen (/print) enimmäismäärä (aseta 0 poistaaksesi käytöstä)" full_name_required: "Koko nimi on käyttäjäprofiilin vaadittu kohta" enable_names: "Näytä käyttäjän koko nimi profiilissa, käyttäjäkortissa ja sähköposteissa. Poista käytöstä piilottaaksesi koko nimen kaikkialla." @@ -1307,6 +1316,7 @@ fi: auto_close_topics_post_count: "Maksimimäärä viestejä ketjussa, kunnes se suljetaan automaattisesti (0 poistaaksesi käytöstä)" code_formatting_style: "Viestikentän koodipainike käyttää oletuksena tätä koodimuotoilutyyliä" max_allowed_message_recipients: "Kuinka monta vastaanottajaa yksityisviestillä voi olla." + watched_words_regular_expressions: "Tarkkaillut sanat ovat säännöllisiä lausekkeita." default_email_digest_frequency: "Kuinka usein käyttäjille lähetetään sähköpostikooste oletuksena." default_include_tl0_in_digests: "Sisällytä uusien käyttäjien viestit sähköpostikoosteisiin oletuksena. Tätä voi muuttaa käyttäjäasetuksissa." default_email_private_messages: "Lähetä oletuksena sähköposti, kun joku lähettää käyttäjälle viestin." @@ -1377,6 +1387,8 @@ fi: invalid_regex: "Säännöllinen lauseke ei kelpaa tai ei ole sallittu." email_editable_enabled: "Asetus 'email editable' on otettava pois käytöstä ennen tämän asetuksen käyttöönottoa." enable_sso_disabled: "Asetus 'enable sso' on otettava käyttöön ennen tämän asetuksen käyttöönottoa." + staged_users_disabled: "\"Esikäyttäjät\" on otettava käyttöön ennen tämän asetuksen käyttöönottoa." + reply_by_email_disabled: "Asetus \"reply by email\" täytyy ottaa käyttöön ennen tämän asetuksen käyttöönottoa." search: within_post: "#%{post_number} käyttäjältä %{username}" types: @@ -1521,6 +1533,7 @@ fi: max_new_accounts_per_registration_ip: "Rekisteröitymisiä ei oteta vastaan IP-osoitteestasi (maksimimäärä saavutettu). Ota yhteyttä henkilökuntaan." website: domain_not_allowed: "Verkkosivu ei kelpaa. Sallitus verkkotunnukset ovat: %{domains}" + auto_rejected: "Hylättiin automaattisesti iän perusteella. Katso auto_handle_queued_age -sivustoasetus." flags_reminder: flags_were_submitted: one: "Viestejä liputettiin yli tunti sitten. [Tarkastele niitä](/admin/flags)." @@ -1875,6 +1888,13 @@ fi: Vastauksesi lähetettiin estetystä sähköpostiosoitteesta. Yritä lähettää viesti toisesta sähköpostiosoitteesta tai [ota yhteyttä henkilökuntaan](%{base_url}/about). + email_reject_not_allowed_email: + title: "Sähköposti hylätty - osoite ei sallittu" + subject_template: "[%{email_prefix}] Sähköpostiongelma -- Estetty osoite" + text_body_template: | + Pahoittelemme, mutta sähköpostin lähettäminen tänne %{destination} (otsikolla %{former_title}) ei onnistunut. + + Vastauksesi on peräisin estetystä sähköpostiosoitteesta. Kokeile lähettää toisesta sähköpostiosoitteesta tai [ota yhteyttä henkilökuntaan](%{base_url}/about). email_reject_inactive_user: title: "Sähköposti hylätty - aktivoimaton käyttäjä" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Aktivoimaton käyttäjä" @@ -1923,9 +1943,17 @@ fi: email_reject_invalid_access: title: "Sähköposti hylätty - pääsy estetty" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Ei pääsyoikeutta" + text_body_template: | + Pahoittelut, sähköpostiviestiäsi tänne: %{destination} (otsikolla %{former_title}) ei voitu toimittaa. + + Käyttäjätililläsi ei ole oikeutta luoda ketjua sille alueelle. Jos uskot, että tämä johtuu virheestä, [ota yhteyttä henkilökuntaan](%{base_url}/about). email_reject_strangers_not_allowed: title: "Sähköposti hylätty - vierailla ei pääsyä" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Ei pääsyoikeutta" + text_body_template: | + Pahoittelut, sähköpostiviestisi lähetys kohteeseen %{destination} (otsikolla %{former_title}) ei onnistunut. + + Alueelle jolle lähetit viestin voivat kirjoittaa ne, joilla on käypä käyttäjätunnus ja sähköpostiosoite. Jos uskot, että tämä johtuu virheestä, [ota yhteyttä henkilökuntaan](%{base_url}/about). email_reject_invalid_post: title: "Sähköposti hylätty - viesti ei kelpaa" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Lähetysvirhe" @@ -1954,18 +1982,41 @@ fi: email_reject_reply_key: title: "Sähköposti hylätty - vastausavain" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Tuntematon vastausavain" + text_body_template: | + Pahoittelut, sähköpostiviestisi lähetys kohteeseen %{destination} (titled %{former_title}) ei onnistunut. + + Sähköpostiviestin vastaustunniste, engl. 'reply key', ei ole kelvollinen, minkä vuoksi ei tiedetä, mihin asiaan viestisi oli tarkoitus vastata. [Ota yhteyttä henkilökuntaan](%{base_url}/about). email_reject_bad_destination_address: title: "Sähköposti hylätty - tuntematon vastaanottajaosoite" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Tuntematon Vastaanottaja: -osoite" email_reject_topic_not_found: title: "Sähköposti hylätty - ketjua ei löytynyt" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Ketjua ei löytynyt" + text_body_template: | + Pahoittelut, sähköpostiviestisi lähetys kohteeseen %{destination} (otsikolla %{former_title}) ei onnistunut. + + Ketjua johon yritit kirjoittaa ei ole enää olemassa -- ehkä se poistettiin? Jos uskot, että tämä johtuu virheestä, [ota yhteyttä henkilökuntaan](%{base_url}/about). email_reject_topic_closed: title: "Sähköposti hylätty - ketju suljettu" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Suljettu ketju" + text_body_template: | + Pahoittelut, sähköpostiviestiäsi tänne: %{destination} (otsikolla %{former_title}) ei voitu toimittaa. + + Ketju, johon yritit vastata on tällä hetkellä suljettu, eikä siihen voi enää vastata. Jos uskot, että tämä johtuu virheestä, [ota yhteyttä henkilökuntaan](%{base_url}/about). email_reject_auto_generated: title: "Sähköposti hylätty - automaattivastaus" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Automaattivastaus" + text_body_template: | + Pahoittelut, sähköpostiviestisi lähetys kohteeseen %{destination} (otsikolla %{former_title}) ei onnistunut. + + Järjestelmä havaitsi viestisi olevan tietokoneen automaattisesti luoma eikä ihmisen kirjoittama, eikä viestiä voitu siksi hyväksyä. Jos uskot, että tämä johtuu virheestä, [ota yhteyttä henkilökuntaan](%{base_url}/about). + email_reject_unrecognized_error: + title: "Sähköposti hylätty - Tunnistamaton virhe" + subject_template: "[%{email_prefix}] Sähköpostiongelma -- Tunnistamaton virhe" + text_body_template: | + Pahoittelut, sähköpostiviestisi lähetys kohteeseen %{destination} (otsikolla %{former_title}) ei onnistunut. + + Viestiäsi käsiteltäessä tapahtui tunnistamaton virhe eikä sitä siksi julkaistu. Kokeile uudelleen tai [ota yhteyttä henkilökuntaan](%{base_url}/about). email_error_notification: title: "Ilmoitus sähköpostivirheestä" subject_template: "[%{email_prefix}] Sähköpostiongelma -- virhe POP-autentikoinnissa" @@ -2068,6 +2119,15 @@ fi: Palkinto myönnetään vain kahdelle käyttäjälle joka kuukausi, ja se näkyy pysyvästi [käyttäjäsivullasi](%{base_url}/my/badges). Sinusta on äkkiä tullut tärkeä osa yhteisöä. Kiitos kun liityit, ja jatka samaa rataa! + queued_posts_reminder: + title: "Muistutukset jonossa olevista viesteistä" + subject_template: + one: "Yksi viesti odottaa tarkastusta" + other: "%{count} viestiä odottaa tarkastusta" + text_body_template: | + Hei + + Uusien käyttäjien viestejä odottaa valvojan hyväksyntää. [Hyväksy tai hylkää ne täällä](%{base_url}/queued-posts). unsubscribe_link: | Jos et enää halua näitä viestejä, [klikkaa tästä](%{unsubscribe_url}). unsubscribe_link_and_mail: | @@ -2093,6 +2153,8 @@ fi: visit_link_to_respond: "[Vieraile ketjussa](%{base_url}%{url}) vastataksesi." visit_link_to_respond_pm: "[Vieraile ketjussa](%{base_url}%{url}) vastataksesi." posted_by: "Käyttäjältä %{username} %{post_date}" + user_invited_to_private_message_pm_group: + title: "Ryhmä kutsuttiin yksityiskeskusteluun" user_invited_to_private_message_pm: title: "Käyttäjä kutsuttiin yksityiskeskusteluun" subject_template: "[%{email_prefix}] %{username} kutsui sinut yksityiskeskusteluun '%{topic_title}'" @@ -2225,6 +2287,26 @@ fi: text_body_template: |2 %{message} + account_suspended: + title: "Tili hyllytetty" + subject_template: "[%{email_prefix}] Tilisi on hyllytetty" + text_body_template: | + Sinut hyllytettiin palstalta %{suspended_till} asti. + + %{reason} + + %{message} + account_exists: + title: "Tili on jo olemassa" + subject_template: "[%{email_prefix}] TIli on jo olemassa" + text_body_template: | + Yritit luoda tilin sivustolle %{site_name} tai yritit muuttaa tilin sähköpostiosoitteeksi %{email}. Sähköpostiosoitteella %{email} on kuitenkin jo tili olemassa. + + Jos unohdit salasanasi, [voit uusia sen nyt](%{base_url}/password-reset). + + Jos et yrittänyt luoda tunnusta sähköpostiosoitteella %{email} tai vaihtaa sähköpostiosoitettasi, älä huoli - voit huoletta jättää tämän viestin huomiotta. + + Jos sinulla on kysyttävää, [ota yhteyttä avuliaaseen henkilökuntaamme](%{base_url}/about). digest: why: "Lyhyt kooste siitä mitä on tapahtunut sivustolla %{site_link} viimeisimmän vierailusi jälkeen %{last_seen_at}." since_last_visit: "Viime vierailusi jälkeen" @@ -2341,6 +2423,10 @@ fi: see_more: "Lisää" search_title: "Etsi tältä sivustolta" search_google: "Google" + login_required: + welcome_message: | + ## [Tervetuloa sivustolle %{title}](#welcome) + Käyttäjätili tarvitaan. Luo tili tai kirjaudu sisään. terms_of_service: title: "Käyttöehdot" signup_form_message: 'Olen lukenut ja ymmärtänyt Käyttöehdot.' @@ -2806,6 +2892,18 @@ fi: description: Erinomaista osallistumista ensimmäisen kuukauden aikana long_description: | Tämä ansiomerkki myönnetään kahdelle uudelle käyttäjälle joka kuukausi kiitoksena erinomaisesta osallistumisestaan. Mittari on tykkäykset: kuinka usein viesteistä tykätään ja kuka tykkää. + enthusiast: + name: Intoilija + description: Vieraili 10 päivänä + long_description: Tämä ansiomerkki myönnetään, kun olet vieraillut 10 peräkkäisenä päivänä. Kiitos kun olet ollut kanssamme yli viikon ajan! + aficionado: + name: Hullaantunut + description: Vieraili 100 päivänä + long_description: Tämä ansiomerkki myönnetään, kun olet vieraillut 100 peräkkäisenä päivänä. Sehän on yli kolme kuukautta! + devotee: + name: Omistautunut + description: Vieraili 365 päivänä + long_description: Tämä ansiomerkki myönnetään, kun olet vieraillut 365 peräkkäisenä päivänä. Vau, kokonainen vuosi! badge_title_metadata: "%{display_name} -ansiomerkki sivustolla %{site_title}" admin_login: success: "Sähköposti lähetetty" @@ -2890,6 +2988,7 @@ fi: description: "Sinun tai organisaatiosi yleinen yhteydenottosivu. Näytetään Tietoja-sivulla." site_contact: label: "Automaattiset viestit" + description: "Tämän käyttäjän nimissä Discourse lähettää käyttäjille kaikki automaattiset yksityisviestit, kuten liputusvaroitukset ja ilmoitukset valmistuneista varmuuskopioista." corporate: title: "Organisaatio" description: "Nämä nimet näkyvät rekisteriselosteen ja käyttöehtojen yhteydessä, joita voit milloin vain muokata henkilökunta-alueella. Jos taustalla ei ole yritystä, voit hypätä tämän vaiheen yli toistaiseksi." diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index a6d9f8a8fa..842705b220 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -984,7 +984,7 @@ fr: allow_index_in_robots_txt: "Préciser dans robots.txt que le site est autorisé à être indexé par les robots des moteurs de recherche." email_domains_blacklist: "Liste des domaines de courriel qui ne sont pas autorisés lors de la création de compte, délimitée par des pipes. Exemple : mailinator.com|trashmail.net" email_domains_whitelist: "Liste des domaines de courriel avec lesquelles les utilisateurs DOIVENT s'enregistrer, délimités par un pipe. ATTENTION : les utilisateurs ayant une adresse de courriel sur un autre domaine ne pourront pas s'enregistrer." - forgot_password_strict: "Ne pas mentionner l'existence d'un compte utilisateur quand un utilisateur utilise le formulaire d'oubli de mot de passe." + hide_email_address_taken: "Ne pas mentionner l'existence d'un compte utilisateur quand un utilisateur utilise le formulaire d'oubli de mot de passe." log_out_strict: "Lors de la déconnexion, déconnecter TOUTES les sessions pour l'utilisateur sur tous les appareils" version_checks: "Ping les serveurs de Discourse afin d'obtenir les mises à jours et affiche les nouveaux messages d'information dans le tableau de bord /admin" new_version_emails: "Envoyer un courriel à contact_email quand une nouvelle version de Discourse est disponible." diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 9f0909d571..c3ca363b93 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -974,7 +974,7 @@ he: allow_index_in_robots_txt: "פרטו ב-robots.txt שלאתר זה מותר להיות מאונדקס על ידי מנועי חיפוש." email_domains_blacklist: "רשימה מופרדת בצינור (pipe) של דומיינים של אימייל אשר מהם משתמשים לא מורשים לרשום חשבונות. למשל: mailinator.com|trashmail.net" email_domains_whitelist: "רשימה מופרדת בצינור (pipe) אשר ר-ק ממנה משתמשים יכולים לרשום חשבונות. א-ז-ה-ר-ה: משתמשים עם אימיילים מדומיינים אחרים לא יורשו!" - forgot_password_strict: "אל תיידעו משתמשים בנוגע לקיום חשבון כשהם משתמשים באפשרות של ״שכחתי סיסמה״." + hide_email_address_taken: "אל תיידעו משתמשים בנוגע לקיום חשבון כשהם משתמשים באפשרות של ״שכחתי סיסמה״." log_out_strict: "בהתנתקות, נתקו את כל ההפעלות של המשתמש/ת בכל המכשירים" version_checks: "שלחו פינג להאב של Discourse לעדכוני גרסה וכדי להציג מסרים אודות גרסאות בלוח התצוגה ב /admin" new_version_emails: "שלחו דוא\"ל לכתובת של contact_email כשגרסה חדשה של Discourse זמינה." diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index a9ca197a7f..0fc8aacbdc 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -57,6 +57,8 @@ it: topic_closed_error: "Succede quando arriva una risposta ma l'argomento relativo è stato chiuso." bounced_email_error: "L'email rappresenta un rapporto di email tornata indietro." screened_email_error: "Succede quando l'indirizzo email del mittente era stato già vagliato." + unsubscribe_not_allowed: "Accade quando annullare l'iscrizione tramite email non è consentito per questo utente." + email_not_allowed: "Accade quando l'indirizzo email non è in whitelist o è nella blacklist." unrecognized_error: "Errore Non Riconosciuto" errors: &errors format: '%{attribute} %{message}' @@ -972,6 +974,7 @@ it: gtm_container_id: "Container id di Google Tag Manager. Es: GTM-ABCDEF" enable_escaped_fragments: "Usa le Ajax-Crawling API di Google se non viene rilevato un webcrawler. Vedi https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" allow_moderators_to_create_categories: "Permetti ai moderatori di creare nuove categorie" + crawler_user_agents: "Elenco degli user agent che sono considerati crawler e hanno servito l'HTML statico anziché il payload di JavaScript" cors_origins: "Origini permesse per richieste cross-origin (CORS). Ogni origine deve includere http:// o https://. La variabile d'ambiente DISCOURSE_ENABLE_CORS deve essere impostata come true per abilitare CORS." use_admin_ip_whitelist: "Gli amministratori possono connettersi soltanto se hanno un indirizzo IP definito nell'elenco degli IP scansionati (Amministrazione > Log > IP scansionati). " blacklist_ip_blocks: "Un elenco di blocchi di IP privati che non dovrebbero mai essere scansionati da Discourse" @@ -1000,7 +1003,7 @@ it: allow_index_in_robots_txt: "Specifica nel file robots.txt che questo sito permette l'indicizzazione da parte dei motori di ricerca." email_domains_blacklist: "Una lista di domini email separati dal carattere pipe \"|\" con cui gli utenti non possono registrare un account. Ad esempio: mailinator.com/trashmail.net" email_domains_whitelist: "Una lista di domini email delimitati da carattere pipe (|) che gli utenti DEVONO usare per poter registrare i propri account. ATTENZIONE: gli utenti con domini email differenti da questi non saranno accettati!" - forgot_password_strict: "Non informare gli utenti dell'esistenza o meno dell'account quando richiamano la funzione per la password dimenticata." + hide_email_address_taken: "Durante l'iscrizione e nel modulo per la password dimenticata, non informare gli utenti che già esiste un account con un dato indirizzo email." log_out_strict: "Quando ci si disconnette, esci da TUTTE le sessioni dell'utente su tutti i dispositivi" version_checks: "Verifica su Discourse Hub l'esistenza di aggiornamenti e mostra i messaggi per le nuove versioni nel cruscotto /admin" new_version_emails: "Invia un'email all'indirizzo contact_email quando è disponibile una nuova versione di Discourse." @@ -1243,7 +1246,7 @@ it: email_site_title: "Il titolo del sito utilizzato come mittente delle email dal sito. Impostazione di default a 'titolo' se non impostata. Se il tuo 'titolo' contiene caratteri non consentiti nelle stringhe del mittente email, utilizza questa impostazione." find_related_post_with_key: "Utilizza solo la 'reply key' per trovare il messaggio inviato. ATTENZIONE: la disattivazione di questa funzione consente di impersonare l'utente in base all'indirizzo email." minimum_topics_similar: "Quanti argomenti devono esistere prima di presentare argomenti simili quando si creano nuovi argomenti." - relative_date_duration: "Dopo quanti giorni dalla pubblicazione le date verranno mostrate in termini relativi (7g) anziché assoluti (20 feb)." + relative_date_duration: "Dopo quanti giorni dalla pubblicazione le date verranno mostrate in termini assoluti (20 feb) anziché relativi (7g)." delete_user_max_post_age: "Non permettere di cancellare utenti il cui primo messaggio è più vecchio di (x) giorni." delete_all_posts_max: "Numero massimo di messaggi che possono essere eliminati contemporaneamente con il pulsante Cancella Tutti. Se un utente ha un numero maggiore di tali messaggi, questi non possono essere eliminati contemporaneamente e l'utente non può essere cancellato." username_change_period: "Numero di giorni dall'iscrizione dopo i quali è possibile modificare il nome utente (imposta a 0 per non permettere il cambio del nome utente)." @@ -1323,6 +1326,7 @@ it: auto_close_topics_post_count: "Numero massimo di messaggi consentiti in un argomento prima di essere automaticamente chiuso (0 per disabilitare)" code_formatting_style: "Il pulsante del testo preformattato nel composer verrà predefinito a questo stile di formattazione del codice" max_allowed_message_recipients: "Numero massimo di destinatari consentiti in un messaggio privato." + watched_words_regular_expressions: "Le parole osservate sono espressioni regolari." default_email_digest_frequency: "Con quale frequenza gli utenti ricevono email riepilogative di default." default_include_tl0_in_digests: "Per impostazione predefinita, includi i messaggi dei nuovi utenti nelle email riepilogative. Gli utenti possono modificare questa impostazione nelle loro preferenze" default_email_private_messages: " Invia una email quando qualcuno scrive un messaggio ad un utente di default." @@ -1895,6 +1899,13 @@ it: text_body_template: | Spiacenti, ma il tuo messaggio email per %{destination} (intitolato %{former_title}) non ha funzionato. + La tua risposta è stata inviata da un indirizzo email bloccato. Prova a rispondere da un altro indirizzo email, o [contatta un membro dello staff](%{base_url}/about). + email_reject_not_allowed_email: + title: "Email Rifiuta Email Non Consentita" + subject_template: "[%{email_prefix}] Problema email -- Email Bloccata" + text_body_template: | + Spiacenti, ma il tuo messaggio email per %{destination} (intitolato %{former_title}) non ha funzionato. + La tua risposta è stata inviata da un indirizzo email bloccato. Prova a rispondere da un altro indirizzo email, o [contatta un membro dello staff](%{base_url}/about). email_reject_inactive_user: title: "Email Rifiutata Utente Inattivo" @@ -2337,6 +2348,17 @@ it: %{reason} %{message} + account_exists: + title: "L'account esiste già" + subject_template: "[%{email_prefix}] L'account esiste già" + text_body_template: | + Hai appena cercato di creare un account su %{site_name}, o hai tentato di cambiare l'email di un account su %{email}. Tuttavia, un account esiste già per %{email}. + + Se hai dimenticato la tua password, [reimpostala adesso](%{base_url}/password-reset). + + Se non hai cercato di creare un account per %{email} o di cambiare il tuo indirizzo email, non ti preoccupare – puoi tranquillamente ignorare questo messaggio. + + Se hai delle domande, [contatta il nostro amichevole staff](%{base_url}/about). digest: why: "Un breve sommario di %{site_link} dalla tua ultima visita il %{last_seen_at}" since_last_visit: "Dalla tua ultima visita" diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index 942e656ce2..196825c24d 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -925,7 +925,7 @@ ko: allow_index_in_robots_txt: "이 사이트가 검색엔진에 의해 인덱스되는 것을 허용합니다.(robots.txt 수정)" email_domains_blacklist: "가입 금지된 이메일 도메인 목록, 파이프 기호로 구분. 예: mailinator.com|trashmail.net" email_domains_whitelist: "가입하려면 반드시 사용해야하는 이메일 도메인 목록, 파이프 기호로 구분. 경고: 이 목록외의 도메인으로는 가입이 안됩니다!" - forgot_password_strict: "비밀번호 찾기 창에서 사용자 계정의 존재 여부를 알리지 않음." + hide_email_address_taken: "비밀번호 찾기 창에서 사용자 계정의 존재 여부를 알리지 않음." log_out_strict: "로그아웃 할때, 모든 장치에서 다같이 로그아웃" version_checks: "Dicousre Hub에 ping을 날려 버전 업데이트와 새 버전 알림을 /admin 대시보드에 보이게 합니다." new_version_emails: "사용가능한 새로운 업데이트가 있으면 등록된 contact_email 주소로 메일을 발송하여 알려줍니다." diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index 1f036294f7..784d9d9ffc 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -703,12 +703,16 @@ pl_PL: short_description: 'Zagłosuj na ten wpis' long_form: 'zagłosowano za tym wpisem' user_activity: + no_default: + others: "Brak aktywności" no_bookmarks: self: "Nie masz postów dodanych do zakładek, posty w zakładkach pozwalają ci na łatwiejszy dostęp do nich w późniejszym czasie." others: "Brak zakładek." no_likes_given: self: "Nie masz lajkowanych postów." others: "Brak lajkowanych postów." + no_replies: + others: "Brak odpowiedzi." topic_flag_types: spam: title: 'Spam' @@ -1059,7 +1063,7 @@ pl_PL: allow_index_in_robots_txt: "Określ w robots.txt, że ta strona może być indeksowana przez silniki wyszukiwania." email_domains_blacklist: "Lista rozdzielonych pionową kreską domen e-mail, z których użytkownicy nie mogą się rejestrować. Przykład: mailinator.com|trashmail.net" email_domains_whitelist: "Lista rozdzielonych pionową kreską domen e-mail, z których użytkownicy MUSZĄ się rejestrować. UWAGA: Użytkownicy z domenami e-mail innymi niż wypisane nie będą dopuszczeni!" - forgot_password_strict: "Nie informuj użytkowników o istnieniu konta kiedy używają dialogu zapomnianego hasła." + hide_email_address_taken: "Nie informuj użytkowników o istnieniu konta kiedy używają dialogu zapomnianego hasła." log_out_strict: "Po wylogowaniu wyloguj WSZYSTKIE sesje użytkownika na wszystkich urządzeniach." version_checks: "Odpytuj Discourse Hub o aktualizacje i wyświetlaj wiadomości o nowej wersji w panelu /admin" new_version_emails: "Wyślij email na adres contact_email, kiedy nowa wersja Discourse będzie dostępna." diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index b6fcdfe8d6..ace016fe07 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -897,7 +897,7 @@ pt: allow_index_in_robots_txt: "Especificar em robots.txt que este sítio permite ser indexado pelos motores de pesquisa." email_domains_blacklist: "Lista de domínios de email que os utilizadores não podem usar para registo de contas. Exemplo: mailinator.com|trashmail.net" email_domains_whitelist: "Lista de domínios de email que os utilizadores DEVEM usar para registar contas. AVISO: Utilizadores com domínios de email diferentes dos listados não serão permitidos!" - forgot_password_strict: "Não informar utilizadores da existência de uma conta quando estes usam o diálogo de palavra-passe esquecida. " + hide_email_address_taken: "Não informar utilizadores da existência de uma conta quando estes usam o diálogo de palavra-passe esquecida. " log_out_strict: "Ao terminar sessão, saia de TODAS as sessões do utilizador em todos dispositivos" version_checks: "Fazer o ping do Discourse Hub para atualização de versões e mostrar mensagens sobre novas versões no painel de administração" new_version_emails: "Enviar um email para o endereço 'contact_email' quando uma nova versão do Discourse estiver disponível." diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index a31661eacc..252743bddc 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -885,7 +885,7 @@ pt_BR: allow_index_in_robots_txt: "Especificar no robots.txt que este site é permitido de ser indexado por sistemas de busca na web." email_domains_blacklist: "Lista delimitada por barras (|) de domínios de email que não são permitidos registros de contas. Exemplo: mailinator.com|trashmail.net" email_domains_whitelist: "Lista separada por barra (|) de domínios de email que usuários DEVEM usar para registrar contas. CUIDADO: Usuário com domínio de email diferentes da lista não serão permitidos!" - forgot_password_strict: "Não informar os usuários da existência de uma conta quando eles usam o diálogo de esquecimento de senha." + hide_email_address_taken: "Não informar os usuários da existência de uma conta quando eles usam o diálogo de esquecimento de senha." log_out_strict: "Quando deslogando, deslogar TODAS as sessões do usuário em todos os dispositivos" version_checks: "Pingar Discourse Hub para atualizações de versão e exibir mensagens de versão no Painel em /admin" new_version_emails: "Enviar um email para o endereço contact_email quando uma nova versão do Discourse estiver disponível." diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index f611b19cf6..aa71025a2f 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -911,7 +911,7 @@ ro: allow_index_in_robots_txt: "Specifică în robots.txt că acest site poate fi indexat de motoarele de căutare web." email_domains_blacklist: "O listă de domenii de email separate cu simbolul | (pipe) ale căror utilizatori nu au permisiunea să înregistreze conturi. Exemplu: mailinator.com|trashmail.net" email_domains_whitelist: "O listă de domenii de email (separate cu simbolul | (pipe)) cu care utilizatorii TREBUIE să se înregistreze. ATENȚIE: utilizatorii cu alte domenii de email decât cele listate nu vor avea permisiunea să se înregistreze." - forgot_password_strict: "Nu informa utilizatorii despre existența unui cont atunci când folosesc dialogul pentru parolă uitată." + hide_email_address_taken: "Nu informa utilizatorii despre existența unui cont atunci când folosesc dialogul pentru parolă uitată." log_out_strict: "La ieșire, închide TOATE sesiunile pentru utilizator, pe toate dispozitivele." version_checks: "Verifică Hub-ul Discourse pentru actualizări și arată notificările de versiuni noi pe spațiul de lucru /admin ." new_version_emails: "Trimite un email la adresa contact_email când o nouă versiune de Discourse este disponibilă." diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index 1f94f64aea..8cc2c66904 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -848,7 +848,7 @@ sv: allow_index_in_robots_txt: "Specificera i robots.txt att den här webbplatsen tillåter att bli indexerad av sökmotorer. " email_domains_blacklist: "En pipe-avgränsad lista av alla e-postdomän som användare inte tillåts registrera konton med. Exempel: mailinator.com, trashmail.net" email_domains_whitelist: "En pipe-avgränsad lista av alla e-postdomän som användare MÅSTE registrera konton med. VARNING: Användare med e-postdomän som inte finns på listan kommer inte att tillåtas!" - forgot_password_strict: "Informera inte användare om ett kontos existens när de försöker använda dialogen vid bortglömt lösenord. " + hide_email_address_taken: "Informera inte användare om ett kontos existens när de försöker använda dialogen vid bortglömt lösenord. " log_out_strict: "Vid utloggning, logga ut ALLA sessioner för den användaren på alla apparater" version_checks: "Pinga Discourse Hubben för versionsuppdatering och visa nya versionsmeddelanden på /admin översiktspanelen" new_version_emails: "Skicka ett mejl till contact_email när en ny version av Discourse finns tillgängligt." diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index 6fb71b90ff..fbd877771a 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -737,7 +737,7 @@ tr_TR: allow_index_in_robots_txt: "robots.txt dosyasında bu sitenin arama motorları tarafından dizinlenmesine izin verildiğini belirt." email_domains_blacklist: "Kullanıcıların kayıt olurken kullanamayacağı e-posta alan adlarının, dikey çizgilerle ayrıştırılmış listesi. Örneğin: mailinator.com|trashmail.net" email_domains_whitelist: "Kullanıcıların kayıt olurken kullanmak ZORUNDA olduğu e-posta alan adlarının, dikey çizgilerle ayrıştırılmış listesi. UYARI: Bu listede yer almayan e-posta alan adları kabul edilmeyecektir!" - forgot_password_strict: "Parola sıfırlama kullanıldığında kullanıcıyı hesabın varlığı ile ilgili olarak bilgilendirme." + hide_email_address_taken: "Parola sıfırlama kullanıldığında kullanıcıyı hesabın varlığı ile ilgili olarak bilgilendirme." log_out_strict: "Çıkış yapılırken, kullanıcının tüm cihazlardaki TÜM seanslarını sonlandır" version_checks: "Discourse Hub'a sürüm güncellemeleri için haber yolla ve yeni versiyon iletilerine /admin gösterge panelinde yer ver" new_version_emails: "Discourse'un yeni sürümü çıktığında contact_email adresine e-posta gönder." diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index 1211bd3a0d..c0784b48d3 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -953,7 +953,7 @@ zh_CN: allow_index_in_robots_txt: "在 robots.txt 中详细指出这个站点允许被网页搜索引擎检索。" email_domains_blacklist: "用管道符“|”分隔的邮箱域名黑名单列表,其中的域名将不能用来注册账户,例如:mailinator.com|trashmail.net" email_domains_whitelist: "用管道符“|”分隔的电子邮箱域名的列表,用户必须使用这些邮箱域名注册。警告:用户使用不包含在这个列表里的邮箱域名,将无法成功注册。" - forgot_password_strict: "用户找回密码时不提示帐户是否存在。" + hide_email_address_taken: "用户找回密码时不提示帐户是否存在。" log_out_strict: "退出时,退出用户所有设备的会话" version_checks: "访问 Discourse Hub 来检查版本更新,并在管理面板 /admin 显示新版本信息" new_version_emails: "当新版本发布时,发送一封邮件至 contact_email 设置的地址。" diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index 791e179028..353eaf8fdb 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -890,7 +890,7 @@ zh_TW: allow_index_in_robots_txt: "在 robots.txt 中記錄這個網站允許被搜尋引擎索引的部分" email_domains_blacklist: "用管道符“|”分隔的郵箱域名黑名單列表,其中的域名將不能用來註冊賬戶,例如:mailinator.com|trashmail.net" email_domains_whitelist: "用管道符“|”分隔的電子郵箱域名的列表,用戶必須使用這些郵箱域名註冊。警告:用戶使用不包含在這個列表裡的郵箱域名,將無法成功註冊。" - forgot_password_strict: "用戶找回密碼時不提示帳戶是否存在。" + hide_email_address_taken: "用戶找回密碼時不提示帳戶是否存在。" log_out_strict: "登出時,登出用戶所有設備上的所有時段" version_checks: "訪問 Discourse Hub 來檢查版本更新,並在管理面板 /admin 顯示新版本訊息" new_version_emails: "當新版本發佈時,將會發送一封新的 EMail 至 contact_email 設定的位址" diff --git a/config/routes.rb b/config/routes.rb index 2ebd987a42..b3df2161ea 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -193,7 +193,7 @@ Discourse::Application.routes.draw do post "flags/disagree/:id" => "flags#disagree" post "flags/defer/:id" => "flags#defer" - resources :flagged_topics, constraints: AdminConstraint.new + resources :flagged_topics, constraints: StaffConstraint.new resources :themes, constraints: AdminConstraint.new post "themes/import" => "themes#import" diff --git a/config/sidekiq.yml b/config/sidekiq.yml index a3ed5fc5f0..3f94e6a21b 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -1,6 +1,6 @@ --- development: - :concurrency: 1 + :concurrency: 5 :queues: - [critical,4] - [default, 2] diff --git a/config/site_settings.yml b/config/site_settings.yml index 982d5cef5c..3e42c07db8 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -321,7 +321,7 @@ login: email_domains_whitelist: default: '' type: list - forgot_password_strict: false + hide_email_address_taken: false log_out_strict: true pending_users_reminder_delay: min: -1 @@ -926,6 +926,9 @@ security: enable_escaped_fragments: true allow_index_in_robots_txt: true allow_moderators_to_create_categories: false + crawler_user_agents: + default: 'Googlebot|Mediapartners|AdsBot|curl|HTTrack|Twitterbot|facebookexternalhit|bingbot|Baiduspider|ia_archiver|Wayback Save Page|360Spider|Swiftbot|YandexBot' + type: list cors_origins: default: '' type: list @@ -1432,7 +1435,7 @@ user_api: default: '' hidden: true min_trust_level_for_user_api_key: - default: 1 + default: 0 enum: 'TrustLevelSetting' allowed_user_api_push_urls: default: '' diff --git a/config/unicorn.conf.rb b/config/unicorn.conf.rb index e0d0894cec..decabee44e 100644 --- a/config/unicorn.conf.rb +++ b/config/unicorn.conf.rb @@ -85,6 +85,11 @@ before_fork do |server, worker| end end + # preload discourse version + Discourse.git_version + Discourse.git_branch + Discourse.full_version + # get rid of rubbish so we don't share it GC.start diff --git a/db/migrate/20171003180951_rename_forgot_password_strict_setting.rb b/db/migrate/20171003180951_rename_forgot_password_strict_setting.rb new file mode 100644 index 0000000000..0a9945bb96 --- /dev/null +++ b/db/migrate/20171003180951_rename_forgot_password_strict_setting.rb @@ -0,0 +1,9 @@ +class RenameForgotPasswordStrictSetting < ActiveRecord::Migration[5.1] + def up + execute "UPDATE site_settings SET name = 'hide_email_address_taken' WHERE name = 'forgot_password_strict'" + end + + def down + execute "UPDATE site_settings SET name = 'forgot_password_strict' WHERE name = 'hide_email_address_taken'" + end +end diff --git a/docs/DEVELOPER-ADVANCED.md b/docs/DEVELOPER-ADVANCED.md index a8bf7a1b84..9b417e5d99 100644 --- a/docs/DEVELOPER-ADVANCED.md +++ b/docs/DEVELOPER-ADVANCED.md @@ -26,13 +26,9 @@ To get your Ubuntu 16.04 LTS install up and running to develop Discourse and Dis gem install bundler mailcatcher # Postgresql - sudo su postgres - createuser --createdb --superuser -Upostgres $(cat /tmp/username) + sudo -u postgres -i + createuser --superuser -Upostgres $(cat /tmp/username) psql -c "ALTER USER $(cat /tmp/username) WITH PASSWORD 'password';" - psql -c "create database discourse_development owner $(cat /tmp/username) encoding 'UTF8' TEMPLATE template0;" - psql -c "create database discourse_test owner $(cat /tmp/username) encoding 'UTF8' TEMPLATE template0;" - psql -d discourse_development -c "CREATE EXTENSION hstore;" - psql -d discourse_development -c "CREATE EXTENSION pg_trgm;" exit # Node @@ -50,8 +46,14 @@ If everything goes alright, let's clone Discourse and start hacking: git clone https://github.com/discourse/discourse.git ~/discourse cd ~/discourse bundle install - bundle exec rake db:migrate - RAILS_ENV=test bundle exec rake db:migrate + + # run this if there was a pre-existing database + bundle exec rake db:drop + RAILS_ENV=test bundle exec rake db:drop + + # time to create the database and run migrations + bundle exec rake db:create db:migrate + RAILS_ENV=test bundle exec rake db:create db:migrate # run the specs (optional) bundle exec rake autospec # CTRL + C to stop @@ -59,17 +61,19 @@ If everything goes alright, let's clone Discourse and start hacking: # launch discourse bundle exec rails s -b 0.0.0.0 # open browser on http://localhost:3000 and you should see Discourse -Create a test account, and enable it with: +Create an admin account with: - bundle exec rails c - u = User.find(1) - u.activate - u.grant_admin! - exit + bundle exec rake admin:create + +If you ever need to recreate your database: + + bundle exec rake db:drop db:create db:migrate + bundle exec rake admin:create + RAILS_ENV=test bundle exec rake db:drop db:create db:migrate Discourse does a lot of stuff async, so it's better to run sidekiq even on development mode: - ruby $(mailcatcher) # open http://localhost:1080 to see the emails, stop with pkill -f mailcatcher + mailcatcher # open http://localhost:1080 to see the emails, stop with pkill -f mailcatcher bundle exec sidekiq # open http://localhost:3000/sidekiq to see queues bundle exec rails server diff --git a/lib/crawler_detection.rb b/lib/crawler_detection.rb index a8892fdc76..5d222ecf7b 100644 --- a/lib/crawler_detection.rb +++ b/lib/crawler_detection.rb @@ -1,9 +1,17 @@ module CrawlerDetection + # added 'ia_archiver' based on https://meta.discourse.org/t/unable-to-archive-discourse-pages-with-the-internet-archive/21232 # added 'Wayback Save Page' based on https://meta.discourse.org/t/unable-to-archive-discourse-with-the-internet-archive-save-page-now-button/22875 # added 'Swiftbot' based on https://meta.discourse.org/t/how-to-add-html-markup-or-meta-tags-for-external-search-engine/28220 + def self.to_matcher(string) + escaped = string.split('|').map { |agent| Regexp.escape(agent) }.join('|') + Regexp.new(escaped) + end def self.crawler?(user_agent) - !/Googlebot|Mediapartners|AdsBot|curl|HTTrack|Twitterbot|facebookexternalhit|bingbot|Baiduspider|ia_archiver|Wayback Save Page|360Spider|Swiftbot|YandexBot/.match(user_agent).nil? + # this is done to avoid regenerating regexes + @matchers ||= {} + matcher = (@matchers[SiteSetting.crawler_user_agents] ||= to_matcher(SiteSetting.crawler_user_agents)) + matcher.match?(user_agent) end end diff --git a/lib/discourse.rb b/lib/discourse.rb index dfcc1a90bd..f09037cb43 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -327,42 +327,50 @@ module Discourse end end - def self.git_version - return $git_version if $git_version + def self.ensure_version_file_loaded + unless @version_file_loaded + version_file = "#{Rails.root}/config/version.rb" + require version_file if File.exists?(version_file) + @version_file_loaded = true + end + end - git_cmd = 'git rev-parse HEAD' - self.load_version_or_git(git_cmd, Discourse::VERSION::STRING) { $git_version } + def self.git_version + ensure_version_file_loaded + $git_version ||= + begin + git_cmd = 'git rev-parse HEAD' + self.try_git(git_cmd, Discourse::VERSION::STRING) + end end def self.git_branch - return $git_branch if $git_branch - git_cmd = 'git rev-parse --abbrev-ref HEAD' - self.load_version_or_git(git_cmd, 'unknown') { $git_branch } + ensure_version_file_loaded + $git_branch ||= + begin + git_cmd = 'git rev-parse --abbrev-ref HEAD' + self.try_git(git_cmd, 'unknown') + end end def self.full_version - return $full_version if $full_version - git_cmd = 'git describe --dirty --match "v[0-9]*"' - self.load_version_or_git(git_cmd, 'unknown') { $full_version } + ensure_version_file_loaded + $full_version ||= + begin + git_cmd = 'git describe --dirty --match "v[0-9]*"' + self.try_git(git_cmd, 'unknown') + end end - def self.load_version_or_git(git_cmd, default_value) - version_file = "#{Rails.root}/config/version.rb" + def self.try_git(git_cmd, default_value) version_value = false - if File.exists?(version_file) - require version_file - version_value = yield + begin + version_value = `#{git_cmd}`.strip + rescue + version_value = default_value end - # file does not exist or does not define the expected global variable - unless version_value - begin - version_value = `#{git_cmd}`.strip - rescue - version_value = default_value - end - end if version_value.empty? version_value = default_value end diff --git a/lib/discourse_hub.rb b/lib/discourse_hub.rb index 940800be17..3ed120aaa9 100644 --- a/lib/discourse_hub.rb +++ b/lib/discourse_hub.rb @@ -1,4 +1,5 @@ require_dependency 'version' +require_dependency 'site_setting' module DiscourseHub diff --git a/lib/discourse_redis.rb b/lib/discourse_redis.rb index bcfc120c6b..c2b49c3232 100644 --- a/lib/discourse_redis.rb +++ b/lib/discourse_redis.rb @@ -2,6 +2,7 @@ # A wrapper around redis that namespaces keys with the current site id # require_dependency 'cache' + class DiscourseRedis class FallbackHandler include Singleton @@ -14,16 +15,24 @@ class DiscourseRedis @running = false @mutex = Mutex.new @slave_config = DiscourseRedis.slave_config - @timer_task = init_timer_task @message_bus_keepalive_interval = MessageBus.keepalive_interval end def verify_master - synchronize do - return if @timer_task.running? - end + synchronize { return if @thread && @thread.alive? } - @timer_task.execute + @thread = Thread.new do + loop do + begin + thread = Thread.new { initiate_fallback_to_master } + thread.join + break if synchronize { @master } + sleep 10 + ensure + thread.kill + end + end + end end def initiate_fallback_to_master @@ -31,10 +40,10 @@ class DiscourseRedis begin slave_client = ::Redis::Client.new(@slave_config) - logger.info "#{log_prefix}: Checking connection to master server..." + logger.warn "#{log_prefix}: Checking connection to master server..." if slave_client.call([:info]).split("\r\n").include?(MASTER_LINK_STATUS) - logger.info "#{log_prefix}: Master server is active, killing all connections to slave..." + logger.warn "#{log_prefix}: Master server is active, killing all connections to slave..." self.master = true @@ -67,18 +76,8 @@ class DiscourseRedis end end - def running? - @timer_task.running? - end - private - def init_timer_task - Concurrent::TimerTask.new(execution_interval: 10) do |task| - task.shutdown if initiate_fallback_to_master - end - end - def synchronize @mutex.synchronize { yield } end @@ -101,7 +100,7 @@ class DiscourseRedis def resolve(client = nil) if !@fallback_handler.master - @fallback_handler.verify_master unless @fallback_handler.running? + @fallback_handler.verify_master return @slave_options end @@ -114,7 +113,7 @@ class DiscourseRedis rescue Redis::ConnectionError, Redis::CannotConnectError, RuntimeError => ex raise ex if ex.class == RuntimeError && ex.message != "Name or service not known" @fallback_handler.master = false - @fallback_handler.verify_master unless @fallback_handler.running? + @fallback_handler.verify_master raise ex ensure client.disconnect @@ -182,7 +181,7 @@ class DiscourseRedis :msetnx, :persist, :pexpire, :pexpireat, :psetex, :pttl, :rename, :renamenx, :rpop, :rpoplpush, :rpush, :rpushx, :sadd, :scard, :sdiff, :set, :setbit, :setex, :setnx, :setrange, :sinter, :sismember, :smembers, :sort, :spop, :srandmember, :srem, :strlen, :sunion, :ttl, :type, :watch, :zadd, :zcard, :zcount, :zincrby, :zrange, :zrangebyscore, :zrank, :zrem, :zremrangebyrank, - :zremrangebyscore, :zrevrange, :zrevrangebyscore, :zrevrank, :zrangebyscore].each do |m| + :zremrangebyscore, :zrevrange, :zrevrangebyscore, :zrevrank, :zrangebyscore, :evalsha, :eval].each do |m| define_method m do |*args| args[0] = "#{namespace}:#{args[0]}" if @namespace DiscourseRedis.ignore_readonly { @redis.send(m, *args) } diff --git a/lib/email/processor.rb b/lib/email/processor.rb index 20d2704da6..2e94811256 100644 --- a/lib/email/processor.rb +++ b/lib/email/processor.rb @@ -40,6 +40,7 @@ module Email when Email::Receiver::NoBodyDetectedError then :email_reject_empty when Email::Receiver::UserNotFoundError then :email_reject_user_not_found when Email::Receiver::ScreenedEmailError then :email_reject_screened_email + when Email::Receiver::EmailNotAllowed then :email_reject_not_allowed_email when Email::Receiver::AutoGeneratedEmailError then :email_reject_auto_generated when Email::Receiver::InactiveUserError then :email_reject_inactive_user when Email::Receiver::BlockedUserError then :email_reject_blocked_user @@ -50,6 +51,7 @@ module Email when Email::Receiver::TopicNotFoundError then :email_reject_topic_not_found when Email::Receiver::TopicClosedError then :email_reject_topic_closed when Email::Receiver::InvalidPost then :email_reject_invalid_post + when Email::Receiver::UnsubscribeNotAllowed then :email_reject_invalid_post when ActiveRecord::Rollback then :email_reject_invalid_post when Email::Receiver::InvalidPostAction then :email_reject_invalid_post_action when Discourse::InvalidAccess then :email_reject_invalid_access diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 7fae5ec3b5..9eb6a69cd8 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -30,6 +30,8 @@ module Email class TopicClosedError < ProcessingError; end class InvalidPost < ProcessingError; end class InvalidPostAction < ProcessingError; end + class UnsubscribeNotAllowed < ProcessingError; end + class EmailNotAllowed < ProcessingError; end attr_reader :incoming_email attr_reader :raw_email @@ -38,7 +40,7 @@ module Email def initialize(mail_string) raise EmptyEmailError if mail_string.blank? - @staged_users_created = 0 + @staged_users = [] @raw_email = try_to_encode(mail_string, "UTF-8") || try_to_encode(mail_string, "ISO-8859-1") || mail_string @mail = Mail.new(@raw_email) @message_id = @mail.message_id.presence || Digest::MD5.hexdigest(mail_string) @@ -82,14 +84,13 @@ module Email raise NoSenderDetectedError if @from_email.blank? raise ScreenedEmailError if ScreenedEmail.should_block?(@from_email) - user = find_or_create_user(@from_email, @from_display_name) + user = find_user(@from_email) - raise UserNotFoundError if user.nil? - - @incoming_email.update_columns(user_id: user.id) - - raise InactiveUserError if !user.active && !user.staged - raise BlockedUserError if user.blocked + if user.present? + log_and_validate_user(user) + else + raise UserNotFoundError unless SiteSetting.enable_staged_users + end body, elided = select_body body ||= "" @@ -102,9 +103,19 @@ module Email end if action = subscription_action_for(body, subject) - message = SubscriptionMailer.send(action, user) - Email::Sender.new(message, :subscription).send - elsif post = find_related_post + raise UnsubscribeNotAllowed if user.nil? + send_subscription_mail(action, user) + return + end + + # Lets create a staged user if there isn't one yet. We will try to + # delete staged users in process!() if something bad happens. + if user.nil? + user = find_or_create_user(@from_email, @from_display_name) + log_and_validate_user(user) + end + + if post = find_related_post create_reply(user: user, raw: body, elided: elided, @@ -128,6 +139,13 @@ module Email end end + def log_and_validate_user(user) + @incoming_email.update_columns(user_id: user.id) + + raise InactiveUserError if !user.active && !user.staged + raise BlockedUserError if user.blocked + end + def is_bounce? return false unless @mail.bounced? || verp @@ -310,14 +328,20 @@ module Email @suject ||= @mail.subject.presence || I18n.t("emails.incoming.default_subject", email: @from_email) end + def find_user(email) + User.find_by_email(email) + end + def find_or_create_user(email, display_name) user = nil User.transaction do - begin - user = User.find_by_email(email) + user = User.find_by_email(email) - if user.nil? && SiteSetting.enable_staged_users + if user.nil? && SiteSetting.enable_staged_users + raise EmailNotAllowed unless EmailValidator.allowed?(email) + + begin username = UserNameSuggester.sanitize_username(display_name) if display_name.present? user = User.create!( email: email, @@ -325,10 +349,10 @@ module Email name: display_name.presence || User.suggest_name(email), staged: true ) - @staged_users_created += 1 + @staged_users << user + rescue + user = nil end - rescue - user = nil end end @@ -596,6 +620,12 @@ module Email def create_post_with_attachments(options = {}) # deal with attachments + options[:raw] = add_attachments(options[:raw], options[:user].id, options) + + create_post(options) + end + + def add_attachments(raw, user_id, options = {}) attachments.each do |attachment| tmp = Tempfile.new(["discourse-email-attachment", File.extname(attachment.filename)]) begin @@ -603,19 +633,19 @@ module Email File.open(tmp.path, "w+b") { |f| f.write attachment.body.decoded } # create the upload for the user opts = { for_group_message: options[:is_group_message] } - upload = UploadCreator.new(tmp, attachment.filename, opts).create_for(options[:user].id) + upload = UploadCreator.new(tmp, attachment.filename, opts).create_for(user_id) if upload && upload.errors.empty? # try to inline images - if attachment.content_type.start_with?("image/") - if options[:raw][attachment.url] - options[:raw].sub!(attachment.url, upload.url) - elsif options[:raw][/\[image:.*?\d+[^\]]*\]/i] - options[:raw].sub!(/\[image:.*?\d+[^\]]*\]/i, attachment_markdown(upload)) + if attachment.content_type&.start_with?("image/") + if raw[attachment.url] + raw.sub!(attachment.url, upload.url) + elsif raw[/\[image:.*?\d+[^\]]*\]/i] + raw.sub!(/\[image:.*?\d+[^\]]*\]/i, attachment_markdown(upload)) else - options[:raw] << "\n\n#{attachment_markdown(upload)}\n\n" + raw << "\n\n#{attachment_markdown(upload)}\n\n" end else - options[:raw] << "\n\n#{attachment_markdown(upload)}\n\n" + raw << "\n\n#{attachment_markdown(upload)}\n\n" end end ensure @@ -623,7 +653,7 @@ module Email end end - create_post(options) + raw end def attachment_markdown(upload) @@ -662,7 +692,7 @@ module Email if result.post @incoming_email.update_columns(topic_id: result.post.topic_id, post_id: result.post.id) if result.post.topic && result.post.topic.private_message? - add_other_addresses(result.post.topic, user) + add_other_addresses(result.post, user) end end @@ -677,7 +707,7 @@ module Email html end - def add_other_addresses(topic, sender) + def add_other_addresses(post, sender) %i(to cc bcc).each do |d| if @mail[d] && @mail[d].address_list && @mail[d].address_list.addresses @mail[d].address_list.addresses.each do |address_field| @@ -688,18 +718,19 @@ module Email next unless email["@"] if should_invite?(email) user = find_or_create_user(email, display_name) - if user && can_invite?(topic, user) - topic.topic_allowed_users.create!(user_id: user.id) - topic.add_small_action(sender, "invited_user", user.username) + if user && can_invite?(post.topic, user) + post.topic.topic_allowed_users.create!(user_id: user.id) + TopicUser.auto_notification_for_staging(user.id, post.topic_id, TopicUser.notification_reasons[:auto_watch]) + post.topic.add_small_action(sender, "invited_user", user.username) end # cap number of staged users created per email - if @staged_users_created > SiteSetting.maximum_staged_users_per_email - topic.add_moderator_post(sender, I18n.t("emails.incoming.maximum_staged_user_per_email_reached")) + if @staged_users.count > SiteSetting.maximum_staged_users_per_email + post.topic.add_moderator_post(sender, I18n.t("emails.incoming.maximum_staged_user_per_email_reached")) return end end - rescue ActiveRecord::RecordInvalid - # don't care if user already allowed + rescue ActiveRecord::RecordInvalid, EmailNotAllowed + # don't care if user already allowed or the user's email address is not allowed end end end @@ -717,6 +748,10 @@ module Email !topic.topic_allowed_groups.where("group_id IN (SELECT group_id FROM group_users WHERE user_id = ?)", user.id).exists? end + def send_subscription_mail(action, user) + message = SubscriptionMailer.send(action, user) + Email::Sender.new(message, :subscription).send + end end end diff --git a/lib/email_updater.rb b/lib/email_updater.rb index 12a2878194..ddb0627fef 100644 --- a/lib/email_updater.rb +++ b/lib/email_updater.rb @@ -27,12 +27,16 @@ class EmailUpdater EmailValidator.new(attributes: :email).validate_each(self, :email, email) if existing_user = User.find_by_email(email) - error_message = 'change_email.error' - error_message << '_staged' if existing_user.staged? - errors.add(:base, I18n.t(error_message)) + if SiteSetting.hide_email_address_taken + Jobs.enqueue(:critical_user_email, type: :account_exists, user_id: existing_user.id) + else + error_message = 'change_email.error' + error_message << '_staged' if existing_user.staged? + errors.add(:base, I18n.t(error_message)) + end end - if errors.blank? + if errors.blank? && existing_user.nil? args = { old_email: @user.email, new_email: email, diff --git a/lib/freedom_patches/regexp.rb b/lib/freedom_patches/regexp.rb new file mode 100644 index 0000000000..5ff804c490 --- /dev/null +++ b/lib/freedom_patches/regexp.rb @@ -0,0 +1,9 @@ +unless ::Regexp.instance_methods.include?(:match?) + class ::Regexp + # this is the fast way of checking a regex (zero string allocs) added in Ruby 2.4 + # backfill it for now + def match?(string) + !!(string =~ self) + end + end +end diff --git a/lib/guardian/topic_guardian.rb b/lib/guardian/topic_guardian.rb index 8b8f08a12d..b8db72d315 100644 --- a/lib/guardian/topic_guardian.rb +++ b/lib/guardian/topic_guardian.rb @@ -63,7 +63,9 @@ module TopicGuardian end def can_convert_topic?(topic) + return false if topic.blank? return false if topic && topic.trashed? + return false if Category.where("topic_id = ?", topic.id).exists? return true if is_admin? is_moderator? && can_create_post?(topic) end diff --git a/lib/javascripts/widget-hbs-compiler.js.es6 b/lib/javascripts/widget-hbs-compiler.js.es6 index 3f96a64eaa..7e756f3746 100644 --- a/lib/javascripts/widget-hbs-compiler.js.es6 +++ b/lib/javascripts/widget-hbs-compiler.js.es6 @@ -1,20 +1,52 @@ function resolve(path) { - if (path.indexOf('settings') === 0) { + if (path.indexOf('settings') === 0 || path.indexOf('transformed') === 0) { return `this.${path}`; - } else if (path.indexOf('parentState') === 0) { - return `attrs._${path}`; } - return path; } +function sexp(value) { + if (value.path.original === "hash") { + + let result = []; + + value.hash.pairs.forEach(p => { + let pValue = p.value.original; + if (p.value.type === "StringLiteral") { + pValue = JSON.stringify(pValue); + } + + result.push(`"${p.key}": ${pValue}`); + }); + + return `{ ${result.join(", ")} }`; + } +} + +function argValue(arg) { + let value = arg.value; + if (value.type === "SubExpression") { + return sexp(arg.value); + } else if (value.type === "PathExpression") { + return value.original; + } else if (value.type === "StringLiteral") { + return JSON.stringify(value.value); + } +} + function mustacheValue(node, state) { let path = node.path.original; switch(path) { case 'attach': - const widgetName = node.hash.pairs.find(p => p.key === "widget").value.value; - return `this.attach("${widgetName}", state ? $.extend({}, attrs, { _parentState: state }) : attrs)`; + let widgetName = argValue(node.hash.pairs.find(p => p.key === "widget")); + + let attrs = node.hash.pairs.find(p => p.key === "attrs"); + if (attrs) { + return `this.attach(${widgetName}, ${argValue(attrs)})`; + } + return `this.attach(${widgetName}, attrs)`; + break; case 'yield': return `this.attrs.contents()`; @@ -24,7 +56,7 @@ function mustacheValue(node, state) { if (node.params[0].type === "StringLiteral") { value = `"${node.params[0].value}"`; } else if (node.params[0].type === "PathExpression") { - value = node.params[0].original; + value = resolve(node.params[0].original); } if (value) { @@ -39,7 +71,12 @@ function mustacheValue(node, state) { return `__iN("${icon}")`; break; default: - return `${resolve(path)}`; + if (node.escaped) { + return `${resolve(path)}`; + } else { + state.helpersUsed.rawHtml = true; + return `new __rH({ html: '' + ${resolve(path)} + ''})`; + } break; } } @@ -105,9 +142,13 @@ class Compiler { } break; case "BlockStatement": + let negate = ''; + switch(node.path.original) { + case 'unless': + negate = '!'; case 'if': - instructions.push(`if (${node.params[0].original}) {`); + instructions.push(`if (${negate}${resolve(node.params[0].original)}) {`); node.program.body.forEach(child => { instructions = instructions.concat(this.processNode(parentAcc, child)); }); @@ -121,7 +162,7 @@ class Compiler { instructions.push(`}`); break; case 'each': - const collection = node.params[0].original; + const collection = resolve(node.params[0].original); instructions.push(`if (${collection} && ${collection}.length) {`); instructions.push(` ${collection}.forEach(${node.program.blockParams[0]} => {`); node.program.body.forEach(child => { @@ -155,7 +196,10 @@ function compile(template) { let imports = ''; if (compiler.state.helpersUsed.iconNode) { - imports = "var __iN = Discourse.__widget_helpers.iconNode; "; + imports += "var __iN = Discourse.__widget_helpers.iconNode; "; + } + if (compiler.state.helpersUsed.rawHtml) { + imports += "var __rH = Discourse.__widget_helpers.rawHtml; "; } return `function(attrs, state) { ${imports}var _r = [];\n${code}\nreturn _r; }`; diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index e84ab6e413..db1c866baf 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -109,6 +109,12 @@ class Plugin::Instance end end + def rescue_from(exception, &block) + reloadable_patch do |plugin| + ::ApplicationController.rescue_from(exception, &block) + end + end + # Extend a class but check that the plugin is enabled # for class methods use `add_class_method` def add_to_class(class_name, attr, &block) diff --git a/lib/post_creator.rb b/lib/post_creator.rb index 42b7055823..dc28b66bce 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -92,21 +92,27 @@ class PostCreator return false end - # Make sure max_allowed_message_recipients setting is respected if @opts[:target_usernames].present? && !skip_validations? && !@user.staff? - errors[:base] << I18n.t(:max_pm_recepients, recipients_limit: SiteSetting.max_allowed_message_recipients) if @opts[:target_usernames].split(',').length > SiteSetting.max_allowed_message_recipients - return false if errors[:base].present? - end + names = @opts[:target_usernames].split(',') - # Make sure none of the users have muted the creator - names = @opts[:target_usernames] - if names.present? && !skip_validations? && !@user.staff? - users = User.where(username: names.split(',').flatten).pluck(:id, :username).to_h + # Make sure max_allowed_message_recipients setting is respected + max_allowed_message_recipients = SiteSetting.max_allowed_message_recipients + + if names.length > max_allowed_message_recipients + errors[:base] << I18n.t(:max_pm_recepients, + recipients_limit: max_allowed_message_recipients + ) + + return false + end + + # Make sure none of the users have muted the creator + users = User.where(username: names).pluck(:id, :username).to_h MutedUser.where(user_id: users.keys, muted_user_id: @user.id).pluck(:user_id).each do |m| errors[:base] << I18n.t(:not_accepting_pms, username: users[m]) + return false end - return false if errors[:base].present? end if new_topic? @@ -502,12 +508,9 @@ class PostCreator if @user.staged TopicUser.auto_notification_for_staging(@user.id, @topic.id, TopicUser.notification_reasons[:auto_watch]) - elsif @user.user_option.notification_level_when_replying === NotificationLevels.topic_levels[:watching] - TopicUser.auto_notification(@user.id, @topic.id, TopicUser.notification_reasons[:created_post], NotificationLevels.topic_levels[:watching]) - elsif @user.user_option.notification_level_when_replying === NotificationLevels.topic_levels[:regular] - TopicUser.auto_notification(@user.id, @topic.id, TopicUser.notification_reasons[:created_post], NotificationLevels.topic_levels[:regular]) else - TopicUser.auto_notification(@user.id, @topic.id, TopicUser.notification_reasons[:created_post], NotificationLevels.topic_levels[:tracking]) + notification_level = @user.user_option.notification_level_when_replying || NotificationLevels.topic_levels[:tracking] + TopicUser.auto_notification(@user.id, @topic.id, TopicUser.notification_reasons[:created_post], notification_level) end end diff --git a/lib/pretty_text/helpers.rb b/lib/pretty_text/helpers.rb index fcf66188f0..22383b3cec 100644 --- a/lib/pretty_text/helpers.rb +++ b/lib/pretty_text/helpers.rb @@ -91,7 +91,8 @@ module PrettyText if !is_tag && category = Category.query_from_hashtag_slug(text) [category.url_with_id, text] - elsif is_tag && tag = Tag.find_by_name(text.gsub!("#{tag_postfix}", '')) + elsif (!is_tag && tag = Tag.find_by(name: text)) || + (is_tag && tag = Tag.find_by(name: text.gsub!("#{tag_postfix}", ''))) ["#{Discourse.base_url}/tags/#{tag.name}", text] else nil diff --git a/lib/s3_helper.rb b/lib/s3_helper.rb index 778808a701..27348d631e 100644 --- a/lib/s3_helper.rb +++ b/lib/s3_helper.rb @@ -1,4 +1,4 @@ -require "aws-sdk" +require "aws-sdk-s3" class S3Helper @@ -46,21 +46,57 @@ class S3Helper rescue Aws::S3::Errors::NoSuchKey end - def update_tombstone_lifecycle(grace_period) - return if @tombstone_prefix.blank? + def update_lifecycle(id, days, prefix: nil) # cf. http://docs.aws.amazon.com/AmazonS3/latest/dev/object-lifecycle-mgmt.html + rule = { + id: id, + status: "Enabled", + expiration: { days: days } + } + + if prefix + rule[:prefix] = prefix + end + + rules = s3_resource.client.get_bucket_lifecycle_configuration(bucket: @s3_bucket_name).rules + + rules.delete_if do |r| + r.id == id + end + + rules.map! { |r| r.to_h } + + rules << rule + s3_resource.client.put_bucket_lifecycle(bucket: @s3_bucket_name, lifecycle_configuration: { - rules: [ - { - id: "purge-tombstone", - status: "Enabled", - expiration: { days: grace_period }, - prefix: @tombstone_prefix - } - ] - }) + rules: rules + }) + end + + def update_tombstone_lifecycle(grace_period) + return if @tombstone_prefix.blank? + update_lifecycle("purge_tombstone", grace_period, prefix: @tombstone_prefix) + end + + def list + s3_bucket.objects(prefix: @s3_bucket_folder_path) + end + + def tag_file(key, tags) + tag_array = [] + tags.each do |k, v| + tag_array << { key: k.to_s, value: v.to_s } + end + + s3_resource.client.put_object_tagging( + bucket: @s3_bucket_name, + key: key, + tagging: { + tag_set: tag_array + } + ) end private diff --git a/lib/stylesheet/manager.rb b/lib/stylesheet/manager.rb index 85cd6bd083..92442b5606 100644 --- a/lib/stylesheet/manager.rb +++ b/lib/stylesheet/manager.rb @@ -257,7 +257,7 @@ class Stylesheet::Manager def color_scheme_digest cs = theme&.color_scheme - category_updated = Category.where("uploaded_background_id IS NOT NULL").last_updated_at + category_updated = Category.where("uploaded_background_id IS NOT NULL").pluck(:updated_at).map(&:to_i).sum if cs || category_updated > 0 Digest::SHA1.hexdigest "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{Stylesheet::Manager.last_file_updated}-#{category_updated}" diff --git a/lib/tasks/posts.rake b/lib/tasks/posts.rake index 6814c5c093..6c13d53ae4 100644 --- a/lib/tasks/posts.rake +++ b/lib/tasks/posts.rake @@ -42,23 +42,18 @@ end desc 'Rebake all posts matching string/regex and optionally delay the loop' task 'posts:rebake_match', [:pattern, :type, :delay] => [:environment] do |_, args| + args.with_defaults(type: 'string') pattern = args[:pattern] - type = args[:type] - type = type.downcase if type - delay = args[:delay].to_i if args[:delay] + type = args[:type]&.downcase + delay = args[:delay]&.to_i + if !pattern puts "ERROR: Expecting rake posts:rebake_match[pattern,type,delay]" exit 1 elsif delay && delay < 1 puts "ERROR: delay parameter should be an integer and greater than 0" exit 1 - end - - if type == "regex" - search = Post.where("raw ~ ?", pattern) - elsif type == "string" || !type - search = Post.where("raw ILIKE ?", "%#{pattern}%") - else + elsif type != 'string' && type != 'regex' puts "ERROR: Expecting rake posts:rebake_match[pattern,type] where type is string or regex" exit 1 end @@ -66,7 +61,7 @@ task 'posts:rebake_match', [:pattern, :type, :delay] => [:environment] do |_, ar rebaked = 0 total = search.count - search.find_each do |post| + Post.raw_match(pattern, type).find_each do |post| rebake_post(post) print_status(rebaked += 1, total) sleep(delay) if delay @@ -130,11 +125,15 @@ task 'posts:normalize_code' => :environment do puts "#{i} posts normalized!" end -def remap_posts(find, replace = "") +def remap_posts(find, type, replace = "") i = 0 - Post.where("raw LIKE ?", "%#{find}%").each do |p| - new_raw = p.raw.dup - new_raw = new_raw.gsub!(/#{Regexp.escape(find)}/, replace) || new_raw + + Post.raw_match(find, type).find_each do |p| + new_raw = + case type + when 'string' then p.raw.gsub(/#{Regexp.escape(find)}/, replace) + when 'regex' then p.raw.gsub(/#{find}/, replace) + end if new_raw != p.raw p.revise(Discourse.system_user, { raw: new_raw }, bypass_bump: true, skip_revision: true) @@ -142,42 +141,59 @@ def remap_posts(find, replace = "") i += 1 end end + i end desc 'Remap all posts matching specific string' -task 'posts:remap', [:find, :replace] => [:environment] do |_, args| +task 'posts:remap', [:find, :replace, :type] => [:environment] do |_, args| + require 'highline/import' + args.with_defaults(type: 'string') find = args[:find] replace = args[:replace] + type = args[:type]&.downcase + if !find puts "ERROR: Expecting rake posts:remap['find','replace']" exit 1 elsif !replace puts "ERROR: Expecting rake posts:remap['find','replace']. Want to delete a word/string instead? Try rake posts:delete_word['word-to-delete']" exit 1 + elsif type != 'string' && type != 'regex' + puts "ERROR: Expecting rake posts:delete_word[pattern, type] where type is string or regex" + exit 1 + else + confirm_replace = ask("Are you sure you want to replace all #{type} occurrences of '#{find}' with '#{replace}'? (Y/n)") + exit 1 unless (confirm_replace == "" || confirm_replace.downcase == 'y') end puts "Remapping" - total = remap_posts(find, replace) + total = remap_posts(find, type, replace) puts "", "#{total} posts remapped!", "" end desc 'Delete occurrence of a word/string' -task 'posts:delete_word', [:find] => [:environment] do |_, args| +task 'posts:delete_word', [:find, :type] => [:environment] do |_, args| require 'highline/import' + args.with_defaults(type: 'string') find = args[:find] + type = args[:type]&.downcase + if !find puts "ERROR: Expecting rake posts:delete_word['word-to-delete']" exit 1 + elsif type != 'string' && type != 'regex' + puts "ERROR: Expecting rake posts:delete_word[pattern, type] where type is string or regex" + exit 1 else - confirm_replace = ask("Are you sure you want to remove all occurrences of '#{find}'? (Y/n) ") - exit 1 unless (confirm_replace == "" || confirm_replace.downcase == 'y') + confirm_delete = ask("Are you sure you want to remove all #{type} occurrences of '#{find}'? (Y/n)") + exit 1 unless (confirm_delete == "" || confirm_delete.downcase == 'y') end puts "Processing" - total = remap_posts(find) + total = remap_posts(find, type) puts "", "#{total} posts updated!", "" end @@ -222,3 +238,27 @@ task 'posts:defer_all_flags' => :environment do puts "", "#{flags_deferred} flags deferred!", "" end + +desc 'Refreshes each post that was received via email' +task 'posts:refresh_emails', [:topic_id] => [:environment] do |_, args| + posts = Post.where.not(raw_email: nil).where(via_email: true) + posts = posts.where(topic_id: args[:topic_id]) if args[:topic_id] + + updated = 0 + total = posts.count + + posts.find_each do |post| + receiver = Email::Receiver.new(post.raw_email) + + body, elided = receiver.select_body + body = receiver.add_attachments(body || '', post.user_id) + body << Email::Receiver.elided_html(elided) if elided.present? + + post.revise(Discourse.system_user, { raw: body }, skip_revision: true, skip_validations: true) + updated += 1 + + print_status(updated, total) + end + + puts "", "Done. #{updated} posts updated.", "" +end diff --git a/lib/tasks/s3.rake b/lib/tasks/s3.rake new file mode 100644 index 0000000000..a26c89ec74 --- /dev/null +++ b/lib/tasks/s3.rake @@ -0,0 +1,133 @@ +require_dependency "s3_helper" + +def brotli_s3_path(path) + ext = File.extname(path) + "#{path[0..-ext.length]}br#{ext}" +end + +def gzip_s3_path(path) + ext = File.extname(path) + "#{path[0..-ext.length]}gz#{ext}" +end + +def should_skip?(path) + return true if ENV['FORCE_S3_UPLOADS'] + @existing_assets ||= Set.new(helper.list.map(&:key)) + @existing_assets.include?('assets/' + path) +end + +def upload_asset(helper, path, recurse: true, content_type: nil, fullpath: nil, content_encoding: nil) + fullpath ||= (Rails.root + "public/assets/#{path}").to_s + + content_type ||= MiniMime.lookup_by_filename(path).content_type + + options = { + cache_control: 'max-age=31556952, public, immutable', + content_type: content_type, + acl: 'public-read', + tagging: '' + } + + if content_encoding + options[:content_encoding] = content_encoding + end + + if should_skip?(path) + puts "Skipping: #{path}" + else + puts "Uploading: #{path}" + helper.upload(fullpath, path, options) + end + + if recurse + if File.exist?(fullpath + ".br") + brotli_path = brotli_s3_path(path) + upload_asset(helper, brotli_path, + fullpath: fullpath + ".br", + recurse: false, + content_type: content_type, + content_encoding: 'br' + ) + end + + if File.exist?(fullpath + ".gz") + gzip_path = gzip_s3_path(path) + upload_asset(helper, gzip_path, + fullpath: fullpath + ".gz", + recurse: false, + content_type: content_type, + content_encoding: 'gzip' + ) + end + + if File.exist?(fullpath + ".map") + upload_asset(helper, path + ".map", recurse: false, content_type: 'application/json') + end + end +end + +def assets + cached = Rails.application.assets&.cached + manifest = Sprockets::Manifest.new(cached, Rails.root + 'public/assets', Rails.application.config.assets.manifest) + + raise Discourse::SiteSettingMissing.new("s3_upload_bucket") if SiteSetting.s3_upload_bucket.blank? + manifest.assets +end + +def helper + @helper ||= S3Helper.new(SiteSetting.s3_upload_bucket.downcase + '/assets') +end + +def in_manifest + found = [] + assets.each do |_, path| + fullpath = (Rails.root + "public/assets/#{path}").to_s + + asset_path = "assets/#{path}" + found << asset_path + + if File.exist?(fullpath + '.br') + found << brotli_s3_path(asset_path) + end + + if File.exist?(fullpath + '.gz') + found << gzip_s3_path(asset_path) + end + + if File.exist?(fullpath + '.map') + found << asset_path + '.map' + end + + end + Set.new(found) +end + +task 's3:upload_assets' => :environment do + assets.each do |name, fingerprint| + upload_asset(helper, fingerprint) + end +end + +task 's3:expire_missing_assets' => :environment do + keep = in_manifest + + count = 0 + puts "Ensuring AWS assets are tagged correctly for removal" + helper.list.each do |f| + if keep.include?(f.key) + helper.tag_file(f.key, old: true) + count += 1 + else + # ensure we do not delete this by mistake + helper.tag_file(f.key, {}) + end + end + + puts "#{count} assets were flagged for removal in 10 days" + + puts "Ensuring AWS rule exists for purging old assets" + #helper.update_lifecycle("delete_old_assets", 10, prefix: 'old=true') + + puts "Waiting on https://github.com/aws/aws-sdk-ruby/issues/1623" + +end diff --git a/lib/tasks/topics.rake b/lib/tasks/topics.rake index f0cb196f0e..bbbf56ac79 100644 --- a/lib/tasks/topics.rake +++ b/lib/tasks/topics.rake @@ -1,4 +1,4 @@ -def print_status(label, current, max) +def print_status_with_label(label, current, max) print "\r%s%9d / %d (%5.1f%%)" % [label, current, max, ((current.to_f / max.to_f) * 100).round(1)] end @@ -21,7 +21,7 @@ def close_old_topics(category) topics.find_each do |topic| topic.update_status("closed", true, Discourse.system_user) - print_status(" closing old topics: ", topics_closed += 1, total) + print_status_with_label(" closing old topics: ", topics_closed += 1, total) end end @@ -47,7 +47,7 @@ def apply_auto_close(category) topics.find_each do |topic| topic.inherit_auto_close_from_category - print_status(" applying auto-close to topics: ", topics_closed += 1, total) + print_status_with_label(" applying auto-close to topics: ", topics_closed += 1, total) end end diff --git a/lib/topic_view.rb b/lib/topic_view.rb index 57626225eb..9949a3f322 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -277,19 +277,21 @@ class TopicView def post_counts_by_user @post_counts_by_user ||= begin - return {} if @posts.blank? + post_ids = unfiltered_posts.pluck(:id) + + return {} if post_ids.blank? sql = <<~SQL - SELECT user_id, count(*) AS count_all - FROM posts - WHERE id IN (:post_ids) - AND user_id IS NOT NULL + SELECT user_id, count(*) AS count_all + FROM posts + WHERE id IN (:post_ids) + AND user_id IS NOT NULL GROUP BY user_id ORDER BY count_all DESC - LIMIT 24 + LIMIT 24 SQL - Hash[Post.exec_sql(sql, post_ids: @posts.pluck(:id)).values] + Hash[Post.exec_sql(sql, post_ids: post_ids).values] end end diff --git a/lib/validators/email_validator.rb b/lib/validators/email_validator.rb index 468c005071..64463d53cb 100644 --- a/lib/validators/email_validator.rb +++ b/lib/validators/email_validator.rb @@ -1,27 +1,32 @@ class EmailValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) - if (setting = SiteSetting.email_domains_whitelist).present? - unless email_in_restriction_setting?(setting, value) || is_developer?(value) - record.errors.add(attribute, I18n.t(:'user.email.not_allowed')) - end - elsif (setting = SiteSetting.email_domains_blacklist).present? - if email_in_restriction_setting?(setting, value) && !is_developer?(value) - record.errors.add(attribute, I18n.t(:'user.email.not_allowed')) - end + unless EmailValidator.allowed?(value) + record.errors.add(attribute, I18n.t(:'user.email.not_allowed')) end + if record.errors[attribute].blank? && value && ScreenedEmail.should_block?(value) record.errors.add(attribute, I18n.t(:'user.email.blocked')) end end - def email_in_restriction_setting?(setting, value) + def self.allowed?(email) + if (setting = SiteSetting.email_domains_whitelist).present? + return email_in_restriction_setting?(setting, email) || is_developer?(email) + elsif (setting = SiteSetting.email_domains_blacklist).present? + return !(email_in_restriction_setting?(setting, email) && !is_developer?(email)) + end + + true + end + + def self.email_in_restriction_setting?(setting, value) domains = setting.gsub('.', '\.') regexp = Regexp.new("@(.+\\.)?(#{domains})", true) value =~ regexp end - def is_developer?(value) + def self.is_developer?(value) Rails.configuration.respond_to?(:developer_emails) && Rails.configuration.developer_emails.include?(value) end diff --git a/lib/validators/regex_setting_validation.rb b/lib/validators/regex_setting_validation.rb new file mode 100644 index 0000000000..cd0df29f49 --- /dev/null +++ b/lib/validators/regex_setting_validation.rb @@ -0,0 +1,17 @@ +module RegexSettingValidation + + def initialize_regex_opts(opts = {}) + @regex = Regexp.new(opts[:regex]) if opts[:regex] + @regex_error = opts[:regex_error] || 'site_settings.errors.regex_mismatch' + end + + def regex_match?(val) + if @regex && !(val =~ @regex) + @regex_fail = true + return false + end + + true + end + +end diff --git a/lib/validators/string_setting_validator.rb b/lib/validators/string_setting_validator.rb index b7ffc2dcf3..e2dda64458 100644 --- a/lib/validators/string_setting_validator.rb +++ b/lib/validators/string_setting_validator.rb @@ -1,8 +1,10 @@ class StringSettingValidator + + include RegexSettingValidation + def initialize(opts = {}) @opts = opts - @regex = Regexp.new(opts[:regex]) if opts[:regex] - @regex_error = opts[:regex_error] || 'site_settings.errors.regex_mismatch' + initialize_regex_opts(opts) end def valid_value?(val) @@ -13,12 +15,7 @@ class StringSettingValidator return false end - if @regex && !(val =~ @regex) - @regex_fail = true - return false - end - - true + regex_match?(val) end def error_message diff --git a/lib/validators/username_setting_validator.rb b/lib/validators/username_setting_validator.rb index d9aa18ad5f..52ae5abd05 100644 --- a/lib/validators/username_setting_validator.rb +++ b/lib/validators/username_setting_validator.rb @@ -1,13 +1,21 @@ class UsernameSettingValidator + + include RegexSettingValidation + def initialize(opts = {}) @opts = opts + initialize_regex_opts(opts) end def valid_value?(val) - !val.present? || User.where(username: val).exists? + !val.present? || (User.where(username: val).exists? && regex_match?(val)) end def error_message - I18n.t('site_settings.errors.invalid_username') + if @regex_fail + I18n.t(@regex_error) + else + I18n.t('site_settings.errors.invalid_username') + end end end diff --git a/lib/version.rb b/lib/version.rb index 1db5d75b2a..6d524226ed 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -5,7 +5,7 @@ module Discourse MAJOR = 1 MINOR = 9 TINY = 0 - PRE = 'beta11' + PRE = 'beta12' STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/plugins/discourse-narrative-bot/config/locales/client.tr_TR.yml b/plugins/discourse-narrative-bot/config/locales/client.tr_TR.yml index 1b2a4b6c34..f97a347f3b 100644 --- a/plugins/discourse-narrative-bot/config/locales/client.tr_TR.yml +++ b/plugins/discourse-narrative-bot/config/locales/client.tr_TR.yml @@ -9,5 +9,5 @@ tr_TR: js: discourse_narrative_bot: welcome_post_type: - new_user_track: "Yeni başlayan kullanıcılar için öğretici adımları başlatın" + new_user_track: "Tüm yeni başlayan kullanıcılar için yeni kullanıcı öğreticisini başlatın" welcome_message: "Hızlı başlangıç rehberi ile tüm yeni kullanıcılara hoş geldin mesajı gönder" diff --git a/plugins/discourse-narrative-bot/config/locales/client.uk.yml b/plugins/discourse-narrative-bot/config/locales/client.uk.yml index 3acfff74e8..7bf1ef3dff 100644 --- a/plugins/discourse-narrative-bot/config/locales/client.uk.yml +++ b/plugins/discourse-narrative-bot/config/locales/client.uk.yml @@ -5,4 +5,9 @@ # To work with us on translations, join this project: # https://www.transifex.com/projects/p/discourse-org/ -uk: {} +uk: + js: + discourse_narrative_bot: + welcome_post_type: + new_user_track: "Переглянути посібник для нових користувачів" + welcome_message: "Надіслати всім новим користувачам - вітальне повідомлення з швидким переходом до посібника користувача" diff --git a/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml b/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml index 41c6746496..db42d8e667 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml @@ -7,7 +7,7 @@ tr_TR: site_settings: - discourse_narrative_bot_enabled: 'Botu etkinleştir' + discourse_narrative_bot_enabled: 'Discourse anlatı botunu etkinleştir' badges: certified: name: Sertifikalı diff --git a/plugins/discourse-narrative-bot/config/locales/server.uk.yml b/plugins/discourse-narrative-bot/config/locales/server.uk.yml index 3acfff74e8..c2ab7cfa66 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.uk.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.uk.yml @@ -5,4 +5,25 @@ # To work with us on translations, join this project: # https://www.transifex.com/projects/p/discourse-org/ -uk: {} +uk: + badges: + certified: + name: Сертифікований + discourse_narrative_bot: + dice: + trigger: "крутити" + quote: + '9': + author: "Брюс Лі" + magic_8_ball: + answers: + '9': "Так" + '17': "Моя відповідь - ні" + track_selector: + reset_trigger: 'розпочати' + skip_trigger: 'пропустити' + help_trigger: 'показати допомогу' + new_user_narrative: + reset_trigger: "новий користувач" + advanced_user_narrative: + reset_trigger: 'професійний користувач' diff --git a/plugins/discourse-narrative-bot/jobs/narrative_init.rb b/plugins/discourse-narrative-bot/jobs/narrative_init.rb index e2e9219bee..68fb003f11 100644 --- a/plugins/discourse-narrative-bot/jobs/narrative_init.rb +++ b/plugins/discourse-narrative-bot/jobs/narrative_init.rb @@ -1,3 +1,6 @@ +require_dependency 'i18n' +require_dependency 'user' + module Jobs class NarrativeInit < Jobs::Base sidekiq_options queue: 'critical' diff --git a/plugins/discourse-presence/plugin.rb b/plugins/discourse-presence/plugin.rb index 7d7a57ac8f..1383b5d245 100644 --- a/plugins/discourse-presence/plugin.rb +++ b/plugins/discourse-presence/plugin.rb @@ -100,71 +100,73 @@ after_initialize do before_action :ensure_logged_in def publish - data = params.permit(:response_needed, - current: [:action, :topic_id, :post_id], - previous: [:action, :topic_id, :post_id] - ) + data = params.permit( + :response_needed, + current: [:action, :topic_id, :post_id], + previous: [:action, :topic_id, :post_id] + ) - if data[:previous] && - data[:previous][:action].in?(['edit', 'reply']) + payload = {} + if data[:previous] && data[:previous][:action].in?(['edit', 'reply']) type = data[:previous][:post_id] ? 'post' : 'topic' id = data[:previous][:post_id] ? data[:previous][:post_id] : data[:previous][:topic_id] topic = if type == 'post' - Post.find_by(id: id).topic + Post.find_by(id: id)&.topic else Topic.find_by(id: id) end - guardian.ensure_can_see!(topic) + if topic + guardian.ensure_can_see!(topic) - any_changes = false - any_changes ||= Presence::PresenceManager.remove(type, id, current_user.id) - any_changes ||= Presence::PresenceManager.cleanup(type, id) + any_changes = false + any_changes ||= Presence::PresenceManager.remove(type, id, current_user.id) + any_changes ||= Presence::PresenceManager.cleanup(type, id) - users = Presence::PresenceManager.publish(type, id) if any_changes + users = Presence::PresenceManager.publish(type, id) if any_changes + end end - if data[:current] && - data[:current][:action].in?(['edit', 'reply']) - + if data[:current] && data[:current][:action].in?(['edit', 'reply']) type = data[:current][:post_id] ? 'post' : 'topic' id = data[:current][:post_id] ? data[:current][:post_id] : data[:current][:topic_id] topic = if type == 'post' - Post.find_by!(id: id).topic + Post.find_by(id: id)&.topic else - Topic.find_by!(id: id) + Topic.find_by(id: id) end - guardian.ensure_can_see!(topic) + if topic + guardian.ensure_can_see!(topic) - any_changes = false - any_changes ||= Presence::PresenceManager.add(type, id, current_user.id) - any_changes ||= Presence::PresenceManager.cleanup(type, id) + any_changes = false + any_changes ||= Presence::PresenceManager.add(type, id, current_user.id) + any_changes ||= Presence::PresenceManager.cleanup(type, id) - users = Presence::PresenceManager.publish(type, id) if any_changes + users = Presence::PresenceManager.publish(type, id) if any_changes - if data[:response_needed] - users ||= Presence::PresenceManager.get_users(type, id) + if data[:response_needed] + users ||= Presence::PresenceManager.get_users(type, id) - serialized_users = users.map { |u| BasicUserSerializer.new(u, root: false) } + serialized_users = users.map { |u| BasicUserSerializer.new(u, root: false) } - messagebus_channel = Presence::PresenceManager.get_messagebus_channel(type, id) + messagebus_channel = Presence::PresenceManager.get_messagebus_channel(type, id) - render json: { - messagebus_channel: messagebus_channel, - messagebus_id: MessageBus.last_id(messagebus_channel), - users: serialized_users - } - return + payload = { + messagebus_channel: messagebus_channel, + messagebus_id: MessageBus.last_id(messagebus_channel), + users: serialized_users + } + end end end - render json: {} + render json: payload end end diff --git a/plugins/discourse-presence/spec/presence_controller_spec.rb b/plugins/discourse-presence/spec/requests/presence_controller_spec.rb similarity index 77% rename from plugins/discourse-presence/spec/presence_controller_spec.rb rename to plugins/discourse-presence/spec/requests/presence_controller_spec.rb index 28d62753b3..2e667e0c02 100644 --- a/plugins/discourse-presence/spec/presence_controller_spec.rb +++ b/plugins/discourse-presence/spec/requests/presence_controller_spec.rb @@ -1,7 +1,6 @@ require 'rails_helper' -describe ::Presence::PresencesController, type: :request do - +describe ::Presence::PresencesController do before do SiteSetting.presence_enabled = true end @@ -13,7 +12,7 @@ describe ::Presence::PresencesController, type: :request do let(:post1) { Fabricate(:post) } let(:post2) { Fabricate(:post) } - after(:each) do + after do $redis.del("presence:topic:#{post1.topic.id}") $redis.del("presence:topic:#{post2.topic.id}") $redis.del("presence:post:#{post1.id}") @@ -36,7 +35,6 @@ describe ::Presence::PresencesController, type: :request do end it "uses guardian to secure endpoint" do - # Private message private_post = Fabricate(:private_message_post) post '/presence/publish.json', params: { @@ -45,7 +43,6 @@ describe ::Presence::PresencesController, type: :request do expect(response.code.to_i).to eq(403) - # Secure category group = Fabricate(:group) category = Fabricate(:private_category, group: group) private_topic = Fabricate(:topic, category: category) @@ -64,7 +61,7 @@ describe ::Presence::PresencesController, type: :request do } end - expect(messages.count).to eq (1) + expect(messages.count).to eq(1) data = JSON.parse(response.body) @@ -80,7 +77,7 @@ describe ::Presence::PresencesController, type: :request do } end - expect(messages.count).to eq (1) + expect(messages.count).to eq(1) data = JSON.parse(response.body) expect(data).to eq({}) @@ -93,7 +90,7 @@ describe ::Presence::PresencesController, type: :request do } end - expect(messages.count).to eq (1) + expect(messages.count).to eq(1) messages = MessageBus.track_publish do post '/presence/publish.json', params: { @@ -101,7 +98,7 @@ describe ::Presence::PresencesController, type: :request do } end - expect(messages.count).to eq (0) + expect(messages.count).to eq(0) end it "clears 'previous' state when supplied" do @@ -116,7 +113,28 @@ describe ::Presence::PresencesController, type: :request do } end - expect(messages.count).to eq (3) + expect(messages.count).to eq(3) + end + + describe 'when post has been deleted' do + it 'should return an empty response' do + post1.destroy! + + post '/presence/publish.json', params: { + current: { compose_state: 'open', action: 'edit', post_id: post1.id } + } + + expect(response.status).to eq(200) + expect(JSON.parse(response.body)).to eq({}) + + post '/presence/publish.json', params: { + current: { compose_state: 'open', action: 'edit', post_id: post2.id }, + previous: { compose_state: 'open', action: 'edit', post_id: post1.id } + } + + expect(response.status).to eq(200) + expect(JSON.parse(response.body)).to eq({}) + end end end diff --git a/script/discourse b/script/discourse index 9013c87b79..85b7fa491a 100755 --- a/script/discourse +++ b/script/discourse @@ -87,11 +87,17 @@ class DiscourseCLI < Thor desc "restore", "Restore a Discourse backup" def restore(filename = nil) + if File.exist?('/usr/local/bin/discourse') + discourse = 'discourse' + else + discourse = './script/discourse' + end + if !filename puts "You must provide a filename to restore. Did you mean one of the following?\n\n" - Dir["public/backups/default/*"].each do |f| - puts "discourse restore #{File.basename(f)}" + Dir["public/backups/default/*"].sort_by { |filename| File.mtime(filename) }.reverse.each do |f| + puts "#{discourse} restore #{File.basename(f)}" end return @@ -110,7 +116,8 @@ class DiscourseCLI < Thor puts '', 'The filename argument was missing.', '' usage rescue BackupRestore::RestoreDisabledError - puts '', 'Restores are not allowed.', 'An admin needs to set allow_restore to true in the site settings before restores can be run.', '' + puts '', 'Restores are not allowed.', 'An admin needs to set allow_restore to true in the site settings before restores can be run.' + puts "Enable now with", '', "#{discourse} enable_restore", '' puts 'Restore cancelled.', '' end diff --git a/script/test_hbs_compiler.rb b/script/test_hbs_compiler.rb new file mode 100644 index 0000000000..0045b94254 --- /dev/null +++ b/script/test_hbs_compiler.rb @@ -0,0 +1,38 @@ +template = <<~HBS + {{attach widget="widget-name" attrs=attrs}} + {{a}} + {{{htmlValue}}} + {{#if state.category}} + {{attach widget="category-display" attrs=(hash category=state.category someNumber=123 someString="wat")}} + {{/if}} + {{#each transformed.something as |s|}} + {{s.wat}} + {{/each}} + + {{attach widget=settings.widgetName}} + + {{#unless settings.hello}} + XYZ + {{/unless}} +HBS + +ctx = MiniRacer::Context.new(timeout: 15000) +ctx.eval("var self = this; #{File.read("#{Rails.root}/vendor/assets/javascripts/babel.js")}") +ctx.eval(File.read(Ember::Source.bundled_path_for('ember-template-compiler.js'))) +ctx.eval("module = {}; exports = {};"); +ctx.attach("rails.logger.info", proc { |err| puts(err.to_s) }) +ctx.attach("rails.logger.error", proc { |err| puts(err.to_s) }) +ctx.eval <#unknown::tag #known

- HTML + [ + "#unknown::tag", + "#known", + "#known", + "#testing" + ].each do |element| - expect(cooked).to eq(html.strip) + expect(cooked).to include(element) + end cooked = PrettyText.cook("[`a` #known::tag here](http://somesite.com)") diff --git a/spec/components/scheduler/manager_spec.rb b/spec/components/scheduler/manager_spec.rb index 0a229eed32..6ae829c558 100644 --- a/spec/components/scheduler/manager_spec.rb +++ b/spec/components/scheduler/manager_spec.rb @@ -70,14 +70,13 @@ describe Scheduler::Manager do manager.remove(Testing::SuperLongJob) manager.remove(Testing::PerHostJob) $redis.flushall - expect(ActiveRecord::Base.connection_pool.connections.reject { |c| !c.in_use? }.length).to eq(1) # connections that are not in use must be removed # otherwise active record gets super confused ActiveRecord::Base.connection_pool.connections.reject { |c| c.in_use? }.each do |c| ActiveRecord::Base.connection_pool.remove(c) end - expect(ActiveRecord::Base.connection_pool.connections.length).to eq(1) + expect(ActiveRecord::Base.connection_pool.connections.length).to (be <= 1) on_thread_mismatch = lambda do current = Thread.list.map { |t| t.object_id } diff --git a/spec/components/site_setting_extension_spec.rb b/spec/components/site_setting_extension_spec.rb index 5d87e8f983..1c2d8cb7f9 100644 --- a/spec/components/site_setting_extension_spec.rb +++ b/spec/components/site_setting_extension_spec.rb @@ -145,11 +145,11 @@ describe SiteSettingExtension do settings.setting("test_setting", 100) settings.setting("test_setting", nil, client: true) - messages = MessageBus.track_publish do + message = MessageBus.track_publish('/client_settings') do settings.test_setting = 88 - end + end.first - expect(messages.map(&:channel).include?('/client_settings')).to eq(true) + expect(message).to be_present end end end diff --git a/spec/components/stylesheet/manager_spec.rb b/spec/components/stylesheet/manager_spec.rb index 98a28e4b9e..07571887fa 100644 --- a/spec/components/stylesheet/manager_spec.rb +++ b/spec/components/stylesheet/manager_spec.rb @@ -64,4 +64,30 @@ describe Stylesheet::Manager do # our theme better have a name with the theme_id as part of it expect(new_link).to include("/stylesheets/desktop_theme_#{theme.id}_") end + + describe 'color_scheme_digest' do + it "changes with category background image" do + theme = Theme.new( + name: 'parent', + user_id: -1 + ) + category1 = Fabricate(:category, uploaded_background_id: 123, updated_at: 1.week.ago) + category2 = Fabricate(:category, uploaded_background_id: 456, updated_at: 2.days.ago) + + manager = Stylesheet::Manager.new(:desktop_theme, theme.key) + + digest1 = manager.color_scheme_digest + + category2.update_attributes(uploaded_background_id: 789, updated_at: 1.day.ago) + + digest2 = manager.color_scheme_digest + expect(digest2).to_not eq(digest1) + + category1.update_attributes(uploaded_background_id: nil, updated_at: 5.minutes.ago) + + digest3 = manager.color_scheme_digest + expect(digest3).to_not eq(digest2) + expect(digest3).to_not eq(digest1) + end + end end diff --git a/spec/components/validators/email_validator_spec.rb b/spec/components/validators/email_validator_spec.rb index 5b06cafe57..71f6bb746a 100644 --- a/spec/components/validators/email_validator_spec.rb +++ b/spec/components/validators/email_validator_spec.rb @@ -30,6 +30,14 @@ describe EmailValidator do expect(blocks?('sam@e-mail.com')).to eq(true) expect(blocks?('sam@googlemail.com')).to eq(false) end + + it "blocks based on email_domains_whitelist" do + SiteSetting.email_domains_whitelist = "googlemail.com|email.com" + expect(blocks?('sam@email.com')).to eq(false) + expect(blocks?('sam@bob.email.com')).to eq(false) + expect(blocks?('sam@e-mail.com')).to eq(true) + expect(blocks?('sam@googlemail.com')).to eq(false) + end end context '.email_regex' do diff --git a/spec/components/validators/username_setting_validator_spec.rb b/spec/components/validators/username_setting_validator_spec.rb index 8e06e4fd83..8b302bba35 100644 --- a/spec/components/validators/username_setting_validator_spec.rb +++ b/spec/components/validators/username_setting_validator_spec.rb @@ -17,5 +17,26 @@ describe UsernameSettingValidator do it "returns false if value does not match a user's username" do expect(validator.valid_value?('no way')).to eq(false) end + + context "regex support" do + let!(:darthvader) { Fabricate(:user, username: 'darthvader') } + let!(:luke) { Fabricate(:user, username: 'luke') } + + it "returns false if regex doesn't match" do + v = described_class.new(regex: 'darth') + expect(v.valid_value?('luke')).to eq(false) + expect(v.valid_value?('vader')).to eq(false) + end + + it "returns true if regex matches" do + v = described_class.new(regex: 'darth') + expect(v.valid_value?('darthvader')).to eq(true) + end + + it "returns false if regex matches but username doesn't match a user" do + v = described_class.new(regex: 'darth') + expect(v.valid_value?('darthmaul')).to eq(false) + end + end end end diff --git a/spec/controllers/list_controller_spec.rb b/spec/controllers/list_controller_spec.rb index 3de5a762b2..412833aa17 100644 --- a/spec/controllers/list_controller_spec.rb +++ b/spec/controllers/list_controller_spec.rb @@ -28,11 +28,6 @@ describe ListController do parsed = JSON.parse(response.body) expect(parsed["topic_list"]["topics"].length).to eq(1) end - - it "doesn't throw an error with a negative page" do - get :top, params: { page: -1024 } - expect(response).to be_success - end end describe 'RSS feeds' do diff --git a/spec/controllers/tags_controller_spec.rb b/spec/controllers/tags_controller_spec.rb index f0e296345d..62c8c55d5a 100644 --- a/spec/controllers/tags_controller_spec.rb +++ b/spec/controllers/tags_controller_spec.rb @@ -190,4 +190,30 @@ describe TagsController do end end end + + describe 'destroy' do + context 'tagging enabled' do + before do + log_in(:admin) + SiteSetting.tagging_enabled = true + end + + context 'with an existent tag name' do + it 'deletes the tag' do + tag = Fabricate(:tag) + delete :destroy, params: { tag_id: tag.name }, format: :json + expect(response).to be_success + end + end + + context 'with a nonexistent tag name' do + it 'returns a tag not found message' do + delete :destroy, params: { tag_id: 'idontexist' }, format: :json + expect(response).not_to be_success + json = ::JSON.parse(response.body) + expect(json['error_type']).to eq('not_found') + end + end + end + end end diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb index 15e6d6f606..74b02ba222 100644 --- a/spec/controllers/topics_controller_spec.rb +++ b/spec/controllers/topics_controller_spec.rb @@ -1059,12 +1059,14 @@ describe TopicsController do end it 'returns 403 for an invalid key' do - get :show, params: { - topic_id: topic.id, slug: topic.slug, api_key: "bad" - }, format: :json + [:json, :html].each do |format| + get :show, params: { + topic_id: topic.id, slug: topic.slug, api_key: "bad" + }, format: format - expect(response.code.to_i).to be(403) - expect(response.body).to eq(I18n.t("invalid_access")) + expect(response.code.to_i).to be(403) + expect(response.body).to eq(I18n.t("invalid_access")) + end end end end diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 58819817aa..f8cf43eb75 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -52,13 +52,11 @@ describe UploadsController do it 'is successful with an image' do Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything) - message = MessageBus.track_publish do + message = MessageBus.track_publish('/uploads/avatar') do post :create, params: { file: logo, type: "avatar", format: :json } - end.find { |m| m.channel == "/uploads/avatar" } + end.first expect(response.status).to eq 200 - - expect(message.channel).to eq("/uploads/avatar") expect(message.data["id"]).to be end @@ -67,12 +65,11 @@ describe UploadsController do Jobs.expects(:enqueue).never - message = MessageBus.track_publish do + message = MessageBus.track_publish('/uploads/composer') do post :create, params: { file: text_file, type: "composer", format: :json } - end.find { |m| m.channel == "/uploads/composer" } + end.first expect(response.status).to eq 200 - expect(message.channel).to eq("/uploads/composer") expect(message.data["id"]).to be end @@ -103,7 +100,7 @@ describe UploadsController do log_in :admin Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything).never - message = MessageBus.track_publish do + message = MessageBus.track_publish('/uploads/profile_background') do post :create, params: { file: logo, retain_hours: 100, @@ -119,7 +116,7 @@ describe UploadsController do it 'requires a file' do Jobs.expects(:enqueue).never - message = MessageBus.track_publish do + message = MessageBus.track_publish('/uploads/composer') do post :create, params: { type: "composer", format: :json } end.first @@ -132,7 +129,7 @@ describe UploadsController do Jobs.expects(:enqueue).never - message = MessageBus.track_publish do + message = MessageBus.track_publish("/uploads/avatar") do post :create, params: { file: text_file, type: "avatar", format: :json } end.first @@ -157,7 +154,7 @@ describe UploadsController do SiteSetting.allow_staff_to_upload_any_file_in_pm = true @user.update_columns(moderator: true) - message = MessageBus.track_publish do + message = MessageBus.track_publish('/uploads/composer') do post :create, params: { file: text_file, type: "composer", @@ -173,13 +170,11 @@ describe UploadsController do it 'returns an error when it could not determine the dimensions of an image' do Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything).never - message = MessageBus.track_publish do + message = MessageBus.track_publish('/uploads/composer') do post :create, params: { file: fake_jpg, type: "composer", format: :json } - end.find { |m| m.channel == '/uploads/composer' } + end.first expect(response.status).to eq 200 - - expect(message.channel).to eq("/uploads/composer") expect(message.data["errors"]).to contain_exactly(I18n.t("upload.images.size_not_found")) end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index a6906c06ef..258d788f9b 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -621,6 +621,28 @@ describe UsersController do expect(session[SessionController::ACTIVATE_USER_KEY]).to be_present end end + + context 'users already exists with given email' do + let!(:existing) { Fabricate(:user, email: post_user_params[:email]) } + + it 'returns an error if hide_email_address_taken is disabled' do + SiteSetting.hide_email_address_taken = false + post_user + json = JSON.parse(response.body) + expect(json['success']).to eq(false) + expect(json['message']).to be_present + end + + it 'returns success if hide_email_address_taken is enabled' do + SiteSetting.hide_email_address_taken = true + expect { + post_user + }.to_not change { User.count } + json = JSON.parse(response.body) + expect(json['active']).to be_falsey + expect(session["user_created_message"]).to be_present + end + end end context "creating as active" do @@ -1601,88 +1623,6 @@ describe UsersController do end end - describe "search_users" do - - let(:topic) { Fabricate :topic } - let(:user) { Fabricate :user, username: "joecabot", name: "Lawrence Tierney" } - - before do - SearchIndexer.enable - Fabricate :post, user: user, topic: topic - end - - it "searches when provided the term only" do - post :search_users, params: { term: user.name.split(" ").last }, format: :json - expect(response).to be_success - json = JSON.parse(response.body) - expect(json["users"].map { |u| u["username"] }).to include(user.username) - end - - it "searches when provided the topic only" do - post :search_users, params: { topic_id: topic.id }, format: :json - expect(response).to be_success - json = JSON.parse(response.body) - expect(json["users"].map { |u| u["username"] }).to include(user.username) - end - - it "searches when provided the term and topic" do - post :search_users, params: { - term: user.name.split(" ").last, topic_id: topic.id - }, format: :json - - expect(response).to be_success - json = JSON.parse(response.body) - expect(json["users"].map { |u| u["username"] }).to include(user.username) - end - - it "searches only for users who have access to private topic" do - privileged_user = Fabricate(:user, trust_level: 4, username: "joecabit", name: "Lawrence Tierney") - privileged_group = Fabricate(:group) - privileged_group.add(privileged_user) - privileged_group.save - - category = Fabricate(:category) - category.set_permissions(privileged_group => :readonly) - category.save - - private_topic = Fabricate(:topic, category: category) - - post :search_users, params: { - term: user.name.split(" ").last, topic_id: private_topic.id, topic_allowed_users: "true" - }, format: :json - - expect(response).to be_success - json = JSON.parse(response.body) - expect(json["users"].map { |u| u["username"] }).to_not include(user.username) - expect(json["users"].map { |u| u["username"] }).to include(privileged_user.username) - end - - context "when `enable_names` is true" do - before do - SiteSetting.enable_names = true - end - - it "returns names" do - post :search_users, params: { term: user.name }, format: :json - json = JSON.parse(response.body) - expect(json["users"].map { |u| u["name"] }).to include(user.name) - end - end - - context "when `enable_names` is false" do - before do - SiteSetting.enable_names = false - end - - it "returns names" do - post :search_users, params: { term: user.name }, format: :json - json = JSON.parse(response.body) - expect(json["users"].map { |u| u["name"] }).not_to include(user.name) - end - end - - end - describe 'send_activation_email' do context 'for an existing user' do let(:user) { Fabricate(:user, active: false) } diff --git a/spec/fixtures/emails/blacklist_whitelist_email.eml b/spec/fixtures/emails/blacklist_whitelist_email.eml new file mode 100644 index 0000000000..f6b38bfcce --- /dev/null +++ b/spec/fixtures/emails/blacklist_whitelist_email.eml @@ -0,0 +1,9 @@ +Return-Path: +From: Foo +Date: Fri, 15 Jan 2016 00:12:43 +0100 +Message-ID: <51@foo.bar.mail> +Mime-Version: 1.0 +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +Email from a domain on blacklist or whitelist. diff --git a/spec/fixtures/emails/unsubscribe_new_user.eml b/spec/fixtures/emails/unsubscribe_new_user.eml new file mode 100644 index 0000000000..337f2c88b6 --- /dev/null +++ b/spec/fixtures/emails/unsubscribe_new_user.eml @@ -0,0 +1,11 @@ +Return-Path: +From: Foo Bar +To: reply@bar.com +Date: Thu, 13 Jun 2013 17:03:48 -0400 +Message-ID: <56@foo.bar.mail> +Subject: UnSuBScRiBe +Mime-Version: 1.0 +Content-Type: text/plain; +Content-Transfer-Encoding: 7bit + +I've basically had enough of your mailing list and would very much like it if you went away. diff --git a/spec/fixtures/feed/feed.rss b/spec/fixtures/feed/feed.rss new file mode 100644 index 0000000000..2de7185c43 --- /dev/null +++ b/spec/fixtures/feed/feed.rss @@ -0,0 +1,30 @@ + + + Discourse + + https://blog.discourse.org + Official blog for the open source Discourse project + Thu, 14 Sep 2017 15:22:33 +0000 + en-US + hourly + 1 + https://wordpress.org/?v=4.8.1 + + Poll Feed Spec Fixture + https://blog.discourse.org/2017/09/poll-feed-spec-fixture/ + Thu, 14 Sep 2017 15:22:33 +0000 + + + https://blog.discourse.org/?p=pollfeedspec + + This is the body & content.

]]>
+
+
+
diff --git a/spec/jobs/download_avatar_from_url_spec.rb b/spec/jobs/download_avatar_from_url_spec.rb new file mode 100644 index 0000000000..6758a647a0 --- /dev/null +++ b/spec/jobs/download_avatar_from_url_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +RSpec.describe Jobs::DownloadAvatarFromUrl do + let(:user) { Fabricate(:user) } + + describe 'when url is invalid' do + it 'should not raise any error' do + expect do + described_class.new.execute( + url: '/assets/something/nice.jpg', + user_id: user.id + ) + end.to_not raise_error + end + end +end diff --git a/spec/jobs/poll_feed_spec.rb b/spec/jobs/poll_feed_spec.rb index 0fcfa7d02c..27f108b3ee 100644 --- a/spec/jobs/poll_feed_spec.rb +++ b/spec/jobs/poll_feed_spec.rb @@ -43,4 +43,73 @@ describe Jobs::PollFeed do end + describe '#poll_feed' do + let(:embed_by_username) { 'eviltrout' } + let(:embed_username_key_from_feed) { 'dc_creator' } + let!(:default_user) { Fabricate(:evil_trout) } + let!(:feed_author) { Fabricate(:user, username: 'xrav3nz', email: 'hi@bye.com') } + + before do + SiteSetting.feed_polling_enabled = true + SiteSetting.feed_polling_url = 'https://blog.discourse.org/feed/' + SiteSetting.embed_by_username = embed_by_username + + stub_request(:get, SiteSetting.feed_polling_url).to_return( + status: 200, + body: file_from_fixtures('feed.rss', 'feed').read, + headers: { "Content-Type" => "application/rss+xml" } + ) + end + + describe 'author username parsing' do + context 'when neither embed_by_username nor embed_username_key_from_feed is set' do + before do + SiteSetting.embed_by_username = "" + SiteSetting.embed_username_key_from_feed = "" + end + + it 'does not import topics' do + expect { poller.poll_feed }.not_to change { Topic.count } + end + end + + context 'when embed_by_username is set' do + before do + SiteSetting.embed_by_username = embed_by_username + SiteSetting.embed_username_key_from_feed = "" + end + + it 'creates the new topics under embed_by_username' do + expect { poller.poll_feed }.to change { Topic.count }.by(1) + expect(Topic.last.user).to eq(default_user) + end + end + + context 'when embed_username_key_from_feed is set' do + before do + SiteSetting.embed_username_key_from_feed = embed_username_key_from_feed + end + + it 'creates the new topics under the username found' do + expect { poller.poll_feed }.to change { Topic.count }.by(1) + expect(Topic.last.user).to eq(feed_author) + end + end + end + + it 'parses the title correctly' do + expect { poller.poll_feed }.to change { Topic.count }.by(1) + expect(Topic.last.title).to eq('Poll Feed Spec Fixture') + end + + it 'parses the content correctly' do + expect { poller.poll_feed }.to change { Topic.count }.by(1) + expect(Topic.last.first_post.raw).to include('

This is the body & content.

') + end + + it 'parses the link correctly' do + expect { poller.poll_feed }.to change { Topic.count }.by(1) + expect(Topic.last.topic_embed.embed_url).to eq('https://blog.discourse.org/2017/09/poll-feed-spec-fixture') + end + end end diff --git a/spec/jobs/publish_topic_to_category_spec.rb b/spec/jobs/publish_topic_to_category_spec.rb index 2090b3106f..ebb5e79663 100644 --- a/spec/jobs/publish_topic_to_category_spec.rb +++ b/spec/jobs/publish_topic_to_category_spec.rb @@ -50,7 +50,9 @@ RSpec.describe Jobs::PublishTopicToCategory do message = MessageBus.track_publish do described_class.new.execute(topic_timer_id: topic.public_topic_timer.id) - end.first + end.find do |m| + Hash === m.data && m.data.key?(:reload_topic) + end topic.reload expect(topic.category).to eq(another_category) diff --git a/spec/models/category_spec.rb b/spec/models/category_spec.rb index e61de161b1..7c9fbc565b 100644 --- a/spec/models/category_spec.rb +++ b/spec/models/category_spec.rb @@ -19,14 +19,6 @@ describe Category do expect(cats.errors[:name]).to be_present end - describe "last_updated_at" do - it "returns a number value of when the category was last updated" do - last = Category.last_updated_at - expect(last).to be_present - expect(last.to_i).to eq(last) - end - end - describe "resolve_permissions" do it "can determine read_restricted" do read_restricted, resolved = Category.resolve_permissions(everyone: :full) diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 9eee947198..3920a7da43 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -577,4 +577,21 @@ describe Group do expect(group.group_users.map(&:user_id)).to contain_exactly(user.id, admin.id) end end + + it "Correctly updates has_messages" do + group = Fabricate(:group, has_messages: true) + topic = Fabricate(:private_message_topic) + + # when group message is not present + Group.refresh_has_messages! + group.reload + expect(group.has_messages?).to eq false + + # when group message is present + group.update!(has_messages: true) + TopicAllowedGroup.create!(topic_id: topic.id, group_id: group.id) + Group.refresh_has_messages! + group.reload + expect(group.has_messages?).to eq true + end end diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index c152875acc..706b2ee497 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -1167,6 +1167,22 @@ describe Topic do end end + describe '#private_topic_timer' do + let(:user) { Fabricate(:user) } + + let(:topic_timer) do + Fabricate(:topic_timer, + public_type: false, + user: user, + status_type: TopicTimer.private_types[:reminder] + ) + end + + it 'should return the right record' do + expect(topic_timer.topic.private_topic_timer(user)).to eq(topic_timer) + end + end + describe '#set_or_create_timer' do let(:topic) { Fabricate.build(:topic) } @@ -1333,6 +1349,14 @@ describe Topic do }.to change { TopicTimer.count }.by(1) end + it 'should not be override when setting a public topic timer' do + reminder + + expect do + topic.set_or_create_timer(TopicTimer.types[:close], 3, by_user: reminder.user) + end.to change { TopicTimer.count }.by(1) + end + it "can update a user's existing record" do freeze_time now diff --git a/spec/models/user_option_spec.rb b/spec/models/user_option_spec.rb index d494896cef..832de55276 100644 --- a/spec/models/user_option_spec.rb +++ b/spec/models/user_option_spec.rb @@ -56,20 +56,20 @@ describe UserOption do let!(:user) { Fabricate(:user) } it "should have no reason when `SiteSetting.redirect_users_to_top_page` is disabled" do - SiteSetting.expects(:redirect_users_to_top_page).returns(false) + SiteSetting.redirect_users_to_top_page = false expect(user.user_option.redirected_to_top).to eq(nil) end context "when `SiteSetting.redirect_users_to_top_page` is enabled" do - before { SiteSetting.expects(:redirect_users_to_top_page).returns(true) } + before { SiteSetting.redirect_users_to_top_page = true } it "should have no reason when top is not in the `SiteSetting.top_menu`" do - SiteSetting.expects(:top_menu).returns("latest") + SiteSetting.top_menu = "latest" expect(user.user_option.redirected_to_top).to eq(nil) end context "and when top is in the `SiteSetting.top_menu`" do - before { SiteSetting.expects(:top_menu).returns("latest|top") } + before { SiteSetting.top_menu = "latest|top" } it "should have no reason when there are not enough topics" do SiteSetting.expects(:min_redirected_to_top_period).returns(nil) @@ -87,8 +87,12 @@ describe UserOption do end it "should have a reason for the first visit" do - expect(user.user_option.redirected_to_top).to eq(reason: I18n.t('redirected_to_top_reasons.new_user'), - period: :monthly) + freeze_time do + delay = SiteSetting.active_user_rate_limit_secs / 2 + Jobs.expects(:enqueue_in).with(delay, :update_top_redirection, user_id: user.id, redirected_at: Time.zone.now) + + expect(user.user_option.redirected_to_top).to eq(reason: I18n.t('redirected_to_top_reasons.new_user'), period: :monthly) + end end it "should not have a reason for next visits" do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 65461bfd24..cd07511834 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1420,9 +1420,8 @@ describe User do let(:user) { Fabricate(:user) } it 'should publish the right message' do - message = MessageBus.track_publish { user.logged_out }.find { |m| m.channel == '/logout' } + message = MessageBus.track_publish('/logout') { user.logged_out }.first - expect(message.channel).to eq('/logout') expect(message.data).to eq(user.id) end end @@ -1527,9 +1526,9 @@ describe User do notification = Fabricate(:notification, user: user) notification2 = Fabricate(:notification, user: user, read: true) - message = MessageBus.track_publish do + message = MessageBus.track_publish("/notification/#{user.id}") do user.publish_notifications_state - end.find { |m| m.channel = "/notification/#{user.id}" } + end.first expect(message.data[:recent]).to eq([ [notification2.id, true], [notification.id, false] diff --git a/spec/requests/admin/email_templates_controller_spec.rb b/spec/requests/admin/email_templates_controller_spec.rb new file mode 100644 index 0000000000..6ddd34380b --- /dev/null +++ b/spec/requests/admin/email_templates_controller_spec.rb @@ -0,0 +1,287 @@ +require 'rails_helper' + +RSpec.describe Admin::EmailTemplatesController do + let(:admin) { Fabricate(:admin) } + let(:user) { Fabricate(:user) } + + def original_text(key) + I18n.overrides_disabled { I18n.t(key) } + end + + let(:original_subject) { original_text('user_notifications.admin_login.subject_template') } + let(:original_body) { original_text('user_notifications.admin_login.text_body_template') } + let(:headers) { { ACCEPT: 'application/json' } } + + after do + TranslationOverride.delete_all + I18n.reload! + end + + context "#index" do + it "raises an error if you aren't logged in" do + expect do + get '/admin/customize/email_templates.json' + end.to raise_error(ActionController::RoutingError) + end + + it "raises an error if you aren't an admin" do + sign_in(user) + expect do + get '/admin/customize/email_templates.json' + end.to raise_error(ActionController::RoutingError) + end + + it "should work if you are an admin" do + sign_in(admin) + get '/admin/customize/email_templates.json' + + expect(response).to be_success + + json = ::JSON.parse(response.body) + expect(json['email_templates']).to be_present + end + end + + context "#update" do + it "raises an error if you aren't logged in" do + expect do + put '/admin/customize/email_templates/some_id', params: { + email_template: { subject: 'Subject', body: 'Body' } + }, headers: headers + end.to raise_error(ActionController::RoutingError) + end + + it "raises an error if you aren't an admin" do + sign_in(user) + expect do + put '/admin/customize/email_templates/some_id', params: { + email_template: { subject: 'Subject', body: 'Body' } + }, headers: headers + end.to raise_error(ActionController::RoutingError) + end + + context "when logged in as admin" do + before do + sign_in(admin) + end + + it "returns 'not found' when an unknown email template id is used" do + put '/admin/customize/email_templates/non_existent_template', params: { + email_template: { subject: 'Foo', body: 'Bar' } + }, headers: headers + + expect(response).not_to be_success + + json = ::JSON.parse(response.body) + expect(json['error_type']).to eq('not_found') + end + + shared_examples "invalid email template" do + it "returns the right error messages" do + put '/admin/customize/email_templates/user_notifications.admin_login', params: { + email_template: { subject: email_subject, body: email_body } + }, headers: headers + + json = ::JSON.parse(response.body) + expect(json).to be_present + + errors = json['errors'] + expect(errors).to be_present + expect(errors).to eq(expected_errors) + end + + it "doesn't create translation overrides" do + put '/admin/customize/email_templates/user_notifications.admin_login', params: { + email_template: { subject: email_subject, body: email_body } + }, headers: headers + + expect(I18n.t('user_notifications.admin_login.subject_template')).to eq(original_subject) + expect(I18n.t('user_notifications.admin_login.text_body_template')).to eq(original_body) + end + + it "doesn't create entries in the Staff Log" do + put '/admin/customize/email_templates/user_notifications.admin_login', params: { + email_template: { subject: email_subject, body: email_body } + }, headers: headers + + log = UserHistory.find_by_subject('user_notifications.admin_login.subject_template') + expect(log).to be_nil + + log = UserHistory.find_by_subject('user_notifications.admin_login.text_body_template') + expect(log).to be_nil + end + end + + context "when subject is invalid" do + let(:email_subject) { 'Subject with missing interpolation key' } + let(:email_body) { 'The body contains [%{site_name}](%{base_url}) and %{email_token}.' } + let(:expected_errors) { ['Subject: The following interpolation key(s) are missing: "email_prefix"'] } + + include_examples "invalid email template" + end + + context "when body is invalid" do + let(:email_subject) { '%{email_prefix} Foo' } + let(:email_body) { 'Body with some missing interpolation keys: %{email_token}' } + let(:expected_errors) { ['Body: The following interpolation key(s) are missing: "site_name, base_url"'] } + + include_examples "invalid email template" + end + + context "when subject and body are invalid invalid" do + let(:email_subject) { 'Subject with missing interpolation key' } + let(:email_body) { 'Body with some missing interpolation keys: %{email_token}' } + let(:expected_errors) do + ['Subject: The following interpolation key(s) are missing: "email_prefix"', + 'Body: The following interpolation key(s) are missing: "site_name, base_url"'] + end + + include_examples "invalid email template" + end + + context "when subject and body contain all required interpolation keys" do + let(:email_subject) { '%{email_prefix} Foo' } + let(:email_body) { 'The body contains [%{site_name}](%{base_url}) and %{email_token}.' } + + it "returns the successfully updated email template" do + put '/admin/customize/email_templates/user_notifications.admin_login', params: { + email_template: { subject: email_subject, body: email_body } + }, headers: headers + + expect(response).to be_success + + json = ::JSON.parse(response.body) + expect(json).to be_present + + template = json['email_template'] + expect(template).to be_present + + expect(template['id']).to eq('user_notifications.admin_login') + expect(template['title']).to eq('Admin Login') + expect(template['subject']).to eq(email_subject) + expect(template['body']).to eq(email_body) + expect(template['can_revert']).to eq(true) + end + + it "creates translation overrides" do + put '/admin/customize/email_templates/user_notifications.admin_login', params: { + email_template: { subject: email_subject, body: email_body } + }, headers: headers + + expect(I18n.t('user_notifications.admin_login.subject_template')).to eq(email_subject) + expect(I18n.t('user_notifications.admin_login.text_body_template')).to eq(email_body) + end + + it "creates entries in the Staff Log" do + put '/admin/customize/email_templates/user_notifications.admin_login', params: { + email_template: { subject: email_subject, body: email_body } + }, headers: headers + + log = UserHistory.find_by_subject('user_notifications.admin_login.subject_template') + + expect(log).to be_present + expect(log.action).to eq(UserHistory.actions[:change_site_text]) + expect(log.previous_value).to eq(original_subject) + expect(log.new_value).to eq(email_subject) + + log = UserHistory.find_by_subject('user_notifications.admin_login.text_body_template') + + expect(log).to be_present + expect(log.action).to eq(UserHistory.actions[:change_site_text]) + expect(log.previous_value).to eq(original_body) + expect(log.new_value).to eq(email_body) + end + end + + end + + end + + context "#revert" do + it "raises an error if you aren't logged in" do + expect do + delete '/admin/customize/email_templates/some_id', headers: headers + end.to raise_error(ActionController::RoutingError) + end + + it "raises an error if you aren't an admin" do + sign_in(user) + expect do + delete '/admin/customize/email_templates/some_id', headers: headers + end.to raise_error(ActionController::RoutingError) + end + + context "when logged in as admin" do + before do + sign_in(admin) + end + + it "returns 'not found' when an unknown email template id is used" do + delete '/admin/customize/email_templates/non_existent_template', headers: headers + expect(response).not_to be_success + + json = ::JSON.parse(response.body) + expect(json['error_type']).to eq('not_found') + end + + context "when email template has translation overrides" do + let(:email_subject) { '%{email_prefix} Foo' } + let(:email_body) { 'The body contains [%{site_name}](%{base_url}) and %{email_token}.' } + + before do + put '/admin/customize/email_templates/user_notifications.admin_login', params: { + email_template: { subject: email_subject, body: email_body } + }, headers: headers + end + + it "restores the original subject and body" do + expect(I18n.t('user_notifications.admin_login.subject_template')).to eq(email_subject) + expect(I18n.t('user_notifications.admin_login.text_body_template')).to eq(email_body) + + delete '/admin/customize/email_templates/user_notifications.admin_login', headers: headers + + expect(I18n.t('user_notifications.admin_login.subject_template')).to eq(original_subject) + expect(I18n.t('user_notifications.admin_login.text_body_template')).to eq(original_body) + end + + it "returns the restored email template" do + delete '/admin/customize/email_templates/user_notifications.admin_login', headers: headers + expect(response).to be_success + + json = ::JSON.parse(response.body) + expect(json).to be_present + + template = json['email_template'] + expect(template).to be_present + + expect(template['id']).to eq('user_notifications.admin_login') + expect(template['title']).to eq('Admin Login') + expect(template['subject']).to eq(original_subject) + expect(template['body']).to eq(original_body) + expect(template['can_revert']).to eq(false) + end + + it "creates entries in the Staff Log" do + UserHistory.delete_all + delete '/admin/customize/email_templates/user_notifications.admin_login', headers: headers + + log = UserHistory.find_by_subject('user_notifications.admin_login.subject_template') + + expect(log).to be_present + expect(log.action).to eq(UserHistory.actions[:change_site_text]) + expect(log.previous_value).to eq(email_subject) + expect(log.new_value).to eq(original_subject) + + log = UserHistory.find_by_subject('user_notifications.admin_login.text_body_template') + + expect(log).to be_present + expect(log.action).to eq(UserHistory.actions[:change_site_text]) + expect(log.previous_value).to eq(email_body) + expect(log.new_value).to eq(original_body) + end + end + end + + end + +end diff --git a/spec/requests/admin/emojis_controller_spec.rb b/spec/requests/admin/emojis_controller_spec.rb index 329778e50e..d2d0250584 100644 --- a/spec/requests/admin/emojis_controller_spec.rb +++ b/spec/requests/admin/emojis_controller_spec.rb @@ -11,14 +11,13 @@ RSpec.describe Admin::EmojisController do describe "#create" do describe 'when upload is invalid' do it 'should publish the right error' do - message = MessageBus.track_publish do + message = MessageBus.track_publish("/uploads/emoji") do post "/admin/customize/emojis.json", params: { name: 'test', file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/fake.jpg") } - end.find { |m| m.channel == "/uploads/emoji" } + end.first - expect(message.channel).to eq("/uploads/emoji") expect(message.data["errors"]).to eq([I18n.t('upload.images.size_not_found')]) end end @@ -27,14 +26,12 @@ RSpec.describe Admin::EmojisController do it 'should publish the right error' do CustomEmoji.create!(name: 'test', upload: upload) - message = MessageBus.track_publish do + message = MessageBus.track_publish("/uploads/emoji") do post "/admin/customize/emojis.json", params: { name: 'test', file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png") } - end.find { |m| m.channel == "/uploads/emoji" } - - expect(message.channel).to eq("/uploads/emoji") + end.first expect(message.data["errors"]).to eq([ "Name #{I18n.t('activerecord.errors.models.custom_emoji.attributes.name.taken')}" @@ -45,18 +42,17 @@ RSpec.describe Admin::EmojisController do it 'should allow an admin to add a custom emoji' do Emoji.expects(:clear_cache) - message = MessageBus.track_publish do + message = MessageBus.track_publish("/uploads/emoji") do post "/admin/customize/emojis.json", params: { name: 'test', file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png") } - end.find { |m| m.channel == "/uploads/emoji" } + end.first custom_emoji = CustomEmoji.last upload = custom_emoji.upload expect(upload.original_filename).to eq('logo.png') - expect(message.channel).to eq("/uploads/emoji") expect(message.data["errors"]).to eq(nil) expect(message.data["name"]).to eq(custom_emoji.name) expect(message.data["url"]).to eq(upload.url) diff --git a/spec/requests/admin/flagged_topics_controller_spec.rb b/spec/requests/admin/flagged_topics_controller_spec.rb index 594caff1ed..8f768294bc 100644 --- a/spec/requests/admin/flagged_topics_controller_spec.rb +++ b/spec/requests/admin/flagged_topics_controller_spec.rb @@ -1,19 +1,33 @@ require 'rails_helper' RSpec.describe Admin::FlaggedTopicsController do - let(:admin) { Fabricate(:admin) } let!(:flag) { Fabricate(:flag) } - before do - sign_in(admin) + shared_examples "successfully retrieve list of flagged topics" do + it "returns a list of flagged topics" do + get "/admin/flagged_topics.json" + expect(response).to be_success + + data = ::JSON.parse(response.body) + expect(data['flagged_topics']).to be_present + expect(data['users']).to be_present + end end - it "returns a list of flagged topics" do - get "/admin/flagged_topics.json" - expect(response).to be_success + context "as admin" do + before do + sign_in(Fabricate(:admin)) + end - data = ::JSON.parse(response.body) - expect(data['flagged_topics']).to be_present - expect(data['users']).to be_present + include_examples "successfully retrieve list of flagged topics" end + + context "as moderator" do + before do + sign_in(Fabricate(:moderator)) + end + + include_examples "successfully retrieve list of flagged topics" + end + end diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb index f808ed7578..5e4f729a24 100644 --- a/spec/requests/groups_controller_spec.rb +++ b/spec/requests/groups_controller_spec.rb @@ -183,7 +183,9 @@ describe GroupsController do ) end - let(:group) { Fabricate(:group, users: [user1, user2, user3]) } + let(:bot) { Fabricate(:user, id: -999) } + + let(:group) { Fabricate(:group, users: [user1, user2, user3, bot]) } it "should allow members to be sorted by" do get "/groups/#{group.name}/members.json", params: { diff --git a/spec/requests/list_controller_spec.rb b/spec/requests/list_controller_spec.rb index cec635654e..9a0d21a5c9 100644 --- a/spec/requests/list_controller_spec.rb +++ b/spec/requests/list_controller_spec.rb @@ -3,6 +3,18 @@ require 'rails_helper' RSpec.describe ListController do let(:topic) { Fabricate(:topic) } + describe '#index' do + it "doesn't throw an error with a negative page" do + get "/#{Discourse.anonymous_filters[1]}", params: { page: -1024 } + expect(response).to be_success + end + + it "doesn't throw an error with page params as an array" do + get "/#{Discourse.anonymous_filters[1]}", params: { page: ['7'] } + expect(response).to be_success + end + end + describe 'titles for crawler layout' do it 'has no title for the default URL' do topic diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 53d5eb5a36..002d3cc5b2 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -85,4 +85,154 @@ RSpec.describe UsersController do end end end + + describe "search_users" do + let(:topic) { Fabricate :topic } + let(:user) { Fabricate :user, username: "joecabot", name: "Lawrence Tierney" } + let(:post1) { Fabricate(:post, user: user, topic: topic) } + + before do + SearchIndexer.enable + post1 + end + + it "searches when provided the term only" do + get "/u/search/users.json", params: { term: user.name.split(" ").last } + expect(response).to be_success + json = JSON.parse(response.body) + expect(json["users"].map { |u| u["username"] }).to include(user.username) + end + + it "searches when provided the topic only" do + get "/u/search/users.json", params: { topic_id: topic.id } + expect(response).to be_success + json = JSON.parse(response.body) + expect(json["users"].map { |u| u["username"] }).to include(user.username) + end + + it "searches when provided the term and topic" do + get "/u/search/users.json", params: { + term: user.name.split(" ").last, topic_id: topic.id + } + + expect(response).to be_success + json = JSON.parse(response.body) + expect(json["users"].map { |u| u["username"] }).to include(user.username) + end + + it "searches only for users who have access to private topic" do + privileged_user = Fabricate(:user, trust_level: 4, username: "joecabit", name: "Lawrence Tierney") + privileged_group = Fabricate(:group) + privileged_group.add(privileged_user) + privileged_group.save + + category = Fabricate(:category) + category.set_permissions(privileged_group => :readonly) + category.save + + private_topic = Fabricate(:topic, category: category) + + get "/u/search/users.json", params: { + term: user.name.split(" ").last, topic_id: private_topic.id, topic_allowed_users: "true" + } + + expect(response).to be_success + json = JSON.parse(response.body) + expect(json["users"].map { |u| u["username"] }).to_not include(user.username) + expect(json["users"].map { |u| u["username"] }).to include(privileged_user.username) + end + + context "when `enable_names` is true" do + before do + SiteSetting.enable_names = true + end + + it "returns names" do + get "/u/search/users.json", params: { term: user.name } + json = JSON.parse(response.body) + expect(json["users"].map { |u| u["name"] }).to include(user.name) + end + end + + context "when `enable_names` is false" do + before do + SiteSetting.enable_names = false + end + + it "returns names" do + get "/u/search/users.json", params: { term: user.name } + json = JSON.parse(response.body) + expect(json["users"].map { |u| u["name"] }).not_to include(user.name) + end + end + + context 'groups' do + let!(:mentionable_group) { Fabricate(:group, mentionable_level: 99, messageable_level: 0) } + let!(:messageable_group) { Fabricate(:group, mentionable_level: 0, messageable_level: 99) } + + describe 'when signed in' do + before do + sign_in(user) + end + + it "doesn't search for groups" do + get "/u/search/users.json", params: { + include_mentionable_groups: 'false', + include_messageable_groups: 'false' + } + + expect(response).to be_success + expect(JSON.parse(response.body)).not_to have_key(:groups) + end + + it "searches for messageable groups" do + get "/u/search/users.json", params: { + include_mentionable_groups: 'false', + include_messageable_groups: 'true' + } + + expect(response).to be_success + expect(JSON.parse(response.body)["groups"].first['name']).to eq(messageable_group.name) + end + + it 'searches for mentionable groups' do + get "/u/search/users.json", params: { + include_messageable_groups: 'false', + include_mentionable_groups: 'true' + } + + expect(response).to be_success + expect(JSON.parse(response.body)["groups"].first['name']).to eq(mentionable_group.name) + end + end + + describe 'when not signed in' do + it 'should not include mentionable/messageable groups' do + get "/u/search/users.json", params: { + include_mentionable_groups: 'false', + include_messageable_groups: 'false' + } + + expect(response).to be_success + expect(JSON.parse(response.body)).not_to have_key(:groups) + + get "/u/search/users.json", params: { + include_mentionable_groups: 'false', + include_messageable_groups: 'true' + } + + expect(response).to be_success + expect(JSON.parse(response.body)).not_to have_key(:groups) + + get "/u/search/users.json", params: { + include_messageable_groups: 'false', + include_mentionable_groups: 'true' + } + + expect(response).to be_success + expect(JSON.parse(response.body)).not_to have_key(:groups) + end + end + end + end end diff --git a/spec/requests/users_email_controller_spec.rb b/spec/requests/users_email_controller_spec.rb index 28d5d89e83..78c83cbe3e 100644 --- a/spec/requests/users_email_controller_spec.rb +++ b/spec/requests/users_email_controller_spec.rb @@ -96,20 +96,40 @@ describe UsersEmailController do context 'when the new email address is taken' do let!(:other_user) { Fabricate(:coding_horror) } - it 'raises an error' do - put "/u/#{user.username}/preferences/email.json", params: { - email: other_user.email - } + context 'hide_email_address_taken is disabled' do + before do + SiteSetting.hide_email_address_taken = false + end - expect(response).to_not be_success + it 'raises an error' do + put "/u/#{user.username}/preferences/email.json", params: { + email: other_user.email + } + + expect(response).to_not be_success + end + + it 'raises an error if there is whitespace too' do + put "/u/#{user.username}/preferences/email.json", params: { + email: "#{other_user.email} " + } + + expect(response).to_not be_success + end end - it 'raises an error if there is whitespace too' do - put "/u/#{user.username}/preferences/email.json", params: { - email: "#{other_user.email} " - } + context 'hide_email_address_taken is enabled' do + before do + SiteSetting.hide_email_address_taken = true + end - expect(response).to_not be_success + it 'responds with success' do + put "/u/#{user.username}/preferences/email.json", params: { + email: other_user.email + } + + expect(response).to be_success + end end end diff --git a/spec/support/diagnostics_helper.rb b/spec/support/diagnostics_helper.rb index bab725d817..11d131c98d 100644 --- a/spec/support/diagnostics_helper.rb +++ b/spec/support/diagnostics_helper.rb @@ -1,7 +1,7 @@ module MessageBus::DiagnosticsHelper def publish(channel, data, opts = nil) id = super(channel, data, opts) - if @tracking + if @tracking && (@channel.nil? || @channel == channel) m = MessageBus::Message.new(-1, id, channel, data) m.user_ids = opts[:user_ids] if opts m.group_ids = opts[:group_ids] if opts @@ -10,7 +10,8 @@ module MessageBus::DiagnosticsHelper id end - def track_publish + def track_publish(channel = nil) + @channel = channel @tracking = tracking = [] yield tracking diff --git a/spec/tasks/posts_spec.rb b/spec/tasks/posts_spec.rb index f69a3bbf68..7f3e307f9e 100644 --- a/spec/tasks/posts_spec.rb +++ b/spec/tasks/posts_spec.rb @@ -1,18 +1,46 @@ require 'rails_helper' +require 'highline/import' +require 'highline/simulate' RSpec.describe "Post rake tasks" do before do + Rake::Task.clear Discourse::Application.load_tasks IO.any_instance.stubs(:puts) end describe 'remap' do + let!(:tricky_post) { Fabricate(:post, raw: 'Today ^Today') } + it 'should remap posts' do post = Fabricate(:post, raw: "The quick brown fox jumps over the lazy dog") - Rake::Task['posts:remap'].invoke("brown", "red") + HighLine::Simulate.with('y') do + Rake::Task['posts:remap'].invoke("brown", "red") + end + post.reload expect(post.raw).to eq('The quick red fox jumps over the lazy dog') end + + context 'when type == string' do + it 'remaps input as string' do + HighLine::Simulate.with('y') do + Rake::Task['posts:remap'].invoke('^Today', 'Yesterday', 'string') + end + + expect(tricky_post.reload.raw).to eq('Today Yesterday') + end + end + + context 'when type == regex' do + it 'remaps input as regex' do + HighLine::Simulate.with('y') do + Rake::Task['posts:remap'].invoke('^Today', 'Yesterday', 'regex') + end + + expect(tricky_post.reload.raw).to eq('Yesterday ^Today') + end + end end end diff --git a/test/javascripts/acceptance/groups-test.js.es6 b/test/javascripts/acceptance/groups-test.js.es6 index 0038ff093a..65ead1b17d 100644 --- a/test/javascripts/acceptance/groups-test.js.es6 +++ b/test/javascripts/acceptance/groups-test.js.es6 @@ -132,7 +132,7 @@ QUnit.test("Admin Viewing Group", assert => { andThen(() => { assert.ok(find(".nav-pills li a[title='Edit Group']").length === 1, 'it should show edit group tab if user is admin'); assert.ok(find(".nav-pills li a[title='Logs']").length === 1, 'it should show Logs tab if user is admin'); - + assert.equal(count('.group-message-button'), 1, 'it displays show group message button'); assert.equal(find('.group-info-name').text(), 'Awesome Team', 'it should display the group name'); }); diff --git a/test/javascripts/acceptance/topic-test.js.es6 b/test/javascripts/acceptance/topic-test.js.es6 index a6510a2f47..530ea1cad3 100644 --- a/test/javascripts/acceptance/topic-test.js.es6 +++ b/test/javascripts/acceptance/topic-test.js.es6 @@ -142,6 +142,26 @@ QUnit.test("Reply as new message", assert => { }); }); +QUnit.test("Visit topic routes", assert => { + visit("/t/12"); + + andThen(() => { + assert.equal( + find('.fancy-title').text().trim(), 'PM for testing', + 'it routes to the right topic' + ); + }); + + visit("/t/280/20"); + + andThen(() => { + assert.equal( + find('.fancy-title').text().trim(), 'Internationalization / localization', + 'it routes to the right topic' + ); + }); +}); + QUnit.test("Updating the topic title with emojis", assert => { visit("/t/internationalization-localization/280"); click('#topic-title .d-icon-pencil'); diff --git a/test/javascripts/fixtures/group-fixtures.js.es6 b/test/javascripts/fixtures/group-fixtures.js.es6 index 291db11d23..78aab0abf8 100644 --- a/test/javascripts/fixtures/group-fixtures.js.es6 +++ b/test/javascripts/fixtures/group-fixtures.js.es6 @@ -12,7 +12,8 @@ export default { "public_exit":true, "flair_url": 'fa-adjust', "is_group_owner":true, - "mentionable":true + "mentionable":true, + "messageable":true } }, "/groups/discourse/counts.json":{ diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6 index 6c70dad01f..a466f632cc 100644 --- a/test/javascripts/helpers/create-pretender.js.es6 +++ b/test/javascripts/helpers/create-pretender.js.es6 @@ -131,6 +131,7 @@ export default function() { this.put('/u/eviltrout.json', () => response({ user: {} })); this.get("/t/280.json", () => response(fixturesByUrl['/t/280/1.json'])); + this.get("/t/280/20.json", () => response(fixturesByUrl['/t/280/1.json'])); this.get("/t/28830.json", () => response(fixturesByUrl['/t/28830/1.json'])); this.get("/t/9.json", () => response(fixturesByUrl['/t/9/1.json'])); this.get("/t/12.json", () => response(fixturesByUrl['/t/12/1.json'])); diff --git a/test/javascripts/widgets/widget-test.js.es6 b/test/javascripts/widgets/widget-test.js.es6 index 6305d6ab78..78502f6ccd 100644 --- a/test/javascripts/widgets/widget-test.js.es6 +++ b/test/javascripts/widgets/widget-test.js.es6 @@ -188,7 +188,7 @@ widgetTest('widget attaching', { createWidget('attach-test', { tagName: 'div.container', - template: hbs`{{attach widget="test-embedded"}}` + template: hbs`{{attach widget="test-embedded" attrs=attrs}}` }); },