Skip to content

Commit

Permalink
feat: allow custom fields in user csv export, closes #12401
Browse files Browse the repository at this point in the history
  • Loading branch information
barisusakli committed Mar 11, 2024
1 parent bb29caf commit 83ca23c
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 31 deletions.
22 changes: 22 additions & 0 deletions public/language/en-GB/admin/manage/users.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
63 changes: 54 additions & 9 deletions public/src/admin/manage/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,60 @@ define('admin/manage/users', [
timeout: 0,
});
});
socket.emit('admin.user.exportUsersCSV', {}, 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,
});

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: Math.max(5000, (ajaxify.data.userCount / 5000) * 500),
});
});
},
},
},
});

return false;
Expand Down
4 changes: 2 additions & 2 deletions src/socket.io/admin/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,15 +162,15 @@ 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,
ip: socket.ip,
});
setTimeout(async () => {
try {
await user.exportUsersCSV();
await user.exportUsersCSV(data.fields);
if (socket.emit) {
socket.emit('event:export-users-csv');
}
Expand Down
39 changes: 19 additions & 20 deletions src/user/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 83ca23c

Please sign in to comment.