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,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> | ||||
|   ); | ||||
|  | ||||
| @ -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
	 Timo Paulssen
						Timo Paulssen