diff --git a/.gitignore b/.gitignore index fbaaa54386..63fc931ed1 100644 --- a/.gitignore +++ b/.gitignore @@ -19,8 +19,6 @@ public/tombstone/* # Ignore bundler config /.bundle -/.vagrant -/.vagrantfile /cache /coverage/* @@ -95,9 +93,6 @@ config/fog_credentials.yml script/download_db script/refresh_db -# temp directory for chef (used to configure vagrant VM) -chef/tmp/* - # .procfile .procfile diff --git a/.tx/config b/.tx/config index 26ec9541c4..83873e2d8c 100644 --- a/.tx/config +++ b/.tx/config @@ -50,6 +50,24 @@ 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..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..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..yml +source_file = plugins/discourse-nginx-performance-report/config/locales/server.en.yml +source_lang = en +type = YML + [discourse-org.403html] file_filter = public/403..html source_file = public/403.html diff --git a/Gemfile b/Gemfile index 75c6438657..2f4e597291 100644 --- a/Gemfile +++ b/Gemfile @@ -24,8 +24,7 @@ else gem 'seed-fu' end -gem 'mail' -gem 'mime-types', require: 'mime/types/columnar' +gem 'mail', require: false gem 'mini_mime' gem 'mini_suffix' @@ -59,7 +58,7 @@ gem 'aws-sdk-s3', require: false gem 'excon', require: false gem 'unf', require: false -gem 'email_reply_trimmer', '0.1.10' +gem 'email_reply_trimmer', '0.1.11' # Forked until https://github.com/toy/image_optim/pull/149 is merged gem 'discourse_image_optim', require: 'image_optim' diff --git a/Gemfile.lock b/Gemfile.lock index 4ee395b285..0d7600cb2b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -91,7 +91,7 @@ GEM image_size (~> 1.5) in_threads (~> 1.3) progress (~> 3.0, >= 3.0.1) - email_reply_trimmer (0.1.10) + email_reply_trimmer (0.1.11) ember-data-source (2.2.1) ember-source (>= 1.8, < 3.0) ember-handlebars-template (0.7.5) @@ -159,20 +159,17 @@ GEM logstash-logger (0.25.1) logstash-event (~> 1.2) logster (1.2.9) - loofah (2.2.1) + loofah (2.2.2) crass (~> 1.0.2) nokogiri (>= 1.5.9) lru_redux (1.1.0) - mail (2.6.6) - mime-types (>= 1.16, < 4) + mail (2.7.0) + mini_mime (>= 0.1.1) memory_profiler (0.9.10) message_bus (2.1.2) rack (>= 1.1.3) metaclass (0.0.4) method_source (0.8.2) - mime-types (3.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) mini_mime (0.1.3) mini_portile2 (2.3.0) mini_racer (0.1.15) @@ -256,21 +253,21 @@ GEM public_suffix (2.0.5) puma (3.9.1) r2 (0.2.6) - rack (2.0.3) - rack-mini-profiler (0.10.7) + rack (2.0.4) + rack-mini-profiler (1.0.0) rack (>= 1.2.0) rack-openid (1.3.1) rack (>= 1.1.0) ruby-openid (>= 2.1.8) - rack-protection (2.0.0) + rack-protection (2.0.1) rack rack-test (0.7.0) rack (>= 1.0, < 3) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.0.3) - loofah (~> 2.0) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) rails_multisite (2.0.4) activerecord (> 4.2, < 6) railties (> 4.2, < 6) @@ -417,7 +414,7 @@ DEPENDENCIES cppjieba_rb discourse-qunit-rails discourse_image_optim - email_reply_trimmer (= 0.1.10) + email_reply_trimmer (= 0.1.11) ember-handlebars-template (= 0.7.5) ember-rails (= 0.18.5) ember-source (= 2.13.3) @@ -445,7 +442,6 @@ DEPENDENCIES mail memory_profiler message_bus - mime-types mini_mime mini_racer mini_suffix diff --git a/README.md b/README.md index 1069b7ee84..51b8314318 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Browse [lots more notable Discourse instances](https://www.discourse.org/custome ## 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). diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index bac277c090..0000000000 --- a/Vagrantfile +++ /dev/null @@ -1,48 +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-16.04' - config.vm.box_url = "https://www.dropbox.com/s/2132770g1e05c6d/discourse.box?dl=1" - - # 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" - -end diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 index c961c6d017..28ae718afa 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 @@ -5,6 +5,8 @@ export default Ember.Controller.extend({ maximized: false, section: null, + editRouteName: 'adminCustomizeThemes.edit', + targets: [ { id: 0, name: 'common' }, { id: 1, name: 'desktop' }, @@ -52,7 +54,7 @@ export default Ember.Controller.extend({ let fields = this.get('model.theme_fields'); let field = fields && fields.find(f => (f.target === target)); - this.replaceRoute('adminCustomizeThemes.edit', this.get('model.id'), target, field && field.name); + this.replaceRoute(this.get('editRouteName'), this.get('model.id'), target, field && field.name); } } }, diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 index b8850b10f8..16eff82947 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 @@ -8,6 +8,8 @@ const THEME_UPLOAD_VAR = 2; export default Ember.Controller.extend({ + editRouteName: 'adminCustomizeThemes.edit', + @computed("model", "allThemes") parentThemes(model, allThemes) { let parents = allThemes.filter(theme => @@ -142,7 +144,7 @@ export default Ember.Controller.extend({ }, editTheme() { - let edit = ()=>this.transitionToRoute('adminCustomizeThemes.edit', this.get('model.id'), 'common', 'scss'); + let edit = ()=>this.transitionToRoute(this.get('editRouteName'), this.get('model.id'), 'common', 'scss'); if (this.get("model.remote_theme")) { bootbox.confirm(I18n.t("admin.customize.theme.edit_confirm"), result => { diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 index d06b8dc06e..6652002fb4 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 @@ -19,6 +19,7 @@ export default Ember.Controller.extend({ versionCheck: null, dashboardFetchedAt: null, showVersionChecks: setting('version_checks'), + exceptionController: Ember.inject.controller('exception'), @computed('problems.length') foundProblems(problemsLength) { @@ -39,10 +40,10 @@ export default Ember.Controller.extend({ fetchDashboard() { if (!this.get('dashboardFetchedAt') || moment().subtract(30, 'minutes').toDate() > this.get('dashboardFetchedAt')) { - this.set('dashboardFetchedAt', new Date()); this.set('loading', true); const versionChecks = this.siteSettings.version_checks; AdminDashboard.find().then(d => { + this.set('dashboardFetchedAt', new Date()); if (versionChecks) { this.set('versionCheck', VersionCheck.create(d.version_check)); } @@ -56,6 +57,10 @@ export default Ember.Controller.extend({ } ATTRIBUTES.forEach(a => this.set(a, d[a])); + }).catch(e => { + this.get('exceptionController').set('thrown', e.jqXHR); + this.replaceRoute('exception'); + }).finally(() => { this.set('loading', false); }); } diff --git a/app/assets/javascripts/admin/controllers/admin-group.js.es6 b/app/assets/javascripts/admin/controllers/admin-group.js.es6 deleted file mode 100644 index cab20f908f..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-group.js.es6 +++ /dev/null @@ -1,110 +0,0 @@ -import { popupAjaxError } from 'discourse/lib/ajax-error'; -import computed from 'ember-addons/ember-computed-decorators'; - -export default Ember.Controller.extend({ - adminGroupsType: Ember.inject.controller(), - disableSave: false, - savingStatus: '', - - aliasLevelOptions: function() { - return [ - { name: I18n.t("groups.alias_levels.nobody"), value: 0 }, - { name: I18n.t("groups.alias_levels.mods_and_admins"), value: 2 }, - { name: I18n.t("groups.alias_levels.members_mods_and_admins"), value: 3 }, - { name: I18n.t("groups.alias_levels.everyone"), value: 99 } - ]; - }.property(), - - visibilityLevelOptions: function() { - return [ - { name: I18n.t("groups.visibility_levels.public"), value: 0 }, - { name: I18n.t("groups.visibility_levels.members"), value: 1 }, - { name: I18n.t("groups.visibility_levels.staff"), value: 2 }, - { name: I18n.t("groups.visibility_levels.owners"), value: 3 } - ]; - }.property(), - - trustLevelOptions: function() { - return [ - { name: I18n.t("groups.trust_levels.none"), value: 0 }, - { name: 1, value: 1 }, { name: 2, value: 2 }, { name: 3, value: 3 }, { name: 4, value: 4 } - ]; - }.property(), - - @computed('model.visibility_level', 'model.public_admission') - disableMembershipRequestSetting(visibility_level, publicAdmission) { - visibility_level = parseInt(visibility_level); - return (visibility_level !== 0) || publicAdmission; - }, - - @computed('model.visibility_level', 'model.allow_membership_requests') - disablePublicSetting(visibility_level, allowMembershipRequests) { - visibility_level = parseInt(visibility_level); - return (visibility_level !== 0) || allowMembershipRequests; - }, - - actions: { - removeOwner(member) { - const self = this, - message = I18n.t("admin.groups.delete_owner_confirm", { username: member.get("username"), group: this.get("model.name") }); - return bootbox.confirm(message, I18n.t("no_value"), I18n.t("yes_value"), function(confirm) { - if (confirm) { - self.get("model").removeOwner(member); - } - }); - }, - - addOwners() { - if (Em.isEmpty(this.get("model.ownerUsernames"))) { return; } - this.get("model").addOwners(this.get("model.ownerUsernames")).catch(popupAjaxError); - this.set("model.ownerUsernames", null); - }, - - save() { - const group = this.get('model'), - groupsController = this.get("adminGroupsType"), - groupType = groupsController.get("type"); - - this.set('disableSave', true); - this.set('savingStatus', I18n.t('saving')); - - let promise = group.get("id") ? group.save() : group.create().then(() => groupsController.get('model').addObject(group)); - - promise.then(() => { - this.transitionToRoute("adminGroup", groupType, group.get('name')); - this.set('savingStatus', I18n.t('saved')); - }).catch(popupAjaxError) - .finally(() => this.set('disableSave', false)); - }, - - destroy() { - const group = this.get('model'), - groupsController = this.get('adminGroupsType'), - self = this; - - if (!group.get('id')) { - self.transitionToRoute('adminGroupsType.index', 'custom'); - return; - } - - this.set('disableSave', true); - - bootbox.confirm( - I18n.t("admin.groups.delete_confirm"), - I18n.t("no_value"), - I18n.t("yes_value"), - function(confirmed) { - if (confirmed) { - group.destroy().then(() => { - groupsController.get('model').removeObject(group); - self.transitionToRoute('adminGroups.index'); - }).catch(() => bootbox.alert(I18n.t("admin.groups.delete_failed"))) - .finally(() => self.set('disableSave', false)); - } else { - self.set('disableSave', false); - } - } - ); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-groups-bulk-complete.js.es6 b/app/assets/javascripts/admin/controllers/admin-groups-bulk-complete.js.es6 deleted file mode 100644 index 780543b724..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-groups-bulk-complete.js.es6 +++ /dev/null @@ -1,4 +0,0 @@ -export default Ember.Controller.extend({ - adminGroupsBulk: Ember.inject.controller(), - bulkAddResponse: Ember.computed.alias('adminGroupsBulk.bulkAddResponse') -}); diff --git a/app/assets/javascripts/admin/controllers/admin-groups-bulk.js.es6 b/app/assets/javascripts/admin/controllers/admin-groups-bulk.js.es6 deleted file mode 100644 index 8f8b28f6b4..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-groups-bulk.js.es6 +++ /dev/null @@ -1,37 +0,0 @@ -import { ajax } from 'discourse/lib/ajax'; -import computed from 'ember-addons/ember-computed-decorators'; -import { popupAjaxError } from 'discourse/lib/ajax-error'; - -export default Ember.Controller.extend({ - users: null, - groupId: null, - saving: false, - bulkAddResponse: null, - - @computed('saving', 'users', 'groupId') - buttonDisabled(saving, users, groupId) { - return saving || !groupId || !users || !users.length; - }, - - actions: { - addToGroup() { - if (this.get('saving')) { return; } - - const users = this.get('users').split("\n") - .uniq() - .reject(x => x.length === 0); - - this.set('saving', true); - ajax('/admin/groups/bulk', { - data: { users, group_id: this.get('groupId') }, - method: 'PUT' - }).then(result => { - this.set('bulkAddResponse', result); - this.transitionToRoute('adminGroups.bulkComplete'); - }).catch(popupAjaxError).finally(() => { - this.set('saving', false); - }); - - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-groups-type-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-groups-type-index.js.es6 deleted file mode 100644 index ca80f093d1..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-groups-type-index.js.es6 +++ /dev/null @@ -1,11 +0,0 @@ -import computed from 'ember-addons/ember-computed-decorators'; - -export default Ember.Controller.extend({ - adminGroupsType: Ember.inject.controller(), - sortedGroups: Ember.computed.alias("adminGroupsType.sortedGroups"), - - @computed("sortedGroups") - messageKey(sortedGroups) { - return `admin.groups.${sortedGroups.length > 0 ? 'none_selected' : 'no_custom_groups'}`; - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-groups-type.js.es6 b/app/assets/javascripts/admin/controllers/admin-groups-type.js.es6 deleted file mode 100644 index 10d7ad01cf..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-groups-type.js.es6 +++ /dev/null @@ -1,20 +0,0 @@ -import { ajax } from 'discourse/lib/ajax'; -export default Ember.Controller.extend({ - sortedGroups: Ember.computed.sort('model', 'groupSorting'), - groupSorting: ['name'], - - refreshingAutoGroups: false, - - isAuto: Ember.computed.equal('type', 'automatic'), - - actions: { - refreshAutoGroups() { - this.set('refreshingAutoGroups', true); - ajax('/admin/groups/refresh_automatic_groups', {type: 'POST'}).then(() => { - this.transitionToRoute("adminGroupsType", "automatic").then(() => { - this.set('refreshingAutoGroups', false); - }); - }); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 index b80dd29cec..ee49a1c119 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 @@ -24,9 +24,13 @@ export default Ember.Controller.extend(CanCheckEmails, { 'model.can_disable_second_factor' ), - automaticGroups: function() { - return this.get("model.automaticGroups").map((g) => g.name).join(", "); - }.property("model.automaticGroups"), + @computed("model.automaticGroups") + automaticGroups(automaticGroups) { + return automaticGroups.map(group => { + const name = Ember.String.htmlSafe(group.name); + return `${name}`; + }).join(", "); + }, userFields: function() { const siteUserFields = this.site.get('user_fields'), diff --git a/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 b/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 index bd11f15fcf..9e899c4296 100644 --- a/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 @@ -10,11 +10,11 @@ export default Ember.Controller.extend({ return (this.get('adminWatchedWords.model') || []).findBy('nameKey', actionName); }, - @computed('adminWatchedWords.model', 'actionNameKey') - filteredContent() { - if (!this.get('actionNameKey')) { return []; } + @computed('actionNameKey', 'adminWatchedWords.model') + filteredContent(actionNameKey) { + if (!actionNameKey) { return []; } - const a = this.findAction(this.get('actionNameKey')); + const a = this.findAction(actionNameKey); return a ? a.words : []; }, @@ -23,6 +23,12 @@ export default Ember.Controller.extend({ return I18n.t('admin.watched_words.action_descriptions.' + actionNameKey); }, + @computed('actionNameKey', 'adminWatchedWords.model') + wordCount(actionNameKey) { + const a = this.findAction(actionNameKey); + return a ? a.words.length : 0; + }, + actions: { recordAdded(arg) { const a = this.findAction(this.get('actionNameKey')); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 index 7bbdaa3de5..56e43ccbd8 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 @@ -3,9 +3,13 @@ import { ajax } from 'discourse/lib/ajax'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; import { popupAjaxError } from 'discourse/lib/ajax-error'; +const THEME_FIELD_VARIABLE_TYPE_IDS = [2, 3, 4]; + export default Ember.Controller.extend(ModalFunctionality, { adminCustomizeThemesShow: Ember.inject.controller(), + uploadUrl: '/admin/themes/upload_asset', + onShow() { this.set('name', null); this.set('fileSelected', false); @@ -14,9 +18,11 @@ export default Ember.Controller.extend(ModalFunctionality, { enabled: Em.computed.and('nameValid', 'fileSelected'), disabled: Em.computed.not('enabled'), - @computed('name') - nameValid(name) { - return name && name.match(/^[a-z_][a-z0-9_-]*$/i); + @computed('name', 'adminCustomizeThemesShow.model.theme_fields') + nameValid(name, themeFields) { + return name && + name.match(/^[a-z_][a-z0-9_-]*$/i) && + !themeFields.some(tf => THEME_FIELD_VARIABLE_TYPE_IDS.includes(tf.type_id) && name === tf.name); }, @observes('name') @@ -48,7 +54,7 @@ export default Ember.Controller.extend(ModalFunctionality, { options.data.append('file', file); - ajax('/admin/themes/upload_asset', options).then(result => { + ajax(this.get('uploadUrl'), options).then(result => { const upload = { upload_id: result.upload_id, name: this.get('name'), diff --git a/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 index 950df15a84..e1076667b9 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 @@ -9,6 +9,8 @@ export default Ember.Controller.extend(ModalFunctionality, { selection: 'local', adminCustomizeThemes: Ember.inject.controller(), loading: false, + keyGenUrl: '/admin/themes/generate_key_pair', + importUrl: '/admin/themes/import', checkPrivate: Ember.computed.match('uploadUrl', /^git/), @@ -17,7 +19,7 @@ export default Ember.Controller.extend(ModalFunctionality, { const checked = this.get('privateChecked'); if (checked && !this._keyLoading) { this._keyLoading = true; - ajax('/admin/themes/generate_key_pair', {method: 'POST'}) + ajax(this.get('keyGenUrl'), {method: 'POST'}) .then(pair => { this.set('privateKey', pair.private_key); this.set('publicKey', pair.public_key); @@ -52,7 +54,7 @@ export default Ember.Controller.extend(ModalFunctionality, { } this.set('loading', true); - ajax('/admin/themes/import', options).then(result=>{ + ajax(this.get('importUrl'), options).then(result=>{ const theme = this.store.createRecord('theme',result.theme); this.get('adminCustomizeThemes').send('addTheme', theme); this.send('closeModal'); diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6 index af078528b7..3ed442bb0d 100644 --- a/app/assets/javascripts/admin/models/report.js.es6 +++ b/app/assets/javascripts/admin/models/report.js.es6 @@ -2,6 +2,7 @@ import { ajax } from 'discourse/lib/ajax'; import round from "discourse/lib/round"; import { fmt } from 'discourse/lib/computed'; import { fillMissingDates } from 'discourse/lib/utilities'; +import computed from 'ember-addons/ember-computed-decorators'; const Report = Discourse.Model.extend({ reportUrl: fmt("type", "/admin/reports/%@"), @@ -42,7 +43,8 @@ const Report = Discourse.Model.extend({ lastSevenDaysCount: function() { return this.valueFor(1, 7); }.property("data"), lastThirtyDaysCount: function() { return this.valueFor(1, 30); }.property("data"), - yesterdayTrend: function() { + @computed('data') + yesterdayTrend() { const yesterdayVal = this.valueAt(1); const twoDaysAgoVal = this.valueAt(2); if (yesterdayVal > twoDaysAgoVal) { @@ -52,9 +54,10 @@ const Report = Discourse.Model.extend({ } else { return "no-change"; } - }.property("data"), + }, - sevenDayTrend: function() { + @computed('data') + sevenDayTrend() { const currentPeriod = this.valueFor(1, 7); const prevPeriod = this.valueFor(8, 14); if (currentPeriod > prevPeriod) { @@ -64,36 +67,39 @@ const Report = Discourse.Model.extend({ } else { return "no-change"; } - }.property("data"), + }, - thirtyDayTrend: function() { - if (this.get("prev30Days")) { + @computed('prev30Days', 'data') + thirtyDayTrend(prev30Days) { + if (prev30Days) { const currentPeriod = this.valueFor(1, 30); if (currentPeriod > this.get("prev30Days")) { return "trending-up"; - } else if (currentPeriod < this.get("prev30Days")) { + } else if (currentPeriod < prev30Days) { return "trending-down"; } } return "no-change"; - }.property("data", "prev30Days"), + }, - icon: function() { - switch (this.get("type")) { + @computed('type') + icon(type) { + switch (type) { case "flags": return "flag"; case "likes": return "heart"; case "bookmarks": return "bookmark"; default: return null; } - }.property("type"), + }, - method: function() { - if (this.get("type") === "time_to_first_response") { + @computed('type') + method(type) { + if (type === "time_to_first_response") { return "average"; } else { return "sum"; } - }.property("type"), + }, percentChangeString(val1, val2) { const val = ((val1 - val2) / val2) * 100; @@ -114,21 +120,31 @@ const Report = Discourse.Model.extend({ return title; }, - yesterdayCountTitle: function() { + @computed('data') + yesterdayCountTitle() { return this.changeTitle(this.valueAt(1), this.valueAt(2), "two days ago"); - }.property("data"), + }, - sevenDayCountTitle: function() { + @computed('data') + sevenDayCountTitle() { return this.changeTitle(this.valueFor(1, 7), this.valueFor(8, 14), "two weeks ago"); - }.property("data"), + }, - thirtyDayCountTitle: function() { - return this.changeTitle(this.valueFor(1, 30), this.get("prev30Days"), "in the previous 30 day period"); - }.property("data"), + @computed('prev30Days', 'data') + thirtyDayCountTitle(prev30Days) { + return this.changeTitle(this.valueFor(1, 30), prev30Days, "in the previous 30 day period"); + }, - dataReversed: function() { - return this.get("data").toArray().reverse(); - }.property("data") + @computed('data') + sortedData(data) { + return this.get('xAxisIsDate') ? data.toArray().reverse() : data.toArray(); + }, + + @computed('data') + xAxisIsDate() { + if (!this.data[0]) return false; + return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/); + } }); @@ -145,13 +161,21 @@ Report.reopenClass({ }).then(json => { // Add zero values for missing dates if (json.report.data.length > 0) { - const startDateFormatted = moment(json.report.start_date).format('YYYY-MM-DD'); - const endDateFormatted = moment(json.report.end_date).format('YYYY-MM-DD'); + const startDateFormatted = moment(json.report.start_date).utc().format('YYYY-MM-DD'); + const endDateFormatted = moment(json.report.end_date).utc().format('YYYY-MM-DD'); json.report.data = fillMissingDates(json.report.data, startDateFormatted, endDateFormatted); } const model = Report.create({ type: type }); model.setProperties(json.report); + + if (json.report.related_report) { + // TODO: fillMissingDates if xaxis is date + const related = Report.create({ type: json.report.related_report.type }); + related.setProperties(json.report.related_report); + model.set('relatedReport', related); + } + return model; }); } diff --git a/app/assets/javascripts/admin/models/theme.js.es6 b/app/assets/javascripts/admin/models/theme.js.es6 index d2a82defec..d7c5a89fd0 100644 --- a/app/assets/javascripts/admin/models/theme.js.es6 +++ b/app/assets/javascripts/admin/models/theme.js.es6 @@ -2,10 +2,11 @@ import RestModel from 'discourse/models/rest'; import { default as computed } from 'ember-addons/ember-computed-decorators'; const THEME_UPLOAD_VAR = 2; -const FIELDS_IDS = [0, 1]; const Theme = RestModel.extend({ + FIELDS_IDS: [0, 1], + @computed('theme_fields') themeFields(fields) { @@ -16,7 +17,7 @@ const Theme = RestModel.extend({ let hash = {}; fields.forEach(field => { - if (!field.type_id || FIELDS_IDS.includes(field.type_id)) { + if (!field.type_id || this.get('FIELDS_IDS').includes(field.type_id)) { hash[this.getKey(field)] = field; } }); diff --git a/app/assets/javascripts/admin/routes/admin-group.js.es6 b/app/assets/javascripts/admin/routes/admin-group.js.es6 deleted file mode 100644 index 0009f834e1..0000000000 --- a/app/assets/javascripts/admin/routes/admin-group.js.es6 +++ /dev/null @@ -1,24 +0,0 @@ -import Group from 'discourse/models/group'; - -export default Discourse.Route.extend({ - - model(params) { - if (params.name === 'new') { - return Group.create({ automatic: false, visibility_level: 0 }); - } - - const group = this.modelFor('adminGroupsType').findBy('name', params.name); - - if (!group) { return this.transitionTo('adminGroups.index'); } - - return group; - }, - - setupController(controller, model) { - controller.set("model", model); - controller.set("model.usernames", null); - controller.set("savingStatus", ""); - model.findMembers(); - } - -}); diff --git a/app/assets/javascripts/admin/routes/admin-groups-bulk.js.es6 b/app/assets/javascripts/admin/routes/admin-groups-bulk.js.es6 deleted file mode 100644 index 8d9554556f..0000000000 --- a/app/assets/javascripts/admin/routes/admin-groups-bulk.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -import Group from 'discourse/models/group'; - -export default Ember.Route.extend({ - model() { - return Group.findAll().then(groups => { - return groups.filter(g => !g.get('automatic')); - }); - }, - - setupController(controller, groups) { - controller.setProperties({ groups, groupId: null, users: null }); - } -}); diff --git a/app/assets/javascripts/admin/routes/admin-groups-index.js.es6 b/app/assets/javascripts/admin/routes/admin-groups-index.js.es6 deleted file mode 100644 index e650d150fb..0000000000 --- a/app/assets/javascripts/admin/routes/admin-groups-index.js.es6 +++ /dev/null @@ -1,5 +0,0 @@ -export default Discourse.Route.extend({ - redirect: function() { - this.transitionTo("adminGroupsType", "custom"); - } -}); diff --git a/app/assets/javascripts/admin/routes/admin-groups-type.js.es6 b/app/assets/javascripts/admin/routes/admin-groups-type.js.es6 deleted file mode 100644 index 52e383bf9a..0000000000 --- a/app/assets/javascripts/admin/routes/admin-groups-type.js.es6 +++ /dev/null @@ -1,15 +0,0 @@ -import Group from 'discourse/models/group'; - -export default Discourse.Route.extend({ - model(params) { - this.set("type", params.type); - return Group.findAll().then(function(groups) { - return groups.filterBy("type", params.type); - }); - }, - - setupController(controller, model){ - controller.set("type", this.get("type")); - controller.set("model", model); - } -}); diff --git a/app/assets/javascripts/admin/routes/admin-reports.js.es6 b/app/assets/javascripts/admin/routes/admin-reports.js.es6 index 65771110e8..acddcff5d6 100644 --- a/app/assets/javascripts/admin/routes/admin-reports.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-reports.js.es6 @@ -12,8 +12,8 @@ export default Discourse.Route.extend({ model: model, categoryId: (model.get('category_id') || 'all'), groupId: model.get('group_id'), - startDate: moment(model.get('start_date')).format('YYYY-MM-DD'), - endDate: moment(model.get('end_date')).format('YYYY-MM-DD') + startDate: moment(model.get('start_date')).utc().format('YYYY-MM-DD'), + endDate: moment(model.get('end_date')).utc().format('YYYY-MM-DD') }); } }); diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 index a6ccabc810..b4926b9a5a 100644 --- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 @@ -76,14 +76,6 @@ export default function() { }); }); - this.route('adminGroups', { path: '/groups', resetNamespace: true }, function() { - this.route('bulk'); - this.route('bulkComplete', { path: 'bulk-complete' }); - this.route('adminGroupsType', { path: '/:type', resetNamespace: true }, function() { - this.route('adminGroup', { path: '/:name', resetNamespace: true }); - }); - }); - this.route('adminUsers', { path: '/users', resetNamespace: true }, function() { this.route('adminUser', { path: '/:user_id/:username', resetNamespace: true }, function() { this.route('badges'); diff --git a/app/assets/javascripts/admin/templates/admin.hbs b/app/assets/javascripts/admin/templates/admin.hbs index 7cea833c78..092133b7c1 100644 --- a/app/assets/javascripts/admin/templates/admin.hbs +++ b/app/assets/javascripts/admin/templates/admin.hbs @@ -12,7 +12,6 @@ {{nav-item route='adminBadges' label='admin.badges.title'}} {{/if}} {{#if currentUser.admin}} - {{nav-item route='adminGroups' label='admin.groups.title'}} {{nav-item route='adminEmail' label='admin.email.title'}} {{/if}} {{nav-item route='adminFlags' label='admin.flags.title'}} diff --git a/app/assets/javascripts/admin/templates/components/admin-table-report.hbs b/app/assets/javascripts/admin/templates/components/admin-table-report.hbs new file mode 100644 index 0000000000..53ea2e51c6 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/admin-table-report.hbs @@ -0,0 +1,17 @@ +{{#if model.sortedData}} + + + + + + + {{#each model.sortedData as |row|}} + + + + + {{/each}} +
{{model.xaxis}}{{model.yaxis}}
{{row.x}} + {{row.y}} +
+{{/if}} diff --git a/app/assets/javascripts/admin/templates/components/flagged-post-title.hbs b/app/assets/javascripts/admin/templates/components/flagged-post-title.hbs index 161a8f9f92..0871c5c6f1 100644 --- a/app/assets/javascripts/admin/templates/components/flagged-post-title.hbs +++ b/app/assets/javascripts/admin/templates/components/flagged-post-title.hbs @@ -3,3 +3,6 @@ {{/if}} {{topic-status topic=flaggedPost.topic}} {{{unbound flaggedPost.topic.fancyTitle}}} +{{#if flaggedPost.reply_count}} + {{i18n 'admin.flags.replies' count=flaggedPost.reply_count}} +{{/if}} diff --git a/app/assets/javascripts/admin/templates/group.hbs b/app/assets/javascripts/admin/templates/group.hbs deleted file mode 100644 index c10ca20bb3..0000000000 --- a/app/assets/javascripts/admin/templates/group.hbs +++ /dev/null @@ -1,173 +0,0 @@ -
- -
- {{#if model.automatic}} -

{{model.name}}

- {{else}} - - {{text-field name="name" value=model.name placeholderKey="groups.name_placeholder"}} - {{/if}} -
- - {{#unless model.automatic}} -
- - {{input type='text' name='full_name' value=model.full_name class='group-edit-full-name'}} -
- -
- - {{d-editor value=model.bio_raw}} -
- - {{#if model.hasOwners}} -
- -
- {{#each model.owners as |member|}} - {{group-member member=member removeAction="removeOwner"}} - {{/each}} -
-
- {{/if}} - -
- - - {{user-selector usernames=model.ownerUsernames - placeholderKey="groups.selector_placeholder" - id="owner-selector"}} - - {{#if model.id}} - {{d-button - action="addOwners" - class="add" - icon="plus" - label="admin.groups.add"}} - {{/if}} -
- {{/unless}} - -
- {{group-members-input model=model addButton=model.id}} -
- -
- - {{combo-box name="alias" - valueAttribute="value" - value=model.visibility_level - content=visibilityLevelOptions - castInteger=true}} -
- - {{#unless model.automatic}} -
- -
- -
- -
- -
- -
- - {{#if model.allow_membership_requests}} -
- - - {{expanding-text-area name="membership-request-template" - value=model.membership_request_template}} -
- {{/if}} - -
- -
- {{/unless}} - -
- - {{combo-box name="alias" valueAttribute="value" value=model.mentionable_level content=aliasLevelOptions}} -
- -
- - {{combo-box name="alias" valueAttribute="value" value=model.messageable_level content=aliasLevelOptions}} -
- -
- - {{notifications-button i18nPrefix='groups.notifications' value=model.default_notification_level}} -
-
- - {{#unless model.automatic}} -
- - {{list-setting name="automatic_membership" settingValue=model.emailDomains}} - -
- -
- - {{input value=model.title}} -
- -
- - {{combo-box name="grant_trust_level" valueAttribute="value" value=model.grant_trust_level content=trustLevelOptions}} -
- - {{#if siteSettings.email_in}} - - {{text-field name="incoming_email" value=model.incoming_email placeholderKey="admin.groups.incoming_email_placeholder"}} - {{plugin-outlet name="group-email-in" args=(hash model=model)}} - {{/if}} - {{/unless}} - - {{#unless model.automatic}} - {{group-flair-inputs model=model}} - {{/unless}} - - {{plugin-outlet name="group-edit" args=(hash group=model)}} - -
- - {{#unless model.automatic}} - - {{/unless}} - - {{savingStatus}} -
- -
diff --git a/app/assets/javascripts/admin/templates/groups-bulk-complete.hbs b/app/assets/javascripts/admin/templates/groups-bulk-complete.hbs deleted file mode 100644 index 6a2d8b13e5..0000000000 --- a/app/assets/javascripts/admin/templates/groups-bulk-complete.hbs +++ /dev/null @@ -1,11 +0,0 @@ -{{#if bulkAddResponse}} -

{{{bulkAddResponse.message}}}

- {{#if bulkAddResponse.users_not_added}} -

{{i18n "admin.groups.bulk_complete_users_not_added"}}

- {{#each bulkAddResponse.users_not_added as |user|}} - {{user}}
- {{/each}} - {{/if}} -{{else}} -

{{i18n "admin.groups.bulk_complete"}}

-{{/if}} diff --git a/app/assets/javascripts/admin/templates/groups-bulk.hbs b/app/assets/javascripts/admin/templates/groups-bulk.hbs deleted file mode 100644 index 337ab37cb3..0000000000 --- a/app/assets/javascripts/admin/templates/groups-bulk.hbs +++ /dev/null @@ -1,19 +0,0 @@ -
-

{{i18n "admin.groups.bulk_paste"}}

- -
- {{textarea value=users class="paste-users"}} -
- -
- {{combo-box filterable=true content=groups value=groupId none="admin.groups.bulk_select"}} -
- -
- {{d-button disabled=buttonDisabled - class="btn-primary" - action="addToGroup" - icon="plus" - label="admin.groups.bulk"}} -
-
diff --git a/app/assets/javascripts/admin/templates/groups-type-index.hbs b/app/assets/javascripts/admin/templates/groups-type-index.hbs deleted file mode 100644 index 196ded96d2..0000000000 --- a/app/assets/javascripts/admin/templates/groups-type-index.hbs +++ /dev/null @@ -1,9 +0,0 @@ -
-

{{i18n messageKey}}

- -
- {{#link-to 'adminGroup' 'new' class="btn"}} - {{d-icon "plus"}} {{i18n 'admin.groups.new'}} - {{/link-to}} -
-
diff --git a/app/assets/javascripts/admin/templates/groups-type.hbs b/app/assets/javascripts/admin/templates/groups-type.hbs deleted file mode 100644 index 47bbb1c51e..0000000000 --- a/app/assets/javascripts/admin/templates/groups-type.hbs +++ /dev/null @@ -1,31 +0,0 @@ -
- {{#if sortedGroups}} -
-

{{i18n 'admin.groups.edit'}}

-
    - {{#each sortedGroups as |group|}} -
  • - {{#link-to "adminGroup" group.type group.name}}{{group.name}} - {{#if group.userCountDisplay}} - {{number group.userCountDisplay}} - {{/if}} - {{/link-to}} -
  • - {{/each}} -
-
- {{#if isAuto}} - {{d-button action="refreshAutoGroups" icon="refresh" label="admin.groups.refresh" disabled=refreshingAutoGroups}} - {{else}} - {{#link-to 'adminGroup' 'new' class="btn"}} - {{d-icon "plus"}} {{i18n 'admin.groups.new'}} - {{/link-to}} - {{/if}} -
-
- {{/if}} - -
- {{outlet}} -
-
diff --git a/app/assets/javascripts/admin/templates/groups.hbs b/app/assets/javascripts/admin/templates/groups.hbs deleted file mode 100644 index aa7d9213ca..0000000000 --- a/app/assets/javascripts/admin/templates/groups.hbs +++ /dev/null @@ -1,9 +0,0 @@ -{{#admin-nav}} - {{nav-item route='adminGroupsType' routeParam='custom' label='admin.groups.custom'}} - {{nav-item route='adminGroupsType' routeParam='automatic' label='admin.groups.automatic'}} - {{nav-item route='adminGroups.bulk' label='admin.groups.bulk'}} -{{/admin-nav}} - -
- {{outlet}} -
diff --git a/app/assets/javascripts/admin/templates/plugins-index.hbs b/app/assets/javascripts/admin/templates/plugins-index.hbs index b7d64f2a34..dc5405471b 100644 --- a/app/assets/javascripts/admin/templates/plugins-index.hbs +++ b/app/assets/javascripts/admin/templates/plugins-index.hbs @@ -14,6 +14,7 @@ + @@ -23,6 +24,14 @@ {{#each model as |plugin|}} + + + - diff --git a/app/assets/javascripts/discourse/templates/components/group-manage-save-button.hbs b/app/assets/javascripts/discourse/templates/components/group-manage-save-button.hbs new file mode 100644 index 0000000000..0276ac16e5 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/group-manage-save-button.hbs @@ -0,0 +1,12 @@ +
+ {{#d-button action="save" + disabled=saving + class='btn btn-primary group-manage-save'}} + + {{savingText}} + {{/d-button}} + + {{#if saved}} + {{i18n 'saved'}} + {{/if}} +
diff --git a/app/assets/javascripts/discourse/templates/components/group-members-input.hbs b/app/assets/javascripts/discourse/templates/components/group-members-input.hbs index fc0e9667b8..036bf9b1ca 100644 --- a/app/assets/javascripts/discourse/templates/components/group-members-input.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-members-input.hbs @@ -24,7 +24,7 @@ class="add" icon="plus" disabled=disableAddButton - label="groups.edit.add_members"}} + label="groups.manage.add_members"}} {{/if}} {{/unless}} diff --git a/app/assets/javascripts/discourse/templates/components/group-membership-button.hbs b/app/assets/javascripts/discourse/templates/components/group-membership-button.hbs index 435f77fa52..2bd1956492 100644 --- a/app/assets/javascripts/discourse/templates/components/group-membership-button.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-membership-button.hbs @@ -11,21 +11,9 @@ label="groups.leave" disabled=updatingMembership}} {{else if model.allow_membership_requests}} - {{#if userIsGroupUser}} - {{#if showMembershipStatus}} - {{d-button - class="btn-primary" - icon="user" - label="groups.is_group_user" - disabled=true}} - {{/if}} - {{else}} - {{d-button action="showRequestMembershipForm" - class="group-index-request" - disabled=loading - icon="user-plus" - label="groups.request"}} - {{/if}} -{{else}} - {{yield}} + {{d-button action="showRequestMembershipForm" + class="group-index-request" + disabled=loading + icon="user-plus" + label="groups.request"}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/group-navigation.hbs b/app/assets/javascripts/discourse/templates/components/group-navigation.hbs index b97cdfa040..29ad6b7f1e 100644 --- a/app/assets/javascripts/discourse/templates/components/group-navigation.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-navigation.hbs @@ -1,4 +1,14 @@ {{#mobile-nav class='group-nav' desktopClass="nav nav-pills" currentPath=currentPath}} + {{#if site.mobileView}} +
  • + {{#link-to "groups.index"}} + {{i18n "groups.index.all"}} + {{/link-to}} +
  • + {{else}} + {{group-dropdown content=group.extras.visible_group_names value=group.name}} + {{/if}} + {{#each tabs as |tab|}}
  • {{#link-to tab.route group title=tab.message class=tab.name}} diff --git a/app/assets/javascripts/discourse/templates/components/groups-form-interaction-fields.hbs b/app/assets/javascripts/discourse/templates/components/groups-form-interaction-fields.hbs new file mode 100644 index 0000000000..5ed87a5c02 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/groups-form-interaction-fields.hbs @@ -0,0 +1,58 @@ +{{#if currentUser.admin}} +
    + + + + {{combo-box name="alias" + valueAttribute="value" + value=model.visibility_level + content=visibilityLevelOptions + castInteger=true + class="groups-form-visibility-level"}} +
    +{{/if}} + +
    + + + + {{combo-box name="alias" + valueAttribute="value" + value=model.mentionable_level + content=aliasLevelOptions + class="groups-form-mentionable-level"}} +
    + +
    + + + {{combo-box name="alias" + valueAttribute="value" + value=model.messageable_level + content=aliasLevelOptions + class="groups-form-messageable-level"}} +
    + +{{#if showEmailSettings}} +
    + + + + {{text-field name="incoming_email" + class="input-xxlarge groups-form-incoming-email" + value=model.incoming_email + placeholderKey="admin.groups.manage.interaction.incoming_email_placeholder"}} + + {{plugin-outlet name="group-email-in" args=(hash model=model)}} +
    +{{/if}} + + + +
    + + + {{notifications-button i18nPrefix='groups.notifications' + value=model.default_notification_level + class="groups-form-default-notification-level"}} +
    diff --git a/app/assets/javascripts/discourse/templates/components/groups-form-membership-fields.hbs b/app/assets/javascripts/discourse/templates/components/groups-form-membership-fields.hbs new file mode 100644 index 0000000000..03f5755d8f --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/groups-form-membership-fields.hbs @@ -0,0 +1,82 @@ +{{#if currentUser.admin}} +
    + + + + + {{list-setting name="automatic_membership" + settingValue=model.emailDomains + class="group-form-automatic-membership-automatic"}} + + + + +
    + +
    + + + + {{combo-box name="grant_trust_level" + valueAttribute="value" + value=model.grant_trust_level + content=trustLevelOptions + class="groups-form-grant-trust-level"}} +
    +{{/if}} + +
    + + + + + + + + + {{#if model.allow_membership_requests}} +
    + + + {{expanding-text-area name="membership-request-template" + class='group-form-membership-request-template input-xxlarge' + value=model.membership_request_template}} +
    + {{/if}} +
    diff --git a/app/assets/javascripts/discourse/templates/components/groups-form-profile-fields.hbs b/app/assets/javascripts/discourse/templates/components/groups-form-profile-fields.hbs new file mode 100644 index 0000000000..81de113225 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/groups-form-profile-fields.hbs @@ -0,0 +1,47 @@ +{{#if this.currentUser.admin}} +
    + + + {{text-field name="name" + class="input-xxlarge group-form-name" + value=nameInput + placeholderKey="admin.groups.name_placeholder"}} + + {{input-tip validation=nameValidation}} +
    +{{/if}} + +
    + + + {{text-field name='full_name' + class="input-xxlarge group-form-full-name" + value=model.full_name}} +
    + +{{#if this.currentUser.admin}} +
    + + + {{input value=model.title name="title" class="input-xxlarge"}} + +
    + {{i18n 'admin.groups.default_title_description'}} +
    +
    +{{/if}} + +
    + + {{d-editor value=model.bio_raw class="group-form-bio input-xxlarge"}} +
    + +{{yield}} + +
    + {{group-flair-inputs model=model}} +
    + +{{plugin-outlet name="group-edit" args=(hash group=model)}} diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index e07a6eddf8..204c01ac72 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -9,7 +9,6 @@ {{composer-messages composer=model messageCount=messageCount addLinkLookup="addLinkLookup"}} - {{#if model.viewOpen}}
    @@ -65,7 +64,8 @@
    {{/if}} {{#if canEditTags}} - {{mini-tag-chooser tags=model.tags tabindex="4" categoryId=model.categoryId}} + {{mini-tag-chooser tags=model.tags tabindex="4" categoryId=model.categoryId minimum=model.minimumRequiredTags}} + {{popup-input-tip validation=tagValidation}} {{/if}} @@ -105,11 +105,18 @@ label=model.saveLabel disableSubmit=disableSubmit}} {{#if site.mobileView}} - {{d-icon "trash-o"}} + + {{#if canEdit}} + {{d-icon "times"}} + {{else}} + {{d-icon "trash-o"}} + {{/if}} + {{else}} {{i18n 'cancel'}} {{/if}} + {{#if site.mobileView}} {{#if whisperOrUnlistTopic}} diff --git a/app/assets/javascripts/discourse/templates/full-page-search.hbs b/app/assets/javascripts/discourse/templates/full-page-search.hbs index 01edd14023..68d2869183 100644 --- a/app/assets/javascripts/discourse/templates/full-page-search.hbs +++ b/app/assets/javascripts/discourse/templates/full-page-search.hbs @@ -1,163 +1,198 @@ {{#d-section pageClass="search" class="search-container"}} - {{scroll-tracker name="full-page-search" tag=searchTerm}} -
  • {{i18n "admin.plugins.name"}} {{i18n "admin.plugins.version"}} {{i18n "admin.plugins.enabled"}}
    + {{#if plugin.is_official}} + {{d-icon "check-circle" + title="admin.plugins.official" + class="admin-plugins-official-badge"}} + {{/if}} + {{#if plugin.url}} {{plugin.name}} @@ -58,4 +67,3 @@ {{/if}}

    {{i18n "admin.plugins.howto"}}

    - diff --git a/app/assets/javascripts/admin/templates/reports.hbs b/app/assets/javascripts/admin/templates/reports.hbs index 3927d1e384..1a6b4bf047 100644 --- a/app/assets/javascripts/admin/templates/reports.hbs +++ b/app/assets/javascripts/admin/templates/reports.hbs @@ -31,20 +31,10 @@ {{#if viewingGraph}} {{admin-graph model=model}} {{else}} - - - - - + {{admin-table-report model=model}} + {{/if}} - {{#each model.dataReversed as |row|}} - - - - - {{/each}} -
    {{model.xaxis}}{{model.yaxis}}
    {{row.x}} - {{row.y}} -
    + {{#if model.relatedReport}} + {{admin-table-report model=model.relatedReport}} {{/if}} {{/conditional-loading-spinner}} diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs index a09048c759..b1e164a01b 100644 --- a/app/assets/javascripts/admin/templates/user-index.hbs +++ b/app/assets/javascripts/admin/templates/user-index.hbs @@ -56,6 +56,8 @@ + {{plugin-outlet name="admin-user-below-names" args=(hash user=model) tagName='' connectorTagName=''}} + {{#if canCheckEmails}}
    {{#d-button class="btn-small" action="filter" actionParam=(hash value=log.action key="action")}} {{log.actionTitle}} @@ -33,7 +33,7 @@ {{bound-date log.created_at}} + {{#if log.prev_value}} {{#if expandDetails}} {{d-icon 'ellipsis-v'}} @@ -48,11 +48,11 @@

    - {{i18n 'groups.logs.from'}}: {{log.prev_value}} + {{i18n 'groups.manage.logs.from'}}: {{log.prev_value}}

    - {{i18n 'groups.logs.to'}}: {{log.new_value}} + {{i18n 'groups.manage.logs.to'}}: {{log.new_value}}

    {{group-index-toggle order=order desc=desc field='username_lower' i18nKey='username'}} + {{group-index-toggle order=order desc=desc field='last_posted_at' i18nKey='last_post'}} {{group-index-toggle order=order desc=desc field='last_seen_at' i18nKey='last_seen'}} @@ -16,10 +31,17 @@ {{#each model.members as |m|}} + + + diff --git a/app/assets/javascripts/discourse/templates/group-logs.hbs b/app/assets/javascripts/discourse/templates/group-logs.hbs deleted file mode 100644 index 154b78d678..0000000000 --- a/app/assets/javascripts/discourse/templates/group-logs.hbs +++ /dev/null @@ -1,33 +0,0 @@ -{{#if model.logs}} -
    - {{group-logs-filter clearFilter="clearFilter" value=filters.action type="action"}} - {{group-logs-filter clearFilter="clearFilter" value=filters.acting_user type="acting_user"}} - {{group-logs-filter clearFilter="clearFilter" value=filters.target_user type="target_user"}} - {{group-logs-filter clearFilter="clearFilter" value=filters.subject type="subject"}} -
    - - {{#load-more selector=".group-logs .group-logs-row" action="loadMore"}} -
    {{i18n "groups.members.owner"}}
    - {{#user-info user=m skipName=skipName}} - {{#if m.owner}}{{i18n "groups.owner"}}{{/if}} - {{/user-info}} + {{user-info user=m skipName=skipName}} + {{#if m.owner}} + + {{d-icon "shield"}} + + {{/if}} + {{bound-date m.last_posted_at}}
    - - - - - - - - - - - {{#each model.logs as |log|}} - {{group-logs-row - log=log - filters=filters}} - {{/each}} - -
    {{i18n 'groups.logs.action'}}{{i18n 'groups.logs.acting_user'}}{{i18n 'groups.logs.target_user'}}{{i18n 'groups.logs.subject'}}{{i18n 'groups.logs.when'}}
    - {{/load-more}} - - {{conditional-loading-spinner condition=loading}} -{{else}} -
    {{i18n "groups.empty.logs"}}
    -{{/if}} diff --git a/app/assets/javascripts/discourse/templates/group.hbs b/app/assets/javascripts/discourse/templates/group.hbs index 6fc009b830..53fb6bf976 100644 --- a/app/assets/javascripts/discourse/templates/group.hbs +++ b/app/assets/javascripts/discourse/templates/group.hbs @@ -1,12 +1,5 @@ {{plugin-outlet name="before-group-container" args=(hash group=model)}} -{{#link-to "groups"}} - {{d-icon 'arrow-left'}} - {{i18n "groups.index.title"}} -{{/link-to}} - -
    -
    @@ -36,27 +29,34 @@

    {{{model.bio_cooked}}}

    {{/if}} + +
    + {{group-membership-button + class="inline" + model=model + showLogin='showLogin'}} + + {{#if displayGroupMessageButton}} + {{d-button + action="messageGroup" + class="btn-primary group-message-button inline" + icon="envelope" + label="groups.message"}} + {{/if}} + + {{#if currentUser.admin}} + {{d-button action="destroy" + disabled=destroying + icon="trash" + class='btn-danger' + label="admin.groups.delete"}} + {{/if}} +
    {{group-navigation group=model currentPath=application.currentPath tabs=tabs}} - - {{#if canManageGroup}} - {{group-navigation-dropdown - model=model - manageMembership=(route-action "showGroupMembershipModal")}} - {{/if}} - - {{#if displayGroupMessageButton}} - {{d-button - action="messageGroup" - class="btn-primary group-message-button" - icon="envelope" - label="groups.message"}} - {{/if}} - - {{group-membership-button model=model showLogin='showLogin'}}
    diff --git a/app/assets/javascripts/discourse/templates/group/activity.hbs b/app/assets/javascripts/discourse/templates/group/activity.hbs index 957c08e939..06dd803657 100644 --- a/app/assets/javascripts/discourse/templates/group/activity.hbs +++ b/app/assets/javascripts/discourse/templates/group/activity.hbs @@ -1,5 +1,5 @@
    - {{#mobile-nav class='group-activity-nav' desktopClass="pull-left nav nav-stacked" currentPath=application.currentPath}} + {{#mobile-nav class='group-activity-nav group-navigation' desktopClass="pull-left nav nav-stacked" currentPath=application.currentPath}} {{group-activity-filter filter="posts" categoryId=category_id}} {{group-activity-filter filter="topics" categoryId=category_id}} {{#if siteSettings.enable_mentions}} diff --git a/app/assets/javascripts/discourse/templates/group/manage.hbs b/app/assets/javascripts/discourse/templates/group/manage.hbs new file mode 100644 index 0000000000..0f1e4c9f88 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/group/manage.hbs @@ -0,0 +1,15 @@ +
    + {{#mobile-nav class='group-navigation' desktopClass='pull-left nav nav-stacked' currentPath=application.currentPath}} + {{#each tabs as |tab|}} +
  • + {{#link-to tab.route model.name}} + {{i18n tab.title}} + {{/link-to}} +
  • + {{/each}} + {{/mobile-nav}} + +
    + {{outlet}} +
    +
    diff --git a/app/assets/javascripts/discourse/templates/group/manage/interaction.hbs b/app/assets/javascripts/discourse/templates/group/manage/interaction.hbs new file mode 100644 index 0000000000..8879b9000c --- /dev/null +++ b/app/assets/javascripts/discourse/templates/group/manage/interaction.hbs @@ -0,0 +1,4 @@ +
    + {{groups-form-interaction-fields model=model}} + {{group-manage-save-button model=model}} +
    diff --git a/app/assets/javascripts/discourse/templates/group/manage/logs.hbs b/app/assets/javascripts/discourse/templates/group/manage/logs.hbs new file mode 100644 index 0000000000..d21a3a98ae --- /dev/null +++ b/app/assets/javascripts/discourse/templates/group/manage/logs.hbs @@ -0,0 +1,33 @@ +{{#if model.logs}} +
    + {{group-manage-logs-filter clearFilter="clearFilter" value=filters.action type="action"}} + {{group-manage-logs-filter clearFilter="clearFilter" value=filters.acting_user type="acting_user"}} + {{group-manage-logs-filter clearFilter="clearFilter" value=filters.target_user type="target_user"}} + {{group-manage-logs-filter clearFilter="clearFilter" value=filters.subject type="subject"}} +
    + + {{#load-more selector=".group-manage-logs .group-manage-logs-row" action="loadMore"}} + + + + + + + + + + + + {{#each model.logs as |log|}} + {{group-manage-logs-row + log=log + filters=filters}} + {{/each}} + +
    {{i18n 'groups.manage.logs.action'}}{{i18n 'groups.manage.logs.acting_user'}}{{i18n 'groups.manage.logs.target_user'}}{{i18n 'groups.manage.logs.subject'}}{{i18n 'groups.manage.logs.when'}}
    + {{/load-more}} + + {{conditional-loading-spinner condition=loading}} +{{else}} +
    {{i18n "groups.empty.logs"}}
    +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/group/manage/membership.hbs b/app/assets/javascripts/discourse/templates/group/manage/membership.hbs new file mode 100644 index 0000000000..bb0a12982f --- /dev/null +++ b/app/assets/javascripts/discourse/templates/group/manage/membership.hbs @@ -0,0 +1,4 @@ +
    + {{groups-form-membership-fields model=model}} + {{group-manage-save-button model=model}} +
    diff --git a/app/assets/javascripts/discourse/templates/group/manage/profile.hbs b/app/assets/javascripts/discourse/templates/group/manage/profile.hbs new file mode 100644 index 0000000000..4c4ab73ba1 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/group/manage/profile.hbs @@ -0,0 +1,4 @@ +
    + {{groups-form-profile-fields model=model disableSave=saving}} + {{group-manage-save-button model=model saving=saving}} +
    diff --git a/app/assets/javascripts/discourse/templates/group/messages.hbs b/app/assets/javascripts/discourse/templates/group/messages.hbs index 5d86ab3df4..923a6017d0 100644 --- a/app/assets/javascripts/discourse/templates/group/messages.hbs +++ b/app/assets/javascripts/discourse/templates/group/messages.hbs @@ -1,5 +1,6 @@
    - {{#mobile-nav class='group-messages-nav' desktopClass='pull-left nav nav-stacked' currentPath=application.currentPath}} + {{#mobile-nav class='group-navigation' desktopClass='pull-left nav nav-stacked' currentPath=application.currentPath}} +
  • {{#link-to 'group.messages.inbox' model.name}} {{i18n 'user.messages.inbox'}} diff --git a/app/assets/javascripts/discourse/templates/groups.hbs b/app/assets/javascripts/discourse/templates/groups/index.hbs similarity index 55% rename from app/assets/javascripts/discourse/templates/groups.hbs rename to app/assets/javascripts/discourse/templates/groups/index.hbs index 2bfc5d1f5a..d4f9a95dc2 100644 --- a/app/assets/javascripts/discourse/templates/groups.hbs +++ b/app/assets/javascripts/discourse/templates/groups/index.hbs @@ -1,16 +1,24 @@ {{#d-section pageClass="groups"}} -

    {{i18n "groups.index.title"}}

    +
    + {{#if currentUser.admin}} + {{d-button action="new" + class="groups-header-new pull-right" + icon="plus" + label="admin.groups.new.title"}} + {{/if}} -
    - {{combo-box value=type - content=types - clearable=true - none="groups.index.all_groups" - class="groups-type-filter"}} +
    + {{text-field value=filterInput + placeholderKey="groups.index.all_groups" + class="groups-header-filters-name no-blur"}} - {{text-field value=filterInput - placeholderKey="groups.filter_name" - class="groups-name-filter no-blur"}} + {{combo-box value=type + content=types + clearable=true + allowAutoSelectFirst=false + noneLabel="groups.index.filter" + class="groups-header-filters-type"}} +
    {{#if model}} @@ -19,10 +27,10 @@
    - + {{directory-toggle field="name" labelKey="groups.group_name" order=order asc=asc}} {{directory-toggle field="user_count" labelKey="groups.user_count" order=order asc=asc}} + - @@ -58,25 +66,33 @@ - - {{/each}} diff --git a/app/assets/javascripts/discourse/templates/groups/new.hbs b/app/assets/javascripts/discourse/templates/groups/new.hbs new file mode 100644 index 0000000000..2501e0be9f --- /dev/null +++ b/app/assets/javascripts/discourse/templates/groups/new.hbs @@ -0,0 +1,41 @@ +{{#d-section pageClass="groups-new"}} +

    {{i18n "admin.groups.new.title"}}

    + +
    + + + {{#groups-form-profile-fields model=model disableSave=saving}} +
    + + + {{user-selector usernames=model.ownerUsernames + class="input-xxlarge" + placeholderKey="groups.selector_placeholder" + id="owner-selector"}} +
    + +
    + + + {{user-selector usernames=model.usernames + class="input-xxlarge" + placeholderKey="groups.selector_placeholder" + id="member-selector"}} +
    + {{/groups-form-profile-fields}} + + {{groups-form-membership-fields model=model}} + {{groups-form-interaction-fields model=model}} + +
    + {{d-button action="save" + disabled=saving + class='btn btn-primary group-form-save' + label="admin.groups.new.create"}} + + {{#link-to "groups"}} + {{i18n 'cancel'}} + {{/link-to}} +
    + +{{/d-section}} diff --git a/app/assets/javascripts/discourse/templates/list/activity-column.raw.hbs b/app/assets/javascripts/discourse/templates/list/activity-column.raw.hbs index 56a994a551..5bb3d82280 100644 --- a/app/assets/javascripts/discourse/templates/list/activity-column.raw.hbs +++ b/app/assets/javascripts/discourse/templates/list/activity-column.raw.hbs @@ -1 +1 @@ -<{{tagName}} class="{{class}} {{cold-age-class topic.createdAt startDate=topic.bumpedAt class=""}} activity" title="{{{topic.bumpedAtTitle}}}">{{format-date topic.bumpedAt format="tiny" noTitle="true"}} +<{{tagName}} class="{{class}} {{cold-age-class topic.createdAt startDate=topic.bumpedAt class=""}} activity" title="{{{topic.bumpedAtTitle}}}">{{format-date topic.bumpedAt format="tiny" noTitle="true"}} diff --git a/app/assets/javascripts/discourse/templates/list/post-count-or-badges.raw.hbs b/app/assets/javascripts/discourse/templates/list/post-count-or-badges.raw.hbs index 4e41ba326b..f997dcbff0 100644 --- a/app/assets/javascripts/discourse/templates/list/post-count-or-badges.raw.hbs +++ b/app/assets/javascripts/discourse/templates/list/post-count-or-badges.raw.hbs @@ -1,5 +1,5 @@ {{#if view.showBadges}} - {{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl}} + {{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl newDotText=newDotText}} {{else}} {{raw "list/posts-count-column" topic=topic tagName="div"}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs b/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs index 942e3f8d82..2831237bc5 100644 --- a/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs +++ b/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs @@ -14,7 +14,7 @@ {{/if}} {{raw-plugin-outlet name="topic-list-after-title"}} {{#if showTopicPostBadges}} - {{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl}} + {{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl newDotText=newDotText}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/mobile/group-index.hbs b/app/assets/javascripts/discourse/templates/mobile/group-index.hbs index cc0751fc36..b1c05322a1 100644 --- a/app/assets/javascripts/discourse/templates/mobile/group-index.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/group-index.hbs @@ -2,6 +2,20 @@ placeholderKey=filterPlaceholder class="group-username-filter no-blur"}} +
    + {{#if canManageGroup}} + {{#if currentUser.admin}} + {{group-members-dropdown + showAddMembersModal="showAddMembersModal" + showBulkAddModal="showBulkAddModal"}} + {{else}} + {{d-button icon="plus" + label="groups.add_members.title" + class="group-members-add"}} + {{/if}} + {{/if}} +
    + {{#if hasMembers}} {{#load-more selector=".group-members .user-info" action="loadMore"}}
    diff --git a/app/assets/javascripts/discourse/templates/modal/create-account.hbs b/app/assets/javascripts/discourse/templates/modal/create-account.hbs index 483a42827d..c2cfea754d 100644 --- a/app/assets/javascripts/discourse/templates/modal/create-account.hbs +++ b/app/assets/javascripts/discourse/templates/modal/create-account.hbs @@ -79,7 +79,7 @@
    diff --git a/app/assets/javascripts/discourse/templates/modal/forgot-password.hbs b/app/assets/javascripts/discourse/templates/modal/forgot-password.hbs index dd24392734..f2d00aac48 100644 --- a/app/assets/javascripts/discourse/templates/modal/forgot-password.hbs +++ b/app/assets/javascripts/discourse/templates/modal/forgot-password.hbs @@ -12,7 +12,7 @@ {{d-button action="resetPassword" label="forgot_password.reset" disabled=submitDisabled - class="btn-primary"}} + class="btn-primary forgot-password-reset"}} {{else}} {{d-button class="btn-large btn-primary" label="forgot_password.button_ok" diff --git a/app/assets/javascripts/discourse/templates/modal/group-add-members.hbs b/app/assets/javascripts/discourse/templates/modal/group-add-members.hbs new file mode 100644 index 0000000000..59db581299 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/modal/group-add-members.hbs @@ -0,0 +1,35 @@ +{{#d-modal-body title="groups.add_members.title"}} + +
    + + + {{user-selector + class="input-xxlarge" + usernames=model.usernames + placeholderKey="groups.selector_placeholder" + id="group-add-members-user-selector"}} +
    + + {{#if currentUser.admin}} +
    + +
    + {{/if}} + +{{/d-modal-body}} + + diff --git a/app/assets/javascripts/discourse/templates/modal/group-bulk-add.hbs b/app/assets/javascripts/discourse/templates/modal/group-bulk-add.hbs new file mode 100644 index 0000000000..887e5a9011 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/modal/group-bulk-add.hbs @@ -0,0 +1,38 @@ +{{#d-modal-body title="admin.groups.bulk_add.title"}} + {{#if result}} + {{#if result.message}} +
    + {{result.message}} +
    + {{/if}} + + {{#if result.invalidUsers}} +
    + {{i18n "admin.groups.bulk_add.complete_users_not_added"}} + {{result.invalidUsers}} +
    + {{/if}} + + + {{else}} + +
    + + + {{textarea value=input}} +
    + + {{/if}} +{{/d-modal-body}} + + diff --git a/app/assets/javascripts/discourse/templates/modal/group-membership.hbs b/app/assets/javascripts/discourse/templates/modal/group-membership.hbs deleted file mode 100644 index 92c35ab9f0..0000000000 --- a/app/assets/javascripts/discourse/templates/modal/group-membership.hbs +++ /dev/null @@ -1,25 +0,0 @@ -{{#d-modal-body class='group-membership' title="groups.add_members.title"}} -
    - - - {{user-selector - usernames=model.usernames - placeholderKey="groups.selector_placeholder" - id="group-membership-user-selector"}} -
    - - {{#if this.currentUser.admin}} -
    - - {{input type="checkbox" class="inline" checked=setAsOwner}} -
    - {{/if}} -{{/d-modal-body}} - - diff --git a/app/assets/javascripts/discourse/templates/preferences/account.hbs b/app/assets/javascripts/discourse/templates/preferences/account.hbs index 6862fac382..c9a6b01105 100644 --- a/app/assets/javascripts/discourse/templates/preferences/account.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/account.hbs @@ -100,8 +100,10 @@
    - {{model.title}} - {{#link-to "preferences.badgeTitle" class="btn btn-small btn-icon pad-left no-text"}}{{d-icon "pencil"}}{{/link-to}} + {{combo-box + value=newTitleInput + content=model.availableTitles + none="user.title.none"}}
    {{/if}} diff --git a/app/assets/javascripts/discourse/templates/tag-groups-show.hbs b/app/assets/javascripts/discourse/templates/tag-groups-show.hbs index 0a7dc1ef70..55dea80a7c 100644 --- a/app/assets/javascripts/discourse/templates/tag-groups-show.hbs +++ b/app/assets/javascripts/discourse/templates/tag-groups-show.hbs @@ -15,7 +15,7 @@ {{tag-chooser tags=model.parent_tag_name everyTag=true - limit=1 + maximum=1 allowCreate=true filterPlaceholder="tagging.groups.parent_tag_placeholder"}} {{i18n 'tagging.groups.parent_tag_description'}} @@ -28,6 +28,13 @@ +
    + +
    + {{model.savingStatus}} diff --git a/app/assets/javascripts/discourse/templates/topic-post-badges.raw.hbs b/app/assets/javascripts/discourse/templates/topic-post-badges.raw.hbs index b2076113c3..2cbf0f9b69 100644 --- a/app/assets/javascripts/discourse/templates/topic-post-badges.raw.hbs +++ b/app/assets/javascripts/discourse/templates/topic-post-badges.raw.hbs @@ -6,6 +6,6 @@ {{newPosts}} {{/if}} {{#if unseen ~}} - {{i18n 'filters.new.lower_title'}} + {{newDotText}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/user.hbs b/app/assets/javascripts/discourse/templates/user.hbs index 469dc53953..09e41badad 100644 --- a/app/assets/javascripts/discourse/templates/user.hbs +++ b/app/assets/javascripts/discourse/templates/user.hbs @@ -67,12 +67,19 @@ connectorTagName="li" args=(hash model=model)}} - {{#if collapsedInfo}} - {{#if viewingSelf}} -
  • {{d-icon "angle-double-down"}}{{i18n 'user.expand_profile'}}
  • + {{#if canExpandProfile}} +
  • + {{#if collapsedInfo}} + + {{d-icon "angle-double-down"}} {{i18n 'user.expand_profile'}} + + {{else}} + + {{d-icon "angle-double-up"}} {{i18n 'user.collapse_profile'}} + + {{/if}} +
  • {{/if}} - {{/if}} - @@ -144,23 +151,23 @@
    {{#if model.created_at}} -
    {{i18n 'user.created'}}
    {{bound-date model.created_at}}
    +
    {{i18n 'user.created'}}
    {{bound-date model.created_at}}
    {{/if}} {{#if model.last_posted_at}} -
    {{i18n 'user.last_posted'}}
    {{bound-date model.last_posted_at}}
    +
    {{i18n 'user.last_posted'}}
    {{bound-date model.last_posted_at}}
    {{/if}} {{#if model.last_seen_at}} -
    {{i18n 'user.last_seen'}}
    {{bound-date model.last_seen_at}}
    +
    {{i18n 'user.last_seen'}}
    {{bound-date model.last_seen_at}}
    {{/if}} -
    {{i18n 'views'}}
    {{model.profile_view_count}}
    +
    {{i18n 'views'}}
    {{model.profile_view_count}}
    {{#if model.invited_by}} -
    {{i18n 'user.invited_by'}}
    {{#link-to 'user' model.invited_by}}{{model.invited_by.username}}{{/link-to}}
    +
    {{i18n 'user.invited_by'}}
    {{#link-to 'user' model.invited_by}}{{model.invited_by.username}}{{/link-to}}
    {{/if}} {{#if model.trust_level}} -
    {{i18n 'user.trust_level'}}
    {{model.trustLevel.name}}
    +
    {{i18n 'user.trust_level'}}
    {{model.trustLevel.name}}
    {{/if}} {{#if canCheckEmails}} -
    {{i18n 'user.email.title'}}
    +
    {{i18n 'user.email.title'}}
    {{#if model.email}} {{model.email}} @@ -168,10 +175,11 @@ {{d-button action="checkEmail" actionParam=model icon="envelope-o" label="admin.users.check_email.text" class="btn-primary"}} {{/if}}
    +
    {{/if}} {{#if model.displayGroups}} -
    {{i18n 'groups.title' count=model.displayGroups.length}}
    +
    {{i18n 'groups.title' count=model.displayGroups.length}}
    {{#each model.displayGroups as |group|}} {{#link-to 'group' group.name class="group-link"}}{{group.name}}{{/link-to}} @@ -181,10 +189,11 @@ ... {{/link-to}}
    +
    {{/if}} - + {{#if canDeleteUser}} - {{d-button action="adminDelete" icon="exclamation-triangle" label="user.admin_delete" class="btn-danger"}} +
    {{d-button action="adminDelete" icon="exclamation-triangle" label="user.admin_delete" class="btn-danger"}}
    {{/if}}
    {{plugin-outlet name="user-profile-secondary" args=(hash model=model)}} diff --git a/app/assets/javascripts/discourse/templates/user/messages.hbs b/app/assets/javascripts/discourse/templates/user/messages.hbs index 1a8982ae74..0e53780129 100644 --- a/app/assets/javascripts/discourse/templates/user/messages.hbs +++ b/app/assets/javascripts/discourse/templates/user/messages.hbs @@ -40,10 +40,17 @@ {{#if pmTaggingEnabled}}
  • - {{#link-to 'userPrivateMessages.tags' model}} - {{i18n 'user.messages.tags'}} - {{/link-to}} -
  • + {{#link-to 'userPrivateMessages.tags' model}} + {{i18n 'user.messages.tags'}} + {{/link-to}} + + {{#if tagId}} +
  • + {{#link-to 'userPrivateMessages.tagsShow' tagId}} + {{tagId}} + {{/link-to}} +
  • + {{/if}} {{/if}} {{/mobile-nav}} {{/d-section}} diff --git a/app/assets/javascripts/discourse/widgets/button.js.es6 b/app/assets/javascripts/discourse/widgets/button.js.es6 index aa65deae5e..1794c0ab85 100644 --- a/app/assets/javascripts/discourse/widgets/button.js.es6 +++ b/app/assets/javascripts/discourse/widgets/button.js.es6 @@ -28,15 +28,14 @@ export const ButtonClass = { buildAttributes() { const attrs = this.attrs; + const attributes = {}; - let title; if (attrs.title) { - title = I18n.t(attrs.title, attrs.titleOptions); - } else if (attrs.label) { - title = I18n.t(attrs.label, attrs.labelOptions); + const title = I18n.t(attrs.title, attrs.titleOptions); + attributes["aria-label"] = title; + attributes.title = title; } - const attributes = { "aria-label": title, title }; if (attrs.disabled) { attributes.disabled = "true"; } if (attrs.data) { diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index 77e8b33186..d221407768 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -116,7 +116,7 @@ export default createWidget('hamburger-menu', { links.push({ route: 'users', className: 'user-directory-link', label: 'directory.title' }); } - if (this.siteSettings.enable_group_directory) { + if (this.siteSettings.enable_group_directory || (this.currentUser && this.currentUser.staff)) { links.push({ route: 'groups', className: 'groups-link', label: 'groups.index.title' }); } diff --git a/app/assets/javascripts/discourse/widgets/notification-item.js.es6 b/app/assets/javascripts/discourse/widgets/notification-item.js.es6 index 83f0c368fa..1b5892fff7 100644 --- a/app/assets/javascripts/discourse/widgets/notification-item.js.es6 +++ b/app/assets/javascripts/discourse/widgets/notification-item.js.es6 @@ -101,10 +101,8 @@ createWidget('notification-item', { let { data } = attrs; let infoKey = notName === 'custom' ? data.message : notName; - let title = I18n.t(`notifications.alt.${infoKey}`); - let icon = iconNode(`notification.${infoKey}`, { title }); - let text = emojiUnescape(this.text(notificationType, notName)); + let icon = iconNode(`notification.${infoKey}`); // We can use a `

    ` tag here once other languages have fixed their HTML // translations. @@ -113,7 +111,7 @@ createWidget('notification-item', { let contents = [ icon, html ]; const href = this.url(); - return href ? h('a', { attributes: { href, title, 'data-auto-route': true } }, contents) : contents; + return href ? h('a', { attributes: { href, 'data-auto-route': true } }, contents) : contents; }, click(e) { diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 index 46b9819457..c60f506b51 100644 --- a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 @@ -69,11 +69,15 @@ registerButton('like-count', attrs => { const title = attrs.liked ? count === 1 ? 'post.has_likes_title_only_you' : 'post.has_likes_title_you' : 'post.has_likes_title'; + const icon = attrs.yours ? 'heart' : ''; + const additionalClass = attrs.yours ? 'my-likes' : 'regular-likes'; return { action: 'toggleWhoLiked', title, - className: 'like-count highlight-action', - contents: I18n.t("post.has_likes", { count }), + className: `like-count highlight-action ${additionalClass}`, + contents: count, + icon, + iconRight: true, titleOptions: {count: attrs.liked ? (count-1) : count } }; } @@ -281,15 +285,18 @@ export default createWidget('post-menu', { replaceButton(orderedButtons, 'reply', 'wiki-edit'); } - orderedButtons.forEach(i => { - const button = this.attachButton(i, attrs); - if (button) { - allButtons.push(button); + orderedButtons + .filter(x => x !== "like-count" && x !== "like") + .forEach(i => { + const button = this.attachButton(i, attrs); - if ((attrs.yours && button.attrs.alwaysShowYours) || (hiddenButtons.indexOf(i) === -1)) { - visibleButtons.push(button); + if (button) { + allButtons.push(button); + + if ((attrs.yours && button.attrs.alwaysShowYours) || (hiddenButtons.indexOf(i) === -1)) { + visibleButtons.push(button); + } } - } }); if (!this.settings.collapseButtons) { @@ -310,6 +317,11 @@ export default createWidget('post-menu', { visibleButtons.splice(visibleButtons.length - 1, 0, showMore); } + visibleButtons.unshift(h('div.like-button', [ + this.attachButton("like-count", attrs), + this.attachButton("like", attrs) + ])); + Object.keys(_extraButtons).forEach(k => { const builder = _extraButtons[k]; if (builder) { @@ -384,6 +396,9 @@ export default createWidget('post-menu', { showMoreActions() { this.state.collapsed = false; + if (!this.state.likedUsers.length) { + return this.getWhoLiked(); + } }, like() { diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 index fd59b7328b..97ad98445d 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 @@ -62,7 +62,7 @@ const rule = { if (primaryGroupName && primaryGroupName.length !== 0) { token.attrs.push(['class', `quote group-${primaryGroupName}`]); } else { - token.attrs.push(['class', 'quote']); + token.attrs.push(['class', 'quote no-group']); } if (postNumber) { @@ -153,7 +153,8 @@ export function setup(helper) { helper.whiteList({ custom(tag, name, value) { if (tag === 'aside' && name === 'class') { - return !!/^quote group\-(.+)$/.exec(value); + return value === "quote no-group" || + !!/^quote group\-(.+)$/.exec(value); } } }); diff --git a/app/assets/javascripts/select-kit/components/category-drop.js.es6 b/app/assets/javascripts/select-kit/components/category-drop.js.es6 index debffeb966..cbfaa32961 100644 --- a/app/assets/javascripts/select-kit/components/category-drop.js.es6 +++ b/app/assets/javascripts/select-kit/components/category-drop.js.es6 @@ -18,8 +18,8 @@ export default ComboBoxComponent.extend({ noCategoriesLabel: I18n.t("categories.no_subcategory"), mutateAttributes() {}, fullWidthOnMobile: true, - caretDownIcon: "caret-right fa-fw", - caretUpIcon: "caret-down fa-fw", + caretDownIcon: "caret-right", + caretUpIcon: "caret-down", init() { this._super(); diff --git a/app/assets/javascripts/select-kit/components/category-notifications-button.js.es6 b/app/assets/javascripts/select-kit/components/category-notifications-button.js.es6 index ff97c4b12d..602369f521 100644 --- a/app/assets/javascripts/select-kit/components/category-notifications-button.js.es6 +++ b/app/assets/javascripts/select-kit/components/category-notifications-button.js.es6 @@ -3,7 +3,7 @@ import NotificationOptionsComponent from "select-kit/components/notifications-bu export default NotificationOptionsComponent.extend({ pluginApiIdentifiers: ["category-notifications-button"], classNames: "category-notifications-button", - isHidden: Ember.computed.or("category.deleted", "site.isMobileDevice"), + isHidden: Ember.computed.or("category.deleted"), headerIcon: Ember.computed.alias("iconForSelectedDetails"), i18nPrefix: "category.notifications", showFullTitle: false, diff --git a/app/assets/javascripts/select-kit/components/category-row.js.es6 b/app/assets/javascripts/select-kit/components/category-row.js.es6 index 8d1c8cd3ed..74f146e51c 100644 --- a/app/assets/javascripts/select-kit/components/category-row.js.es6 +++ b/app/assets/javascripts/select-kit/components/category-row.js.es6 @@ -20,6 +20,12 @@ export default SelectKitRowComponent.extend({ return displayCategoryDescription; }, + @computed("description", "category.name") + title(categoryDescription, categoryName) { + if (categoryDescription) return categoryDescription; + return categoryName; + }, + @computed("computedContent.value", "computedContent.name") category(value, name) { if (Ember.isEmpty(value)) { @@ -76,6 +82,8 @@ export default SelectKitRowComponent.extend({ @computed("category.description") description(description) { - return `${description.substr(0, 200)}${description.length > 200 ? '…' : ''}`; + if (description) { + return `${description.substr(0, 200)}${description.length > 200 ? '…' : ''}`; + } } }); diff --git a/app/assets/javascripts/select-kit/components/combo-box.js.es6 b/app/assets/javascripts/select-kit/components/combo-box.js.es6 index e3bba9697c..a43839a6ee 100644 --- a/app/assets/javascripts/select-kit/components/combo-box.js.es6 +++ b/app/assets/javascripts/select-kit/components/combo-box.js.es6 @@ -7,8 +7,8 @@ export default SingleSelectComponent.extend({ autoFilterable: true, headerComponent: "combo-box/combo-box-header", - caretUpIcon: "caret-up fa-fw", - caretDownIcon: "caret-down fa-fw", + caretUpIcon: "caret-up", + caretDownIcon: "caret-down", clearable: false, computeHeaderContent() { @@ -25,7 +25,7 @@ export default SingleSelectComponent.extend({ @on("didReceiveAttrs") _setComboBoxOptions() { this.get("headerComponentOptions").setProperties({ - clearable: this.get("clearable"), + clearable: this.get("clearable") }); } }); diff --git a/app/assets/javascripts/select-kit/components/group-dropdown.js.es6 b/app/assets/javascripts/select-kit/components/group-dropdown.js.es6 new file mode 100644 index 0000000000..b2939b0278 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/group-dropdown.js.es6 @@ -0,0 +1,44 @@ +import ComboBoxComponent from "select-kit/components/combo-box"; +import DiscourseURL from "discourse/lib/url"; +import { default as computed } from "ember-addons/ember-computed-decorators"; + +export default ComboBoxComponent.extend({ + pluginApiIdentifiers: ["group-dropdown"], + classNames: "group-dropdown", + content: Ember.computed.alias("groups"), + tagName: "li", + caretDownIcon: "caret-right", + caretUpIcon: "caret-down", + allowAutoSelectFirst: false, + valueAttribute: 'name', + + @computed("content") + filterable(content) { + return content && content.length >= 10; + }, + + computeHeaderContent() { + let content = this._super(); + + if (!this.get("hasSelection")) { + content.label = `${I18n.t("groups.index.all")}`; + } + + return content; + }, + + @computed + collectionHeader() { + return ` + + ${I18n.t("groups.index.all").toLowerCase()} + + `.htmlSafe(); + }, + + actions: { + onSelect(groupName) { + DiscourseURL.routeTo(Discourse.getURL(`/groups/${groupName}`)); + } + } +}); diff --git a/app/assets/javascripts/select-kit/components/group-members-dropdown.js.es6 b/app/assets/javascripts/select-kit/components/group-members-dropdown.js.es6 new file mode 100644 index 0000000000..cda2720a25 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/group-members-dropdown.js.es6 @@ -0,0 +1,33 @@ +import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box"; + +export default DropdownSelectBoxComponent.extend({ + classNames: "group-members-dropdown", + headerIcon: ["bars", "caret-down"], + showFullTitle: false, + allowInitialValueMutation: false, + autoHighlight() {}, + + computeContent() { + const items = [ + { + id: "showAddMembersModal", + name: I18n.t("groups.add_members.title"), + icon: "user-plus" + } + ]; + + if (this.currentUser.admin) { + items.push({ + id: "showBulkAddModal", + name: I18n.t("admin.groups.bulk_add.title"), + icon: "users" + }); + } + + return items; + }, + + mutateValue(value) { + this.sendAction(value); + } +}); diff --git a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 index b3c6929a12..740fd9e61b 100644 --- a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 @@ -34,17 +34,17 @@ export default ComboBox.extend(Tags, { }); }); - this.set("limit", parseInt(this.get("limit") || this.get("siteSettings.max_tags_per_topic"))); + this.set("maximum", parseInt(this.get("limit") || this.get("maximum") || this.get("siteSettings.max_tags_per_topic"))); }, - @computed("hasReachedLimit") - caretIcon(hasReachedLimit) { - return hasReachedLimit ? null : "plus fa-fw"; + @computed("hasReachedMaximum") + caretIcon(hasReachedMaximum) { + return hasReachedMaximum ? null : "plus fa-fw"; }, @computed("tags") selection(tags) { - return makeArray(tags); + return makeArray(tags).map(c => this.computeContentItem(c)); }, filterComputedContent(computedContent) { @@ -56,7 +56,7 @@ export default ComboBox.extend(Tags, { this.$(".select-kit-body").on("click.mini-tag-chooser", ".selected-tag", (event) => { event.stopImmediatePropagation(); - this.destroyTags($(event.target).attr("data-value")); + this.destroyTags(this.computeContentItem($(event.target).attr("data-value"))); }); this.$(".select-kit-header").on("focus.mini-tag-chooser", ".selected-name", (event) => { @@ -110,7 +110,7 @@ export default ComboBox.extend(Tags, { } tags.map((tag) => { - const isHighlighted = highlightedSelection.includes(tag); + const isHighlighted = highlightedSelection.map(s => get(s, "value")).includes(tag); output += ` {{/if}} -{{d-icon caretIcon class="caret-icon"}} +{{d-icon caretIcon class="caret-icon fa-fw"}} diff --git a/app/assets/javascripts/select-kit/templates/components/multi-select.hbs b/app/assets/javascripts/select-kit/templates/components/multi-select.hbs index b3d4f47364..d541cc2c1c 100644 --- a/app/assets/javascripts/select-kit/templates/components/multi-select.hbs +++ b/app/assets/javascripts/select-kit/templates/components/multi-select.hbs @@ -41,7 +41,7 @@ computedValue=computedValue rowComponentOptions=rowComponentOptions noContentRow=noContentRow - maxContentRow=maxContentRow + validationMessage=validationMessage }} {{/unless}} {{/if}} diff --git a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs index 6a7089e9ec..b673bd18d2 100644 --- a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs +++ b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs @@ -30,26 +30,25 @@ }} {{/if}} -{{#if maxContentRow}} -

  • - {{maxContentRow}} +{{#if noContentRow}} +
  • + {{noContentRow}}
  • {{else}} - {{#if noContentRow}} -
  • - {{noContentRow}} -
  • - {{else}} - {{#each collectionComputedContent as |computedContent|}} - {{component rowComponent - computedContent=computedContent - highlighted=highlighted - computedValue=computedValue - templateForRow=templateForRow - onClickRow=onClickRow - onMouseoverRow=onMouseoverRow - options=rowComponentOptions - }} - {{/each}} + {{#if validationMessage}} +
    + {{validationMessage}} +
    {{/if}} + {{#each collectionComputedContent as |computedContent|}} + {{component rowComponent + computedContent=computedContent + highlighted=highlighted + computedValue=computedValue + templateForRow=templateForRow + onClickRow=onClickRow + onMouseoverRow=onMouseoverRow + options=rowComponentOptions + }} + {{/each}} {{/if}} diff --git a/app/assets/javascripts/select-kit/templates/components/single-select.hbs b/app/assets/javascripts/select-kit/templates/components/single-select.hbs index 46128823d3..e01241d319 100644 --- a/app/assets/javascripts/select-kit/templates/components/single-select.hbs +++ b/app/assets/javascripts/select-kit/templates/components/single-select.hbs @@ -40,7 +40,7 @@ computedValue=computedValue rowComponentOptions=rowComponentOptions noContentRow=noContentRow - maxContentRow=maxContentRow + validationMessage=validationMessage }} {{/if}}
    diff --git a/app/assets/javascripts/select-kit/templates/components/tag-drop/tag-drop-header.hbs b/app/assets/javascripts/select-kit/templates/components/tag-drop/tag-drop-header.hbs index 7928a3b94b..7e3ea1baf4 100644 --- a/app/assets/javascripts/select-kit/templates/components/tag-drop/tag-drop-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/tag-drop/tag-drop-header.hbs @@ -2,4 +2,4 @@ {{{label}}} -{{d-icon caretIcon class="caret-icon"}} +{{d-icon caretIcon class="caret-icon fa-fw"}} diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index 9bf5830056..651bf08e02 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -5,7 +5,29 @@ @import "common/foundation/base"; @import "common/foundation/mixins"; @import "common/foundation/variables"; -@import "common/select-kit/*"; +@import "common/select-kit/admin-agree-flag-dropdown"; +@import "common/select-kit/admin-delete-flag-dropdown"; +@import "common/select-kit/categories-admin-dropdown"; +@import "common/select-kit/category-chooser"; +@import "common/select-kit/category-drop"; +@import "common/select-kit/category-row"; +@import "common/select-kit/category-selector"; +@import "common/select-kit/combo-box"; +@import "common/select-kit/composer-actions"; +@import "common/select-kit/dropdown-select-box"; +@import "common/select-kit/future-date-input-selector"; +@import "common/select-kit/group-dropdown"; +@import "common/select-kit/list-setting"; +@import "common/select-kit/mini-tag-chooser"; +@import "common/select-kit/multi-select"; +@import "common/select-kit/notifications-button"; +@import "common/select-kit/period-chooser"; +@import "common/select-kit/pinned-button"; +@import "common/select-kit/select-kit"; +@import "common/select-kit/tag-chooser"; +@import "common/select-kit/tag-drop"; +@import "common/select-kit/toolbar-popup-menu-options"; +@import "common/select-kit/topic-notifications-button"; @import "common/components/*"; @import "common/input_tip"; @import "common/topic-entrance"; diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 122a647925..81d5eab682 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -167,6 +167,20 @@ $mobile-breakpoint: 700px; } } + &.web_crawlers { + tr { + th:nth-of-type(1) { + width: 60%; + } + } + td.x-value { + max-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + .bar-container { float: left; width: 300px; @@ -1313,6 +1327,10 @@ table.api-keys { width: 20px; } +.admin-plugins-official-badge { + color: $success; +} + // Backups // -------------------------------------------------- diff --git a/app/assets/stylesheets/common/admin/flagging.scss b/app/assets/stylesheets/common/admin/flagging.scss index 298826baad..e4d191678e 100644 --- a/app/assets/stylesheets/common/admin/flagging.scss +++ b/app/assets/stylesheets/common/admin/flagging.scss @@ -67,6 +67,10 @@ } } + .flagged-post-reply-count { + font-weight: normal; + } + .flag-user-lists { display: flex; align-items: flex-start; diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index a9033684d8..552e6f8e63 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -86,7 +86,7 @@ } } - > tbody > tr:first-of-type { + > tbody { border-top: 3px solid $primary-low; } diff --git a/app/assets/stylesheets/common/base/directory.scss b/app/assets/stylesheets/common/base/directory.scss index 65114a11b5..516922baed 100644 --- a/app/assets/stylesheets/common/base/directory.scss +++ b/app/assets/stylesheets/common/base/directory.scss @@ -1,10 +1,10 @@ .directory { margin-bottom: 100px; - + .user-info { margin-bottom: 0; } - + .period-chooser { float: left; } @@ -18,25 +18,25 @@ .spinner { clear: both; } - + table { width: 100%; margin-bottom: 1em; - + td, th { padding: 0.5em; text-align: left; - border-bottom: 1px solid $primary-low; - + border-bottom: 1px solid $primary-low; + .number, .time-read { font-size: $font-up-3; - color: $primary-medium; + color: $primary-medium; } .time-read { white-space: nowrap; } } - + th.sortable { cursor: pointer; white-space: nowrap; @@ -48,9 +48,9 @@ .d-icon-chevron-down, .d-icon-chevron-up { margin-left: 0.5em; } - + &:hover { - background-color: $primary-low; + background-color: $primary-low; } } } diff --git a/app/assets/stylesheets/common/base/group.scss b/app/assets/stylesheets/common/base/group.scss index 082adb401e..9813e7a593 100644 --- a/app/assets/stylesheets/common/base/group.scss +++ b/app/assets/stylesheets/common/base/group.scss @@ -2,6 +2,26 @@ background: rgba(230, 230, 230, 0.3); padding: 20px; margin-bottom: 15px; + position: relative; +} + +.group-outlet { + position: relative; +} + +.group-members-actions { + position: absolute; + top: -49px; + right: 0px; + + .group-username-filter { + margin: 0px; + vertical-align: middle; + } + + .group-members-dropdown, .group-members-add { + vertical-align: middle; + } } .group-post { @@ -78,7 +98,7 @@ } } -.group-logs-filter { +.group-manage-logs-filter { margin-right: 10px; &:hover { @@ -86,7 +106,7 @@ } } -table.group-logs { +table.group-manage-logs { width: 100%; th, tr { @@ -102,7 +122,7 @@ table.group-logs { padding: 10px 0; } - .group-logs-expand-details { + .group-manage-logs-expand-details { cursor: pointer; i { @@ -120,7 +140,7 @@ table.group-members { } th:first-child { - width: 60%; + width: 30%; text-align: left; } @@ -172,23 +192,9 @@ table.group-members { color: $primary; } -.form-horizontal { +.form-vertical { .group-flair-inputs { display: inline-block; - - input[type="text"] { - width: 80% !important; - margin-bottom: 10px; - } - - .group-flair-left { - float: left; - } - - .group-flair-right { - float: left; - margin-left: 30px; - } } .avatar-flair-preview { @@ -201,24 +207,12 @@ table.group-members { } } -.group-edit { - .form-horizontal { - label { - font-weight: bold; - } - } +.group-form-save { + margin-right: 20px; } -.group-membership { - .ac-wrap { - width: 100% !important; - } - - label { - font-weight: bold; - } - - .group-membership-make-owner { +.group-add-members { + .group-add-members-make-owner { label { display: inline; vertical-align: middle; diff --git a/app/assets/stylesheets/common/base/groups.scss b/app/assets/stylesheets/common/base/groups.scss index 1c8ae40b3f..3ff525b1f5 100644 --- a/app/assets/stylesheets/common/base/groups.scss +++ b/app/assets/stylesheets/common/base/groups.scss @@ -5,15 +5,18 @@ } } -.groups-filter { - display: inline-block; - float: right; +.groups-header { + margin-bottom: 30px; +} - .groups-type-filter { +.groups-header-filters { + display: inline-block; + + .groups-header-filters-type { vertical-align: middle; } - .groups-name-filter { + .groups-header-filters-name { vertical-align: middle; margin: 0; } @@ -39,8 +42,8 @@ border-bottom: 1px solid $primary-low; td { - color: blend-primary-secondary(50%); padding: 0.8em; + color: $primary-medium; } td.groups-info { @@ -54,9 +57,26 @@ } td.groups-user-count { - font-size: $font-up-2 + width: 17%; + font-size: $font-up-2; } - } + + td.groups-table-type { + width: 17%; + font-size: $font-up-1; + } + + td.groups-table-membership { + .group-membership-button { + display: inline-block; + } + + > span { + margin-right: 5px; + font-size: $font-up-1; + } + } + } .groups-info { .groups-info-name { diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss index f4c70b4b19..cd5fe5fabb 100644 --- a/app/assets/stylesheets/common/base/modal.scss +++ b/app/assets/stylesheets/common/base/modal.scss @@ -29,13 +29,13 @@ .modal-header { display: flex; align-items: center; - padding: 10px 15px; + padding: 10px 15px; border-bottom: 1px solid $primary-low; h3 { margin-bottom: 0; } .modal-close { - order: 2; + order: 2; margin-left: auto; } } @@ -372,6 +372,12 @@ } } } + + section.minimum-required-tags { + input[type=number] { + width: 50px; + } + } } .incoming-email-modal { @@ -421,22 +427,22 @@ } .change-timestamp { - + .date-picker { width: 10em; } - + #date-container { .pika-single { position: relative !important; // overriding another important display: inline-block; } } - + input[type=time] { width: 6em; } - + form { margin: 0; } @@ -491,4 +497,3 @@ width: 95%; } - \ No newline at end of file diff --git a/app/assets/stylesheets/common/base/request-group-membership-form.scss b/app/assets/stylesheets/common/base/request-group-membership-form.scss index 63dfd0aec7..33669fc0e2 100644 --- a/app/assets/stylesheets/common/base/request-group-membership-form.scss +++ b/app/assets/stylesheets/common/base/request-group-membership-form.scss @@ -4,6 +4,6 @@ } } -.group-edit-membership-request-template { +.group-add-membership-request-template { width: 98%; } diff --git a/app/assets/stylesheets/common/base/search.scss b/app/assets/stylesheets/common/base/search.scss index 46e6bb36f6..bac455f1f7 100644 --- a/app/assets/stylesheets/common/base/search.scss +++ b/app/assets/stylesheets/common/base/search.scss @@ -1,9 +1,182 @@ +.search-container { + display: flex; + justify-content: space-between; + + .search-bar { + display: flex; + justify-content: space-between; + align-items: center; + justify-content: space-between; + margin-bottom: 1em; + + .search-query { + flex: 1; + margin: 0 .5em 0 0; + } + + .search-cta { + padding-bottom: 6.5px; + padding-top: 6.5px; + } + } + + .search-advanced { + width: 70%; + @include small-width { width: 65%; } + + .search-actions, + .search-notice, + .search-results, + .search-title, + .search-bar { + margin-bottom: 1em; + } + + .search-info { + display: flex; + flex-wrap: wrap; + border-bottom: 3px solid $primary-low; + padding-bottom: .5em; + margin-bottom: 1em; + flex-direction: row; + align-items: center; + + .result-count { + display: flex; + + .term { + font-weight: bold; + } + + // spans can be in different orders depending of locale + span + span { + margin-left: .25em; + } + } + + .sort-by { + display: flex; + margin-left: auto; + align-items: center; + + .desc { + margin-right: .5em; + } + + .combo-box { + min-width: 150px; + } + } + } + + .search-title { + display: flex; + justify-content: flex-start; + align-items: center; + + .bulk-select { + margin-left: .5em; + } + + .fps-select a { + margin-left: .5em; + font-size: $font-down-1; + + &:hover { + text-decoration: underline; + } + } + } + + .search-notice { + .fps-invalid { + padding: .5em; + background-color: $danger-low; + border: 1px solid $danger-medium; + color: $danger; + } + } + } + + .search-advanced-sidebar { + width: 30%; + @include small-width { width: 35%; } + margin-left: 1em; + display: flex; + flex-direction: column; + + #search-min-post-count, + .date-picker, + .combo-box, + .ac-wrap, + .control-group, + .date-picker-wrapper, + .search-advanced-category-chooser { + box-sizing: border-box; + width: 100%; + min-width: 100%; + input, .item { + padding-left: 4px; // temporarily normalizing input padding for this section + } + } + + .date-picker-wrapper { + margin-top: .5em; + } + + .date-picker { + box-sizing: border-box; + text-align: left; + padding: 4px; + margin-bottom: 0; + } + + .search-advanced-title { + background: $primary-low; + padding: .358em 1em; + @include small-width { padding: .358em .5em; } + font-weight: 700; + text-align: left; + font-weight: bold; + + &.btn { + background: $primary-low; + } + + .d-icon { + margin: 0; + } + } + + .search-advanced-filters { + background: $primary-very-low; + padding: 1em; + .control-group { + margin-bottom: 15px; + } + section.field { + margin-top: 5px; + } + @include small-width { + padding: .75em .5em; + .ac-wrap, .choices, .select-kit.multi-select { // overriding inline width from JS + width: 100% !important; + } + .select-kit { + min-width: unset; + } + } + } + } +} + + .fps-invalid { margin-bottom: 1em; } .fps-result { - + display: flex; .author { display: inline-block; vertical-align: top; @@ -71,42 +244,6 @@ display: inline-block; } -.fps-select { - margin-top: -15px; - margin-bottom: 15px; - a:hover { - color: $secondary; - background-color: $tertiary; - } - a { - margin-right: 15px; - font-size: $font-down-1; - padding: 2px 5px; - } -} - -.search.row { - margin-bottom: 15px; - .input-xxlarge { - width: 100%; - } - - .search-bar { - display: flex; - margin-bottom: 10px; - max-width: 780px; - input { - height: 22px; - padding-left: 6px; - margin: 0 5px 0 0; - } - } - - .new-topic-btn { - float:right; - } -} - .no-results-suggestion { margin-top: 30px; } @@ -118,96 +255,6 @@ float: left; } -.search-title { - position: relative; - margin: 10px 0 15px; - max-width: 780px; - border-bottom: 3px solid $primary-low; - width: 100%; - .term { - font-weight: bold; - } - - .result-count { - float: left; - margin-bottom: 4px; - span { - line-height: $line-height-large; - height: 28px; - display: inline-block; - } - } - - .sort-by { - float: right; - margin-bottom: 4px; - .desc { - margin-right: 5px; - } - select { - margin-bottom: 0; - width: auto; - min-width: 150px; - } - } -} - .google-search-form { margin-top: 2em; } - -.search-advanced { - display: flex; - flex-wrap: wrap; - margin-bottom: 20px; - max-width: 780px; - .search-advanced-options { - border: 1px solid $primary-low; - padding: 0 20px; - width: 100%; - .date-picker-wrapper { - vertical-align: top; - } - @media screen and (max-width: 715px) { - padding: 0 10px; - #postTime { - margin: 0 0 5px 0; - } - } - } - - .search-advanced-btn { - flex: 1 1 100%; - width: 100%; - text-align: left; - font-weight: bold; - } - - .tag-chooser { - width: 70%; - } - - .container { - display: flex; - flex: 1 1 100%; - padding: 15px 0 10px 0; - .all-tags { - margin-bottom: 0; - } - @media screen and (max-width: 600px) { - flex-wrap: wrap; - } - &:not(:first-of-type) { - border-top: 1px solid $primary-low; - } - .control-group { - flex: 1 1 100%; - @media screen and (max-width: 600px) { - margin: 0; - &:nth-of-type(2) { - margin-top: 5px; - } - } - } - } -} diff --git a/app/assets/stylesheets/common/base/topic.scss b/app/assets/stylesheets/common/base/topic.scss index 9122c92528..fba521fcba 100644 --- a/app/assets/stylesheets/common/base/topic.scss +++ b/app/assets/stylesheets/common/base/topic.scss @@ -122,6 +122,11 @@ a.badge-category { } } +.private-message-glyph { + color: dark-light-choose($primary-low-mid, $secondary-high); + margin-right: 5px; +} + .archetype-private_message #topic-title .edit-topic-title .tag-chooser { margin-left: 19px; } diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss index 2eae6df77c..88f744e700 100644 --- a/app/assets/stylesheets/common/base/user.scss +++ b/app/assets/stylesheets/common/base/user.scss @@ -27,17 +27,33 @@ width: 100%; .secondary { + background: $secondary; + border-top: 1px solid $primary-low; + border-bottom: 1px solid $primary-low; .btn { - padding: 3px 12px; + padding: 4px 12px; } dl { margin: 0; + padding: 5px 10px; + div { + display: inline-flex; + align-items: baseline; + margin-right: 15px; + margin: 4px 0; + } + } + + dt, dd { + display: inline-flex; + align-items: center; } dd { padding: 0; + margin: 0 15px 0 0; overflow: hidden; text-overflow: ellipsis; color: $primary; @@ -54,7 +70,7 @@ dt { color: $secondary-medium; - margin: 0; + margin-right: 5px; display: inline-block; } } diff --git a/app/assets/stylesheets/common/components/groups-form-membership-fields.scss b/app/assets/stylesheets/common/components/groups-form-membership-fields.scss new file mode 100644 index 0000000000..d47b250696 --- /dev/null +++ b/app/assets/stylesheets/common/components/groups-form-membership-fields.scss @@ -0,0 +1,3 @@ +.group-form-automatic-membership-automatic { + margin-bottom: 10px; +} diff --git a/app/assets/stylesheets/common/d-editor.scss b/app/assets/stylesheets/common/d-editor.scss index a8e1541d60..5ccdd6b224 100644 --- a/app/assets/stylesheets/common/d-editor.scss +++ b/app/assets/stylesheets/common/d-editor.scss @@ -52,11 +52,17 @@ background-color: $secondary; position: relative; border: 1px solid $primary-medium; - + textarea { background: transparent; } + &.disabled { + cursor: not-allowed; + .d-editor-button-bar { + visibility: hidden; + } + } } .d-editor-preview-wrapper { @@ -73,7 +79,7 @@ min-height: 30px; padding-left: 3px; border-bottom: 1px solid $primary-low; - + button { background-color: transparent; color: $primary-high; @@ -108,13 +114,13 @@ .d-editor-input, .d-editor-preview { box-sizing: border-box; - flex: 1 1 100%; - width: 100%; + flex: 1 1 100%; + width: 100%; margin: 0; min-height: auto; word-wrap: break-word; -webkit-appearance: none; - border-radius: 0; + border-radius: 0; &:focus { box-shadow: none; border: 0; @@ -127,7 +133,7 @@ padding: 10px; height: 100%; overflow-x: hidden; - resize: none; + resize: none; } .d-editor-preview { @@ -155,6 +161,7 @@ } .user-preferences .bio-composer, +.group-form-bio, .edit-category-tab-topic-template { textarea { width: 100%; @@ -179,7 +186,8 @@ } } -.user-preferences .bio-composer { +.user-preferences .bio-composer, +.group-form-bio { padding: 10px; border: 1px solid $primary-low; } diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss index f028ab048d..6c28ae2416 100644 --- a/app/assets/stylesheets/common/foundation/variables.scss +++ b/app/assets/stylesheets/common/foundation/variables.scss @@ -26,13 +26,13 @@ $github: #6d6d6d !default; $base-font-size: 14px !default; $base-font-family: Helvetica, Arial, sans-serif !default; -// Font-size defintions, multiplier ^ (step / interval) +// Font-size defintions, multiplier ^ (step / interval) $font-up-6: 2.296em; $font-up-5: 2em; $font-up-4: 1.7511em; $font-up-3: 1.5157em; $font-up-2: 1.3195em; -$font-up-1: 1.1487em; // 2^(1/5) +$font-up-1: 1.1487em; // 2^(1/5) $font-0: 1em; $font-down-1: .8706em; // 2^(-1/5) $font-down-2: .7579em; // Smallest size we use based on the 1em base @@ -46,7 +46,7 @@ $line-height-small: 1; $line-height-medium: 1.2; // Headings or large text $line-height-large: 1.4; // Normal or small text -// These files don't actually exist. They're injected by Stylesheet::Compiler. +// These files don't actually exist. They're injected by Stylesheet::Compiler. // -------------------------------------------------- @import "theme_variables"; @@ -57,10 +57,10 @@ $line-height-large: 1.4; // Normal or small text // -------------------------------------------------- $z-layers: ( - "max": 9999, + "max": 9999, "fullscreen": 1700, "modal": ( - "tooltip": 1600, + "tooltip": 1600, "popover": 1500, "dropdown": 1400, "content": 1300, @@ -73,7 +73,7 @@ $z-layers: ( "popover": 500, "content": 400, ), - "dropdown": 300, + "dropdown": 300, "usercard": 200, "timeline": 100, "base": 1 diff --git a/app/assets/stylesheets/common/select-kit/category-drop.scss b/app/assets/stylesheets/common/select-kit/category-drop.scss index 201ce200b5..70f21e7814 100644 --- a/app/assets/stylesheets/common/select-kit/category-drop.scss +++ b/app/assets/stylesheets/common/select-kit/category-drop.scss @@ -40,6 +40,10 @@ font-size: $font-0; transition: none; + &.is-none .selected-name { + color: inherit; + } + .badge-wrapper { margin-right: 0; } diff --git a/app/assets/stylesheets/common/select-kit/combo-box.scss b/app/assets/stylesheets/common/select-kit/combo-box.scss index df7b809762..dd6a2e9859 100644 --- a/app/assets/stylesheets/common/select-kit/combo-box.scss +++ b/app/assets/stylesheets/common/select-kit/combo-box.scss @@ -1,6 +1,5 @@ .select-kit { &.combo-box { - .select-kit-body { width: 100%; } diff --git a/app/assets/stylesheets/common/select-kit/dropdown-select-box.scss b/app/assets/stylesheets/common/select-kit/dropdown-select-box.scss index c229b00422..20345715ec 100644 --- a/app/assets/stylesheets/common/select-kit/dropdown-select-box.scss +++ b/app/assets/stylesheets/common/select-kit/dropdown-select-box.scss @@ -51,6 +51,7 @@ } .texts { + min-width: 0; line-height: $line-height-large; -webkit-box-flex: 1; -ms-flex: 1; @@ -76,6 +77,10 @@ font-size: $font-0; color: $primary; padding: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; } .desc { diff --git a/app/assets/stylesheets/common/select-kit/group-dropdown.scss b/app/assets/stylesheets/common/select-kit/group-dropdown.scss new file mode 100644 index 0000000000..e38c0a4283 --- /dev/null +++ b/app/assets/stylesheets/common/select-kit/group-dropdown.scss @@ -0,0 +1,76 @@ +.select-kit { + &.combo-box { + &.group-dropdown { + min-width: auto; + + .combo-box-header { + background: $primary-low; + color: $primary; + border: 1px solid transparent; + padding: 4.5px 5px 4.5px 10px; + font-size: $font-0; + transition: none; + + .d-icon { + opacity: 1; + font-size: $font-0; + margin: 0; + } + } + + &.is-expanded .tag-drop-header { + border: 1px solid $tertiary; + box-shadow: shadow("focus"); + } + + .select-kit-collection { + display: flex; + flex-direction: column; + padding: 0; + max-height: 300px; + + .collection-header { + .group-dropdown-filter { + white-space: nowrap; + color: $primary; + font-size: $font-down-1; + line-height: $line-height-medium; + font-weight: bold; + display: block; + padding: 10px 5px; + + &:hover { + text-decoration: underline; + } + } + } + } + + .select-kit-filter .filter-input { + width: auto; + } + + .select-kit-body { + width: auto; + min-width: 150px; + border-radius: 0; + box-shadow: shadow("dropdown"); + } + + .select-kit-row { + margin: 0; + font-size: $font-down-1; + font-weight: bold; + color: $tertiary; + + &.no-content { + font-weight: normal; + } + } + + &.is-expanded .select-kit-wrapper, .select-kit-wrapper { + display: none; + } + } + } +} diff --git a/app/assets/stylesheets/common/select-kit/multi-select.scss b/app/assets/stylesheets/common/select-kit/multi-select.scss index 91ef4c1539..1978d1c43a 100644 --- a/app/assets/stylesheets/common/select-kit/multi-select.scss +++ b/app/assets/stylesheets/common/select-kit/multi-select.scss @@ -65,7 +65,6 @@ .choices { margin: 0; - padding: 2.5px; box-sizing: border-box; display: inline-flex; justify-content: flex-start; diff --git a/app/assets/stylesheets/common/select-kit/select-kit.scss b/app/assets/stylesheets/common/select-kit/select-kit.scss index 3f61e5e260..22598ff418 100644 --- a/app/assets/stylesheets/common/select-kit/select-kit.scss +++ b/app/assets/stylesheets/common/select-kit/select-kit.scss @@ -104,6 +104,12 @@ color: inherit; } + &.is-none { + .selected-name { + color: dark-light-choose($primary-high, $secondary-low); + } + } + .btn-clear { padding: 0 10px; border: 0; @@ -165,11 +171,6 @@ white-space: nowrap; } - &.max-content { - white-space: nowrap; - color: $danger; - } - .name { margin: 0; overflow: hidden; @@ -193,6 +194,10 @@ &.is-selected.is-highlighted { background: $tertiary-low; } + + .discourse-tag, .discourse-tag-count { + color: $primary; + } } .select-kit-collection { @@ -205,6 +210,14 @@ padding: 0; max-height: 200px; + .validation-message { + white-space: nowrap; + color: $danger; + flex: 1 0 auto; + margin: 5px; + padding: 0 2px; + } + .select-kit-collection { padding: 0; margin: 0; diff --git a/app/assets/stylesheets/common/select-kit/tag-drop.scss b/app/assets/stylesheets/common/select-kit/tag-drop.scss index 48c114f303..566cf296fd 100644 --- a/app/assets/stylesheets/common/select-kit/tag-drop.scss +++ b/app/assets/stylesheets/common/select-kit/tag-drop.scss @@ -11,6 +11,10 @@ font-size: $font-0; transition: none; + &.is-none .selected-name { + color: inherit; + } + .d-icon { opacity: 1; font-size: $font-0; diff --git a/app/assets/stylesheets/desktop.scss b/app/assets/stylesheets/desktop.scss index e11252ac35..b1a6d6b849 100644 --- a/app/assets/stylesheets/desktop.scss +++ b/app/assets/stylesheets/desktop.scss @@ -20,6 +20,7 @@ @import "desktop/history"; @import "desktop/queued-posts"; @import "desktop/group"; +@import "desktop/groups"; // Import all component-specific files @import "desktop/components/*"; diff --git a/app/assets/stylesheets/desktop/discourse.scss b/app/assets/stylesheets/desktop/discourse.scss index 900195da46..c20dbce7fb 100644 --- a/app/assets/stylesheets/desktop/discourse.scss +++ b/app/assets/stylesheets/desktop/discourse.scss @@ -225,6 +225,11 @@ input { margin-bottom: 10px; } +.control-instructions { + color: dark-light-choose($primary-medium, $secondary-medium); + font-size: 0.875rem; +} + .control-group { margin-bottom: 9px; diff --git a/app/assets/stylesheets/desktop/group.scss b/app/assets/stylesheets/desktop/group.scss index e279256e5c..527f297e01 100644 --- a/app/assets/stylesheets/desktop/group.scss +++ b/app/assets/stylesheets/desktop/group.scss @@ -1,4 +1,12 @@ .group-nav { + .group-dropdown { + margin-right: 10px; + + i { + color: $primary; + } + } + li { float: left; @@ -20,7 +28,7 @@ margin-bottom: 20px; } -.group-activity-nav, .group-messages-nav { +.group-navigation { width: 15%; background-color: transparent; @@ -28,32 +36,39 @@ border: none; a { - padding: 8px 13px; - } + color: dark-light-choose($primary-medium, $secondary-high); + padding: 8px 0; - a.active { - background-color: transparent; - font-weight: bold; - color: $primary; - } + &.active { + background-color: transparent; + font-weight: bold; + color: $primary; - a.active:after { - display: none; + &:after { + display: none; + } + } } } } -.group-activity-outlet, .group-messages-outlet { +.group-activity-outlet, +.group-messages-outlet, +.group-manage-outlet +{ width: 85%; } -.group-edit { - border: 1px solid $primary-low; - padding: 10px; - +.group-manage { .form-horizontal { button { float: none; } } } + +.group-details-button { + position: absolute; + top: 20px; + right: 20px; +} diff --git a/app/assets/stylesheets/desktop/groups.scss b/app/assets/stylesheets/desktop/groups.scss new file mode 100644 index 0000000000..a43ce6edba --- /dev/null +++ b/app/assets/stylesheets/desktop/groups.scss @@ -0,0 +1,19 @@ +.groups-page { + .list-controls { + float: right; + } +} + +$filter-line-height: 1.5; + +.groups-header-filters { + .groups-header-filters-type { + .select-kit-header { + line-height: $filter-line-height; + } + } + + input { + line-height: $filter-line-height; + } +} diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 99d25b737e..434d449847 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -54,9 +54,66 @@ nav.post-controls { padding: 0; - .like-count { - font-size: inherit; - margin-right: -5px; + .like-button { + display: inline-flex; + + .like-count { + color: dark-light-choose($primary-low-mid, $secondary-high); + } + + .widget-button { + background: none; + } + + &:hover { + background: $primary-low; + .like-count { + color: $primary-medium; + &.d-hover { + color: $primary; + } + } + .d-hover { + background: none; + } + } + + &:active { + box-shadow: inset 0 1px 3px rgba(0,0,0, .4); + + .widget-button { + box-shadow: none; + } + } + + .like { + &:focus { + background: none; + } + } + + .like-count { + font-size: $font-up-1; + margin-left: 0; + i.fa-heart { + padding-left: 5px; + color: dark-light-choose($primary-low-mid, $secondary-high); + } + &.my-likes { + margin-right: -2px; + i.fa-heart { + padding-left: 10px; + } + } + &.regular-likes { + margin-right: -12px; + } + } + + .toggle-like { + padding: 8px 8px; + margin-left: 2px; + } } .highlight-action { @@ -205,7 +262,7 @@ nav.post-controls { .embedded-posts { h1, h2, h3 { margin: 10px 0; } border: 1px solid $primary-low; - + .topic-body { box-sizing: border-box; width: calc(100% - 70px); // [100% - .topic-avatar width] @@ -628,7 +685,7 @@ $topic-avatar-width: 45px; .time-gap + .topic-post .embedded-posts.top { border-bottom: 1px solid $primary-low; -} +} .posts-wrapper { diff --git a/app/assets/stylesheets/desktop/topic.scss b/app/assets/stylesheets/desktop/topic.scss index b4387dcfca..a670968d9d 100644 --- a/app/assets/stylesheets/desktop/topic.scss +++ b/app/assets/stylesheets/desktop/topic.scss @@ -44,10 +44,9 @@ } .private-message-glyph { - color: dark-light-choose($primary-low-mid, $secondary-high); float: left; - margin-right: 5px; } + .private_message #topic-title .private-message-glyph { display: inline; } .topic-error { diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 7345548003..6244b8aaf8 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -147,26 +147,6 @@ } } - .secondary { - background: $secondary; - border-top: 1px solid $primary-low; - border-bottom: 1px solid $primary-low; - - dl { - padding: 8px 10px; - } - - dd { - display: inline; - margin: 0 10px 0 0; - } - - dt { - margin: 0 5px 0 0; - padding: 0; - } - } - .details { padding: 0 0 4px 0; margin-top: -200px; diff --git a/app/assets/stylesheets/mobile/group.scss b/app/assets/stylesheets/mobile/group.scss index 0ab1c39142..7bbf810764 100644 --- a/app/assets/stylesheets/mobile/group.scss +++ b/app/assets/stylesheets/mobile/group.scss @@ -19,19 +19,18 @@ } .group-nav.mobile-nav { - margin-bottom: 15px; float: left; } -.group-activity { +.group-activity, .group-manage { position: relative; } -.group-activity-nav, .group-messages-nav { +.group-navigation { &.mobile-nav { position: absolute; right: 0; - top: -57px; + top: -51px; } } @@ -51,7 +50,7 @@ } } -table.group-logs { +table.group-manage-logs { width: 130%; } @@ -80,3 +79,28 @@ table.group-logs { } } } + +.group-outlet { + .group-username-filter { + position: absolute; + right: 0px; + top: -42px; + height: 27px; + margin-bottom: 0px; + } + + .group-members-manage { + width: 100%; + text-align: right; + } + + .group-members-dropdown, + .group-members-add { + margin: 5px 0px; + } +} + + +.group-add-members-btn { + margin-bottom: 10px; +} diff --git a/app/assets/stylesheets/mobile/groups.scss b/app/assets/stylesheets/mobile/groups.scss index 71d3caa4f9..a7e6df7af1 100644 --- a/app/assets/stylesheets/mobile/groups.scss +++ b/app/assets/stylesheets/mobile/groups.scss @@ -3,12 +3,14 @@ margin-top: 20px; } - .groups-filter { + .groups-header-filters { display: block; float: none; } - .groups-name-filter { + .groups-header-filters-name, + .groups-header-filters-type, + .groups-header-new { margin-top: 10px; } } diff --git a/app/assets/stylesheets/mobile/search.scss b/app/assets/stylesheets/mobile/search.scss index c9903295ec..b27a8d326f 100644 --- a/app/assets/stylesheets/mobile/search.scss +++ b/app/assets/stylesheets/mobile/search.scss @@ -1,43 +1,45 @@ -.search button.btn-primary, .search button.btn { - float: none; -} +.search-container { + flex-direction: column; + margin-top: .5em; -.search-advanced { - .search-advanced-options { - .control-group { - margin-bottom: 10px; - .controls { - input:not([type="checkbox"]),select { - width: 75%; + .search-advanced { + order: 1; + width: 100%; + + .search-info { + flex-direction: column; + align-items: left; + justify-content: center; + + .sort-by { + display: flex; + align-items: center; + margin-top: .5em; + margin-left: 0; + width: 100%; + + .select-kit { + flex: 1 1 auto; } } } } -} -.search-title { - .sort-by { - display: flex; + .search-notice { + margin-top: 1em; + } + + .search-advanced-sidebar { + order: 0; width: 100%; - align-items: center; - .select-kit { - flex: 1 1 auto; + margin: 0; + + .tag-chooser { + width: 100%; } } } -.search.row { - margin-top: 10px; -} - -.search.row input.search { - height: 25px; -} - -.fps-search-context { - margin-bottom: 15px; -} - .fps-topic { max-width: 75%; } diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index 3b48e5c91e..ddb3b4c036 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -29,7 +29,7 @@ span.badge-posts { .topic-post { nav.post-controls { clear: both; - color: dark-light-choose($primary-low-mid, $secondary-high); + color: dark-light-choose($primary-low-mid, $secondary-high); .d-icon { opacity: 1.0; } @@ -51,7 +51,7 @@ span.badge-posts { } &.reply { float: right; - color: $primary-high; + color: $primary-high; } &.has-like { color: $love; @@ -62,8 +62,18 @@ span.badge-posts { } button.like-count { - font-size: $font-0; + font-size: $font-up-1; padding: 8px 4px; + i.fa-heart { + padding-left: 8px; + color: dark-light-choose($primary-low-mid, $secondary-high); + } + &.my-likes { + margin-right: 5px; + } + &.regular-likes { + margin-right: -5px; + } } } } @@ -404,7 +414,7 @@ blockquote { } .gap { - padding: 0.25em 0; + padding: 0.25em 0; } .gutter { display: none; } diff --git a/app/assets/stylesheets/mobile/topic.scss b/app/assets/stylesheets/mobile/topic.scss index 48e682e147..df79b7f6f3 100644 --- a/app/assets/stylesheets/mobile/topic.scss +++ b/app/assets/stylesheets/mobile/topic.scss @@ -31,10 +31,8 @@ .private-message-glyph { display: none; } } -.private-message-glyph { color: dark-light-choose($primary-low-mid, $secondary-high); } .private_message #topic-title .private-message-glyph { display: inline; } - /* both blocks that appear under the standard post control buttons */ .notification-options, .pinned-options { float: left; diff --git a/app/assets/stylesheets/mobile/upload.scss b/app/assets/stylesheets/mobile/upload.scss index ce8135103b..2d487d8013 100644 --- a/app/assets/stylesheets/mobile/upload.scss +++ b/app/assets/stylesheets/mobile/upload.scss @@ -1,6 +1,6 @@ .upload-selector { input[type="text"]{ - width: 520px; + width: calc(100% - 20px); } input[type="file"] { font-size: $font-0; @@ -9,6 +9,27 @@ .description { color: dark-light-choose($primary-medium, $secondary-medium); } + .radios { + display: flex; + flex-wrap: wrap; + align-items: center; + &:first-of-type { + margin-bottom: 1em; + } + input, label { + min-height: 20px; + line-height: $line-height-medium; + margin: 0; + } + .radio { + padding-left: 5px; + } + .inputs { + margin-top: 10px; + width: 100%; + } + } + } .uploaded-image-preview { diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index 09df789b5a..a8cd147c22 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -9,7 +9,7 @@ } .user-main { - +margin-top: 10px; table.group-members { width: 100%; p { @@ -77,18 +77,9 @@ } .secondary { - background: $primary-low; - dl { - display: flex; - flex-wrap: wrap; - align-items: center; - padding: 10px; - - } - - dd { - margin: 0 15px 0 5px; + padding: 5px 0; + } } diff --git a/app/controllers/admin/email_templates_controller.rb b/app/controllers/admin/email_templates_controller.rb index 3d83a71cd7..33e3c67f8a 100644 --- a/app/controllers/admin/email_templates_controller.rb +++ b/app/controllers/admin/email_templates_controller.rb @@ -17,6 +17,7 @@ class Admin::EmailTemplatesController < Admin::AdminController "system_messages.email_reject_screened_email", "system_messages.email_reject_unrecognized_error", "system_messages.pending_users_reminder", "system_messages.post_hidden", + "system_messages.post_hidden_again", "system_messages.restore_failed", "system_messages.restore_succeeded", "system_messages.spam_post_blocked", "system_messages.too_many_spam_flags", "system_messages.unsilenced", "system_messages.user_automatically_silenced", diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 5de992cee6..e56fe53c11 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -1,105 +1,45 @@ class Admin::GroupsController < Admin::AdminController - def index - groups = Group.order(:name).where("groups.id <> ?", Group::AUTO_GROUPS[:everyone]) - - if search = params[:search].to_s - groups = groups.where("name ILIKE ?", "%#{search}%") - end - - if params[:ignore_automatic].to_s == "true" - groups = groups.where(automatic: false) - end - - render_serialized(groups, BasicGroupSerializer) - end - - def show - render body: nil - end - def bulk - render body: nil end def bulk_perform - group = Group.find(params[:group_id].to_i) + group = Group.find_by(id: params[:group_id].to_i) + raise Discourse::NotFound unless group users_added = 0 - if group.present? - users = (params[:users] || []).map { |u| u.downcase } - valid_emails = {} - valid_usernames = {} - valid_users = User.joins(:user_emails) - .where("username_lower IN (:users) OR user_emails.email IN (:users)", users: users) - .pluck(:id, :username_lower, :"user_emails.email") + users = (params[:users] || []).map { |user| user.downcase!; user } + valid_emails = {} + valid_usernames = {} - valid_users.map! do |id, username_lower, email| - valid_emails[email] = valid_usernames[username_lower] = id - id - end - valid_users.uniq! - invalid_users = users.reject! { |u| valid_emails[u] || valid_usernames[u] } - group.bulk_add(valid_users) if valid_users.present? - users_added = valid_users.count + valid_users = User.joins(:user_emails) + .where("username_lower IN (:users) OR lower(user_emails.email) IN (:users)", users: users) + .pluck(:id, :username_lower, :"user_emails.email") + + valid_users.map! do |id, username_lower, email| + valid_emails[email] = valid_usernames[username_lower] = id + id end - render json: { success: true, message: I18n.t('groups.success.bulk_add', users_added: users_added), users_not_added: invalid_users } + valid_users.uniq! + invalid_users = users.reject { |u| valid_emails[u] || valid_usernames[u] } + group.bulk_add(valid_users) if valid_users.present? + users_added = valid_users.count + + response = success_json.merge(users_not_added: invalid_users) + + if users_added > 0 + response[:message] = I18n.t('groups.success.bulk_add', count: users_added) + end + + render json: response end def create - save_group(Group.new) - end + attributes = group_params.to_h.except(:owner_usernames, :usernames) + group = Group.new(attributes) - def update - group = Group.find(params[:id]) - - # group rename is ignored for automatic groups - group.name = group_params[:name] if group_params[:name] && !group.automatic - save_group(group) { |g| GroupActionLogger.new(current_user, g).log_change_group_settings } - end - - def save_group(group) - group.name = group_params[:name] if group_params[:name].present? && !group.automatic - group.mentionable_level = group_params[:mentionable_level].to_i if group_params[:mentionable_level].present? - group.messageable_level = group_params[:messageable_level].to_i if group_params[:messageable_level].present? - - if group_params[:visibility_level] - group.visibility_level = group_params[:visibility_level] - end - - grant_trust_level = group_params[:grant_trust_level].to_i - group.grant_trust_level = (grant_trust_level > 0 && grant_trust_level <= 4) ? grant_trust_level : nil - - group.automatic_membership_email_domains = group_params[:automatic_membership_email_domains] unless group.automatic - group.automatic_membership_retroactive = group_params[:automatic_membership_retroactive] == "true" unless group.automatic - - group.primary_group = group.automatic ? false : group_params["primary_group"] == "true" - - group.incoming_email = group.automatic ? nil : group_params[:incoming_email] - - title = group_params[:title] if group_params[:title].present? - group.title = group.automatic ? nil : title - - group.flair_url = group_params[:flair_url].presence - group.flair_bg_color = group_params[:flair_bg_color].presence - group.flair_color = group_params[:flair_color].presence - - %i{public_admission public_exit}.each do |key| - if group_params[key] - group.public_send("#{key}=", group_params[key]) - end - end - - group.bio_raw = group_params[:bio_raw] if group_params[:bio_raw] - group.full_name = group_params[:full_name] if group_params[:full_name] - - if group_params.key?(:default_notification_level) - group.default_notification_level = group_params[:default_notification_level] - end - - if group_params[:allow_membership_requests] - group.allow_membership_requests = group_params[:allow_membership_requests] - group.membership_request_template = group_params[:membership_request_template] + unless group_params[:allow_membership_requests] + group.membership_request_template = nil end if group_params[:owner_usernames].present? @@ -123,9 +63,6 @@ class Admin::GroupsController < Admin::AdminController if group.save group.restore_user_count! - - yield(group) if block_given? - render_serialized(group, BasicGroupSerializer) else render_json_error group @@ -133,23 +70,21 @@ class Admin::GroupsController < Admin::AdminController end def destroy - group = Group.find(params[:id]) + group = Group.find_by(id: params[:id]) + raise Discourse::NotFound unless group if group.automatic can_not_modify_automatic else - group.destroy + group.destroy! render json: success_json end end - def refresh_automatic_groups - Group.refresh_automatic_groups! - render json: success_json - end - def add_owners - group = Group.find(params.require(:id)) + group = Group.find_by(id: params.require(:id)) + raise Discourse::NotFound unless group + return can_not_modify_automatic if group.automatic users = User.where(username: group_params[:usernames].split(",")) @@ -170,7 +105,9 @@ class Admin::GroupsController < Admin::AdminController end def remove_owner - group = Group.find(params.require(:id)) + group = Group.find_by(params.require(:id)) + raise Discourse::NotFound unless group + return can_not_modify_automatic if group.automatic user = User.find(params[:user_id].to_i) @@ -211,9 +148,9 @@ class Admin::GroupsController < Admin::AdminController :allow_membership_requests, :full_name, :default_notification_level, - :usernames, + :membership_request_template, :owner_usernames, - :membership_request_template + :usernames ) end end diff --git a/app/controllers/admin/themes_controller.rb b/app/controllers/admin/themes_controller.rb index 99b4caa3bd..a40ec46998 100644 --- a/app/controllers/admin/themes_controller.rb +++ b/app/controllers/admin/themes_controller.rb @@ -78,7 +78,8 @@ class Admin::ThemesController < Admin::AdminController begin @theme = RemoteTheme.import_theme(params[:remote], current_user, private_key: params[:private_key]) render json: @theme, status: :created - rescue RuntimeError + rescue RuntimeError => e + Discourse.warn_exception(e, message: "Error importing theme") render_json_error I18n.t('themes.error_importing') end elsif params[:bundle] diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index d8775cb177..9305e4cebd 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -308,7 +308,8 @@ class Admin::UsersController < Admin::AdminController silenced: true, silence_reason: silencer.user_history.try(:details), silenced_till: @user.silenced_till, - suspended_at: @user.silenced_at + suspended_at: @user.silenced_at, + silenced_by: BasicUserSerializer.new(current_user, root: false).as_json } ) end @@ -389,7 +390,13 @@ class Admin::UsersController < Admin::AdminController ip = params[:ip] # should we cache results in redis? - location = Excon.get("https://ipinfo.io/#{ip}/json", read_timeout: 10, connect_timeout: 10).body rescue nil + begin + location = Excon.get( + "https://ipinfo.io/#{ip}/json", + read_timeout: 10, connect_timeout: 10 + )&.body + rescue Excon::Error + end render json: location end @@ -423,7 +430,7 @@ class Admin::UsersController < Admin::AdminController } AdminUserIndexQuery.new(params).find_users(50).each do |user| - user_destroyer.destroy(user, options) rescue nil + user_destroyer.destroy(user, options) end render json: success_json diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f2a7b47501..89719d67d7 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -641,6 +641,8 @@ class ApplicationController < ActionController::Base # save original URL in a session so we can redirect after login session[:destination_url] = destination_url redirect_to path('/session/sso') + elsif params[:authComplete].present? + redirect_to path("/login?authComplete=true") else # save original URL in a cookie (javascript redirects after login in this case) cookies[:destination_url] = destination_url diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index aaaf1ec7bd..6fb2bd07cc 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -149,6 +149,7 @@ class CategoriesController < ApplicationController old_permissions = cat.permissions_params if result = cat.update(category_params) + DiscourseEvent.trigger(:category_updated, cat) Scheduler::Defer.later "Log staff action change category settings" do @staff_action_logger.log_category_settings_change(@category, category_params, old_permissions) end @@ -165,6 +166,7 @@ class CategoriesController < ApplicationController custom_slug = params[:slug].to_s if custom_slug.present? && @category.update_attributes(slug: custom_slug) + DiscourseEvent.trigger(:category_updated, @category) render json: success_json else render_json_error(@category) @@ -281,6 +283,7 @@ class CategoriesController < ApplicationController :default_view, :subcategory_list_style, :default_top_period, + :minimum_required_tags, custom_fields: [params[:custom_fields].try(:keys)], permissions: [*p.try(:keys)], allowed_tags: [], diff --git a/app/controllers/embed_controller.rb b/app/controllers/embed_controller.rb index fff17a578a..686019e850 100644 --- a/app/controllers/embed_controller.rb +++ b/app/controllers/embed_controller.rb @@ -103,9 +103,12 @@ class EmbedController < ApplicationController end def ensure_embeddable + if !(Rails.env.development? && current_user&.admin?) + referer = request.referer - if !(Rails.env.development? && current_user.try(:admin?)) - raise Discourse::InvalidAccess.new('invalid referer host') unless EmbeddableHost.url_allowed?(request.referer) + unless referer && EmbeddableHost.url_allowed?(referer) + raise Discourse::InvalidAccess.new('invalid referer host') + end end response.headers['X-Frame-Options'] = "ALLOWALL" diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index b41f170edf..ff14238113 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,5 +1,4 @@ class GroupsController < ApplicationController - requires_login only: [ :set_notifications, :mentionable, @@ -7,7 +6,8 @@ class GroupsController < ApplicationController :update, :histories, :request_membership, - :search + :search, + :new ] skip_before_action :preload_json, :check_xhr, only: [:posts_feed, :mentions_feed] @@ -37,7 +37,7 @@ class GroupsController < ApplicationController } def index - unless SiteSetting.enable_group_directory? + unless SiteSetting.enable_group_directory? || current_user&.staff? raise Discourse::InvalidAccess.new(:enable_group_directory) end @@ -108,20 +108,35 @@ class GroupsController < ApplicationController end format.json do - render_serialized(group, GroupShowSerializer, root: 'basic_group') + groups = Group.visible_groups(current_user) + + if !guardian.is_staff? + groups = groups.where(automatic: false) + end + + render_json_dump( + group: serialize_data(group, GroupShowSerializer, root: nil), + extras: { + visible_group_names: groups.pluck(:name) + } + ) end end end + def new + end + def edit end def update group = Group.find(params[:id]) - guardian.ensure_can_edit!(group) + guardian.ensure_can_edit!(group) unless current_user.admin - if group.update_attributes(group_params) + if group.update(group_params(automatic: group.automatic)) GroupActionLogger.new(current_user, group).log_change_group_settings + DiscourseEvent.trigger(:group_updated, group) render json: success_json else @@ -253,7 +268,7 @@ class GroupsController < ApplicationController if (usernames = group.users.where(id: users.pluck(:id)).pluck(:username)).present? render_json_error(I18n.t( "groups.errors.member_already_exist", - username: usernames.join(", "), + username: usernames.sort.join(", "), count: usernames.size )) else @@ -289,7 +304,8 @@ class GroupsController < ApplicationController end def remove_member - group = Group.find(params[:id]) + group = Group.find_by(id: params[:id]) + raise Discourse::NotFound unless group group.public_exit ? ensure_logged_in : guardian.ensure_can_edit!(group) user = @@ -373,7 +389,7 @@ class GroupsController < ApplicationController def histories group = find_group(:group_id) - guardian.ensure_can_edit!(group) + guardian.ensure_can_edit!(group) unless current_user.admin page_size = 25 offset = (params[:offset] && params[:offset].to_i) || 0 @@ -410,23 +426,54 @@ class GroupsController < ApplicationController private - def group_params - params.require(:group).permit( - :flair_url, - :flair_bg_color, - :flair_color, - :bio_raw, - :full_name, - :public_admission, - :public_exit, - :allow_membership_requests, - :membership_request_template, - ) + def group_params(automatic: false) + permitted_params = + if automatic + %i{ + visibility_level + mentionable_level + messageable_level + default_notification_level + } + else + default_params = %i{ + mentionable_level + messageable_level + title + flair_url + flair_bg_color + flair_color + bio_raw + public_admission + public_exit + allow_membership_requests + full_name + default_notification_level + membership_request_template + } + + if current_user.admin + default_params.push(*[ + :incoming_email, + :primary_group, + :visibility_level, + :name, + :grant_trust_level, + :automatic_membership_email_domains, + :automatic_membership_retroactive + ]) + end + + default_params + end + + params.require(:group).permit(*permitted_params) end def find_group(param_name) name = params.require(param_name) - group = Group.find_by("lower(name) = ?", name.downcase) + group = Group + group = group.find_by("lower(name) = ?", name.downcase) guardian.ensure_can_see!(group) group end diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index 659e66f008..bf07fdc028 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -71,15 +71,24 @@ class ListController < ApplicationController list = TopicQuery.new(user, list_opts).public_send("list_#{filter}") - if @category.present? && guardian.can_create_shared_draft? - shared_drafts = TopicQuery.new( - user, - category: SiteSetting.shared_drafts_category, - destination_category_id: list_opts[:category] - ).list_latest + if guardian.can_create_shared_draft? && @category.present? + if @category.id == SiteSetting.shared_drafts_category.to_i + # On shared drafts, show the destination category + list.topics.each do |t| + t.includes_destination_category = true + end + else + # When viewing a non-shared draft category, find topics whose + # destination are this category + shared_drafts = TopicQuery.new( + user, + category: SiteSetting.shared_drafts_category, + destination_category_id: list_opts[:category] + ).list_latest - if shared_drafts.present? && shared_drafts.topics.present? - list.shared_drafts = shared_drafts.topics + if shared_drafts.present? && shared_drafts.topics.present? + list.shared_drafts = shared_drafts.topics + end end end diff --git a/app/controllers/robots_txt_controller.rb b/app/controllers/robots_txt_controller.rb index 4a8600774c..b5dfdb3e62 100644 --- a/app/controllers/robots_txt_controller.rb +++ b/app/controllers/robots_txt_controller.rb @@ -2,8 +2,60 @@ class RobotsTxtController < ApplicationController layout false skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required + # NOTE: order is important! + DISALLOWED_PATHS ||= %w{ + /auth/cas + /auth/facebook/callback + /auth/twitter/callback + /auth/google/callback + /auth/yahoo/callback + /auth/github/callback + /auth/cas/callback + /assets/browser-update*.js + /users/ + /u/ + /badges/ + /search + /search/ + /tags + /tags/ + /email/ + /session + /session/ + /admin + /admin/ + /user-api-key + /user-api-key/ + /*?api_key* + /*?*api_key* + /groups + /groups/ + /t/*/*.rss + /tags/*.rss + /c/*.rss + } + def index - path = SiteSetting.allow_index_in_robots_txt ? :index : :no_index + if SiteSetting.allow_index_in_robots_txt + path = :index + + @crawler_delayed_agents = SiteSetting.slow_down_crawler_user_agents.split('|').map { |agent| + [agent, SiteSetting.slow_down_crawler_rate] + } + + if SiteSetting.whitelisted_crawler_user_agents.present? + @allowed_user_agents = SiteSetting.whitelisted_crawler_user_agents.split('|') + @disallowed_user_agents = ['*'] + elsif SiteSetting.blacklisted_crawler_user_agents.present? + @allowed_user_agents = ['*'] + @disallowed_user_agents = SiteSetting.blacklisted_crawler_user_agents.split('|') + else + @allowed_user_agents = ['*'] + end + else + path = :no_index + end + render path, content_type: 'text/plain' end end diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index 2421ebbcae..2e8a75ab07 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -10,7 +10,7 @@ class SessionController < ApplicationController before_action :check_local_login_allowed, only: %i(create forgot_password email_login) before_action :rate_limit_login, only: %i(create email_login) skip_before_action :redirect_to_login_if_required - skip_before_action :preload_json, :check_xhr, only: %i(sso sso_login become sso_provider destroy email_login) + skip_before_action :preload_json, :check_xhr, only: %i(sso sso_login sso_provider destroy email_login) ACTIVATE_USER_KEY = "activate_user" @@ -52,7 +52,7 @@ class SessionController < ApplicationController sso.external_id = current_user.id.to_s sso.admin = current_user.admin? sso.moderator = current_user.moderator? - sso.groups = current_user.groups.pluck(:name) + sso.groups = current_user.groups.pluck(:name).join(",") if sso.return_sso_url.blank? render plain: "return_sso_url is blank, it must be provided", status: 400 @@ -75,13 +75,17 @@ class SessionController < ApplicationController # For use in development mode only when login options could be limited or disabled. # NEVER allow this to work in production. - def become - raise Discourse::InvalidAccess.new unless Rails.env.development? - user = User.find_by_username(params[:session_id]) - raise "User #{params[:session_id]} not found" if user.blank? + if !Rails.env.production? + skip_before_action :check_xhr, only: [:become] - log_on_user(user) - redirect_to path("/") + def become + raise Discourse::InvalidAccess if Rails.env.production? + user = User.find_by_username(params[:session_id]) + raise "User #{params[:session_id]} not found" if user.blank? + + log_on_user(user) + redirect_to path("/") + end end def sso_login diff --git a/app/controllers/stylesheets_controller.rb b/app/controllers/stylesheets_controller.rb index ef1837eab9..7377d47989 100644 --- a/app/controllers/stylesheets_controller.rb +++ b/app/controllers/stylesheets_controller.rb @@ -38,7 +38,13 @@ class StylesheetsController < ApplicationController end cache_time = request.env["HTTP_IF_MODIFIED_SINCE"] - cache_time = Time.rfc2822(cache_time) rescue nil if cache_time + + if cache_time + begin + cache_time = Time.rfc2822(cache_time) + rescue ArgumentError + end + end query = StylesheetCache.where(target: target) if digest @@ -82,12 +88,21 @@ class StylesheetsController < ApplicationController def handle_missing_cache(location, name, digest) location = location.sub(".css.map", ".css") source_map_location = location + ".map" + existing = read_file(location) - existing = File.read(location) rescue nil if existing && digest - source_map = File.read(source_map_location) rescue nil + source_map = read_file(source_map_location) StylesheetCache.add(name, digest, existing, source_map) end end + private + + def read_file(location) + begin + File.read(location) + rescue Errno::ENOENT + end + end + end diff --git a/app/controllers/tag_groups_controller.rb b/app/controllers/tag_groups_controller.rb index 8d819827f7..069f73830a 100644 --- a/app/controllers/tag_groups_controller.rb +++ b/app/controllers/tag_groups_controller.rb @@ -70,7 +70,20 @@ class TagGroupsController < ApplicationController end def tag_groups_params - result = params.permit(:id, :name, :one_per_topic, tag_names: [], parent_tag_name: []) + if permissions = params[:permissions] + permissions.each do |k, v| + permissions[k] = v.to_i + end + end + + result = params.permit( + :id, + :name, + :one_per_topic, + tag_names: [], + parent_tag_name: [], + permissions: [*permissions&.keys] + ) result[:tag_names] ||= [] result[:parent_tag_name] ||= [] result[:one_per_topic] = (params[:one_per_topic] == "true") diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 3a6d5f0d67..4069ab6093 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -44,7 +44,10 @@ class TagsController < ::ApplicationController extras: { tag_groups: grouped_tag_counts } } else - unrestricted_tags = Tag.where("tags.id NOT IN (select tag_id from category_tags) AND tags.topic_count > 0") + unrestricted_tags = DiscourseTagging.filter_visible( + Tag.where("tags.id NOT IN (select tag_id from category_tags) AND tags.topic_count > 0"), + guardian + ) categories = Category.where("id in (select category_id from category_tags)") .where("id in (?)", guardian.allowed_category_ids) @@ -107,6 +110,7 @@ class TagsController < ::ApplicationController tag.name = new_tag_name if tag.save StaffActionLogger.new(current_user).log_custom('renamed_tag', previous_value: params[:tag_id], new_value: new_tag_name) + DiscourseEvent.trigger(:tag_updated, tag) render json: { tag: { id: new_tag_name } } else render_json_error tag.errors.full_messages diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 9f6467212d..9f4fe20b35 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -117,6 +117,14 @@ class TopicsController < ApplicationController canonical_url UrlHelper.absolute_without_cdn(@topic_view.canonical_path) + # provide hint to crawlers only for now + # we would like to give them a bit more signal about age of data + if use_crawler_layout? + if last_modified = @topic_view.posts&.map { |p| p.updated_at }&.max&.httpdate + response.headers['Last-Modified'] = last_modified + end + end + perform_show_response rescue Discourse::InvalidAccess => ex @@ -668,9 +676,10 @@ class TopicsController < ApplicationController raise ActionController::ParameterMissing.new(:topic_ids) end - operation = params.require(:operation) - operation.permit! - operation = operation.to_h.symbolize_keys + operation = params + .require(:operation) + .permit(:type, :group, :category_id, :notification_level_id, tags: []) + .to_h.symbolize_keys raise ActionController::ParameterMissing.new(:operation_type) if operation[:type].blank? operator = TopicsBulkAction.new(current_user, topic_ids, operation, group: operation[:group]) diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 39fc24124b..b2e1b9fac2 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -127,7 +127,7 @@ class UploadsController < ApplicationController upload.errors.empty? ? upload : { errors: upload.errors.values.flatten } ensure - tempfile&.close! rescue nil + tempfile&.close! end end diff --git a/app/controllers/user_badges_controller.rb b/app/controllers/user_badges_controller.rb index cd456a8c40..b6b415d69a 100644 --- a/app/controllers/user_badges_controller.rb +++ b/app/controllers/user_badges_controller.rb @@ -58,7 +58,11 @@ class UserBadgesController < ApplicationController post_id = nil if params[:reason].present? - path = URI.parse(params[:reason]).path rescue nil + path = begin + URI.parse(params[:reason]).path + rescue URI::InvalidURIError + end + route = Rails.application.routes.recognize_path(path) if path if route topic_id = route[:topic_id].to_i diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 768898585a..c8edddf65b 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -48,7 +48,11 @@ class Users::OmniauthCallbacksController < ApplicationController end if origin.present? - parsed = URI.parse(origin) rescue nil + parsed = begin + URI.parse(origin) + rescue URI::InvalidURIError + end + if parsed @origin = "#{parsed.path}?#{parsed.query}" end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 1a0118a152..61507d2c0d 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -592,7 +592,7 @@ class UsersController < ApplicationController end email_token_user = EmailToken.confirmable(token)&.user - totp_enabled = email_token_user.totp_enabled? + totp_enabled = email_token_user&.totp_enabled? second_factor_token = params[:second_factor_token] confirm_email = false @@ -1064,7 +1064,7 @@ class UsersController < ApplicationController permitted.concat UserUpdater::OPTION_ATTR permitted.concat UserUpdater::CATEGORY_IDS.keys.map { |k| { k => [] } } - permitted.concat UserUpdater::TAG_NAMES.keys.map { |k| { k => [] } } + permitted.concat UserUpdater::TAG_NAMES.keys result = params .permit(permitted) diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb index 1c56b33dd1..b6e1ad2093 100644 --- a/app/controllers/webhooks_controller.rb +++ b/app/controllers/webhooks_controller.rb @@ -77,8 +77,8 @@ class WebhooksController < ActionController::Base def mandrill events = params["mandrill_events"] events.each do |event| - message_id = event["msg"]["metadata"]["message_id"] rescue nil - to_address = event["msg"]["email"] rescue nil + message_id = event.dig("msg", "metadata", "message_id") + to_address = event.dig("msg", "email") case event["event"] when "hard_bounce" @@ -94,12 +94,12 @@ class WebhooksController < ActionController::Base def sparkpost events = params["_json"] || [params] events.each do |event| - message_event = event["msys"]["message_event"] rescue nil + message_event = event.dig("msys", "message_event") next unless message_event - message_id = message_event["rcpt_meta"]["message_id"] rescue nil - to_address = message_event["rcpt_to"] rescue nil - bounce_class = message_event["bounce_class"] rescue nil + message_id = message_event.dig("rcpt_meta", "message_id") + to_address = message_event["rcpt_to"] + bounce_class = message_event["bounce_class"] next unless bounce_class bounce_class = bounce_class.to_i diff --git a/app/helpers/common_helper.rb b/app/helpers/common_helper.rb index 1d8317554c..6e37098d2d 100644 --- a/app/helpers/common_helper.rb +++ b/app/helpers/common_helper.rb @@ -5,9 +5,15 @@ module CommonHelper end end - def render_google_tag_manager_code + def render_google_tag_manager_head_code if Rails.env.production? && SiteSetting.gtm_container_id.present? - render partial: "common/google_tag_manager" + render partial: "common/google_tag_manager_head" + end + end + + def render_google_tag_manager_body_code + if Rails.env.production? && SiteSetting.gtm_container_id.present? + render partial: "common/google_tag_manager_body" end end end diff --git a/app/helpers/user_notifications_helper.rb b/app/helpers/user_notifications_helper.rb index 3e9683359e..dd57d45794 100644 --- a/app/helpers/user_notifications_helper.rb +++ b/app/helpers/user_notifications_helper.rb @@ -37,7 +37,7 @@ module UserNotificationsHelper result = "" length = 0 - doc.css('body > p, aside.onebox').each do |node| + doc.css('body > p, aside.onebox, body > ul').each do |node| if node.text.present? result << node.to_s length += node.inner_text.length diff --git a/app/jobs/regular/bulk_invite.rb b/app/jobs/regular/bulk_invite.rb index 5aa29987be..2d6f0e6454 100644 --- a/app/jobs/regular/bulk_invite.rb +++ b/app/jobs/regular/bulk_invite.rb @@ -17,15 +17,15 @@ module Jobs filename = args[:filename] @current_user = User.find_by(id: args[:current_user_id]) raise Discourse::InvalidParameters.new(:filename) if filename.blank? + csv_path = "#{Invite.base_directory}/#{filename}" # read csv file, and send out invitations - read_csv_file("#{Invite.base_directory}/#{filename}") + read_csv_file(csv_path) ensure # send notification to user regarding progress notify_user - # since emails have already been sent out, delete the uploaded csv file - FileUtils.rm_rf(csv_path) rescue nil + FileUtils.rm_rf(csv_path) if csv_path end def read_csv_file(csv_path) diff --git a/app/jobs/regular/emit_web_hook_event.rb b/app/jobs/regular/emit_web_hook_event.rb index cc80fcebb2..5ec6f4fa68 100644 --- a/app/jobs/regular/emit_web_hook_event.rb +++ b/app/jobs/regular/emit_web_hook_event.rb @@ -39,7 +39,7 @@ module Jobs end def setup_topic(args) - topic_view = (TopicView.new(args[:topic_id], Discourse.system_user) rescue nil) + topic_view = TopicView.new(args[:topic_id], Discourse.system_user) return if topic_view.blank? args[:payload] = WebHookTopicViewSerializer.new(topic_view, scope: guardian, root: false).as_json end @@ -50,6 +50,30 @@ module Jobs args[:payload] = WebHookUserSerializer.new(user, scope: guardian, root: false).as_json end + def setup_group(args) + group = Group.find(args[:group_id]) + return if group.blank? + args[:payload] = WebHookGroupSerializer.new(group, scope: guardian, root: false).as_json + end + + def setup_category(args) + category = Category.find(args[:category_id]) + return if category.blank? + args[:payload] = WebHookCategorySerializer.new(category, scope: guardian, root: false).as_json + end + + def setup_tag(args) + tag = Tag.find(args[:tag_id]) + return if tag.blank? + args[:payload] = TagSerializer.new(tag, scope: guardian, root: false).as_json + end + + def setup_flag(args) + flag = PostAction.find(args[:flag_id]) + return if flag.blank? + args[:payload] = WebHookFlagSerializer.new(flag, scope: guardian, root: false).as_json + end + def ping_event?(event_type) event_type.to_s == 'ping'.freeze end diff --git a/app/jobs/regular/export_csv_file.rb b/app/jobs/regular/export_csv_file.rb index 0a0899e572..1212bb1c87 100644 --- a/app/jobs/regular/export_csv_file.rb +++ b/app/jobs/regular/export_csv_file.rb @@ -142,8 +142,8 @@ module Jobs def report_export return enum_for(:report_export) unless block_given? - @extra[:start_date] = @extra[:start_date].to_date if @extra[:start_date].is_a?(String) - @extra[:end_date] = @extra[:end_date].to_date if @extra[:end_date].is_a?(String) + @extra[:start_date] = @extra[:start_date].to_date.beginning_of_day if @extra[:start_date].is_a?(String) + @extra[:end_date] = @extra[:end_date].to_date.end_of_day if @extra[:end_date].is_a?(String) @extra[:category_id] = @extra[:category_id].present? ? @extra[:category_id].to_i : nil @extra[:group_id] = @extra[:group_id].present? ? @extra[:group_id].to_i : nil @@ -152,7 +152,7 @@ module Jobs report_hash[row[:x].to_s(:db)] = row[:y].to_s(:db) end - (@extra[:start_date]..@extra[:end_date]).each do |date| + (@extra[:start_date].to_date..@extra[:end_date].to_date).each do |date| yield [date.to_s(:db), report_hash.fetch(date.to_s(:db), 0)] end end diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb index b920c60590..ce88cd6546 100644 --- a/app/jobs/regular/pull_hotlinked_images.rb +++ b/app/jobs/regular/pull_hotlinked_images.rb @@ -24,7 +24,7 @@ module Jobs follow_redirect: true ) rescue - if (retries -= 1) > 0 + if (retries -= 1) > 0 && !Rails.env.test? sleep 1 retry end @@ -47,9 +47,9 @@ module Jobs downloaded_urls = {} - large_images = JSON.parse(post.custom_fields[Post::LARGE_IMAGES].presence || "[]") rescue [] - broken_images = JSON.parse(post.custom_fields[Post::BROKEN_IMAGES].presence || "[]") rescue [] - downloaded_images = JSON.parse(post.custom_fields[Post::DOWNLOADED_IMAGES].presence || "{}") rescue {} + large_images = JSON.parse(post.custom_fields[Post::LARGE_IMAGES].presence || "[]") + broken_images = JSON.parse(post.custom_fields[Post::BROKEN_IMAGES].presence || "[]") + downloaded_images = JSON.parse(post.custom_fields[Post::DOWNLOADED_IMAGES].presence || "{}") has_new_large_image = false has_new_broken_image = false @@ -62,7 +62,9 @@ module Jobs if is_valid_image_url(src) begin # have we already downloaded that file? - unless downloaded_images.include?(src) || large_images.include?(src) || broken_images.include?(src) + schemeless_src = remove_scheme(original_src) + + unless downloaded_images.include?(schemeless_src) || large_images.include?(schemeless_src) || broken_images.include?(schemeless_src) if hotlinked = download(src) if File.size(hotlinked.path) <= @max_size filename = File.basename(URI.parse(src).path) @@ -70,17 +72,17 @@ module Jobs upload = UploadCreator.new(hotlinked, filename, origin: src).create_for(post.user_id) if upload.persisted? downloaded_urls[src] = upload.url - downloaded_images[src.sub(/^https?:/i, "")] = upload.id + downloaded_images[remove_scheme(src)] = upload.id has_downloaded_image = true else log(:info, "Failed to pull hotlinked image for post: #{post_id}: #{src} - #{upload.errors.full_messages.join("\n")}") end else - large_images << original_src.sub(/^https?:/i, "") + large_images << remove_scheme(original_src) has_new_large_image = true end else - broken_images << original_src.sub(/^https?:/i, "") + broken_images << remove_scheme(original_src) has_new_broken_image = true end end @@ -170,6 +172,11 @@ module Jobs ) end + private + + def remove_scheme(src) + src.sub(/^https?:/i, "") + end end end diff --git a/app/jobs/scheduled/clean_up_crawler_stats.rb b/app/jobs/scheduled/clean_up_crawler_stats.rb new file mode 100644 index 0000000000..816e91f0fd --- /dev/null +++ b/app/jobs/scheduled/clean_up_crawler_stats.rb @@ -0,0 +1,26 @@ +module Jobs + + class CleanUpCrawlerStats < Jobs::Scheduled + every 1.day + + def execute(args) + WebCrawlerRequest.where('date < ?', WebCrawlerRequest.max_record_age.ago).delete_all + + # keep count of only the top user agents + WebCrawlerRequest.exec_sql <<~SQL + WITH ranked_requests AS ( + SELECT row_number() OVER (ORDER BY count DESC) as row_number, id + FROM web_crawler_requests + WHERE date = '#{1.day.ago.strftime("%Y-%m-%d")}' + ) + DELETE FROM web_crawler_requests + WHERE id IN ( + SELECT ranked_requests.id + FROM ranked_requests + WHERE row_number > #{WebCrawlerRequest.max_records_per_day} + ) + SQL + end + end + +end diff --git a/app/models/admin_dashboard_data.rb b/app/models/admin_dashboard_data.rb index 8ba294d424..797e3176eb 100644 --- a/app/models/admin_dashboard_data.rb +++ b/app/models/admin_dashboard_data.rb @@ -21,6 +21,7 @@ class AdminDashboardData PRIVATE_MESSAGE_REPORTS ||= [ 'user_to_user_private_messages', + 'user_to_user_private_messages_with_replies', 'system_private_messages', 'notify_moderators_private_messages', 'notify_user_private_messages', diff --git a/app/models/application_request.rb b/app/models/application_request.rb index f13e7275a7..1e4459f681 100644 --- a/app/models/application_request.rb +++ b/app/models/application_request.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true class ApplicationRequest < ActiveRecord::Base + enum req_type: %i(http_total http_2xx http_background @@ -12,41 +13,12 @@ class ApplicationRequest < ActiveRecord::Base page_view_logged_in_mobile page_view_anon_mobile) - cattr_accessor :autoflush, :autoflush_seconds, :last_flush - # auto flush if backlog is larger than this - self.autoflush = 2000 - - # auto flush if older than this - self.autoflush_seconds = 5.minutes - self.last_flush = Time.now.utc + include CachedCounting def self.increment!(type, opts = nil) - key = redis_key(type) - val = $redis.incr(key).to_i - - # readonly mode it is going to be 0, skip - return if val == 0 - - # 3.days, see: https://github.com/rails/rails/issues/21296 - $redis.expire(key, 259200) - - autoflush = (opts && opts[:autoflush]) || self.autoflush - if autoflush > 0 && val >= autoflush - write_cache! - return - end - - if (Time.now.utc - last_flush).to_i > autoflush_seconds - write_cache! - end + perform_increment!(redis_key(type), opts) end - GET_AND_RESET = <<~LUA - local val = redis.call('get', KEYS[1]) - redis.call('set', KEYS[1], '0') - return val - LUA - def self.write_cache!(date = nil) if date.nil? write_cache!(Time.now.utc) @@ -58,13 +30,9 @@ class ApplicationRequest < ActiveRecord::Base date = date.to_date - # this may seem a bit fancy but in so it allows - # for concurrent calls without double counting req_types.each do |req_type, _| - key = redis_key(req_type, date) + val = get_and_reset(redis_key(req_type, date)) - namespaced_key = $redis.namespace_key(key) - val = $redis.without_namespace.eval(GET_AND_RESET, keys: [namespaced_key]).to_i next if val == 0 id = req_id(date, req_type) diff --git a/app/models/category.rb b/app/models/category.rb index e551227ed6..432509ded6 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -61,6 +61,9 @@ class Category < ActiveRecord::Base after_update :rename_category_definition, if: :saved_change_to_name? after_update :create_category_permalink, if: :saved_change_to_slug? + after_commit :trigger_category_created_event, on: :create + after_commit :trigger_category_destroyed_event, on: :destroy + belongs_to :parent_category, class_name: 'Category' has_many :subcategories, class_name: 'Category', foreign_key: 'parent_category_id' @@ -321,6 +324,30 @@ SQL end end + def self.resolve_permissions(permissions) + read_restricted = true + + everyone = Group::AUTO_GROUPS[:everyone] + full = CategoryGroup.permission_types[:full] + + mapped = permissions.map do |group, permission| + group_id = Group.group_id_from_param(group) + permission = CategoryGroup.permission_types[permission] unless permission.is_a?(Integer) + + [group_id, permission] + end + + mapped.each do |group, permission| + if group == everyone && permission == full + return [false, []] + end + + read_restricted = false if group == everyone + end + + [read_restricted, mapped] + end + def allowed_tags=(tag_names_arg) DiscourseTagging.add_or_create_tags_by_name(self, tag_names_arg, unlimited: true) end @@ -379,33 +406,6 @@ SQL self.update_attributes(latest_topic_id: latest_topic_id, latest_post_id: latest_post_id) end - def self.resolve_permissions(permissions) - read_restricted = true - - everyone = Group::AUTO_GROUPS[:everyone] - full = CategoryGroup.permission_types[:full] - - mapped = permissions.map do |group, permission| - group = group.id if group.is_a?(Group) - - # subtle, using Group[] ensures the group exists in the DB - group = Group[group.to_sym].id unless group.is_a?(Integer) - permission = CategoryGroup.permission_types[permission] unless permission.is_a?(Integer) - - [group, permission] - end - - mapped.each do |group, permission| - if group == everyone && permission == full - return [false, []] - end - - read_restricted = false if group == everyone - end - - [read_restricted, mapped] - end - def self.query_parent_category(parent_slug) self.where(slug: parent_slug, parent_category_id: nil).pluck(:id).first || self.where(id: parent_slug.to_i).pluck(:id).first @@ -511,6 +511,16 @@ SQL def subcategory_list_includes_topics? subcategory_list_style.end_with?("with_featured_topics") end + + def trigger_category_created_event + DiscourseEvent.trigger(:category_created, self) + true + end + + def trigger_category_destroyed_event + DiscourseEvent.trigger(:category_destroyed, self) + true + end end # == Schema Information @@ -563,10 +573,11 @@ end # default_top_period :string(20) default("all") # mailinglist_mirror :boolean default(FALSE), not null # suppress_from_latest :boolean default(FALSE) +# minimum_required_tags :integer default(0) # # Indexes # # index_categories_on_email_in (email_in) UNIQUE # index_categories_on_topic_count (topic_count) -# unique_index_categories_on_name ((COALESCE(parent_category_id, '-1'::integer)), name) UNIQUE +# unique_index_categories_on_name (COALESCE(parent_category_id, '-1'::integer), name) UNIQUE # diff --git a/app/models/concerns/cached_counting.rb b/app/models/concerns/cached_counting.rb new file mode 100644 index 0000000000..867c8fb250 --- /dev/null +++ b/app/models/concerns/cached_counting.rb @@ -0,0 +1,69 @@ +module CachedCounting + extend ActiveSupport::Concern + + included do + class << self + attr_accessor :autoflush, :autoflush_seconds, :last_flush + end + + # auto flush if backlog is larger than this + self.autoflush = 2000 + + # auto flush if older than this + self.autoflush_seconds = 5.minutes + + self.last_flush = Time.now.utc + end + + class_methods do + def perform_increment!(key, opts = nil) + val = $redis.incr(key).to_i + + # readonly mode it is going to be 0, skip + return if val == 0 + + # 3.days, see: https://github.com/rails/rails/issues/21296 + $redis.expire(key, 259200) + + autoflush = (opts && opts[:autoflush]) || self.autoflush + if autoflush > 0 && val >= autoflush + write_cache! + return + end + + if (Time.now.utc - last_flush).to_i > autoflush_seconds + write_cache! + end + end + + def write_cache!(date = nil) + raise NotImplementedError + end + + GET_AND_RESET = <<~LUA + local val = redis.call('get', KEYS[1]) + redis.call('set', KEYS[1], '0') + return val + LUA + + # this may seem a bit fancy but in so it allows + # for concurrent calls without double counting + def get_and_reset(key) + namespaced_key = $redis.namespace_key(key) + val = $redis.without_namespace.eval(GET_AND_RESET, keys: [namespaced_key]).to_i + $redis.expire(key, 259200) # SET removes expiry, so set it again + val + end + + def request_id(query_params, retries = 0) + id = where(query_params).pluck(:id).first + id ||= create!(query_params.merge(count: 0)).id + rescue # primary key violation + if retries == 0 + request_id(query_params, 1) + else + raise + end + end + end +end diff --git a/app/models/discourse_single_sign_on.rb b/app/models/discourse_single_sign_on.rb index f25a716431..bc8988d9de 100644 --- a/app/models/discourse_single_sign_on.rb +++ b/app/models/discourse_single_sign_on.rb @@ -106,7 +106,33 @@ class DiscourseSingleSignOn < SingleSignOn private + def synchronize_groups(user) + names = (groups || "").split(",").map(&:downcase) + ids = Group.where('LOWER(NAME) in (?) AND NOT automatic', names).pluck(:id) + + group_users = GroupUser + .where('group_id IN (SELECT id FROM groups WHERE NOT automatic)') + .where(user_id: user.id) + + delete_group_users = group_users + if ids.length > 0 + delete_group_users = group_users.where('group_id NOT IN (?)', ids) + end + delete_group_users.destroy_all + + ids -= group_users.where('group_id IN (?)', ids).pluck(:group_id) + + ids.each do |group_id| + GroupUser.create(group_id: group_id, user_id: user.id) + end + end + def apply_group_rules(user) + if SiteSetting.sso_overrides_groups + synchronize_groups(user) + return + end + if add_groups split = add_groups.split(",").map(&:downcase) if split.length > 0 @@ -130,49 +156,53 @@ class DiscourseSingleSignOn < SingleSignOn end def match_email_or_create_user(ip_address) - unless user = User.find_by_email(email) - try_name = name.presence - try_username = username.presence + # Use a mutex here to counter SSO requests that are sent at the same time w + # the same email payload + DistributedMutex.synchronize("discourse_single_sign_on_#{email}") do + unless user = User.find_by_email(email) + try_name = name.presence + try_username = username.presence - user_params = { - primary_email: UserEmail.new(email: email, primary: true), - name: try_name || User.suggest_name(try_username || email), - username: UserNameSuggester.suggest(try_username || try_name || email), - ip_address: ip_address - } + user_params = { + primary_email: UserEmail.new(email: email, primary: true), + name: try_name || User.suggest_name(try_username || email), + username: UserNameSuggester.suggest(try_username || try_name || email), + ip_address: ip_address + } - user = User.create!(user_params) + user = User.create!(user_params) - if SiteSetting.verbose_sso_logging - Rails.logger.warn("Verbose SSO log: New User (user_id: #{user.id}) Params: #{user_params} User Params: #{user.attributes} User Errors: #{user.errors.full_messages} Email: #{user.primary_email.attributes} Email Error: #{user.primary_email.errors.full_messages}") + if SiteSetting.verbose_sso_logging + Rails.logger.warn("Verbose SSO log: New User (user_id: #{user.id}) Params: #{user_params} User Params: #{user.attributes} User Errors: #{user.errors.full_messages} Email: #{user.primary_email.attributes} Email Error: #{user.primary_email.errors.full_messages}") + end end - end - if user - if sso_record = user.single_sign_on_record - sso_record.last_payload = unsigned_payload - sso_record.external_id = external_id - else - if avatar_url.present? - Jobs.enqueue(:download_avatar_from_url, - url: avatar_url, - user_id: user.id, - override_gravatar: SiteSetting.sso_overrides_avatar + if user + if sso_record = user.single_sign_on_record + sso_record.last_payload = unsigned_payload + sso_record.external_id = external_id + else + if avatar_url.present? + Jobs.enqueue(:download_avatar_from_url, + url: avatar_url, + user_id: user.id, + override_gravatar: SiteSetting.sso_overrides_avatar + ) + end + + user.create_single_sign_on_record!( + last_payload: unsigned_payload, + external_id: external_id, + external_username: username, + external_email: email, + external_name: name, + external_avatar_url: avatar_url ) end - - user.create_single_sign_on_record!( - last_payload: unsigned_payload, - external_id: external_id, - external_username: username, - external_email: email, - external_name: name, - external_avatar_url: avatar_url - ) end - end - user + user + end end def change_external_attributes_and_override(sso_record, user) diff --git a/app/models/embeddable_host.rb b/app/models/embeddable_host.rb index 2fb3e4ed15..c6f0effbbe 100644 --- a/app/models/embeddable_host.rb +++ b/app/models/embeddable_host.rb @@ -12,7 +12,10 @@ class EmbeddableHost < ActiveRecord::Base def self.record_for_url(uri) if uri.is_a?(String) - uri = URI(UrlHelper.escape_uri(uri)) rescue nil + uri = begin + URI(UrlHelper.escape_uri(uri)) + rescue URI::InvalidURIError + end end return false unless uri.present? @@ -40,7 +43,11 @@ class EmbeddableHost < ActiveRecord::Base # Work around IFRAME reload on WebKit where the referer will be set to the Forum URL return true if url&.starts_with?(Discourse.base_url) - uri = URI(UrlHelper.escape_uri(url)) rescue nil + uri = begin + URI(UrlHelper.escape_uri(url)) + rescue URI::InvalidURIError + end + uri.present? && record_for_url(uri).present? end diff --git a/app/models/global_setting.rb b/app/models/global_setting.rb index 3b747df9a8..f0fb74b5a3 100644 --- a/app/models/global_setting.rb +++ b/app/models/global_setting.rb @@ -117,6 +117,8 @@ class GlobalSetting hostnames = [ hostname ] hostnames << backup_hostname if backup_hostname.present? + hostnames << URI.parse(cdn_url).host if cdn_url.present? + hash["host_names"] = hostnames hash["database"] = db_name diff --git a/app/models/group.rb b/app/models/group.rb index 90bb845f63..9f049c8821 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -35,16 +35,20 @@ class Group < ActiveRecord::Base after_save :expire_cache after_destroy :expire_cache + after_commit :trigger_group_created_event, on: :create + after_commit :trigger_group_destroyed_event, on: :destroy + def expire_cache ApplicationSerializer.expire_cache_fragment!("group_names") end validate :name_format_validator - validates :name, presence: true, uniqueness: { case_sensitive: false } + validates :name, presence: true validate :automatic_membership_email_domains_format_validator validate :incoming_email_validator validate :can_allow_membership_requests, if: :allow_membership_requests validates :flair_url, url: true, if: Proc.new { |g| g.flair_url && g.flair_url[0, 3] != 'fa-' } + validate :validate_grant_trust_level, if: :will_save_change_to_grant_trust_level? AUTO_GROUPS = { everyone: 0, @@ -434,6 +438,15 @@ class Group < ActiveRecord::Base end end + # given something that might be a group name, id, or record, return the group id + def self.group_id_from_param(group_param) + return group_param.id if group_param.is_a?(Group) + return group_param if group_param.is_a?(Integer) + + # subtle, using Group[] ensures the group exists in the DB + Group[group_param.to_sym].id + end + def self.builtin Enum.new(:moderators, :admins, :trust_level_1, :trust_level_2) end @@ -571,11 +584,33 @@ class Group < ActiveRecord::Base self.member_of(groups, user).where("gu.owner") end + def trigger_group_created_event + DiscourseEvent.trigger(:group_created, self) + true + end + + def trigger_group_destroyed_event + DiscourseEvent.trigger(:group_destroyed, self) + true + end + protected def name_format_validator self.name.strip! - UsernameValidator.perform_validation(self, 'name') + self.name.downcase! + + UsernameValidator.perform_validation(self, 'name') || begin + if will_save_change_to_name? && name_was&.downcase != self.name + existing = Group.exec_sql( + User::USERNAME_EXISTS_SQL, username: self.name + ).values.present? + + if existing + errors.add(:name, I18n.t("activerecord.errors.messages.taken")) + end + end + end end def automatic_membership_email_domains_format_validator @@ -654,6 +689,15 @@ class Group < ActiveRecord::Base private + def validate_grant_trust_level + unless TrustLevel.valid?(self.grant_trust_level) + self.errors.add(:base, I18n.t( + 'groups.errors.grant_trust_level_not_valid', + trust_level: self.grant_trust_level + )) + end + end + def can_allow_membership_requests valid = true diff --git a/app/models/group_user.rb b/app/models/group_user.rb index 5356933012..5840b53664 100644 --- a/app/models/group_user.rb +++ b/app/models/group_user.rb @@ -82,15 +82,8 @@ class GroupUser < ActiveRecord::Base .includes(:group) .maximum("groups.grant_trust_level") - if highest_level.nil? - # If the user no longer has a group with a trust level, - # unlock them, start at 0 and consider promotions. - user.update!(group_locked_trust_level: nil) - Promotion.recalculate(user) - else - user.update!(group_locked_trust_level: highest_level) - user.change_trust_level!(highest_level) - end + user.update!(group_locked_trust_level: highest_level) + Promotion.recalculate(user) end end diff --git a/app/models/incoming_email.rb b/app/models/incoming_email.rb index be5bfdce60..b355235d26 100644 --- a/app/models/incoming_email.rb +++ b/app/models/incoming_email.rb @@ -5,7 +5,24 @@ class IncomingEmail < ActiveRecord::Base scope :errored, -> { where("NOT is_bounce AND error IS NOT NULL") } - scope :addressed_to, -> (email) { where('incoming_emails.to_addresses ILIKE :email OR incoming_emails.cc_addresses ILIKE :email', email: "%#{email}%") } + scope :addressed_to, -> (email) do + where(<<~SQL, email: "%#{email}%") + incoming_emails.to_addresses ILIKE :email OR + incoming_emails.cc_addresses ILIKE :email + SQL + end + + scope :addressed_to_user, ->(user) do + where(<<~SQL, user_id: user.id) + EXISTS( + SELECT 1 + FROM user_emails + WHERE user_emails.user_id = :user_id AND + (incoming_emails.to_addresses ILIKE '%' || user_emails.email || '%' OR + incoming_emails.cc_addresses ILIKE '%' || user_emails.email || '%') + ) + SQL + end end # == Schema Information diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb index 54c8d6c490..d25f40e30d 100644 --- a/app/models/optimized_image.rb +++ b/app/models/optimized_image.rb @@ -86,7 +86,7 @@ class OptimizedImage < ActiveRecord::Base # make sure we remove the cached copy from external stores if Discourse.store.external? - external_copy.try(:close!) rescue nil + external_copy&.close end thumbnail @@ -293,15 +293,15 @@ class OptimizedImage < ActiveRecord::Base DbHelper.remap(previous_url, optimized_image.url) # remove the old file (when local) unless external - FileUtils.rm(path, force: true) rescue nil + FileUtils.rm(path, force: true) end rescue => e problems << { optimized_image: optimized_image, ex: e } # just ditch the optimized image if there was any errors optimized_image.destroy ensure - file.try(:unlink) rescue nil - file.try(:close) rescue nil + file&.unlink + file&.close end end end diff --git a/app/models/permalink.rb b/app/models/permalink.rb index 62e3ef84f5..6b3799a554 100644 --- a/app/models/permalink.rb +++ b/app/models/permalink.rb @@ -37,7 +37,7 @@ class Permalink < ActiveRecord::Base end if regex.length > 1 - [Regexp.new(regex[1..-1]), sub[1..-1] || ""] rescue nil + [Regexp.new(regex[1..-1]), sub[1..-1] || ""] end end diff --git a/app/models/post.rb b/app/models/post.rb index 78551d1f81..7d8da4859e 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -527,9 +527,10 @@ class Post < ActiveRecord::Base return if user_id == new_user.id edit_reason = I18n.with_locale(SiteSetting.default_locale) do - I18n.t('change_owner.post_revision_text', - old_user: (self.user.username_lower rescue nil) || I18n.t('change_owner.deleted_user'), - new_user: new_user.username_lower + I18n.t( + 'change_owner.post_revision_text', + old_user: self.user&.username_lower || I18n.t('change_owner.deleted_user'), + new_user: new_user.username_lower ) end diff --git a/app/models/post_action.rb b/app/models/post_action.rb index db6892e1e8..8e705527cd 100644 --- a/app/models/post_action.rb +++ b/app/models/post_action.rb @@ -155,6 +155,7 @@ SQL DiscourseEvent.trigger(:confirmed_spam_post, post) if trigger_spam DiscourseEvent.trigger(:flag_reviewed, post) + DiscourseEvent.trigger(:flag_agreed, actions.first) if actions.first.present? update_flagged_posts_count end @@ -188,6 +189,7 @@ SQL Post.with_deleted.where(id: post.id).update_all(cached) DiscourseEvent.trigger(:flag_reviewed, post) + DiscourseEvent.trigger(:flag_disagreed, actions.first) if actions.first.present? update_flagged_posts_count end @@ -206,6 +208,7 @@ SQL end DiscourseEvent.trigger(:flag_reviewed, post) + DiscourseEvent.trigger(:flag_deferred, actions.first) if actions.first.present? update_flagged_posts_count end @@ -306,6 +309,10 @@ SQL end end + if post_action && PostActionType.notify_flag_type_ids.include?(post_action_type_id) + DiscourseEvent.trigger(:flag_created, post_action) + end + GivenDailyLike.increment_for(user.id) if post_action_type_id == PostActionType.types[:like] # agree with other flags @@ -569,6 +576,8 @@ SQL reason = guess_hide_reason(post) end + hiding_again = post.hidden_at.present? + post.hidden = true post.hidden_at = Time.zone.now post.hidden_reason_id = reason @@ -584,7 +593,10 @@ SQL flag_reason: I18n.t("flag_reasons.#{post_action_type}"), } - Jobs.enqueue_in(5.seconds, :send_system_message, user_id: post.user.id, message_type: :post_hidden, message_options: options) + Jobs.enqueue_in(5.seconds, :send_system_message, + user_id: post.user.id, + message_type: hiding_again ? :post_hidden_again : :post_hidden, + message_options: options) end end diff --git a/app/models/post_analyzer.rb b/app/models/post_analyzer.rb index ab8ec1903d..5d069c72eb 100644 --- a/app/models/post_analyzer.rb +++ b/app/models/post_analyzer.rb @@ -111,9 +111,9 @@ class PostAnalyzer return @raw_links if @raw_links.present? @raw_links = [] - cooked_stripped.css("a[href]").each do |l| + cooked_stripped.css("a").each do |l| # Don't include @mentions in the link count - next if l['href'].blank? || link_is_a_mention?(l) + next if link_is_a_mention?(l) @raw_links << l['href'].to_s end diff --git a/app/models/post_custom_field.rb b/app/models/post_custom_field.rb index 999500de52..1eedcc8848 100644 --- a/app/models/post_custom_field.rb +++ b/app/models/post_custom_field.rb @@ -15,6 +15,7 @@ end # # Indexes # +# idx_post_custom_fields_akismet (post_id) # index_post_custom_fields_on_name_and_value (name, "left"(value, 200)) # index_post_custom_fields_on_post_id_and_name (post_id,name) # diff --git a/app/models/report.rb b/app/models/report.rb index 04f27453a0..2d6c8a3624 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -27,7 +27,11 @@ class Report category_id: category_id, group_id: group_id, prev30Days: self.prev30Days - } + }.tap do |json| + if type == 'page_view_crawler_reqs' + json[:related_report] = Report.find('web_crawlers', start_date: start_date, end_date: end_date)&.as_json + end + end end def Report.add_report(name, &block) @@ -202,14 +206,20 @@ class Report # Private messages counts: def self.private_messages_report(report, topic_subtype) - basic_report_about report, Post, :private_messages_count_per_day, report.start_date, report.end_date, topic_subtype - add_counts report, Post.private_posts.with_topic_subtype(topic_subtype), 'posts.created_at' + basic_report_about report, Topic, :private_message_topics_count_per_day, report.start_date, report.end_date, topic_subtype + add_counts report, Topic.private_messages.with_subtype(topic_subtype), 'topics.created_at' end def self.report_user_to_user_private_messages(report) private_messages_report report, TopicSubtype.user_to_user end + def self.report_user_to_user_private_messages_with_replies(report) + topic_subtype = TopicSubtype.user_to_user + basic_report_about report, Post, :private_messages_count_per_day, report.start_date, report.end_date, topic_subtype + add_counts report, Post.private_posts.with_topic_subtype(topic_subtype), 'posts.created_at' + end + def self.report_system_private_messages(report) private_messages_report report, TopicSubtype.system_message end @@ -225,4 +235,12 @@ class Report def self.report_notify_user_private_messages(report) private_messages_report report, TopicSubtype.notify_user end + + def self.report_web_crawlers(report) + report.data = WebCrawlerRequest.where('date >= ? and date <= ?', report.start_date, report.end_date) + .limit(200) + .order('sum_count DESC') + .group(:user_agent).sum(:count) + .map { |ua, count| { x: ua, y: count } } + end end diff --git a/app/models/shared_draft.rb b/app/models/shared_draft.rb index 3f4dccee95..a8a1b93b89 100644 --- a/app/models/shared_draft.rb +++ b/app/models/shared_draft.rb @@ -2,3 +2,19 @@ class SharedDraft < ActiveRecord::Base belongs_to :topic belongs_to :category end + +# == Schema Information +# +# Table name: shared_drafts +# +# topic_id :integer not null +# category_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# id :integer not null, primary key +# +# Indexes +# +# index_shared_drafts_on_category_id (category_id) +# index_shared_drafts_on_topic_id (topic_id) UNIQUE +# diff --git a/app/models/tag.rb b/app/models/tag.rb index f110e677ce..3182d62635 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -16,6 +16,9 @@ class Tag < ActiveRecord::Base after_save :index_search + after_commit :trigger_tag_created_event, on: :create + after_commit :trigger_tag_destroyed_event, on: :destroy + def self.ensure_consistency! update_topic_counts # topic_count counter cache can miscount end @@ -46,11 +49,14 @@ class Tag < ActiveRecord::Base return [] if scope_category_ids.empty? + filter_sql = guardian&.is_staff? ? '' : (' AND ' + DiscourseTagging.filter_visible_sql) + tag_names_with_counts = Tag.exec_sql <<~SQL SELECT tags.name as tag_name, SUM(stats.topic_count) AS sum_topic_count FROM category_tag_stats stats INNER JOIN tags ON stats.tag_id = tags.id AND stats.topic_count > 0 WHERE stats.category_id in (#{scope_category_ids.join(',')}) + #{filter_sql} GROUP BY tags.name ORDER BY sum_topic_count DESC, tag_name ASC LIMIT #{limit} @@ -98,6 +104,16 @@ class Tag < ActiveRecord::Base def index_search SearchIndexer.index(self) end + + def trigger_tag_created_event + DiscourseEvent.trigger(:tag_created, self) + true + end + + def trigger_tag_destroyed_event + DiscourseEvent.trigger(:tag_destroyed, self) + true + end end # == Schema Information diff --git a/app/models/tag_group.rb b/app/models/tag_group.rb index b1c7088c70..2ed60e0780 100644 --- a/app/models/tag_group.rb +++ b/app/models/tag_group.rb @@ -5,9 +5,14 @@ class TagGroup < ActiveRecord::Base has_many :tags, through: :tag_group_memberships has_many :category_tag_groups, dependent: :destroy has_many :categories, through: :category_tag_groups + has_many :tag_group_permissions, dependent: :destroy belongs_to :parent_tag, class_name: 'Tag' + before_save :apply_permissions + + attr_accessor :permissions + def tag_names=(tag_names_arg) DiscourseTagging.add_or_create_tags_by_name(self, tag_names_arg, unlimited: true) end @@ -22,13 +27,47 @@ class TagGroup < ActiveRecord::Base end end + def permissions=(permissions) + @permissions = TagGroup.resolve_permissions(permissions) + end + + def self.resolve_permissions(permissions) + everyone_group_id = Group::AUTO_GROUPS[:everyone] + full = TagGroupPermission.permission_types[:full] + + mapped = permissions.map do |group, permission| + group_id = Group.group_id_from_param(group) + permission = TagGroupPermission.permission_types[permission] unless permission.is_a?(Integer) + + return [] if group_id == everyone_group_id && permission == full + + [group_id, permission] + end + end + + def apply_permissions + if @permissions + tag_group_permissions.destroy_all + @permissions.each do |group_id, permission_type| + tag_group_permissions.build(group_id: group_id, permission_type: permission_type) + end + @permissions = nil + end + end + + def visible_only_to_staff + # currently only "everyone" and "staff" groups are supported + tag_group_permissions.count > 0 + end + def self.allowed(guardian) if guardian.is_staff? TagGroup else category_permissions_filter = <<~SQL - id IN ( SELECT tag_group_id FROM category_tag_groups WHERE category_id IN (?)) - OR id NOT IN (SELECT tag_group_id FROM category_tag_groups) + (id IN ( SELECT tag_group_id FROM category_tag_groups WHERE category_id IN (?)) + OR id NOT IN (SELECT tag_group_id FROM category_tag_groups)) + AND id NOT IN (SELECT tag_group_id FROM tag_group_permissions) SQL TagGroup.where(category_permissions_filter, guardian.allowed_category_ids) diff --git a/app/models/tag_group_permission.rb b/app/models/tag_group_permission.rb new file mode 100644 index 0000000000..bec3573565 --- /dev/null +++ b/app/models/tag_group_permission.rb @@ -0,0 +1,26 @@ +# Who can see and use tags belonging to a tag group. +class TagGroupPermission < ActiveRecord::Base + belongs_to :tag_group + belongs_to :group + + def self.permission_types + @permission_types ||= Enum.new(full: 1) #, see: 2 + end +end + +# == Schema Information +# +# Table name: tag_group_permissions +# +# id :integer not null, primary key +# tag_group_id :integer not null +# group_id :integer not null +# permission_type :integer default(1), not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_tag_group_permissions_on_group_id (group_id) +# index_tag_group_permissions_on_tag_group_id (tag_group_id) +# diff --git a/app/models/theme_field.rb b/app/models/theme_field.rb index 1eeb40831f..36636251fa 100644 --- a/app/models/theme_field.rb +++ b/app/models/theme_field.rb @@ -59,6 +59,14 @@ PLUGIN_API_JS doc = Nokogiri::HTML.fragment(html) doc.css('script[type="text/x-handlebars"]').each do |node| name = node["name"] || node["data-template-name"] || "broken" + + setting_helpers = '' + theme.cached_settings.each do |k, v| + val = v.is_a?(String) ? "\"#{v.gsub('"', "\\u0022")}\"" : v + setting_helpers += "{{theme-setting-injector context=this key=\"#{k}\" value=#{val}}}\n" + end + hbs_template = setting_helpers + node.inner_html + is_raw = name =~ /\.raw$/ if is_raw template = "requirejs('discourse-common/lib/raw-handlebars').template(#{Barber::Precompiler.compile(node.inner_html)})" @@ -70,7 +78,7 @@ PLUGIN_API_JS COMPILED else - template = "Ember.HTMLBars.template(#{Barber::Ember::Precompiler.compile(node.inner_html)})" + template = "Ember.HTMLBars.template(#{Barber::Ember::Precompiler.compile(hbs_template)})" node.replace < (function() { diff --git a/app/models/topic.rb b/app/models/topic.rb index a9169ad515..abc0f5f270 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -31,7 +31,7 @@ class Topic < ActiveRecord::Base def_delegator :notifier, :mute!, :notify_muted! def_delegator :notifier, :toggle_mute, :toggle_mute - attr_accessor :allowed_user_ids, :tags_changed + attr_accessor :allowed_user_ids, :tags_changed, :includes_destination_category DiscourseEvent.on(:site_setting_saved) do |site_setting| if site_setting.name.to_s == "slug_generation_method" && site_setting.saved_change_to_value? @@ -191,6 +191,8 @@ class Topic < ActiveRecord::Base where("topics.category_id IS NULL OR topics.category_id IN (SELECT id FROM categories WHERE #{condition[0]})", condition[1]) } + scope :with_subtype, ->(subtype) { where('topics.subtype = ?', subtype) } + attr_accessor :ignore_category_auto_close attr_accessor :skip_callbacks @@ -1019,11 +1021,16 @@ SQL TopicUser.change(user.id, id, cleared_pinned_at: nil) end - def update_pinned(status, global = false, pinned_until = nil) - pinned_until = Time.parse(pinned_until) rescue nil + def update_pinned(status, global = false, pinned_until = "") + pinned_until ||= '' + + pinned_until = begin + Time.parse(pinned_until) + rescue ArgumentError + end update_columns( - pinned_at: status ? Time.now : nil, + pinned_at: status ? Time.zone.now : nil, pinned_globally: global, pinned_until: pinned_until ) @@ -1161,19 +1168,31 @@ SQL def message_archived?(user) return false unless user && user.id - sql = < 0 @@ -1307,6 +1326,10 @@ SQL MiniSuffix.domain(URI.parse(URI.encode(self.featured_link)).hostname) end + def self.private_message_topics_count_per_day(start_date, end_date, topic_subtype) + private_messages.with_subtype(topic_subtype).where('topics.created_at >= ? AND topics.created_at <= ?', start_date, end_date).group('date(topics.created_at)').order('date(topics.created_at)').count + end + private def update_category_topic_count_by(num) diff --git a/app/models/topic_converter.rb b/app/models/topic_converter.rb index aed196943a..76704a7a8c 100644 --- a/app/models/topic_converter.rb +++ b/app/models/topic_converter.rb @@ -24,6 +24,7 @@ class TopicConverter @topic.archetype = Archetype.default @topic.save update_user_stats + update_category_topic_count_by(1) # TODO: Every post in a PRIVATE MESSAGE looks the same: each is a UserAction::NEW_PRIVATE_MESSAGE. # So we need to remove all those user actions and re-log all the posts. @@ -46,6 +47,7 @@ class TopicConverter def convert_to_private_message Topic.transaction do + update_category_topic_count_by(-1) @topic.category_id = nil @topic.archetype = Archetype.private_message add_allowed_users @@ -92,4 +94,10 @@ class TopicConverter end end + def update_category_topic_count_by(num) + if @topic.category_id.present? + Category.where(['id = ?', @topic.category_id]).update_all("topic_count = topic_count " + (num > 0 ? '+' : '') + "#{num}") + end + end + end diff --git a/app/models/topic_link.rb b/app/models/topic_link.rb index 45bf194b87..3238f84ea1 100644 --- a/app/models/topic_link.rb +++ b/app/models/topic_link.rb @@ -115,7 +115,14 @@ SQL PrettyText .extract_links(post.cooked) - .map { |u| [u, URI.parse(u.url)] rescue nil } + .map do |u| + uri = begin + URI.parse(u.url) + rescue URI::InvalidURIError + end + + [u, uri] + end .reject { |_, p| p.nil? || "mailto".freeze == p.scheme } .uniq { |_, p| p } .each do |link, parsed| diff --git a/app/models/topic_link_click.rb b/app/models/topic_link_click.rb index 90cddefe2f..ce0f7fcbe1 100644 --- a/app/models/topic_link_click.rb +++ b/app/models/topic_link_click.rb @@ -16,7 +16,10 @@ class TopicLinkClick < ActiveRecord::Base url = args[:url][0...TopicLink.max_url_length] return nil if url.blank? - uri = URI.parse(url) rescue nil + uri = begin + URI.parse(url) + rescue URI::InvalidURIError + end urls = Set.new urls << url @@ -43,7 +46,11 @@ class TopicLinkClick < ActiveRecord::Base # add a cdn link if uri if Discourse.asset_host.present? - cdn_uri = URI.parse(Discourse.asset_host) rescue nil + cdn_uri = begin + URI.parse(Discourse.asset_host) + rescue URI::InvalidURIError + end + if cdn_uri && cdn_uri.hostname == uri.hostname && uri.path.starts_with?(cdn_uri.path) is_cdn_link = true urls << uri.path[cdn_uri.path.length..-1] @@ -51,7 +58,11 @@ class TopicLinkClick < ActiveRecord::Base end if SiteSetting.Upload.s3_cdn_url.present? - cdn_uri = URI.parse(SiteSetting.Upload.s3_cdn_url) rescue nil + cdn_uri = begin + URI.parse(SiteSetting.Upload.s3_cdn_url) + rescue URI::InvalidURIError + end + if cdn_uri && cdn_uri.hostname == uri.hostname && uri.path.starts_with?(cdn_uri.path) is_cdn_link = true path = uri.path[cdn_uri.path.length..-1] diff --git a/app/models/topic_list.rb b/app/models/topic_list.rb index 6de3eafcbe..5aae90f778 100644 --- a/app/models/topic_list.rb +++ b/app/models/topic_list.rb @@ -26,18 +26,21 @@ class TopicList end end - attr_accessor :more_topics_url, - :prev_topics_url, - :draft, - :draft_key, - :draft_sequence, - :filter, - :for_period, - :per_page, - :top_tags, - :current_user, - :tags, - :shared_drafts + attr_accessor( + :more_topics_url, + :prev_topics_url, + :draft, + :draft_key, + :draft_sequence, + :filter, + :for_period, + :per_page, + :top_tags, + :current_user, + :tags, + :shared_drafts, + :category + ) def initialize(filter, current_user, topics, opts = nil) @filter = filter diff --git a/app/models/upload.rb b/app/models/upload.rb index 842050d2cf..dc1a9c0bc1 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -82,7 +82,11 @@ class Upload < ActiveRecord::Base url = url.sub(SiteSetting.Upload.s3_cdn_url, Discourse.store.absolute_base_url) if SiteSetting.Upload.s3_cdn_url.present? # always try to get the path - uri = URI(url) rescue nil + uri = begin + URI(url) + rescue URI::InvalidURIError + end + url = uri.path if uri.try(:scheme) Upload.find_by(url: url) @@ -134,13 +138,13 @@ class Upload < ActiveRecord::Base DbHelper.remap(previous_url, upload.url) # remove the old file (when local) unless external - FileUtils.rm(path, force: true) rescue nil + FileUtils.rm(path, force: true) end rescue => e problems << { upload: upload, ex: e } ensure - file.try(:unlink) rescue nil - file.try(:close) rescue nil + file&.unlink + file&.close end end end diff --git a/app/models/user.rb b/app/models/user.rb index a821e0c572..08beb52ac0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1121,11 +1121,25 @@ class User < ActiveRecord::Base self.username_lower = username.downcase end + USERNAME_EXISTS_SQL = <<~SQL + (SELECT users.id AS user_id FROM users + WHERE users.username_lower = :username) + + UNION ALL + + (SELECT groups.id AS group_id FROM groups + WHERE lower(groups.name) = :username) + SQL + def username_validator username_format_validator || begin lower = username.downcase - existing = User.find_by(username_lower: lower) - if will_save_change_to_username? && existing && existing.id != self.id + + existing = User.exec_sql( + USERNAME_EXISTS_SQL, username: lower + ).to_a.first + + if will_save_change_to_username? && existing.present? && existing["user_id"] != self.id errors.add(:username, I18n.t(:'user.username.unique')) end end diff --git a/app/models/user_avatar.rb b/app/models/user_avatar.rb index a247bb4752..4774df14f3 100644 --- a/app/models/user_avatar.rb +++ b/app/models/user_avatar.rb @@ -35,7 +35,7 @@ class UserAvatar < ActiveRecord::Base upload = UploadCreator.new(tempfile, 'gravatar.png', origin: gravatar_url, type: "avatar").create_for(user_id) if gravatar_upload_id != upload.id - gravatar_upload.try(:destroy!) rescue nil + gravatar_upload&.destroy! self.gravatar_upload = upload save! end diff --git a/app/models/web_crawler_request.rb b/app/models/web_crawler_request.rb new file mode 100644 index 0000000000..8f082222d8 --- /dev/null +++ b/app/models/web_crawler_request.rb @@ -0,0 +1,90 @@ +class WebCrawlerRequest < ActiveRecord::Base + include CachedCounting + + # auto flush if older than this + self.autoflush_seconds = 1.hour + + cattr_accessor :max_record_age, :max_records_per_day + + # only keep the top records based on request count + self.max_records_per_day = 200 + + # delete records older than this + self.max_record_age = 30.days + + def self.increment!(user_agent, opts = nil) + ua_list_key = user_agent_list_key + $redis.sadd(ua_list_key, user_agent) + $redis.expire(ua_list_key, 259200) # 3.days + + perform_increment!(redis_key(user_agent), opts) + end + + def self.write_cache!(date = nil) + if date.nil? + write_cache!(Time.now.utc) + write_cache!(Time.now.utc.yesterday) + return + end + + self.last_flush = Time.now.utc + + date = date.to_date + ua_list_key = user_agent_list_key(date) + + while user_agent = $redis.spop(ua_list_key) + val = get_and_reset(redis_key(user_agent, date)) + + next if val == 0 + + self.where(id: req_id(date, user_agent)).update_all(["count = count + ?", val]) + end + rescue Redis::CommandError => e + raise unless e.message =~ /READONLY/ + nil + end + + def self.clear_cache!(date = nil) + if date.nil? + clear_cache!(Time.now.utc) + clear_cache!(Time.now.utc.yesterday) + return + end + + ua_list_key = user_agent_list_key(date) + + while user_agent = $redis.spop(ua_list_key) + $redis.del redis_key(user_agent, date) + end + + $redis.del(ua_list_key) + end + + protected + + def self.user_agent_list_key(time = Time.now.utc) + "crawl_ua_list:#{time.strftime('%Y%m%d')}" + end + + def self.redis_key(user_agent, time = Time.now.utc) + "crawl_req:#{time.strftime('%Y%m%d')}:#{user_agent}" + end + + def self.req_id(date, user_agent) + request_id(date: date, user_agent: user_agent) + end +end + +# == Schema Information +# +# Table name: web_crawler_requests +# +# id :integer not null, primary key +# date :date not null +# user_agent :string not null +# count :integer default(0), not null +# +# Indexes +# +# index_web_crawler_requests_on_date_and_user_agent (date,user_agent) UNIQUE +# diff --git a/app/models/web_hook_event_type.rb b/app/models/web_hook_event_type.rb index d3596bfdaf..7d6d595566 100644 --- a/app/models/web_hook_event_type.rb +++ b/app/models/web_hook_event_type.rb @@ -2,6 +2,10 @@ class WebHookEventType < ActiveRecord::Base TOPIC = 1 POST = 2 USER = 3 + GROUP = 4 + CATEGORY = 5 + TAG = 6 + FLAG = 7 has_and_belongs_to_many :web_hooks diff --git a/app/serializers/admin_plugin_serializer.rb b/app/serializers/admin_plugin_serializer.rb index 6a00ded374..b104d69adb 100644 --- a/app/serializers/admin_plugin_serializer.rb +++ b/app/serializers/admin_plugin_serializer.rb @@ -5,7 +5,8 @@ class AdminPluginSerializer < ApplicationSerializer :url, :admin_route, :enabled, - :enabled_setting + :enabled_setting, + :is_official def id object.metadata.name @@ -51,4 +52,8 @@ class AdminPluginSerializer < ApplicationSerializer def include_admin_route? admin_route.present? end + + def is_official + Plugin::Metadata::OFFICIAL_PLUGINS.include?(object.name) + end end diff --git a/app/serializers/basic_category_serializer.rb b/app/serializers/basic_category_serializer.rb index 01a088976e..4149a32cf2 100644 --- a/app/serializers/basic_category_serializer.rb +++ b/app/serializers/basic_category_serializer.rb @@ -24,7 +24,8 @@ class BasicCategorySerializer < ApplicationSerializer :num_featured_topics, :default_view, :subcategory_list_style, - :default_top_period + :default_top_period, + :minimum_required_tags has_one :uploaded_logo, embed: :object, serializer: CategoryUploadSerializer has_one :uploaded_background, embed: :object, serializer: CategoryUploadSerializer diff --git a/app/serializers/concerns/topic_tags_mixin.rb b/app/serializers/concerns/topic_tags_mixin.rb index 6ad88b47ea..c2dc1254d2 100644 --- a/app/serializers/concerns/topic_tags_mixin.rb +++ b/app/serializers/concerns/topic_tags_mixin.rb @@ -9,7 +9,7 @@ module TopicTagsMixin def tags # Calling method `pluck` along with `includes` causing N+1 queries - topic.tags.map(&:name) + DiscourseTagging.filter_visible(topic.tags, scope).map(&:name) end def topic diff --git a/app/serializers/post_revision_serializer.rb b/app/serializers/post_revision_serializer.rb index b86fda2379..0e0348cb39 100644 --- a/app/serializers/post_revision_serializer.rb +++ b/app/serializers/post_revision_serializer.rb @@ -160,7 +160,11 @@ class PostRevisionSerializer < ApplicationSerializer end def tags_changes - { previous: previous["tags"], current: current["tags"] } + changes = { + previous: filter_visible_tags(previous["tags"]), + current: filter_visible_tags(current["tags"]) + } + changes[:previous] == changes[:current] ? nil : changes end def include_tags_changes? @@ -250,4 +254,9 @@ class PostRevisionSerializer < ApplicationSerializer object.user || Discourse.system_user end + def filter_visible_tags(tags) + @hidden_tag_names ||= DiscourseTagging.hidden_tag_names(scope) + tags.is_a?(Array) ? (tags - @hidden_tag_names) : tags + end + end diff --git a/app/serializers/tag_group_serializer.rb b/app/serializers/tag_group_serializer.rb index 62858aff20..e0a4734f24 100644 --- a/app/serializers/tag_group_serializer.rb +++ b/app/serializers/tag_group_serializer.rb @@ -1,5 +1,5 @@ class TagGroupSerializer < ApplicationSerializer - attributes :id, :name, :tag_names, :parent_tag_name, :one_per_topic + attributes :id, :name, :tag_names, :parent_tag_name, :one_per_topic, :visible_only_to_staff def tag_names object.tags.map(&:name).sort diff --git a/app/serializers/topic_list_item_serializer.rb b/app/serializers/topic_list_item_serializer.rb index 663cc2c9a2..1f77b45d75 100644 --- a/app/serializers/topic_list_item_serializer.rb +++ b/app/serializers/topic_list_item_serializer.rb @@ -30,8 +30,9 @@ class TopicListItemSerializer < ListableTopicSerializer end def category_id + # If it's a shared draft, show the destination topic instead - if object.category_id == SiteSetting.shared_drafts_category.to_i && object.shared_draft + if object.includes_destination_category && object.shared_draft return object.shared_draft.category_id end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 1e45721b68..06e620e25b 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -188,7 +188,11 @@ class UserSerializer < BasicUserSerializer end def website_name - uri = URI(website.to_s) rescue nil + uri = begin + URI(website.to_s) + rescue URI::InvalidURIError + end + return if uri.nil? || uri.host.nil? uri.host.sub(/^www\./, '') + uri.path end diff --git a/app/serializers/web_hook_category_serializer.rb b/app/serializers/web_hook_category_serializer.rb new file mode 100644 index 0000000000..57b979e0ce --- /dev/null +++ b/app/serializers/web_hook_category_serializer.rb @@ -0,0 +1,13 @@ +class WebHookCategorySerializer < CategorySerializer + + %i{ + can_edit + notification_level + available_groups + }.each do |attr| + define_method("include_#{attr}?") do + false + end + end + +end diff --git a/app/serializers/web_hook_flag_serializer.rb b/app/serializers/web_hook_flag_serializer.rb new file mode 100644 index 0000000000..67674fef8e --- /dev/null +++ b/app/serializers/web_hook_flag_serializer.rb @@ -0,0 +1,43 @@ +class WebHookFlagSerializer < ApplicationSerializer + attributes :id, + :post, + :flag_type, + :created_by, + :created_at, + :resolved_at, + :resolved_by + + def post + BasicPostSerializer.new(object.post, scope: scope, root: false).as_json + end + + def flag_type + object.post_action_type_key + end + + def include_post? + object.post.present? + end + + def created_by + object.user && object.user.username + end + + def resolved_at + object.disposed_at + end + + def include_resolved_at? + object.disposed_at.present? + end + + def resolved_by + if object.disposed_by_id.present? + User.find(object.disposed_by_id).username + end + end + + def include_resolved_by? + object.disposed_by_id.present? + end +end diff --git a/app/serializers/web_hook_group_serializer.rb b/app/serializers/web_hook_group_serializer.rb new file mode 100644 index 0000000000..c580451db8 --- /dev/null +++ b/app/serializers/web_hook_group_serializer.rb @@ -0,0 +1,12 @@ +class WebHookGroupSerializer < GroupShowSerializer + + %i{ + is_group_user + is_group_owner + }.each do |attr| + define_method("include_#{attr}?") do + false + end + end + +end diff --git a/app/serializers/web_hook_topic_view_serializer.rb b/app/serializers/web_hook_topic_view_serializer.rb index 2beca4f3ee..8c1aa0a3a7 100644 --- a/app/serializers/web_hook_topic_view_serializer.rb +++ b/app/serializers/web_hook_topic_view_serializer.rb @@ -10,6 +10,7 @@ class WebHookTopicViewSerializer < TopicViewSerializer draft_key draft_sequence message_bus_last_id + suggested_topics }.each do |attr| define_method("include_#{attr}?") do false diff --git a/app/services/destroy_task.rb b/app/services/destroy_task.rb new file mode 100644 index 0000000000..86768a79f7 --- /dev/null +++ b/app/services/destroy_task.rb @@ -0,0 +1,90 @@ +## Because these methods are meant to be called from a rake task +# we are capturing all log output into a log array to return +# to the rake task rather than using `puts` statements. +class DestroyTask + def self.destroy_topics(category) + c = Category.find_by_slug(category) + log = [] + return "A category with the slug: #{category} could not be found" if c.nil? + topics = Topic.where(category_id: c.id, pinned_at: nil).where.not(user_id: -1) + log << "There are #{topics.count} topics to delete in #{category} category" + topics.each do |topic| + log << "Deleting #{topic.slug}..." + first_post = topic.ordered_posts.first + if first_post.nil? + return log << "Topic.ordered_posts.first was nil" + end + system_user = User.find(-1) + log << PostDestroyer.new(system_user, first_post).destroy + end + log + end + + def self.destroy_topics_all_categories + categories = Category.all + log = [] + categories.each do |c| + log << destroy_topics(c.slug) + end + log + end + + def self.destroy_private_messages + pms = Topic.where(archetype: "private_message") + current_user = User.find(-1) #system + log = [] + pms.each do |pm| + log << "Destroying #{pm.slug} pm" + first_post = pm.ordered_posts.first + log << PostDestroyer.new(current_user, first_post).destroy + end + log + end + + def self.destroy_groups + groups = Group.where(automatic: false) + log = [] + groups.each do |group| + log << "destroying group: #{group.id}" + log << group.destroy + end + log + end + + def self.destroy_users + log = [] + users = User.where(admin: false, id: 1..Float::INFINITY) + log << "There are #{users.count} users to delete" + options = {} + options[:delete_posts] = true + current_user = User.find(-1) #system + users.each do |user| + begin + if UserDestroyer.new(current_user).destroy(user, options) + log << "#{user.username} deleted" + else + log << "#{user.username} not deleted" + end + rescue UserDestroyer::PostsExistError + raise Discourse::InvalidAccess.new("User #{user.username} has #{user.post_count} posts, so can't be deleted.") + rescue NoMethodError + log << "#{user.username} could not be deleted" + end + end + log + end + + def self.destroy_stats + ApplicationRequest.destroy_all + IncomingLink.destroy_all + UserVisit.destroy_all + UserProfileView.destroy_all + user_profiles = UserProfile.all + user_profiles.each do |user_profile| + user_profile.views = 0 + user_profile.save! + end + PostAction.unscoped.destroy_all + EmailLog.destroy_all + end +end diff --git a/app/services/handle_chunk_upload.rb b/app/services/handle_chunk_upload.rb index de5bf7837d..75362e54e5 100644 --- a/app/services/handle_chunk_upload.rb +++ b/app/services/handle_chunk_upload.rb @@ -42,8 +42,11 @@ class HandleChunkUpload tmp_directory = @params[:tmp_directory] # delete destination files - File.delete(upload_path) rescue nil - File.delete(tmp_upload_path) rescue nil + begin + File.delete(upload_path) + File.delete(tmp_upload_path) + rescue Errno::ENOENT + end # merge all the chunks File.open(tmp_upload_path, "a") do |file| @@ -59,7 +62,10 @@ class HandleChunkUpload FileUtils.mv(tmp_upload_path, upload_path, force: true) # remove tmp directory - FileUtils.rm_rf(tmp_directory) rescue nil + begin + FileUtils.rm_rf(tmp_directory) + rescue Errno::ENOENT + end end end diff --git a/app/services/post_action_notifier.rb b/app/services/post_action_notifier.rb index f5e5a7f2a4..b9d1997996 100644 --- a/app/services/post_action_notifier.rb +++ b/app/services/post_action_notifier.rb @@ -109,4 +109,17 @@ class PostActionNotifier ) end + def self.after_post_unhide(post, flaggers) + return if @disabled || post.last_editor.blank? || flaggers.blank? + + flaggers.each do |flagger| + alerter.create_notification( + flagger, + Notification.types[:edited], + post, + display_username: post.last_editor.username, + acting_user_id: post.last_editor.id + ) + end + end end diff --git a/app/services/post_alerter.rb b/app/services/post_alerter.rb index 640382b217..44973d7adf 100644 --- a/app/services/post_alerter.rb +++ b/app/services/post_alerter.rb @@ -38,6 +38,10 @@ class PostAlerter allowed_group_users(post) end + def only_allowed_users(users, post) + users.select { |u| allowed_users(post).include?(u) || allowed_group_users(post).include?(u) } + end + def notify_about_reply?(post) post.post_type == Post.types[:regular] || post.post_type == Post.types[:whisper] end @@ -50,17 +54,20 @@ class PostAlerter if mentioned_groups || mentioned_users mentioned_opts = {} + editor = post.last_editor + if post.last_editor_id != post.user_id # Mention comes from an edit by someone else, so notification should say who added the mention. - editor = post.last_editor mentioned_opts = { user_id: editor.id, original_username: editor.username, display_username: editor.username } end expand_group_mentions(mentioned_groups, post) do |group, users| + users = only_allowed_users(users, post) if editor.id < 0 notified += notify_users(users - notified, :group_mentioned, post, mentioned_opts.merge(group: group)) end if mentioned_users + mentioned_users = only_allowed_users(mentioned_users, post) if editor.id < 0 notified += notify_users(mentioned_users - notified, :mentioned, post, mentioned_opts) end end @@ -405,8 +412,7 @@ class PostAlerter ) if created.id && !existing_notification && NOTIFIABLE_TYPES.include?(type) && !user.suspended? - # we may have an invalid post somehow, dont blow up - post_url = original_post.url rescue nil + post_url = original_post.url if post_url payload = { notification_type: type, diff --git a/app/services/user_silencer.rb b/app/services/user_silencer.rb index a9fff88dd0..dcd56e42fc 100644 --- a/app/services/user_silencer.rb +++ b/app/services/user_silencer.rb @@ -45,6 +45,7 @@ class UserSilencer DiscourseEvent.trigger( :user_silenced, user: @user, + silenced_by: @by_user, reason: @opts[:reason], message: @opts[:message_body], user_history: @user_history, diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb index 305f96fddc..d2e92f6308 100644 --- a/app/services/user_updater.rb +++ b/app/services/user_updater.rb @@ -73,7 +73,9 @@ class UserUpdater end TAG_NAMES.each do |attribute, level| - TagUser.batch_set(user, level, attributes[attribute]) + if attributes.has_key?(attribute) + TagUser.batch_set(user, level, attributes[attribute]&.split(',') || []) + end end save_options = false diff --git a/app/views/common/_discourse_javascript.html.erb b/app/views/common/_discourse_javascript.html.erb index dac6b19fbe..e2f7e2cd21 100644 --- a/app/views/common/_discourse_javascript.html.erb +++ b/app/views/common/_discourse_javascript.html.erb @@ -45,7 +45,7 @@ Discourse.ThemeSettings = ps.get('themeSettings'); Discourse.LetterAvatarVersion = '<%= LetterAvatar.version %>'; Discourse.MarkdownItURL = '<%= asset_url('markdown-it-bundle.js') %>'; - Discourse.ServiceWorkerURL = '<%= Rails.application.assets_manifest.assets['service-worker.js'] %>' + Discourse.ServiceWorkerURL = Discourse.Environment != "development" ? '<%= Rails.application.assets_manifest.assets['service-worker.js'] %>' : 'service-worker.js'; I18n.defaultLocale = '<%= SiteSetting.default_locale %>'; Discourse.start(); Discourse.set('assetVersion','<%= Discourse.assets_digest %>'); diff --git a/app/views/common/_google_tag_manager_body.html.erb b/app/views/common/_google_tag_manager_body.html.erb new file mode 100644 index 0000000000..6c98df8117 --- /dev/null +++ b/app/views/common/_google_tag_manager_body.html.erb @@ -0,0 +1,4 @@ + + + diff --git a/app/views/common/_google_tag_manager.html.erb b/app/views/common/_google_tag_manager_head.html.erb similarity index 58% rename from app/views/common/_google_tag_manager.html.erb rename to app/views/common/_google_tag_manager_head.html.erb index dca75352a0..2320f2c7b0 100644 --- a/app/views/common/_google_tag_manager.html.erb +++ b/app/views/common/_google_tag_manager_head.html.erb @@ -2,10 +2,10 @@ dataLayer = [<%= google_tag_manager_json %>]; - + + diff --git a/app/views/embed/comments.html.erb b/app/views/embed/comments.html.erb index 23f69584fe..b6863c21aa 100644 --- a/app/views/embed/comments.html.erb +++ b/app/views/embed/comments.html.erb @@ -27,7 +27,7 @@ <%= get_html(post.cooked) %> - <%- if post.reply_count > 0 %> + <%- if post.reply_count > 0 && post.replies.exists? %> <%- if post.reply_count == 1 %> <%= link_to I18n.t('embed.replies', count: post.reply_count), post.full_url, 'data-link-to-post' => post.replies.first.id.to_s, :class => 'post-replies button' %> <% else %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 361466c354..58fff0c1a1 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -44,6 +44,7 @@ <%= raw theme_lookup("head_tag") %> <%- end %> + <%= render_google_tag_manager_head_code %> <%= render_google_universal_analytics_code %> @@ -60,8 +61,7 @@ - <%= render_google_tag_manager_code %> - + <%= render_google_tag_manager_body_code %>
    {{i18n "groups.index.group_type"}} {{i18n "groups.membership"}}
    {{group.user_count}} - {{#group-membership-button model=group - showMembershipStatus=true - showLogin='showLogin'}} - - {{d-button icon="ban" - label=(if group.automatic 'groups.automatic_group' 'groups.close_group') - disabled=true}} - {{/group-membership-button}} + + {{#if group.public_admission}} + {{i18n 'groups.index.public'}} + {{else if group.isPrivate}} + {{d-icon "eye-slash"}} + {{i18n 'groups.index.private'}} + {{else}} + {{#if group.automatic}} + {{i18n 'groups.index.automatic'}} + {{else}} + {{i18n 'groups.index.closed'}} + {{/if}} + {{/if}} - {{#if group.is_group_user}} - {{d-icon "user" title="groups.is_group_user"}} + + {{#if group.is_group_owner}} + + {{i18n "groups.index.is_group_owner"}} + + {{else if group.is_group_user}} + + {{i18n "groups.index.is_group_user"}} + {{/if}} - {{#if group.is_group_owner}} - {{d-icon "shield" title="groups.is_group_owner"}} - {{/if}} + {{group-membership-button model=group showLogin='showLogin'}}
    - {{input type="password" value=accountPasswordConfirm id="new-account-confirmation"}} + {{input type="password" value=accountPasswordConfirm id="new-account-confirmation" autocomplete="false"}} {{input value=accountChallenge id="new-account-challenge"}}