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.x-users-found": "%1 user(s) found, (%2 seconds)",
|
||||
"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-completed": "Users exported as csv, click here to download.",
|
||||
"email": "Email",
|
||||
|
||||
@@ -27,16 +27,61 @@ define('admin/manage/users', [
|
||||
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) {
|
||||
return alerts.error(err);
|
||||
}
|
||||
alerts.alert({
|
||||
alert_id: 'export-users-start',
|
||||
message: '[[admin/manage/users:export-users-started]]',
|
||||
timeout: (ajaxify.data.userCount / 5000) * 500,
|
||||
timeout: Math.max(5000, (ajaxify.data.userCount / 5000) * 500),
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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({
|
||||
type: 'exportUsersCSV',
|
||||
uid: socket.uid,
|
||||
@@ -170,7 +170,7 @@ User.exportUsersCSV = async function (socket) {
|
||||
});
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await user.exportUsersCSV();
|
||||
await user.exportUsersCSV(data.fields);
|
||||
if (socket.emit) {
|
||||
socket.emit('event:export-users-csv');
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const winston = require('winston');
|
||||
const validator = require('validator');
|
||||
const json2csvAsync = require('json2csv').parseAsync;
|
||||
|
||||
const { baseDir } = require('../constants').paths;
|
||||
const db = require('../database');
|
||||
@@ -47,41 +48,39 @@ module.exports = function (User) {
|
||||
return csvContent;
|
||||
};
|
||||
|
||||
User.exportUsersCSV = async function () {
|
||||
User.exportUsersCSV = async function (fieldsToExport = ['email', 'username', 'uid', 'ip']) {
|
||||
winston.verbose('[user/exportUsersCSV] Exporting User CSV data');
|
||||
|
||||
const { fields, showIps } = await plugins.hooks.fire('filter:user.csvFields', {
|
||||
fields: ['email', 'username', 'uid'],
|
||||
showIps: true,
|
||||
fields: fieldsToExport,
|
||||
showIps: fieldsToExport.includes('ip'),
|
||||
});
|
||||
|
||||
if (!showIps && fields.includes('ip')) {
|
||||
fields.splice(fields.indexOf('ip'), 1);
|
||||
}
|
||||
const fd = await fs.promises.open(
|
||||
path.join(baseDir, 'build/export', 'users.csv'),
|
||||
'w'
|
||||
);
|
||||
fs.promises.appendFile(fd, `${fields.join(',')}${showIps ? ',ip' : ''}\n`);
|
||||
await batch.processSortedSet('users:joindate', async (uids) => {
|
||||
const usersData = await User.getUsersFields(uids, fields.slice());
|
||||
let userIPs = '';
|
||||
let ips = [];
|
||||
|
||||
fs.promises.appendFile(fd, `${fields.map(f => `"${f}"`).join(',')}\n`);
|
||||
await batch.processSortedSet('group:administrators:members', async (uids) => {
|
||||
const userFieldsToLoad = fields.filter(field => field !== 'ip' && field !== 'password');
|
||||
const usersData = await User.getUsersFields(uids, userFieldsToLoad);
|
||||
let userIps = [];
|
||||
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) => {
|
||||
line += `${fields
|
||||
.map(field => (isFinite(user[field]) ? `'${user[field]}'` : user[field]))
|
||||
.join(',')}`;
|
||||
if (showIps) {
|
||||
userIPs = ips[index] ? ips[index].join(',') : '';
|
||||
line += `,"${userIPs}"\n`;
|
||||
} else {
|
||||
line += '\n';
|
||||
if (Array.isArray(userIps[index])) {
|
||||
user.ip = userIps[index].join(',');
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
interval: 250,
|
||||
|
||||
Reference in New Issue
Block a user