Import users from CSV

Change-Id: Id05363ecc39aee4fdc4ac6afbcb039558b2a17ed
This commit is contained in:
Timo Paulssen 2020-06-15 02:42:59 +02:00 committed by Michael Albert
parent 7b5c0e2845
commit 1aaa137afe
7 changed files with 300 additions and 181 deletions

View File

@ -40,6 +40,7 @@ const App = () => (
dataProvider={dataProvider}
i18nProvider={i18nProvider}
customRoutes={[
<Route key="csvImport" path="/importcsv" component={ImportFeature} />,
<Route key="showpdf" path="/showpdf" component={ShowUserPdf} />,
]}
>

View File

@ -20,6 +20,7 @@ import {
import { useTranslate } from "ra-core";
import Container from "@material-ui/core/Container/Container";
import { generateRandomUser } from "./users";
import ShowUserPdf from "./ShowUserPdf";
const LOGGING = true;
@ -59,6 +60,8 @@ const FilePicker = props => {
const [progress, setProgress] = useState(null);
const [pdfRecords, setPdfRecords] = useState(null);
const [importResults, setImportResults] = useState(null);
const [skippedRecords, setSkippedRecords] = useState(null);
@ -66,17 +69,23 @@ const FilePicker = props => {
const [passwordMode, setPasswordMode] = useState(true);
const [useridMode, setUseridMode] = useState("ignore");
const [showingPdf, setShowingPdf] = useState(false);
const translate = useTranslate();
const notify = useNotify();
const dataProvider = useDataProvider();
const onFileChange = async e => {
if (progress !== null) return;
if (progress !== null) {
return;
}
if (LOGGING) console.log("onFileChange was called");
setValues(null);
setError(null);
setStats(null);
setPdfRecords(null);
setImportResults(null);
const file = e.target.files ? e.target.files[0] : null;
/* Let's refuse some unreasonably big files instead of freezing
@ -126,6 +135,11 @@ const FilePicker = props => {
});
if (eF.length !== 0) {
if (LOGGING) {
console.log(meta.fields);
console.log(eF);
console.log(oF);
}
setError(
translate("import_users.error.required_field", { field: eF[0] })
);
@ -226,6 +240,9 @@ const FilePicker = props => {
setProgress,
setError
);
setPdfRecords(results.recordsForPdf);
setImportResults(results);
// offer CSV download of skipped or errored records
// (so that the user doesn't have to filter out successful
@ -251,6 +268,8 @@ const FilePicker = props => {
let skippedRecords = [];
let erroredRecords = [];
let succeededRecords = [];
let recordsForPdf = [];
let changeStats = {
toAdmin: 0,
toGuest: 0,
@ -365,6 +384,14 @@ const FilePicker = props => {
await dataProvider.create("users", { data: recordData });
}
succeededRecords.push(recordData);
if (recordData.password !== undefined) {
recordsForPdf.push({
id: recordData.id,
password: recordData.password,
displayname: recordData.displayname,
});
}
}
);
};
@ -389,6 +416,7 @@ const FilePicker = props => {
erroredRecords,
succeededRecords,
totalRecordCount: entriesCount,
recordsForPdf,
changeStats,
wasDryRun: dryRun,
};
@ -618,6 +646,10 @@ const FilePicker = props => {
<br />,
]
: ""}
{translate(
"import_users.cards.results.for_print",
importResults.recordsForPdf.length
)}
<br />
{importResults.wasDryRun && [
translate("import_users.cards.results.simulated_only"),
@ -655,6 +687,27 @@ const FilePicker = props => {
</CardActions>
);
let pdfDisplay =
pdfRecords && showingPdf && pdfRecords.length ? (
<ShowUserPdf records={pdfRecords} />
) : null;
let pdfActions = pdfRecords ? (
<CardActions>
<Button
size="large"
onClick={e => {
setShowingPdf(true);
}}
>
{translate("import_users.goToPdf")}
</Button>
</CardActions>
) : null;
if (pdfRecords && showingPdf) {
return <Card>{pdfDisplay}</Card>;
} else {
let allCards = [];
if (uploadCard) allCards.push(uploadCard);
if (errorCards) allCards.push(errorCards);
@ -662,6 +715,7 @@ const FilePicker = props => {
if (statsCards) allCards.push(...statsCards);
if (startImportCard) allCards.push(startImportCard);
if (resultsCard) allCards.push(resultsCard);
if (pdfActions) allCards.push(pdfActions);
let cardContainer = <Card>{allCards}</Card>;
@ -669,6 +723,7 @@ const FilePicker = props => {
<Title defaultTitle={translate("import_users.title")} />,
cardContainer,
];
}
};
export const ImportFeature = FilePicker;

View File

@ -1,8 +1,9 @@
import React from "react";
import React, { useRef } from "react";
import { Title, Button } from "react-admin";
import { makeStyles } from "@material-ui/core/styles";
import { PDFExport } from "@progress/kendo-react-pdf";
import QRCode from "qrcode.react";
import { string, any } from "prop-types";
function xor(a, b) {
var res = "";
@ -14,15 +15,113 @@ function xor(a, b) {
function calculateQrString(serverUrl, username, password) {
const magicString = "wo9k5tep252qxsa5yde7366kugy6c01w7oeeya9hrmpf0t7ii7";
var urlString = "user=" + username + "&password=" + password;
const origUrlString = "user=" + username + "&password=" + password;
urlString = xor(urlString, magicString); // xor with magic string
var urlString = xor(origUrlString, magicString); // xor with magic string
if (origUrlString !== xor(urlString, magicString)) {
console.error(
"xoring this url string with magicString twice gave different results:",
origUrlString,
urlString,
xor(urlString, magicString)
);
}
urlString = btoa(urlString); // to base64
return serverUrl + "/#" + urlString;
}
const ShowUserPdf = props => {
UserPdfPage.propTypes = {
classes: any,
displayname: string,
qrCode: any,
serverUrl: string,
username: string,
password: string,
};
function UserPdfPage({
classes,
displayname,
qrCode,
serverUrl,
username,
password,
}) {
return (
<div className={classes.page}>
<div className={classes.header}>
<div className={classes.name}>{displayname}</div>
<img className={classes.logo} alt="Logo" src="images/logo.png" />
</div>
<div className={classes.body}>
<table>
<tbody>
<tr>
<td width="200px">
<div className={classes.code_note}>
Ihr persönlicher Anmeldecode:
</div>
</td>
<td className={classes.table_cell}>
<div className={classes.credentials_note}>
Ihre persönlichen Zugangsdaten:
</div>
</td>
</tr>
<tr>
<td>
<div className={classes.qr}>{qrCode}</div>
</td>
<td className={classes.table_cell}>
<div className={classes.credentials_text}>
<br />
<table>
<tbody>
<tr>
<td>Heimserver:</td>
<td>
<span className={classes.credentials}>
{serverUrl}
</span>
</td>
</tr>
<tr>
<td>Benutzername:</td>
<td>
<span className={classes.credentials}>
{username}
</span>
</td>
</tr>
<tr>
<td>Passwort:</td>
<td>
<span className={classes.credentials}>
{password}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
<div className={classes.note}>
Hier können Sie Ihre selbst gewählte Schlüsselsicherungs-Passphrase
notieren:
<br />
<br />
<br />
<hr />
</div>
</div>
</div>
);
}
const useStyles = makeStyles(theme => ({
page: {
height: 800,
@ -87,36 +186,33 @@ const ShowUserPdf = props => {
},
}));
const ShowUserPdf = props => {
const classes = useStyles();
var resume;
const userPdf = useRef(null);
const exportPDF = () => {
resume.save();
userPdf.current.save();
};
var qrCode = "";
var displayname = "";
var id = "";
var password = "";
var username = "";
var serverUrl = "";
let userRecords;
if (props.records) {
userRecords = props.records;
}
if (
props.location &&
props.location.state &&
props.location.state.id &&
props.location.state.password
) {
id = props.location.state.id;
password = props.location.state.password;
username = id.substring(1, id.indexOf(":"));
serverUrl = "https://" + id.substring(id.indexOf(":") + 1);
const qrString = calculateQrString(serverUrl, username, password);
qrCode = <QRCode value={qrString} size={128} />;
displayname = props.location.state.displayname;
userRecords = [
{
id: props.location.state.id,
password: props.location.state.password,
displayname: props.location.state.displayname,
},
];
}
return (
@ -126,82 +222,39 @@ const ShowUserPdf = props => {
<PDFExport
paperSize={"A4"}
fileName="User.pdf"
fileName="Users.pdf"
title=""
subject=""
keywords=""
ref={r => (resume = r)}
ref={userPdf}
//ref={r => (resume = r)}
>
<div className={classes.page}>
<div className={classes.header}>
<div className={classes.name}>{displayname}</div>
<img className={classes.logo} alt="Logo" src="images/logo.png" />
</div>
<div className={classes.body}>
<table>
<tbody>
<tr>
<td width="200px">
<div className={classes.code_note}>
Ihr persönlicher Anmeldecode:
</div>
</td>
<td className={classes.table_cell}>
<div className={classes.credentials_note}>
Ihre persönlichen Zugangsdaten:
</div>
</td>
</tr>
<tr>
<td>
<div className={classes.qr}>{qrCode}</div>
</td>
<td className={classes.table_cell}>
<div className={classes.credentials_text}>
<br />
<table>
<tbody>
<tr>
<td>Heimserver:</td>
<td>
<span className={classes.credentials}>
{serverUrl}
</span>
</td>
</tr>
<tr>
<td>Benutzername:</td>
<td>
<span className={classes.credentials}>
{username}
</span>
</td>
</tr>
<tr>
<td>Passwort:</td>
<td>
<span className={classes.credentials}>
{password}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
<div className={classes.note}>
Hier können Sie Ihre selbst gewählte
Schlüsselsicherungs-Passphrase notieren:
<br />
<br />
<br />
<hr />
</div>
</div>
</div>
{userRecords.map(record => {
if (record.id && record.password) {
const username = record.id.substring(1, record.id.indexOf(":"));
const serverUrl =
"https://" + record.id.substring(record.id.indexOf(":") + 1);
const qrString = calculateQrString(
serverUrl,
username,
record.password
);
const qrCode = <QRCode value={qrString} size={128} />;
return (
<UserPdfPage
classes={classes}
displayname={record.displayname}
qrCode={qrCode}
serverUrl={serverUrl}
username={username}
password={record.password}
/>
);
} else {
/* Skip empty PDF pages */
return null;
}
})}
</PDFExport>
</div>
);

