@@ -0,0 +1,201 @@
name: CI

on:
push:
branches:
- master
pull_request:
branches-ignore:
- "tests-passed"

jobs:
build:
name: "${{ matrix.target }}-${{ matrix.build_types }}"
runs-on: ${{ matrix.os }}
container: discourse/discourse_test:release
timeout-minutes: 60

env:
DISCOURSE_HOSTNAME: www.example.com
RUBY_GLOBAL_METHOD_CACHE_SIZE: 131072
BUILD_TYPE: ${{ matrix.build_types }}
TARGET: ${{ matrix.target }}
RAILS_ENV: test
PGHOST: postgres
PGUSER: discourse
PGPASSWORD: discourse

strategy:
fail-fast: false

matrix:
build_types: ["BACKEND", "FRONTEND", "LINT"]
target: ["PLUGINS", "CORE"]
os: [ubuntu-latest]
ruby: ["2.6"]
postgres: ["12"]
redis: ["4.x"]

services:
postgres:
image: postgres:${{ matrix.postgres }}
ports:
- 5432:5432
env:
POSTGRES_USER: discourse
POSTGRES_PASSWORD: discourse
POSTGRES_DB: discourse_test
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@master
with:
fetch-depth: 1

- name: Setup Git
run: |
git config --global user.email "ci@ci.invalid"
git config --global user.name "Discourse CI"
- name: Setup redis
uses: shogo82148/actions-setup-redis@v1
if: env.BUILD_TYPE != 'LINT'
with:
redis-version: ${{ matrix.redis }}

- name: Bundler cache
uses: actions/cache@v2
with:
path: vendor/bundle
key: ${{ runner.os }}-${{ matrix.ruby }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-${{ matrix.ruby }}-gem-
- name: Setup gems
run: |
bundle config --local path vendor/bundle
bundle config --local deployment true
bundle config --local without development
bundle install --jobs 4
bundle clean
- name: Get yarn cache directory
id: yarn-cache-dir
run: echo "::set-output name=dir::$(yarn cache dir)"

- name: Yarn cache
uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ matrix.os }}-yarn-
- name: Yarn install
run: yarn install

- name: "Checkout official plugins"
if: env.TARGET == 'PLUGINS'
run: bin/rake plugin:install_all_official

- name: Create database
if: env.BUILD_TYPE != 'LINT'
run: |
bin/rake db:create
bin/rake db:migrate
- name: Create parallel databases
if: env.BUILD_TYPE == 'BACKEND' && env.TARGET == 'CORE'
run: |
bin/rake parallel:create
bin/rake parallel:migrate
- name: Rubocop (core and core plugins)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
run: bundle exec rubocop .

- name: Rubocop (all plugins)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'PLUGINS'
run: bundle exec rubocop plugins

- name: ESLint (core)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern app/assets/javascripts

- name: ESLint (core plugins)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern plugins/**/{test,assets}/javascripts

- name: ESLint (all plugins)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'PLUGINS'
run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern plugins/**/{test,assets}/javascripts

- name: Prettier (core and core plugins)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
run: |
yarn prettier -v
yarn prettier --list-different \
"app/assets/stylesheets/**/*.scss" \
"app/assets/javascripts/**/*.{js,es6}" \
"plugins/**/assets/stylesheets/**/*.scss" \
"plugins/**/assets/javascripts/**/*.{js,es6}"
- name: Prettier (all plugins)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'PLUGINS'
run: |
yarn prettier -v
yarn prettier --list-different \
"plugins/**/assets/stylesheets/**/*.scss" \
"plugins/**/assets/javascripts/**/*.{js,es6}"
- name: Ember template lint (core and core plugins)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
run: |
yarn ember-template-lint \
app/assets/javascripts \
plugins/**/assets/javascripts
- name: Ember template lint (all plugins)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'PLUGINS'
run: |
yarn ember-template-lint \
plugins/**/assets/javascripts
- name: Core English locale
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
run: bundle exec ruby script/i18n_lint.rb "config/**/locales/{client,server}.en.yml"

- name: Plugin English locale
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'PLUGINS'
run: bundle exec ruby script/i18n_lint.rb "plugins/**/locales/{client,server}.en.yml"

- name: Core RSpec
if: env.BUILD_TYPE == 'BACKEND' && env.TARGET == 'CORE'
run: |
bin/turbo_rspec
bin/rake plugin:spec
- name: Plugin RSpec
if: env.BUILD_TYPE == 'BACKEND' && env.TARGET == 'PLUGINS'
run: bin/rake plugin:spec

- name: Core QUnit
if: env.BUILD_TYPE == 'FRONTEND' && env.TARGET == 'CORE'
run: bundle exec rake qunit:test['1200000']
timeout-minutes: 30

- name: Wizard QUnit
if: env.BUILD_TYPE == 'FRONTEND' && env.TARGET == 'CORE'
run: bundle exec rake qunit:test['1200000','/wizard/qunit']
timeout-minutes: 30

- name: Plugin QUnit # Tests core plugins in TARGET=CORE, and all plugins in TARGET=PLUGINS
if: env.BUILD_TYPE == 'FRONTEND'
run: bundle exec rake plugin:qunit['*','1200000']
timeout-minutes: 30
@@ -32,6 +32,7 @@ config/discourse.conf
# Ignore the default SQLite database and db dumps
*.sql
*.sql.gz
!/spec/fixtures/**/*.sql
/db/*.sqlite3
/db/structure.sql
/db/schema.rb
@@ -46,12 +47,13 @@ bootsnap-compile-cache/

# Ignore plugins except for the bundled ones.
/plugins/*
!/plugins/lazyYT/
!/plugins/lazy-yt/
!/plugins/poll/
!/plugins/discourse-details/
!/plugins/discourse-nginx-performance-report
!/plugins/discourse-narrative-bot
!/plugins/discourse-presence
!/plugins/styleguide
!/plugins/discourse-local-dates
/plugins/*/auto_generated/

@@ -86,6 +88,7 @@ config/multisite.yml
config/multisite1.yml
config/fog_credentials.yml

/public/fonts
/public/uploads
/public/backups
/public/stylesheet-cache/*
@@ -120,7 +123,30 @@ vendor/bundle/*
*.swn

# ignore nodejs files
/node_modules
node_modules
/package-lock.json

/vendor/data/GeoLite2-City.mmdb

# Vagrant
.vagrant

# ignore auto-generated plugin js assets
/app/assets/javascripts/plugins/*

# ignore generated api documentation files
openapi/*

# ignore VSCode config files
.vscode

# ignore direnv
.envrc

# ember-cli generated
dist

# Copyright Deposits
copyright

yarn-error.log
@@ -0,0 +1,12 @@
sources:
yarn: true
bundler: true
allowed:
- mit
- apache-2.0
- bsd-2-clause
- bsd-3-clause
- cc0-1.0
- isc
- other
- none

This file was deleted.

This file was deleted.

@@ -1,3 +1,26 @@
app/assets/stylesheets/vendor/
plugins/**/assets/stylesheets/vendor/
plugins/**/assets/javascripts/vendor/
package.json
config/locales/**/*.yml
!config/locales/**/*.en*.yml
script/import_scripts/**/*.yml

app/assets/javascripts/discourse-loader.js
app/assets/javascripts/env.js
app/assets/javascripts/main_include_admin.js
app/assets/javascripts/vendor.js
app/assets/javascripts/locales/i18n.js
app/assets/javascripts/ember-addons/
app/assets/javascripts/discourse/lib/autosize.js
lib/javascripts/locale/
lib/javascripts/messageformat.js
lib/highlight_js/
plugins/**/lib/javascripts/locale
public/
vendor/
app/assets/javascripts/discourse/tests/test_helper.js
app/assets/javascripts/discourse/tests/fixtures
node_modules/
dist/
**/*.rb
@@ -0,0 +1 @@
{}
@@ -0,0 +1,4 @@
--format progress
--format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log
--format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log
--format ParallelTests::RSpec::FailuresLogger --out tmp/failing_specs.log
@@ -1,116 +1,9 @@
AllCops:
TargetRubyVersion: 2.4
DisabledByDefault: true
Exclude:
- 'db/schema.rb'
- 'bundle/**/*'
- 'vendor/**/*'
- 'node_modules/**/*'
- 'public/**/*'
inherit_gem:
rubocop-discourse: default.yml

# Prefer &&/|| over and/or.
Style/AndOr:
Enabled: true

