diff --git a/.eslintignore b/.eslintignore index 6676467368..e877e0062b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -13,3 +13,5 @@ vendor/ test/javascripts/test_helper.js test/javascripts/fixtures test/javascripts/helpers/assertions.js +node_modules/ +dist/ diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 15d8c36a8a..e848bdcf4c 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -23,12 +23,6 @@ acc5cbdf8ecb9293a0fa9474ee73baf499c02428 # Rename wizard from es6 -> js 1ac02422011f89716ab27250d39b0e0212e03892 -# Rename discourse-common es6 -> js -167503ca4824e37a2e93d74b3f50271556d0ba8e - -# Rename ember-addons es6 -> js -16ba50bce362c1eefe1881f86c67bec66f493abb - # Rename some root files 11938d58d4b1bea1ff43306450da7b24f05db0a @@ -40,3 +34,6 @@ b66b277dc44bcd2122dc21965dab209c30636214 # DEV: enforces double quotes ember-template-lint c4644c61d97c823b7dd940ffaf0967a104f4b58c + +# Migrate to app directory +7a2e8d3ead63c7d99e1069fc7823e933f931ba85 diff --git a/.gitignore b/.gitignore index 59ad3c0b6f..97d1a6b930 100644 --- a/.gitignore +++ b/.gitignore @@ -54,7 +54,7 @@ bootsnap-compile-cache/ !/plugins/discourse-narrative-bot !/plugins/discourse-presence !/plugins/discourse-local-dates -!/plugins/discourse-internet-explorer +!/plugins/discourse-unsupported-browser /plugins/*/auto_generated/ /spec/fixtures/plugins/my_plugin/auto_generated @@ -122,7 +122,7 @@ vendor/bundle/* *.swn # ignore nodejs files -/node_modules +node_modules /package-lock.json /vendor/data/GeoLite2-City.mmdb @@ -132,3 +132,9 @@ vendor/bundle/* # ignore auto-generated plugin js assets /app/assets/javascripts/plugins/* + +# ignore generated api documentation files +openapi/* + +# ember-cli generated +dist \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml index f389e688ca..0cb02e3e97 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -179,6 +179,8 @@ RSpec/DescribedClassModuleWrapping: RSpec/EmptyExampleGroup: Enabled: true + Exclude: + - 'spec/requests/api/*' RSpec/EmptyLineAfterExample: Enabled: false # TODO diff --git a/Gemfile b/Gemfile index c440694fff..742bf82d0b 100644 --- a/Gemfile +++ b/Gemfile @@ -118,7 +118,6 @@ gem 'rake' gem 'thor', require: false gem 'diffy', require: false gem 'rinku' -gem 'sanitize' gem 'sidekiq' gem 'mini_scheduler' @@ -168,17 +167,17 @@ group :test, :development do # we would like to upgrade it if possible gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false - # TODO once 4.0.0 is released upgrade to it, at time of writing 3.9.0 is latest - gem 'rspec-rails', '4.0.0.beta2', require: false + gem 'rspec-rails' gem 'shoulda-matchers', require: false gem 'rspec-html-matchers' - gem 'pry-nav' gem 'byebug', require: ENV['RM_INFO'].nil?, platform: :mri gem 'rubocop', require: false gem "rubocop-discourse", require: false gem "rubocop-rspec", require: false gem 'parallel_tests' + + gem 'rswag-specs' end group :development do @@ -255,10 +254,3 @@ end gem 'webpush', require: false gem 'colored2', require: false gem 'maxminddb' - -# These are not direct dependencies, but we need to restrict -# versions for compatibility with https://github.com/discourse/discourse-zendesk-plugin -# These restrictions can be removed once the zendesk_api gem is updated -# for newer versions of hashie and faraday -gem 'hashie', '< 4.0.0', require: false # https://github.com/zendesk/zendesk_api_client_rb/pull/422 -gem 'faraday', '< 1.0.0', require: false # https://github.com/zendesk/zendesk_api_client_rb/pull/421 diff --git a/Gemfile.lock b/Gemfile.lock index ab18090bea..d16ced6284 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -61,12 +61,12 @@ GEM aws-sdk-sns (1.22.0) aws-sdk-core (~> 3, >= 3.71.0) aws-sigv4 (~> 1.1) - aws-sigv4 (1.1.2) + aws-sigv4 (1.1.3) aws-eventstream (~> 1.0, >= 1.0.2) barber (0.12.2) ember-source (>= 1.0, < 3.1) execjs (>= 1.2, < 3) - better_errors (2.6.0) + better_errors (2.7.0) coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) @@ -78,7 +78,7 @@ GEM bullet (6.1.0) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) - byebug (11.1.2) + byebug (11.1.3) cbor (0.5.9.6) certified (1.0.0) chunky_png (1.3.11) @@ -126,7 +126,7 @@ GEM exifr (1.3.6) fabrication (2.21.1) fakeweb (1.3.0) - faraday (0.17.3) + faraday (1.0.1) multipart-post (>= 1.2, < 3) fast_blank (1.0.0) fast_xor (1.1.3) @@ -142,7 +142,7 @@ GEM activesupport (>= 4.2.0) guess_html_encoding (0.0.11) hashdiff (1.0.1) - hashie (3.6.0) + hashie (4.1.0) highline (1.7.10) hkdf (0.3.0) htmlentities (4.3.4) @@ -158,6 +158,8 @@ GEM railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (2.3.0) + json-schema (2.8.1) + addressable (>= 2.4) jwt (2.2.1) kgio (2.11.3) libv8 (7.3.492.27.1) @@ -182,9 +184,9 @@ GEM mini_mime (>= 0.1.1) maxminddb (0.1.22) memory_profiler (0.9.14) - message_bus (2.2.4) + message_bus (3.1.0) rack (>= 1.1.3) - method_source (0.9.2) + method_source (1.0.0) mini_mime (1.0.2) mini_portile2 (2.4.0) mini_racer (0.2.10) @@ -251,15 +253,13 @@ GEM parallel (1.19.1) parallel_tests (2.32.0) parallel - parser (2.7.1.1) + parser (2.7.1.2) ast (~> 2.4.0) pg (1.2.3) progress (3.5.2) - pry (0.12.2) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - pry-nav (0.3.0) - pry (>= 0.9.10, < 0.13.0) + pry (0.13.1) + coderay (~> 1.1) + method_source (~> 1.0) pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (4.0.4) @@ -292,7 +292,7 @@ GEM rake (13.0.1) rake-compiler (1.1.0) rake - rb-fsevent (0.10.3) + rb-fsevent (0.10.4) rb-inotify (0.10.1) ffi (~> 1.0) rbtrace (0.4.12) @@ -300,7 +300,7 @@ GEM msgpack (>= 0.4.3) optimist (>= 3.0.0) rchardet (1.8.0) - redis (4.1.3) + redis (4.1.4) redis-namespace (1.7.0) redis (>= 3.0.4) request_store (1.5.0) @@ -312,13 +312,13 @@ GEM rqrcode (1.1.2) chunky_png (~> 1.0) rqrcode_core (~> 0.1) - rqrcode_core (0.1.1) + rqrcode_core (0.1.2) rspec (3.9.0) rspec-core (~> 3.9.0) rspec-expectations (~> 3.9.0) rspec-mocks (~> 3.9.0) - rspec-core (3.9.1) - rspec-support (~> 3.9.1) + rspec-core (3.9.2) + rspec-support (~> 3.9.3) rspec-expectations (3.9.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) @@ -328,15 +328,19 @@ GEM rspec-mocks (3.9.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) - rspec-rails (4.0.0.beta2) + rspec-rails (4.0.0) actionpack (>= 4.2) activesupport (>= 4.2) railties (>= 4.2) - rspec-core (~> 3.8) - rspec-expectations (~> 3.8) - rspec-mocks (~> 3.8) - rspec-support (~> 3.8) - rspec-support (3.9.2) + rspec-core (~> 3.9) + rspec-expectations (~> 3.9) + rspec-mocks (~> 3.9) + rspec-support (~> 3.9) + rspec-support (3.9.3) + rswag-specs (2.3.1) + activesupport (>= 3.1, < 7.0) + json-schema (~> 2.2) + railties (>= 3.1, < 7.0) rtlit (0.0.5) rubocop (0.82.0) jaro_winkler (~> 1.5.1) @@ -346,9 +350,10 @@ GEM rexml ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 2.0) - rubocop-discourse (2.0.1) + rubocop-discourse (2.1.2) rubocop (>= 0.69.0) - rubocop-rspec (1.38.1) + rubocop-rspec (>= 1.39.0) + rubocop-rspec (1.39.0) rubocop (>= 0.68.1) ruby-prof (1.3.2) ruby-progressbar (1.10.1) @@ -405,7 +410,7 @@ GEM unf_ext unf_ext (0.0.7.7) unicode-display_width (1.7.0) - unicorn (5.5.4) + unicorn (5.5.5) kgio (~> 2.6) raindrops (~> 0.7) uniform_notifier (1.13.0) @@ -457,14 +462,12 @@ DEPENDENCIES execjs fabrication fakeweb - faraday (< 1.0.0) fast_blank fast_xor fast_xs fastimage flamegraph gc_tracer - hashie (< 4.0.0) highline (~> 1.7.0) htmlentities http_accept_language @@ -502,7 +505,6 @@ DEPENDENCIES onebox parallel_tests pg - pry-nav pry-rails puma r2 @@ -523,7 +525,8 @@ DEPENDENCIES rqrcode rspec rspec-html-matchers - rspec-rails (= 4.0.0.beta2) + rspec-rails + rswag-specs rtlit rubocop rubocop-discourse @@ -531,7 +534,6 @@ DEPENDENCIES ruby-prof ruby-readability rubyzip - sanitize sassc (= 2.0.1) sassc-rails seed-fu diff --git a/README.md b/README.md index ddf32ba126..fce15c577d 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https ## Contributing -[](https://travis-ci.org/discourse/discourse) +[](https://github.com/discourse/discourse/actions) Discourse is **100% free** and **open source**. We encourage and support an active, healthy community that accepts contributions from the public – including you! diff --git a/app/assets/javascripts/admin/components/admin-report.js b/app/assets/javascripts/admin/components/admin-report.js index 178bef98f2..757a90a7db 100644 --- a/app/assets/javascripts/admin/components/admin-report.js +++ b/app/assets/javascripts/admin/components/admin-report.js @@ -1,7 +1,7 @@ import discourseComputed from "discourse-common/utils/decorators"; import { makeArray } from "discourse-common/lib/helpers"; -import { alias, or, and, reads, equal, notEmpty } from "@ember/object/computed"; -import EmberObject from "@ember/object"; +import { alias, or, and, equal, notEmpty, not } from "@ember/object/computed"; +import EmberObject, { computed, action } from "@ember/object"; import { next } from "@ember/runloop"; import Component from "@ember/component"; import ReportLoader from "discourse/lib/reports-loader"; @@ -9,6 +9,7 @@ import { exportEntity } from "discourse/lib/export-csv"; import { outputExportResult } from "discourse/lib/export-result"; import Report, { SCHEMA_VERSION } from "admin/models/report"; import ENV from "discourse-common/config/environment"; +import { isPresent } from "@ember/utils"; const TABLE_OPTIONS = { perPage: 8, @@ -40,7 +41,12 @@ function collapseWeekly(data, average) { } export default Component.extend({ - classNameBindings: ["isEnabled", "isLoading", "dasherizedDataSourceName"], + classNameBindings: [ + "isVisible", + "isEnabled", + "isLoading", + "dasherizedDataSourceName" + ], classNames: ["admin-report"], isEnabled: true, disabledLabel: I18n.t("admin.dashboard.disabled"), @@ -62,6 +68,7 @@ export default Component.extend({ showDatesOptions: alias("model.dates_filtering"), showRefresh: or("showDatesOptions", "model.available_filters.length"), shouldDisplayTrend: and("showTrend", "model.prev_period"), + isVisible: not("isHidden"), init() { this._super(...arguments); @@ -69,8 +76,28 @@ export default Component.extend({ this._reports = []; }, - startDate: reads("filters.startDate"), - endDate: reads("filters.endDate"), + isHidden: computed("siteSettings.dashboard_hidden_reports", function() { + return (this.siteSettings.dashboard_hidden_reports || "") + .split("|") + .filter(Boolean) + .includes(this.dataSourceName); + }), + + startDate: computed("filters.startDate", function() { + if (this.filters && isPresent(this.filters.startDate)) { + return moment(this.filters.startDate, "YYYY-MM-DD"); + } else { + return moment(); + } + }), + + endDate: computed("filters.endDate", function() { + if (this.filters && isPresent(this.filters.endDate)) { + return moment(this.filters.endDate, "YYYY-MM-DD"); + } else { + return moment(); + } + }), didReceiveAttrs() { this._super(...arguments); @@ -126,39 +153,18 @@ export default Component.extend({ return `admin-report-${currentMode.replace(/_/g, "-")}`; }, - @discourseComputed("startDate") - normalizedStartDate(startDate) { - return startDate && typeof startDate.isValid === "function" - ? moment - .utc(startDate.toISOString()) - .locale("en") - .format("YYYYMMDD") - : moment(startDate) - .locale("en") - .format("YYYYMMDD"); - }, - - @discourseComputed("endDate") - normalizedEndDate(endDate) { - return endDate && typeof endDate.isValid === "function" - ? moment - .utc(endDate.toISOString()) - .locale("en") - .format("YYYYMMDD") - : moment(endDate) - .locale("en") - .format("YYYYMMDD"); - }, - @discourseComputed( "dataSourceName", - "normalizedStartDate", - "normalizedEndDate", + "startDate", + "endDate", "filters.customFilters" ) reportKey(dataSourceName, startDate, endDate, customFilters) { if (!dataSourceName || !startDate || !endDate) return null; + startDate = startDate.toISOString(true).split("T")[0]; + endDate = endDate.toISOString(true).split("T")[0]; + let reportKey = "reports:"; reportKey += [ dataSourceName, @@ -179,74 +185,61 @@ export default Component.extend({ return reportKey; }, - actions: { - onChangeEndDate(date) { - const startDate = moment(this.startDate); - const newEndDate = moment(date).endOf("day"); + @action + onChangeDateRange(range) { + this.send("refreshReport", { + startDate: range.from, + endDate: range.to + }); + }, - if (newEndDate.isSameOrAfter(startDate)) { - this.set("endDate", newEndDate.format("YYYY-MM-DD")); - } else { - this.set("endDate", startDate.endOf("day").format("YYYY-MM-DD")); - } + @action + applyFilter(id, value) { + let customFilters = this.get("filters.customFilters") || {}; - this.send("refreshReport"); - }, - - onChangeStartDate(date) { - const endDate = moment(this.endDate); - const newStartDate = moment(date).startOf("day"); - - if (newStartDate.isSameOrBefore(endDate)) { - this.set("startDate", newStartDate.format("YYYY-MM-DD")); - } else { - this.set("startDate", endDate.startOf("day").format("YYYY-MM-DD")); - } - - this.send("refreshReport"); - }, - - applyFilter(id, value) { - let customFilters = this.get("filters.customFilters") || {}; - - if (typeof value === "undefined") { - delete customFilters[id]; - } else { - customFilters[id] = value; - } - - this.attrs.onRefresh({ - type: this.get("model.type"), - startDate: this.startDate, - endDate: this.endDate, - filters: customFilters - }); - }, - - refreshReport() { - this.attrs.onRefresh({ - type: this.get("model.type"), - startDate: this.startDate, - endDate: this.endDate, - filters: this.get("filters.customFilters") - }); - }, - - exportCsv() { - const customFilters = this.get("filters.customFilters") || {}; - - exportEntity("report", { - name: this.get("model.type"), - start_date: this.startDate, - end_date: this.endDate, - category_id: customFilters.category, - group_id: customFilters.group - }).then(outputExportResult); - }, - - changeMode(mode) { - this.set("currentMode", mode); + if (typeof value === "undefined") { + delete customFilters[id]; + } else { + customFilters[id] = value; } + + this.send("refreshReport", { + filters: customFilters + }); + }, + + @action + refreshReport(options = {}) { + this.attrs.onRefresh({ + type: this.get("model.type"), + startDate: + typeof options.startDate === "undefined" + ? this.startDate + : options.startDate, + endDate: + typeof options.endDate === "undefined" ? this.endDate : options.endDate, + filters: + typeof options.filters === "undefined" + ? this.get("filters.customFilters") + : options.filters + }); + }, + + @action + exportCsv() { + const customFilters = this.get("filters.customFilters") || {}; + exportEntity("report", { + name: this.get("model.type"), + start_date: this.startDate.toISOString(true).split("T")[0], + end_date: this.endDate.toISOString(true).split("T")[0], + category_id: customFilters.category, + group_id: customFilters.group + }).then(outputExportResult); + }, + + @action + changeMode(mode) { + this.set("currentMode", mode); }, _computeReport() { @@ -276,10 +269,8 @@ export default Component.extend({ if (!this.startDate || !this.endDate) { report = sort(filteredReports)[0]; } else { - const reportKey = this.reportKey; - report = sort( - filteredReports.filter(r => r.report_key.includes(reportKey)) + filteredReports.filter(r => r.report_key.includes(this.reportKey)) )[0]; if (!report) return; @@ -339,15 +330,15 @@ export default Component.extend({ let payload = { data: { cache: true, facets } }; if (this.startDate) { - payload.data.start_date = moment - .utc(this.startDate, "YYYY-MM-DD") - .toISOString(); + payload.data.start_date = moment(this.startDate) + .toISOString(true) + .split("T")[0]; } if (this.endDate) { - payload.data.end_date = moment - .utc(this.endDate, "YYYY-MM-DD") - .toISOString(); + payload.data.end_date = moment(this.endDate) + .toISOString(true) + .split("T")[0]; } if (this.get("reportOptions.table.limit")) { diff --git a/app/assets/javascripts/admin/components/site-settings/tag-list.js b/app/assets/javascripts/admin/components/site-settings/tag-list.js index c8a8e0a06f..011c734b7d 100644 --- a/app/assets/javascripts/admin/components/site-settings/tag-list.js +++ b/app/assets/javascripts/admin/components/site-settings/tag-list.js @@ -1,15 +1,17 @@ import discourseComputed from "discourse-common/utils/decorators"; import Component from "@ember/component"; +import { action } from "@ember/object"; export default Component.extend({ @discourseComputed("value") selectedTags: { get(value) { - return value.split("|"); - }, - set(value) { - this.set("value", value.join("|")); - return value; + return value.split("|").filter(Boolean); } + }, + + @action + changeSelectedTags(tags) { + this.set("value", tags.join("|")); } }); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js index 006f1728da..5440c4260a 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js @@ -31,6 +31,7 @@ export default Controller.extend({ availableComponentsNames: mapBy("availableChildThemes", "name"), availableActiveComponentsNames: mapBy("availableActiveChildThemes", "name"), childThemesNames: mapBy("model.childThemes", "name"), + extraFiles: filterBy("model.theme_fields", "target", "extra_js"), @discourseComputed("model.editedFields") editedFieldsFormatted() { diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-general.js b/app/assets/javascripts/admin/controllers/admin-dashboard-general.js index b77e3e0288..5f2ea86819 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-general.js +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-general.js @@ -23,9 +23,44 @@ export default Controller.extend(PeriodComputationMixin, { @discourseComputed("siteSettings.dashboard_general_tab_activity_metrics") activityMetrics(metrics) { - return (metrics || "").split("|").filter(m => m); + return (metrics || "").split("|").filter(Boolean); }, + hiddenReports: computed("siteSettings.dashboard_hidden_reports", function() { + return (this.siteSettings.dashboard_hidden_reports || "") + .split("|") + .filter(Boolean); + }), + + isActivityMetricsVisible: computed( + "activityMetrics", + "hiddenReports", + function() { + return ( + this.activityMetrics.length && + this.activityMetrics.some(x => !this.hiddenReports.includes(x)) + ); + } + ), + + isSearchReportsVisible: computed("hiddenReports", function() { + return ["top_referred_topics", "trending_search"].some( + x => !this.hiddenReports.includes(x) + ); + }), + + isCommunityHealthVisible: computed("hiddenReports", function() { + return [ + "consolidated_page_views", + "signups", + "topics", + "posts", + "dau_by_mau", + "daily_engaged_users", + "new_contributors" + ].some(x => !this.hiddenReports.includes(x)); + }), + @discourseComputed activityMetricsFilters() { return { diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js b/app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js index 8925825fba..95f820b598 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js @@ -1,6 +1,7 @@ import discourseComputed from "discourse-common/utils/decorators"; import Controller from "@ember/controller"; import PeriodComputationMixin from "admin/mixins/period-computation"; +import { computed } from "@ember/object"; export default Controller.extend(PeriodComputationMixin, { @discourseComputed @@ -13,6 +14,16 @@ export default Controller.extend(PeriodComputationMixin, { }; }, + isModeratorsActivityVisible: computed( + "siteSettings.dashboard_hidden_reports", + function() { + return !(this.siteSettings.dashboard_hidden_reports || "") + .split("|") + .filter(Boolean) + .includes("moderators_activity"); + } + ), + @discourseComputed userFlaggingRatioOptions() { return { diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-reports.js b/app/assets/javascripts/admin/controllers/admin-dashboard-reports.js index d6e15496f7..c096c3e763 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-reports.js +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-reports.js @@ -8,17 +8,27 @@ const { get } = Ember; export default Controller.extend({ filter: null, - @discourseComputed("model.[]", "filter") + @discourseComputed( + "model.[]", + "filter", + "siteSettings.dashboard_hidden_reports" + ) filterReports(reports, filter) { if (filter) { filter = filter.toLowerCase(); - return reports.filter(report => { + reports = reports.filter(report => { return ( (get(report, "title") || "").toLowerCase().indexOf(filter) > -1 || (get(report, "description") || "").toLowerCase().indexOf(filter) > -1 ); }); } + + const hiddenReports = (this.siteSettings.dashboard_hidden_reports || "") + .split("|") + .filter(Boolean); + reports = reports.filter(report => !hiddenReports.includes(report.type)); + return reports; }, diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard.js b/app/assets/javascripts/admin/controllers/admin-dashboard.js index bd8561abc1..5946a1736d 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard.js +++ b/app/assets/javascripts/admin/controllers/admin-dashboard.js @@ -1,7 +1,7 @@ import discourseComputed from "discourse-common/utils/decorators"; -import { inject } from "@ember/controller"; -import Controller from "@ember/controller"; +import Controller, { inject } from "@ember/controller"; import { setting } from "discourse/lib/computed"; +import { computed } from "@ember/object"; import AdminDashboard from "admin/models/admin-dashboard"; import VersionCheck from "admin/models/version-check"; @@ -18,6 +18,24 @@ export default Controller.extend({ return this.currentUser.get("admin") && (problemsLength || 0) > 0; }, + visibleTabs: computed("siteSettings.dashboard_visible_tabs", function() { + return (this.siteSettings.dashboard_visible_tabs || "") + .split("|") + .filter(Boolean); + }), + + isModerationTabVisible: computed("visibleTabs", function() { + return this.visibleTabs.includes("moderation"); + }), + + isSecurityTabVisible: computed("visibleTabs", function() { + return this.visibleTabs.includes("security"); + }), + + isReportsTabVisible: computed("visibleTabs", function() { + return this.visibleTabs.includes("reports"); + }), + fetchProblems() { if (this.isLoadingProblems) return; diff --git a/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js b/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js index 84325f90d1..c5baa8eb9a 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js +++ b/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js @@ -42,7 +42,7 @@ export default Controller.extend({ if (grant.post_id) { i18nKey += "_post"; i18nParams.link = ` - ${Handlebars.Utils.escapeExpression(grant.title)} + ${escapeExpression(grant.title)} `; } diff --git a/app/assets/javascripts/admin/controllers/modals/admin-merge-users-confirmation.js b/app/assets/javascripts/admin/controllers/modals/admin-merge-users-confirmation.js index 2195c29631..91dbe7d072 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-merge-users-confirmation.js +++ b/app/assets/javascripts/admin/controllers/modals/admin-merge-users-confirmation.js @@ -2,6 +2,7 @@ import Controller, { inject as controller } from "@ember/controller"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import discourseComputed from "discourse-common/utils/decorators"; import { alias } from "@ember/object/computed"; +import { action } from "@ember/object"; export default Controller.extend(ModalFunctionality, { adminUserIndex: controller(), @@ -14,7 +15,10 @@ export default Controller.extend(ModalFunctionality, { @discourseComputed("username", "targetUsername") text(username, targetUsername) { - return `transfer @${username} to @${targetUsername}`; + return I18n.t(`admin.user.merge.confirmation.text`, { + username, + targetUsername + }); }, @discourseComputed("value", "text") @@ -22,14 +26,14 @@ export default Controller.extend(ModalFunctionality, { return !value || text !== value; }, - actions: { - merge() { - this.adminUserIndex.send("merge", this.targetUsername); - this.send("closeModal"); - }, + @action + confirm() { + this.adminUserIndex.send("merge", this.targetUsername); + this.send("closeModal"); + }, - cancel() { - this.send("closeModal"); - } + @action + close() { + this.send("closeModal"); } }); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-merge-users-prompt.js b/app/assets/javascripts/admin/controllers/modals/admin-merge-users-prompt.js index 535870cd6f..28fd6058e4 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-merge-users-prompt.js +++ b/app/assets/javascripts/admin/controllers/modals/admin-merge-users-prompt.js @@ -2,6 +2,7 @@ import Controller, { inject as controller } from "@ember/controller"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import discourseComputed from "discourse-common/utils/decorators"; import { alias } from "@ember/object/computed"; +import { action } from "@ember/object"; export default Controller.extend(ModalFunctionality, { adminUserIndex: controller(), @@ -16,14 +17,14 @@ export default Controller.extend(ModalFunctionality, { return !targetUsername || username === targetUsername; }, - actions: { - merge() { - this.send("closeModal"); - this.adminUserIndex.send("showMergeConfirmation", this.targetUsername); - }, + @action + showConfirmation() { + this.send("closeModal"); + this.adminUserIndex.send("showMergeConfirmation", this.targetUsername); + }, - cancel() { - this.send("closeModal"); - } + @action + close() { + this.send("closeModal"); } }); diff --git a/app/assets/javascripts/admin/helpers/check-icon.js b/app/assets/javascripts/admin/helpers/check-icon.js index 88cddc3d4d..4641a15fa2 100644 --- a/app/assets/javascripts/admin/helpers/check-icon.js +++ b/app/assets/javascripts/admin/helpers/check-icon.js @@ -1,7 +1,8 @@ import { registerUnbound } from "discourse-common/lib/helpers"; import { renderIcon } from "discourse-common/lib/icon-library"; +import { htmlSafe } from "@ember/template"; registerUnbound("check-icon", function(value) { let icon = value ? "check" : "times"; - return new Handlebars.SafeString(renderIcon("string", icon)); + return htmlSafe(renderIcon("string", icon)); }); diff --git a/app/assets/javascripts/admin/mixins/setting-component.js b/app/assets/javascripts/admin/mixins/setting-component.js index 200ec00c1f..e7afc119f2 100644 --- a/app/assets/javascripts/admin/mixins/setting-component.js +++ b/app/assets/javascripts/admin/mixins/setting-component.js @@ -7,6 +7,7 @@ import Mixin from "@ember/object/mixin"; import showModal from "discourse/lib/show-modal"; import { Promise } from "rsvp"; import { ajax } from "discourse/lib/ajax"; +import { htmlSafe } from "@ember/template"; const CUSTOM_TYPES = [ "bool", @@ -63,7 +64,7 @@ export default Mixin.create({ } let preview = setting.get("preview"); if (preview) { - return new Handlebars.SafeString( + return htmlSafe( "
당신이 지금 처음에 돌아 왔음을 알았습니까? `%{search_answer}` 그림 이모티콘 **을 사용하여이 가난한 배고픈 카피 바라에게 먹이를 주면 자동으로 끝납니다.
reply: |-
예이, 찾았네! :tada:
@@ -243,11 +262,17 @@ ko:
- 진짜 실물 :keyboard:가 있다면, ?를 눌러서 간편한 단축키가 뭐가 있는지 알아봐.
not_found: |-
흠.... 아무래도 문제가 있어 보이네. 미안해. 검색
+
+ name' 속성을 사용해서 투표에 특색을 부여해보세요."
multiple_polls_with_same_name: "같은 이름 %{name} 으로 투표가 여러 개 있습니다. 'name' 속성을 사용하여 투표를 구분해보세요."
+ default_poll_must_have_at_least_1_option: "설문 조사에는 1 개 이상의 옵션이 있어야합니다."
+ named_poll_must_have_at_least_1_option: "이름이 %{name} 인 설문 조사에는 하나 이상의 옵션이 있어야합니다."
default_poll_must_have_less_options:
other: "투표 항목은 %{count}개보다 적어야합니다."
named_poll_must_have_less_options:
other: "%{name} 투표 에는 최소 %{count} 개의 투표 항목이 있어야 합니다."
default_poll_must_have_different_options: "투표 항목은 각각의 내용이 달라야 합니다."
named_poll_must_have_different_options: "%{name} 투표의 투표 항목 내용이 제각기 달라야 합니다."
+ default_poll_must_not_have_any_empty_options: "설문 조사에는 빈 옵션이 없어야합니다."
+ named_poll_must_not_have_any_empty_options: "이름이 %{name} 인 폴에는 빈 옵션이 없어야 합니다."
default_poll_with_multiple_choices_has_invalid_parameters: "복수응답 가능한 투표가 잘못된 매개변수를 가지고 있습니다."
named_poll_with_multiple_choices_has_invalid_parameters: "복수응답 가능한 %{name} 투표가 잘못된 매개변수를 가지고 있습니다."
requires_at_least_1_valid_option: "유효한 투표 항목을 적어도 1개는 선택해야 합니다."
+ edit_window_expired:
+ cannot_edit_default_poll_with_votes: "처음 %{minutes} 분 후에는 폴링을 변경할 수 없습니다."
+ cannot_edit_named_poll_with_votes: "처음 %{minutes} 분 후에는 폴 이름 $ {name} 을 (를) 변경할 수 없습니다."
no_poll_with_this_name: "이 포스트와 관련한 %{name} 라는 이름의 투표는 없습니다."
post_is_deleted: "지워진 포스트에는 불가능합니다."
+ user_cant_post_in_topic: "이 주제에 게시 할 수 없으므로 투표 할 수 없습니다."
topic_must_be_open_to_vote: "토픽이 열려야 투표를 할 수 있습니다."
poll_must_be_open_to_vote: "투표가 열려있어야 표결할 수 있습니다."
topic_must_be_open_to_toggle_status: "토픽이 열려야 상태 변경을 할 수 있습니다."
only_staff_or_op_can_toggle_status: "운영진이나 작성자만이 투표 상태를 변경할 수 있습니다."
+ insufficient_rights_to_create: "설문 조사를 만들 수 없습니다."
email:
link_to_poll: "클릭해서 투표보기"
+ user_field:
+ no_data: "데이터 없음"
diff --git a/plugins/poll/spec/models/poll_spec.rb b/plugins/poll/spec/models/poll_spec.rb
index 55e6567d47..fed3310b84 100644
--- a/plugins/poll/spec/models/poll_spec.rb
+++ b/plugins/poll/spec/models/poll_spec.rb
@@ -69,4 +69,18 @@ describe ::DiscoursePoll::Poll do
expect(poll.can_see_results?(user)).to eq(true)
end
end
+
+ describe 'when post is trashed' do
+ it "maintains the association" do
+ user = Fabricate(:user)
+ post = Fabricate(:post, raw: "[poll results=staff_only]\n- A\n- B\n[/poll]", user: user)
+ poll = post.polls.first
+
+ post.trash!
+ poll.reload
+
+ expect(poll.post).to eq(post)
+ end
+
+ end
end
diff --git a/script/bulk_import/base.rb b/script/bulk_import/base.rb
index 8af89155e1..9fc7b1cd17 100644
--- a/script/bulk_import/base.rb
+++ b/script/bulk_import/base.rb
@@ -26,7 +26,7 @@ module BulkImport; end
class BulkImport::Base
- NOW ||= "now()".freeze
+ NOW ||= "now()"
PRIVATE_OFFSET ||= 2**30
# rubocop:disable Layout/HashAlignment
@@ -660,7 +660,7 @@ class BulkImport::Base
imported_ids << mapped[:imported_id] unless mapped[:imported_id].nil?
imported_ids |= mapped[:imported_ids] unless mapped[:imported_ids].nil?
@raw_connection.put_copy_data columns.map { |c| processed[c] }
- print "\r%7d - %6d/sec".freeze % [imported_ids.size, imported_ids.size.to_f / (Time.now - start)] if imported_ids.size % 5000 == 0
+ print "\r%7d - %6d/sec" % [imported_ids.size, imported_ids.size.to_f / (Time.now - start)] if imported_ids.size % 5000 == 0
rescue => e
puts "\n"
puts "ERROR: #{e.inspect}"
@@ -669,7 +669,7 @@ class BulkImport::Base
end
if imported_ids.size > 0
- print "\r%7d - %6d/sec".freeze % [imported_ids.size, imported_ids.size.to_f / (Time.now - start)]
+ print "\r%7d - %6d/sec" % [imported_ids.size, imported_ids.size.to_f / (Time.now - start)]
puts
end
diff --git a/script/bulk_import/discourse_merger.rb b/script/bulk_import/discourse_merger.rb
index 9029bdb28a..35ff800822 100644
--- a/script/bulk_import/discourse_merger.rb
+++ b/script/bulk_import/discourse_merger.rb
@@ -4,7 +4,7 @@ require_relative "base"
class BulkImport::DiscourseMerger < BulkImport::Base
- NOW ||= "now()".freeze
+ NOW ||= "now()"
CUSTOM_FIELDS = ['category', 'group', 'post', 'topic', 'user']
# DB_NAME: name of database being merged into the current local db
diff --git a/script/bulk_import/vanilla.rb b/script/bulk_import/vanilla.rb
index 2efed5f31f..71307dba0a 100644
--- a/script/bulk_import/vanilla.rb
+++ b/script/bulk_import/vanilla.rb
@@ -209,7 +209,7 @@ class BulkImport::Vanilla < BulkImport::Base
User.find_each do |u|
count += 1
- print "\r%7d - %6d/sec".freeze % [count, count.to_f / (Time.now - start)]
+ print "\r%7d - %6d/sec" % [count, count.to_f / (Time.now - start)]
next unless u.custom_fields["import_id"]
@@ -276,7 +276,7 @@ class BulkImport::Vanilla < BulkImport::Base
Post.where("raw LIKE '%/us.v-cdn.net/%' OR raw LIKE '%[attachment%'").find_each do |post|
count += 1
- print "\r%7d - %6d/sec".freeze % [count, count.to_f / (Time.now - start)]
+ print "\r%7d - %6d/sec" % [count, count.to_f / (Time.now - start)]
new_raw = post.raw.dup
new_raw.gsub!(attachment_regex) do |s|
@@ -613,7 +613,7 @@ class BulkImport::Vanilla < BulkImport::Base
)
end
- print "\r%7d - %6d/sec".freeze % [count, count.to_f / (Time.now - start)] if count % 5000 == 0
+ print "\r%7d - %6d/sec" % [count, count.to_f / (Time.now - start)] if count % 5000 == 0
end
end
@@ -645,7 +645,7 @@ class BulkImport::Vanilla < BulkImport::Base
end
end
- print "\r%7d - %6d/sec".freeze % [count, count.to_f / (Time.now - start)] if count % 5000 == 0
+ print "\r%7d - %6d/sec" % [count, count.to_f / (Time.now - start)] if count % 5000 == 0
end
end
end
diff --git a/script/bulk_import/vbulletin.rb b/script/bulk_import/vbulletin.rb
index fef661fa91..b437137c38 100644
--- a/script/bulk_import/vbulletin.rb
+++ b/script/bulk_import/vbulletin.rb
@@ -608,7 +608,7 @@ class BulkImport::VBulletin < BulkImport::Base
count = 0
Dir.foreach(AVATAR_DIR) do |item|
- print "\r%7d - %6d/sec".freeze % [count, count.to_f / (Time.now - start)]
+ print "\r%7d - %6d/sec" % [count, count.to_f / (Time.now - start)]
next if item == ('.') || item == ('..') || item == ('.DS_Store')
next unless item =~ /avatar(\d+)_(\d).gif/
diff --git a/script/import_scripts/base.rb b/script/import_scripts/base.rb
index ea43db2730..48bd67e516 100644
--- a/script/import_scripts/base.rb
+++ b/script/import_scripts/base.rb
@@ -606,9 +606,10 @@ class ImportScripts::Base
skipped += 1
puts "Skipping bookmark for user id #{params[:user_id]} and post id #{params[:post_id]}"
else
- result = PostActionCreator.create(user, post, :bookmark)
- created += 1 if result.success?
- skipped += 1 if result.failed?
+ result = BookmarkManager.new(user).create(post_id: post.id)
+
+ created += 1 if result.errors.none?
+ skipped += 1 if result.errors.any?
end
end
diff --git a/script/import_scripts/mbox/support/indexer.rb b/script/import_scripts/mbox/support/indexer.rb
index 7017c2a59f..dc6e092c29 100644
--- a/script/import_scripts/mbox/support/indexer.rb
+++ b/script/import_scripts/mbox/support/indexer.rb
@@ -39,7 +39,7 @@ module ImportScripts::Mbox
private
- METADATA_FILENAME = 'metadata.yml'.freeze
+ METADATA_FILENAME = 'metadata.yml'
IGNORED_FILE_EXTENSIONS = ['.dbindex', '.dbnames', '.digest', '.subjects', '.yml']
def index_category(directory)
diff --git a/script/import_scripts/vbulletin5.rb b/script/import_scripts/vbulletin5.rb
index 38489314b2..682cbcdc50 100644
--- a/script/import_scripts/vbulletin5.rb
+++ b/script/import_scripts/vbulletin5.rb
@@ -6,14 +6,19 @@ require 'htmlentities'
class ImportScripts::VBulletin < ImportScripts::Base
BATCH_SIZE = 1000
- DBPREFIX = "vb_"
ROOT_NODE = 2
-
- # CHANGE THESE BEFORE RUNNING THE IMPORTER
- DATABASE = "yourforum"
TIMEZONE = "America/Los_Angeles"
- ATTACHMENT_DIR = '/home/discourse/yourforum/customattachments/'
- AVATAR_DIR = '/home/discourse/yourforum/avatars/'
+
+ # override these using environment vars
+
+ URL_PREFIX ||= ENV['URL_PREFIX'] || "forum/"
+ DB_PREFIX ||= ENV['DB_PREFIX'] || "vb_"
+ DB_HOST ||= ENV['DB_HOST'] || "localhost"
+ DB_NAME ||= ENV['DB_NAME'] || "vbulletin"
+ DB_PASS ||= ENV['DB_PASS'] || "password"
+ DB_USER ||= ENV['DB_USER'] || "username"
+ ATTACH_DIR ||= ENV['ATTACH_DIR'] || "/home/discourse/vbulletin/attach"
+ AVATAR_DIR ||= ENV['AVATAR_DIR'] || "/home/discourse/vbulletin/avatars"
def initialize
super
@@ -25,12 +30,15 @@ class ImportScripts::VBulletin < ImportScripts::Base
@htmlentities = HTMLEntities.new
@client = Mysql2::Client.new(
- host: "localhost",
- username: "root",
- database: DATABASE,
- password: "password"
+ host: DB_HOST,
+ username: DB_USER,
+ database: DB_NAME,
+ password: DB_PASS
)
+ @forum_typeid = mysql_query("SELECT contenttypeid FROM #{DB_PREFIX}contenttype WHERE class='Forum'").first['contenttypeid']
+ @channel_typeid = mysql_query("SELECT contenttypeid FROM #{DB_PREFIX}contenttype WHERE class='Channel'").first['contenttypeid']
+ @text_typeid = mysql_query("SELECT contenttypeid FROM #{DB_PREFIX}contenttype WHERE class='Text'").first['contenttypeid']
end
def execute
@@ -40,8 +48,10 @@ class ImportScripts::VBulletin < ImportScripts::Base
import_topics
import_posts
import_attachments
+ import_tags
close_topics
post_process_posts
+ create_permalinks
end
def import_groups
@@ -49,7 +59,7 @@ class ImportScripts::VBulletin < ImportScripts::Base
groups = mysql_query <<-SQL
SELECT usergroupid, title
- FROM #{DBPREFIX}usergroup
+ FROM #{DB_PREFIX}usergroup
ORDER BY usergroupid
SQL
@@ -64,7 +74,7 @@ class ImportScripts::VBulletin < ImportScripts::Base
def import_users
puts "", "importing users"
- user_count = mysql_query("SELECT COUNT(userid) count FROM #{DBPREFIX}user").first["count"]
+ user_count = mysql_query("SELECT COUNT(userid) count FROM #{DB_PREFIX}user").first["count"]
batches(BATCH_SIZE) do |offset|
users = mysql_query <<-SQL
@@ -73,8 +83,8 @@ class ImportScripts::VBulletin < ImportScripts::Base
WHEN u.scheme='legacy' THEN REPLACE(token, ' ', ':')
END AS password,
IF(ug.title = 'Administrators', 1, 0) AS admin
- FROM #{DBPREFIX}user u
- LEFT JOIN #{DBPREFIX}usergroup ug ON ug.usergroupid = u.usergroupid
+ FROM #{DB_PREFIX}user u
+ LEFT JOIN #{DB_PREFIX}usergroup ug ON ug.usergroupid = u.usergroupid
ORDER BY userid
LIMIT #{BATCH_SIZE}
OFFSET #{offset}
@@ -101,7 +111,7 @@ class ImportScripts::VBulletin < ImportScripts::Base
post_create_action: proc do |u|
@old_username_to_new_usernames[user["username"]] = u.username
import_profile_picture(user, u)
- import_profile_background(user, u)
+ # import_profile_background(user, u)
end
}
end
@@ -111,7 +121,7 @@ class ImportScripts::VBulletin < ImportScripts::Base
def import_profile_picture(old_user, imported_user)
query = mysql_query <<-SQL
SELECT filedata, filename
- FROM #{DBPREFIX}customavatar
+ FROM #{DB_PREFIX}customavatar
WHERE userid = #{old_user["userid"]}
ORDER BY dateline DESC
LIMIT 1
@@ -148,7 +158,7 @@ class ImportScripts::VBulletin < ImportScripts::Base
def import_profile_background(old_user, imported_user)
query = mysql_query <<-SQL
SELECT filedata, filename
- FROM #{DBPREFIX}customprofilepic
+ FROM #{DB_PREFIX}customprofilepic
WHERE userid = #{old_user["userid"]}
ORDER BY dateline DESC
LIMIT 1
@@ -176,13 +186,13 @@ class ImportScripts::VBulletin < ImportScripts::Base
puts "", "importing top level categories..."
categories = mysql_query("SELECT nodeid AS forumid, title, description, displayorder, parentid
- FROM #{DBPREFIX}node
+ FROM #{DB_PREFIX}node
WHERE parentid=#{ROOT_NODE}
UNION
SELECT nodeid, title, description, displayorder, parentid
- FROM #{DBPREFIX}node
- WHERE contenttypeid = 23
- AND parentid IN (SELECT nodeid FROM #{DBPREFIX}node WHERE parentid=#{ROOT_NODE})").to_a
+ FROM #{DB_PREFIX}node
+ WHERE contenttypeid = #{@channel_typeid}
+ AND parentid IN (SELECT nodeid FROM #{DB_PREFIX}node WHERE parentid=#{ROOT_NODE})").to_a
top_level_categories = categories.select { |c| c["parentid"] == ROOT_NODE }
@@ -224,19 +234,26 @@ class ImportScripts::VBulletin < ImportScripts::Base
# keep track of closed topics
@closed_topic_ids = []
- topic_count = mysql_query("select count(nodeid) cnt from #{DBPREFIX}node where parentid in (
- select nodeid from #{DBPREFIX}node where contenttypeid=23 ) and contenttypeid=22;").first["cnt"]
+ topic_count = mysql_query("SELECT COUNT(nodeid) cnt
+ FROM #{DB_PREFIX}node
+ WHERE (unpublishdate = 0 OR unpublishdate IS NULL)
+ AND (approved = 1 AND showapproved = 1)
+ AND parentid IN (
+ SELECT nodeid FROM #{DB_PREFIX}node WHERE contenttypeid=#{@channel_typeid} ) AND contenttypeid=#{@text_typeid};"
+ ).first["cnt"]
batches(BATCH_SIZE) do |offset|
topics = mysql_query <<-SQL
SELECT t.nodeid AS threadid, t.title, t.parentid AS forumid,t.open,t.userid AS postuserid,t.publishdate AS dateline,
nv.count views, 1 AS visible, t.sticky,
CONVERT(CAST(rawtext AS BINARY)USING utf8) AS raw
- FROM #{DBPREFIX}node t
- LEFT JOIN #{DBPREFIX}nodeview nv ON nv.nodeid=t.nodeid
- LEFT JOIN #{DBPREFIX}text txt ON txt.nodeid=t.nodeid
- WHERE t.parentid in ( select nodeid from #{DBPREFIX}node where contenttypeid=23 )
- AND t.contenttypeid = 22
+ FROM #{DB_PREFIX}node t
+ LEFT JOIN #{DB_PREFIX}nodeview nv ON nv.nodeid=t.nodeid
+ LEFT JOIN #{DB_PREFIX}text txt ON txt.nodeid=t.nodeid
+ WHERE t.parentid in ( select nodeid from #{DB_PREFIX}node where contenttypeid=#{@channel_typeid} )
+ AND t.contenttypeid = #{@text_typeid}
+ AND (t.unpublishdate = 0 OR t.unpublishdate IS NULL)
+ AND t.approved = 1 AND t.showapproved = 1
ORDER BY t.nodeid
LIMIT #{BATCH_SIZE}
OFFSET #{offset}
@@ -277,19 +294,19 @@ class ImportScripts::VBulletin < ImportScripts::Base
rescue
end
- post_count = mysql_query("SELECT COUNT(nodeid) cnt FROM #{DBPREFIX}node WHERE parentid NOT IN (
- SELECT nodeid FROM #{DBPREFIX}node WHERE contenttypeid=23 ) AND contenttypeid=22;").first["cnt"]
+ post_count = mysql_query("SELECT COUNT(nodeid) cnt FROM #{DB_PREFIX}node WHERE parentid NOT IN (
+ SELECT nodeid FROM #{DB_PREFIX}node WHERE contenttypeid=#{@channel_typeid} ) AND contenttypeid=#{@text_typeid};").first["cnt"]
batches(BATCH_SIZE) do |offset|
posts = mysql_query <<-SQL
SELECT p.nodeid AS postid, p.userid AS userid, p.parentid AS threadid,
CONVERT(CAST(rawtext AS BINARY)USING utf8) AS raw, p.publishdate AS dateline,
1 AS visible, p.parentid AS parentid
- FROM #{DBPREFIX}node p
- LEFT JOIN #{DBPREFIX}nodeview nv ON nv.nodeid=p.nodeid
- LEFT JOIN #{DBPREFIX}text txt ON txt.nodeid=p.nodeid
- WHERE p.parentid NOT IN ( select nodeid from #{DBPREFIX}node where contenttypeid=23 )
- AND p.contenttypeid = 22
+ FROM #{DB_PREFIX}node p
+ LEFT JOIN #{DB_PREFIX}nodeview nv ON nv.nodeid=p.nodeid
+ LEFT JOIN #{DB_PREFIX}text txt ON txt.nodeid=p.nodeid
+ WHERE p.parentid NOT IN ( select nodeid from #{DB_PREFIX}node where contenttypeid=#{@channel_typeid} )
+ AND p.contenttypeid = #{@text_typeid}
ORDER BY postid
LIMIT #{BATCH_SIZE}
OFFSET #{offset}
@@ -320,86 +337,65 @@ class ImportScripts::VBulletin < ImportScripts::Base
end
end
- # find the uploaded file information from the db
- def find_upload(post, attachment_id)
- sql = "SELECT a.filedataid, a.filename, fd.userid, LENGTH(fd.filedata) AS dbsize, filedata
- FROM #{DBPREFIX}attach a
- LEFT JOIN #{DBPREFIX}filedata fd ON fd.filedataid = a.filedataid
- WHERE a.nodeid = #{attachment_id}"
- results = mysql_query(sql)
-
- unless (row = results.first)
- puts "Couldn't find attachment record for post.id = #{post.id}, import_id = #{post.custom_fields['import_id']}"
- return nil
- end
-
- filename = File.join(ATTACHMENT_DIR, row['userid'].to_s.split('').join('/'), "#{row['filedataid']}.attach")
- real_filename = row['filename']
- real_filename.prepend SecureRandom.hex if real_filename[0] == '.'
-
- unless File.exists?(filename)
- if row['dbsize'].to_i == 0
- puts "Attachment file #{row['filedataid']} doesn't exist"
- return nil
- end
-
- tmpfile = 'attach_' + row['filedataid'].to_s
- filename = File.join('/tmp/', tmpfile)
- File.open(filename, 'wb') { |f|
- #f.write(PG::Connection.unescape_bytea(row['filedata']))
- f.write(row['filedata'])
- }
- end
-
- upload = create_upload(post.user.id, filename, real_filename)
-
- if upload.nil? || !upload.valid?
- puts "Upload not valid :("
- puts upload.errors.inspect if upload
- return nil
- end
-
- [upload, real_filename]
- rescue Mysql2::Error => e
- puts "SQL Error"
- puts e.message
- puts sql
- nil
- end
-
def import_attachments
puts '', 'importing attachments...'
+ ext = mysql_query("SELECT GROUP_CONCAT(DISTINCT(extension)) exts FROM #{DB_PREFIX}filedata").first['exts'].split(',')
+ SiteSetting.authorized_extensions = (SiteSetting.authorized_extensions.split("|") + ext).uniq.join("|")
+
+ uploads = mysql_query <<-SQL
+ SELECT n.parentid nodeid, a.filename, fd.userid, LENGTH(fd.filedata) AS dbsize, filedata, fd.filedataid
+ FROM #{DB_PREFIX}attach a
+ LEFT JOIN #{DB_PREFIX}filedata fd ON fd.filedataid = a.filedataid
+ LEFT JOIN #{DB_PREFIX}node n on n.nodeid = a.nodeid
+ SQL
+
current_count = 0
- total_count = mysql_query("SELECT COUNT(nodeid) cnt FROM #{DBPREFIX}node WHERE contenttypeid=22 ").first["cnt"]
+ total_count = uploads.count
- success_count = 0
- fail_count = 0
+ uploads.each do |upload|
+ post_id = PostCustomField.where(name: 'import_id').where(value: upload['nodeid']).first&.post_id
+ post_id = PostCustomField.where(name: 'import_id').where(value: "thread-#{upload['nodeid']}").first&.post_id unless post_id
+ if post_id.nil?
+ puts "Post for #{upload['nodeid']} not found"
+ next
+ end
+ post = Post.find(post_id)
- attachment_regex = /\[attach[^\]]*\]n(\d+)\[\/attach\]/i
+ filename = File.join(ATTACH_DIR, upload['userid'].to_s.split('').join('/'), "#{upload['filedataid']}.attach")
+ real_filename = upload['filename']
+ real_filename.prepend SecureRandom.hex if real_filename[0] == '.'
- Post.find_each do |post|
- current_count += 1
- print_status current_count, total_count
-
- new_raw = post.raw.dup
- new_raw.gsub!(attachment_regex) do |s|
- matches = attachment_regex.match(s)
- attachment_id = matches[1]
-
- upload, filename = find_upload(post, attachment_id)
- unless upload
- fail_count += 1
+ unless File.exists?(filename)
+ # attachments can be on filesystem or in database
+ # try to retrieve from database if the file did not exist on filesystem
+ if upload['dbsize'].to_i == 0
+ puts "Attachment file #{upload['filedataid']} doesn't exist"
next
end
- html_for_upload(upload, filename)
+
+ tmpfile = 'attach_' + upload['filedataid'].to_s
+ filename = File.join('/tmp/', tmpfile)
+ File.open(filename, 'wb') { |f|
+ #f.write(PG::Connection.unescape_bytea(row['filedata']))
+ f.write(upload['filedata'])
+ }
end
- if new_raw != post.raw
- PostRevisor.new(post).revise!(post.user, { raw: new_raw }, bypass_bump: true, edit_reason: 'Import attachments from vBulletin')
+ upl_obj = create_upload(post.user.id, filename, real_filename)
+ if upl_obj&.persisted?
+ html = html_for_upload(upl_obj, real_filename)
+ if !post.raw[html]
+ post.raw += "\n\n#{html}\n\n"
+ post.save!
+ PostUpload.create!(post: post, upload: upl_obj) unless PostUpload.where(post: post, upload: upl_obj).exists?
+ end
+ else
+ puts "Fail"
+ exit
end
-
- success_count += 1
+ current_count += 1
+ print_status(current_count, total_count)
end
end
@@ -619,6 +615,105 @@ class ImportScripts::VBulletin < ImportScripts::Base
raw
end
+ def create_permalinks
+ puts "", "creating permalinks..."
+
+ current_count = 0
+ total_count = mysql_query("SELECT COUNT(nodeid) cnt
+ FROM #{DB_PREFIX}node
+ WHERE (unpublishdate = 0 OR unpublishdate IS NULL)
+ AND (approved = 1 AND showapproved = 1)
+ AND parentid IN (
+ SELECT nodeid FROM #{DB_PREFIX}node WHERE contenttypeid=#{@channel_typeid} ) AND contenttypeid=#{@text_typeid};"
+ ).first["cnt"]
+
+ batches(BATCH_SIZE) do |offset|
+ topics = mysql_query <<-SQL
+ SELECT p.urlident p1, f.urlident p2, t.nodeid, t.urlident p3
+ FROM #{DB_PREFIX}node f
+ LEFT JOIN #{DB_PREFIX}node t ON t.parentid = f.nodeid
+ LEFT JOIN #{DB_PREFIX}node p ON p.nodeid = f.parentid
+ WHERE f.contenttypeid = #{@channel_typeid}
+ AND t.contenttypeid = #{@text_typeid}
+ AND t.approved = 1 AND t.showapproved = 1
+ AND (t.unpublishdate = 0 OR t.unpublishdate IS NULL)
+ ORDER BY t.nodeid
+ LIMIT #{BATCH_SIZE}
+ OFFSET #{offset}
+ SQL
+
+ break if topics.size < 1
+
+ topics.each do |topic|
+ current_count += 1
+ print_status current_count, total_count
+ disc_topic = topic_lookup_from_imported_post_id("thread-#{topic['nodeid']}")
+
+ Permalink.create(
+ url: "#{URL_PREFIX}#{topic['p1']}/#{topic['p2']}/#{topic['nodeid']}-#{topic['p3']}",
+ topic_id: disc_topic[:topic_id]
+ ) rescue nil
+ end
+ end
+
+ # cats
+ cats = mysql_query <<-SQL
+ SELECT nodeid, urlident
+ FROM #{DB_PREFIX}node
+ WHERE contenttypeid=#{@channel_typeid}
+ AND parentid=#{ROOT_NODE};
+ SQL
+ cats.each do |c|
+ category_id = CategoryCustomField.where(name: 'import_id').where(value: c['nodeid']).first.category_id
+ Permalink.create(url: "#{URL_PREFIX}#{c['urlident']}", category_id: category_id) rescue nil
+ end
+
+ # subcats
+ subcats = mysql_query <<-SQL
+ SELECT n1.nodeid,n2.urlident p1,n1.urlident p2
+ FROM #{DB_PREFIX}node n1
+ LEFT JOIN #{DB_PREFIX}node n2 ON n2.nodeid=n1.parentid
+ WHERE n2.parentid = #{ROOT_NODE}
+ AND n1.contenttypeid=#{@channel_typeid};
+ SQL
+ subcats.each do |sc|
+ category_id = CategoryCustomField.where(name: 'import_id').where(value: sc['nodeid']).first.category_id
+ Permalink.create(url: "#{URL_PREFIX}#{sc['p1']}/#{sc['p2']}", category_id: category_id) rescue nil
+ end
+ end
+
+ def import_tags
+ puts "", "importing tags..."
+
+ SiteSetting.tagging_enabled = true
+ SiteSetting.max_tags_per_topic = 100
+ staff_guardian = Guardian.new(Discourse.system_user)
+
+ records = mysql_query(<<~SQL
+ SELECT nodeid, GROUP_CONCAT(tagtext) tags
+ FROM #{DB_PREFIX}tag t
+ LEFT JOIN #{DB_PREFIX}tagnode tn ON tn.tagid = t.tagid
+ WHERE t.tagid IS NOT NULL
+ AND tn.nodeid IS NOT NULL
+ GROUP BY nodeid
+ SQL
+ ).to_a
+
+ current_count = 0
+ total_count = records.count
+
+ records.each do |rec|
+ current_count += 1
+ print_status current_count, total_count
+ tl = topic_lookup_from_imported_post_id("thread-#{rec['nodeid']}")
+ next if tl.nil? # topic might have been deleted
+
+ topic = Topic.find(tl[:topic_id])
+ tag_names = rec['tags'].force_encoding("UTF-8").split(',')
+ DiscourseTagging.tag_topic_by_names(topic, staff_guardian, tag_names)
+ end
+ end
+
def parse_timestamp(timestamp)
Time.zone.at(@tz.utc_to_local(timestamp))
end
@@ -626,7 +721,6 @@ class ImportScripts::VBulletin < ImportScripts::Base
def mysql_query(sql)
@client.query(sql, cache_rows: false)
end
-
end
ImportScripts::VBulletin.new.perform
diff --git a/spec/components/auth/github_authenticator_spec.rb b/spec/components/auth/github_authenticator_spec.rb
index 37c14b1546..ae50424c90 100644
--- a/spec/components/auth/github_authenticator_spec.rb
+++ b/spec/components/auth/github_authenticator_spec.rb
@@ -60,6 +60,17 @@ describe Auth::GithubAuthenticator do
expect(result.email_valid).to eq(true)
end
+ it 'can authenticate and update GitHub screen_name for existing user' do
+ GithubUserInfo.create!(user_id: user.id, github_user_id: 100, screen_name: "boris")
+
+ result = authenticator.after_authenticate(data)
+
+ expect(result.user.id).to eq(user.id)
+ expect(result.email).to eq(user.email)
+ expect(result.email_valid).to eq(true)
+ expect(GithubUserInfo.where(user_id: user.id).pluck(:screen_name)).to eq([user.username])
+ end
+
it 'should use primary email for new user creation over other available emails' do
hash = {
extra: {
diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb
index 94d87ce0b0..47262f1342 100644
--- a/spec/components/cooked_post_processor_spec.rb
+++ b/spec/components/cooked_post_processor_spec.rb
@@ -552,7 +552,8 @@ describe CookedPostProcessor do
upload.update(secure: true, access_control_post: post)
end
- it "handles secure images with the correct lightbox link href" do
+ # TODO fix this spec, it is sometimes getting CDN links when it runs concurrently
+ xit "handles secure images with the correct lightbox link href" do
cpp.post_process
expect(cpp.html).to match_html cooked_html
diff --git a/spec/components/discourse_tagging_spec.rb b/spec/components/discourse_tagging_spec.rb
index 35ff67a9a9..b8d328f980 100644
--- a/spec/components/discourse_tagging_spec.rb
+++ b/spec/components/discourse_tagging_spec.rb
@@ -475,7 +475,7 @@ describe DiscourseTagging do
describe "clean_tag" do
it "downcases new tags if setting enabled" do
- expect(DiscourseTagging.clean_tag("HeLlO".freeze)).to eq("hello")
+ expect(DiscourseTagging.clean_tag("HeLlO")).to eq("hello")
SiteSetting.force_lowercase_tags = false
expect(DiscourseTagging.clean_tag("HeLlO")).to eq("HeLlO")
diff --git a/spec/components/guardian/user_guardian_spec.rb b/spec/components/guardian/user_guardian_spec.rb
index e870843549..b812fa9526 100644
--- a/spec/components/guardian/user_guardian_spec.rb
+++ b/spec/components/guardian/user_guardian_spec.rb
@@ -341,4 +341,73 @@ describe UserGuardian do
include_examples "can_delete_user staff examples"
end
end
+
+ describe "#can_merge_user?" do
+ shared_examples "can_merge_user examples" do
+ it "isn't allowed if user is a staff" do
+ staff = Fabricate(:moderator)
+ expect(guardian.can_merge_user?(staff)).to eq(false)
+ end
+ end
+
+ context "for moderators" do
+ let(:guardian) { Guardian.new(moderator) }
+ include_examples "can_merge_user examples"
+
+ it "isn't allowed if current_user is not an admin" do
+ expect(guardian.can_merge_user?(user)).to eq(false)
+ end
+ end
+
+ context "for admins" do
+ let(:guardian) { Guardian.new(admin) }
+ include_examples "can_merge_user examples"
+ end
+ end
+
+ describe "#can_see_review_queue?" do
+ it 'returns true when the user is a staff member' do
+ guardian = Guardian.new(moderator)
+ expect(guardian.can_see_review_queue?).to eq(true)
+ end
+
+ it 'returns false for a regular user' do
+ guardian = Guardian.new(user)
+ expect(guardian.can_see_review_queue?).to eq(false)
+ end
+
+ it "returns true when the user's group can review an item in the queue" do
+ group = Fabricate(:group)
+ group.add(user)
+ guardian = Guardian.new(user)
+ SiteSetting.enable_category_group_review = true
+
+ Fabricate(:reviewable_flagged_post, reviewable_by_group: group, category: nil)
+
+ expect(guardian.can_see_review_queue?).to eq(true)
+ end
+
+ it 'returns false if category group review is disabled' do
+ group = Fabricate(:group)
+ group.add(user)
+ guardian = Guardian.new(user)
+ SiteSetting.enable_category_group_review = false
+
+ Fabricate(:reviewable_flagged_post, reviewable_by_group: group, category: nil)
+
+ expect(guardian.can_see_review_queue?).to eq(false)
+ end
+
+ it 'returns false if the reviewable is under a read restricted category' do
+ group = Fabricate(:group)
+ group.add(user)
+ guardian = Guardian.new(user)
+ SiteSetting.enable_category_group_review = true
+ category = Fabricate(:category, read_restricted: true)
+
+ Fabricate(:reviewable_flagged_post, reviewable_by_group: group, category: category)
+
+ expect(guardian.can_see_review_queue?).to eq(false)
+ end
+ end
end
diff --git a/spec/components/html_to_markdown_spec.rb b/spec/components/html_to_markdown_spec.rb
index 266e875aca..3d19c66f9a 100644
--- a/spec/components/html_to_markdown_spec.rb
+++ b/spec/components/html_to_markdown_spec.rb
@@ -10,7 +10,7 @@ describe HtmlToMarkdown do
end
it "remove whitespaces" do
- expect(html_to_markdown(<<-HTML
+ html = <<-HTML
Let me see if it happens by answering your message through + Thunderbird.
+Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 + Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 + Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 + Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 + Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 + Long sentence 1 +
+ + HTML + + markdown = <<~MD + Let me see if it happens by answering your message through Thunderbird. + + Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 Long sentence 1 + MD + + expect(html_to_markdown(html)).to eq(markdown.strip) + + html = <<~HTML + This post
+ has lots
of
+ space
+
This space was left untouched !+ HTML + + markdown = <<~MD + This post has lots + of space + + ``` + This space was left untouched ! + ``` + MD + + expect(html_to_markdown(html)).to eq(markdown.strip) end it "skips hidden tags" do expect(html_to_markdown(%Q{
Hello cruel World!
})).to eq("Hello World!") + expect(html_to_markdown(%Q{Hello cruel World!
})).to eq("Hello World!") end it "converts " do @@ -37,13 +80,15 @@ describe HtmlToMarkdown do expect(html_to_markdown("B*ld")).to eq("__B*ld__") html = <<~HTML + BeforeBold
" do @@ -221,11 +276,11 @@ describe HtmlToMarkdown do it "handles" do expect(html_to_markdown("
1st paragraph
2nd paragraph
")).to eq("1st paragraph\n\n2nd paragraph") - expect(html_to_markdown("1st paragraph
\n2nd paragraph\n 2nd paragraph
\n3rd paragraph
")).to eq("1st paragraph\n\n2nd paragraph\n2nd paragraph\n\n3rd paragraph") + expect(html_to_markdown("1st paragraph
\n2nd paragraph\n 2nd paragraph
\n3rd paragraph
")).to eq("1st paragraph\n\n2nd paragraph 2nd paragraph\n\n3rd paragraph") end it "handles" do - expect(html_to_markdown("1st div2nd div")).to eq("1st div\n\n2nd div") + expect(html_to_markdown("1st div2nd div")).to eq("1st div\n2nd div") end it "swallows " do @@ -257,15 +312,19 @@ describe HtmlToMarkdown do context "with an oddly placed
" do it "handles " do - expect(html_to_markdown("
Bold")).to eq("**Bold**") - expect(html_to_markdown("Bold
")).to eq("**Bold**") - expect(html_to_markdown("Bold
text")).to eq("**Bold\ntext**") + expect(html_to_markdown("Hello
Bold World")).to eq("Hello\n**Bold** World") + expect(html_to_markdown("Hello Bold
World")).to eq("Hello **Bold**\nWorld") + expect(html_to_markdown("Hello Bold
text World")).to eq("Hello **Bold**\n**text** World") end it "handles " do - expect(html_to_markdown("
Italic")).to eq("*Italic*") - expect(html_to_markdown("Italic
")).to eq("*Italic*") - expect(html_to_markdown("Italic
text")).to eq("*Italic\ntext*") + expect(html_to_markdown("Hello
Italic World")).to eq("Hello\n*Italic* World") + expect(html_to_markdown("Hello Italic
World")).to eq("Hello *Italic*\nWorld") + expect(html_to_markdown("Hello Italic
text World")).to eq("Hello *Italic*\n*text* World") + end + + it "works" do + expect(html_to_markdown("A B C")).to eq("A __B *C*__\n__*D* E__\n**F** G") end end @@ -314,4 +373,64 @@ describe HtmlToMarkdown do end + it "supoorts
D E
F G