View
@@ -0,0 +1,113 @@
+AllCops:
+ TargetRubyVersion: 2.4
+ DisabledByDefault: true
+ Exclude:
+ - 'db/schema.rb'
+ - 'bundle/**/*'
+ - 'vendor/**/*'
+ - 'node_modules/**/*'
+ - 'public/**/*'
+
+# 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
+
+Layout/MultilineMethodCallIndentation:
+ Enabled: true
+ EnforcedStyle: indented
+
+Layout/AlignHash:
+ Enabled: true
+
+Bundler/OrderedGems:
+ Enabled: false
View
@@ -1 +1 @@
-ruby-2.0.0-p195
+2.4.1
View
@@ -5,38 +5,74 @@ env:
- DISCOURSE_HOSTNAME=www.example.com
- RUBY_GC_MALLOC_LIMIT=50000000
matrix:
- - "RAILS_MASTER=1"
- - "RAILS_MASTER=0"
+ - "RAILS_MASTER=0 QUNIT_RUN=0 RUN_LINT=0"
+ - "RAILS_MASTER=0 QUNIT_RUN=1 RUN_LINT=0"
+ - "RAILS_MASTER=0 QUNIT_RUN=0 RUN_LINT=1"
+
+addons:
+ chrome: stable
+ postgresql: 9.5
+ apt:
+ packages:
+ - gifsicle
+ - jpegoptim
+ - optipng
+ - jhead
matrix:
- allow_failures:
- - rvm: 2.0.0
- env: "RAILS_MASTER=1"
- - rvm: 2.1
- env: "RAILS_MASTER=1"
- - rvm: rbx-2
fast_finish: true
rvm:
- - 2.0.0
- - 2.1
- - rbx-2
+ - 2.5.0
+ - 2.4.2
+ - 2.3.4
services:
- redis-server
-sudo: false
+sudo: required
+dist: trusty
-cache: bundler
+cache:
+ yarn: true
+ directories:
+ - vendor/bundle
before_install:
- - npm i -g jshint
- - jshint .
+ - gem install bundler
+ - git clone --depth=1 https://github.com/discourse/discourse-backup-uploads-to-s3.git plugins/discourse-backup-uploads-to-s3
+ - git clone --depth=1 https://github.com/discourse/discourse-spoiler-alert.git plugins/discourse-spoiler-alert
+ - git clone --depth=1 https://github.com/discourse/discourse-cakeday.git plugins/discourse-cakeday
+ - git clone --depth=1 https://github.com/discourse/discourse-canned-replies.git plugins/discourse-canned-replies
+ - git clone --depth=1 https://github.com/discourse/discourse-chat-integration.git plugins/discourse-chat-integration
+ - git clone --depth=1 https://github.com/discourse/discourse-assign.git plugins/discourse-assign
+ - git clone --depth=1 https://github.com/discourse/discourse-patreon.git plugins/discourse-patreon
+ - export PATH=$HOME/.yarn/bin:$PATH
-before_script:
- - psql -c 'create database discourse_test;' -U postgres
- - bundle exec rake db:migrate
+install:
+ - bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails seed-fu; fi"
+ - bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3; fi"
+ - bash -c "if [ '$RUN_LINT' == '1' ]; then yarn global add eslint babel-eslint; fi"
+ - bash -c "if [ '$QUNIT_RUN' == '1' ]; then yarn install --dev; fi"
-bundler_args: --without development --deployment --retry=3 --jobs=3
+script:
+ - |
+ bash -c "
+ if [ '$RUN_LINT' == '1' ]; then
+ bundle exec rubocop --parallel && \
+ eslint --ext .es6 app/assets/javascripts && \
+ eslint --ext .es6 test/javascripts && \
+ eslint --ext .es6 plugins/**/assets/javascripts && \
+ eslint --ext .es6 plugins/**/test/javascripts && \
+ eslint app/assets/javascripts test/javascripts
+ else
+ bundle exec rake db:create db:migrate
-script: 'bundle exec rspec && bundle exec rake plugin:spec && bundle exec rake qunit:test'
+ if [ '$QUNIT_RUN' == '1' ]; then
+ bundle exec rake qunit:test['500000'] && \
+ bundle exec rake plugin:qunit
+ else
+ bundle exec rspec && bundle exec rake plugin:spec
+ fi
+ fi
+ "
View
@@ -1,92 +1,93 @@
[main]
host = https://www.transifex.com
+lang_map = el_GR: el, es_ES: es, fr_FR: fr, ko_KR: ko, pt_PT: pt, sk_SK: sk, vi_VN: vi
[discourse-org.clientenyml]
file_filter = config/locales/client.<lang>.yml
source_file = config/locales/client.en.yml
source_lang = en
-trans.es_ES = config/locales/client.es.yml
-trans.fr_FR = config/locales/client.fr.yml
-trans.ko_KR = config/locales/client.ko.yml
-trans.pt_PT = config/locales/client.pt.yml
type = YML
[discourse-org.serverenyml]
file_filter = config/locales/server.<lang>.yml
source_file = config/locales/server.en.yml
source_lang = en
-trans.es_ES = config/locales/server.es.yml
-trans.fr_FR = config/locales/server.fr.yml
-trans.ko_KR = config/locales/server.ko.yml
-trans.pt_PT = config/locales/server.pt.yml
type = YML
[discourse-org.pollclientenyml]
file_filter = plugins/poll/config/locales/client.<lang>.yml
source_file = plugins/poll/config/locales/client.en.yml
source_lang = en
-trans.es_ES = plugins/poll/config/locales/client.es.yml
-trans.fr_FR = plugins/poll/config/locales/client.fr.yml
-trans.ko_KR = plugins/poll/config/locales/client.ko.yml
-trans.pt_PT = plugins/poll/config/locales/client.pt.yml
type = YML
[discourse-org.pollserverenyml]
file_filter = plugins/poll/config/locales/server.<lang>.yml
source_file = plugins/poll/config/locales/server.en.yml
source_lang = en
-trans.es_ES = plugins/poll/config/locales/server.es.yml
-trans.fr_FR = plugins/poll/config/locales/server.fr.yml
-trans.ko_KR = plugins/poll/config/locales/server.ko.yml
-trans.pt_PT = plugins/poll/config/locales/server.pt.yml
type = YML
-[discourse-org.imgurserverenyml]
-file_filter = vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.<lang>.yml
-source_file = vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.en.yml
+[discourse-org.narrativeclientenyml]
+file_filter = plugins/discourse-narrative-bot/config/locales/client.<lang>.yml
+source_file = plugins/discourse-narrative-bot/config/locales/client.en.yml
+source_lang = en
+type = YML
+
+[discourse-org.narrativeserverenyml]
+file_filter = plugins/discourse-narrative-bot/config/locales/server.<lang>.yml
+source_file = plugins/discourse-narrative-bot/config/locales/server.en.yml
+source_lang = en
+type = YML
+
+[discourse-org.discourse-presenceclientenyml]
+file_filter = plugins/discourse-presence/config/locales/client.<lang>.yml
+source_file = plugins/discourse-presence/config/locales/client.en.yml
+source_lang = en
+type = YML
+
+[discourse-org.discourse-presenceserverenyml]
+file_filter = plugins/discourse-presence/config/locales/server.<lang>.yml
+source_file = plugins/discourse-presence/config/locales/server.en.yml
+source_lang = en
+type = YML
+
+[discourse-org.coreplugindetailsclientyml]
+file_filter = plugins/discourse-details/config/locales/client.<lang>.yml
+source_file = plugins/discourse-details/config/locales/client.en.yml
+source_lang = en
+type = YML
+
+[discourse-org.coreplugindetailsserveryml]
+file_filter = plugins/discourse-details/config/locales/server.<lang>.yml
+source_file = plugins/discourse-details/config/locales/server.en.yml
+source_lang = en
+type = YML
+
+[discourse-org.corepluginnginx-performance-reportserveryml]
+file_filter = plugins/discourse-nginx-performance-report/config/locales/server.<lang>.yml
+source_file = plugins/discourse-nginx-performance-report/config/locales/server.en.yml
source_lang = en
-trans.es_ES = vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.es.yml
-trans.fr_FR = vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.fr.yml
-trans.ko_KR = vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.ko.yml
-trans.pt_PT = vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.pt.yml
type = YML
[discourse-org.403html]
file_filter = public/403.<lang>.html
source_file = public/403.html
source_lang = en
-trans.es_ES = public/403.es.html
-trans.fr_FR = public/403.fr.html
-trans.ko_KR = public/403.ko.html
-trans.pt_PT = public/403.pt.html
type = HTML
[discourse-org.422html]
file_filter = public/422.<lang>.html
source_file = public/422.html
source_lang = en
-trans.es_ES = public/422.es.html
-trans.fr_FR = public/422.fr.html
-trans.ko_KR = public/422.ko.html
-trans.pt_PT = public/422.pt.html
type = HTML
[discourse-org.500html]
file_filter = public/500.<lang>.html
source_file = public/500.html
source_lang = en
-trans.es_ES = public/500.es.html
-trans.fr_FR = public/500.fr.html
-trans.ko_KR = public/500.ko.html
-trans.pt_PT = public/500.pt.html
type = HTML
[discourse-org.503html]
file_filter = public/503.<lang>.html
source_file = public/503.html
source_lang = en
-trans.es_ES = public/503.es.html
-trans.fr_FR = public/503.fr.html
-trans.ko_KR = public/503.ko.html
-trans.pt_PT = public/503.pt.html
type = HTML
View
@@ -1,22 +1,10 @@
# Install development dependencies on Mac OS X using Homebrew (http://mxcl.github.com/homebrew)
-# ensure that Homebrew's sources are up to date
-update
-
-# add this repo to Homebrew's sources
-tap homebrew/dupes
-
-# install the gcc compiler required for ruby
-install apple-gcc42
-
# you probably already have git installed; ensure that it is the latest version
-install git
+brew 'git'
# install the PostgreSQL database
-install postgresql
+brew 'postgresql'
# install the Redis datastore
-install redis
-
-# install headless Javascript testing library
-install phantomjs
+brew 'redis'
View
@@ -1,129 +1,27 @@
# Contributing to Discourse
-## Before You Start
+## Important note for Developers
-Anyone wishing to contribute to the **[Discourse/Discourse](https://github.com/discourse/discourse)** project **MUST read & sign the [Electronic Discourse Forums Contribution License Agreement](http://www.discourse.org/cla)**. The Discourse team is legally prevented from accepting any pull requests from users who have not signed the CLA first.
+Anyone wishing to contribute to the [github.com/discourse/discourse](https://github.com/discourse/discourse) project **must read & sign our [Contributor License Agreement](http://www.discourse.org/cla)**. The Discourse team is legally prevented from accepting any pull requests from users who have not signed the CLA first.
-## Reporting Bugs
+For more information on
-1. Always update to the most recent master release; the bug may already be resolved.
+- how to set up your development environment
+- first-time project suggestions
+- code conventions
+- step-by-step guide for GitHub commits
-2. Search for similar issues on the [Discourse meta forum][m]; it may already be an identified problem.
+**please read our [Discourse Development Contribution Guidelines](https://meta.discourse.org/t/discourse-development-contribution-guidelines/3823)**
-3. Make sure you can reproduce your problem on our sandbox at [try.discourse.org](http://try.discourse.org)
+## Everything Else
-4. If this is a bug or problem that **requires any kind of extended discussion -- open [a topic on meta][m] about it**.
+There are many other ways to contribute to Discourse besides code. We've outlined the most common ones below.
-5. If possible, submit a Pull Request with a failing test. If you'd rather take matters into your own hands, fix the bug yourself (jump down to the "Contributing (Step-by-step)" section).
+- [Reporting Bugs](https://meta.discourse.org/t/how-to-make-bug-reports-for-discourse/33070)
+- [Requesting Features](https://meta.discourse.org/t/how-to-request-new-features-for-discourse/32986)
+- [Translation](https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882)
+- Documentation (TBA)
-6. When the bug is fixed, we will do our best to update the Discourse topic.
+For anything else, just start a new topic on [Meta](https://meta.discourse.org/) and let us know what you're interested in working on.
-## Requesting New Features
-
-1. Do not submit a feature request on GitHub; all feature requests on GitHub will be closed. Instead, visit the **[Discourse meta forum, features category](http://meta.discourse.org/category/feature)**, and search this list for similar feature requests. It's possible somebody has already asked for this feature or provided a pull request that we're still discussing.
-
-2. Provide a clear and detailed explanation of the feature you want and why it's important to add. The feature must apply to a wide array of users of Discourse; for smaller, more targeted "one-off" features, you might consider writing a plugin for Discourse. You may also want to provide us with some advance documentation on the feature, which will help the community to better understand where it will fit.
-
-3. If you're a Rock Star programmer, build the feature yourself (refer to the "Contributing (Step-by-step)" section below).
-
-## Contributing (Step-by-step)
-
-1. Clone the Repo:
-
- git clone git://github.com/discourse/discourse.git
-
-2. Create a new Branch:
-
- cd discourse
- git checkout -b new_discourse_branch
-
- > Please keep your code clean: one feature or bug-fix per branch. If you find another bug, you want to fix while being in a new branch, please fix it in a separated branch instead.
-
-3. Code
- * Adhere to common conventions you see in the existing code
- * Include tests, and ensure they pass
- * Search to see if your new functionality has been discussed on [the Discourse meta forum](http://meta.discourse.org), and include updates as appropriate
-
-4. Follow the Coding Conventions
- * two spaces, no tabs
- * no trailing whitespaces, blank lines should have no spaces
- * use spaces around operators, after commas, colons, semicolons, around `{` and before `}`
- * no space after `(`, `[` or before `]`, `)`
- * use Ruby 1.9 hash syntax: prefer `{ a: 1 }` over `{ :a => 1 }`
- * prefer `class << self; def method; end` over `def self.method` for class methods
- * prefer `{ ... }` over `do ... end` for single-line blocks, avoid using `{ ... }` for multi-line blocks
- * avoid `return` when not required
-
- > However, please note that **pull requests consisting entirely of style changes are not welcome on this project**. Style changes in the context of pull requests that also refactor code, fix bugs, improve functionality *are* welcome.
-
-5. Commit
-
- For every commit please write a short (max 72 characters) summary in the first line followed with a blank line and then more detailed descriptions of the change. Use markdown syntax for simple styling.
-
- **NEVER leave the commit message blank!** Provide a detailed, clear, and complete description of your commit!
-
-
-6. Update your branch
-
- ```
- git fetch origin
- git rebase origin/master
- ```
-
-7. Fork
-
- ```
- git remote add mine git@github.com:<your user name>/discourse.git
- ```
-
-8. Push to your remote
-
- ```
- git push mine new_discourse_branch
- ```
-
-9. Issue a Pull Request
-
- Before submitting a pull-request, clean up the history, go over your commits and squash together minor changes and fixes into the corresponding commits. You can squash commits with the interactive rebase command:
-
- ```
- git fetch origin
- git checkout new_discourse_branch
- git rebase origin/master
- git rebase -i
-
- < the editor opens and allows you to change the commit history >
- < follow the instructions on the bottom of the editor >
-
- git push -f mine new_discourse_branch
- ```
-
-
- In order to make a pull request,
- * Navigate to the Discourse repository you just pushed to (e.g. https://github.com/your-user-name/discourse)
- * Click "Pull Request".
- * Write your branch name in the branch field (this is filled with "master" by default)
- * Click "Update Commit Range".
- * Ensure the changesets you introduced are included in the "Commits" tab.
- * Ensure that the "Files Changed" incorporate all of your changes.
- * Fill in some details about your potential patch including a meaningful title.
- * Click "Send pull request".
-
- Thanks for that -- we'll get to your pull request ASAP, we love pull requests!
-
-10. Responding to Feedback
-
- The Discourse team may recommend adjustments to your code. Part of interacting with a healthy open-source community requires you to be open to learning new techniques and strategies; *don't get discouraged!* Remember: if the Discourse team suggest changes to your code, **they care enough about your work that they want to include it**, and hope that you can assist by implementing those revisions on your own.
-
- > Though we ask you to clean your history and squash commit before submitting a pull-request, please do not change any commits you've submitted already (as other work might be build on top).
-
-## Translations
-
-Translators can do their work in our [Transifex project](https://www.transifex.com/projects/p/discourse-org/). For more information, please see these how-to topics:
-
-* [Contributing a translation to Discourse](https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882)
-* [How to add a new language](https://meta.discourse.org/t/how-to-add-a-new-language/14970)
-
-
-
-[m]: http://meta.discourse.org
+*Thanks for contributing!*
View
226 Gemfile
@@ -2,222 +2,145 @@ source 'https://rubygems.org'
# if there is a super emergency and rubygems is playing up, try
#source 'http://production.cf.rubygems.org'
-module ::Kernel
- def rails_master?
- ENV["RAILS_MASTER"] == '1'
- end
-end
-
-if rails_master?
- # monkey patching to support dual booting
- module Bundler::SharedHelpers
- def default_lockfile=(path)
- @default_lockfile = path
- end
- def default_lockfile
- @default_lockfile ||= Pathname.new("#{default_gemfile}.lock")
- end
- end
-
- Bundler::SharedHelpers.default_lockfile = Pathname.new("#{Bundler::SharedHelpers.default_gemfile}_master.lock")
-
- # Bundler::Dsl.evaluate already called with an incorrect lockfile ... fix it
- class Bundler::Dsl
- # A bit messy, this can be called multiple times by bundler, avoid blowing the stack
- unless self.method_defined? :to_definition_unpatched
- alias_method :to_definition_unpatched, :to_definition
- end
- def to_definition(bad_lockfile, unlock)
- to_definition_unpatched(Bundler::SharedHelpers.default_lockfile, unlock)
- end
- end
+gem 'bootsnap', require: false, platform: :mri
+def rails_master?
+ ENV["RAILS_MASTER"] == '1'
end
-# Monkey patch bundler to support mri_21
-unless Bundler::Dependency::PLATFORM_MAP.include? :mri_21
- STDERR.puts
- STDERR.puts "WARNING: --------------------------------------------------------------------------"
- STDERR.puts "You are running an old version of bundler, please update by running: gem install bundler"
- STDERR.puts
- map = Bundler::Dependency::PLATFORM_MAP.dup
- map[:mri_21] = Gem::Platform::RUBY
- map.freeze
- Bundler::Dependency.send(:remove_const, "PLATFORM_MAP")
- Bundler::Dependency.const_set("PLATFORM_MAP", map)
-
- Bundler::Dsl.send(:remove_const, "VALID_PLATFORMS")
- Bundler::Dsl.const_set("VALID_PLATFORMS", map.keys.freeze)
- class ::Bundler::CurrentRuby
- def on_21?
- RUBY_VERSION =~ /^2\.1/
- end
- def mri_21?
- mri? && on_21?
- end
- end
- class ::Bundler::Dependency
- private
- def on_21?
- RUBY_VERSION =~ /^2\.1/
- end
- def mri_21?
- mri? && on_21?
- end
- end
-end
-
-
if rails_master?
gem 'arel', git: 'https://github.com/rails/arel.git'
gem 'rails', git: 'https://github.com/rails/rails.git'
- gem 'rails-observers', git: 'https://github.com/SamSaffron/rails-observers.git'
gem 'seed-fu', git: 'https://github.com/SamSaffron/seed-fu.git', branch: 'discourse'
else
- gem 'seed-fu', '~> 2.3.3'
- gem 'rails'
- gem 'rails-observers'
+ gem 'actionmailer', '~> 5.1'
+ gem 'actionpack', '~> 5.1'
+ gem 'actionview', '~> 5.1'
+ gem 'activemodel', '~> 5.1'
+ gem 'activerecord', '~> 5.1'
+ gem 'activesupport', '~> 5.1'
+ gem 'railties', '~> 5.1'
+ gem 'sprockets-rails'
+ gem 'seed-fu'
end
-gem 'actionpack-action_caching'
+gem 'mail', require: false
+gem 'mini_mime'
+gem 'mini_suffix'
-# Rails 4.1.6+ will relax the mail gem version requirement to `~> 2.5, >= 2.5.4`.
-# However, mail gem 2.6.x currently does not work with discourse because of the
-# reference to `Mail::RFC2822Parser` in `lib/email.rb`. This ensure discourse
-# would continue to work with Rails 4.1.6+ when it is released.
-gem 'mail', '~> 2.5.4'
-
-#gem 'redis-rails'
gem 'hiredis'
gem 'redis', require: ["redis", "redis/connection/hiredis"]
+gem 'redis-namespace'
-# We use some ams 0.8.0 features, need to amend code
-# to support 0.9 etc, bench needs to run and ensure no
-# perf regressions
-if rails_master?
- gem 'active_model_serializers', github: 'rails-api/active_model_serializers', branch: '0-8-stable'
-else
- gem 'active_model_serializers', '~> 0.8.0'
-end
+gem 'active_model_serializers', '~> 0.8.3'
+gem 'onebox', '1.8.45'
-gem 'onebox'
+gem 'http_accept_language', '~>2.0.5', require: false
-gem 'ember-rails'
-gem 'ember-source', '1.9.0.beta.4'
-gem 'handlebars-source', '2.0.0'
+gem 'ember-rails', '0.18.5'
+gem 'ember-source', '2.13.3'
+gem 'ember-handlebars-template', '0.7.5'
gem 'barber'
-gem '6to5'
gem 'message_bus'
-gem 'rails_multisite', path: 'vendor/gems/rails_multisite'
-gem 'redcarpet', require: false
-gem 'eventmachine'
-gem 'fast_xs'
+gem 'rails_multisite'
+
+gem 'fast_xs', platform: :mri
-gem 'fast_xor'
+# may move to xorcist post: https://github.com/fny/xorcist/issues/4
+gem 'fast_xor', platform: :mri
-# while we sort out https://github.com/sdsykes/fastimage/pull/46
-gem 'fastimage_discourse', require: 'fastimage'
-gem 'fog', '1.26.0', require: false
+gem 'fastimage'
+
+gem 'aws-sdk-s3', require: false
+gem 'excon', require: false
gem 'unf', require: false
-gem 'email_reply_parser'
+gem 'email_reply_trimmer', '0.1.11'
-# note: for image_optim to correctly work you need
-# sudo apt-get install -y advancecomp gifsicle jpegoptim libjpeg-progs optipng pngcrush
-#
-# Sam: held back, getting weird errors in latest
-gem 'image_optim', '0.9.1'
+# Forked until https://github.com/toy/image_optim/pull/149 is merged
+gem 'discourse_image_optim', require: 'image_optim'
gem 'multi_json'
gem 'mustache'
gem 'nokogiri'
+
gem 'omniauth'
gem 'omniauth-openid'
gem 'openid-redis-store'
gem 'omniauth-facebook'
gem 'omniauth-twitter'
-
-# forked while https://github.com/intridea/omniauth-github/pull/41 is being upstreamd
-gem 'omniauth-github-discourse', require: 'omniauth-github'
+gem 'omniauth-instagram'
+gem 'omniauth-github'
gem 'omniauth-oauth2', require: false
+
gem 'omniauth-google-oauth2'
gem 'oj'
-gem 'pg'
+gem 'pg', '~> 0.21.0'
gem 'pry-rails', require: false
+gem 'r2', '~> 0.2.5', require: false
gem 'rake'
-
-gem 'rest-client'
+gem 'thor', require: false
gem 'rinku'
gem 'sanitize'
-gem 'sass'
gem 'sidekiq'
# for sidekiq web
-gem 'sinatra', require: nil
+gem 'tilt', require: false
-gem 'therubyracer'
-gem 'thin', require: false
+gem 'execjs', require: false
+gem 'mini_racer'
gem 'highline', require: false
gem 'rack-protection' # security
-# Gems used only for assets and not required
-# in production environments by default.
-# allow everywhere for now cause we are allowing asset debugging in prd
+# 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
group :assets do
-
- if rails_master?
- gem 'sass-rails', git: 'https://github.com/rails/sass-rails.git'
- else
- # later is breaking our asset compliation extensions
- gem 'sass-rails', '4.0.2'
- end
-
gem 'uglifier'
gem 'rtlit', require: false # for css rtling
end
group :test do
+ gem 'webmock', require: false
gem 'fakeweb', '~> 1.3.0', require: false
gem 'minitest', require: false
end
group :test, :development do
- # while upgrading to 3
- gem 'rspec', '2.99.0'
+ gem 'rspec'
gem 'mock_redis'
- gem 'listen', '0.7.3', require: false
+ gem 'listen', require: false
gem 'certified', require: false
# later appears to break Fabricate(:topic, category: category)
gem 'fabrication', '2.9.8', require: false
- gem 'qunit-rails'
+ gem 'discourse-qunit-rails', require: 'qunit-rails'
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 'simplecov', require: false
- gem 'timecop'
- gem 'rspec-given'
+ gem 'rspec-html-matchers'
gem 'pry-nav'
- gem 'spork-rails'
+ gem 'byebug', require: ENV['RM_INFO'].nil?
+ gem 'rubocop', require: false
end
group :development do
+ gem 'ruby-prof', require: false
+ gem 'bullet', require: !!ENV['BULLET']
gem 'better_errors'
gem 'binding_of_caller'
- gem 'librarian', '>= 0.0.25', require: false
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' #, github: "SamSaffron/fast_blank"
+gem 'fast_blank', platform: :mri
# this provides a very efficient lru cache
gem 'lru_redux'
@@ -231,32 +154,35 @@ gem 'htmlentities', require: false
gem 'flamegraph', require: false
gem 'rack-mini-profiler', require: false
-gem 'unicorn', require: false
+gem 'unicorn', require: false, platform: :mri
gem 'puma', require: false
gem 'rbtrace', require: false, platform: :mri
+gem 'gc_tracer', require: false, platform: :mri
# required for feed importing and embedding
-#
gem 'ruby-readability', require: false
-gem 'simple-rss', require: false
+gem 'stackprof', require: false, platform: :mri
+gem 'memory_profiler', require: false, platform: :mri
-# TODO mri_22 should be here, but bundler was real slow to pick it up
-# not even in production bundler yet, monkey patching it in feels bad
-gem 'gctools', require: false, platform: :mri_21
-gem 'stackprof', require: false, platform: :mri_21
-gem 'memory_profiler', require: false, platform: :mri_21
+gem 'cppjieba_rb', require: false
-gem 'rmmseg-cpp', require: false
+gem 'lograge', require: false
+gem 'logstash-event', require: false
+gem 'logstash-logger', require: false
+gem 'logster'
-gem 'stringex', require: false
+gem 'sassc', require: false
-gem 'logster'
+gem 'rotp'
+gem 'rqrcode'
+
+gem 'sshkey', require: false
-# perftools only works on 1.9 atm
-group :profile do
- # travis refuses to install this, instead of fuffing, just avoid it for now
- #
- # if you need to profile, uncomment out this line
- # gem 'rack-perftools_profiler', require: 'rack/perftools_profiler', platform: :mri_19
+if ENV["IMPORT"] == "1"
+ gem 'mysql2'
+ gem 'redcarpet'
+ gem 'sqlite3', '~> 1.3.13'
+ gem 'ruby-bbcode-to-md', github: 'nlalonde/ruby-bbcode-to-md'
+ gem 'reverse_markdown'
end
View

Large diffs are not rendered by default.

Oops, something went wrong.
View

Large diffs are not rendered by default.

Oops, something went wrong.
View
@@ -1,50 +1,49 @@
<a href="http://www.discourse.org/">![Logo](images/discourse.png)</a>
-Discourse is the 100% open source discussion platform built for the next decade of the Internet. It works as:
+Discourse is the 100% open source discussion platform built for the next decade of the Internet. Use it as a:
-- a mailing list
-- a discussion forum
-- a long-form chat room
+- 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).
## Screenshots
-[![](https://raw.githubusercontent.com/discourse/discourse-docimages/master/readme/boing-boing-latest-small2.png)](http://bbs.boingboing.net)
-[![](https://raw.githubusercontent.com/discourse/discourse-docimages/master/readme/how-to-geek-profile-small2.png)](http://discuss.howtogeek.com)
-[![](https://raw.githubusercontent.com/discourse/discourse-docimages/master/readme/new-relic-categories-small2.png)](http://discuss.newrelic.com)
-[![](https://raw.githubusercontent.com/discourse/discourse-docimages/master/readme/turtle-rock-topic-small2.jpg)](https://talk.turtlerockstudios.com/)
-[![](https://raw.githubusercontent.com/discourse/discourse-docimages/master/readme/nexus-7-mobile-discourse-small3.png)](http://discuss.atom.io)
-[![](https://raw.githubusercontent.com/discourse/discourse-docimages/master/readme/iphone-5s-mobile-discourse-small4.png)](http://discourse.soylent.me)
-Browse [lots more notable Discourse instances](http://www.discourse.org/faq/customers/).
+<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>
+
+<img src="https://www.discourse.org/a/img/about/mobile-devices-2x.jpg" alt="Mobile" width="414">
+
+Browse [lots more notable Discourse instances](https://www.discourse.org/customers).
## Development
-1. If you're **brand new to Ruby and Rails**, please see [**Discourse as Your First Rails App**](http://blog.discourse.org/2013/04/discourse-as-your-first-rails-app/) or our [**Discourse Vagrant Developer Guide**](docs/VAGRANT.md), which includes a development environment in a virtual machine.
+1. If you're **brand new to Ruby and Rails**, please see [**Discourse as Your First Rails App**](http://blog.discourse.org/2013/04/discourse-as-your-first-rails-app/).
-2. If you're familiar with how Rails works and are comfortable setting up your own environment, use our [**Discourse Advanced Developer Guide**](docs/DEVELOPER-ADVANCED.md).
+2. If you're familiar with how Rails works and are comfortable setting up your own environment, use our [**Discourse Advanced Developer Guide**](docs/DEVELOPER-ADVANCED.md).
-Before you get started, ensure you have the following minimum versions: [Ruby 2.0.0+](http://www.ruby-lang.org/en/downloads/), [PostgreSQL 9.1+](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.3+](http://www.ruby-lang.org/en/downloads/), [PostgreSQL 9.3+](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!
-## Setting up a Discourse Forum
+## Setting up Discourse
If you want to set up a Discourse forum for production use, see our [**Discourse Install Guide**](docs/INSTALL.md).
-If you're looking for business class hosting, see [discourse.org/buy](http://www.discourse.org/buy/).
+If you're looking for business class hosting, see [discourse.org/buy](https://www.discourse.org/buy/).
## Requirements
Discourse is built for the *next* 10 years of the Internet, so our requirements are high:
-| Browsers | Tablets | Smartphones |
+| Browsers | Tablets | Phones |
| -------- | ------- | ----------- |
-| Safari 5.1+| iPad 2+ | iOS 6+ |
-| Google Chrome 23+ | Android 4.1+ | Android 4.1+ |
-| Internet Explorer 10+ | Windows 8 | Windows Phone 8 |
-| Firefox 16+ | |
-
-Internet Explorer 9.0 is technically supported, but it is our absolute minimum spec browser and may not be fully functional.
+| Safari 6.1+ | iPad 3+ | iOS 8+ |
+| Google Chrome 32+ | Android 4.3+ | Android 4.3+ |
+| Internet Explorer 11+ | | |
+| Firefox 27+ | | |
## Built With
@@ -57,24 +56,25 @@ Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https
## Contributing
-[![Build Status](https://travis-ci.org/discourse/discourse.svg)](https://travis-ci.org/discourse/discourse)
-[![Code Climate](https://codeclimate.com/github/discourse/discourse.svg)](https://codeclimate.com/github/discourse/discourse)
+[![Build Status](https://api.travis-ci.org/discourse/discourse.svg?branch=master)](https://travis-ci.org/discourse/discourse)
-Discourse is **100% free** and **open-source**. We encourage and support an active, healthy community that
+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).
3. Dig into [**CONTRIBUTING.MD**](CONTRIBUTING.md), which covers submitting bugs, requesting new features, preparing your code for a pull request, etc.
-4. 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)
+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)
+
We look forward to seeing your pull requests!
## Security
-We take security very seriously at Discourse; all our code is 100% open source and peer reviewed. Please read [our security guide](https://github.com/discourse/discourse/blob/master/docs/SECURITY.md) for an overview of security measures in Discourse.
+We take security very seriously at Discourse; all our code is 100% open source and peer reviewed. Please read [our security guide](https://github.com/discourse/discourse/blob/master/docs/SECURITY.md) for an overview of security measures in Discourse, or if you wish to report a security issue.
## The Discourse Team
@@ -83,7 +83,7 @@ The original Discourse code contributors can be found in [**AUTHORS.MD**](docs/A
## Copyright / License
-Copyright 2014 Civilized Discourse Construction Kit, Inc.
+Copyright 2014 - 2017 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.
View
1 Rakefile 100644 → 100755
@@ -9,4 +9,3 @@ Discourse::Application.load_tasks
# this prevents crashes when migrating a database in production in certain
# PostgreSQL configuations when trying to create structure.sql
Rake::Task["db:structure:dump"].clear if Rails.env.production?
-
View
@@ -1,65 +0,0 @@
-# -*- mode: ruby -*-
-# vi: set ft=ruby :
-# See https://github.com/discourse/discourse/blob/master/docs/VAGRANT.md
-#
-Vagrant.configure("2") do |config|
- config.vm.box= "discourse/discourse-0.9.9.15.box"
- config.vm.box_url = "https://vagrantcloud.com/discourse/discourse-0.9.9.15.box"
-
- # Make this VM reachable on the host network as well, so that other
- # VM's running other browsers can access our dev server.
- config.vm.network :private_network, ip: "192.168.10.200"
-
- # Make it so that network access from the vagrant guest is able to
- # use SSH private keys that are present on the host without copying
- # them into the VM.
- config.ssh.forward_agent = true
-
- config.vm.provider :virtualbox do |v|
- # This setting gives the VM 1024MB of RAM instead of the default 384.
- v.customize ["modifyvm", :id, "--memory", [ENV['DISCOURSE_VM_MEM'].to_i, 1024].max]
-
- # Who has a single core cpu these days anyways?
- cpu_count = 2
-
- # Determine the available cores in host system.
- # This mostly helps on linux, but it couldn't hurt on MacOSX.
- if RUBY_PLATFORM =~ /linux/
- cpu_count = `nproc`.to_i
- elsif RUBY_PLATFORM =~ /darwin/
- cpu_count = `sysctl -n hw.ncpu`.to_i
- end
-
- # Assign additional cores to the guest OS.
- v.customize ["modifyvm", :id, "--cpus", cpu_count]
- v.customize ["modifyvm", :id, "--ioapic", "on"]
-
- # This setting makes it so that network access from inside the vagrant guest
- # is able to resolve DNS using the hosts VPN connection.
- v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
- end
-
- config.vm.network :forwarded_port, guest: 3000, host: 4000
- config.vm.network :forwarded_port, guest: 1080, host: 4080 # Mailcatcher
-
- nfs_setting = RUBY_PLATFORM =~ /darwin/ || RUBY_PLATFORM =~ /linux/
- config.vm.synced_folder ".", "/vagrant", id: "vagrant-root", :nfs => nfs_setting
-
- config.vm.provision :shell, :inline => "apt-get -qq update && apt-get -qq -y install ruby1.9.3 build-essential && gem install chef --no-rdoc --no-ri --conservative"
-
- chef_cookbooks_path = ["chef/cookbooks"]
-
- # This run uses the updated chef-solo and does normal configuration
- config.vm.provision :chef_solo do |chef|
- chef.binary_env = "GEM_HOME=/opt/chef/embedded/lib/ruby/gems/1.9.1/ GEM_PATH= "
- chef.binary_path = "/opt/chef/bin/"
- chef.cookbooks_path = chef_cookbooks_path
-
- chef.add_recipe "recipe[apt]"
- chef.add_recipe "recipe[build-essential]"
- chef.add_recipe "recipe[vim]"
- chef.add_recipe "recipe[java]"
- chef.add_recipe "recipe[imagemagick]"
- chef.add_recipe "discourse"
- end
-end
View
Binary file not shown.
View
BIN app/assets/fonts/fontawesome-webfont.eot 100755 → 100644
Binary file not shown.
View
3,169 app/assets/fonts/fontawesome-webfont.svg 100755 → 100644

Large diffs are not rendered by default.

Oops, something went wrong.
View
BIN app/assets/fonts/fontawesome-webfont.ttf 100755 → 100644
Binary file not shown.
View
BIN app/assets/fonts/fontawesome-webfont.woff 100755 → 100644
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View

Large diffs are not rendered by default.

Oops, something went wrong.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
Binary file not shown.
View
@@ -2,4 +2,12 @@
require_asset("main_include_admin.js")
DiscoursePluginRegistry.admin_javascripts.each { |js| require_asset(js) }
+
+DiscoursePluginRegistry.each_globbed_asset(admin: true) do |f, ext|
+ if File.directory?(f)
+ depend_on(f)
+ elsif f.to_s.end_with?(".#{ext}")
+ require_asset(f)
+ end
+end
%>
View
@@ -0,0 +1,9 @@
+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);
+ }
+ });
+}
View
@@ -0,0 +1,7 @@
+import RestAdapter from 'discourse/adapters/rest';
+
+export default RestAdapter.extend({
+ basePath() {
+ return "/admin/customize/";
+ }
+});
View
@@ -0,0 +1,7 @@
+import RestAdapter from 'discourse/adapters/rest';
+
+export default RestAdapter.extend({
+ pathFor() {
+ return "/admin/customize/embedding";
+ }
+});
View
@@ -0,0 +1,38 @@
+import RestAdapter from 'discourse/adapters/rest';
+
+export default RestAdapter.extend({
+ pathFor(store, type, findArgs) {
+ let args = _.merge({ rest_api: true }, findArgs);
+ delete args.filter;
+ return `/admin/flags/${findArgs.filter}.json?${$.param(args)}`;
+ },
+
+ afterFindAll(results, helper) {
+ results.forEach(flag => {
+ let conversations = [];
+ flag.post_actions.forEach(pa => {
+ if (pa.conversation) {
+ let conversation = {
+ permalink: pa.permalink,
+ hasMore: pa.conversation.has_more,
+ response: {
+ excerpt: pa.conversation.response.excerpt,
+ user: helper.lookup('user', pa.conversation.response.user_id)
+ }
+ };
+
+ if (pa.conversation.reply) {
+ conversation.reply = {
+ excerpt: pa.conversation.reply.excerpt,
+ user: helper.lookup('user', pa.conversation.reply.user_id)
+ };
+ }
+ conversations.push(conversation);
+ }
+ });
+ flag.set('conversations', conversations);
+ });
+
+ return results;
+ }
+});
View
@@ -0,0 +1,2 @@
+import CustomizationBase from 'admin/adapters/customization-base';
+export default CustomizationBase;
View
@@ -0,0 +1,20 @@
+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);
+ });
+ return results;
+ },
+
+ jsonMode: true
+});
View
@@ -0,0 +1,2 @@
+import CustomizationBase from 'admin/adapters/customization-base';
+export default CustomizationBase;
View
@@ -0,0 +1,7 @@
+import RESTAdapter from 'discourse/adapters/rest';
+
+export default RESTAdapter.extend({
+ basePath() {
+ return '/admin/api/';
+ }
+});
View
@@ -0,0 +1,7 @@
+import RESTAdapter from 'discourse/adapters/rest';
+
+export default RESTAdapter.extend({
+ basePath() {
+ return '/admin/api/';
+ }
+});
View
@@ -0,0 +1,124 @@
+import loadScript from 'discourse/lib/load-script';
+import { observes } from 'ember-addons/ember-computed-decorators';
+
+const LOAD_ASYNC = !Ember.testing;
+
+export default Ember.Component.extend({
+ mode: 'css',
+ classNames: ['ace-wrapper'],
+ _editor: null,
+ _skipContentChangeEvent: null,
+ disabled: false,
+
+ @observes('editorId')
+ editorIdChanged() {
+ if (this.get('autofocus')) {
+ this.send('focus');
+ }
+ },
+
+ @observes('content')
+ contentChanged() {
+ if (this._editor && !this._skipContentChangeEvent) {
+ this._editor.getSession().setValue(this.get('content'));
+ }
+ },
+
+ @observes('mode')
+ modeChanged() {
+ if (LOAD_ASYNC && this._editor && !this._skipContentChangeEvent) {
+ this._editor.getSession().setMode("ace/mode/" + this.get('mode'));
+ }
+ },
+
+ @observes('disabled')
+ disabledStateChanged() {
+ this.changeDisabledState();
+ },
+
+ changeDisabledState() {
+ const editor = this._editor;
+ if (editor) {
+ const disabled = this.get('disabled');
+ editor.setOptions({
+ readOnly: disabled,
+ highlightActiveLine: !disabled,
+ highlightGutterLine: !disabled
+ });
+ editor.container.parentNode.setAttribute("data-disabled", disabled);
+ }
+ },
+
+ _destroyEditor: 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, this.resize);
+ }
+
+ $(window).off('ace:resize');
+
+ }.on('willDestroyElement'),
+
+ resize() {
+ if (this._editor) {
+ this._editor.resize();
+ }
+ },
+
+ didInsertElement() {
+ this._super();
+
+ loadScript("/javascripts/ace/ace.js", { scriptTag: true }).then(() => {
+ window.ace.require(['ace/ace'], loadedAce => {
+ if (!this.element || this.isDestroying || this.isDestroyed) { return; }
+ const editor = loadedAce.edit(this.$('.ace')[0]);
+
+ if (LOAD_ASYNC) {
+ editor.setTheme("ace/theme/chrome");
+ }
+ editor.setShowPrintMargin(false);
+ editor.setOptions({fontSize: "14px"});
+ if (LOAD_ASYNC) {
+ editor.getSession().setMode("ace/mode/" + this.get('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.$().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.get("autofocus")) {
+ this.send("focus");
+ }
+ });
+ });
+ },
+
+ actions: {
+ focus() {
+ if (this._editor) {
+ this._editor.focus();
+ this._editor.navigateFileEnd();
+ }
+ }
+ }
+});
View
@@ -0,0 +1,57 @@
+import debounce from 'discourse/lib/debounce';
+import { renderSpinner } from 'discourse/helpers/loading-spinner';
+import { escapeExpression } from 'discourse/lib/utilities';
+import { bufferedRender } from 'discourse-common/lib/buffered-render';
+
+export default Ember.Component.extend(bufferedRender({
+ classNames: ["admin-backups-logs"],
+
+ init() {
+ this._super();
+ this._reset();
+ },
+
+ _reset() {
+ this.setProperties({ formattedLogs: "", index: 0 });
+ },
+
+ _scrollDown() {
+ const $div = this.$()[0];
+ $div.scrollTop = $div.scrollHeight;
+ },
+
+ _updateFormattedLogs: debounce(function() {
+ const logs = this.get("logs");
+ if (logs.length === 0) {
+ this._reset(); // reset the cached logs whenever the model is reset
+ } else {
+ // do the log formatting only once for HELLish performance
+ let formattedLogs = this.get("formattedLogs");
+ for (let i = this.get("index"), length = logs.length; i < length; i++) {
+ const date = logs[i].get("timestamp"),
+ message = escapeExpression(logs[i].get("message"));
+ formattedLogs += "[" + date + "] " + message + "\n";
+ }
+ // update the formatted logs & cache index
+ this.setProperties({ formattedLogs: formattedLogs, index: logs.length });
+ // force rerender
+ this.rerenderBuffer();
+ }
+ Ember.run.scheduleOnce('afterRender', this, this._scrollDown);
+ }, 150).observes("logs.[]").on('init'),
+
+ buildBuffer(buffer) {
+ const formattedLogs = this.get("formattedLogs");
+ if (formattedLogs && formattedLogs.length > 0) {
+ buffer.push("<pre>");
+ buffer.push(formattedLogs);
+ buffer.push("</pre>");
+ } else {
+ buffer.push("<p>" + I18n.t("admin.backups.logs.none") + "</p>");
+ }
+ // add a loading indicator
+ if (this.get("status.isOperationRunning")) {
+ buffer.push(renderSpinner('small'));
+ }
+ }
+}));
View
@@ -0,0 +1,33 @@
+import { iconHTML } from 'discourse-common/lib/icon-library';
+import { bufferedRender } from 'discourse-common/lib/buffered-render';
+
+export default Ember.Component.extend(bufferedRender({
+ tagName: 'th',
+ classNames: ['sortable'],
+ rerenderTriggers: ['order', 'ascending'],
+
+ buildBuffer(buffer) {
+ const icon = this.get('icon');
+
+ if (icon) {
+ buffer.push(iconHTML(icon));
+ }
+
+ buffer.push(I18n.t(this.get('i18nKey')));
+
+ if (this.get('field') === this.get('order')) {
+ buffer.push(iconHTML(this.get('ascending') ? 'chevron-up' : 'chevron-down'));
+ }
+ },
+
+ click() {
+ const currentOrder = this.get('order');
+ const field = this.get('field');
+
+ if (currentOrder === field) {
+ this.set('ascending', this.get('ascending') ? null : true);
+ } else {
+ this.setProperties({ order: field, ascending: null });
+ }
+ }
+}));
View
@@ -0,0 +1,3 @@
+export default Ember.Component.extend({
+ classNames: ['row']
+});
View
@@ -0,0 +1,42 @@
+import loadScript from 'discourse/lib/load-script';
+
+export default Ember.Component.extend({
+ tagName: 'canvas',
+ refreshChart(){
+ const ctx = this.$()[0].getContext("2d");
+ const model = this.get("model");
+ const rawData = this.get("model.data");
+
+ var data = {
+ labels: rawData.map(r => r.x),
+ datasets: [{
+ data: rawData.map(r => r.y),
+ label: model.get('title'),
+ backgroundColor: "rgba(200,220,240,0.3)",
+ borderColor: "#08C"
+ }]
+ };
+
+ const config = {
+ type: 'line',
+ data: data,
+ options: {
+ responsive: true,
+ scales: {
+ yAxes: [{
+ display: true,
+ ticks: {
+ suggestedMin: 0
+ }
+ }]
+ }
+ },
+ };
+
+ this._chart = new window.Chart(ctx, config);
+ },
+
+ didInsertElement(){
+ loadScript("/javascripts/Chart.min.js").then(() => this.refreshChart.apply(this));
+ }
+});
View
@@ -1,34 +0,0 @@
-export default Ember.Component.extend({
- tagName: 'div',
-
- didInsertElement: function(){
- this.$("input").select2({
- multiple: true,
- width: '100%',
- query: function(opts){
- opts.callback({
- results: this.get("available").map(this._format)
- });
- }.bind(this)
- }).on("change", function(evt) {
- if (evt.added){
- this.triggerAction({action: "groupAdded",
- actionContext: this.get("available"
- ).findBy("id", evt.added.id)});
- } else if (evt.removed) {
- this.triggerAction({action:"groupRemoved",
- actionContext: this.get("selected"
- ).findBy("id", evt.removed.id)});
- }
- }.bind(this));
- this._refreshOnReset();
- },
-
- _format: function(item){
- return {"text": item.name, "id": item.id, "locked": item.automatic};
- },
-
- _refreshOnReset: function() {
- this.$("input").select2("data", this.get("selected").map(this._format));
- }.observes("selected")
-});
View
@@ -1,18 +0,0 @@
-export default Ember.Component.extend({
- tagName: 'li',
- classNameBindings: ['active'],
-
- router: function() {
- return this.container.lookup('router:main');
- }.property(),
-
- active: function() {
- const route = this.get('route');
- if (!route) { return; }
-
- const routeParam = this.get('routeParam'),
- router = this.get('router');
-
- return routeParam ? router.isActive(route, routeParam) : router.isActive(route);
- }.property('router.url', 'route')
-});
View
@@ -0,0 +1,3 @@
+export default Ember.Component.extend({
+ tagName: ''
+});
View
@@ -1,3 +1,5 @@
export default Ember.Component.extend({
- tagName: 'tbody'
+ tagName: 'tr',
+ reverseColors: Ember.computed.match('report.type', /^(time_to_first_response|topics_with_no_response)$/),
+ classNameBindings: ['reverseColors']
});
View
@@ -0,0 +1,3 @@
+export default Ember.Component.extend({
+ tagName: 'tr'
+});
View
@@ -0,0 +1,3 @@
+export default Ember.Component.extend({
+ tagName: 'tr'
+});
View
@@ -0,0 +1,98 @@
+import UserField from 'admin/models/user-field';
+import { bufferedProperty } from 'discourse/mixins/buffered-content';
+import { popupAjaxError } from 'discourse/lib/ajax-error';
+import { propertyEqual } from 'discourse/lib/computed';
+
+export default Ember.Component.extend(bufferedProperty('userField'), {
+ editing: Ember.computed.empty('userField.id'),
+ classNameBindings: [':user-field'],
+
+ cantMoveUp: propertyEqual('userField', 'firstField'),
+ cantMoveDown: propertyEqual('userField', 'lastField'),
+
+ userFieldsDescription: function() {
+ return I18n.t('admin.user_fields.description');
+ }.property(),
+
+ bufferedFieldType: function() {
+ return UserField.fieldTypeById(this.get('buffered.field_type'));
+ }.property('buffered.field_type'),
+
+ _focusOnEdit: function() {
+ if (this.get('editing')) {
+ Ember.run.scheduleOnce('afterRender', this, '_focusName');
+ }
+ }.observes('editing').on('didInsertElement'),
+
+ _focusName: function() {
+ $('.user-field-name').select();
+ },
+
+ fieldName: function() {
+ return UserField.fieldTypeById(this.get('userField.field_type')).get('name');
+ }.property('userField.field_type'),
+
+ flags: function() {
+ const ret = [];
+ if (this.get('userField.editable')) {
+ ret.push(I18n.t('admin.user_fields.editable.enabled'));
+ }
+ if (this.get('userField.required')) {
+ ret.push(I18n.t('admin.user_fields.required.enabled'));
+ }
+ if (this.get('userField.show_on_profile')) {
+ ret.push(I18n.t('admin.user_fields.show_on_profile.enabled'));
+ }
+ if (this.get('userField.show_on_user_card')) {
+ ret.push(I18n.t('admin.user_fields.show_on_user_card.enabled'));
+ }
+
+ return ret.join(', ');
+ }.property('userField.editable', 'userField.required', 'userField.show_on_profile', 'userField.show_on_user_card'),
+
+ actions: {
+ save() {
+ const self = this;
+ const buffered = this.get('buffered');
+ const attrs = buffered.getProperties('name',
+ 'description',
+ 'field_type',
+ 'editable',
+ 'required',
+ 'show_on_profile',
+ 'show_on_user_card',
+ 'options');
+
+ this.get('userField').save(attrs).then(function() {
+ self.set('editing', false);
+ self.commitBuffer();
+ }).catch(popupAjaxError);
+ },
+
+ moveUp() {
+ this.sendAction('moveUpAction', this.get('userField'));
+ },
+
+ moveDown() {
+ this.sendAction('moveDownAction', this.get('userField'));
+ },
+
+ edit() {
+ this.set('editing', true);
+ },
+
+ destroy() {
+ this.sendAction('destroyAction', this.get('userField'));
+ },
+
+ cancel() {
+ const id = this.get('userField.id');
+ if (Ember.isEmpty(id)) {
+ this.sendAction('destroyAction', this.get('userField'));
+ } else {
+ this.rollbackBuffer();
+ this.set('editing', false);
+ }
+ }
+ }
+});
View
@@ -0,0 +1,19 @@
+import { iconHTML } from 'discourse-common/lib/icon-library';
+import { bufferedRender } from 'discourse-common/lib/buffered-render';
+
+export default Ember.Component.extend(bufferedRender({
+ classNames: ['watched-word'],
+
+ buildBuffer(buffer) {
+ buffer.push(iconHTML('times'));
+ buffer.push(' ' + this.get('word.word'));
+ },
+
+ click() {
+ this.get('word').destroy().then(() => {
+ this.sendAction('action', this.get('word'));
+ }).catch(e => {
+ bootbox.alert(I18n.t("generic_error_with_reason", {error: `http: ${e.status} - ${e.body}`}));
+ });;
+ }
+}));
View
@@ -0,0 +1,42 @@
+import computed from 'ember-addons/ember-computed-decorators';
+
+export default Ember.Component.extend({
+ classNames: ['hook-event'],
+ typeName: Ember.computed.alias('type.name'),
+
+ @computed('typeName')
+ name(typeName) {
+ return I18n.t(`admin.web_hooks.${typeName}_event.name`);
+ },
+
+ @computed('typeName')
+ details(typeName) {
+ return I18n.t(`admin.web_hooks.${typeName}_event.details`);
+ },
+
+ @computed('model.[]', 'typeName')
+ eventTypeExists(eventTypes, typeName) {
+ return eventTypes.any(event => event.name === typeName);
+ },
+
+ @computed('eventTypeExists')
+ enabled: {
+ get(eventTypeExists) {
+ return eventTypeExists;
+ },
+ set(value, eventTypeExists) {
+ const type = this.get('type');
+ const model = this.get('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;
+ }
+ }
+});
View
@@ -0,0 +1,78 @@
+import computed from 'ember-addons/ember-computed-decorators';
+import { ajax } from 'discourse/lib/ajax';
+import { popupAjaxError } from 'discourse/lib/ajax-error';
+import { ensureJSON, plainJSON, prettyJSON } from 'discourse/lib/formatter';
+
+export default Ember.Component.extend({
+ tagName: 'li',
+ expandDetails: null,
+
+ @computed('model.status')
+ statusColorClasses(status) {
+ if (!status) return '';
+
+ if (status >= 200 && status <= 299) {
+ return 'text-successful';
+ } else {
+ return 'text-danger';
+ }
+ },
+
+ @computed('model.created_at')
+ createdAt(createdAt) {
+ return moment(createdAt).format('YYYY-MM-DD HH:mm:ss');
+ },
+
+ @computed('model.duration')
+ completion(duration) {
+ const seconds = Math.floor(duration / 10.0) / 100.0;
+ return I18n.t('admin.web_hooks.events.completed_in', { count: seconds });
+ },
+
+ 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 = 'request';
+
+ if (this.get('expandDetails') !== expandDetailsKey) {
+ let headers = _.extend({
+ '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 = 'response';
+
+ if (this.get('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);
+ }
+ }
+ }
+
+});
View
@@ -0,0 +1,28 @@
+import computed from 'ember-addons/ember-computed-decorators';
+import { iconHTML } from 'discourse-common/lib/icon-library';
+import { bufferedRender } from 'discourse-common/lib/buffered-render';
+
+export default Ember.Component.extend(bufferedRender({
+ classes: ["text-muted", "text-danger", "text-successful"],
+ icons: ["circle-o", "times-circle", "circle"],
+
+ @computed('deliveryStatuses', 'model.last_delivery_status')
+ status(deliveryStatuses, lastDeliveryStatus) {
+ return deliveryStatuses.find(s => s.id === lastDeliveryStatus);
+ },
+
+ @computed('status.id', 'icons')
+ icon(statusId, icons) {
+ return icons[statusId - 1];
+ },
+
+ @computed('status.id', 'classes')
+ class(statusId, classes) {
+ return classes[statusId - 1];
+ },
+
+ buildBuffer(buffer) {
+ buffer.push(iconHTML(this.get('icon'), { class: this.get('class') }));
+ buffer.push(I18n.t(`admin.web_hooks.delivery_status.${this.get('status.name')}`));
+ }
+}));
View
@@ -0,0 +1,11 @@
+export default Ember.Component.extend({
+ didInsertElement() {
+ this._super();
+ $('body').addClass('admin-interface');
+ },
+
+ willDestroyElement() {
+ this._super();
+ $('body').removeClass('admin-interface');
+ }
+});
View
@@ -0,0 +1,3 @@
+export default Ember.Component.extend({
+ tagName: ''
+});
View
@@ -1,3 +1,5 @@
+import {default as loadScript, loadCSS } from 'discourse/lib/load-script';
+
/**
An input field for a color.
@@ -6,19 +8,36 @@
@params valid is a boolean indicating if the input field is a valid color.
**/
export default Ember.Component.extend({
+ classNames: ['color-picker'],
hexValueChanged: function() {
var hex = this.get('hexValue');
+ let $text = this.$('input.hex-input');
+
if (this.get('valid')) {
- this.$('input').attr('style', 'color: ' + (this.get('brightnessValue') > 125 ? 'black' : 'white') + '; background-color: #' + hex + ';');
+ $text.attr('style', 'color: ' + (this.get('brightnessValue') > 125 ? 'black' : 'white') + '; background-color: #' + hex + ';');
+
+ if (this.get('pickerLoaded')) {
+ this.$('.picker').spectrum({color: "#" + this.get('hexValue')});
+ }
} else {
- this.$('input').attr('style', '');
+ $text.attr('style', '');
}
}.observes('hexValue', 'brightnessValue', 'valid'),
- _triggerHexChanged: function() {
- var self = this;
- Em.run.schedule('afterRender', function() {
- self.hexValueChanged();
+ didInsertElement() {
+ loadScript('/javascripts/spectrum.js').then(()=>{
+ loadCSS('/javascripts/spectrum.css').then(()=>{
+ Em.run.schedule('afterRender', ()=>{
+ this.$('.picker').spectrum({color: "#" + this.get('hexValue')})
+ .on("change.spectrum", (me, color)=>{
+ this.set('hexValue', color.toHexString().replace("#",""));
+ });
+ this.set('pickerLoaded', true);
+ });
+ });
+ });
+ Em.run.schedule('afterRender', ()=>{
+ this.hexValueChanged();
});
- }.on('didInsertElement')
+ }
});
View
@@ -0,0 +1,64 @@
+import { bufferedProperty } from 'discourse/mixins/buffered-content';
+import computed from 'ember-addons/ember-computed-decorators';
+import { on, observes } from 'ember-addons/ember-computed-decorators';
+import { popupAjaxError } from 'discourse/lib/ajax-error';
+
+export default Ember.Component.extend(bufferedProperty('host'), {
+ editToggled: false,
+ tagName: 'tr',
+ categoryId: null,
+
+ editing: Ember.computed.or('host.isNew', 'editToggled'),
+
+ @on('didInsertElement')
+ @observes('editing')
+ _focusOnInput() {
+ Ember.run.schedule('afterRender', () => { this.$('.host-name').focus(); });
+ },
+
+ @computed('buffered.host', 'host.isSaving')
+ cantSave(host, isSaving) {
+ return isSaving || Ember.isEmpty(host);
+ },
+
+ actions: {
+ edit() {
+ this.set('categoryId', this.get('host.category.id'));
+ this.set('editToggled', true);
+ },
+
+ save() {
+ if (this.get('cantSave')) { return; }
+
+ const props = this.get('buffered').getProperties('host', 'path_whitelist', 'class_name');
+ props.category_id = this.get('categoryId');
+
+ const host = this.get('host');
+
+ host.save(props).then(() => {
+ host.set('category', Discourse.Category.findById(this.get('categoryId')));
+ this.set('editToggled', false);
+ }).catch(popupAjaxError);
+ },
+
+ delete() {
+ bootbox.confirm(I18n.t('admin.embedding.confirm_delete'), (result) => {
+ if (result) {
+ this.get('host').destroyRecord().then(() => {
+ this.sendAction('deleteHost', this.get('host'));
+ });
+ }
+ });
+ },
+
+ cancel() {
+ const host = this.get('host');
+ if (host.get('isNew')) {
+ this.sendAction('deleteHost', host);
+ } else {
+ this.rollbackBuffer();
+ this.set('editToggled', false);
+ }
+ }
+ }
+});
View
@@ -0,0 +1,23 @@
+import computed from 'ember-addons/ember-computed-decorators';
+
+export default Ember.Component.extend({
+ classNames: ['embed-setting'],
+
+ @computed('field')
+ inputId(field) { return field.dasherize(); },
+
+ @computed('field')
+ translationKey(field) { return `admin.embedding.${field}`; },
+
+ @computed('type')
+ isCheckbox(type) { return type === "checkbox"; },
+
+ @computed('value')
+ checked: {
+ get(value) { return !!value; },
+ set(value) {
+ this.set('value', value);
+ return value;
+ }
+ }
+});
View
@@ -0,0 +1,3 @@
+export default Ember.Component.extend({
+ classNames: ['flag-user-lists']
+});
View
@@ -0,0 +1,3 @@
+export default Ember.Component.extend({
+ classNames: ['flagged-post-response']
+});
View
@@ -0,0 +1,3 @@
+export default Ember.Component.extend({
+ tagName: 'h3'
+});
View
@@ -0,0 +1,56 @@
+import showModal from 'discourse/lib/show-modal';
+import computed from 'ember-addons/ember-computed-decorators';
+
+export default Ember.Component.extend({
+ adminTools: Ember.inject.service(),
+ expanded: false,
+ tagName: 'div',
+ classNameBindings: [
+ ':flagged-post',
+ 'flaggedPost.hidden:hidden-post',
+ 'flaggedPost.deleted'
+ ],
+
+ canAct: Ember.computed.alias('actableFilter'),
+
+ @computed('filter')
+ actableFilter(filter) {
+ return filter === 'active';
+ },
+
+ removeAfter(promise) {
+ return promise.then(() => this.attrs.removePost());
+ },
+
+ _spawnModal(name, model, modalClass) {
+ let controller = showModal(name, { model, admin: true, modalClass });
+ controller.removeAfter = (p) => this.removeAfter(p);
+ },
+
+ actions: {
+ removeAfter(promise) {
+ return this.removeAfter(promise);
+ },
+
+ disagree() {
+ this.removeAfter(this.get('flaggedPost').disagreeFlags());
+ },
+
+ defer() {
+ this.removeAfter(this.get('flaggedPost').deferFlags());
+ },
+
+ expand() {
+ this.get('flaggedPost').expandHidden().then(() => {
+ this.set('expanded', true);
+ });
+ },
+
+ showModerationHistory() {
+ this.get('adminTools').showModerationHistory({
+ filter: 'post',
+ post_id: this.get('flaggedPost.id')
+ });
+ }
+ }
+});
View
@@ -0,0 +1,12 @@
+import { on, observes } from 'ember-addons/ember-computed-decorators';
+import highlightSyntax from 'discourse/lib/highlight-syntax';
+
+export default Ember.Component.extend({
+
+ @on('didInsertElement')
+ @observes('code')
+ _refresh: function() {
+ highlightSyntax(this.$());
+ }
+
+});
View
@@ -0,0 +1,36 @@
+import {default as computed, observes} from "ember-addons/ember-computed-decorators";
+
+export default Ember.Component.extend({
+ init(){
+ this._super();
+ this.set("checkedInternal", this.get("checked"));
+ },
+
+ classNames: ['inline-edit'],
+
+ @observes("checked")
+ checkedChanged() {
+ this.set("checkedInternal", this.get("checked"));
+ },
+
+ @computed("labelKey")
+ label(key) {
+ return I18n.t(key);
+ },
+
+ @computed("checked", "checkedInternal")
+ changed(checked, checkedInternal) {
+ return (!!checked) !== (!!checkedInternal);
+ },
+
+ actions: {
+ cancelled(){
+ this.set("checkedInternal", this.get("checked"));
+ },
+
+ finished(){
+ this.set("checked", this.get("checkedInternal"));
+ this.sendAction();
+ }
+ }
+});
View
@@ -1,3 +1,6 @@
+import { ajax } from 'discourse/lib/ajax';
+import AdminUser from 'admin/models/admin-user';
+
export default Ember.Component.extend({
classNames: ["ip-lookup"],
@@ -22,7 +25,7 @@ export default Ember.Component.extend({
this.set("show", true);
if (!this.get("location")) {
- Discourse.ajax("/admin/users/ip-info.json", {
+ ajax("/admin/users/ip-info", {
data: { ip: this.get("ip") }
}).then(function (location) {
self.set("location", Em.Object.create(location));
@@ -38,11 +41,11 @@ export default Ember.Component.extend({
"order": "trust_level DESC"
};
- Discourse.ajax("/admin/users/total-others-with-same-ip.json", { data: data }).then(function (result) {
+ ajax("/admin/users/total-others-with-same-ip", { data }).then(function (result) {
self.set("totalOthersWithSameIP", result.total);
});
- Discourse.AdminUser.findAll("active", data).then(function (users) {
+ AdminUser.findAll("active", data).then(function (users) {
self.setProperties({
other_accounts: users,
otherAccountsLoading: false,
@@ -65,7 +68,7 @@ export default Ember.Component.extend({
totalOthersWithSameIP: null
});
- Discourse.ajax("/admin/users/delete-others-with-same-ip.json", {
+ ajax("/admin/users/delete-others-with-same-ip.json", {
type: "DELETE",
data: {
"ip": self.get("ip"),
View
@@ -1,53 +0,0 @@
-/**
- Provide a nice GUI for a pipe-delimited list in the site settings.
-
- @param settingValue is a reference to SiteSetting.value.
- @param choices is a reference to SiteSetting.choices
-**/
-export default Ember.Component.extend({
-
- _select2FormatSelection: function(selectedObject, jqueryWrapper, htmlEscaper) {
- var text = selectedObject.text;
- if (text.length <= 6) {
- jqueryWrapper.closest('li.select2-search-choice').css({"border-bottom": '7px solid #'+text});
- }
- return htmlEscaper(text);
- },
-
- _initializeSelect2: function(){
- var options = {
- multiple: false,
- separator: "|",
- tokenSeparators: ["|"],
- tags : this.get("choices") || [],
- width: 'off',
- dropdownCss: this.get("choices") ? {} : {display: 'none'}
- };
-
- var settingName = this.get('settingName');
- if (typeof settingName === 'string' && settingName.indexOf('colors') > -1) {
- options.formatSelection = this._select2FormatSelection;
- }
-
- var self = this;
- this.$("input").select2(options).on("change", function(obj) {
- self.set("settingValue", obj.val.join("|"));
- self.refreshSortables();
- });
-
- this.refreshSortables();
- }.on('didInsertElement'),
-
- refreshOnReset: function() {
- this.$("input").select2("val", this.get("settingValue").split("|"));
- }.observes("settingValue"),
-
- refreshSortables: function() {
- var self = this;
- this.$("ul.select2-choices").sortable().on('sortupdate', function() {
- self.$("input").select2("onSortEnd");
- });
- }
-});
-
-
View
@@ -0,0 +1,3 @@
+export default Ember.Component.extend({
+ tagName: 'tr',
+});
View
@@ -0,0 +1,32 @@
+import computed from 'ember-addons/ember-computed-decorators';
+
+const ACTIONS = ['delete', 'edit', 'none'];
+export default Ember.Component.extend({
+ postAction: null,
+ postEdit: null,
+
+ @computed
+ penaltyActions() {
+ return ACTIONS.map(id => {
+ return { id, name: I18n.t(`admin.user.penalty_post_${id}`) };
+ });
+ },
+
+ editing: Ember.computed.equal('postAction', 'edit'),
+
+ actions: {
+ penaltyChanged() {
+ let postAction = this.get('postAction');
+
+ // If we switch to edit mode, jump to the edit textarea
+ if (postAction === 'edit') {
+ Ember.run.scheduleOnce('afterRender', () => {
+ let $elem = this.$();
+ let body = $elem.closest('.modal-body');
+ body.scrollTop(body.height());
+ $elem.find('.post-editor').focus();
+ });
+ }
+ }
+ }
+});
View
@@ -0,0 +1,58 @@
+import Permalink from 'admin/models/permalink';
+
+export default Ember.Component.extend({
+ classNames: ['permalink-form'],
+ formSubmitted: false,
+ permalinkType: 'topic_id',
+
+ permalinkTypes: function() {
+ return [
+ {id: 'topic_id', name: I18n.t('admin.permalink.topic_id')},
+ {id: 'post_id', name: I18n.t('admin.permalink.post_id')},
+ {id: 'category_id', name: I18n.t('admin.permalink.category_id')},
+ {id: 'external_url', name: I18n.t('admin.permalink.external_url')}
+ ];
+ }.property(),
+
+ permalinkTypePlaceholder: function() {
+ return 'admin.permalink.' + this.get('permalinkType');
+ }.property('permalinkType'),
+
+ actions: {
+ submit: function() {
+ if (!this.get('formSubmitted')) {
+ const self = this;
+ self.set('formSubmitted', true);
+ const permalink = Permalink.create({url: self.get('url'), permalink_type: self.get('permalinkType'), permalink_type_value: self.get('permalink_type_value')});
+ permalink.save().then(function(result) {
+ self.set('url', '');
+ self.set('permalink_type_value', '');
+ self.set('formSubmitted', false);
+ self.sendAction('action', Permalink.create(result.permalink));
+ Em.run.schedule('afterRender', function() { self.$('.permalink-url').focus(); });
+ }, function(e) {
+ self.set('formSubmitted', false);
+ let error;
+ if (e.responseJSON && e.responseJSON.errors) {
+ error = I18n.t("generic_error_with_reason", {error: e.responseJSON.errors.join('. ')});
+ } else {
+ error = I18n.t("generic_error");
+ }
+ bootbox.alert(error, function() { self.$('.permalink-url').focus(); });
+ });
+ }
+ }
+ },
+
+ didInsertElement: function() {
+ var self = this;
+ self._super();
+ Em.run.schedule('afterRender', function() {
+ self.$('.external-url').keydown(function(e) {
+ if (e.keyCode === 13) { // enter key
+ self.send('submit');
+ }
+ });
+ });
+ }
+});
View
@@ -0,0 +1,123 @@
+import { iconHTML } from 'discourse-common/lib/icon-library';
+import { bufferedRender } from 'discourse-common/lib/buffered-render';
+
+/*global Resumable:true */
+
+/**
+ Example usage:
+
+ {{resumable-upload
+ target="/admin/backups/upload"
+ success="successAction"
+ error="errorAction"
+ uploadText="UPLOAD"
+ }}
+**/
+export default Ember.Component.extend(bufferedRender({
+ tagName: "button",
+ classNames: ["btn", "ru"],
+ classNameBindings: ["isUploading"],
+ attributeBindings: ["translatedTitle:title"],
+
+ resumable: null,
+
+ isUploading: false,
+ progress: 0,
+
+ rerenderTriggers: ['isUploading', 'progress'],
+
+ translatedTitle: function() {
+ const title = this.get('title');
+ return title ? I18n.t(title) : this.get('text');
+ }.property('title', 'text'),
+
+ text: function() {
+ if (this.get("isUploading")) {
+ return this.get("progress") + " %";
+ } else {
+ return this.get("uploadText");
+ }
+ }.property("isUploading", "progress"),
+
+ buildBuffer(buffer) {
+ const icon = this.get("isUploading") ? "times" : "upload";
+ buffer.push(iconHTML(icon));
+ buffer.push("<span class='ru-label'>" + this.get("text") + "</span>");
+ buffer.push("<span class='ru-progress' style='width:" + this.get("progress") + "%'></span>");
+ },
+
+ click: function() {
+ if (this.get("isUploading")) {
+ this.resumable.cancel();
+ var self = this;
+ Em.run.later(function() { self._reset(); });
+ return false;
+ } else {
+ return true;
+ }
+ },
+
+ _reset: function() {
+ this.setProperties({ isUploading: false, progress: 0 });
+ },
+
+ _initialize: function() {
+ this.resumable = new Resumable({
+ target: Discourse.getURL(this.get("target")),
+ maxFiles: 1, // only 1 file at a time
+ headers: { "X-CSRF-Token": $("meta[name='csrf-token']").attr("content") }
+ });
+
+ var self = this;
+
+ this.resumable.on("fileAdded", function() {
+ // automatically upload the selected file
+ self.resumable.upload();
+ // mark as uploading
+ Em.run.later(function() {
+ self.set("isUploading", true);
+ });
+ });
+
+ this.resumable.on("fileProgress", function(file) {
+ // update progress
+ Em.run.later(function() {
+ self.set("progress", parseInt(file.progress() * 100, 10));
+ });
+ });
+
+ this.resumable.on("fileSuccess", function(file) {
+ Em.run.later(function() {
+ // mark as not uploading anymore
+ self._reset();
+ // fire an event to allow the parent route to reload its model
+ self.sendAction("success", file.fileName);
+ });
+ });
+
+ this.resumable.on("fileError", function(file, message) {
+ Em.run.later(function() {
+ // mark as not uploading anymore
+ self._reset();
+ // fire an event to allow the parent route to display the error message
+ self.sendAction("error", file.fileName, message);
+ });
+ });
+
+ }.on("init"),
+
+ _assignBrowse: function() {
+ var self = this;
+ Em.run.schedule("afterRender", function() {
+ self.resumable.assignBrowse(self.$());
+ });
+ }.on("didInsertElement"),
+
+ _teardown: function() {
+ if (this.resumable) {
+ this.resumable.cancel();
+ this.resumable = null;
+ }
+ }.on("willDestroyElement")
+
+}));
View
@@ -1,114 +0,0 @@
-/*global Resumable:true */
-
-/**
- Example usage:
-
- {{resumable-upload
- target="/admin/backups/upload"
- success="successAction"
- error="errorAction"
- uploadText="UPLOAD"
- }}
-**/
-Discourse.ResumableUploadComponent = Ember.Component.extend(Discourse.StringBuffer, {
- tagName: "button",
- classNames: ["btn", "ru"],
- classNameBindings: ["isUploading"],
-
- resumable: null,
-
- isUploading: false,
- progress: 0,
-
- rerenderTriggers: ['isUploading', 'progress'],
-
- text: function() {
- if (this.get("isUploading")) {
- return this.get("progress") + " %";
- } else {
- return this.get("uploadText");
- }
- }.property("isUploading", "progress"),
-
- renderString: function(buffer) {
- var icon = this.get("isUploading") ? "times" : "upload";
- buffer.push("<i class='fa fa-" + icon + "'></i>");
- buffer.push("<span class='ru-label'>" + this.get("text") + "</span>");
- buffer.push("<span class='ru-progress' style='width:" + this.get("progress") + "%'></span>");
- },
-
- click: function() {
- if (this.get("isUploading")) {
- this.resumable.cancel();
- var self = this;
- Em.run.later(function() { self._reset(); });
- return false;
- } else {
- return true;
- }
- },
-
- _reset: function() {
- this.setProperties({ isUploading: false, progress: 0 });
- },
-
- _initialize: function() {
- this.resumable = new Resumable({
- target: this.get("target"),
- maxFiles: 1, // only 1 file at a time
- headers: { "X-CSRF-Token": $("meta[name='csrf-token']").attr("content") }
- });
-
- var self = this;
-
- this.resumable.on("fileAdded", function() {
- // automatically upload the selected file
- self.resumable.upload();
- // mark as uploading
- Em.run.later(function() {
- self.set("isUploading", true);
- });
- });
-
- this.resumable.on("fileProgress", function(file) {
- // update progress
- Em.run.later(function() {
- self.set("progress", parseInt(file.progress() * 100, 10));
- });
- });
-
- this.resumable.on("fileSuccess", function(file) {
- Em.run.later(function() {
- // mark as not uploading anymore
- self._reset();
- // fire an event to allow the parent route to reload its model
- self.sendAction("success", file.fileName);
- });
- });
-
- this.resumable.on("fileError", function(file, message) {
- Em.run.later(function() {
- // mark as not uploading anymore
- self._reset();
- // fire an event to allow the parent route to display the error message
- self.sendAction("error", file.fileName, message);
- });
- });
-
- }.on("init"),
-
- _assignBrowse: function() {
- var self = this;
- Em.run.schedule("afterRender", function() {
- self.resumable.assignBrowse(self.$());
- });
- }.on("didInsertElement"),
-
- _teardown: function() {
- if (this.resumable) {
- this.resumable.cancel();
- this.resumable = null;
- }
- }.on("willDestroyElement")
-
-});
View
@@ -0,0 +1,18 @@
+import computed from 'ember-addons/ember-computed-decorators';
+
+export default Ember.Component.extend({
+ classNames: ['controls'],
+
+ buttonDisabled: Ember.computed.or('model.isSaving', 'saveDisabled'),
+
+ @computed('model.isSaving')
+ savingText(saving) {
+ return saving ? 'saving' : 'save';
+ },
+
+ actions: {
+ saveChanges() {
+ this.sendAction();
+ }
+ }
+});
View
@@ -0,0 +1,79 @@
+/**
+ A form to create an IP address that will be blocked or whitelisted.
+ Example usage:
+
+ {{screened-ip-address-form action="recordAdded"}}
+
+ where action is a callback on the controller or route that will get called after
+ the new record is successfully saved. It is called with the new ScreenedIpAddress record
+ as an argument.
+**/
+
+import ScreenedIpAddress from 'admin/models/screened-ip-address';
+import computed from 'ember-addons/ember-computed-decorators';
+import { on } from 'ember-addons/ember-computed-decorators';
+
+export default Ember.Component.extend({
+ classNames: ['screened-ip-address-form'],
+ formSubmitted: false,
+ actionName: 'block',
+
+ @computed
+ adminWhitelistEnabled() {
+ return Discourse.SiteSettings.use_admin_ip_whitelist;
+ },
+
+ @computed("adminWhitelistEnabled")
+ actionNames(adminWhitelistEnabled) {
+ if (adminWhitelistEnabled) {
+ return [
+ {id: 'block', name: I18n.t('admin.logs.screened_ips.actions.block')},
+ {id: 'do_nothing', name: I18n.t('admin.logs.screened_ips.actions.do_nothing')},
+ {id: 'allow_admin', name: I18n.t('admin.logs.screened_ips.actions.allow_admin')}
+ ];
+ } else {
+ return [
+ {id: 'block', name: I18n.t('admin.logs.screened_ips.actions.block')},
+ {id: 'do_nothing', name: I18n.t('admin.logs.screened_ips.actions.do_nothing')}
+ ];
+ }
+ },
+
+ actions: {
+ submit() {
+ if (!this.get('formSubmitted')) {
+ this.set('formSubmitted', true);
+ const screenedIpAddress = ScreenedIpAddress.create({
+ ip_address: this.get('ip_address'),
+ action_name: this.get('actionName')
+ });
+ screenedIpAddress.save().then(result => {
+ if (result.success) {
+ this.setProperties({ ip_address: '', formSubmitted: false });
+ this.sendAction('action', ScreenedIpAddress.create(result.screened_ip_address));
+ Ember.run.schedule('afterRender', () => this.$('.ip-address-input').focus());
+ } else {
+ bootbox.alert(result.errors);
+ }
+ }).catch(e => {
+ this.set('formSubmitted', false);
+ const msg = (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) ?
+ I18n.t("generic_error_with_reason", {error: e.jqXHR.responseJSON.errors.join('. ')}) :
+ I18n.t("generic_error");
+ bootbox.alert(msg, () => this.$('.ip-address-input').focus());
+ });
+ }
+ }
+ },
+
+ @on("didInsertElement")
+ _init() {
+ Ember.run.schedule('afterRender', () => {
+ this.$('.ip-address-input').keydown(e => {
+ if (e.keyCode === 13) {
+ this.send('submit');
+ }
+ });
+ });
+ }
+});
View
@@ -1,65 +0,0 @@
-/**
- A form to create an IP address that will be blocked or whitelisted.
- Example usage:
-
- {{screened-ip-address-form action="recordAdded"}}
-
- where action is a callback on the controller or route that will get called after
- the new record is successfully saved. It is called with the new ScreenedIpAddress record
- as an argument.
-
- @class ScreenedIpAddressFormComponent
- @extends Ember.Component
- @namespace Discourse
- @module Discourse
-**/
-Discourse.ScreenedIpAddressFormComponent = Ember.Component.extend({
- classNames: ['screened-ip-address-form'],
- formSubmitted: false,
- actionName: 'block',
-
- actionNames: function() {
- return [
- {id: 'block', name: I18n.t('admin.logs.screened_ips.actions.block')},
- {id: 'do_nothing', name: I18n.t('admin.logs.screened_ips.actions.do_nothing')},
- {id: 'allow_admin', name: I18n.t('admin.logs.screened_ips.actions.allow_admin')}
- ];
- }.property(),
-
- actions: {
- submit: function() {
- if (!this.get('formSubmitted')) {
- var self = this;
- this.set('formSubmitted', true);
- var screenedIpAddress = Discourse.ScreenedIpAddress.create({ip_address: this.get('ip_address'), action_name: this.get('actionName')});
- screenedIpAddress.save().then(function(result) {
- self.set('ip_address', '');
- self.set('formSubmitted', false);
- self.sendAction('action', Discourse.ScreenedIpAddress.create(result.screened_ip_address));
- Em.run.schedule('afterRender', function() { self.$('.ip-address-input').focus(); });
- }, function(e) {
- self.set('formSubmitted', false);
- var msg;
- if (e.responseJSON && e.responseJSON.errors) {
- msg = I18n.t("generic_error_with_reason", {error: e.responseJSON.errors.join('. ')});
- } else {
- msg = I18n.t("generic_error");
- }
- bootbox.alert(msg, function() { self.$('.ip-address-input').focus(); });
- });
- }
- }
- },
-
- didInsertElement: function() {
- var self = this;
- this._super();
- Em.run.schedule('afterRender', function() {
- self.$('.ip-address-input').keydown(function(e) {
- if (e.keyCode === 13) { // enter key
- self.send('submit');
- }
- });
- });
- }
-});
View
@@ -0,0 +1,3 @@
+export default Ember.Component.extend({
+ tagName: ''
+});
View
@@ -0,0 +1,10 @@
+import BufferedContent from 'discourse/mixins/buffered-content';
+import SiteSetting from 'admin/models/site-setting';
+import SettingComponent from 'admin/mixins/setting-component';
+
+export default Ember.Component.extend(BufferedContent, SettingComponent, {
+ _save() {
+ const setting = this.get('buffered');
+ return SiteSetting.update(setting.get('setting'), setting.get('value'));
+ }
+});
View
@@ -0,0 +1,17 @@
+import computed from "ember-addons/ember-computed-decorators";
+
+export default Ember.Component.extend({
+
+ @computed("value")
+ enabled: {
+ get(value) {
+ if (Ember.isEmpty(value)) { return false; }
+ return value.toString() === "true";
+ },
+ set(value) {
+ this.set("value", value ? "true" : "false");
+ return value;
+ }
+ },
+
+});
View
@@ -0,0 +1,16 @@
+import computed from "ember-addons/ember-computed-decorators";
+
+export default Ember.Component.extend({
+
+ @computed("value")
+ selectedCategories: {
+ get(value) {
+ return Discourse.Category.findByIds(value.split("|"));
+ },
+ set(value) {
+ this.set("value", value.mapBy("id").join("|"));
+ return value;
+ }
+ }
+
+});
View
@@ -0,0 +1,25 @@
+import { on } from 'ember-addons/ember-computed-decorators';
+
+export default Ember.Component.extend({
+ classNames: ['site-text'],
+ classNameBindings: ['siteText.overridden'],
+
+ @on('didInsertElement')
+ highlightTerm() {
+ const term = this.get('term');
+ if (term) {
+ this.$('.site-text-id, .site-text-value').highlight(term, {className: 'text-highlight'});
+ }
+ this.$('.site-text-value').ellipsis();
+ },
+
+ click() {
+ this.send('edit');
+ },
+
+ actions: {
+ edit() {
+ this.sendAction('editAction', this.get('siteText'));
+ }
+ }
+});
View
@@ -0,0 +1,22 @@
+import DiscourseURL from 'discourse/lib/url';
+
+export default Ember.Component.extend({
+ classNames: ['table', 'staff-actions'],
+
+ willDestroyElement() {
+ this.$().off('click.discourse-staff-logs');
+ },
+
+ didInsertElement() {
+ this._super();
+
+ this.$().on('click.discourse-staff-logs', '[data-link-post-id]', e => {
+ let postId = $(e.target).attr('data-link-post-id');
+
+ this.store.find('post', postId).then(p => {
+ DiscourseURL.routeTo(p.get('url'));
+ });
+ return false;
+ });
+ }
+});
View
@@ -0,0 +1,3 @@
+export default Ember.Component.extend({
+ tagName: ''
+});
View
@@ -0,0 +1,9 @@
+import BufferedContent from 'discourse/mixins/buffered-content';
+import SettingComponent from 'admin/mixins/setting-component';
+
+export default Ember.Component.extend(BufferedContent, SettingComponent, {
+ layoutName: 'admin/templates/components/site-setting',
+ _save() {
+ return this.get('model').saveSettings(this.get('setting.setting'), this.get('buffered.value'));
+ }
+});
View
@@ -0,0 +1,104 @@
+export default Ember.Component.extend({
+ classNameBindings: [':value-list'],
+
+ _enableSorting: function() {
+ const self = this;
+ const placeholder = document.createElement("div");
+ placeholder.className = "placeholder";
+
+ let dragging = null;
+ let over = null;
+ let nodePlacement;
+
+ this.$().on('dragstart.discourse', '.values .value', function(e) {
+ dragging = e.currentTarget;
+ e.dataTransfer.effectAllowed = 'move';
+ e.dataTransfer.setData("text/html", e.currentTarget);
+ });
+
+ this.$().on('dragend.discourse', '.values .value', function() {
+ Ember.run(function() {
+ dragging.parentNode.removeChild(placeholder);
+ dragging.style.display = 'block';
+
+ // Update data
+ const from = Number(dragging.dataset.index);
+ let to = Number(over.dataset.index);
+ if (from < to) to--;
+ if (nodePlacement === "after") to++;
+
+ const collection = self.get('collection');
+ const fromObj = collection.objectAt(from);
+ collection.replace(from, 1);
+ collection.replace(to, 0, [fromObj]);
+ self._saveValues();
+ });
+ return false;
+ });
+
+ this.$().on('dragover.discourse', '.values', function(e) {
+ e.preventDefault();
+ dragging.style.display = 'none';
+ if (e.target.className === "placeholder") { return; }
+ over = e.target;
+
+ const relY = e.originalEvent.clientY - over.offsetTop;
+ const height = over.offsetHeight / 2;
+ const parent = e.target.parentNode;
+
+ if (relY > height) {
+ nodePlacement = "after";
+ parent.insertBefore(placeholder, e.target.nextElementSibling);
+ } else if(relY < height) {
+ nodePlacement = "before";
+ parent.insertBefore(placeholder, e.target);
+ }
+ });
+ }.on('didInsertElement'),
+
+ _removeSorting: function() {
+ this.$().off('dragover.discourse').off('dragend.discourse').off('dragstart.discourse');
+ }.on('willDestroyElement'),
+
+ _setupCollection: function() {
+ const values = this.get('values');
+ if (this.get('inputType') === "array") {
+ this.set('collection', values || []);
+ } else {
+ this.set('collection', (values && values.length) ? values.split("\n") : []);
+ }
+ }.on('init').observes('values'),
+
+ _saveValues: function() {
+ if (this.get('inputType') === "array") {
+ this.set('values', this.get('collection'));
+ } else {
+ this.set('values', this.get('collection').join("\n"));
+ }
+ },
+
+ inputInvalid: Ember.computed.empty('newValue'),
+
+ keyDown(e) {
+ if (e.keyCode === 13) {
+ this.send('addValue');
+ }
+ },
+
+ actions: {
+ addValue() {
+ if (this.get('inputInvalid')) { return; }
+
+ this.get('collection').addObject(this.get('newValue'));
+ this.set('newValue', '');
+
+ this._saveValues();
+ },
+
+ removeValue(value) {
+ const collection = this.get('collection');
+ collection.removeObject(value);
+ this._saveValues();
+ }
+ }
+});
View
@@ -0,0 +1,55 @@
+import WatchedWord from 'admin/models/watched-word';
+import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
+
+export default Ember.Component.extend({
+ classNames: ['watched-word-form'],
+ formSubmitted: false,
+ actionKey: null,
+ showSuccessMessage: false,
+
+ @computed('regularExpressions')
+ placeholderKey(regularExpressions) {
+ return "admin.watched_words.form.placeholder" +
+ (regularExpressions ? "_regexp" : "");
+ },
+
+ @observes('word')
+ removeSuccessMessage() {
+ if (this.get('showSuccessMessage') && !Ember.isEmpty(this.get('word'))) {
+ this.set('showSuccessMessage', false);
+ }
+ },
+
+ actions: {
+ submit() {
+ if (!this.get('formSubmitted')) {
+ this.set('formSubmitted', true);
+
+ const watchedWord = WatchedWord.create({ word: this.get('word'), action: this.get('actionKey') });
+
+ watchedWord.save().then(result => {
+ this.setProperties({ word: '', formSubmitted: false, showSuccessMessage: true });
+ this.sendAction('action', WatchedWord.create(result));
+ Ember.run.schedule('afterRender', () => this.$('.watched-word-input').focus());
+ }).catch(e => {
+ this.set('formSubmitted', false);
+ const msg = (e.responseJSON && e.responseJSON.errors) ?
+ I18n.t("generic_error_with_reason", {error: e.responseJSON.errors.join('. ')}) :
+ I18n.t("generic_error");
+ bootbox.alert(msg, () => this.$('.watched-word-input').focus());
+ });
+ }
+ }
+ },
+
+ @on("didInsertElement")
+ _init() {
+ Ember.run.schedule('afterRender', () => {
+ this.$('.watched-word-input').keydown(e => {
+ if (e.keyCode === 13) {
+ this.send('submit');
+ }
+ });
+ });
+ }
+});
View
@@ -0,0 +1,25 @@
+import computed from "ember-addons/ember-computed-decorators";
+import UploadMixin from "discourse/mixins/upload";
+
+export default Em.Component.extend(UploadMixin, {
+ type: 'csv',
+ classNames: 'watched-words-uploader',
+ uploadUrl: '/admin/logs/watched_words/upload',
+ addDisabled: Em.computed.alias("uploading"),
+
+ validateUploadedFilesOptions() {
+ return { csvOnly: true };
+ },
+
+ @computed('actionKey')
+ data(actionKey) {
+ return { action_key: actionKey };
+ },
+
+ uploadDone() {
+ if (this) {
+ bootbox.alert(I18n.t("admin.watched_words.form.upload_successful"));
+ this.sendAction("done");
+ }
+ }
+});
View
@@ -1,26 +0,0 @@
-import ModalFunctionality from 'discourse/mixins/modal-functionality';
-import ObjectController from 'discourse/controllers/object';
-
-export default ObjectController.extend(ModalFunctionality, {
- needs: ["admin-flags-list"],
-
- _agreeFlag: function (actionOnPost) {
- var adminFlagController = this.get("controllers.admin-flags-list");
- var post = this.get("content");
- var self = this;
-
- return post.agreeFlags(actionOnPost).then(function () {
- adminFlagController.removeObject(post);
- self.send("closeModal");
- }, function () {
- bootbox.alert(I18n.t("admin.flags.error"));
- });
- },
-
- actions: {
- agreeFlagHidePost: function () { return this._agreeFlag("hide"); },
- agreeFlagKeepPost: function () { return this._agreeFlag("keep"); },
- agreeFlagRestorePost: function () { return this._agreeFlag("restore"); }
- }
-
-});
View
@@ -0,0 +1,32 @@
+import ApiKey from 'admin/models/api-key';
+
+export default Ember.Controller.extend({
+
+ actions: {
+ generateMasterKey() {
+ ApiKey.generateMasterKey().then(key => this.get('model').pushObject(key));
+ },
+
+ regenerateKey(key) {
+ bootbox.confirm(I18n.t("admin.api.confirm_regen"), I18n.t("no_value"), I18n.t("yes_value"), result => {
+ if (result) {
+ key.regenerate();
+ }
+ });
+ },
+
+ revokeKey(key) {
+ bootbox.confirm(I18n.t("admin.api.confirm_revoke"), I18n.t("no_value"), I18n.t("yes_value"), result => {
+ if (result) {
+ key.revoke().then(() => this.get('model').removeObject(key));
+ }
+ });
+ }
+ },
+
+ // Has a master key already been generated?
+ hasMasterKey: function() {
+ return !!this.get('model').findBy('user', null);
+ }.property('model.[]')
+
+});
Oops, something went wrong.