Merge branch 'private-groups', closed #2588

v1.18.x
Julian Lam 10 years ago
commit 50ac813388

@ -59,7 +59,7 @@
"socket.io-redis": "^0.1.3",
"socketio-wildcard": "~0.1.1",
"string": "^3.0.0",
"templates.js": "0.1.9",
"templates.js": "0.1.10",
"uglify-js": "git+https://github.com/julianlam/UglifyJS2.git",
"underscore": "~1.7.0",
"validator": "~3.26.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

@ -33,6 +33,7 @@
"header.tags": "Tags",
"header.popular": "Popular",
"header.users": "Users",
"header.groups": "Groups",
"header.chats": "Chats",
"header.notifications": "Notifications",
"header.search": "Search",

@ -1,9 +1,25 @@
{
"groups": "Groups",
"view_group": "View Group",
"owner": "Group Owner",
"new_group": "Create New Group",
"no_groups_found": "There are no groups to see",
"cover-instructions": "Drag and Drop a photo, drag to position, and hit <strong>Save</strong>",
"cover-change": "Change",
"cover-save": "Save",
"cover-saving": "Saving",
"details.title": "Group Details",
"details.members": "Member List",
"details.pending": "Pending Members",
"details.has_no_posts": "This group's members have not made any posts.",
"details.latest_posts": "Latest Posts"
"details.latest_posts": "Latest Posts",
"details.private": "Private Group",
"details.public": "Public Group",
"details.owner_options": "Group Administration",
"event.updated": "Group details have been updated",
"event.deleted": "The group \"%1\" has been deleted"
}

