diff --git a/app/assets/javascripts/discourse/controllers/group/members.js.es6 b/app/assets/javascripts/discourse/controllers/group/members.js.es6
index a22c82705ae..29294d1548d 100644
--- a/app/assets/javascripts/discourse/controllers/group/members.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group/members.js.es6
@@ -3,7 +3,29 @@ export default Ember.Controller.extend({
limit: null,
offset: null,
+ isOwner: function() {
+ if (this.get('currentUser.admin')) {
+ return true;
+ }
+ const owners = this.get('model.owners');
+ const currentUserId = this.get('currentUser.id');
+ if (currentUserId) {
+ return !!owners.findBy('id', currentUserId);
+ }
+ }.property('model.owners.@each'),
+
actions: {
+ removeMember(user) {
+ this.get('model').removeMember(user);
+ },
+
+ addMembers() {
+ const usernames = this.get('usernames');
+ if (usernames && usernames.length > 0) {
+ this.get('model').addMembers(usernames).then(() => this.set('usernames', []));
+ }
+ },
+
loadMore() {
if (this.get("loading")) { return; }
// we've reached the end
diff --git a/app/assets/javascripts/discourse/models/group.js.es6 b/app/assets/javascripts/discourse/models/group.js.es6
index f90aee368dd..03c0cb56a39 100644
--- a/app/assets/javascripts/discourse/models/group.js.es6
+++ b/app/assets/javascripts/discourse/models/group.js.es6
@@ -1,12 +1,17 @@
+import computed from 'ember-addons/ember-computed-decorators';
+
const Group = Discourse.Model.extend({
limit: 50,
offset: 0,
user_count: 0,
+ owners: [],
- emailDomains: function() {
- var value = this.get("automatic_membership_email_domains");
+ hasOwners: Ember.computed.notEmpty('owners'),
+
+ @computed("automatic_membership_email_domains")
+ emailDomains(value) {
return Em.isEmpty(value) ? "" : value;
- }.property("automatic_membership_email_domains"),
+ },
type: function() {
return this.get("automatic") ? "automatic" : "custom";
@@ -24,18 +29,41 @@ const Group = Discourse.Model.extend({
const self = this, offset = Math.min(this.get("user_count"), Math.max(this.get("offset"), 0));
return Discourse.Group.loadMembers(this.get("name"), offset, this.get("limit")).then(function (result) {
+ var ownerIds = {};
+ result.owners.forEach(function(owner){
+ ownerIds[owner.id] = true;
+ });
+ const owners = result.owners.map(owner => Discourse.User.create(owner));
+
self.setProperties({
user_count: result.meta.total,
limit: result.meta.limit,
offset: result.meta.offset,
- members: result.members.map(member => Discourse.User.create(member))
+ members: result.members.map(member => {
+ if (ownerIds[member.id]) {
+ member.owner = true;
+ }
+ return Discourse.User.create(member);
+ }),
+ owners: result.owners.map(owner => Discourse.User.create(owner))
});
});
},
+ removeOwner(member) {
+ var self = this;
+ return Discourse.ajax('/admin/groups/' + this.get('id') + '/owners.json', {
+ type: "DELETE",
+ data: { user_id: member.get("id") }
+ }).then(function() {
+ // reload member list
+ self.findMembers();
+ });
+ },
+
removeMember(member) {
var self = this;
- return Discourse.ajax('/admin/groups/' + this.get('id') + '/members.json', {
+ return Discourse.ajax('/groups/' + this.get('id') + '/members.json', {
type: "DELETE",
data: { user_id: member.get("id") }
}).then(function() {
@@ -46,7 +74,17 @@ const Group = Discourse.Model.extend({
addMembers(usernames) {
var self = this;
- return Discourse.ajax('/admin/groups/' + this.get('id') + '/members.json', {
+ return Discourse.ajax('/groups/' + this.get('id') + '/members.json', {
+ type: "PUT",
+ data: { usernames: usernames }
+ }).then(function() {
+ self.findMembers();
+ });
+ },
+
+ addOwners(usernames) {
+ var self = this;
+ return Discourse.ajax('/admin/groups/' + this.get('id') + '/owners.json', {
type: "PUT",
data: { usernames: usernames }
}).then(function() {
diff --git a/app/assets/javascripts/discourse/templates/group/members.hbs b/app/assets/javascripts/discourse/templates/group/members.hbs
index 8421949e044..405125993f3 100644
--- a/app/assets/javascripts/discourse/templates/group/members.hbs
+++ b/app/assets/javascripts/discourse/templates/group/members.hbs
@@ -1,18 +1,40 @@
{{#if model}}
+ {{#if isOwner}}
+
+
+
+ {{/if}}
{{i18n 'last_post'}} |
{{i18n 'last_seen'}} |
+ {{#if isOwner}}
+ |
+ {{/if}}
{{#each model.members as |m|}}
- {{user-small user=m}} |
+ {{user-small user=m}}
+ {{#if m.owner}}
+ {{i18n "groups.owner"}}
+ {{/if}}
+ |
{{bound-date m.last_posted_at}}
|
{{bound-date m.last_seen_at}}
|
+ {{#if isOwner}}
+
+ {{#unless m.owner}}
+
+ {{/unless}}
+ |
+ {{/if}}
{{/each}}
diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss
index 4115a8dec7c..4d2c8ad6588 100644
--- a/app/assets/stylesheets/common/base/compose.scss
+++ b/app/assets/stylesheets/common/base/compose.scss
@@ -75,6 +75,23 @@ div.ac-wrap.disabled {
}
}
+div.ac-wrap div.item a.remove, .remove-link {
+ margin-left: 4px;
+ font-size: 11px;
+ line-height: 10px;
+ padding: 1.5px 1.5px 1.5px 2.5px;
+ border-radius: 12px;
+ width: 10px;
+ display: inline-block;
+ border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
+ &:hover {
+ background-color: scale-color($danger, $lightness: 75%);
+ border: 1px solid scale-color($danger, $lightness: 30%);
+ text-decoration: none;
+ color: $danger;
+ }
+}
+
div.ac-wrap {
background-color: $secondary;
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
@@ -88,22 +105,6 @@ div.ac-wrap {
display: inline-block;
line-height: 20px;
}
- a.remove {
- margin-left: 4px;
- font-size: 11px;
- line-height: 10px;
- padding: 1.5px 1.5px 1.5px 2.5px;
- border-radius: 12px;
- width: 10px;
- display: inline-block;
- border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
- &:hover {
- background-color: scale-color($danger, $lightness: 75%);
- border: 1px solid scale-color($danger, $lightness: 30%);
- text-decoration: none;
- color: $danger;
- }
- }
}
input[type="text"] {
float: left;
diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss
index 10a6b9d4558..71d1481196b 100644
--- a/app/assets/stylesheets/desktop/user.scss
+++ b/app/assets/stylesheets/desktop/user.scss
@@ -101,6 +101,21 @@
.user-main {
margin-bottom: 50px;
+ // name hacky so lastpass does not freak out
+ // -search- means it is bypassed
+ #add-user-to-group {
+ button, .ac-wrap {
+ float: left;
+ }
+ button {
+ margin-top: 3px;
+ margin-left: 10px;
+ }
+ #user-search-selector {
+ width: 400px;
+ }
+ }
+
table.group-members {
width: 100%;
p {
@@ -116,6 +131,16 @@
}
td.avatar {
width: 60px;
+ position: relative;
+ .is-owner {
+ position: absolute;
+ right: 0;
+ top: 20px;
+ color: dark-light-diff($primary, $secondary, 50%, -50%);
+ }
+ }
+ td.remove-user {
+ text-align: right;
}
td {
padding: 0.5em;
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 67e8d86966d..f614a15b5f2 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -107,58 +107,30 @@ class Admin::GroupsController < Admin::AdminController
render json: success_json
end
- def add_members
+ def add_owners
group = Group.find(params.require(:id))
-
return can_not_modify_automatic if group.automatic
- if params[:usernames].present?
- users = User.where(username: params[:usernames].split(","))
- elsif params[:user_ids].present?
- users = User.find(params[:user_ids].split(","))
- elsif params[:user_emails].present?
- users = User.where(email: params[:user_emails].split(","))
- else
- raise Discourse::InvalidParameters.new('user_ids or usernames or user_emails must be present')
- end
+ users = User.where(username: params[:usernames].split(","))
users.each do |user|
if !group.users.include?(user)
group.add(user)
- else
- return render_json_error I18n.t('groups.errors.member_already_exist', username: user.username)
end
+ group.group_users.where(user_id: user.id).update_all(owner: true)
end
- if group.save
- render json: success_json
- else
- render_json_error(group)
- end
+ render json: success_json
end
- def remove_member
+ def remove_owner
group = Group.find(params.require(:id))
-
return can_not_modify_automatic if group.automatic
- if params[:user_id].present?
- user = User.find(params[:user_id])
- elsif params[:username].present?
- user = User.find_by_username(params[:username])
- else
- raise Discourse::InvalidParameters.new('user_id or username must be present')
- end
+ user = User.find(params[:user_id].to_i)
+ group.group_users.where(user_id: user.id).update_all(owner: false)
- user.primary_group_id = nil if user.primary_group_id == group.id
-
- group.users.delete(user.id)
-
- if group.save && user.save
- render json: success_json
- else
- render_json_error(group)
- end
+ render json: success_json
end
protected
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 9a51833a623..605d04b7d25 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -23,10 +23,12 @@ class GroupsController < ApplicationController
offset = params[:offset].to_i
total = group.users.count
- members = group.users.order(:username_lower).limit(limit).offset(offset)
+ members = group.users.order('NOT group_users.owner').order(:username_lower).limit(limit).offset(offset)
+ owners = group.users.order(:username_lower).where('group_users.owner')
render json: {
members: serialize_data(members, GroupUserSerializer),
+ owners: serialize_data(owners, GroupUserSerializer),
meta: {
total: total,
limit: limit,
@@ -36,35 +38,56 @@ class GroupsController < ApplicationController
end
def add_members
- guardian.ensure_can_edit!(the_group)
+ group = Group.find(params[:id])
+ guardian.ensure_can_edit!(group)
- added_users = []
- usernames = params.require(:usernames)
- usernames.split(",").each do |username|
- if user = User.find_by_username(username)
- unless the_group.users.include?(user)
- the_group.add(user)
- added_users << user
- end
+ if params[:usernames].present?
+ users = User.where(username: params[:usernames].split(","))
+ elsif params[:user_ids].present?
+ users = User.find(params[:user_ids].split(","))
+ elsif params[:user_emails].present?
+ users = User.where(email: params[:user_emails].split(","))
+ else
+ raise Discourse::InvalidParameters.new('user_ids or usernames or user_emails must be present')
+ end
+
+ users.each do |user|
+ if !group.users.include?(user)
+ group.add(user)
+ else
+ return render_json_error I18n.t('groups.errors.member_already_exist', username: user.username)
end
end
- # always succeeds, even if bogus usernames were provided
- render_serialized(added_users, GroupUserSerializer)
+ if group.save
+ render json: success_json
+ else
+ render_json_error(group)
+ end
end
def remove_member
- guardian.ensure_can_edit!(the_group)
+ group = Group.find(params[:id])
+ guardian.ensure_can_edit!(group)
- removed_users = []
- username = params.require(:username)
- if user = User.find_by_username(username)
- the_group.remove(user)
- removed_users << user
+ if params[:user_id].present?
+ user = User.find(params[:user_id])
+ elsif params[:username].present?
+ user = User.find_by_username(params[:username])
+ else
+ raise Discourse::InvalidParameters.new('user_id or username must be present')
+ end
+
+ user.primary_group_id = nil if user.primary_group_id == group.id
+
+ group.users.delete(user.id)
+
+ if group.save && user.save
+ render json: success_json
+ else
+ render_json_error(group)
end
- # always succeeds, even if user was not a member
- render_serialized(removed_users, GroupUserSerializer)
end
private
diff --git a/app/models/group.rb b/app/models/group.rb
index c14f5bb758b..d43704bf699 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -7,9 +7,6 @@ class Group < ActiveRecord::Base
has_many :categories, through: :category_groups
has_many :users, through: :group_users
- has_many :group_managers, dependent: :destroy
- has_many :managers, through: :group_managers
-
after_save :destroy_deletions
after_save :automatic_group_membership
after_save :update_primary_group
@@ -283,8 +280,8 @@ class Group < ActiveRecord::Base
user.update_columns(primary_group_id: nil) if user.primary_group_id == self.id
end
- def appoint_manager(user)
- managers << user
+ def add_owner(user)
+ self.group_users.create(user_id: user.id, owner: true)
end
protected
diff --git a/app/models/group_user.rb b/app/models/group_user.rb
index a39a470561a..e3129ebc4a0 100644
--- a/app/models/group_user.rb
+++ b/app/models/group_user.rb
@@ -66,6 +66,7 @@ end
# user_id :integer not null
# created_at :datetime not null
# updated_at :datetime not null
+# owner :boolean default(FALSE), not null
#
# Indexes
#
diff --git a/app/models/topic.rb b/app/models/topic.rb
index d3fbb268dd9..deda3d2256d 100644
--- a/app/models/topic.rb
+++ b/app/models/topic.rb
@@ -1062,6 +1062,7 @@ end
# auto_close_based_on_last_post :boolean default(FALSE)
# auto_close_hours :float
# pinned_until :datetime
+# fancy_title :string(400)
#
# Indexes
#
diff --git a/app/models/user.rb b/app/models/user.rb
index cca5693bbfd..e9e45fe27eb 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -52,9 +52,6 @@ class User < ActiveRecord::Base
has_many :groups, through: :group_users
has_many :secure_categories, through: :groups, source: :categories
- has_many :group_managers, dependent: :destroy
- has_many :managed_groups, through: :group_managers, source: :group
-
has_many :muted_user_records, class_name: 'MutedUser'
has_many :muted_users, through: :muted_user_records
@@ -1081,13 +1078,14 @@ end
# uploaded_avatar_id :integer
# email_always :boolean default(FALSE), not null
# mailing_list_mode :boolean default(FALSE), not null
-# locale :string(10)
# primary_group_id :integer
+# locale :string(10)
# registration_ip_address :inet
# last_redirected_to_top_at :datetime
# disable_jump_reply :boolean default(FALSE), not null
# edit_history_public :boolean default(FALSE), not null
# trust_level_locked :boolean default(FALSE), not null
+# staged :boolean default(FALSE), not null
#
# Indexes
#
diff --git a/app/models/user_profile_view.rb b/app/models/user_profile_view.rb
index e3ae1ca7282..0a62a426d1c 100644
--- a/app/models/user_profile_view.rb
+++ b/app/models/user_profile_view.rb
@@ -45,3 +45,21 @@ class UserProfileView < ActiveRecord::Base
profile_views.group("date(viewed_at)").order("date(viewed_at)").count
end
end
+
+# == Schema Information
+#
+# Table name: user_profile_views
+#
+# id :integer not null, primary key
+# user_profile_id :integer not null
+# viewed_at :datetime not null
+# ip_address :inet not null
+# user_id :integer
+#
+# Indexes
+#
+# index_user_profile_views_on_user_id (user_id)
+# index_user_profile_views_on_user_profile_id (user_profile_id)
+# unique_profile_view_ip (viewed_at,ip_address,user_profile_id) UNIQUE
+# unique_profile_view_user (viewed_at,user_id,user_profile_id) UNIQUE
+#
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index d78aabb42e1..162a03aaff0 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -330,6 +330,9 @@ en:
other: "%{count} users"
groups:
+ add: "Add"
+ selector_placeholder: "Add members"
+ owner: "owner"
visible: "Group is visible to all users"
title:
one: "group"
@@ -1937,6 +1940,7 @@ en:
delete_confirm: "Delete this group?"
delete_failed: "Unable to delete group. If this is an automatic group, it cannot be destroyed."
delete_member_confirm: "Remove '%{username}' from the '%{group}' group?"
+ delete_owner_confirm: "Remove owner privilege for '%{username}'?"
name: "Name"
add: "Add"
add_members: "Add members"
@@ -1950,6 +1954,9 @@ en:
automatic_membership_retroactive: "Apply the same email domain rule to add existing registered users"
default_title: "Default title for all users in this group"
primary_group: "Automatically set as primary group"
+ group_owners: Owners
+ add_owners: Add owners
+
api:
generate_master: "Generate Master API Key"
diff --git a/config/routes.rb b/config/routes.rb
index 0af5799a22d..7c02453cbd9 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -65,8 +65,8 @@ Discourse::Application.routes.draw do
put 'bulk' => 'groups#bulk_perform'
end
member do
- put "members" => "groups#add_members"
- delete "members" => "groups#remove_member"
+ put "owners" => "groups#add_owners"
+ delete "owners" => "groups#remove_owner"
end
end
@@ -332,10 +332,16 @@ Discourse::Application.routes.draw do
get 'posts'
get 'counts'
- put "members" => "groups#add_members"
- delete "members/:username" => "groups#remove_member"
+ member do
+ put "members" => "groups#add_members"
+ delete "members" => "groups#remove_member"
+ end
end
+ # aliases so old API code works
+ delete "admin/groups/:id/members" => "groups#remove_member", constraints: AdminConstraint.new
+ put "admin/groups/:id/members" => "groups#add_members", constraints: AdminConstraint.new
+
# In case people try the wrong URL
get '/group/:id', to: redirect('/groups/%{id}')
get '/group/:id/members', to: redirect('/groups/%{id}/members')
diff --git a/db/migrate/20151107042241_add_owner_to_group_users.rb b/db/migrate/20151107042241_add_owner_to_group_users.rb
new file mode 100644
index 00000000000..c834ac13752
--- /dev/null
+++ b/db/migrate/20151107042241_add_owner_to_group_users.rb
@@ -0,0 +1,5 @@
+class AddOwnerToGroupUsers < ActiveRecord::Migration
+ def change
+ add_column :group_users, :owner, :boolean, null: false, default: false
+ end
+end
diff --git a/db/migrate/20151109124147_drop_group_managers.rb b/db/migrate/20151109124147_drop_group_managers.rb
new file mode 100644
index 00000000000..2e197cfb591
--- /dev/null
+++ b/db/migrate/20151109124147_drop_group_managers.rb
@@ -0,0 +1,15 @@
+class DropGroupManagers < ActiveRecord::Migration
+ def up
+ # old data under old structure
+ execute "UPDATE group_users SET owner = true
+ WHERE exists (SELECT 1 FROM group_managers m
+ WHERE m.group_id = group_users.group_id AND
+ m.user_id = group_users.user_id)"
+
+ drop_table "group_managers"
+ end
+
+ def down
+ raise ActiveRecord::IrriversableMigration
+ end
+end
diff --git a/lib/guardian/group_guardian.rb b/lib/guardian/group_guardian.rb
index 308d95b2be0..89b829c5fbc 100644
--- a/lib/guardian/group_guardian.rb
+++ b/lib/guardian/group_guardian.rb
@@ -5,7 +5,7 @@ module GroupGuardian
# Automatic groups are not represented in the GROUP_USERS
# table and thus do not allow membership changes.
def can_edit_group?(group)
- (group.managers.include?(user) || is_admin?) && !group.automatic
+ (group.users.where('group_users.owner').include?(user) || is_admin?) && !group.automatic
end
end
diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb
index 5d12ef0f198..f0d8b94aa10 100644
--- a/spec/components/guardian_spec.rb
+++ b/spec/components/guardian_spec.rb
@@ -291,7 +291,7 @@ describe Guardian do
let(:admin) { Fabricate(:admin) }
let(:private_category) { Fabricate(:private_category, group: group) }
let(:group_private_topic) { Fabricate(:topic, category: private_category) }
- let(:group_manager) { group_private_topic.user.tap { |u| group.add(u); group.appoint_manager(u) } }
+ let(:group_owner) { group_private_topic.user.tap { |u| group.add_owner(u) } }
it 'handles invitation correctly' do
expect(Guardian.new(nil).can_invite_to?(topic)).to be_falsey
@@ -324,8 +324,8 @@ describe Guardian do
expect(Guardian.new(admin).can_invite_to?(private_topic)).to be_truthy
end
- it 'returns true for a group manager' do
- expect(Guardian.new(group_manager).can_invite_to?(group_private_topic)).to be_truthy
+ it 'returns true for a group owner' do
+ expect(Guardian.new(group_owner).can_invite_to?(group_private_topic)).to be_truthy
end
end
diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb
index e7c2233ec14..2735c8736f4 100644
--- a/spec/controllers/admin/groups_controller_spec.rb
+++ b/spec/controllers/admin/groups_controller_spec.rb
@@ -127,92 +127,6 @@ describe Admin::GroupsController do
end
- context ".add_members" do
- it "cannot add members to automatic groups" do
- xhr :put, :add_members, id: 1, usernames: "l77t"
- expect(response.status).to eq(422)
- end
-
- context "is able to add several members to a group" do
-
- let(:user1) { Fabricate(:user) }
- let(:user2) { Fabricate(:user) }
- let(:group) { Fabricate(:group) }
-
- it "adds by username" do
- xhr :put, :add_members, id: group.id, usernames: [user1.username, user2.username].join(",")
-
- expect(response).to be_success
- group.reload
- expect(group.users.count).to eq(2)
- end
-
- it "adds by id" do
- xhr :put, :add_members, id: group.id, user_ids: [user1.id, user2.id].join(",")
-
- expect(response).to be_success
- group.reload
- expect(group.users.count).to eq(2)
- end
- end
-
- it "returns 422 if member already exists" do
- group = Fabricate(:group)
- existing_member = Fabricate(:user)
- group.add(existing_member)
- group.save
-
- xhr :put, :add_members, id: group.id, usernames: existing_member.username
- expect(response.status).to eq(422)
- end
-
- end
-
- context ".remove_member" do
-
- it "cannot remove members from automatic groups" do
- xhr :put, :remove_member, id: 1, user_id: 42
- expect(response.status).to eq(422)
- end
-
- context "is able to remove a member" do
-
- let(:user) { Fabricate(:user) }
- let(:group) { Fabricate(:group) }
-
- before do
- group.add(user)
- group.save
- end
-
- it "removes by id" do
- xhr :delete, :remove_member, id: group.id, user_id: user.id
-
- expect(response).to be_success
- group.reload
- expect(group.users.count).to eq(0)
- end
-
- it "removes by username" do
- xhr :delete, :remove_member, id: group.id, username: user.username
-
- expect(response).to be_success
- group.reload
- expect(group.users.count).to eq(0)
- end
-
- it "removes user.primary_group_id when user is removed from group" do
- user.primary_group_id = group.id
- user.save
-
- xhr :delete, :remove_member, id: group.id, username: user.username
-
- user.reload
- expect(user.primary_group_id).to eq(nil)
- end
- end
-
- end
end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index a148b5eea55..2dc71b4fe18 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -90,18 +90,18 @@ describe GroupsController do
it "refuses membership changes to unauthorized users" do
Guardian.any_instance.stubs(:can_edit?).with(group).returns(false)
- xhr :put, :add_members, group_id: group.name, usernames: "bob"
+ xhr :put, :add_members, id: group.id, usernames: "bob"
expect(response).to be_forbidden
- xhr :delete, :remove_member, group_id: group.name, username: "bob"
+ xhr :delete, :remove_member, id: group.id, username: "bob"
expect(response).to be_forbidden
end
it "cannot add members to automatic groups" do
Guardian.any_instance.stubs(:is_admin?).returns(true)
- auto_group = Fabricate(:group, name: "auto_group", automatic: true)
+ group = Fabricate(:group, name: "auto_group", automatic: true)
- xhr :put, :add_members, group_id: group.name, usernames: "bob"
+ xhr :put, :add_members, id: group.id, usernames: "bob"
expect(response).to be_forbidden
end
end
@@ -117,45 +117,117 @@ describe GroupsController do
it "can make incremental adds" do
user2 = Fabricate(:user)
- xhr :put, :add_members, group_id: group.name, usernames: user2.username
+ xhr :put, :add_members, id: group.id, usernames: user2.username
expect(response).to be_success
group.reload
expect(group.users.count).to eq(2)
end
- it "succeeds silently when adding non-existent users" do
- xhr :put, :add_members, group_id: group.name, usernames: "nosuchperson"
-
- expect(response).to be_success
- group.reload
- expect(group.users.count).to eq(1)
- end
-
- it "succeeds silently when adding duplicate users" do
- xhr :put, :add_members, group_id: group.name, usernames: @user1.username
-
- expect(response).to be_success
- group.reload
- expect(group.users).to eq([@user1])
- end
-
it "can make incremental deletes" do
- xhr :delete, :remove_member, group_id: group.name, username: @user1.username
+ xhr :delete, :remove_member, id: group.id, username: @user1.username
expect(response).to be_success
group.reload
expect(group.users.count).to eq(0)
end
- it "succeeds silently when removing non-members" do
- user2 = Fabricate(:user)
- xhr :delete, :remove_member, group_id: group.name, username: user2.username
+ end
- expect(response).to be_success
- group.reload
- expect(group.users.count).to eq(1)
+ context ".add_members" do
+
+ before do
+ @admin = log_in(:admin)
end
+
+ it "cannot add members to automatic groups" do
+ xhr :put, :add_members, id: 1, usernames: "l77t"
+ expect(response.status).to eq(403)
+ end
+
+ context "is able to add several members to a group" do
+
+ let(:user1) { Fabricate(:user) }
+ let(:user2) { Fabricate(:user) }
+ let(:group) { Fabricate(:group) }
+
+ it "adds by username" do
+ xhr :put, :add_members, id: group.id, usernames: [user1.username, user2.username].join(",")
+
+ expect(response).to be_success
+ group.reload
+ expect(group.users.count).to eq(2)
+ end
+
+ it "adds by id" do
+ xhr :put, :add_members, id: group.id, user_ids: [user1.id, user2.id].join(",")
+
+ expect(response).to be_success
+ group.reload
+ expect(group.users.count).to eq(2)
+ end
+ end
+
+ it "returns 422 if member already exists" do
+ group = Fabricate(:group)
+ existing_member = Fabricate(:user)
+ group.add(existing_member)
+ group.save
+
+ xhr :put, :add_members, id: group.id, usernames: existing_member.username
+ expect(response.status).to eq(422)
+ end
+
+ end
+
+ context ".remove_member" do
+
+ before do
+ @admin = log_in(:admin)
+ end
+
+ it "cannot remove members from automatic groups" do
+ xhr :put, :remove_member, id: 1, user_id: 42
+ expect(response.status).to eq(403)
+ end
+
+ context "is able to remove a member" do
+
+ let(:user) { Fabricate(:user) }
+ let(:group) { Fabricate(:group) }
+
+ before do
+ group.add(user)
+ group.save
+ end
+
+ it "removes by id" do
+ xhr :delete, :remove_member, id: group.id, user_id: user.id
+
+ expect(response).to be_success
+ group.reload
+ expect(group.users.count).to eq(0)
+ end
+
+ it "removes by username" do
+ xhr :delete, :remove_member, id: group.id, username: user.username
+
+ expect(response).to be_success
+ group.reload
+ expect(group.users.count).to eq(0)
+ end
+
+ it "removes user.primary_group_id when user is removed from group" do
+ user.primary_group_id = group.id
+ user.save
+
+ xhr :delete, :remove_member, id: group.id, username: user.username
+
+ user.reload
+ expect(user.primary_group_id).to eq(nil)
+ end
+ end
+
end
end
diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb
index c77f30863da..36ace4d612e 100644
--- a/spec/controllers/posts_controller_spec.rb
+++ b/spec/controllers/posts_controller_spec.rb
@@ -861,8 +861,7 @@ describe PostsController do
Fabricate(:moderator)
group = Fabricate(:group)
- group.add(user)
- group.appoint_manager(user)
+ group.add_owner(user)
secured_category = Fabricate(:private_category, group: group)
secured_post = create_post(user: user, category: secured_category)
diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb
index aba6ae9fec5..feaf5603c63 100644
--- a/spec/controllers/topics_controller_spec.rb
+++ b/spec/controllers/topics_controller_spec.rb
@@ -939,7 +939,7 @@ describe TopicsController do
describe 'when logged in as group manager' do
let(:group_manager) { log_in }
- let(:group) { Fabricate(:group).tap { |g| g.add(group_manager); g.appoint_manager(group_manager) } }
+ let(:group) { Fabricate(:group).tap { |g| g.add_owner(group_manager) } }
let(:private_category) { Fabricate(:private_category, group: group) }
let(:group_private_topic) { Fabricate(:topic, category: private_category, user: group_manager) }
let(:recipient) { 'jake@adventuretime.ooo' }
diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb
index 9ad310e031e..e9b2b29b912 100644
--- a/spec/models/topic_spec.rb
+++ b/spec/models/topic_spec.rb
@@ -1538,7 +1538,7 @@ describe Topic do
context 'invite by group manager' do
let(:group_manager) { Fabricate(:user) }
- let(:group) { Fabricate(:group).tap { |g| g.add(group_manager); g.appoint_manager(group_manager) } }
+ let(:group) { Fabricate(:group).tap { |g| g.add_owner(group_manager) } }
let(:private_category) { Fabricate(:private_category, group: group) }
let(:group_private_topic) { Fabricate(:topic, category: private_category, user: group_manager) }
@@ -1564,8 +1564,5 @@ describe Topic do
end
end
- context 'to a previously-invited user' do
-
- end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 2f3fbfd2730..29be63f8a78 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -986,22 +986,6 @@ describe User do
end
end
- context "group management" do
- let!(:user) { Fabricate(:user) }
-
- it "by default has no managed groups" do
- expect(user.managed_groups).to be_empty
- end
-
- it "can manage multiple groups" do
- 3.times do |i|
- g = Fabricate(:group, name: "group_#{i}")
- g.appoint_manager(user)
- end
- expect(user.managed_groups.count).to eq(3)
- end
- end
-
describe "should_be_redirected_to_top" do
let!(:user) { Fabricate(:user) }
diff --git a/test/javascripts/fixtures/group-fixtures.js.es6 b/test/javascripts/fixtures/group-fixtures.js.es6
index 03382b9a749..fb5a5737803 100644
--- a/test/javascripts/fixtures/group-fixtures.js.es6
+++ b/test/javascripts/fixtures/group-fixtures.js.es6
@@ -1,6 +1,6 @@
export default {
"/groups/discourse.json": {"basic_group":{"id":47,"automatic":false,"name":"discourse","user_count":8,"alias_level":0,"visible":true}},
"/groups/discourse/counts.json": {"counts":{"posts":17829,"members":7}},
- "/groups/discourse/members.json": {"members":[{"id":2770,"username":"awesomerobot","uploaded_avatar_id":33872,"avatar_template":"/user_avatar/meta.discourse.org/awesomerobot/{size}/33872.png","name":"","last_seen_at":"2015-01-23T15:53:17.844Z"},{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png","name":"Jeff Atwood","last_seen_at":"2015-01-23T06:05:25.457Z"},{"id":19,"username":"eviltrout","uploaded_avatar_id":5275,"avatar_template":"/user_avatar/meta.discourse.org/eviltrout/{size}/5275.png","name":"Robin Ward","last_seen_at":"2015-01-23T16:03:45.098Z"},{"id":2,"username":"neil","uploaded_avatar_id":5245,"avatar_template":"/user_avatar/meta.discourse.org/neil/{size}/5245.png","name":"Neil Lalonde","last_seen_at":"2015-01-23T15:22:10.244Z"},{"id":1,"username":"sam","uploaded_avatar_id":5243,"avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","name":"Sam Saffron","last_seen_at":"2015-01-23T11:07:06.233Z"},{"id":3,"username":"supermathie","uploaded_avatar_id":34097,"avatar_template":"/user_avatar/meta.discourse.org/supermathie/{size}/34097.png","name":"Michael Brown","last_seen_at":"2015-01-22T05:16:42.254Z"},{"id":1995,"username":"zogstrip","uploaded_avatar_id":8630,"avatar_template":"/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png","name":"Régis Hanol","last_seen_at":"2015-01-23T15:45:34.196Z"}],"meta":{"total":7,"limit":50,"offset":0}},
+ "/groups/discourse/members.json": {"owners":[],"members":[{"id":2770,"username":"awesomerobot","uploaded_avatar_id":33872,"avatar_template":"/user_avatar/meta.discourse.org/awesomerobot/{size}/33872.png","name":"","last_seen_at":"2015-01-23T15:53:17.844Z"},{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png","name":"Jeff Atwood","last_seen_at":"2015-01-23T06:05:25.457Z"},{"id":19,"username":"eviltrout","uploaded_avatar_id":5275,"avatar_template":"/user_avatar/meta.discourse.org/eviltrout/{size}/5275.png","name":"Robin Ward","last_seen_at":"2015-01-23T16:03:45.098Z"},{"id":2,"username":"neil","uploaded_avatar_id":5245,"avatar_template":"/user_avatar/meta.discourse.org/neil/{size}/5245.png","name":"Neil Lalonde","last_seen_at":"2015-01-23T15:22:10.244Z"},{"id":1,"username":"sam","uploaded_avatar_id":5243,"avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","name":"Sam Saffron","last_seen_at":"2015-01-23T11:07:06.233Z"},{"id":3,"username":"supermathie","uploaded_avatar_id":34097,"avatar_template":"/user_avatar/meta.discourse.org/supermathie/{size}/34097.png","name":"Michael Brown","last_seen_at":"2015-01-22T05:16:42.254Z"},{"id":1995,"username":"zogstrip","uploaded_avatar_id":8630,"avatar_template":"/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png","name":"Régis Hanol","last_seen_at":"2015-01-23T15:45:34.196Z"}],"meta":{"total":7,"limit":50,"offset":0}},
"/groups/discourse/posts.json": [{"id":94607,"cooked":"
Right now we have two entirely different styles for new topics and new posts within a topic... we can probably fix that pretty easily.
\n\n
\n\n
So the simple change would be:
\n\n
\n\n
but... while the dot makes the \"• new\" stand out more... it doesn't communicate any information other than \"look at me\" — can we add more context without adding more noise?
\n\n
","created_at":"2015-01-23T15:13:01.935Z","title":"Consistent new indicator","url":"/t/consistent-new-indicator/24355/1","user_title":"designerator","user_long_name":"","category":{"id":9,"name":"ux","color":"5F497A","topic_id":2628,"topic_count":540,"created_at":"2013-02-10T03:52:21.322Z","updated_at":"2015-01-22T18:05:32.152Z","user_id":32,"topics_year":370,"topics_month":33,"topics_week":3,"slug":"ux","description":"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":5823,"latest_post_id":94610,"latest_topic_id":24355,"position":25,"parent_category_id":null,"posts_year":4264,"posts_month":609,"posts_week":103,"email_in":null,"email_in_allow_strangers":false,"topics_day":0,"posts_day":28,"logo_url":null,"background_url":null,"allow_badges":true,"name_lower":"ux","auto_close_based_on_last_post":false},"user":{"id":2770,"username":"awesomerobot","uploaded_avatar_id":33872,"avatar_template":"/user_avatar/meta.discourse.org/awesomerobot/{size}/33872.png"}},{"id":94603,"cooked":"
Agree that the markup isn't ideal - it's kind of hacked together at the moment; especially because we have two different styles. I think once we settle on the specifics it can be re-written entirely.
","created_at":"2015-01-23T14:59:21.941Z","title":"The end of Clown Vomit, or, simplified category styles","url":"/t/the-end-of-clown-vomit-or-simplified-category-styles/24249/63","user_title":"designerator","user_long_name":"","category":{"id":9,"name":"ux","color":"5F497A","topic_id":2628,"topic_count":540,"created_at":"2013-02-10T03:52:21.322Z","updated_at":"2015-01-22T18:05:32.152Z","user_id":32,"topics_year":370,"topics_month":33,"topics_week":3,"slug":"ux","description":"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":5823,"latest_post_id":94610,"latest_topic_id":24355,"position":25,"parent_category_id":null,"posts_year":4264,"posts_month":609,"posts_week":103,"email_in":null,"email_in_allow_strangers":false,"topics_day":0,"posts_day":28,"logo_url":null,"background_url":null,"allow_badges":true,"name_lower":"ux","auto_close_based_on_last_post":false},"user":{"id":2770,"username":"awesomerobot","uploaded_avatar_id":33872,"avatar_template":"/user_avatar/meta.discourse.org/awesomerobot/{size}/33872.png"}},{"id":94601,"cooked":"
Yeah I think this category arrangement is the way to go at the very least - much easier to scan two columns...
\n\n
Also, maybe square off the bars?
\n\n
\n\n
","created_at":"2015-01-23T14:51:55.497Z","title":"The end of Clown Vomit, or, simplified category styles","url":"/t/the-end-of-clown-vomit-or-simplified-category-styles/24249/62","user_title":"designerator","user_long_name":"","category":{"id":9,"name":"ux","color":"5F497A","topic_id":2628,"topic_count":540,"created_at":"2013-02-10T03:52:21.322Z","updated_at":"2015-01-22T18:05:32.152Z","user_id":32,"topics_year":370,"topics_month":33,"topics_week":3,"slug":"ux","description":"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":5823,"latest_post_id":94610,"latest_topic_id":24355,"position":25,"parent_category_id":null,"posts_year":4264,"posts_month":609,"posts_week":103,"email_in":null,"email_in_allow_strangers":false,"topics_day":0,"posts_day":28,"logo_url":null,"background_url":null,"allow_badges":true,"name_lower":"ux","auto_close_based_on_last_post":false},"user":{"id":2770,"username":"awesomerobot","uploaded_avatar_id":33872,"avatar_template":"/user_avatar/meta.discourse.org/awesomerobot/{size}/33872.png"}},{"id":94577,"cooked":"
Yup, that's the latest version
\n\n
\n\n
(click to view animated version)
","created_at":"2015-01-23T10:50:55.846Z","title":"Quote reply insertion at cursor position","url":"/t/quote-reply-insertion-at-cursor-position/24344/4","user_title":"team","user_long_name":"Régis Hanol","category":{"id":2,"name":"feature","color":"0E76BD","topic_id":11,"topic_count":1592,"created_at":"2013-02-02T21:42:52.552Z","updated_at":"2015-01-22T18:05:32.647Z","user_id":1,"topics_year":919,"topics_month":60,"topics_week":20,"slug":"feature","description":"Discussion about features or potential features of Discourse: how they work, why they work, etc.","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":14360,"latest_post_id":94600,"latest_topic_id":24344,"position":25,"parent_category_id":null,"posts_year":8617,"posts_month":690,"posts_week":190,"email_in":null,"email_in_allow_strangers":false,"topics_day":2,"posts_day":8,"logo_url":null,"background_url":null,"allow_badges":true,"name_lower":"feature","auto_close_based_on_last_post":false},"user":{"id":1995,"username":"zogstrip","uploaded_avatar_id":8630,"avatar_template":"/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png"}},{"id":94574,"cooked":"
\n\n
It used to be that but that was fixed a while ago. Are you running a recent version?
","created_at":"2015-01-23T10:31:29.222Z","title":"Quote reply insertion at cursor position","url":"/t/quote-reply-insertion-at-cursor-position/24344/2","user_title":"team","user_long_name":"Régis Hanol","category":{"id":2,"name":"feature","color":"0E76BD","topic_id":11,"topic_count":1592,"created_at":"2013-02-02T21:42:52.552Z","updated_at":"2015-01-22T18:05:32.647Z","user_id":1,"topics_year":919,"topics_month":60,"topics_week":20,"slug":"feature","description":"Discussion about features or potential features of Discourse: how they work, why they work, etc.","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":14360,"latest_post_id":94600,"latest_topic_id":24344,"position":25,"parent_category_id":null,"posts_year":8617,"posts_month":690,"posts_week":190,"email_in":null,"email_in_allow_strangers":false,"topics_day":2,"posts_day":8,"logo_url":null,"background_url":null,"allow_badges":true,"name_lower":"feature","auto_close_based_on_last_post":false},"user":{"id":1995,"username":"zogstrip","uploaded_avatar_id":8630,"avatar_template":"/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png"}},{"id":94572,"cooked":"
\n\n
That's an Ember update that introduced this change.
","created_at":"2015-01-23T09:46:00.901Z","title":"Translations frequently broken","url":"/t/translations-frequently-broken/22546/27","user_title":"team","user_long_name":"Régis Hanol","category":{"id":27,"name":"translations","color":"808281","topic_id":14549,"topic_count":146,"created_at":"2014-04-07T20:30:17.623Z","updated_at":"2015-01-22T18:05:33.111Z","user_id":2,"topics_year":134,"topics_month":5,"topics_week":3,"slug":"translations","description":"This category is for discussion about localizing Discourse.","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":1167,"latest_post_id":94575,"latest_topic_id":24301,"position":25,"parent_category_id":7,"posts_year":965,"posts_month":60,"posts_week":29,"email_in":null,"email_in_allow_strangers":false,"topics_day":1,"posts_day":5,"logo_url":null,"background_url":null,"allow_badges":true,"name_lower":"translations","auto_close_based_on_last_post":false},"user":{"id":1995,"username":"zogstrip","uploaded_avatar_id":8630,"avatar_template":"/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png"}},{"id":94555,"cooked":"
I don't know how to pronounce that in English, but this makes me think of the French word \"disquette\" (floppy disk)
","created_at":"2015-01-23T08:17:31.700Z","title":"Introducing Discette - a minimal ember-cli front end to Discourse","url":"/t/introducing-discette-a-minimal-ember-cli-front-end-to-discourse/24321/3","user_title":"team","user_long_name":"Régis Hanol","category":{"id":7,"name":"dev","color":"000","topic_id":1026,"topic_count":574,"created_at":"2013-02-06T08:43:41.550Z","updated_at":"2015-01-22T18:05:32.855Z","user_id":32,"topics_year":298,"topics_month":29,"topics_week":2,"slug":"dev","description":"This category is for topics related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth.","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":4196,"latest_post_id":94590,"latest_topic_id":24349,"position":25,"parent_category_id":null,"posts_year":2095,"posts_month":172,"posts_week":16,"email_in":null,"email_in_allow_strangers":false,"topics_day":0,"posts_day":3,"logo_url":null,"background_url":null,"allow_badges":true,"name_lower":"dev","auto_close_based_on_last_post":false},"user":{"id":1995,"username":"zogstrip","uploaded_avatar_id":8630,"avatar_template":"/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png"}},{"id":94544,"cooked":"
@techapj fixed this for 1.2.
","created_at":"2015-01-23T05:49:35.881Z","title":"After sign-in, I'm not redirected to the conversation","url":"/t/after-sign-in-im-not-redirected-to-the-conversation/17753/8","user_title":"co-founder","user_long_name":"Jeff Atwood","category":{"id":9,"name":"ux","color":"5F497A","topic_id":2628,"topic_count":540,"created_at":"2013-02-10T03:52:21.322Z","updated_at":"2015-01-22T18:05:32.152Z","user_id":32,"topics_year":370,"topics_month":33,"topics_week":3,"slug":"ux","description":"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":5823,"latest_post_id":94610,"latest_topic_id":24355,"position":25,"parent_category_id":null,"posts_year":4264,"posts_month":609,"posts_week":103,"email_in":null,"email_in_allow_strangers":false,"topics_day":0,"posts_day":28,"logo_url":null,"background_url":null,"allow_badges":true,"name_lower":"ux","auto_close_based_on_last_post":false},"user":{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png"}},{"id":94543,"cooked":"
Oh yes IOS 8.2 -- well, let's see what happens because there is really no fix on our end. Basic HTML / CSS stuff is broken.
","created_at":"2015-01-23T05:45:40.306Z","title":"Dealing with iOS 8 Mobile Safari bugs?","url":"/t/dealing-with-ios-8-mobile-safari-bugs/24101/7","user_title":"co-founder","user_long_name":"Jeff Atwood","category":{"id":2,"name":"feature","color":"0E76BD","topic_id":11,"topic_count":1592,"created_at":"2013-02-02T21:42:52.552Z","updated_at":"2015-01-22T18:05:32.647Z","user_id":1,"topics_year":919,"topics_month":60,"topics_week":20,"slug":"feature","description":"Discussion about features or potential features of Discourse: how they work, why they work, etc.","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":14360,"latest_post_id":94600,"latest_topic_id":24344,"position":25,"parent_category_id":null,"posts_year":8617,"posts_month":690,"posts_week":190,"email_in":null,"email_in_allow_strangers":false,"topics_day":2,"posts_day":8,"logo_url":null,"background_url":null,"allow_badges":true,"name_lower":"feature","auto_close_based_on_last_post":false},"user":{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png"}},{"id":94542,"cooked":"
Hmm that looks like a bug, @techapj can you have a look?
","created_at":"2015-01-23T05:43:55.602Z","title":"RSS is not valid","url":"/t/rss-is-not-valid/24338/2","user_title":"co-founder","user_long_name":"Jeff Atwood","category":{"id":6,"name":"support","color":"CEA9A9","topic_id":389,"topic_count":1781,"created_at":"2013-02-05T22:16:38.672Z","updated_at":"2015-01-22T18:05:33.572Z","user_id":1,"topics_year":1541,"topics_month":167,"topics_week":49,"slug":"support","description":"Support on configuring and using Discourse after it is up and running. For installation questions, use the install category.","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":12272,"latest_post_id":94602,"latest_topic_id":24346,"position":25,"parent_category_id":null,"posts_year":10571,"posts_month":1254,"posts_week":413,"email_in":null,"email_in_allow_strangers":false,"topics_day":5,"posts_day":70,"logo_url":"","background_url":"","allow_badges":true,"name_lower":"support","auto_close_based_on_last_post":false},"user":{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png"}},{"id":94522,"cooked":"
Oh I see. @zogstrip can you have a look?
","created_at":"2015-01-23T03:00:20.485Z","title":"Pasted image upload size error","url":"/t/pasted-image-upload-size-error/24320/4","user_title":"co-founder","user_long_name":"Jeff Atwood","category":{"id":1,"name":"bug","color":"e9dd00","topic_id":2,"topic_count":1729,"created_at":"2013-02-01T04:56:34.914Z","updated_at":"2015-01-22T18:05:33.426Z","user_id":1,"topics_year":1114,"topics_month":69,"topics_week":22,"slug":"bug","description":"A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.","text_color":"000000","read_restricted":false,"auto_close_hours":null,"post_count":11179,"latest_post_id":94611,"latest_topic_id":24350,"position":25,"parent_category_id":null,"posts_year":7138,"posts_month":397,"posts_week":121,"email_in":null,"email_in_allow_strangers":false,"topics_day":1,"posts_day":6,"logo_url":null,"background_url":null,"allow_badges":true,"name_lower":"bug","auto_close_based_on_last_post":false},"user":{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png"}},{"id":94521,"cooked":"
\n\n
Yeah probably.
\n\n
\n\n
Definitely a good idea. We have seen some eye melting color schemes people have picked for categories.. Much less subcategories.
\n\n
\n\n
Sure try http://talk.folksy.com -- it's still too much color in boxes. Particularly anywhere a bunch of categories are displayed together, which is a lot of places considering the topic list is the main form of nav, both on the homepage default of latest and in suggested topics at the bottom of every topic...
\n\n
","created_at":"2015-01-23T02:58:27.451Z","title":"The end of Clown Vomit, or, simplified category styles","url":"/t/the-end-of-clown-vomit-or-simplified-category-styles/24249/57","user_title":"co-founder","user_long_name":"Jeff Atwood","category":{"id":9,"name":"ux","color":"5F497A","topic_id":2628,"topic_count":540,"created_at":"2013-02-10T03:52:21.322Z","updated_at":"2015-01-22T18:05:32.152Z","user_id":32,"topics_year":370,"topics_month":33,"topics_week":3,"slug":"ux","description":"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":5823,"latest_post_id":94610,"latest_topic_id":24355,"position":25,"parent_category_id":null,"posts_year":4264,"posts_month":609,"posts_week":103,"email_in":null,"email_in_allow_strangers":false,"topics_day":0,"posts_day":28,"logo_url":null,"background_url":null,"allow_badges":true,"name_lower":"ux","auto_close_based_on_last_post":false},"user":{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png"}},{"id":94519,"cooked":"
What would you suggest writing here that would be more clear?
","created_at":"2015-01-23T02:45:36.859Z","title":"What is \"Born mobile, born to touch\" supposed to tell me?","url":"/t/what-is-born-mobile-born-to-touch-supposed-to-tell-me/24329/3","user_title":"co-founder","user_long_name":"Jeff Atwood","category":{"id":3,"name":"meta","color":"aaa","topic_id":24,"topic_count":139,"created_at":"2013-02-03T00:00:15.230Z","updated_at":"2015-01-22T18:05:32.797Z","user_id":1,"topics_year":68,"topics_month":5,"topics_week":1,"slug":"meta","description":"Discussion about meta.discourse.org itself, the organization of this forum about Discourse, how it works, and how we can improve this site.","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":1116,"latest_post_id":94559,"latest_topic_id":24208,"position":25,"parent_category_id":null,"posts_year":553,"posts_month":33,"posts_week":8,"email_in":null,"email_in_allow_strangers":false,"topics_day":0,"posts_day":0,"logo_url":null,"background_url":null,"allow_badges":true,"name_lower":"meta","auto_close_based_on_last_post":false},"user":{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png"}},{"id":94518,"cooked":"
You should generally create topics to host things like this, then make them wiki, close them, etc.
","created_at":"2015-01-23T02:42:20.053Z","title":"How to Create Static Pages in Discourse?","url":"/t/how-to-create-static-pages-in-discourse/24313/2","user_title":"co-founder","user_long_name":"Jeff Atwood","category":{"id":6,"name":"support","color":"CEA9A9","topic_id":389,"topic_count":1781,"created_at":"2013-02-05T22:16:38.672Z","updated_at":"2015-01-22T18:05:33.572Z","user_id":1,"topics_year":1541,"topics_month":167,"topics_week":49,"slug":"support","description":"Support on configuring and using Discourse after it is up and running. For installation questions, use the install category.","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":12272,"latest_post_id":94602,"latest_topic_id":24346,"position":25,"parent_category_id":null,"posts_year":10571,"posts_month":1254,"posts_week":413,"email_in":null,"email_in_allow_strangers":false,"topics_day":5,"posts_day":70,"logo_url":"","background_url":"","allow_badges":true,"name_lower":"support","auto_close_based_on_last_post":false},"user":{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png"}},{"id":94517,"cooked":"
Doubtful this is a bug, probably dependent on the PNG encoding.
\n\n
Try using PNGOUT, or converting to 8 bit PNGOUT, to see some of the differences. And PNGOUT is lossless!
","created_at":"2015-01-23T02:41:30.287Z","title":"Pasted image upload size error","url":"/t/pasted-image-upload-size-error/24320/2","user_title":"co-founder","user_long_name":"Jeff Atwood","category":{"id":1,"name":"bug","color":"e9dd00","topic_id":2,"topic_count":1729,"created_at":"2013-02-01T04:56:34.914Z","updated_at":"2015-01-22T18:05:33.426Z","user_id":1,"topics_year":1114,"topics_month":69,"topics_week":22,"slug":"bug","description":"A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.","text_color":"000000","read_restricted":false,"auto_close_hours":null,"post_count":11179,"latest_post_id":94611,"latest_topic_id":24350,"position":25,"parent_category_id":null,"posts_year":7138,"posts_month":397,"posts_week":121,"email_in":null,"email_in_allow_strangers":false,"topics_day":1,"posts_day":6,"logo_url":null,"background_url":null,"allow_badges":true,"name_lower":"bug","auto_close_based_on_last_post":false},"user":{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png"}},{"id":94516,"cooked":"
I would worry about getting your expenses down to $5 per month, that seems more likely over time as hosting for Docker compliant sites gets cheaper.
","created_at":"2015-01-23T02:40:11.726Z","title":"Monetizing Discourse Talk","url":"/t/monetizing-discourse-talk/24316/4","user_title":"co-founder","user_long_name":"Jeff Atwood","category":{"id":6,"name":"support","color":"CEA9A9","topic_id":389,"topic_count":1781,"created_at":"2013-02-05T22:16:38.672Z","updated_at":"2015-01-22T18:05:33.572Z","user_id":1,"topics_year":1541,"topics_month":167,"topics_week":49,"slug":"support","description":"Support on configuring and using Discourse after it is up and running. For installation questions, use the install category.","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":12272,"latest_post_id":94602,"latest_topic_id":24346,"position":25,"parent_category_id":null,"posts_year":10571,"posts_month":1254,"posts_week":413,"email_in":null,"email_in_allow_strangers":false,"topics_day":5,"posts_day":70,"logo_url":"","background_url":"","allow_badges":true,"name_lower":"support","auto_close_based_on_last_post":false},"user":{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png"}},{"id":94515,"cooked":"
Liked just for the word \"Discettes\" which is adorable
","created_at":"2015-01-23T02:38:29.185Z","title":"Introducing Discette - a minimal ember-cli front end to Discourse","url":"/t/introducing-discette-a-minimal-ember-cli-front-end-to-discourse/24321/2","user_title":"co-founder","user_long_name":"Jeff Atwood","category":{"id":7,"name":"dev","color":"000","topic_id":1026,"topic_count":574,"created_at":"2013-02-06T08:43:41.550Z","updated_at":"2015-01-22T18:05:32.855Z","user_id":32,"topics_year":298,"topics_month":29,"topics_week":2,"slug":"dev","description":"This category is for topics related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth.","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":4196,"latest_post_id":94590,"latest_topic_id":24349,"position":25,"parent_category_id":null,"posts_year":2095,"posts_month":172,"posts_week":16,"email_in":null,"email_in_allow_strangers":false,"topics_day":0,"posts_day":3,"logo_url":null,"background_url":null,"allow_badges":true,"name_lower":"dev","auto_close_based_on_last_post":false},"user":{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png"}},{"id":94514,"cooked":"
\n\n
This is a good idea, are the documents public web URLs? Perhaps we could help build this onebox if so.
\n\n
\n\n
Hmm. I suspect this could be done via the API. Query all new topics (assuming older topics are already synced), and for those with a certain URL within the topic (first post only? All posts?) ping those URLs.
\n\n
This could potentially be done with a webhook on save on the Discourse side.
\n\n
Let us know how we can help, very interested in public projects like this.
","created_at":"2015-01-23T02:37:39.518Z","title":"How to do \"Object Oriented Discussion\" through Oneboxes?","url":"/t/how-to-do-object-oriented-discussion-through-oneboxes/24328/2","user_title":"co-founder","user_long_name":"Jeff Atwood","category":{"id":5,"name":"extensibility","color":"FE8432","topic_id":28,"topic_count":295,"created_at":"2013-02-03T08:42:06.329Z","updated_at":"2015-01-22T18:05:32.698Z","user_id":1,"topics_year":187,"topics_month":17,"topics_week":7,"slug":"extensibility","description":"Topics about extending the functionality of Discourse with plugins, themes, add-ons, or other mechanisms for extensibility. ","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":2574,"latest_post_id":94582,"latest_topic_id":24328,"position":25,"parent_category_id":null,"posts_year":1485,"posts_month":196,"posts_week":52,"email_in":null,"email_in_allow_strangers":false,"topics_day":2,"posts_day":8,"logo_url":null,"background_url":null,"allow_badges":true,"name_lower":"extensibility","auto_close_based_on_last_post":false},"user":{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png"}},{"id":94512,"cooked":"
Hmm, have not seen problems updating on 1gb instance provided swap is there.
\n\n
Anything else running on the machine?
\n\n
Maybe reboot, then upgrade Docker from command line, then upgrade Discourse from command line.
","created_at":"2015-01-23T02:32:31.383Z","title":"Update Failed and Now Showing Currently Upgrading","url":"/t/update-failed-and-now-showing-currently-upgrading/24332/2","user_title":"co-founder","user_long_name":"Jeff Atwood","category":{"id":6,"name":"support","color":"CEA9A9","topic_id":389,"topic_count":1781,"created_at":"2013-02-05T22:16:38.672Z","updated_at":"2015-01-22T18:05:33.572Z","user_id":1,"topics_year":1541,"topics_month":167,"topics_week":49,"slug":"support","description":"Support on configuring and using Discourse after it is up and running. For installation questions, use the install category.","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":12272,"latest_post_id":94602,"latest_topic_id":24346,"position":25,"parent_category_id":null,"posts_year":10571,"posts_month":1254,"posts_week":413,"email_in":null,"email_in_allow_strangers":false,"topics_day":5,"posts_day":70,"logo_url":"","background_url":"","allow_badges":true,"name_lower":"support","auto_close_based_on_last_post":false},"user":{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png"}},{"id":94511,"cooked":"
Hmm, not sure about that, good odds they will be fixed in iOS 8.1 which is due soon.
","created_at":"2015-01-23T02:27:16.786Z","title":"Dealing with iOS 8 Mobile Safari bugs?","url":"/t/dealing-with-ios-8-mobile-safari-bugs/24101/5","user_title":"co-founder","user_long_name":"Jeff Atwood","category":{"id":2,"name":"feature","color":"0E76BD","topic_id":11,"topic_count":1592,"created_at":"2013-02-02T21:42:52.552Z","updated_at":"2015-01-22T18:05:32.647Z","user_id":1,"topics_year":919,"topics_month":60,"topics_week":20,"slug":"feature","description":"Discussion about features or potential features of Discourse: how they work, why they work, etc.","text_color":"FFFFFF","read_restricted":false,"auto_close_hours":null,"post_count":14360,"latest_post_id":94600,"latest_topic_id":24344,"position":25,"parent_category_id":null,"posts_year":8617,"posts_month":690,"posts_week":190,"email_in":null,"email_in_allow_strangers":false,"topics_day":2,"posts_day":8,"logo_url":null,"background_url":null,"allow_badges":true,"name_lower":"feature","auto_close_based_on_last_post":false},"user":{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png"}}]
};