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} dataProvider={dataProvider}
i18nProvider={i18nProvider} i18nProvider={i18nProvider}
customRoutes={[ customRoutes={[
<Route key="csvImport" path="/importcsv" component={ImportFeature} />,
<Route key="showpdf" path="/showpdf" component={ShowUserPdf} />, <Route key="showpdf" path="/showpdf" component={ShowUserPdf} />,
]} ]}
> >

View File

@ -20,6 +20,7 @@ import {
import { useTranslate } from "ra-core"; import { useTranslate } from "ra-core";
import Container from "@material-ui/core/Container/Container"; import Container from "@material-ui/core/Container/Container";
import { generateRandomUser } from "./users"; import { generateRandomUser } from "./users";
import ShowUserPdf from "./ShowUserPdf";
const LOGGING = true; const LOGGING = true;
@ -59,6 +60,8 @@ const FilePicker = props => {
const [progress, setProgress] = useState(null); const [progress, setProgress] = useState(null);
const [pdfRecords, setPdfRecords] = useState(null);
const [importResults, setImportResults] = useState(null); const [importResults, setImportResults] = useState(null);
const [skippedRecords, setSkippedRecords] = useState(null); const [skippedRecords, setSkippedRecords] = useState(null);
@ -66,17 +69,23 @@ const FilePicker = props => {
const [passwordMode, setPasswordMode] = useState(true); const [passwordMode, setPasswordMode] = useState(true);
const [useridMode, setUseridMode] = useState("ignore"); const [useridMode, setUseridMode] = useState("ignore");
const [showingPdf, setShowingPdf] = useState(false);
const translate = useTranslate(); const translate = useTranslate();
const notify = useNotify(); const notify = useNotify();
const dataProvider = useDataProvider(); const dataProvider = useDataProvider();
const onFileChange = async e => { const onFileChange = async e => {
if (progress !== null) return; if (progress !== null) {
return;
}
if (LOGGING) console.log("onFileChange was called");
setValues(null); setValues(null);
setError(null); setError(null);
setStats(null); setStats(null);
setPdfRecords(null);
setImportResults(null); setImportResults(null);
const file = e.target.files ? e.target.files[0] : null; const file = e.target.files ? e.target.files[0] : null;
/* Let's refuse some unreasonably big files instead of freezing /* Let's refuse some unreasonably big files instead of freezing
@ -126,6 +135,11 @@ const FilePicker = props => {
}); });
if (eF.length !== 0) { if (eF.length !== 0) {
if (LOGGING) {
console.log(meta.fields);
console.log(eF);
console.log(oF);
}
setError( setError(
translate("import_users.error.required_field", { field: eF[0] }) translate("import_users.error.required_field", { field: eF[0] })
); );
@ -226,6 +240,9 @@ const FilePicker = props => {
setProgress, setProgress,
setError setError
); );
setPdfRecords(results.recordsForPdf);
setImportResults(results); setImportResults(results);
// offer CSV download of skipped or errored records // offer CSV download of skipped or errored records
// (so that the user doesn't have to filter out successful // (so that the user doesn't have to filter out successful
@ -251,6 +268,8 @@ const FilePicker = props => {
let skippedRecords = []; let skippedRecords = [];
let erroredRecords = []; let erroredRecords = [];
let succeededRecords = []; let succeededRecords = [];
let recordsForPdf = [];
let changeStats = { let changeStats = {
toAdmin: 0, toAdmin: 0,
toGuest: 0, toGuest: 0,
@ -365,6 +384,14 @@ const FilePicker = props => {
await dataProvider.create("users", { data: recordData }); await dataProvider.create("users", { data: recordData });
} }
succeededRecords.push(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, erroredRecords,
succeededRecords, succeededRecords,
totalRecordCount: entriesCount, totalRecordCount: entriesCount,
recordsForPdf,
changeStats, changeStats,
wasDryRun: dryRun, wasDryRun: dryRun,
}; };
@ -618,6 +646,10 @@ const FilePicker = props => {
<br />, <br />,
] ]
: ""} : ""}
{translate(
"import_users.cards.results.for_print",
importResults.recordsForPdf.length
)}
<br /> <br />
{importResults.wasDryRun && [ {importResults.wasDryRun && [
translate("import_users.cards.results.simulated_only"), translate("import_users.cards.results.simulated_only"),
@ -655,20 +687,43 @@ const FilePicker = props => {
</CardActions> </CardActions>
); );
let allCards = []; let pdfDisplay =
if (uploadCard) allCards.push(uploadCard); pdfRecords && showingPdf && pdfRecords.length ? (
if (errorCards) allCards.push(errorCards); <ShowUserPdf records={pdfRecords} />
if (conflictCards) allCards.push(conflictCards); ) : null;
if (statsCards) allCards.push(...statsCards);
if (startImportCard) allCards.push(startImportCard);
if (resultsCard) allCards.push(resultsCard);
let cardContainer = <Card>{allCards}</Card>; let pdfActions = pdfRecords ? (
<CardActions>
<Button
size="large"
onClick={e => {
setShowingPdf(true);
}}
>
{translate("import_users.goToPdf")}
</Button>
</CardActions>
) : null;
return [ if (pdfRecords && showingPdf) {
<Title defaultTitle={translate("import_users.title")} />, return <Card>{pdfDisplay}</Card>;
cardContainer, } else {
]; let allCards = [];
if (uploadCard) allCards.push(uploadCard);
if (errorCards) allCards.push(errorCards);
if (conflictCards) allCards.push(conflictCards);
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>;
return [
<Title defaultTitle={translate("import_users.title")} />,
cardContainer,
];
}
}; };
export const ImportFeature = FilePicker; 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 { Title, Button } from "react-admin";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import { PDFExport } from "@progress/kendo-react-pdf"; import { PDFExport } from "@progress/kendo-react-pdf";
import QRCode from "qrcode.react"; import QRCode from "qrcode.react";
import { string, any } from "prop-types";
function xor(a, b) { function xor(a, b) {
var res = ""; var res = "";
@ -14,109 +15,204 @@ function xor(a, b) {
function calculateQrString(serverUrl, username, password) { function calculateQrString(serverUrl, username, password) {
const magicString = "wo9k5tep252qxsa5yde7366kugy6c01w7oeeya9hrmpf0t7ii7"; 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 urlString = btoa(urlString); // to base64
return serverUrl + "/#" + urlString; return serverUrl + "/#" + urlString;
} }
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,
width: 566,
padding: "none",
backgroundColor: "white",
boxShadow: "5px 5px 5px black",
margin: "auto",
overflowX: "hidden",
overflowY: "hidden",
fontFamily: "DejaVu Sans, Sans-Serif",
fontSize: 15,
},
header: {
height: 144,
width: 534,
marginLeft: 32,
marginTop: 15,
},
name: {
width: 240,
fontSize: 35,
float: "left",
marginTop: 100,
},
logo: {
width: 90,
marginTop: 50,
marginRight: 70,
float: "right",
},
body: {
clear: "both",
},
table_cell: {
verticalAlign: "top",
},
code_note: {
marginLeft: 32,
marginTop: 86,
},
qr: {
marginTop: 15,
marginLeft: 32,
},
credentials_note: {
marginTop: 86,
marginLeft: 10,
},
credentials_text: {
marginLeft: 10,
fontSize: 12,
},
credentials: {
fontFamily: "DejaVu Sans Mono, monospace",
},
note: {
fontSize: 18,
marginTop: 100,
marginLeft: 32,
marginRight: 32,
},
}));
const ShowUserPdf = props => { const ShowUserPdf = props => {
const useStyles = makeStyles(theme => ({
page: {
height: 800,
width: 566,
padding: "none",
backgroundColor: "white",
boxShadow: "5px 5px 5px black",
margin: "auto",
overflowX: "hidden",
overflowY: "hidden",
fontFamily: "DejaVu Sans, Sans-Serif",
fontSize: 15,
},
header: {
height: 144,
width: 534,
marginLeft: 32,
marginTop: 15,
},
name: {
width: 240,
fontSize: 35,
float: "left",
marginTop: 100,
},
logo: {
width: 90,
marginTop: 50,
marginRight: 70,
float: "right",
},
body: {
clear: "both",
},
table_cell: {
verticalAlign: "top",
},
code_note: {
marginLeft: 32,
marginTop: 86,
},
qr: {
marginTop: 15,
marginLeft: 32,
},
credentials_note: {
marginTop: 86,
marginLeft: 10,
},
credentials_text: {
marginLeft: 10,
fontSize: 12,
},
credentials: {
fontFamily: "DejaVu Sans Mono, monospace",
},
note: {
fontSize: 18,
marginTop: 100,
marginLeft: 32,
marginRight: 32,
},
}));
const classes = useStyles(); const classes = useStyles();
const userPdf = useRef(null);
var resume;
const exportPDF = () => { const exportPDF = () => {
resume.save(); userPdf.current.save();
}; };
var qrCode = ""; let userRecords;
var displayname = "";
var id = ""; if (props.records) {
var password = ""; userRecords = props.records;
var username = ""; }
var serverUrl = "";
if ( if (
props.location &&
props.location.state && props.location.state &&
props.location.state.id && props.location.state.id &&
props.location.state.password props.location.state.password
) { ) {
id = props.location.state.id; userRecords = [
password = props.location.state.password; {
id: props.location.state.id,
username = id.substring(1, id.indexOf(":")); password: props.location.state.password,
serverUrl = "https://" + id.substring(id.indexOf(":") + 1); displayname: props.location.state.displayname,
},
const qrString = calculateQrString(serverUrl, username, password); ];
qrCode = <QRCode value={qrString} size={128} />;
displayname = props.location.state.displayname;
} }
return ( return (
@ -126,82 +222,39 @@ const ShowUserPdf = props => {
<PDFExport <PDFExport
paperSize={"A4"} paperSize={"A4"}
fileName="User.pdf" fileName="Users.pdf"
title="" title=""
subject="" subject=""
keywords="" keywords=""
ref={r => (resume = r)} ref={userPdf}
//ref={r => (resume = r)}
> >
<div className={classes.page}> {userRecords.map(record => {
<div className={classes.header}> if (record.id && record.password) {
<div className={classes.name}>{displayname}</div> const username = record.id.substring(1, record.id.indexOf(":"));
<img className={classes.logo} alt="Logo" src="images/logo.png" /> const serverUrl =
</div> "https://" + record.id.substring(record.id.indexOf(":") + 1);
<div className={classes.body}> const qrString = calculateQrString(
<table> serverUrl,
<tbody> username,
<tr> record.password
<td width="200px"> );
<div className={classes.code_note}> const qrCode = <QRCode value={qrString} size={128} />;
Ihr persönlicher Anmeldecode: return (
</div> <UserPdfPage
</td> classes={classes}
<td className={classes.table_cell}> displayname={record.displayname}
<div className={classes.credentials_note}> qrCode={qrCode}
Ihre persönlichen Zugangsdaten: serverUrl={serverUrl}
</div> username={username}
</td> password={record.password}
</tr> />
<tr> );
<td> } else {
<div className={classes.qr}>{qrCode}</div> /* Skip empty PDF pages */
</td> return null;
<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>
</PDFExport> </PDFExport>
</div> </div>
); );

View File

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

View File

@ -104,6 +104,8 @@ export default {
with_error: with_error:
"%{smart_count} Eintrag mit Fehlern ||| %{smart_count} Einträge mit Fehlern", "%{smart_count} Eintrag mit Fehlern ||| %{smart_count} Einträge mit Fehlern",
simulated_only: "Import-Vorgang war nur simuliert", 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: with_error:
"%{smart_count} entry with errors ||| %{smart_count} entries with errors", "%{smart_count} entry with errors ||| %{smart_count} entries with errors",
simulated_only: "Run was only simulated", 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`; 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 = { const POWER_LEVELS = {
admin: 100, admin: 100,
mod: 50, mod: 50,