Merge pull request #4577 from tgxworld/allow_group_owner_to_edit

FEATURE: Allow group owners to edit group name and avatar flair.
This commit is contained in:
Guo Xiang Tan 2016-12-05 07:28:55 +01:00 committed by GitHub
commit 0441ae3e95
35 changed files with 596 additions and 318 deletions

View File

@ -1,7 +1,5 @@
import { popupAjaxError } from 'discourse/lib/ajax-error';
import { propertyEqual } from 'discourse/lib/computed';
import { escapeExpression } from 'discourse/lib/utilities';
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
adminGroupsType: Ember.inject.controller(),
@ -37,43 +35,6 @@ export default Ember.Controller.extend({
];
}.property(),
@computed
demoAvatarUrl() {
return Discourse.getURL('/images/avatar.png');
},
@computed('model.flair_url')
flairPreviewIcon() {
return this.get('model.flair_url') && this.get('model.flair_url').substr(0,3) === 'fa-';
},
@computed('flairPreviewIcon')
flairPreviewImage() {
return this.get('model.flair_url') && !this.get('flairPreviewIcon');
},
@computed('flairPreviewImage', 'model.flair_url', 'model.flairBackgroundHexColor', 'model.flairHexColor')
flairPreviewStyle() {
var style = '';
if (this.get('flairPreviewImage')) {
style += 'background-image: url(' + escapeExpression(this.get('model.flair_url')) + '); ';
}
if (this.get('model.flairBackgroundHexColor')) {
style += 'background-color: #' + this.get('model.flairBackgroundHexColor') + ';';
}
if (this.get('model.flairHexColor')) {
style += 'color: #' + this.get('model.flairHexColor') + ';';
}
return style;
},
@computed('model.flairBackgroundHexColor')
flairPreviewClasses() {
if (this.get('model.flairBackgroundHexColor')) {
return 'rounded';
}
},
actions: {
next() {
if (this.get("showingLast")) { return; }

View File

@ -4,8 +4,8 @@
{{#if model.automatic}}
<h3>{{model.name}}</h3>
{{else}}
<label for="name">{{i18n 'admin.groups.name'}}</label>
{{text-field name="name" value=model.name placeholderKey="admin.groups.name_placeholder"}}
<label for="name">{{i18n 'group.name'}}</label>
{{text-field name="name" value=model.name placeholderKey="group.name_placeholder"}}
{{/if}}
</div>
@ -101,59 +101,7 @@
{{/unless}}
{{#unless model.automatic}}
<div class="flair-inputs">
<div class="flair-left">
<div>
<label for="flair_url">{{i18n 'admin.groups.flair_url'}}</label>
{{text-field name="flair_url" value=model.flair_url placeholderKey="admin.groups.flair_url_placeholder"}}
</div>
<div>
<label for="flair_bg_color">{{i18n 'admin.groups.flair_bg_color'}}</label>
{{text-field name="flair_bg_color" class="flair-bg-color" value=model.flair_bg_color placeholderKey="admin.groups.flair_bg_color_placeholder"}}
</div>
{{#if flairPreviewIcon}}
<div>
<label for="flair_color">{{i18n 'admin.groups.flair_color'}}</label>
{{text-field name="flair_color" class="flair-color" value=model.flair_color placeholderKey="admin.groups.flair_color_placeholder"}}
</div>
{{/if}}
<br/>
<div>
<strong>{{i18n 'admin.groups.flair_note'}}</strong>
</div>
</div>
{{#if flairPreviewIcon}}
<div class="flair-right">
<label>{{i18n 'admin.groups.flair_preview'}} Icon</label>
<div class="avatar-flair-preview">
<div class="avatar-wrapper">
<img alt width="45" height="45" src="{{demoAvatarUrl}}" class="avatar actor">
</div>
<div class="avatar-flair demo {{flairPreviewClasses}}" style={{flairPreviewStyle}}>
<i class="fa {{model.flair_url}}"></i>
</div>
</div>
</div>
{{/if}}
{{#if flairPreviewImage}}
<div class="flair-right">
<label>{{i18n 'admin.groups.flair_preview'}} Image</label>
<div class="avatar-flair-preview">
<div class="avatar-wrapper">
<img alt width="45" height="45" src="{{demoAvatarUrl}}" class="avatar actor">
</div>
<div class="avatar-flair demo {{flairPreviewClasses}}" style={{flairPreviewStyle}}></div>
</div>
</div>
{{/if}}
<div class="clearfix"></div>
</div>
{{group-flair-inputs model=model}}
{{/unless}}
<div class='buttons'>

View File

@ -0,0 +1,20 @@
import { observes } from 'ember-addons/ember-computed-decorators';
import MountWidget from 'discourse/components/mount-widget';
export default MountWidget.extend({
widget: 'avatar-flair',
@observes('flairURL', 'flairBgColor', 'flairColor')
_rerender() {
this.queueRerender();
},
buildArgs() {
return {
primary_group_flair_url: this.get('flairURL'),
primary_group_flair_bg_color: this.get('flairBgColor'),
primary_group_flair_color: this.get('flairColor'),
primary_group_name: this.get('groupName')
};
}
});

View File

@ -0,0 +1,50 @@
import computed from 'ember-addons/ember-computed-decorators';
import { escapeExpression } from 'discourse/lib/utilities';
export default Ember.Component.extend({
classNames: ['group-flair-inputs'],
@computed
demoAvatarUrl() {
return Discourse.getURL('/images/avatar.png');
},
@computed('model.flair_url')
flairPreviewIcon(flairURL) {
return flairURL && flairURL.substr(0,3) === 'fa-';
},
@computed('model.flair_url', 'flairPreviewIcon')
flairPreviewImage(flairURL, flairPreviewIcon) {
return flairURL && !flairPreviewIcon;
},
@computed('model.flair_url', 'flairPreviewImage', 'model.flairBackgroundHexColor', 'model.flairHexColor')
flairPreviewStyle(flairURL, flairPreviewImage, flairBackgroundHexColor, flairHexColor) {
let style = '';
if (flairPreviewImage) {
style += `background-image: url(${escapeExpression(flairURL)});`;
}
if (flairBackgroundHexColor) {
style += `background-color: #${flairBackgroundHexColor};`;
}
if (flairHexColor) style += `color: #${flairHexColor};`;
return style;
},
@computed('model.flairBackgroundHexColor')
flairPreviewClasses(flairBackgroundHexColor) {
if (flairBackgroundHexColor) return 'rounded';
},
@computed('flairPreviewImage')
flairPreviewLabel(flairPreviewImage) {
const key = flairPreviewImage ? 'image' : 'icon';
return I18n.t(`group.flair_preview_${key}`);
}
});

View File

@ -0,0 +1,20 @@
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Controller.extend({
saving: false,
actions: {
save() {
this.set('saving', true);
this.get('model').save().then(() => {
this.transitionToRoute('group', this.get('model.name'));
this.send('closeModal');
}).catch(error => {
popupAjaxError(error);
}).finally(() => {
this.set('saving', false);
});
}
}
});

View File

@ -1,22 +1,11 @@
import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from 'ember-addons/ember-computed-decorators';
import Group from 'discourse/models/group';
export default Ember.Controller.extend({
loading: false,
limit: null,
offset: null,
@computed('model.owners.[]')
isOwner(owners) {
if (this.get('currentUser.admin')) {
return true;
}
const currentUserId = this.get('currentUser.id');
if (currentUserId) {
return !!owners.findBy('id', currentUserId);
}
},
isOwner: Ember.computed.alias('model.is_group_owner'),
actions: {
removeMember(user) {

View File

@ -23,6 +23,11 @@ export default Ember.Controller.extend({
Tab.create({ name: 'messages', requiresMembership: true })
],
@computed('model.is_group_owner', 'model.automatic')
canEditGroup(isGroupOwner, automatic) {
return !automatic && isGroupOwner;
},
@computed('model.name')
groupName(name) {
return name.capitalize();

View File

@ -119,13 +119,19 @@ const Group = Discourse.Model.extend({
create() {
var self = this;
return ajax("/admin/groups", { type: "POST", data: this.asJSON() }).then(function(resp) {
return ajax("/admin/groups", { type: "POST", data: { group: this.asJSON() } }).then(function(resp) {
self.set('id', resp.basic_group.id);
});
},
save() {
return ajax("/admin/groups/" + this.get('id'), { type: "PUT", data: this.asJSON() });
const id = this.get('id');
const url = this.get('is_group_owner') ? `/groups/${id}` : `/admin/groups/${id}`;
return ajax(url, {
type: "PUT",
data: { group: this.asJSON() }
});
},
destroy() {

View File

@ -1,4 +1,5 @@
import Group from 'discourse/models/group';
import showModal from 'discourse/lib/show-modal';
export default Discourse.Route.extend({
@ -16,5 +17,12 @@ export default Discourse.Route.extend({
setupController(controller, model) {
controller.setProperties({ model, counts: this.get('counts') });
},
actions: {
showGroupEditor() {
showModal('edit-group');
this.controllerFor('edit-group').set('model', this.modelFor('group'));
}
}
});

View File

@ -0,0 +1,47 @@
<div class="group-flair-left">
<div>
<label for="flair_url">{{i18n 'group.flair_url'}}</label>
{{text-field name="flair_url"
value=model.flair_url
placeholderKey="group.flair_url_placeholder"}}
</div>
<div>
<label for="flair_bg_color">{{i18n 'group.flair_bg_color'}}</label>
{{text-field name="flair_bg_color"
class="group-flair-bg-color"
value=model.flair_bg_color
placeholderKey="group.flair_bg_color_placeholder"}}
</div>
{{#if flairPreviewIcon}}
<div>
<label for="flair_color">{{i18n 'group.flair_color'}}</label>
{{text-field name="flair_color"
class="group-flair-color"
value=model.flair_color
placeholderKey="group.flair_color_placeholder"}}
</div>
{{/if}}
<div>
<strong>{{i18n 'group.flair_note'}}</strong>
</div>
</div>
<div class="group-flair-right">
<label>{{flairPreviewLabel}}</label>
<div class="avatar-flair-preview">
<div class="avatar-wrapper">
<img width="45" height="45" src="{{demoAvatarUrl}}" class="avatar actor">
</div>
{{#if flairPreviewImage}}
<div class="avatar-flair demo {{flairPreviewClasses}}" style={{flairPreviewStyle}}></div>
{{else}}
<div class="avatar-flair demo {{flairPreviewClasses}}" style={{flairPreviewStyle}}>
<i class="fa {{model.flair_url}}"></i>
</div>
{{/if}}
</div>
</div>

View File

@ -7,3 +7,5 @@
{{/each}}
</div>
{{/load-more}}
{{conditional-loading-spinner condition=loading}}

View File

@ -4,7 +4,11 @@
<div class="user-card-avatar">
<a href={{user.path}} {{action "showUser"}} class="card-huge-avatar">{{bound-avatar avatar "huge"}}</a>
{{#if user.primary_group_name}}
{{mount-widget widget="avatar-flair" args=user}}
{{avatar-flair
flairURL=user.primary_group_flair_url
flairBgColor=user.primary_group_flair_bg_color
flairColor=user.primary_group_flair_color
groupName=user.primary_group_name}}
{{/if}}
</div>

View File

@ -1,45 +1,48 @@
{{#if model}}
{{#if isOwner}}
<div class='clearfix'>
<form id='add-user-to-group' autocomplete="off">
{{user-selector usernames=usernames placeholderKey="groups.selector_placeholder" id="user-search-selector" name="usernames"}}
{{d-button action="addMembers" class="add" icon="plus" label="groups.add"}}
</form>
</div>
<form id='add-user-to-group' autocomplete="off">
{{user-selector usernames=usernames placeholderKey="groups.selector_placeholder" id="user-search-selector" name="usernames"}}
{{d-button action="addMembers" class="add" icon="plus" label="groups.add"}}
</form>
{{/if}}
{{#load-more selector=".group-members tr" action="loadMore"}}
<table class='group-members'>
<tr>
<th colspan="2">{{i18n 'last_post'}}</th>
<th>{{i18n 'last_seen'}}</th>
{{#if isOwner}}
<thead>
<th></th>
{{/if}}
</tr>
{{#each model.members as |m|}}
<tr>
<td class='avatar'>
{{user-info user=m}}
{{#if m.owner}}<span class='is-owner'>{{i18n "groups.owner"}}</span>{{/if}}
</td>
<td>
<span class="text">{{bound-date m.last_posted_at}}</span>
</td>
<td>
<span class="text">{{bound-date m.last_seen_at}}</span>
</td>
{{#if isOwner}}
<td class='remove-user'>
{{#unless m.owner}}
<a class="remove-link" {{action "removeMember" m}}><i class="fa fa-times"></i></a>
{{/unless}}
<th>{{i18n 'last_post'}}</th>
<th>{{i18n 'last_seen'}}</th>
<th></th>
</thead>
<tbody>
{{#each model.members as |m|}}
<tr>
<td class='avatar'>
{{#user-info user=m skipName=skipName}}
{{#if m.owner}}<strong class="group-owner-label">{{i18n "groups.owner"}}</strong>{{/if}}
{{/user-info}}
</td>
{{/if}}
</tr>
{{/each}}
<td>
<span class="text">{{bound-date m.last_posted_at}}</span>
</td>
<td>
<span class="text">{{bound-date m.last_seen_at}}</span>
</td>
<td class='remove-user'>
{{#if isOwner}}
{{#unless m.owner}}
<a class="remove-link" {{action "removeMember" m}}><i class="fa fa-times"></i></a>
{{/unless}}
{{/if}}
</td>
</tr>
{{/each}}
</tbody>
</table>
{{/load-more}}
{{conditional-loading-spinner condition=loading}}
{{else}}
<div>{{i18n "groups.empty.users"}}</div>
{{/if}}

View File

@ -1 +1 @@
{{group-post-stream posts=model emptyText=emptyText loadMore="loadMore"}}
{{group-post-stream posts=model emptyText=emptyText loadMore="loadMore" loading=loading}}

View File

@ -1,7 +1,6 @@
<div class="container user-table">
<div class="wrapper">
<section class='user-navigation'>
<ul class='action-list nav-stacked'>
<div class="container group">
<div class='pull-left group-nav'>
<ul class='nav-stacked'>
{{#each getTabs as |tab|}}
<li class="{{if tab.active 'active'}}">
{{#link-to tab.location model title=tab.message}}
@ -11,24 +10,34 @@
</li>
{{/each}}
</ul>
</section>
</div>
<section class='user-main'>
<section class='user-right groups'>
<section class='about group'>
<div class='details'>
<h1 class='group-header'>
{{#if model.flair_url}}
<span class='group-avatar-flair'>
{{mount-widget widget="avatar-flair" args=avatarFlairAttributes}}
</span>
{{/if}}
<span class='group-name'>{{groupName}}</span>
</h1>
</div>
</section>
<div class='pull-left group-outlet'>
<div class='group-details'>
<span>
<h1 class='group-header'>
{{#if model.flair_url}}
<span class='group-avatar-flair'>
{{avatar-flair
flairURL=model.flair_url
flairBgColor=model.flair_bg_color
flairColor=model.flair_color
groupName=model.name}}
</span>
{{/if}}
<span class='group-name'>{{groupName}}</span>
</h1>
</span>
{{#if canEditGroup}}
<span>
{{d-button action="showGroupEditor" class="group-edit-btn btn-small" icon="pencil"}}
</span>
{{/if}}
</div>
<div class='user-main'>
{{outlet}}
</section>
</section>
</div>
</div>
</div>

View File

@ -0,0 +1,10 @@
{{#d-modal-body title="group.edit.title" class="edit-group groups"}}
<form class="form-horizontal">
{{group-flair-inputs model=model}}
</form>
{{/d-modal-body}}
<div class="modal-footer">
{{d-button action="save" class="btn-primary" disabled=saving label="save"}}
<a {{action "closeModal"}}>{{i18n 'cancel'}}</a>
</div>

View File

@ -696,38 +696,6 @@ section.details {
width: 100%;
border-color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%));
}
.avatar-flair-preview {
position: relative;
width: 45px;
.avatar-wrapper {
background-color: #f4f4f4;
}
}
.form-horizontal {
.flair-inputs {
margin-top: 30px;
margin-bottom: 30px;
.flair-left {
float: left;
width: 60%;
input[name=flair_url] {
width: 90%;
}
}
.flair-right {
float: left;
margin-left: 30px;
}
}
}
}
.row.groups {
input[type='text'].flair-bg-color, input[type='text'].flair-color {
width: 200px;
}
}
// Customise area

View File

@ -0,0 +1,101 @@
.group-header {
font-size: 2.142em;
font-weight: normal;
}
table.group-members {
width: 100%;
th, tr {
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
}
th {
text-align: left;
}
tr {
.user-info {
display: block;
}
td {
color: dark-light-diff($primary, $secondary, 50%, -50%);
padding: 0.8em 0;
}
}
}
.group-owner-label {
color: $primary;
}
.group-header, .group-details {
display: table;
span {
display: table-cell;
vertical-align: middle;
}
.avatar-flair {
$size: 40px;
background-size: $size;
height: $size;
width: $size;
i {
font-size: $size !important;
}
}
}
.group-edit-btn {
margin-left: 5px;
}
.form-horizontal {
label {
font-weight: bold;
}
input[type="text"] {
width: 80% !important;
margin-bottom: 10px;
}
.group-flair-inputs {
display: inline-block;
margin-top: 30px;
margin-bottom: 30px;
.group-flair-left {
float: left;
}
.group-flair-right {
float: left;
margin-left: 30px;
}
}
.avatar-flair-preview {
position: relative;
width: 45px;
.avatar-wrapper {
background-color: #f4f4f4;
}
}
}
#add-user-to-group {
.ac-wrap {
width: 100% !important;
}
.add {
margin-top: 10px;
}
}

View File

@ -1,24 +0,0 @@
.groups {
.group-header {
display: table;
}
.group-avatar-flair {
.avatar-flair {
$size: 40px;
background-size: $size;
height: $size;
width: $size;
i {
font-size: $size !important;
}
}
}
.group-avatar-flair, .group-name {
display: table-cell;
vertical-align: middle;
}
}

View File

@ -19,6 +19,7 @@
@import "desktop/history";
@import "desktop/queued-posts";
@import "desktop/menu-panel";
@import "desktop/group";
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */

View File

@ -0,0 +1,12 @@
.group-outlet {
width: 75%;
}
.group-nav {
width: 20%;
margin-right: 30px;
}
.group-details {
margin-bottom: 20px;
}

View File

@ -133,50 +133,6 @@
}
}
table.group-members {
width: 100%;
p {
max-width: 600px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
th {
padding: 0.5em;
text-align: right;
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
}
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;
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
img {
margin-right: 10px;
}
span.text {
float: right;
font-size: 1.2em;
color: dark-light-diff($primary, $secondary, 50%, -50%);
}
}
}
.user-right.groups {
margin-top: 0;
}
.user-right {
width: 900px;
margin-top: 20px;

View File

@ -22,6 +22,7 @@
@import "mobile/search";
@import "mobile/emoji";
@import "mobile/ring";
@import "mobile/group";
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */

View File

@ -0,0 +1,23 @@
.group {
margin-top: 15px;
}
.group-nav, .group-outlet {
width: 100%;
}
table.group-members {
th {
text-align: center;
}
tr {
.user-info {
width: 130px;
}
td {
padding-left: 0.5em;
}
}
}

View File

@ -36,7 +36,7 @@ class Admin::GroupsController < Admin::AdminController
def create
group = Group.new
group.name = (params[:name] || '').strip
group.name = (group_params[:name] || '').strip
save_group(group)
end
@ -44,29 +44,29 @@ class Admin::GroupsController < Admin::AdminController
group = Group.find(params[:id])
# group rename is ignored for automatic groups
group.name = params[:name] if params[:name] && !group.automatic
group.name = group_params[:name] if group_params[:name] && !group.automatic
save_group(group)
end
def save_group(group)
group.alias_level = params[:alias_level].to_i if params[:alias_level].present?
group.visible = params[:visible] == "true"
grant_trust_level = params[:grant_trust_level].to_i
group.alias_level = group_params[:alias_level].to_i if group_params[:alias_level].present?
group.visible = group_params[:visible] == "true"
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 = params[:automatic_membership_email_domains] unless group.automatic
group.automatic_membership_retroactive = params[:automatic_membership_retroactive] == "true" unless group.automatic
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 : params["primary_group"] == "true"
group.primary_group = group.automatic ? false : group_params["primary_group"] == "true"
group.incoming_email = group.automatic ? nil : params[:incoming_email]
group.incoming_email = group.automatic ? nil : group_params[:incoming_email]
title = params[:title] if params[:title].present?
title = group_params[:title] if group_params[:title].present?
group.title = group.automatic ? nil : title
group.flair_url = params[:flair_url].presence
group.flair_bg_color = params[:flair_bg_color].presence
group.flair_color = params[:flair_color].presence
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
if group.save
Group.reset_counters(group.id, :group_users)
@ -124,7 +124,18 @@ class Admin::GroupsController < Admin::AdminController
protected
def can_not_modify_automatic
render json: {errors: I18n.t('groups.errors.can_not_modify_automatic')}, status: 422
end
def can_not_modify_automatic
render json: {errors: I18n.t('groups.errors.can_not_modify_automatic')}, status: 422
end
private
def group_params
params.require(:group).permit(
:name, :alias_level, :visible, :automatic_membership_email_domains,
:automatic_membership_retroactive, :title, :primary_group,
:grant_trust_level, :incoming_email, :flair_url, :flair_bg_color,
:flair_color
)
end
end

View File

@ -1,12 +1,28 @@
class GroupsController < ApplicationController
before_filter :ensure_logged_in, only: [:set_notifications, :mentionable]
before_filter :ensure_logged_in, only: [
:set_notifications,
:mentionable,
:update
]
skip_before_filter :preload_json, :check_xhr, only: [:posts_feed, :mentions_feed]
def show
render_serialized(find_group(:id), GroupShowSerializer, root: 'basic_group')
end
def update
group = Group.find(params[:id])
guardian.ensure_can_edit!(group)
if group.update_attributes(group_params)
render json: success_json
else
render_json_error(group)
end
end
def posts
group = find_group(:group_id)
posts = group.posts_for(guardian, params[:before_post_id]).limit(20)
@ -152,11 +168,15 @@ class GroupsController < ApplicationController
private
def find_group(param_name)
name = params.require(param_name)
group = Group.find_by("lower(name) = ?", name.downcase)
guardian.ensure_can_see!(group)
group
end
def group_params
params.require(:group).permit(:flair_url, :flair_bg_color, :flair_color)
end
def find_group(param_name)
name = params.require(param_name)
group = Group.find_by("lower(name) = ?", name.downcase)
guardian.ensure_can_see!(group)
group
end
end

View File

@ -349,7 +349,11 @@ class Group < ActiveRecord::Base
end
def add_owner(user)
self.group_users.create(user_id: user.id, owner: true)
if group_user = self.group_users.find_by(user: user)
group_user.update_attributes!(owner: true) if !group_user.owner
else
GroupUser.create!(user: user, group: self, owner: true)
end
end
def self.find_by_email(email)

View File

@ -1,11 +1,25 @@
class GroupShowSerializer < BasicGroupSerializer
attributes :is_group_user
attributes :is_group_user, :is_group_owner
def include_is_group_user?
scope.authenticated?
end
def is_group_user
object.users.include?(scope.user)
!!fetch_group_user
end
def include_is_group_owner?
scope.authenticated?
end
def is_group_owner
scope.is_admin? || fetch_group_user&.owner
end
private
def fetch_group_user
@group_user ||= object.group_users.find_by(user: scope.user)
end
end

View File

@ -1845,6 +1845,21 @@ en:
title: "Show the raw source diffs side-by-side"
button: '<i class="fa fa-columns"></i> Raw'
group:
edit:
title: 'Edit Group'
name: "Name"
name_placeholder: "Group name, no spaces, same as username rule"
flair_url: "Avatar Flair Image"
flair_url_placeholder: "(Optional) Image URL or Font Awesome class"
flair_bg_color: "Avatar Flair Background Color"
flair_bg_color_placeholder: "(Optional) Hex color value"
flair_color: "Avatar Flair Color"
flair_color_placeholder: "(Optional) Hex color value"
flair_preview_icon: "Preview Icon"
flair_preview_image: "Preview Image"
flair_note: "Note: Flair will only show for a user's primary group."
category:
can: 'can&hellip; '
none: '(no category)'
@ -2451,7 +2466,6 @@ en:
refresh: "Refresh"
new: "New"
selector_placeholder: "enter username"
name_placeholder: "Group name, no spaces, same as username rule"
about: "Edit your group membership and names here"
group_members: "Group members"
delete: "Delete"
@ -2459,7 +2473,6 @@ en:
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"
custom: "Custom"
@ -2476,14 +2489,6 @@ en:
add_owners: Add owners
incoming_email: "Custom incoming email address"
incoming_email_placeholder: "enter email address"
flair_url: "Avatar Flair Image"
flair_url_placeholder: "(Optional) Image URL or Font Awesome class"
flair_bg_color: "Avatar Flair Background Color"
flair_bg_color_placeholder: "(Optional) Hex color value"
flair_color: "Avatar Flair Color"
flair_color_placeholder: "(Optional) Hex color value"
flair_preview: "Preview"
flair_note: "Note: Flair will only show for a user's primary group."
api:
generate_master: "Generate Master API Key"

View File

@ -66,7 +66,7 @@ describe Admin::GroupsController do
context ".create" do
it "strip spaces on the group name" do
xhr :post, :create, name: " bob "
xhr :post, :create, { group: { name: " bob " } }
expect(response.status).to eq(200)
@ -81,7 +81,7 @@ describe Admin::GroupsController do
context ".update" do
it "ignore name change on automatic group" do
xhr :put, :update, id: 1, name: "WAT", visible: "true"
xhr :put, :update, { id: 1, group: { name: "WAT", visible: "true" } }
expect(response).to be_success
group = Group.find(1)
@ -92,14 +92,14 @@ describe Admin::GroupsController do
it "doesn't launch the 'automatic group membership' job when it's not retroactive" do
Jobs.expects(:enqueue).never
group = Fabricate(:group)
xhr :put, :update, id: group.id, automatic_membership_retroactive: "false"
xhr :put, :update, { id: group.id, group: { automatic_membership_retroactive: "false" } }
expect(response).to be_success
end
it "launches the 'automatic group membership' job when it's retroactive" do
group = Fabricate(:group)
Jobs.expects(:enqueue).with(:automatic_group_membership, group_id: group.id)
xhr :put, :update, id: group.id, automatic_membership_retroactive: "true"
xhr :put, :update, { id: group.id, group: { automatic_membership_retroactive: "true" } }
expect(response).to be_success
end

View File

@ -1,22 +1,22 @@
require 'rails_helper'
describe "Groups" do
describe "checking if a group can be mentioned" do
let(:password) { 'somecomplicatedpassword' }
let(:email_token) { Fabricate(:email_token, confirmed: true) }
let(:user) { email_token.user }
let(:group) { Fabricate(:group, name: 'test', users: [user]) }
let(:password) { 'somecomplicatedpassword' }
let(:email_token) { Fabricate(:email_token, confirmed: true) }
let(:user) { email_token.user }
before do
user.update_attributes!(password: password)
end
before do
user.update_attributes!(password: password)
post "/session.json", { login: user.username, password: password }
expect(response).to be_success
end
describe "checking if a group can be mentioned" do
let(:group) { Fabricate(:group, name: 'test', users: [user]) }
it "should return the right response" do
group
post "/session.json", { login: user.username, password: password }
expect(response).to be_success
get "/groups/test/mentionable.json", { name: group.name }
expect(response).to be_success
@ -33,4 +33,51 @@ describe "Groups" do
expect(response_body["mentionable"]).to eq(true)
end
end
describe "group can be updated" do
let(:group) { Fabricate(:group, name: 'test', users: [user]) }
context "when user is group owner" do
before do
group.add_owner(user)
end
it "should be able update the group" do
xhr :put, "/groups/#{group.id}", { group: {
flair_bg_color: 'FFF',
flair_color: 'BBB',
flair_url: 'fa-adjust'
} }
expect(response).to be_success
group.reload
expect(group.flair_bg_color).to eq('FFF')
expect(group.flair_color).to eq('BBB')
expect(group.flair_url).to eq('fa-adjust')
end
end
context "when user is group admin" do
before do
user.update_attributes!(admin: true)
end
it 'should be able to update the group' do
xhr :put, "/groups/#{group.id}", { group: { flair_color: 'BBB' } }
expect(response).to be_success
expect(group.reload.flair_color).to eq('BBB')
end
end
context "when user is not a group owner or admin" do
it 'should not be able to update the group' do
xhr :put, "/groups/#{group.id}", { group: { name: 'testing' } }
expect(response.status).to eq(403)
end
end
end
end

View File

@ -0,0 +1,31 @@
require 'rails_helper'
describe GroupShowSerializer do
context 'admin user' do
let(:user) { Fabricate(:admin) }
let(:group) { Fabricate(:group, users: [user]) }
it 'should return the right attributes' do
json = GroupShowSerializer.new(group, scope: Guardian.new(user)).as_json
expect(json[:group_show][:is_group_owner]).to eq(true)
expect(json[:group_show][:is_group_user]).to eq(true)
end
end
context 'group owner' do
let(:user) { Fabricate(:user) }
let(:group) { Fabricate(:group) }
before do
group.add_owner(user)
end
it 'should return the right attributes' do
json = GroupShowSerializer.new(group, scope: Guardian.new(user)).as_json
expect(json[:group_show][:is_group_owner]).to eq(true)
expect(json[:group_show][:is_group_user]).to eq(true)
end
end
end

View File

@ -27,18 +27,24 @@ test("Browsing Groups", () => {
visit("/groups/discourse/messages");
andThen(() => {
ok($('.action-list li').length === 4, 'it should not show messages tab');
ok($('.nav-stacked li').length === 4, 'it should not show messages tab');
ok(count('.user-stream .item') > 0, "it lists stream items");
});
});
test("Messages tab", () => {
test("Admin Browsing Groups", () => {
logIn();
Discourse.reset();
visit("/groups/discourse");
andThen(() => {
ok($('.action-list li').length === 5, 'it should show messages tab if user is admin');
ok($('.nav-stacked li').length === 5, 'it should show messages tab if user is admin');
});
click('.group-edit-btn');
andThen(() => {
ok(find('.group-flair-inputs').length === 1, 'it should display avatar flair inputs');
});
});

View File

@ -0,0 +1,19 @@
moduleFor("controller:group");
test("canEditGroup", function() {
const GroupController = this.subject();
GroupController.setProperties({
model: { is_group_owner: true, automatic: true }
});
equal(GroupController.get("canEditGroup"), false, "automatic groups cannot be edited");
GroupController.set("model.automatic", false);
equal(GroupController.get("canEditGroup"), true, "owners can edit groups");
GroupController.set("model.is_group_owner", false);
equal(GroupController.get("canEditGroup"), false, "normal users cannot edit groups");
});

View File

@ -7,7 +7,8 @@ export default {
"user_count":8,
"alias_level":0,
"visible":true,
"flair_url": 'fa-adjust'
"flair_url": 'fa-adjust',
"is_group_owner":true
}
},
"/groups/discourse/counts.json":{