feat: allow custom fields in user csv export, closes #12401

This commit is contained in:
Barış Soner Uşaklı
2024-03-11 11:29:05 -04:00
parent bb29cafcf6
commit 83ca23ca37
4 changed files with 97 additions and 31 deletions

View File

@@ -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",

View File

@@ -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;
});

View File

@@ -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');
}

View File

@@ -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,