mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 11:05:54 +01:00 
			
		
		
		
	user search changes
This commit is contained in:
		| @@ -2,6 +2,8 @@ | ||||
| 	"banned": "Banned", | ||||
| 	"offline": "Offline", | ||||
| 	"username": "User Name", | ||||
| 	"joindate": "Join Date", | ||||
| 	"postcount": "Post Count", | ||||
|  | ||||
| 	"email": "Email", | ||||
| 	"confirm_email": "Confirm Email", | ||||
|   | ||||
| @@ -5,5 +5,8 @@ | ||||
| 	"search": "Search", | ||||
| 	"enter_username": "Enter a username to search", | ||||
| 	"load_more": "Load More", | ||||
| 	"users-found-search-took": "%1 user(s) found! Search took %2 ms." | ||||
| 	"users-found-search-took": "%1 user(s) found! Search took %2 ms.", | ||||
| 	"filter-by": "Filter By", | ||||
| 	"online-only": "Online only", | ||||
| 	"picture-only": "Picture only" | ||||
| } | ||||
| @@ -150,7 +150,7 @@ define('admin/manage/groups', [ | ||||
| 				var searchText = groupDetailsSearch.val(), | ||||
| 					foundUser; | ||||
|  | ||||
| 				socket.emit('admin.user.search', {type: 'username', query:searchText}, function(err, results) { | ||||
| 				socket.emit('admin.user.search', {query: searchText}, function(err, results) { | ||||
| 					if (!err && results && results.users.length > 0) { | ||||
| 						var numResults = results.users.length, x; | ||||
| 						if (numResults > 4) { | ||||
|   | ||||
| @@ -237,7 +237,7 @@ define('admin/manage/users', ['admin/modules/selectable'], function(selectable) | ||||
| 			timeoutId = setTimeout(function() { | ||||
| 				$('.fa-spinner').removeClass('hidden'); | ||||
|  | ||||
| 				socket.emit('admin.user.search', {type: type, query: $this.val()}, function(err, data) { | ||||
| 				socket.emit('admin.user.search', {searchBy: [type], query: $this.val()}, function(err, data) { | ||||
| 					if (err) { | ||||
| 						return app.alertError(err.message); | ||||
| 					} | ||||
|   | ||||
| @@ -86,36 +86,63 @@ define('forum/users', function() { | ||||
|  | ||||
| 	function handleSearch() { | ||||
| 		var timeoutId = 0; | ||||
| 		var lastSearch = null; | ||||
|  | ||||
| 		$('#search-user').on('keyup', function() { | ||||
| 			if (timeoutId !== 0) { | ||||
| 			if (timeoutId) { | ||||
| 				clearTimeout(timeoutId); | ||||
| 				timeoutId = 0; | ||||
| 			} | ||||
|  | ||||
| 			timeoutId = setTimeout(function() { | ||||
| 			timeoutId = setTimeout(doSearch, 250); | ||||
| 		}); | ||||
|  | ||||
| 		$('.search select, .search .checkbox input').on('change', function() { | ||||
| 			console.log('doing search'); | ||||
| 			doSearch(); | ||||
| 		}); | ||||
|  | ||||
| 		$('.pagination').on('click', 'a', function() { | ||||
| 			console.log('loading page', $(this).attr('data-page')); | ||||
| 			doSearch($(this).attr('data-page')); | ||||
| 			return false; | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	function doSearch(page) { | ||||
| 		function reset() { | ||||
| 			notify.html('<i class="fa fa-search"></i>'); | ||||
| 			notify.parent().removeClass('btn-warning label-warning btn-success label-success'); | ||||
| 		} | ||||
|  | ||||
| 		var username = $('#search-user').val(); | ||||
| 		var notify = $('#user-notfound-notify'); | ||||
|  | ||||
| 				if (username === '') { | ||||
| 		page = page || 1; | ||||
| 		if (!username) { | ||||
| 			notify.html('<i class="fa fa-circle-o"></i>'); | ||||
| 			notify.parent().removeClass('btn-warning label-warning btn-success label-success'); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 				if (lastSearch === username) { | ||||
| 					return; | ||||
| 				} | ||||
| 				lastSearch = username; | ||||
|  | ||||
| 		notify.html('<i class="fa fa-spinner fa-spin"></i>'); | ||||
| 		var filters = []; | ||||
| 		$('.user-filter').each(function() { | ||||
| 			var $this = $(this); | ||||
| 			if($this.is(':checked')) { | ||||
| 				filters.push({ | ||||
| 					field:$this.attr('data-filter-field'), | ||||
| 					type: $this.attr('data-filter-type'), | ||||
| 					value: $this.attr('data-filter-value') | ||||
| 				}); | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 				socket.emit('user.search', {query: username, by: $('.search select').val()}, function(err, data) { | ||||
| 		socket.emit('user.search', { | ||||
| 			query: username, | ||||
| 			page: page, | ||||
| 			searchBy: ['username', 'fullname'], | ||||
| 			sortBy: $('.search select').val(), | ||||
| 			filterBy: filters | ||||
| 		}, function(err, data) { | ||||
| 			if (err) { | ||||
| 				reset(); | ||||
| 				return app.alertError(err.message); | ||||
| @@ -125,6 +152,10 @@ define('forum/users', function() { | ||||
| 				return reset(); | ||||
| 			} | ||||
|  | ||||
| 			templates.parse('users', 'pages', data, function(html) { | ||||
| 				$('.pagination').html(html); | ||||
| 			}); | ||||
|  | ||||
| 			templates.parse('users', 'users', data, function(html) { | ||||
| 				translator.translate(html, function(translated) { | ||||
| 					$('#users-container').html(translated); | ||||
| @@ -132,20 +163,17 @@ define('forum/users', function() { | ||||
| 					if (!data.users.length) { | ||||
| 						translator.translate('[[error:no-user]]', function(translated) { | ||||
| 							notify.html(translated); | ||||
| 									notify.parent().addClass('btn-warning label-warning'); | ||||
| 							notify.parent().removeClass('btn-success label-success').addClass('btn-warning label-warning'); | ||||
| 						}); | ||||
| 					} else { | ||||
| 								translator.translate('[[users:users-found-search-took, ' + data.users.length + ', ' + data.timing + ']]', function(translated) { | ||||
| 						translator.translate('[[users:users-found-search-took, ' + data.matchCount + ', ' + data.timing + ']]', function(translated) { | ||||
| 							notify.html(translated); | ||||
| 									notify.parent().addClass('btn-success label-success'); | ||||
| 							notify.parent().removeClass('btn-warning label-warning').addClass('btn-success label-success'); | ||||
| 						}); | ||||
| 					} | ||||
| 				}); | ||||
| 			}); | ||||
| 		}); | ||||
|  | ||||
| 			}, 250); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	function onUserStatusChange(data) { | ||||
|   | ||||
| @@ -4,6 +4,7 @@ var usersController = {}; | ||||
|  | ||||
| var async = require('async'), | ||||
| 	user = require('../user'), | ||||
| 	meta = require('../meta'), | ||||
| 	db = require('../database'); | ||||
|  | ||||
| usersController.getOnlineUsers = function(req, res, next) { | ||||
| @@ -59,25 +60,14 @@ usersController.getUsersSortedByJoinDate = function(req, res, next) { | ||||
| }; | ||||
|  | ||||
| function getUsers(set, res, next) { | ||||
| 	async.parallel({ | ||||
| 		users: function(next) { | ||||
| 			user.getUsersFromSet(set, 0, 49, next); | ||||
| 		}, | ||||
| 		count: function(next) { | ||||
| 			db.getObjectField('global', 'userCount', next); | ||||
| 		} | ||||
| 	}, function(err, results) { | ||||
| 	getUsersAndCount(set, 50, function(err, data) { | ||||
| 		if (err) { | ||||
| 			return next(err); | ||||
| 		} | ||||
| 		results.users = results.users.filter(function(user) { | ||||
| 			return user && parseInt(user.uid, 10); | ||||
| 		}); | ||||
|  | ||||
| 		var userData = { | ||||
| 			search_display: 'hidden', | ||||
| 			loadmore_display: results.count > 50 ? 'block' : 'hide', | ||||
| 			users: results.users, | ||||
| 			loadmore_display: data.count > 50 ? 'block' : 'hide', | ||||
| 			users: data.users, | ||||
| 			show_anon: 'hide' | ||||
| 		}; | ||||
|  | ||||
| @@ -85,15 +75,42 @@ function getUsers(set, res, next) { | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| function getUsersAndCount(set, count, callback) { | ||||
| 	async.parallel({ | ||||
| 		users: function(next) { | ||||
| 			user.getUsersFromSet(set, 0, count - 1, next); | ||||
| 		}, | ||||
| 		count: function(next) { | ||||
| 			db.getObjectField('global', 'userCount', next); | ||||
| 		} | ||||
| 	}, function(err, results) { | ||||
| 		if (err) { | ||||
| 			return callback(err); | ||||
| 		} | ||||
| 		results.users = results.users.filter(function(user) { | ||||
| 			return user && parseInt(user.uid, 10); | ||||
| 		}); | ||||
|  | ||||
| 		callback(null, {users: results.users, count: results.count}); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| usersController.getUsersForSearch = function(req, res, next) { | ||||
| 	var data = { | ||||
| 	var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 20; | ||||
| 	getUsersAndCount('users:joindate', resultsPerPage, function(err, data) { | ||||
| 		if (err) { | ||||
| 			return next(err); | ||||
| 		} | ||||
|  | ||||
| 		var result = { | ||||
| 			search_display: 'block', | ||||
| 			loadmore_display: 'hidden', | ||||
| 		users: [], | ||||
| 			users: data.users, | ||||
| 			show_anon: 'hide' | ||||
| 		}; | ||||
|  | ||||
| 	res.render('users', data); | ||||
| 		res.render('users', result); | ||||
| 	}); | ||||
| }; | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -32,7 +32,7 @@ module.exports = function(Posts) { | ||||
| 			var userData = results.userData; | ||||
| 			for(var i=0; i<userData.length; ++i) { | ||||
| 				userData[i].groups = results.groups[i]; | ||||
| 				userData[i].status = results.online[i] ? (userData[i].status || 'online') : 'offline'; | ||||
| 				userData[i].status = user.getStatus(userData[i].status, results.online[i]); | ||||
| 			} | ||||
|  | ||||
| 			async.map(userData, function(userData, next) { | ||||
|   | ||||
| @@ -175,7 +175,7 @@ User.deleteUsers = function(socket, uids, callback) { | ||||
| }; | ||||
|  | ||||
| User.search = function(socket, data, callback) { | ||||
| 	user.search({query: data.query, by: data.type, startsWith: false}, function(err, searchData) { | ||||
| 	user.search({query: data.query, searchBy: data.searchBy, startsWith: false}, function(err, searchData) { | ||||
| 		if (err) { | ||||
| 			return callback(err); | ||||
| 		} | ||||
|   | ||||
| @@ -54,14 +54,6 @@ SocketUser.emailConfirm = function(socket, data, callback) { | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| SocketUser.increaseViewCount = function(socket, uid, callback) { | ||||
| 	if (uid) { | ||||
| 		if (socket.uid !== parseInt(uid, 10)) { | ||||
| 			user.incrementUserFieldBy(uid, 'profileviews', 1, callback); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| SocketUser.search = function(socket, data, callback) { | ||||
| 	if (!data) { | ||||
| 		return callback(new Error('[[error:invalid-data]]')) | ||||
| @@ -69,7 +61,13 @@ SocketUser.search = function(socket, data, callback) { | ||||
| 	if (!socket.uid) { | ||||
| 		return callback(new Error('[[error:not-logged-in]]')); | ||||
| 	} | ||||
| 	user.search({query: data.query, by: data.by}, callback); | ||||
| 	user.search({ | ||||
| 		query: data.query, | ||||
| 		page: data.page, | ||||
| 		searchBy: data.searchBy, | ||||
| 		sortBy: data.sortBy, | ||||
| 		filterBy: data.filterBy | ||||
| 	}, callback); | ||||
| }; | ||||
|  | ||||
| // Password Reset | ||||
|   | ||||
| @@ -83,7 +83,6 @@ var	async = require('async'), | ||||
| 	}; | ||||
|  | ||||
| 	User.getUsersData = function(uids, callback) { | ||||
|  | ||||
| 		if (!Array.isArray(uids) || !uids.length) { | ||||
| 			return callback(null, []); | ||||
| 		} | ||||
| @@ -237,8 +236,7 @@ var	async = require('async'), | ||||
| 				if (!user) { | ||||
| 					return; | ||||
| 				} | ||||
| 				user.status = !user.status ? 'online' : user.status; | ||||
| 				user.status = !results.isOnline[index] ? 'offline' : user.status; | ||||
| 				user.status = User.getStatus(user.status, results.isOnline[index]); | ||||
| 				user.administrator = results.isAdmin[index]; | ||||
| 				user.banned = parseInt(user.banned, 10) === 1; | ||||
| 				user['email:confirmed'] = parseInt(user['email:confirmed'], 10) === 1; | ||||
| @@ -248,6 +246,10 @@ var	async = require('async'), | ||||
| 		}); | ||||
| 	}; | ||||
|  | ||||
| 	User.getStatus = function(status, isOnline) { | ||||
| 		return isOnline ? (status || 'online') : 'offline'; | ||||
| 	}; | ||||
|  | ||||
| 	User.createGravatarURLFromEmail = function(email) { | ||||
| 		var customGravatarDefaultImage = meta.config.customGravatarDefaultImage; | ||||
| 		if (customGravatarDefaultImage && customGravatarDefaultImage.indexOf('http') === -1) { | ||||
|   | ||||
| @@ -2,65 +2,198 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| var async = require('async'), | ||||
| 	meta = require('../meta'), | ||||
| 	user = require('../user'), | ||||
| 	db = require('../database'); | ||||
|  | ||||
| module.exports = function(User) { | ||||
|  | ||||
| 	User.search = function(data, callback) { | ||||
| 		var query = data.query; | ||||
| 		var by = data.by || 'username'; | ||||
| 		var searchBy = data.searchBy || ['username']; | ||||
| 		var startsWith = data.hasOwnProperty('startsWith') ? data.startsWith : true; | ||||
| 		var page = data.page || 1; | ||||
|  | ||||
| 		if (!query || query.length === 0) { | ||||
| 			return callback(null, {timing:0, users:[]}); | ||||
| 		if (!query) { | ||||
| 			return callback(null, {timing: 0, users: [], matchCount: 0, pages: []}); | ||||
| 		} | ||||
|  | ||||
| 		if (by === 'ip') { | ||||
| 		if (searchBy === 'ip') { | ||||
| 			return searchByIP(query, callback); | ||||
| 		} | ||||
|  | ||||
| 		var start = process.hrtime(); | ||||
| 		var key = by + ':uid'; | ||||
| 		var startTime = process.hrtime(); | ||||
| 		var keys = searchBy.map(function(searchBy) { | ||||
| 			return searchBy + ':uid'; | ||||
| 		}); | ||||
|  | ||||
| 		db.getObject(key, function(err, hash) { | ||||
| 			if (err || !hash) { | ||||
| 				return callback(null, {timing: 0, users:[]}); | ||||
| 		var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 20; | ||||
| 		var start = Math.max(0, page - 1) * resultsPerPage; | ||||
| 		var end = start + resultsPerPage; | ||||
| 		var pageCount = 1; | ||||
| 		var matchCount = 0; | ||||
| 		var filterBy = Array.isArray(data.filterBy) ? data.filterBy : []; | ||||
|  | ||||
| 		async.waterfall([ | ||||
| 			function(next) { | ||||
| 				findUids(query, keys, startsWith, next); | ||||
| 			}, | ||||
| 			function(uids, next) { | ||||
| 				filterAndSortUids(uids, filterBy, data.sortBy, next); | ||||
| 			}, | ||||
| 			function(uids, next) { | ||||
| 				matchCount = uids.length; | ||||
| 				uids = uids.slice(start, end); | ||||
|  | ||||
| 				User.getUsers(uids, next); | ||||
| 			}, | ||||
| 			function(userData, next) { | ||||
|  | ||||
| 				var pages = []; | ||||
| 				if (matchCount > resultsPerPage) { | ||||
| 					pageCount = Math.ceil(matchCount / resultsPerPage); | ||||
| 					var currentPage = Math.max(1, Math.ceil((start + 1) / resultsPerPage)); | ||||
| 					for(var i=1; i<=pageCount; ++i) { | ||||
| 						pages.push({page: i, active: i === currentPage}); | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				var diff = process.hrtime(startTime); | ||||
| 				var timing = (diff[0] * 1e3 + diff[1] / 1e6).toFixed(1); | ||||
| 				next(null, { | ||||
| 					timing: timing, | ||||
| 					users: userData, | ||||
| 					matchCount: matchCount, | ||||
| 					pages: pages | ||||
| 				}); | ||||
| 			} | ||||
| 		], callback); | ||||
| 	}; | ||||
|  | ||||
| 	function findUids(query, keys, startsWith, callback) { | ||||
| 		db.getObjects(keys, function(err, hashes) { | ||||
| 			if (err || !hashes) { | ||||
| 				return callback(err, []); | ||||
| 			} | ||||
|  | ||||
| 			hashes = hashes.filter(Boolean); | ||||
|  | ||||
| 			query = query.toLowerCase(); | ||||
|  | ||||
| 			var	values = Object.keys(hash); | ||||
| 			var uids = []; | ||||
| 			var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 20; | ||||
| 			var hardCap = resultsPerPage * 10; | ||||
|  | ||||
| 			for(var i=0; i<values.length; ++i) { | ||||
| 				if (startsWith) { | ||||
| 					if (values[i].toLowerCase().indexOf(query) === 0) { | ||||
| 						uids.push(values[i]); | ||||
| 			for(var i=0; i<hashes.length; ++i) { | ||||
| 				for(var field in hashes[i]) { | ||||
| 					if ((startsWith && field.toLowerCase().startsWith(query)) || (!startsWith && field.toLowerCase().indexOf(query) !== -1)) { | ||||
| 						uids.push(hashes[i][field]); | ||||
| 						if (uids.length >= hardCap) { | ||||
| 							break; | ||||
| 						} | ||||
| 				} else if (values[i].toLowerCase().indexOf(query) !== -1) { | ||||
| 					uids.push(values[i]); | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 			uids = uids.slice(0, 20) | ||||
| 				.sort(function(a, b) { | ||||
| 					return a > b; | ||||
| 				}) | ||||
| 				.map(function(username) { | ||||
| 					return hash[username]; | ||||
| 				if (uids.length >= hardCap) { | ||||
| 					break; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if (hashes.length > 1) { | ||||
| 				uids = uids.filter(function(uid, index, array) { | ||||
| 					return array.indexOf(uid) === index; | ||||
| 				}); | ||||
| 			} | ||||
|  | ||||
| 			callback(null, uids); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	function filterAndSortUids(uids, filterBy, sortBy, callback) { | ||||
| 		sortBy = sortBy || 'joindate'; | ||||
|  | ||||
| 		var fields = filterBy.map(function(filter) { | ||||
| 			return filter.field; | ||||
| 		}).concat(['uid', sortBy]).filter(function(field, index, array) { | ||||
| 			return array.indexOf(field) === index; | ||||
| 		}); | ||||
|  | ||||
| 			User.getUsers(uids, function(err, userdata) { | ||||
| 		async.parallel({ | ||||
| 			userData: function(next) { | ||||
| 				user.getMultipleUserFields(uids, fields, next); | ||||
| 			}, | ||||
| 			isOnline: function(next) { | ||||
| 				if (fields.indexOf('status') !== -1) { | ||||
| 					require('../socket.io').isUsersOnline(uids, next); | ||||
| 				} else { | ||||
| 					next(); | ||||
| 				} | ||||
| 			} | ||||
| 		}, function(err, results) { | ||||
| 			if (err) { | ||||
| 				return callback(err); | ||||
| 			} | ||||
| 			var userData = results.userData; | ||||
|  | ||||
| 				var diff = process.hrtime(start); | ||||
| 				var timing = (diff[0] * 1e3 + diff[1] / 1e6).toFixed(1); | ||||
| 				callback(null, {timing: timing, users: userdata}); | ||||
| 			if (results.isOnline) { | ||||
| 				userData.forEach(function(userData, index) { | ||||
| 					userData.status = user.getStatus(userData.status, results.isOnline[index]); | ||||
| 				}); | ||||
| 			} | ||||
|  | ||||
| 			userData = filterUsers(userData, filterBy); | ||||
|  | ||||
| 			sortUsers(userData, sortBy); | ||||
|  | ||||
| 			uids = userData.map(function(user) { | ||||
| 				return user && user.uid; | ||||
| 			}); | ||||
| 	}; | ||||
| 			callback(null, uids); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	function filterUsers(userData, filterBy) { | ||||
| 		function passesFilter(user, filter) { | ||||
| 			if (!user || !filter) { | ||||
| 				return false; | ||||
| 			} | ||||
| 			var userValue = user[filter.field]; | ||||
| 			if (filter.type === '=') { | ||||
| 				return userValue === filter.value; | ||||
| 			} else if (filter.type === '!=') { | ||||
| 				return userValue !== filter.value; | ||||
| 			} | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		if (!filterBy.length) { | ||||
| 			return userData; | ||||
| 		} | ||||
|  | ||||
| 		return userData.filter(function(user) { | ||||
| 			for(var i=0; i<filterBy.length; ++i) { | ||||
| 				if (!passesFilter(user, filterBy[i])) { | ||||
| 					return false; | ||||
| 				} | ||||
| 			} | ||||
| 			return true; | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	function sortUsers(userData, sortBy) { | ||||
| 		userData.sort(function(user1, user2) { | ||||
| 			if (sortBy === 'joindate' || sortBy === 'postcount') { | ||||
| 				return user2[sortBy] - user1[sortBy]; | ||||
| 			} else { | ||||
| 				if(user1[sortBy] < user2[sortBy]) { | ||||
| 					return -1; | ||||
| 				} else if(user1[sortBy] > user2[sortBy]) { | ||||
| 					return 1; | ||||
| 				} | ||||
| 				return 0; | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	function searchByIP(ip, callback) { | ||||
| 		var start = process.hrtime(); | ||||
|   | ||||
| @@ -141,4 +141,17 @@ | ||||
| 	</div> | ||||
| </div> | ||||
|  | ||||
| <div class="panel panel-default"> | ||||
| 	<div class="panel-heading">User Search</div> | ||||
| 	<div class="panel-body"> | ||||
| 		<form> | ||||
| 			<div class="form-group"> | ||||
| 				<label>Number of results to display</label> | ||||
| 				<input type="text" class="form-control" value="24" data-field="userSearchResultsPerPage"> | ||||
| 			</div> | ||||
|  | ||||
| 		</form> | ||||
| 	</div> | ||||
| </div> | ||||
|  | ||||
| <!-- IMPORT admin/settings/footer.tpl --> | ||||
		Reference in New Issue
	
	Block a user