# Do not use braces for hash literals when they are the last argument of a
# method call.
Style/BracesAroundHashParameters:
Enabled: true

# Align `when` with `case`.
Layout/CaseIndentation:
Enabled: true

# Align comments with method definitions.
Layout/CommentIndentation:
Enabled: true

# No extra empty lines.
Layout/EmptyLines:
Enabled: true

# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
Style/HashSyntax:
Enabled: true

# Two spaces, no tabs (for indentation).
Layout/IndentationWidth:
Enabled: true

Layout/SpaceAfterColon:
Enabled: true

Layout/SpaceAfterComma:
Enabled: true

Layout/SpaceAroundEqualsInParameterDefault:
Enabled: true

Layout/SpaceAroundKeyword:
Enabled: true

Layout/SpaceAroundOperators:
Enabled: true

Layout/SpaceBeforeFirstArg:
Enabled: true

# Defining a method with parameters needs parentheses.
Style/MethodDefParentheses:
Enabled: true

# Use `foo {}` not `foo{}`.
Layout/SpaceBeforeBlockBraces:
Enabled: true

# Use `foo { bar }` not `foo {bar}`.
Layout/SpaceInsideBlockBraces:
Enabled: true

# Use `{ a: 1 }` not `{a:1}`.
Layout/SpaceInsideHashLiteralBraces:
Enabled: true

Layout/SpaceInsideParens:
Enabled: true

# Detect hard tabs, no hard tabs.
Layout/Tab:
Enabled: true

# Blank lines should not have any spaces.
Layout/TrailingBlankLines:
Enabled: true

# No trailing whitespace.
Layout/TrailingWhitespace:
Enabled: true

Lint/Debugger:
Enabled: true

Layout/BlockAlignment:
Enabled: true

# Align `end` with the matching keyword or starting expression except for
# assignments, where it should be aligned with the LHS.
Layout/EndAlignment:
Enabled: true
EnforcedStyleAlignWith: variable

# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
Lint/RequireParentheses:
Enabled: true

Lint/ShadowingOuterLocalVariable:
Enabled: true

Layout/MultilineMethodCallIndentation:
Enabled: true
EnforcedStyle: indented
# Still work to do in ensuring we don't link old files
Discourse/NoAddReferenceOrAliasesActiveRecordMigration:
Enabled: false

Layout/AlignHash:
Discourse/NoResetColumnInformationInMigrations:
Enabled: true

Bundler/OrderedGems:
Enabled: false
@@ -1 +1 @@
2.4.4
2.6.5
@@ -0,0 +1,55 @@
module.exports = {
extends: "recommended",
ignore: ["**/*.raw"],

// Pending:
// "eol-last": "always",

rules: {
"block-indentation": true,
"deprecated-render-helper": true,
"linebreak-style": true,
"link-rel-noopener": "strict",
"no-abstract-roles": true,
"no-args-paths": true,
"no-attrs-in-components": true,
"no-debugger": true,
"no-duplicate-attributes": true,
"no-extra-mut-helper-argument": true,
"no-html-comments": true,
"no-index-component-invocation": true,
"no-inline-styles": false,
"no-input-block": true,
"no-input-tagname": true,
"no-implicit-this": false,
"no-invalid-interactive": true,
"no-invalid-link-text": true,
"no-invalid-meta": true,
"no-invalid-role": true,
"no-log": true,
"no-negated-condition": true,
"no-nested-interactive": true,
"no-multiple-empty-lines": true,
"no-obsolete-elements": true,
"no-outlet-outside-routes": true,
"no-partial": true,
"no-positive-tabindex": false,
"no-quoteless-attributes": true,
"no-shadowed-elements": true,
"no-trailing-spaces": true,
"no-triple-curlies": true,
"no-unbound": true,
"no-unnecessary-concat": true,
"no-unnecessary-component-helper": true,
"no-unused-block-params": true,
quotes: "double",
"require-button-type": true,
"require-iframe-title": true,
"require-valid-alt-text": false,
"self-closing-void-elements": true,
"simple-unless": true,
"style-concatenation": true,
"table-groups": true,
"link-href-attributes": false,
},
};

This file was deleted.

This file was deleted.

@@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Discourse",
"type": "Ruby",
"request": "launch",
"cwd": "/home/discourse/workspace/discourse",
// run bundle install before rails server
"preLaunchTask": "Prepare discourse",
"env": { "DISCOURSE_DEV_HOSTS": "${env:CLOUDENV_ENVIRONMENT_ID}-9292.apps.codespaces.githubusercontent.com", "UNICORN_BIND_ALL": "1", "UNICORN_WORKERS": "4", "DISCOURSE_DEV_ALLOW_ANON_TO_IMPERSONATE": "1" },
"program": "bin/unicorn",
"args": ["-x"],
}
]
}
@@ -0,0 +1,12 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Prepare discourse",
"type": "shell",
"command": "cd /home/discourse/workspace/discourse && bundle install && yarn && bin/rake db:migrate"
},
],
}
@@ -1,3 +1,5 @@
# frozen_string_literal: true

# Install development dependencies on Mac OS X using Homebrew (http://mxcl.github.com/homebrew)

# you probably already have git installed; ensure that it is the latest version
@@ -29,5 +29,3 @@ Javascript
Ruby

Rails - Copyright (c) 2005-2013 David Heinemeier Hansson, Rails Core Team contributors (MIT)

Thin - Copyright (c) 2012-2013 Marc-Andre Cournoyer

This file was deleted.

151 Gemfile
@@ -1,3 +1,5 @@
# frozen_string_literal: true

source 'https://rubygems.org'
# if there is a super emergency and rubygems is playing up, try
#source 'http://production.cf.rubygems.org'
@@ -11,92 +13,125 @@ end
if rails_master?
gem 'arel', git: 'https://github.com/rails/arel.git'
gem 'rails', git: 'https://github.com/rails/rails.git'
gem 'seed-fu', git: 'https://github.com/SamSaffron/seed-fu.git', branch: 'discourse'
else
gem 'actionmailer', '5.2'
gem 'actionpack', '5.2'
gem 'actionview', '5.2'
gem 'activemodel', '5.2'
gem 'activerecord', '5.2'
gem 'activesupport', '5.2'
gem 'railties', '5.2'
# NOTE: Until rubygems gives us optional dependencies we are stuck with this needing to be explicit
# this allows us to include the bits of rails we use without pieces we do not.
#
# To issue a rails update bump the version number here
gem 'actionmailer', '6.0.3.3'
gem 'actionpack', '6.0.3.3'
gem 'actionview', '6.0.3.3'
gem 'activemodel', '6.0.3.3'
gem 'activerecord', '6.0.3.3'
gem 'activesupport', '6.0.3.3'
gem 'railties', '6.0.3.3'
gem 'sprockets-rails'
gem 'seed-fu'
end

gem 'mail', '2.7.1.rc1', require: false
gem 'json'

# TODO: At the moment Discourse does not work with Sprockets 4, we would need to correct internals
# This is a desired upgrade we should get to.
gem 'sprockets', '3.7.2'

# this will eventually be added to rails,
# allows us to precompile all our templates in the unicorn master
gem 'actionview_precompiler', require: false

gem 'seed-fu'

gem 'mail', require: false
gem 'mini_mime'
gem 'mini_suffix'

gem 'hiredis'
gem 'redis', require: ["redis", "redis/connection/hiredis"]
gem 'redis'

# This is explicitly used by Sidekiq and is an optional dependency.
# We tell Sidekiq to use the namespace "sidekiq" which triggers this
# gem to be used. There is no explicit dependency in sidekiq cause
# redis namespace support is optional
# We already namespace stuff in DiscourseRedis, so we should consider
# just using a single implementation in core vs having 2 namespace implementations
gem 'redis-namespace'

# NOTE: AM serializer gets a lot slower with recent updates
# we used an old branch which is the fastest one out there
# are long term goal here is to fork this gem so we have a
# better maintained living fork
gem 'active_model_serializers', '~> 0.8.3'

gem 'onebox', '1.8.63'
gem 'onebox'

gem 'http_accept_language', require: false

gem 'http_accept_language', '~>2.0.5', require: false
# Ember related gems need to be pinned cause they control client side
# behavior, we will push these versions up when upgrading ember
gem 'discourse-ember-rails', '0.18.6', require: 'ember-rails'
gem 'discourse-ember-source', '~> 3.12.2'
gem 'ember-handlebars-template', '0.8.0'
gem 'discourse-fonts'