@ -3,7 +3,7 @@
define('admin/manage/categories', [
'uploader',
'admin/modules/iconSelect',
'iconSelect',
'admin/modules/colorpicker'
], function(uploader, iconSelect, colorpicker) {
var Categories = {};

@ -2,7 +2,7 @@
/*global define, templates, socket, ajaxify, app, admin, bootbox*/
define('admin/manage/groups', [
'admin/modules/iconSelect',
'iconSelect',
'admin/modules/colorpicker'
], function(iconSelect, colorpicker) {
var Groups = {};
@ -49,7 +49,7 @@ define('admin/manage/groups', [
},
errorText;
socket.emit('admin.groups.create', submitObj, function(err, data) {
socket.emit('admin.groups.create', submitObj, function(err) {
if (err) {
switch (err) {
case 'group-exists':
@ -68,7 +68,7 @@ define('admin/manage/groups', [
createModalError.addClass('hide');
createGroupName.val('');
createModal.on('hidden.bs.modal', function() {
ajaxify.go('admin/manage/groups');
ajaxify.refresh();
});
createModal.modal('hide');
}
@ -102,12 +102,14 @@ define('admin/manage/groups', [
case 'delete':
bootbox.confirm('Are you sure you wish to delete this group?', function(confirm) {
if (confirm) {
socket.emit('admin.groups.delete', groupName, function(err, data) {
socket.emit('groups.delete', {
groupName: groupName
}, function(err, data) {
if(err) {
return app.alertError(err.message);
}
ajaxify.go('admin/manage/groups');
ajaxify.refresh();
});
}
});

@ -1,17 +1,242 @@
"use strict";
/* globals define, socket, ajaxify, app, bootbox, RELATIVE_PATH */
define('forum/groups/details', function() {
var Details = {};
define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker', 'vendor/jquery/draggable-background/backgroundDraggable'], function(iconSelect) {
var Details = {
cover: {}
};
Details.init = function() {
var memberListEl = $('.groups.details .members');
var detailsPage = $('.groups'),
settingsFormEl = detailsPage.find('form');
memberListEl.on('click', '[data-slug]', function() {
var slug = this.getAttribute('data-slug');
ajaxify.go('user/' + slug);
});
if (ajaxify.variables.get('is_owner') === 'true') {
Details.prepareSettings();
Details.initialiseCover();
}
$('.latest-posts .content img').addClass('img-responsive');
detailsPage.on('click', '[data-action]', function() {
var btnEl = $(this),
userRow = btnEl.parents('tr'),
ownerFlagEl = userRow.find('.member-name i'),
isOwner = !ownerFlagEl.hasClass('invisible') ? true : false,
uid = userRow.attr('data-uid'),
action = btnEl.attr('data-action');
switch(action) {
case 'toggleOwnership':
socket.emit('groups.' + (isOwner ? 'rescind' : 'grant'), {
toUid: uid,
groupName: ajaxify.variables.get('group_name')
}, function(err) {
if (!err) {
ownerFlagEl.toggleClass('invisible');
} else {
app.alertError(err);
}
});
break;
case 'update':
Details.update();
break;
case 'delete':
Details.deleteGroup();
break;
case 'join': // intentional fall-throughs!
case 'leave':
case 'accept':
case 'reject':
socket.emit('groups.' + action, {
toUid: uid,
groupName: ajaxify.variables.get('group_name')
}, function(err) {
if (!err) {
ajaxify.refresh();
} else {
app.alertError(err);
}
});
break;
}
});
};
Details.prepareSettings = function() {
var settingsFormEl = $('.groups form'),
colorBtn = settingsFormEl.find('[data-action="color-select"]'),
colorValueEl = settingsFormEl.find('[name="labelColor"]'),
iconBtn = settingsFormEl.find('[data-action="icon-select"]'),
previewEl = settingsFormEl.find('.label'),
previewIcon = previewEl.find('i'),
previewValueEl = settingsFormEl.find('[name="icon"]');
// Add color picker to settings form
colorBtn.ColorPicker({
color: colorValueEl.val() || '#000',
onChange: function(hsb, hex) {
colorValueEl.val('#' + hex);
previewEl.css('background-color', '#' + hex);
},
onShow: function(colpkr) {
$(colpkr).css('z-index', 1051);
}
});
// Add icon selection interface
iconBtn.on('click', function() {
iconSelect.init(previewIcon, function() {
previewValueEl.val(previewIcon.val());
});
});
};
Details.update = function() {
var settingsFormEl = $('.groups form'),
checkboxes = settingsFormEl.find('input[type="checkbox"][name]');
if (settingsFormEl.length) {
require(['vendor/jquery/serializeObject/jquery.ba-serializeobject.min'], function() {
var settings = settingsFormEl.serializeObject();
// Fix checkbox values
checkboxes.each(function(idx, inputEl) {
inputEl = $(inputEl);
if (inputEl.length) {
settings[inputEl.attr('name')] = inputEl.prop('checked');
}
});
socket.emit('groups.update', {
groupName: ajaxify.variables.get('group_name'),
values: settings
}, function(err) {
if (err) {
return app.alertError(err.message);
}
if (settings.name) {
ajaxify.go('groups/' + encodeURIComponent(settings.name));
} else {
ajaxify.refresh();
}
app.alertSuccess('[[groups:event.updated]]');
});
});
}
};
Details.deleteGroup = function() {
bootbox.confirm('Are you sure you want to delete the group: ' + ajaxify.variables.get('group_name'), function(confirm) {
if (confirm) {
bootbox.prompt('Please enter the name of this group in order to delete it:', function(response) {
if (response === ajaxify.variables.get('group_name')) {
socket.emit('groups.delete', {
groupName: ajaxify.variables.get('group_name')
}, function(err) {
if (!err) {
app.alertSuccess('[[groups:event.deleted, ' + ajaxify.variables.get('group_name') + ']]');
ajaxify.go('groups');
} else {
app.alertError(err.message);
}
});
}
});
}
});
};
// Cover Photo Handling Code
Details.initialiseCover = function() {
var coverEl = $('.group-cover');
coverEl.find('.change').on('click', function() {
coverEl.toggleClass('active', 1);
coverEl.backgroundDraggable({
axis: 'y'
});
coverEl.on('dragover', Details.cover.onDragOver);
coverEl.on('drop', Details.cover.onDrop);
});
coverEl.find('.save').on('click', Details.cover.save);
coverEl.addClass('initialised');
};
Details.cover.load = function() {
socket.emit('groups.cover.get', {
groupName: ajaxify.variables.get('group_name')
}, function(err, data) {
if (!err) {
var coverEl = $('.group-cover');
if (data['cover:url']) {
coverEl.css('background-image', 'url(' + data['cover:url'] + ')');
}
if (data['cover:position']) {
coverEl.css('background-position', data['cover:position']);
}
delete Details.cover.newCover;
} else {
app.alertError(err.message);
}
});
};
Details.cover.onDragOver = function(e) {
e.stopPropagation();
e.preventDefault();
e.originalEvent.dataTransfer.dropEffect = 'copy';
};
Details.cover.onDrop = function(e) {
var coverEl = $('.group-cover');
e.stopPropagation();
e.preventDefault();
var files = e.originalEvent.dataTransfer.files,
reader = new FileReader();
if (files.length && files[0].type.match('image.*')) {
reader.onload = function(e) {
coverEl.css('background-image', 'url(' + e.target.result + ')');
coverEl.backgroundDraggable();
Details.cover.newCover = e.target.result;
};
reader.readAsDataURL(files[0]);
}
};
Details.cover.save = function() {
var coverEl = $('.group-cover');
coverEl.addClass('saving');
socket.emit('groups.cover.update', {
groupName: ajaxify.variables.get('group_name'),
imageData: Details.cover.newCover || undefined,
position: $('.group-cover').css('background-position')
}, function(err) {
if (!err) {
coverEl.toggleClass('active', 0);
coverEl.backgroundDraggable('disable');
coverEl.off('dragover', Details.cover.onDragOver);
coverEl.off('drop', Details.cover.onDrop);
Details.cover.load();
} else {
app.alertError(err.message);
}
coverEl.removeClass('saving');
});
};
return Details;

@ -0,0 +1,35 @@
"use strict";
/* globals app, define, ajaxify, socket, bootbox */
define('forum/groups/list', function() {
var Groups = {};
Groups.init = function() {
var groupsEl = $('.groups.row');
groupsEl.on('click', '.list-cover', function() {
var groupName = $(this).parents('[data-group]').attr('data-group');
ajaxify.go('groups/' + groupName);
});
// Group creation
$('button[data-action="new"]').on('click', function() {
bootbox.prompt('Group Name:', function(name) {
if (name && name.length) {
socket.emit('groups.create', {
name: name
}, function(err) {
if (!err) {
ajaxify.go('groups/' + name);
} else {
app.alertError(err.message);
}
});
}
});
});
};
return Groups;
});

@ -26,7 +26,7 @@
if (groupObj.isMember) {
return '<button class="btn btn-danger" data-action="leave" data-group="' + groupObj.name + '"><i class="fa fa-times"></i> Leave Group</button>';
} else {
if (groupObj.pending) {
if (groupObj.isPending) {
return '<button class="btn btn-warning disabled"><i class="fa fa-clock-o"></i> Invitation Pending</button>';
} else {
return '<button class="btn btn-success" data-action="join" data-group="' + groupObj.name + '"><i class="fa fa-plus"></i> Join Group</button>';

@ -7,7 +7,9 @@ define(function() {
iconSelect.init = function(el, onModified) {
onModified = onModified || function() {};
var selected = el.attr('class').replace('fa-2x', '').replace('fa', '').replace(/\s+/g, '');
var doubleSize = el.hasClass('fa-2x'),
selected = el.attr('class').replace('fa-2x', '').replace('fa', '').replace(/\s+/g, '');
$('#icons .selected').removeClass('selected');
if (selected === '') {
@ -25,7 +27,7 @@ define(function() {
categoryIconClass = '';
}
el.attr('class', 'fa fa-2x ' + categoryIconClass);
el.attr('class', 'fa ' + (doubleSize ? 'fa-2x ' : '') + categoryIconClass);
el.val(categoryIconClass);
el.attr('value', categoryIconClass);

@ -3,7 +3,7 @@
height: 176px;
overflow: hidden;
position: absolute;
background: url(./images/custom_background.png);
background: url(./vendor/colorpicker/images/custom_background.png);
font-family: Arial, Helvetica, sans-serif;
display: none;
}
@ -23,7 +23,7 @@
left: 0;
width: 150px;
height: 150px;
background: url(./images/colorpicker_overlay.png);
background: url(./vendor/colorpicker/images/colorpicker_overlay.png);
}
.colorpicker_color div div {
position: absolute;
@ -32,7 +32,7 @@
width: 11px;
height: 11px;
overflow: hidden;
background: url(./images/colorpicker_select.gif);
background: url(./vendor/colorpicker/images/colorpicker_select.gif);
margin: -5px 0 0 -5px;
}
.colorpicker_hue {
@ -48,7 +48,7 @@
width: 35px;
height: 9px;
overflow: hidden;
background: url(./images/custom_indic.gif) left top;
background: url(./vendor/colorpicker/images/custom_indic.gif) left top;
margin: -4px 0 0 0;
left: 0px;
}
@ -86,7 +86,7 @@
position: absolute;
width: 72px;
height: 22px;
background: url(./images/custom_hex.png) top;
background: url(./vendor/colorpicker/images/custom_hex.png) top;
left: 212px;
top: 142px;
}
@ -109,32 +109,32 @@
cursor: n-resize;
}
.colorpicker_rgb_r {
background-image: url(./images/custom_rgb_r.png);
background-image: url(./vendor/colorpicker/images/custom_rgb_r.png);
top: 52px;
left: 212px;
}
.colorpicker_rgb_g {
background-image: url(./images/custom_rgb_g.png);
background-image: url(./vendor/colorpicker/images/custom_rgb_g.png);
top: 82px;
left: 212px;
}
.colorpicker_rgb_b {
background-image: url(./images/custom_rgb_b.png);
background-image: url(./vendor/colorpicker/images/custom_rgb_b.png);
top: 112px;
left: 212px;
}
.colorpicker_hsb_h {
background-image: url(./images/custom_hsb_h.png);
background-image: url(./vendor/colorpicker/images/custom_hsb_h.png);
top: 52px;
left: 282px;
}
.colorpicker_hsb_s {
background-image: url(./images/custom_hsb_s.png);
background-image: url(./vendor/colorpicker/images/custom_hsb_s.png);
top: 82px;
left: 282px;
}
.colorpicker_hsb_b {
background-image: url(./images/custom_hsb_b.png);
background-image: url(./vendor/colorpicker/images/custom_hsb_b.png);
top: 112px;
left: 282px;
}
@ -143,7 +143,7 @@
position: absolute;
width: 22px;
height: 22px;
background: url(./images/custom_submit.png) top;
background: url(./vendor/colorpicker/images/custom_submit.png) top;
left: 322px;
top: 142px;
overflow: hidden;

@ -0,0 +1,157 @@
/**
* Draggable Background plugin for jQuery
*
* v1.2.4
*
* Copyright (c) 2014 Kenneth Chung
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/mit-license.php
*/
;(function($) {
var $window = $(window);
// Helper function to guarantee a value between low and hi unless bool is false
var limit = function(low, hi, value, bool) {
if (arguments.length === 3 || bool) {
if (value < low) return low;
if (value > hi) return hi;
}
return value;
};
// Adds clientX and clientY properties to the jQuery's event object from touch
var modifyEventForTouch = function(e) {
e.clientX = e.originalEvent.touches[0].clientX;
e.clientY = e.originalEvent.touches[0].clientY;
};
var getBackgroundImageDimensions = function($el) {
var bgSrc = ($el.css('background-image').match(/^url\(['"]?(.*?)['"]?\)$/i) || [])[1];
if (!bgSrc) return;
var imageDimensions = { width: 0, height: 0 },
image = new Image();
image.onload = function() {
if ($el.css('background-size') == "cover") {
var elementWidth = $el.innerWidth(),
elementHeight = $el.innerHeight(),
elementAspectRatio = elementWidth / elementHeight;
imageAspectRatio = image.width / image.height,
scale = 1;
if (imageAspectRatio >= elementAspectRatio) {
scale = elementHeight / image.height;
} else {
scale = elementWidth / image.width;
}
imageDimensions.width = image.width * scale;
imageDimensions.height = image.height * scale;
} else {
imageDimensions.width = image.width;
imageDimensions.height = image.height;
}
};
image.src = bgSrc;
return imageDimensions;
};
function Plugin(element, options) {
this.element = element;
this.options = options;
this.init();
}
Plugin.prototype.init = function() {
var $el = $(this.element),
bgSrc = ($el.css('background-image').match(/^url\(['"]?(.*?)['"]?\)$/i) || [])[1],
options = this.options;
if (!bgSrc) return;
// Get the image's width and height if bound
var imageDimensions = { width: 0, height: 0 };
if (options.bound) {
imageDimensions = getBackgroundImageDimensions($el);
}
$el.on('mousedown.dbg touchstart.dbg', function(e) {
if (e.target !== $el[0]) {
return;
}
e.preventDefault();
if (e.originalEvent.touches) {
modifyEventForTouch(e);
} else if (e.which !== 1) {
return;
}
var x0 = e.clientX,
y0 = e.clientY,
pos = $el.css('background-position').match(/(-?\d+).*?\s(-?\d+)/) || [],
xPos = parseInt(pos[1]) || 0,
yPos = parseInt(pos[2]) || 0;
$window.on('mousemove.dbg touchmove.dbg', function(e) {
e.preventDefault();
if (e.originalEvent.touches) {
modifyEventForTouch(e);
}
var x = e.clientX,
y = e.clientY;
xPos = options.axis === 'y' ? xPos : limit($el.innerWidth()-imageDimensions.width, 0, xPos+x-x0, options.bound);
yPos = options.axis === 'x' ? yPos : limit($el.innerHeight()-imageDimensions.height, 0, yPos+y-y0, options.bound);
x0 = x;
y0 = y;
$el.css('background-position', xPos + 'px ' + yPos + 'px');
});
$window.on('mouseup.dbg touchend.dbg mouseleave.dbg', function() {
if (options.done) {
options.done();
}
$window.off('mousemove.dbg touchmove.dbg');
$window.off('mouseup.dbg touchend.dbg mouseleave.dbg');
});
});
};
Plugin.prototype.disable = function() {
var $el = $(this.element);
$el.off('mousedown.dbg touchstart.dbg');
$window.off('mousemove.dbg touchmove.dbg mouseup.dbg touchend.dbg mouseleave.dbg');
}
$.fn.backgroundDraggable = function(options) {
var options = options;
var args = Array.prototype.slice.call(arguments, 1);
return this.each(function() {
var $this = $(this);
if (typeof options == 'undefined' || typeof options == 'object') {
options = $.extend({}, $.fn.backgroundDraggable.defaults, options);
var plugin = new Plugin(this, options);
$this.data('dbg', plugin);
} else if (typeof options == 'string' && $this.data('dbg')) {
var plugin = $this.data('dbg');
Plugin.prototype[options].apply(plugin, args);
}
});
};
$.fn.backgroundDraggable.defaults = {
bound: true,
axis: undefined
};
}(jQuery));

@ -237,6 +237,9 @@ adminController.extend.widgets = function(req, res, next) {
{ name: 'Global Sidebar', template: 'global', location: 'sidebar' },
{ name: 'Global Header', template: 'global', location: 'header' },
{ name: 'Global Footer', template: 'global', location: 'footer' },
{ name: 'Group Page (Left)', template: 'groups/details.tpl', location: 'left'},
{ name: 'Group Page (Right)', template: 'groups/details.tpl', location: 'right'}
];
plugins.fireHook('filter:widgets.getAreas', defaultAreas, next);

@ -9,7 +9,8 @@ var groups = require('../groups'),
groupsController.list = function(req, res, next) {
groups.list({
truncateUserList: true,
expand: true
expand: true,
uid: req.user ? req.user.uid : 0
}, function(err, groups) {
if (err) {
return next(err);
@ -22,10 +23,12 @@ groupsController.list = function(req, res, next) {
groupsController.details = function(req, res, next) {
var uid = req.user ? parseInt(req.user.uid, 10) : 0;
async.parallel({
group: function(next) {
groups.get(req.params.name, {
expand: true
expand: true,
uid: uid
}, next);
},
posts: function(next) {

@ -72,6 +72,10 @@ uploadsController.uploadThumb = function(req, res, next) {
}, next);
};
uploadsController.uploadGroupCover = function(data, next) {
uploadImage(0/*req.user.uid*/, data, next);
};
function uploadImage(uid, image, callback) {
if (plugins.hasListeners('filter:uploadImage')) {
return plugins.fireHook('filter:uploadImage', {image: image, uid: uid}, callback);

@ -3,14 +3,20 @@
var async = require('async'),
winston = require('winston'),
_ = require('underscore'),
crypto = require('crypto'),
path = require('path'),
nconf = require('nconf'),
fs = require('fs'),
user = require('./user'),
meta = require('./meta'),
db = require('./database'),
plugins = require('./plugins'),
posts = require('./posts'),
privileges = require('./privileges'),
utils = require('../public/src/utils');
utils = require('../public/src/utils'),
uploadsController = require('./controllers/uploads');
(function(Groups) {
@ -60,7 +66,12 @@ var async = require('async'),
}
return groups;
}
}/*,
fixImageUrl: function(url) {
if (url) {
return url.indexOf('http') === -1 ? nconf.get('relative_path') + url : url;
}
}*/
};
Groups.list = function(options, callback) {
@ -104,35 +115,132 @@ var async = require('async'),
}
}
if (options.expand) {
async.waterfall([
async.apply(async.map, uids, user.getUserData),
function(users, next) {
// Filter out non-matches
users = users.filter(Boolean);
async.mapLimit(users, 10, function(userObj, next) {
Groups.ownership.isOwner(userObj.uid, groupName, function(err, isOwner) {
if (err) {
winston.warn('[groups.get] Could not determine ownership in group `' + groupName + '` for uid `' + userObj.uid + '`: ' + err.message);
return next(null, userObj);
}
userObj.isOwner = isOwner;
next(null, userObj);
});
}, next);
}
], next);
} else {
next(err, uids);
}
});
},
pending: function (next) {
db.getSetMembers('group:' + groupName + ':pending', function (err, uids) {
if (err) {
return next(err);
}
if (options.expand) {
async.map(uids, user.getUserData, next);
} else {
next(err, uids);
}
});
},
isMember: function(next) {
// Retrieve group membership state, if uid is passed in
if (!options.uid) {
return next();
}
Groups.isMember(options.uid, groupName, function(err, isMember) {
if (err) {
winston.warn('[groups.get] Could not determine membership in group `' + groupName + '` for uid `' + options.uid + '`: ' + err.message);
return next();
}
next(null, isMember);
});
},
isPending: function(next) {
// Retrieve group membership state, if uid is passed in
if (!options.uid) {
return next();
}
db.isSetMember('group:' + groupName + ':pending', options.uid, next);
},
isOwner: function(next) {
// Retrieve group ownership state, if uid is passed in
if (!options.uid) {
return next();
}
Groups.ownership.isOwner(options.uid, groupName, function(err, isOwner) {
if (err) {
winston.warn('[groups.get] Could not determine ownership in group `' + groupName + '` for uid `' + options.uid + '`: ' + err.message);
return next();
}
next(null, isOwner);
});
}
}, function (err, results) {
if (err || !results.base) {
return callback(err);
}
results.base.members = results.users.filter(function(user) {
return typeof user !== 'undefined';
});
// Default image
if (!results.base['cover:url']) {
results.base['cover:url'] = nconf.get('relative_path') + '/images/cover-default.png';
results.base['cover:position'] = '50% 50%';
}
results.base.members = results.users.filter(Boolean);
results.base.pending = results.pending.filter(Boolean);
results.base.count = numUsers || results.base.members.length;
results.base.memberCount = numUsers || results.base.members.length;
results.base.deleted = !!parseInt(results.base.deleted, 10);
results.base.hidden = !!parseInt(results.base.hidden, 10);
results.base.system = !!parseInt(results.base.system, 10);
results.base.private = results.base.private ? !!parseInt(results.base.private, 10) : true;
results.base.deletable = !results.base.system;
results.base.truncated = truncated;
results.base.isMember = results.isMember;
results.base.isPending = results.isPending;
results.base.isOwner = results.isOwner;
callback(err, results.base);
});
};
Groups.getGroupFields = function(groupName, fields, callback) {
db.getObjectFields('group:' + groupName, fields, callback);
};
Groups.setGroupField = function(groupName, field, value, callback) {
plugins.fireHook('action:group.set', {field: field, value: value, type: 'set'});
db.setObjectField('group:' + groupName, field, value, callback);
};
Groups.isPrivate = function(groupName, callback) {
db.getObjectField('group:' + groupName, 'private', function(err, isPrivate) {
isPrivate = isPrivate || isPrivate === null;
if (typeof isPrivate === 'string') {
isPrivate = (isPrivate === '0' ? false : true);
}
callback(err, isPrivate); // Private, if not set at all
});
};
Groups.getMembers = function(groupName, callback) {
db.getSetMembers('group:' + groupName + ':members', callback);
};
@ -283,16 +391,16 @@ var async = require('async'),
}
};
Groups.create = function(name, description, callback) {
if (name.length === 0) {
Groups.create = function(data, callback) {
if (data.name.length === 0) {
return callback(new Error('[[error:group-name-too-short]]'));
}
if (name === 'administrators' || name === 'registered-users') {
if (data.name === 'administrators' || data.name === 'registered-users') {
var system = true;
}
meta.userOrGroupExists(name, function (err, exists) {
meta.userOrGroupExists(data.name, function (err, exists) {
if (err) {
return callback(err);
}
@ -302,24 +410,25 @@ var async = require('async'),
}
var groupData = {
name: name,
userTitle: name,
description: description,
deleted: '0',
hidden: '0',
system: system ? '1' : '0'
};
async.parallel([
function(next) {
db.setAdd('groups', name, next);
name: data.name,
userTitle: data.name,
description: data.description || '',
deleted: '0',
hidden: '0',
system: system ? '1' : '0',
'private': data.private || '1'
},
function(next) {
db.setObject('group:' + name, groupData, function(err) {
Groups.get(name, {}, next);
});
}
], callback);
tasks = [
async.apply(db.setAdd, 'groups', data.name),
async.apply(db.setObject, 'group:' + data.name, groupData)
];
if (data.hasOwnProperty('ownerUid')) {
tasks.push(async.apply(db.setAdd, 'group:' + data.name + ':owners', data.ownerUid));
tasks.push(async.apply(db.setAdd, 'group:' + data.name + ':members', data.ownerUid));
}
async.parallel(tasks, callback);
});
};
@ -340,7 +449,8 @@ var async = require('async'),
description: values.description || '',
icon: values.icon || '',
labelColor: values.labelColor || '#000000',
hidden: values.hidden || '0'
hidden: values.hidden || '0',
'private': values.private === false ? '0' : '1'
}, function(err) {
if (err) {
return callback(err);
@ -425,15 +535,11 @@ var async = require('async'),
Groups.destroy = function(groupName, callback) {
async.parallel([
function(next) {
db.delete('group:' + groupName, next);
},
function(next) {
db.setRemove('groups', groupName, next);
},
function(next) {
db.delete('group:' + groupName + ':members', next);
},
async.apply(db.delete, 'group:' + groupName),
async.apply(db.setRemove, 'groups', groupName),
async.apply(db.delete, 'group:' + groupName + ':members'),
async.apply(db.delete, 'group:' + groupName + ':pending'),
async.apply(db.delete, 'group:' + groupName + ':owners'),
function(next) {
db.getSetMembers('groups', function(err, groups) {
if (err) {
@ -458,7 +564,10 @@ var async = require('async'),
uid: uid
});
} else {
Groups.create(groupName, '', function(err) {
Groups.create({
name: groupName,
description: ''
}, function(err) {
if (err && err.message !== '[[error:group-already-exists]]') {
winston.error('[groups.join] Could not create new hidden group: ' + err.message);
return callback(err);
@ -475,6 +584,25 @@ var async = require('async'),
});
};
Groups.requestMembership = function(groupName, uid, callback) {
db.setAdd('group:' + groupName + ':pending', uid, callback);
plugins.fireHook('action:groups.requestMembership', {
groupName: groupName,
uid: uid
});
};
Groups.acceptMembership = function(groupName, uid, callback) {
// Note: For simplicity, this method intentially doesn't check the caller uid for ownership!
db.setRemove('group:' + groupName + ':pending', uid, callback);
Groups.join.apply(Groups, arguments);
};
Groups.rejectMembership = function(groupName, uid, callback) {
// Note: For simplicity, this method intentially doesn't check the caller uid for ownership!
db.setRemove('group:' + groupName + ':pending', uid, callback);
};
Groups.leave = function(groupName, uid, callback) {
callback = callback || function() {};
@ -585,4 +713,88 @@ var async = require('async'),
});
});
};
Groups.updateCoverPosition = function(groupName, position, callback) {
Groups.setGroupField(groupName, 'cover:position', position, callback);
};
Groups.updateCover = function(data, callback) {
var tempPath, md5sum, url;
// Position only? That's fine
if (!data.imageData && data.position) {
return Groups.updateCoverPosition(data.groupName, data.position, callback);
}
async.series([
function(next) {
// Calculate md5sum of image
// This is required because user data can be private
md5sum = crypto.createHash('md5');
md5sum.update(data.imageData);
md5sum = md5sum.digest('hex');
next();
},
function(next) {
// Save image
tempPath = path.join(nconf.get('base_dir'), nconf.get('upload_path'), md5sum);
var buffer = new Buffer(data.imageData.slice(data.imageData.indexOf('base64') + 7), 'base64');
fs.writeFile(tempPath, buffer, {
encoding: 'base64'
}, next);
},
function(next) {
uploadsController.uploadGroupCover({
path: tempPath
}, function(err, uploadData) {
if (err) {
return next(err);
}
url = uploadData.url;
next();
});
},
function(next) {
Groups.setGroupField(data.groupName, 'cover:url', url, next);
},
function(next) {
fs.unlink(tempPath, next); // Delete temporary file
}
], function(err) {
if (err) {
return callback(err);
}
Groups.updateCoverPosition(data.groupName, data.position, callback);
});
}
Groups.ownership = {};
Groups.ownership.isOwner = function(uid, groupName, callback) {
// Note: All admins are also owners
async.waterfall([
async.apply(db.isSetMember, 'group:' + groupName + ':owners', uid),
function(isOwner, next) {
if (isOwner) {
return next(null, isOwner);
}
user.isAdministrator(uid, next);
}
], callback);
};
Groups.ownership.grant = function(toUid, groupName, callback) {
// Note: No ownership checking is done here on purpose!
db.setAdd('group:' + groupName + ':owners', toUid, callback);
};
Groups.ownership.rescind = function(toUid, groupName, callback) {
// Note: No ownership checking is done here on purpose!
db.setRemove('group:' + groupName + ':owners', toUid, callback);
};
}(module.exports));

@ -49,9 +49,11 @@ module.exports = function(Meta) {
source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui-1.10.4.custom.min.css";';
source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";';
source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";';
acpSource = '\n@import "..' + path.sep + 'public/less/admin/admin";\n' + source;
acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";';
source = '@import "./theme";\n' + source;
async.parallel([

@ -8,13 +8,11 @@ Groups.create = function(socket, data, callback) {
return callback(new Error('[[error:invalid-data]]'));
}
groups.create(data.name, data.description, function(err, groupObj) {
callback(err, groupObj || undefined);
});
};
Groups.delete = function(socket, groupName, callback) {
groups.destroy(groupName, callback);
groups.create({
name: data.name,
description: data.description,
ownerUid: socket.uid
}, callback);
};
Groups.get = function(socket, groupName, callback) {
@ -39,6 +37,7 @@ Groups.leave = function(socket, data, callback) {
groups.leave(data.groupName, data.uid, callback);
};
// Possibly remove this and call SocketGroups.update instead
Groups.update = function(socket, data, callback) {
if(!data) {
return callback(new Error('[[error:invalid-data]]'));

@ -0,0 +1,151 @@
"use strict";
var groups = require('../groups'),
meta = require('../meta'),
SocketGroups = {};
SocketGroups.join = function(socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
if (meta.config.allowPrivateGroups !== '0') {
groups.isPrivate(data.groupName, function(err, isPrivate) {
if (isPrivate) {
groups.requestMembership(data.groupName, socket.uid, callback);
} else {
groups.join(data.groupName, socket.uid, callback);
}
});
} else {
groups.join(data.groupName, socket.uid, callback);
}
};
SocketGroups.leave = function(socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
groups.leave(data.groupName, socket.uid, callback);
};
SocketGroups.grant = function(socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) {
if (!isOwner) {
return callback(new Error('[[error:no-privileges]]'));
}
groups.ownership.grant(data.toUid, data.groupName, callback);
});
};
SocketGroups.rescind = function(socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) {
if (!isOwner) {
return callback(new Error('[[error:no-privileges]]'));
}
groups.ownership.rescind(data.toUid, data.groupName, callback);
});
};
SocketGroups.accept = function(socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) {
if (!isOwner) {
return callback(new Error('[[error:no-privileges]]'));
}
groups.acceptMembership(data.groupName, data.toUid, callback);
});
};
SocketGroups.reject = function(socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) {
if (!isOwner) {
return callback(new Error('[[error:no-privileges]]'));
}
groups.rejectMembership(data.groupName, data.toUid, callback);
});
};
SocketGroups.update = function(socket, data, callback) {
if(!data) {
return callback(new Error('[[error:invalid-data]]'));
}
groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) {
if (!isOwner) {
return callback(new Error('[[error:no-privileges]]'));
}
groups.update(data.groupName, data.values, callback);
});
};
SocketGroups.create = function(socket, data, callback) {
if(!data) {
return callback(new Error('[[error:invalid-data]]'));
} else if (socket.uid === 0) {
return callback(new Error('[[error:no-privileges]]'));
}
data.ownerUid = socket.uid;
groups.create(data, callback);
};
SocketGroups.delete = function(socket, data, callback) {
if(!data) {
return callback(new Error('[[error:invalid-data]]'));
}
groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) {
if (!isOwner) {
return callback(new Error('[[error:no-privileges]]'));
}
groups.destroy(data.groupName, callback);
});
};
SocketGroups.cover = {};
SocketGroups.cover.get = function(socket, data, callback) {
groups.getGroupFields(data.groupName, ['cover:url', 'cover:position'], callback);
};
SocketGroups.cover.update = function(socket, data, callback) {
if(!data) {
return callback(new Error('[[error:invalid-data]]'));
} else if (socket.uid === 0) {
return callback(new Error('[[error:no-privileges]]'));
}
groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) {
if (!isOwner) {
return callback(new Error('[[error:no-privileges]]'));
}
groups.updateCover(data, callback);
});
};
module.exports = SocketGroups;

@ -10,7 +10,7 @@
<!-- END templates -->
</ul>
<div class="tab-content">
<div class="tab-content col-xs-12">
<!-- BEGIN templates -->
<div class="tab-pane <!-- IF @first -->active<!-- ENDIF @first -->" data-template="{templates.template}">
<!-- BEGIN areas -->

@ -5,7 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{relative_path}/vendor/jquery/css/smoothness/jquery-ui-1.10.4.custom.min.css?{cache-buster}">
<link rel="stylesheet" type="text/css" href="{relative_path}/vendor/colorpicker/colorpicker.css?{cache-buster}">
<link rel="stylesheet" type="text/css" href="{relative_path}/vendor/nanoscroller/nanoscroller.css?{cache-buster}">
<link rel="stylesheet" type="text/css" href="{relative_path}/admin.css?{cache-buster}" />

@ -149,7 +149,7 @@
<div class="icon-container">
<div class="row fa-icons">
<i class="fa fa-doesnt-exist"></i>
<!-- IMPORT admin/partials/fontawesome.tpl -->
<!-- IMPORT partials/fontawesome.tpl -->
</div>
</div>
</div>

@ -136,7 +136,7 @@
<div class="icon-container">
<div class="row fa-icons">
<i class="fa fa-doesnt-exist"></i>
<!-- IMPORT admin/partials/fontawesome.tpl -->
<!-- IMPORT partials/fontawesome.tpl -->
</div>
</div>
</div>

File diff suppressed because one or more lines are too long

@ -23,6 +23,7 @@
<li><a href="{relative_path}/admin/settings/reputation">Reputation</a></li>
<li><a href="{relative_path}/admin/settings/email">Email</a></li>
<li><a href="{relative_path}/admin/settings/user">User</a></li>
<li><a href="{relative_path}/admin/settings/group">Group</a></li>
<li><a href="{relative_path}/admin/settings/guest">Guest</a></li>
<li><a href="{relative_path}/admin/settings/post">Post</a></li>
<li><a href="{relative_path}/admin/settings/pagination">Pagination</a></li>

@ -0,0 +1,22 @@
<!-- IMPORT admin/settings/header.tpl -->
<div class="panel panel-default">
<div class="panel-heading">General</div>
<div class="panel-body">
<form role="form">
<div class="checkbox">
<label>
<input type="checkbox" data-field="allowPrivateGroups" checked> <strong>Private Groups</strong>
<p class="help-block">
If enabled, joining of groups requires the approval of the group owner <em>(Default: enabled)</em>
</p>
<p class="help-block">
<strong>Beware!</strong> If this option is disabled and you have private groups, they automatically become public.
</p>
</label>
</div>
</form>
</div>
</div>
<!-- IMPORT admin/settings/footer.tpl -->
Loading…
Cancel
Save