Import users from CSV
Change-Id: Id05363ecc39aee4fdc4ac6afbcb039558b2a17ed
This commit is contained in:
parent
7b5c0e2845
commit
1aaa137afe
@ -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} />,
|
||||
]}
|
||||
>
|
||||
|
@ -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;
|
||||
|
@ -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,124 +15,40 @@ 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 => {
|
||||
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();
|
||||
|
||||
var resume;
|
||||
|
||||
const exportPDF = () => {
|
||||
resume.save();
|
||||
};
|
||||
|
||||
var qrCode = "";
|
||||
var displayname = "";
|
||||
var id = "";
|
||||
var password = "";
|
||||
var username = "";
|
||||
var serverUrl = "";
|
||||
|
||||
if (
|
||||
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;
|
||||
}
|
||||
UserPdfPage.propTypes = {
|
||||
classes: any,
|
||||
displayname: string,
|
||||
qrCode: any,
|
||||
serverUrl: string,
|
||||
username: string,
|
||||
password: string,
|
||||
};
|
||||
|
||||
function UserPdfPage({
|
||||
classes,
|
||||
displayname,
|
||||
qrCode,
|
||||
serverUrl,
|
||||
username,
|
||||
password,
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<Title title="PDF" />
|
||||
<Button label="synapseadmin.action.download_pdf" onClick={exportPDF} />
|
||||
|
||||
<PDFExport
|
||||
paperSize={"A4"}
|
||||
fileName="User.pdf"
|
||||
title=""
|
||||
subject=""
|
||||
keywords=""
|
||||
ref={r => (resume = r)}
|
||||
>
|
||||
<div className={classes.page}>
|
||||
<div className={classes.header}>
|
||||
<div className={classes.name}>{displayname}</div>
|
||||
@ -193,8 +110,8 @@ const ShowUserPdf = props => {
|
||||
</tbody>
|
||||
</table>
|
||||
<div className={classes.note}>
|
||||
Hier können Sie Ihre selbst gewählte
|
||||
Schlüsselsicherungs-Passphrase notieren:
|
||||
Hier können Sie Ihre selbst gewählte Schlüsselsicherungs-Passphrase
|
||||
notieren:
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
@ -202,6 +119,142 @@ const ShowUserPdf = props => {
|
||||
</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 classes = useStyles();
|
||||
const userPdf = useRef(null);
|
||||
|
||||
const exportPDF = () => {
|
||||
userPdf.current.save();
|
||||
};
|
||||
|
||||
let userRecords;
|
||||
|
||||
if (props.records) {
|
||||
userRecords = props.records;
|
||||
}
|
||||
|
||||
if (
|
||||
props.location &&
|
||||
props.location.state &&
|
||||
props.location.state.id &&
|
||||
props.location.state.password
|
||||
) {
|
||||
userRecords = [
|
||||
{
|
||||
id: props.location.state.id,
|
||||
password: props.location.state.password,
|
||||
displayname: props.location.state.displayname,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title title="PDF" />
|
||||
<Button label="synapseadmin.action.download_pdf" onClick={exportPDF} />
|
||||
|
||||
<PDFExport
|
||||
paperSize={"A4"}
|
||||
fileName="Users.pdf"
|
||||
title=""
|
||||
subject=""
|
||||
keywords=""
|
||||
ref={userPdf}
|
||||
//ref={r => (resume = r)}
|
||||
>
|
||||
{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>
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user