gem 'ember-rails', '0.18.5'
gem 'ember-source', '2.13.3'
gem 'ember-handlebars-template', '0.7.5'
gem 'barber'

gem 'message_bus'

gem 'rails_multisite'

gem 'fast_xs', platform: :mri
gem 'fast_xs', platform: :ruby

# may move to xorcist post: https://github.com/fny/xorcist/issues/4
gem 'fast_xor', platform: :mri
gem 'xorcist'

gem 'fastimage'

gem 'aws-sdk-s3', require: false
gem 'aws-sdk-sns', require: false
gem 'excon', require: false
gem 'unf', require: false

gem 'email_reply_trimmer', '~> 0.1'
gem 'email_reply_trimmer'

# Forked until https://github.com/toy/image_optim/pull/162 is merged
# https://github.com/discourse/image_optim
gem 'discourse_image_optim', require: 'image_optim'
gem 'multi_json'
gem 'mustache'
gem 'nokogiri'
gem 'css_parser', require: false

gem 'omniauth'
gem 'omniauth-openid'
gem 'openid-redis-store'
gem 'omniauth-facebook'
gem 'omniauth-twitter'
gem 'omniauth-instagram'
gem 'omniauth-github'

gem 'omniauth-oauth2', require: false

gem 'omniauth-google-oauth2'

gem 'oj'
gem 'pg'
gem 'mini_sql'
gem 'pry-rails', require: false
gem 'r2', '~> 0.2.5', require: false
gem 'pry-byebug', require: false
gem 'r2', require: false
gem 'rake'

gem 'thor', require: false
gem 'diffy', require: false
gem 'rinku'
gem 'sanitize'
gem 'sidekiq'
gem 'mini_scheduler'

# for sidekiq web
gem 'tilt', require: false

gem 'execjs', require: false
gem 'mini_racer'
gem 'highline', '~> 1.7.0', require: false

gem 'highline', require: false

gem 'rack'

gem 'rack-protection' # security
gem 'cbor', require: false
gem 'cose', require: false
gem 'addressable'

# Gems used only for assets and not required in production environments by default.
# Allow everywhere for now cause we are allowing asset debugging in production
@@ -107,42 +142,47 @@ end

group :test do
gem 'webmock', require: false
gem 'fakeweb', '~> 1.3.0', require: false
gem 'fakeweb', require: false
gem 'minitest', require: false
gem 'danger'
gem 'simplecov', require: false
gem "test-prof"
end

group :test, :development do
gem 'rspec'
gem 'mock_redis'
gem 'listen', require: false
gem 'certified', require: false
# later appears to break Fabricate(:topic, category: category)
gem 'fabrication', require: false
gem 'mocha', require: false

gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : false
gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false
gem 'rspec-rails', require: false
gem 'shoulda', require: false

gem 'rspec-rails'

gem 'shoulda-matchers', require: false
gem 'rspec-html-matchers'
gem 'pry-nav'
gem 'byebug', require: ENV['RM_INFO'].nil?
gem 'rubocop', require: false
gem 'byebug', require: ENV['RM_INFO'].nil?, platform: :mri
gem "rubocop-discourse", require: false
gem 'parallel_tests'

gem 'rswag-specs'
gem 'json_schemer'
end

group :development do
gem 'ruby-prof', require: false
gem 'ruby-prof', require: false, platform: :mri
gem 'bullet', require: !!ENV['BULLET']
gem 'better_errors'
gem 'better_errors', platform: :mri, require: !!ENV['BETTER_ERRORS']
gem 'binding_of_caller'
gem 'yaml-lint'
gem 'annotate'
gem 'foreman', require: false
end

# this is an optional gem, it provides a high performance replacement
# to String#blank? a method that is called quite frequently in current
# ActiveRecord, this may change in the future
gem 'fast_blank', platform: :mri
gem 'fast_blank', platform: :ruby

# this provides a very efficient lru cache
gem 'lru_redux'
@@ -153,10 +193,9 @@ gem 'htmlentities', require: false
# If you want to amend mini profiler to do the monkey patches in the railties
# we are open to it. by deferring require to the initializer we can configure discourse installs without it

gem 'flamegraph', require: false
gem 'rack-mini-profiler', require: false
gem 'rack-mini-profiler', require: ['enable_rails_patches']

gem 'unicorn', require: false, platform: :mri
gem 'unicorn', require: false, platform: :ruby
gem 'puma', require: false
gem 'rbtrace', require: false, platform: :mri
gem 'gc_tracer', require: false, platform: :mri
@@ -174,24 +213,36 @@ gem 'logstash-event', require: false
gem 'logstash-logger', require: false
gem 'logster'

gem 'sassc', require: false
# NOTE: later versions of sassc are causing a segfault, possibly dependent on processer architecture
# and until resolved should be locked at 2.0.1
gem 'sassc', '2.0.1', require: false
gem "sassc-rails"

gem 'rotp', require: false

gem 'rotp'
gem 'rqrcode'

gem 'rubyzip', require: false

gem 'sshkey', require: false

gem 'rchardet', require: false
gem 'lz4-ruby', require: false, platform: :ruby

if ENV["IMPORT"] == "1"
gem 'mysql2'
gem 'redcarpet'
gem 'sqlite3', '~> 1.3.13'
gem 'ruby-bbcode-to-md', github: 'nlalonde/ruby-bbcode-to-md'

# NOTE: in import mode the version of sqlite can matter a lot, so we stick it to a specific one
gem 'sqlite3', '~> 1.3', '>= 1.3.13'
gem 'ruby-bbcode-to-md', git: 'https://github.com/nlalonde/ruby-bbcode-to-md'
gem 'reverse_markdown'
gem 'tiny_tds'
gem 'csv'
end

gem 'webpush', require: false
gem 'colored2', require: false
gem 'maxminddb'

gem 'rails_failover', require: false

Large diffs are not rendered by default.

This file was deleted.

@@ -1,22 +1,27 @@
<a href="http://www.discourse.org/">![Logo](images/discourse.png)</a>
<a href="https://www.discourse.org/"><img src=
"https://user-images.githubusercontent.com/1681963/52239617-e2683480-289c-11e9-922b-5da55472e5b4.png"
width="300px"></a>



Discourse is the 100% open source discussion platform built for the next decade of the Internet. Use it as a:

- mailing list
- discussion forum
- long-form chat room