View File

@ -12,10 +12,12 @@ import {
ArrayInput,
ArrayField,
Button,
CreateButton,
Datagrid,
DateField,
Create,
Edit,
ExportButton,
List,
Filter,
Toolbar,
@ -37,11 +39,8 @@ import {
DeleteButton,
SaveButton,
regex,
useRedirect,
useTranslate,
Pagination,
CreateButton,
ExportButton,
TopToolbar,
sanitizeListRestProps,
NumberField,
@ -50,6 +49,13 @@ import SaveQrButton from "./SaveQrButton";
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
import { DeviceRemoveButton } from "./devices";
import { makeStyles } from "@material-ui/core/styles";
import { Link } from "react-router-dom";
const redirect = (basePath, id, data) => {
return {
pathname: "/importcsv",
};
};
const useStyles = makeStyles({
small: {
@ -81,7 +87,6 @@ const UserListActions = ({
total,
...rest
}) => {
const redirectTo = useRedirect();
return (
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
{filters &&
@ -101,6 +106,10 @@ const UserListActions = ({
exporter={exporter}
maxResults={maxResults}
/>
{/* Add your custom actions */}
<Button component={Link} to={redirect} label="CSV Import">
<GetAppIcon style={{ transform: "rotate(180deg)", fontSize: "20" }} />
</Button>
</TopToolbar>
);
};
@ -178,7 +187,7 @@ export const UserList = props => {
};
// redirect to the related Author show page
const redirect = (basePath, id, data) => {
const redirectToPdf = (basePath, id, data) => {
return {
pathname: "/showpdf",
state: {
@ -193,7 +202,7 @@ const UserCreateToolbar = props => (
<Toolbar {...props}>
<SaveQrButton
label="synapseadmin.action.save_and_show"
redirect={redirect}
redirect={redirectToPdf}
submitOnEnter={true}
/>
<SaveButton

View File

@ -104,6 +104,8 @@ export default {
with_error:
"%{smart_count} Eintrag mit Fehlern ||| %{smart_count} Einträge mit Fehlern",
simulated_only: "Import-Vorgang war nur simuliert",
for_print:
"%{smart_count} Eintrag zum Drucken verfügbar |||| %{smart_count} Einträge zum Drucken verfügbar",
},
},
},

View File

@ -104,6 +104,8 @@ export default {
with_error:
"%{smart_count} entry with errors ||| %{smart_count} entries with errors",
simulated_only: "Run was only simulated",
for_print:
"%{smart_count} entry available for printing |||| %{smart_count} entries available for printing",
},
},
},

View File

@ -25,9 +25,6 @@ const mxcUrlToHttp = mxcUrl => {
return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`;
};
const powerLevelToRole = powerLevel =>
powerLevel < 100 ? (powerLevel < 50 ? "user" : "mod") : "admin";
const POWER_LEVELS = {
admin: 100,
mod: 50,