mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 16:46:12 +01:00
feat: allow custom fields in user csv export, closes #12401
This commit is contained in:
@@ -121,6 +121,28 @@
|
|||||||
"alerts.email-sent-to": "An invitation email has been sent to %1",
|
"alerts.email-sent-to": "An invitation email has been sent to %1",
|
||||||
"alerts.x-users-found": "%1 user(s) found, (%2 seconds)",
|
"alerts.x-users-found": "%1 user(s) found, (%2 seconds)",
|
||||||
"alerts.select-a-single-user-to-change-email": "Select a single user to change email",
|
"alerts.select-a-single-user-to-change-email": "Select a single user to change email",
|
||||||
|
"export": "Export",
|
||||||
|
"export-users-fields-title": "Select CSV Fields",
|
||||||
|
"export-field-email": "Email",
|
||||||
|
"export-field-username": "Username",
|
||||||
|
"export-field-uid": "UID",
|
||||||
|
"export-field-ip": "IP",
|
||||||
|
"export-field-joindate": "Join date",
|
||||||
|
"export-field-lastonline": "Last Online",
|
||||||
|
"export-field-lastposttime": "Last Post Time",
|
||||||
|
"export-field-reputation": "Reputation",
|
||||||
|
"export-field-postcount": "Post Count",
|
||||||
|
"export-field-topiccount": "Topic Count",
|
||||||
|
"export-field-profileviews": "Profile Views",
|
||||||
|
"export-field-followercount": "Follower Count",
|
||||||
|
"export-field-followingcount": "Following Count",
|
||||||
|
"export-field-fullname": "Full Name",
|
||||||
|
"export-field-website": "Website",
|
||||||
|
"export-field-location": "Location",
|
||||||
|
"export-field-birthday": "Birthday",
|
||||||
|
"export-field-signature": "Signature",
|
||||||
|
"export-field-aboutme": "About Me",
|
||||||
|
|
||||||
"export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.",
|
"export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.",
|
||||||
"export-users-completed": "Users exported as csv, click here to download.",
|
"export-users-completed": "Users exported as csv, click here to download.",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
|||||||
@@ -27,16 +27,61 @@ define('admin/manage/users', [
|
|||||||
timeout: 0,
|
timeout: 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
socket.emit('admin.user.exportUsersCSV', {}, function (err) {
|
|
||||||
|
const defaultFields = [
|
||||||
|
{ label: '[[admin/manage/users:export-field-email]]', field: 'email', selected: true },
|
||||||
|
{ label: '[[admin/manage/users:export-field-username]]', field: 'username', selected: true },
|
||||||
|
{ label: '[[admin/manage/users:export-field-uid]]', field: 'uid', selected: true },
|
||||||
|
{ label: '[[admin/manage/users:export-field-ip]]', field: 'ip', selected: true },
|
||||||
|
{ label: '[[admin/manage/users:export-field-joindate]]', field: 'joindate', selected: false },
|
||||||
|
{ label: '[[admin/manage/users:export-field-lastonline]]', field: 'lastonline', selected: false },
|
||||||
|
{ label: '[[admin/manage/users:export-field-lastposttime]]', field: 'lastposttime', selected: false },
|
||||||
|
{ label: '[[admin/manage/users:export-field-reputation]]', field: 'reputation', selected: false },
|
||||||
|
{ label: '[[admin/manage/users:export-field-postcount]]', field: 'postcount', selected: false },
|
||||||
|
{ label: '[[admin/manage/users:export-field-topiccount]]', field: 'topiccount', selected: false },
|
||||||
|
{ label: '[[admin/manage/users:export-field-profileviews]]', field: 'profileviews', selected: false },
|
||||||
|
{ label: '[[admin/manage/users:export-field-followercount]]', field: 'followerCount', selected: false },
|
||||||
|
{ label: '[[admin/manage/users:export-field-followingcount]]', field: 'followingCount', selected: false },
|
||||||
|
{ label: '[[admin/manage/users:export-field-fullname]]', field: 'fullname', selected: false },
|
||||||
|
{ label: '[[admin/manage/users:export-field-website]]', field: 'website', selected: false },
|
||||||
|
{ label: '[[admin/manage/users:export-field-location]]', field: 'location', selected: false },
|
||||||
|
{ label: '[[admin/manage/users:export-field-birthday]]', field: 'birthday', selected: false },
|
||||||
|
{ label: '[[admin/manage/users:export-field-signature]]', field: 'signature', selected: false },
|
||||||
|
{ label: '[[admin/manage/users:export-field-aboutme]]', field: 'aboutme', selected: false },
|
||||||
|
];
|
||||||
|
const options = defaultFields.map((field, i) => (`
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input data-field="${field.field}" class="form-check-input" type="checkbox" id="option-${i}" ${field.selected ? 'checked' : ''}>
|
||||||
|
<label class="form-check-label" for="option-${i}">
|
||||||
|
${field.label}
|
||||||
|
</label>
|
||||||
|
</div>`
|
||||||
|
)).join('');
|
||||||
|
|
||||||
|
const modal = bootbox.dialog({
|
||||||
|
message: options,
|
||||||
|
title: '[[admin/manage/users:export-users-fields-title]]',
|
||||||
|
buttons: {
|
||||||
|
submit: {
|
||||||
|
label: '[[admin/manage/users:export]]',
|
||||||
|
callback: function () {
|
||||||
|
const fields = modal.find('[data-field]').filter(
|
||||||
|
(index, el) => $(el).is(':checked')
|
||||||
|
).map((index, el) => $(el).attr('data-field')).get();
|
||||||
|
socket.emit('admin.user.exportUsersCSV', { fields }, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return alerts.error(err);
|
return alerts.error(err);
|
||||||
}
|
}
|
||||||
alerts.alert({
|
alerts.alert({
|
||||||
alert_id: 'export-users-start',
|
alert_id: 'export-users-start',
|
||||||
message: '[[admin/manage/users:export-users-started]]',
|
message: '[[admin/manage/users:export-users-started]]',
|
||||||
timeout: (ajaxify.data.userCount / 5000) * 500,
|
timeout: Math.max(5000, (ajaxify.data.userCount / 5000) * 500),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ User.setReputation = async function (socket, data) {
|
|||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
User.exportUsersCSV = async function (socket) {
|
User.exportUsersCSV = async function (socket, data) {
|
||||||
await events.log({
|
await events.log({
|
||||||
type: 'exportUsersCSV',
|
type: 'exportUsersCSV',
|
||||||
uid: socket.uid,
|
uid: socket.uid,
|
||||||
@@ -170,7 +170,7 @@ User.exportUsersCSV = async function (socket) {
|
|||||||
});
|
});
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
await user.exportUsersCSV();
|
await user.exportUsersCSV(data.fields);
|
||||||
if (socket.emit) {
|
if (socket.emit) {
|
||||||
socket.emit('event:export-users-csv');
|
socket.emit('event:export-users-csv');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const validator = require('validator');
|
const validator = require('validator');
|
||||||
|
const json2csvAsync = require('json2csv').parseAsync;
|
||||||
|
|
||||||
const { baseDir } = require('../constants').paths;
|
const { baseDir } = require('../constants').paths;
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
@@ -47,41 +48,39 @@ module.exports = function (User) {
|
|||||||
return csvContent;
|
return csvContent;
|
||||||
};
|
};
|
||||||
|
|
||||||
User.exportUsersCSV = async function () {
|
User.exportUsersCSV = async function (fieldsToExport = ['email', 'username', 'uid', 'ip']) {
|
||||||
winston.verbose('[user/exportUsersCSV] Exporting User CSV data');
|
winston.verbose('[user/exportUsersCSV] Exporting User CSV data');
|
||||||
|
|
||||||
const { fields, showIps } = await plugins.hooks.fire('filter:user.csvFields', {
|
const { fields, showIps } = await plugins.hooks.fire('filter:user.csvFields', {
|
||||||
fields: ['email', 'username', 'uid'],
|
fields: fieldsToExport,
|
||||||
showIps: true,
|
showIps: fieldsToExport.includes('ip'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!showIps && fields.includes('ip')) {
|
||||||
|
fields.splice(fields.indexOf('ip'), 1);
|
||||||
|
}
|
||||||
const fd = await fs.promises.open(
|
const fd = await fs.promises.open(
|
||||||
path.join(baseDir, 'build/export', 'users.csv'),
|
path.join(baseDir, 'build/export', 'users.csv'),
|
||||||
'w'
|
'w'
|
||||||
);
|
);
|
||||||
fs.promises.appendFile(fd, `${fields.join(',')}${showIps ? ',ip' : ''}\n`);
|
fs.promises.appendFile(fd, `${fields.map(f => `"${f}"`).join(',')}\n`);
|
||||||
await batch.processSortedSet('users:joindate', async (uids) => {
|
await batch.processSortedSet('group:administrators:members', async (uids) => {
|
||||||
const usersData = await User.getUsersFields(uids, fields.slice());
|
const userFieldsToLoad = fields.filter(field => field !== 'ip' && field !== 'password');
|
||||||
let userIPs = '';
|
const usersData = await User.getUsersFields(uids, userFieldsToLoad);
|
||||||
let ips = [];
|
let userIps = [];
|
||||||
|
|
||||||
if (showIps) {
|
if (showIps) {
|
||||||
ips = await db.getSortedSetsMembers(uids.map(uid => `uid:${uid}:ip`));
|
userIps = await db.getSortedSetsMembers(uids.map(uid => `uid:${uid}:ip`));
|
||||||
}
|
}
|
||||||
|
|
||||||
let line = '';
|
|
||||||
usersData.forEach((user, index) => {
|
usersData.forEach((user, index) => {
|
||||||
line += `${fields
|
if (Array.isArray(userIps[index])) {
|
||||||
.map(field => (isFinite(user[field]) ? `'${user[field]}'` : user[field]))
|
user.ip = userIps[index].join(',');
|
||||||
.join(',')}`;
|
|
||||||
if (showIps) {
|
|
||||||
userIPs = ips[index] ? ips[index].join(',') : '';
|
|
||||||
line += `,"${userIPs}"\n`;
|
|
||||||
} else {
|
|
||||||
line += '\n';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await fs.promises.appendFile(fd, line);
|
const opts = { fields, header: false };
|
||||||
|
const csv = await json2csvAsync(usersData, opts);
|
||||||
|
await fs.promises.appendFile(fd, csv);
|
||||||
}, {
|
}, {
|
||||||
batch: 5000,
|
batch: 5000,
|
||||||
interval: 250,
|
interval: 250,
|
||||||
|
|||||||
Reference in New Issue
Block a user