To learn more about the philosophy and goals of the project, [visit **discourse.org**](http://www.discourse.org).
To learn more about the philosophy and goals of the project, [visit **discourse.org**](https://www.discourse.org).

## Screenshots


<a href="https://bbs.boingboing.net"><img alt="Boing Boing" src="https://cloud.githubusercontent.com/assets/1385470/25397876/3fe6cdac-29c0-11e7-8a41-9d0c0279f5a3.png" width="720px"></a>
<a href="https://twittercommunity.com/"><img src="https://cloud.githubusercontent.com/assets/1385470/25397920/71b24e4c-29c0-11e7-8bcf-7a47b888412e.png" width="720px"></a>
<a href="http://discuss.howtogeek.com"><img src="https://cloud.githubusercontent.com/assets/1385470/25398049/f0995962-29c0-11e7-99d7-a3b9c4f0b357.png" width="720px"></a>
<a href="https://talk.turtlerockstudios.com/"><img src="https://cloud.githubusercontent.com/assets/1385470/25398115/2d560d96-29c1-11e7-9a96-b0134a4fedff.png" width="720px"></a>
<a href="https://bbs.boingboing.net"><img alt="Boing Boing" src="https://user-images.githubusercontent.com/1681963/52239245-04ad8280-289c-11e9-9c88-8c173d4a0422.png" width="720px"></a>
<a href="https://twittercommunity.com/"><img src="https://user-images.githubusercontent.com/1681963/52239250-04ad8280-289c-11e9-9e42-574f6eaab9d7.png" width="720px"></a>
<a href="https://discuss.atom.io/"><img src="https://user-images.githubusercontent.com/1681963/89088039-6735f080-d364-11ea-93a6-5629ea8738fe.png" width="720px"></a>
<a href="https://forums.gearboxsoftware.com/"><img src="https://user-images.githubusercontent.com/1681963/89088042-68ffb400-d364-11ea-93be-161ea04d8b29.png" width="720px"></a>


<img src="https://www.discourse.org/a/img/about/mobile-devices-2x.jpg" alt="Mobile" width="414">
<img src="https://user-images.githubusercontent.com/1681963/52239118-b304f800-289b-11e9-9904-16450680d9ec.jpg" alt="Mobile" width="414">

Browse [lots more notable Discourse instances](https://www.discourse.org/customers).

@@ -30,7 +35,7 @@ To get your environment setup, follow the community setup guide for your operati

If you're familiar with how Rails works and are comfortable setting up your own environment, you can also try out the [**Discourse Advanced Developer Guide**](docs/DEVELOPER-ADVANCED.md), which is aimed primarily at Ubuntu and macOS environments.

Before you get started, ensure you have the following minimum versions: [Ruby 2.4+](http://www.ruby-lang.org/en/downloads/), [PostgreSQL 10+](http://www.postgresql.org/download/), [Redis 2.6+](http://redis.io/download). If you're having trouble, please see our [**TROUBLESHOOTING GUIDE**](docs/TROUBLESHOOTING.md) first!
Before you get started, ensure you have the following minimum versions: [Ruby 2.6+](https://www.ruby-lang.org/en/downloads/), [PostgreSQL 10+](https://www.postgresql.org/download/), [Redis 4.0+](https://redis.io/download). If you're having trouble, please see our [**TROUBLESHOOTING GUIDE**](docs/TROUBLESHOOTING.md) first!

## Setting up Discourse

@@ -40,38 +45,41 @@ If you're looking for business class hosting, see [discourse.org/buy](https://ww

## Requirements

Discourse is built for the *next* 10 years of the Internet, so our requirements are high:
Discourse is built for the *next* 10 years of the Internet, so our requirements are high.

Discourse supports the **latest, stable releases** of all major browsers and platforms:

| Browsers | Tablets | Phones |
| --------------------- | ------------ | ------------ |
| Safari 6.1+ | iPad 3+ | iOS 8+ |
| Google Chrome 32+ | Android 4.3+ | Android 4.3+ |
| Internet Explorer 11+ | | |
| Firefox 27+ | | |
| Apple Safari | iPadOS | iOS |
| Google Chrome | Android | Android |
| Microsoft Edge | | |
| Mozilla Firefox | | |

## Built With

- [Ruby on Rails](https://github.com/rails/rails) &mdash; Our back end API is a Rails app. It responds to requests RESTfully in JSON.
- [Ember.js](https://github.com/emberjs/ember.js) &mdash; Our front end is an Ember.js app that communicates with the Rails API.
- [PostgreSQL](http://www.postgresql.org/) &mdash; Our main data store is in Postgres.
- [Redis](http://redis.io/) &mdash; We use Redis as a cache and for transient data.
- [PostgreSQL](https://www.postgresql.org/) &mdash; Our main data store is in Postgres.
- [Redis](https://redis.io/) &mdash; We use Redis as a cache and for transient data.
- [BrowserStack](https://www.browserstack.com/) &mdash; We use BrowserStack to test on real devices and browsers.

Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https://github.com/discourse/discourse/blob/master/Gemfile).

## Contributing

[![Build Status](https://api.travis-ci.org/discourse/discourse.svg?branch=master)](https://travis-ci.org/discourse/discourse)
[![Build Status](https://github.com/discourse/discourse/workflows/CI/badge.svg)](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 &ndash; including you!

Before contributing to Discourse:

1. Please read the complete mission statements on [**discourse.org**](http://www.discourse.org). Yes we actually believe this stuff; you should too.
2. Read and sign the [**Electronic Discourse Forums Contribution License Agreement**](http://discourse.org/cla).
1. Please read the complete mission statements on [**discourse.org**](https://www.discourse.org). Yes we actually believe this stuff; you should too.
2. Read and sign the [**Electronic Discourse Forums Contribution License Agreement**](https://www.discourse.org/cla).
3. Dig into [**CONTRIBUTING.MD**](CONTRIBUTING.md), which covers submitting bugs, requesting new features, preparing your code for a pull request, etc.
4. Always strive to collaborate [with mutual respect](https://github.com/discourse/discourse/blob/master/docs/code-of-conduct.md).
5. Not sure what to work on? [**We've got some ideas.**](http://meta.discourse.org/t/so-you-want-to-help-out-with-discourse/3823)
5. Not sure what to work on? [**We've got some ideas.**](https://meta.discourse.org/t/so-you-want-to-help-out-with-discourse/3823)


We look forward to seeing your pull requests!
@@ -82,18 +90,17 @@ We take security very seriously at Discourse; all our code is 100% open source a

## The Discourse Team

The original Discourse code contributors can be found in [**AUTHORS.MD**](docs/AUTHORS.md). For a complete list of the many individuals that contributed to the design and implementation of Discourse, please refer to [the official Discourse blog](http://blog.discourse.org/2013/02/the-discourse-team/) and [GitHub's list of contributors](https://github.com/discourse/discourse/contributors).

The original Discourse code contributors can be found in [**AUTHORS.MD**](docs/AUTHORS.md). For a complete list of the many individuals that contributed to the design and implementation of Discourse, please refer to [the official Discourse blog](https://blog.discourse.org/2013/02/the-discourse-team/) and [GitHub's list of contributors](https://github.com/discourse/discourse/contributors).

## Copyright / License

Copyright 2014 - 2018 Civilized Discourse Construction Kit, Inc.
Copyright 2014 - 2021 Civilized Discourse Construction Kit, Inc.

Licensed under the GNU General Public License Version 2.0 (or later);
you may not use this work except in compliance with the License.
You may obtain a copy of the License in the LICENSE file, or at:

http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
@@ -105,4 +112,4 @@ Discourse logo and “Discourse Forum” ®, Civilized Discourse Construction Ki

## Dedication

Discourse is built with [love, Internet style.](http://www.youtube.com/watch?v=Xe1TZaElTAs)
Discourse is built with [love, Internet style.](https://www.youtube.com/watch?v=Xe1TZaElTAs)
@@ -1,4 +1,6 @@
#!/usr/bin/env rake
# frozen_string_literal: true

# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

Binary file not shown.
Binary file not shown.

This file was deleted.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN -7 Bytes (100%) app/assets/images/logo-dev.png
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
BIN -88 Bytes (86%) app/assets/images/select2.png
Diff not rendered.
BIN -86 Bytes (90%) app/assets/images/select2x2.png
Diff not rendered.
@@ -0,0 +1,20 @@
// discourse-skip-module
(function () {
setTimeout(function () {
const $activateButton = $("#activate-account-button");
$activateButton.on("click", function () {
$activateButton.prop("disabled", true);
const hpPath = document.getElementById("data-activate-account").dataset
.path;
$.ajax(hpPath)
.then(function (hp) {
$("#password_confirmation").val(hp.value);
$("#challenge").val(hp.challenge.split("").reverse().join(""));
$("#activate-account-form").submit();
})
.fail(function () {
$activateButton.prop("disabled", false);
});
});
}, 50);
})();
@@ -3,10 +3,10 @@ require_asset("main_include_admin.js")

DiscoursePluginRegistry.admin_javascripts.each { |js| require_asset(js) }

DiscoursePluginRegistry.each_globbed_asset(admin: true) do |f, ext|
DiscoursePluginRegistry.each_globbed_asset(admin: true) do |f|
if File.directory?(f)
depend_on(f)
elsif f.to_s.end_with?(".#{ext}")
else
require_asset(f)
end
end

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

@@ -0,0 +1,13 @@
import RESTAdapter from "discourse/adapters/rest";

export default RESTAdapter.extend({
jsonMode: true,

basePath() {
return "/admin/api/";
},

apiNameFor() {
return "key";
},
});
@@ -0,0 +1,11 @@
import RestAdapter from "discourse/adapters/rest";

export default function buildPluginAdapter(pluginName) {
return RestAdapter.extend({
pathFor(store, type, findArgs) {
return (
"/admin/plugins/" + pluginName + this._super(store, type, findArgs)
);
},
});
}
@@ -0,0 +1,7 @@
import RestAdapter from "discourse/adapters/rest";

export default RestAdapter.extend({
basePath() {
return "/admin/customize/";
},
});
@@ -0,0 +1,7 @@
import RestAdapter from "discourse/adapters/rest";

export default RestAdapter.extend({
pathFor() {
return "/admin/customize/email_style";
},
});
@@ -0,0 +1,7 @@
import RestAdapter from "discourse/adapters/rest";

export default RestAdapter.extend({
pathFor() {
return "/admin/customize/embedding";
},
});
File renamed without changes.
@@ -0,0 +1,7 @@
import RestAdapter from "discourse/adapters/rest";

export default RestAdapter.extend({
basePath() {
return "/admin/logs/";
},
});
@@ -0,0 +1,5 @@
import RestAdapter from "discourse/adapters/rest";

export default RestAdapter.extend({
jsonMode: true,
});
@@ -0,0 +1,26 @@
import RestAdapter from "discourse/adapters/rest";

export default RestAdapter.extend({
basePath() {
return "/admin/";
},

afterFindAll(results) {
let map = {};
results.forEach((theme) => {
map[theme.id] = theme;
});
results.forEach((theme) => {
let mapped = theme.get("child_themes") || [];
mapped = mapped.map((t) => map[t.id]);
theme.set("childThemes", mapped);

let mappedParents = theme.get("parent_themes") || [];
mappedParents = mappedParents.map((t) => map[t.id]);
theme.set("parentThemes", mappedParents);
});
return results;
},

jsonMode: true,
});
File renamed without changes.
@@ -0,0 +1,7 @@
import RESTAdapter from "discourse/adapters/rest";

export default RESTAdapter.extend({
basePath() {
return "/admin/api/";
},
});
@@ -0,0 +1,7 @@
import RESTAdapter from "discourse/adapters/rest";

export default RESTAdapter.extend({
basePath() {
return "/admin/api/";
},
});
@@ -0,0 +1,167 @@
import Component from "@ember/component";
import getURL from "discourse-common/lib/get-url";
import loadScript from "discourse/lib/load-script";
import { observes } from "discourse-common/utils/decorators";
import { on } from "@ember/object/evented";

export default Component.extend({
mode: "css",
classNames: ["ace-wrapper"],
_editor: null,
_skipContentChangeEvent: null,
disabled: false,
htmlPlaceholder: false,

@observes("editorId")
editorIdChanged() {
if (this.autofocus) {
this.send("focus");
}
},

@observes("content")
contentChanged() {
const content = this.content || "";
if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setValue(content);
}
},

@observes("mode")
modeChanged() {
if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setMode("ace/mode/" + this.mode);
}
},

@observes("placeholder")
placeholderChanged() {
if (this._editor) {
this._editor.setOptions({
placeholder: this.placeholder,
});
}
},

@observes("disabled")
disabledStateChanged() {
this.changeDisabledState();
},

changeDisabledState() {
const editor = this._editor;
if (editor) {
const disabled = this.disabled;
editor.setOptions({
readOnly: disabled,
highlightActiveLine: !disabled,
highlightGutterLine: !disabled,
});
editor.container.parentNode.setAttribute("data-disabled", disabled);
}
},

_destroyEditor: on("willDestroyElement", function () {
if (this._editor) {
this._editor.destroy();
this._editor = null;
}
if (this.appEvents) {
// xxx: don't run during qunit tests
this.appEvents.off("ace:resize", this, "resize");
}

$(window).off("ace:resize");
}),

resize() {
if (this._editor) {
this._editor.resize();
}
},

didInsertElement() {
this._super(...arguments);
loadScript("/javascripts/ace/ace.js").then(() => {
window.ace.require(["ace/ace"], (loadedAce) => {
loadedAce.config.set("loadWorkerFromBlob", false);
loadedAce.config.set("workerPath", getURL("/javascripts/ace")); // Do not use CDN for workers

if (this.htmlPlaceholder) {
this._overridePlaceholder(loadedAce);
}

if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
const editor = loadedAce.edit(this.element.querySelector(".ace"));

editor.setTheme("ace/theme/chrome");
editor.setShowPrintMargin(false);
editor.setOptions({ fontSize: "14px", placeholder: this.placeholder });
editor.getSession().setMode("ace/mode/" + this.mode);
editor.on("change", () => {
this._skipContentChangeEvent = true;
this.set("content", editor.getSession().getValue());
this._skipContentChangeEvent = false;
});
editor.$blockScrolling = Infinity;
editor.renderer.setScrollMargin(10, 10);

this.element.setAttribute("data-editor", editor);
this._editor = editor;
this.changeDisabledState();

$(window)
.off("ace:resize")
.on("ace:resize", () => this.appEvents.trigger("ace:resize"));

if (this.appEvents) {
// xxx: don't run during qunit tests
this.appEvents.on("ace:resize", this, "resize");
}

if (this.autofocus) {
this.send("focus");
}
});
});
},

actions: {
focus() {
if (this._editor) {
this._editor.focus();
this._editor.navigateFileEnd();
}
},
},

_overridePlaceholder(loadedAce) {
const originalPlaceholderSetter =
loadedAce.config.$defaultOptions.editor.placeholder.set;

loadedAce.config.$defaultOptions.editor.placeholder.set = function () {
if (!this.$updatePlaceholder) {
const originalRendererOn = this.renderer.on;
this.renderer.on = function () {};
originalPlaceholderSetter.call(this, ...arguments);
this.renderer.on = originalRendererOn;

const originalUpdatePlaceholder = this.$updatePlaceholder;

this.$updatePlaceholder = function () {
originalUpdatePlaceholder.call(this, ...arguments);

if (this.renderer.placeholderNode) {
this.renderer.placeholderNode.innerHTML = this.$placeholder || "";
}
}.bind(this);

this.on("input", this.$updatePlaceholder);
}

this.$updatePlaceholder();
};
},
});
@@ -0,0 +1,80 @@
import { observes, on } from "discourse-common/utils/decorators";
import Component from "@ember/component";
import I18n from "I18n";
import discourseDebounce from "discourse-common/lib/debounce";
import { scheduleOnce } from "@ember/runloop";

export default Component.extend({
classNames: ["admin-backups-logs"],
showLoadingSpinner: false,
hasFormattedLogs: false,
noLogsMessage: I18n.t("admin.backups.logs.none"),

init() {
this._super(...arguments);
this._reset();
},

_reset() {
this.setProperties({ formattedLogs: "", index: 0 });
},

_scrollDown() {
const div = this.element;
div.scrollTop = div.scrollHeight;
},

@on("init")
@observes("logs.[]")
_resetFormattedLogs() {
if (this.logs.length === 0) {
this._reset(); // reset the cached logs whenever the model is reset
this.renderLogs();
}
},

_updateFormattedLogsFunc: function () {
const logs = this.logs;
if (logs.length === 0) {
return;
}

// do the log formatting only once for HELLish performance
let formattedLogs = this.formattedLogs;
for (let i = this.index, length = logs.length; i < length; i++) {
const date = logs[i].get("timestamp"),
message = logs[i].get("message");
formattedLogs += "[" + date + "] " + message + "\n";
}
// update the formatted logs & cache index
this.setProperties({
formattedLogs: formattedLogs,
index: logs.length,
});
// force rerender
this.renderLogs();

scheduleOnce("afterRender", this, this._scrollDown);
},

@on("init")
@observes("logs.[]")
_updateFormattedLogs() {
discourseDebounce(this, this._updateFormattedLogsFunc, 150);
},

renderLogs() {
const formattedLogs = this.formattedLogs;
if (formattedLogs && formattedLogs.length > 0) {
this.set("hasFormattedLogs", true);
} else {
this.set("hasFormattedLogs", false);
}
// add a loading indicator
if (this.get("status.isOperationRunning")) {
this.set("showLoadingSpinner", true);
} else {
this.set("showLoadingSpinner", false);
}
},
});
@@ -0,0 +1,24 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "",

buffer: "",
editing: false,

init() {
this._super(...arguments);
this.set("editing", false);
},

actions: {
edit() {
this.set("buffer", this.value);
this.toggleProperty("editing");
},

save() {
// Action has to toggle 'editing' property.
this.action(this.buffer);
},
},
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
classNames: ["row"],
});
@@ -0,0 +1,57 @@
import Component from "@ember/component";
import loadScript from "discourse/lib/load-script";

export default Component.extend({
tagName: "canvas",
type: "line",

refreshChart() {
const ctx = this.element.getContext("2d");
const model = this.model;
const rawData = this.get("model.data");

let data = {
labels: rawData.map((r) => r.x),
datasets: [
{
data: rawData.map((r) => r.y),
label: model.get("title"),
backgroundColor: `rgba(200,220,240,${this.type === "bar" ? 1 : 0.3})`,
borderColor: "#08C",
},
],
};

const config = {
type: this.type,
data: data,
options: {
responsive: true,
tooltips: {
callbacks: {
title: (context) =>
moment(context[0].xLabel, "YYYY-MM-DD").format("LL"),
},
},
scales: {
yAxes: [
{
display: true,
ticks: {
stepSize: 1,
},
},
],
},
},
};

this._chart = new window.Chart(ctx, config);
},

didInsertElement() {
loadScript("/javascripts/Chart.min.js").then(() =>
this.refreshChart.apply(this)
);
},
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "",
});
@@ -0,0 +1,240 @@
import Component from "@ember/component";
import discourseDebounce from "discourse-common/lib/debounce";
import loadScript from "discourse/lib/load-script";
import { makeArray } from "discourse-common/lib/helpers";
import { number } from "discourse/lib/formatter";
import { schedule } from "@ember/runloop";

export default Component.extend({
classNames: ["admin-report-chart"],
limit: 8,
total: 0,
options: null,

init() {
this._super(...arguments);

this.resizeHandler = () =>
discourseDebounce(this, this._scheduleChartRendering, 500);
},

didInsertElement() {
this._super(...arguments);

$(window).on("resize.chart", this.resizeHandler);
},

willDestroyElement() {
this._super(...arguments);

$(window).off("resize.chart", this.resizeHandler);

this._resetChart();
},

didReceiveAttrs() {
this._super(...arguments);

discourseDebounce(this, this._scheduleChartRendering, 100);
},

_scheduleChartRendering() {
schedule("afterRender", () => {
this._renderChart(
this.model,
this.element && this.element.querySelector(".chart-canvas")
);
});
},

_renderChart(model, chartCanvas) {
if (!chartCanvas) {
return;
}

const context = chartCanvas.getContext("2d");
const chartData = this._applyChartGrouping(
model,
makeArray(model.get("chartData") || model.get("data"), "weekly"),
this.options
);
const prevChartData = makeArray(
model.get("prevChartData") || model.get("prev_data")
);

const labels = chartData.map((d) => d.x);

const data = {
labels,
datasets: [
{
data: chartData.map((d) => Math.round(parseFloat(d.y))),
backgroundColor: prevChartData.length
? "transparent"
: model.secondary_color,
borderColor: model.primary_color,
pointRadius: 3,
borderWidth: 1,
pointBackgroundColor: model.primary_color,
pointBorderColor: model.primary_color,
},
],
};

if (prevChartData.length) {
data.datasets.push({
data: prevChartData.map((d) => Math.round(parseFloat(d.y))),
borderColor: model.primary_color,
borderDash: [5, 5],
backgroundColor: "transparent",
borderWidth: 1,
pointRadius: 0,
});
}

loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();

if (!this.element) {
return;
}

this._chart = new window.Chart(
context,
this._buildChartConfig(data, this.options)
);
});
},

_buildChartConfig(data, options) {
return {
type: "line",
data,
options: {
tooltips: {
callbacks: {
title: (tooltipItem) =>
moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL"),
},
},
legend: {
display: false,
},
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
animation: {
duration: 0,
},
layout: {
padding: {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
},
scales: {
yAxes: [
{
display: true,
ticks: {
userCallback: (label) => {
if (Math.floor(label) === label) {
return label;
}
},
callback: (label) => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25,
},
},
],
xAxes: [
{
display: true,
gridLines: { display: false },
type: "time",
time: {
unit: this._unitForGrouping(options),
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50,
},
},
],
},
},
};
},

_resetChart() {
if (this._chart) {
this._chart.destroy();
this._chart = null;
}
},

_applyChartGrouping(model, data, options) {
if (!options.chartGrouping || options.chartGrouping === "daily") {
return data;
}

if (
options.chartGrouping === "weekly" ||
options.chartGrouping === "monthly"
) {
const isoKind = options.chartGrouping === "weekly" ? "isoWeek" : "month";
const kind = options.chartGrouping === "weekly" ? "week" : "month";
const startMoment = moment(model.start_date, "YYYY-MM-DD");

let currentIndex = 0;
let currentStart = startMoment.clone().startOf(isoKind);
let currentEnd = startMoment.clone().endOf(isoKind);
const transformedData = [
{
x: currentStart.format("YYYY-MM-DD"),
y: 0,
},
];

data.forEach((d) => {
let date = moment(d.x, "YYYY-MM-DD");

if (!date.isBetween(currentStart, currentEnd)) {
currentIndex += 1;
currentStart = currentStart.add(1, kind).startOf(isoKind);
currentEnd = currentEnd.add(1, kind).endOf(isoKind);
}

if (transformedData[currentIndex]) {
transformedData[currentIndex].y += d.y;
} else {
transformedData[currentIndex] = {
x: d.x,
y: d.y,
};
}
});

return transformedData;
}

// ensure we return something if grouping is unknown
return data;
},

_unitForGrouping(options) {
switch (options.chartGrouping) {
case "monthly":
return "month";
case "weekly":
return "week";
default:
return "day";
}
},
});
@@ -0,0 +1,6 @@
import Component from "@ember/component";
export default Component.extend({
classNames: ["admin-report-counters"],

attributeBindings: ["model.description:title"],
});
@@ -0,0 +1,11 @@
import Component from "@ember/component";
import { match } from "@ember/object/computed";
export default Component.extend({
allTime: true,
tagName: "tr",
reverseColors: match(
"report.type",
/^(time_to_first_response|topics_with_no_response)$/
),
classNameBindings: ["reverseColors"],
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
classNames: ["admin-report-inline-table"],
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "tr",
});
@@ -0,0 +1,160 @@
import Component from "@ember/component";
import discourseDebounce from "discourse-common/lib/debounce";
import loadScript from "discourse/lib/load-script";
import { makeArray } from "discourse-common/lib/helpers";
import { number } from "discourse/lib/formatter";
import { schedule } from "@ember/runloop";

export default Component.extend({
classNames: ["admin-report-chart", "admin-report-stacked-chart"],

init() {
this._super(...arguments);

this.resizeHandler = () =>
discourseDebounce(this, this._scheduleChartRendering, 500);
},

didInsertElement() {
this._super(...arguments);

$(window).on("resize.chart", this.resizeHandler);
},

willDestroyElement() {
this._super(...arguments);

$(window).off("resize.chart", this.resizeHandler);

this._resetChart();
},

didReceiveAttrs() {
this._super(...arguments);

discourseDebounce(this, this._scheduleChartRendering, 100);
},

_scheduleChartRendering() {
schedule("afterRender", () => {
if (!this.element) {
return;
}

this._renderChart(
this.model,
this.element.querySelector(".chart-canvas")
);
});
},

_renderChart(model, chartCanvas) {
if (!chartCanvas) {
return;
}

const context = chartCanvas.getContext("2d");

const chartData = makeArray(model.get("chartData") || model.get("data"));

const data = {
labels: chartData[0].data.mapBy("x"),
datasets: chartData.map((cd) => {
return {
label: cd.label,
stack: "pageviews-stack",
data: cd.data.map((d) => Math.round(parseFloat(d.y))),
backgroundColor: cd.color,
};
}),
};

loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();

this._chart = new window.Chart(context, this._buildChartConfig(data));
});
},

_buildChartConfig(data) {
return {
type: "bar",
data,
options: {
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
hover: { mode: "index" },
animation: {
duration: 0,
},
tooltips: {
mode: "index",
intersect: false,
callbacks: {
beforeFooter: (tooltipItem) => {
let total = 0;
tooltipItem.forEach(
(item) => (total += parseInt(item.yLabel || 0, 10))
);
return `= ${total}`;
},
title: (tooltipItem) =>
moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL"),
},
},
layout: {
padding: {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
},
scales: {
yAxes: [
{
stacked: true,
display: true,
ticks: {
userCallback: (label) => {
if (Math.floor(label) === label) {
return label;
}
},
callback: (label) => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25,
},
},
],
xAxes: [
{
display: true,
gridLines: { display: false },
type: "time",
offset: true,
time: {
parser: "YYYY-MM-DD",
minUnit: "day",
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50,
},
},
],
},
},
};
},

_resetChart() {
if (this._chart) {
this._chart.destroy();
this._chart = null;
}
},
});
@@ -0,0 +1,43 @@
import Component from "@ember/component";
import I18n from "I18n";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import { setting } from "discourse/lib/computed";

export default Component.extend({
classNames: ["admin-report-storage-stats"],

backupLocation: setting("backup_location"),
backupStats: alias("model.data.backups"),
uploadStats: alias("model.data.uploads"),

@discourseComputed("backupStats")
showBackupStats(stats) {
return stats && this.currentUser.admin;
},

@discourseComputed("backupLocation")
backupLocationName(backupLocation) {
return I18n.t(`admin.backups.location.${backupLocation}`);
},

@discourseComputed("backupStats.used_bytes")
usedBackupSpace(bytes) {
return I18n.toHumanSize(bytes);
},

@discourseComputed("backupStats.free_bytes")
freeBackupSpace(bytes) {
return I18n.toHumanSize(bytes);
},

@discourseComputed("uploadStats.used_bytes")
usedUploadSpace(bytes) {
return I18n.toHumanSize(bytes);
},

@discourseComputed("uploadStats.free_bytes")
freeUploadSpace(bytes) {
return I18n.toHumanSize(bytes);
},
});
@@ -0,0 +1,20 @@
import Component from "@ember/component";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";

export default Component.extend({
tagName: "td",
classNames: ["admin-report-table-cell"],
classNameBindings: ["type", "property"],
options: null,

@discourseComputed("label", "data", "options")
computedLabel(label, data, options) {
return label.compute(data, options || {});
},

type: alias("label.type"),
property: alias("label.mainProperty"),
formatedValue: alias("computedLabel.formatedValue"),
value: alias("computedLabel.value"),
});
@@ -0,0 +1,19 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";

export default Component.extend({
tagName: "th",
classNames: ["admin-report-table-header"],
classNameBindings: ["label.mainProperty", "label.type", "isCurrentSort"],
attributeBindings: ["label.title:title"],

@discourseComputed("currentSortLabel.sortProperty", "label.sortProperty")
isCurrentSort(currentSortField, labelSortField) {
return currentSortField === labelSortField;
},

@discourseComputed("currentSortDirection")
sortIcon(currentSortDirection) {
return currentSortDirection === 1 ? "caret-up" : "caret-down";
},
});
@@ -0,0 +1,6 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "tr",
classNames: ["admin-report-table-row"],
options: null,
});
@@ -0,0 +1,174 @@
import Component from "@ember/component";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import { makeArray } from "discourse-common/lib/helpers";

const PAGES_LIMIT = 8;

export default Component.extend({
classNameBindings: ["sortable", "twoColumns"],
classNames: ["admin-report-table"],
sortable: false,
sortDirection: 1,
perPage: alias("options.perPage"),
page: 0,

@discourseComputed("model.computedLabels.length")
twoColumns(labelsLength) {
return labelsLength === 2;
},

@discourseComputed(
"totalsForSample",
"options.total",
"model.dates_filtering"
)
showTotalForSample(totalsForSample, total, datesFiltering) {
// check if we have at least one cell which contains a value
const sum = totalsForSample
.map((t) => t.value)
.compact()
.reduce((s, v) => s + v, 0);

return sum >= 1 && total && datesFiltering;
},

@discourseComputed("model.total", "options.total", "twoColumns")
showTotal(reportTotal, total, twoColumns) {
return reportTotal && total && twoColumns;
},

@discourseComputed(
"model.{average,data}",
"totalsForSample.1.value",
"twoColumns"
)
showAverage(model, sampleTotalValue, hasTwoColumns) {
return (
model.average &&
model.data.length > 0 &&
sampleTotalValue &&
hasTwoColumns
);
},

@discourseComputed("totalsForSample.1.value", "model.data.length")
averageForSample(totals, count) {
return (totals / count).toFixed(0);
},

@discourseComputed("model.data.length")
showSortingUI(dataLength) {
return dataLength >= 5;
},

@discourseComputed("totalsForSampleRow", "model.computedLabels")
totalsForSample(row, labels) {
return labels.map((label) => {
const computedLabel = label.compute(row);
computedLabel.type = label.type;
computedLabel.property = label.mainProperty;
return computedLabel;
});
},

@discourseComputed("model.data", "model.computedLabels")
totalsForSampleRow(rows, labels) {
if (!rows || !rows.length) {
return {};
}

let totalsRow = {};

labels.forEach((label) => {
const reducer = (sum, row) => {
const computedLabel = label.compute(row);
const value = computedLabel.value;

if (!["seconds", "number", "percent"].includes(label.type)) {
return;
} else {
return sum + Math.round(value || 0);
}
};

const total = rows.reduce(reducer, 0);
totalsRow[label.mainProperty] =
label.type === "percent" ? Math.round(total / rows.length) : total;
});

return totalsRow;
},

@discourseComputed("sortLabel", "sortDirection", "model.data.[]")
sortedData(sortLabel, sortDirection, data) {
data = makeArray(data);

if (sortLabel) {
const compare = (label, direction) => {
return (a, b) => {
const aValue = label.compute(a, { useSortProperty: true }).value;
const bValue = label.compute(b, { useSortProperty: true }).value;
const result = aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
return result * direction;
};
};

return data.sort(compare(sortLabel, sortDirection));
}

return data;
},

@discourseComputed("sortedData.[]", "perPage", "page")
paginatedData(data, perPage, page) {
if (perPage < data.length) {
const start = perPage * page;
return data.slice(start, start + perPage);
}

return data;
},

@discourseComputed("model.data", "perPage", "page")
pages(data, perPage, page) {
if (!data || data.length <= perPage) {
return [];
}

const pagesIndexes = [];
for (let i = 0; i < Math.ceil(data.length / perPage); i++) {
pagesIndexes.push(i);
}

let pages = pagesIndexes.map((v) => {
return {
page: v + 1,
index: v,
class: v === page ? "is-current" : null,
};
});

if (pages.length > PAGES_LIMIT) {
const before = Math.max(0, page - PAGES_LIMIT / 2);
const after = Math.max(PAGES_LIMIT, page + PAGES_LIMIT / 2);
pages = pages.slice(before, after);
}

return pages;
},

actions: {
changePage(page) {
this.set("page", page);
},

sortByLabel(label) {
if (this.sortLabel === label) {
this.set("sortDirection", this.sortDirection === 1 ? -1 : 1);
} else {
this.set("sortLabel", label);
}
},
},
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "tr",
});

Large diffs are not rendered by default.

@@ -0,0 +1,124 @@
import Component from "@ember/component";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed";
import { isDocumentRTL } from "discourse/lib/text-direction";
import { next } from "@ember/runloop";

export default Component.extend({
@discourseComputed("theme.targets", "onlyOverridden", "showAdvanced")
visibleTargets(targets, onlyOverridden, showAdvanced) {
return targets.filter((target) => {
if (target.advanced && !showAdvanced) {
return false;
}
if (!onlyOverridden) {
return true;
}
return target.edited;
});
},

@discourseComputed("currentTargetName", "onlyOverridden", "theme.fields")
visibleFields(targetName, onlyOverridden, fields) {
fields = fields[targetName];
if (onlyOverridden) {
fields = fields.filter((field) => field.edited);
}
return fields;
},

@discourseComputed("currentTargetName", "fieldName")
activeSectionMode(targetName, fieldName) {
if (["settings", "translations"].includes(targetName)) {
return "yaml";
}
if (["extra_scss"].includes(targetName)) {
return "scss";
}
if (["color_definitions"].includes(fieldName)) {
return "scss";
}
return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html";
},

@discourseComputed("currentTargetName", "fieldName")
placeholder(targetName, fieldName) {
if (fieldName && fieldName === "color_definitions") {
const example =
":root {\n" +
" --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)};\n" +
"}";

return I18n.t("admin.customize.theme.color_definitions.placeholder", {
example: isDocumentRTL() ? `<div dir="ltr">${example}</div>` : example,
});
}
return "";
},

@discourseComputed("fieldName", "currentTargetName", "theme")
activeSection: {
get(fieldName, target, model) {
return model.getField(target, fieldName);
},
set(value, fieldName, target, model) {
model.setField(target, fieldName, value);
return value;
},
},

editorId: fmt("fieldName", "currentTargetName", "%@|%@"),

@discourseComputed("maximized")
maximizeIcon(maximized) {
return maximized ? "discourse-compress" : "discourse-expand";
},

@discourseComputed("currentTargetName", "theme.targets")
showAddField(currentTargetName, targets) {
return targets.find((t) => t.name === currentTargetName).customNames;
},

@discourseComputed(
"currentTargetName",
"fieldName",
"theme.theme_fields.@each.error"
)
error(target, fieldName) {
return this.theme.getError(target, fieldName);
},

actions: {
toggleShowAdvanced() {
this.toggleProperty("showAdvanced");
},

toggleAddField() {
this.toggleProperty("addingField");
},

cancelAddField() {
this.set("addingField", false);
},

addField(name) {
if (!name) {
return;
}
name = name.replace(/[^a-zA-Z0-9-_/]/g, "");
this.theme.setField(this.currentTargetName, name, "");
this.setProperties({ newFieldName: "", addingField: false });
this.fieldAdded(this.currentTargetName, name);
},

toggleMaximize: function () {
this.toggleProperty("maximized");
next(() => this.appEvents.trigger("ace:resize"));
},

onlyOverriddenChanged(value) {
this.onlyOverriddenChanged(value);
},
},
});
@@ -0,0 +1,107 @@
import discourseComputed, {
observes,
on,
} from "discourse-common/utils/decorators";
import { i18n, propertyEqual } from "discourse/lib/computed";
import Component from "@ember/component";
import I18n from "I18n";
import UserField from "admin/models/user-field";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { empty } from "@ember/object/computed";
import { isEmpty } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { scheduleOnce } from "@ember/runloop";

export default Component.extend(bufferedProperty("userField"), {
editing: empty("userField.id"),
classNameBindings: [":user-field"],

cantMoveUp: propertyEqual("userField", "firstField"),
cantMoveDown: propertyEqual("userField", "lastField"),

userFieldsDescription: i18n("admin.user_fields.description"),

@discourseComputed("buffered.field_type")
bufferedFieldType(fieldType) {
return UserField.fieldTypeById(fieldType);
},

@on("didInsertElement")
@observes("editing")
_focusOnEdit() {
if (this.editing) {
scheduleOnce("afterRender", this, "_focusName");
}
},

_focusName() {
$(".user-field-name").select();
},

@discourseComputed("userField.field_type")
fieldName(fieldType) {
return UserField.fieldTypeById(fieldType).get("name");
},

@discourseComputed(
"userField.editable",
"userField.required",
"userField.show_on_profile",
"userField.show_on_user_card"
)
flags(editable, required, showOnProfile, showOnUserCard) {
const ret = [];
if (editable) {
ret.push(I18n.t("admin.user_fields.editable.enabled"));
}
if (required) {
ret.push(I18n.t("admin.user_fields.required.enabled"));
}
if (showOnProfile) {
ret.push(I18n.t("admin.user_fields.show_on_profile.enabled"));
}
if (showOnUserCard) {
ret.push(I18n.t("admin.user_fields.show_on_user_card.enabled"));
}

return ret.join(", ");
},

actions: {
save() {
const buffered = this.buffered;
const attrs = buffered.getProperties(
"name",
"description",
"field_type",
"editable",
"required",
"show_on_profile",
"show_on_user_card",
"options"
);

this.userField
.save(attrs)
.then(() => {
this.set("editing", false);
this.commitBuffer();
})
.catch(popupAjaxError);
},

edit() {
this.set("editing", true);
},

cancel() {
const id = this.get("userField.id");
if (isEmpty(id)) {
this.destroyAction(this.userField);
} else {
this.rollbackBuffer();
this.set("editing", false);
}
},
},
});
@@ -0,0 +1,30 @@
import Component from "@ember/component";
import I18n from "I18n";
import bootbox from "bootbox";
import { iconHTML } from "discourse-common/lib/icon-library";

export default Component.extend({
classNames: ["watched-word"],
watchedWord: null,
xIcon: iconHTML("times").htmlSafe(),

init() {
this._super(...arguments);
this.set("watchedWord", this.get("word.word"));
},

click() {
this.word
.destroy()
.then(() => {
this.action(this.word);
})
.catch((e) => {
bootbox.alert(
I18n.t("generic_error_with_reason", {
error: `http: ${e.status} - ${e.body}`,
})
);
});
},
});
@@ -0,0 +1,47 @@
import Component from "@ember/component";
import I18n from "I18n";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";

export default Component.extend({
classNames: ["hook-event"],
typeName: alias("type.name"),

@discourseComputed("typeName")
name(typeName) {
return I18n.t(`admin.web_hooks.${typeName}_event.name`);
},

@discourseComputed("typeName")
details(typeName) {
return I18n.t(`admin.web_hooks.${typeName}_event.details`);
},

@discourseComputed("model.[]", "typeName")
eventTypeExists(eventTypes, typeName) {
return eventTypes.any((event) => event.name === typeName);
},

@discourseComputed("eventTypeExists")
enabled: {
get(eventTypeExists) {
return eventTypeExists;
},
set(value, eventTypeExists) {
const type = this.type;
const model = this.model;
// add an association when not exists
if (value !== eventTypeExists) {
if (value) {
model.addObject(type);
} else {
model.removeObjects(
model.filter((eventType) => eventType.name === type.name)
);
}
}

return value;
},
},
});
@@ -0,0 +1,113 @@
import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter";
import Component from "@ember/component";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";

export default Component.extend({
tagName: "li",
expandDetails: null,
expandDetailsRequestKey: "request",
expandDetailsResponseKey: "response",

@discourseComputed("model.status")
statusColorClasses(status) {
if (!status) {
return "";
}

if (status >= 200 && status <= 299) {
return "text-successful";
} else {
return "text-danger";
}
},

@discourseComputed("model.created_at")
createdAt(createdAt) {
return moment(createdAt).format("YYYY-MM-DD HH:mm:ss");
},

@discourseComputed("model.duration")
completion(duration) {
const seconds = Math.floor(duration / 10.0) / 100.0;
return I18n.t("admin.web_hooks.events.completed_in", { count: seconds });
},

@discourseComputed("expandDetails")
expandRequestIcon(expandDetails) {
return expandDetails === this.expandDetailsRequestKey
? "ellipsis-h"
: "ellipsis-v";
},

@discourseComputed("expandDetails")
expandResponseIcon(expandDetails) {
return expandDetails === this.expandDetailsResponseKey
? "ellipsis-h"
: "ellipsis-v";
},

actions: {
redeliver() {
return bootbox.confirm(
I18n.t("admin.web_hooks.events.redeliver_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
ajax(
`/admin/api/web_hooks/${this.get(
"model.web_hook_id"
)}/events/${this.get("model.id")}/redeliver`,
{ type: "POST" }
)
.then((json) => {
this.set("model", json.web_hook_event);
})
.catch(popupAjaxError);
}
}
);
},

toggleRequest() {
const expandDetailsKey = this.expandDetailsRequestKey;

if (this.expandDetails !== expandDetailsKey) {
let headers = Object.assign(
{
"Request URL": this.get("model.request_url"),
"Request method": "POST",
},
ensureJSON(this.get("model.headers"))
);
this.setProperties({
headers: plainJSON(headers),
body: prettyJSON(this.get("model.payload")),
expandDetails: expandDetailsKey,
bodyLabel: I18n.t("admin.web_hooks.events.payload"),
});
} else {
this.set("expandDetails", null);
}
},

toggleResponse() {
const expandDetailsKey = this.expandDetailsResponseKey;

if (this.expandDetails !== expandDetailsKey) {
this.setProperties({
headers: plainJSON(this.get("model.response_headers")),
body: this.get("model.response_body"),
expandDetails: expandDetailsKey,
bodyLabel: I18n.t("admin.web_hooks.events.body"),
});
} else {
this.set("expandDetails", null);
}
},
},
});
@@ -0,0 +1,38 @@
import Component from "@ember/component";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { iconHTML } from "discourse-common/lib/icon-library";

export default Component.extend({
classes: ["text-muted", "text-danger", "text-successful", "text-muted"],
icons: ["far-circle", "times-circle", "circle", "circle"],
circleIcon: null,
deliveryStatus: null,

@discourseComputed("deliveryStatuses", "model.last_delivery_status")
status(deliveryStatuses, lastDeliveryStatus) {
return deliveryStatuses.find((s) => s.id === lastDeliveryStatus);
},

@discourseComputed("status.id", "icons")
icon(statusId, icons) {
return icons[statusId - 1];
},

@discourseComputed("status.id", "classes")
class(statusId, classes) {
return classes[statusId - 1];
},

didReceiveAttrs() {
this._super(...arguments);
this.set(
"circleIcon",
iconHTML(this.icon, { class: this.class }).htmlSafe()
);
this.set(
"deliveryStatus",
I18n.t(`admin.web_hooks.delivery_status.${this.get("status.name")}`)
);
},
});
@@ -0,0 +1,12 @@
import Component from "@ember/component";
export default Component.extend({
didInsertElement() {
this._super(...arguments);
$("body").addClass("admin-interface");
},

willDestroyElement() {
this._super(...arguments);
$("body").removeClass("admin-interface");
},
});
@@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "",
});