diff --git a/ArduinoFrontend/src/app/Libs/Workspace.ts b/ArduinoFrontend/src/app/Libs/Workspace.ts index d50c93e59..32da14cdb 100644 --- a/ArduinoFrontend/src/app/Libs/Workspace.ts +++ b/ArduinoFrontend/src/app/Libs/Workspace.ts @@ -477,8 +477,8 @@ export class Workspace { if (window.isCodeEditorOpened) { return; } - // console.log([event.ctrlKey, event.key]); - if (event.key === 'Delete' || event.key === 'Backspace') { + if ((event.key === 'Delete' || event.key === 'Backspace') + && !(event['target']['localName'] === 'input' || event['target']['localName'] === 'textarea')) { // Backspace or Delete Workspace.DeleteComponent(); } @@ -1050,4 +1050,76 @@ export class Workspace { // Hide Loading animation window.hideLoading(); } + + + /** + * Function generates a JSON object containing all details of the workspace and downloads it + * @param name string + * @param description string + */ + static SaveJson(name: string = '', description: string = '') { + + const id = Date.now(); + + // Default Save object + const saveObj = { + id, + canvas: { + x: Workspace.translateX, + y: Workspace.translateY, + scale: Workspace.scale + }, + project: { + name, + description, + created_at: Date.now(), + } + }; + + // For each item in the scope + for (const key in window.scope) { + // if atleast one component is present + if (window.scope[key] && window.scope[key].length > 0) { + saveObj[key] = []; + // Add the component to the save object + for (const item of window.scope[key]) { + if (item.save) { + saveObj[key].push(item.save()); + } + } + } + } + + // Export JSON File & Download it + const filename = `${name}.json`; + const jsonStr = JSON.stringify(saveObj); + + const element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(jsonStr)); + element.setAttribute('download', filename); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); + + return true; + + } + + /** + * Function to return if workspace is empty or not + * @returns 'False' if workspace is not empty & 'True' if workspace is empty + */ + static checkIfWorkspaceEmpty() { + for (const key in window.scope) { + if (window.scope[key].length > 0) { + return false; + } + } + return true; + } + } diff --git a/ArduinoFrontend/src/app/app.module.ts b/ArduinoFrontend/src/app/app.module.ts index 740eadc26..059ca67ce 100644 --- a/ArduinoFrontend/src/app/app.module.ts +++ b/ArduinoFrontend/src/app/app.module.ts @@ -35,6 +35,8 @@ import { HeaderComponent } from './header/header.component'; import { ViewProjectComponent } from './view-project/view-project.component'; import { AlertModalComponent } from './alert/alert-modal/alert-modal.component'; import { ConfirmModalComponent } from './alert/confirm-modal/confirm-modal.component'; +import { ExportJSONDialogComponent } from './export-jsondialog/export-jsondialog.component'; +import { ExitConfirmDialogComponent } from './exit-confirm-dialog/exit-confirm-dialog.component'; /** * Monaco OnLoad Function @@ -67,6 +69,8 @@ const monacoConfig: NgxMonacoEditorConfig = { HeaderComponent, AlertModalComponent, ConfirmModalComponent, + ExportJSONDialogComponent, + ExitConfirmDialogComponent, ], imports: [ BrowserModule, @@ -89,7 +93,15 @@ const monacoConfig: NgxMonacoEditorConfig = { // providers: [{provide: LocationStrategy, useClass: PathLocationStrategy}], providers: [{ provide: LocationStrategy, useClass: HashLocationStrategy }], bootstrap: [AppComponent], - entryComponents: [ViewComponentInfoComponent, ExportfileComponent, ComponentlistComponent, AlertModalComponent, ConfirmModalComponent], + entryComponents: [ + ViewComponentInfoComponent, + ExportfileComponent, + ComponentlistComponent, + AlertModalComponent, + ConfirmModalComponent, + ExportJSONDialogComponent, + ExitConfirmDialogComponent, + ], schemas: [ CUSTOM_ELEMENTS_SCHEMA ], diff --git a/ArduinoFrontend/src/app/exit-confirm-dialog/exit-confirm-dialog.component.css b/ArduinoFrontend/src/app/exit-confirm-dialog/exit-confirm-dialog.component.css new file mode 100644 index 000000000..49c838514 --- /dev/null +++ b/ArduinoFrontend/src/app/exit-confirm-dialog/exit-confirm-dialog.component.css @@ -0,0 +1,4 @@ +.action-div{ + display: flex; + justify-content: space-around; +} diff --git a/ArduinoFrontend/src/app/exit-confirm-dialog/exit-confirm-dialog.component.html b/ArduinoFrontend/src/app/exit-confirm-dialog/exit-confirm-dialog.component.html new file mode 100644 index 000000000..8892ea25f --- /dev/null +++ b/ArduinoFrontend/src/app/exit-confirm-dialog/exit-confirm-dialog.component.html @@ -0,0 +1,5 @@ +

Do you want to exit?

+
+ + +
diff --git a/ArduinoFrontend/src/app/exit-confirm-dialog/exit-confirm-dialog.component.spec.ts b/ArduinoFrontend/src/app/exit-confirm-dialog/exit-confirm-dialog.component.spec.ts new file mode 100644 index 000000000..bcf4860d0 --- /dev/null +++ b/ArduinoFrontend/src/app/exit-confirm-dialog/exit-confirm-dialog.component.spec.ts @@ -0,0 +1,30 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogModule, MatDialogRef } from '@angular/material'; + +import { ExitConfirmDialogComponent } from './exit-confirm-dialog.component'; + +describe('ExitConfirmDialogComponent', () => { + let component: ExitConfirmDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [MatDialogModule], + declarations: [ExitConfirmDialogComponent], + providers: [ + { provide: MatDialogRef, useValue: {} }, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ExitConfirmDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ArduinoFrontend/src/app/exit-confirm-dialog/exit-confirm-dialog.component.ts b/ArduinoFrontend/src/app/exit-confirm-dialog/exit-confirm-dialog.component.ts new file mode 100644 index 000000000..d124e98d5 --- /dev/null +++ b/ArduinoFrontend/src/app/exit-confirm-dialog/exit-confirm-dialog.component.ts @@ -0,0 +1,21 @@ +import { Component, OnInit } from '@angular/core'; +import { MatDialogRef } from '@angular/material'; + +@Component({ + selector: 'app-exit-confirm-dialog', + templateUrl: './exit-confirm-dialog.component.html', + styleUrls: ['./exit-confirm-dialog.component.css'] +}) +export class ExitConfirmDialogComponent implements OnInit { + + constructor(public dialogRef: MatDialogRef) { } + + ngOnInit() { + } + + // Function to handle if user want to exit + yesClick() { + this.dialogRef.close(true); + } + +} diff --git a/ArduinoFrontend/src/app/export-jsondialog/export-jsondialog.component.css b/ArduinoFrontend/src/app/export-jsondialog/export-jsondialog.component.css new file mode 100644 index 000000000..c8ee7f6f3 --- /dev/null +++ b/ArduinoFrontend/src/app/export-jsondialog/export-jsondialog.component.css @@ -0,0 +1,3 @@ +.full-width{ + width: 100%; +} diff --git a/ArduinoFrontend/src/app/export-jsondialog/export-jsondialog.component.html b/ArduinoFrontend/src/app/export-jsondialog/export-jsondialog.component.html new file mode 100644 index 000000000..2d0d6b9c0 --- /dev/null +++ b/ArduinoFrontend/src/app/export-jsondialog/export-jsondialog.component.html @@ -0,0 +1,17 @@ +
+ Enter the name of File to be Saved +
+ +
+ + File Name + + + +
+ + + + + + diff --git a/ArduinoFrontend/src/app/export-jsondialog/export-jsondialog.component.spec.ts b/ArduinoFrontend/src/app/export-jsondialog/export-jsondialog.component.spec.ts new file mode 100644 index 000000000..426ec4f97 --- /dev/null +++ b/ArduinoFrontend/src/app/export-jsondialog/export-jsondialog.component.spec.ts @@ -0,0 +1,70 @@ +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { MatDialogModule, MatDialogRef, MatFormFieldModule, MAT_DIALOG_DATA } from '@angular/material'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Workspace } from '../Libs/Workspace'; +import { ExportJSONDialogComponent } from './export-jsondialog.component'; + +describe('ExportJSONDialogComponent', () => { + + let component: ExportJSONDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + MatFormFieldModule, + FormsModule, + MatDialogModule, + ], + declarations: [ + ExportJSONDialogComponent + ], + providers: [ + { provide: MatDialogRef, useValue: {} }, + { provide: MAT_DIALOG_DATA, useValue: { description: 'this is a desc', title: 'title' } }, + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ExportJSONDialogComponent); + component = fixture.componentInstance; + }); + + it('should create the app', () => { + expect(component).toBeTruthy(); + }); + + it('Value of fileName variable should be given string', () => { + expect(component.fileName).toBe('title'); + }); + + it('Value of Description variable should be given string', () => { + expect(component.description).toBe('this is a desc'); + }); + + it('should return truthy as workspace is empty', () => { + expect(Workspace.checkIfWorkspaceEmpty()).toBeTruthy(); + }); + + it('should return falsey as workspace is not empty', () => { + component.ngOnInit(); + window['scope'] = { + id: 1620892078891, + canvas: { x: 0, y: 0, scale: 1 }, + project: { + name: 'Untitled', + description: '', + created_at: 1620892078891 + }, + Resistor: [{ x: 483, y: 209, tx: 68, ty: 100, id: 1620892071196, data: { value: 1000, tolerance: 10 } }] + }; + expect(Workspace.checkIfWorkspaceEmpty()).toBeFalsy(); + }); + + it('should return truthy after downloading json file', () => { + expect(Workspace.SaveJson()).toBeTruthy(); + }); +}); diff --git a/ArduinoFrontend/src/app/export-jsondialog/export-jsondialog.component.ts b/ArduinoFrontend/src/app/export-jsondialog/export-jsondialog.component.ts new file mode 100644 index 000000000..c070f58b2 --- /dev/null +++ b/ArduinoFrontend/src/app/export-jsondialog/export-jsondialog.component.ts @@ -0,0 +1,33 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; +import { Workspace } from '../Libs/Workspace'; + +@Component({ + selector: 'app-export-jsondialog', + templateUrl: './export-jsondialog.component.html', + styleUrls: ['./export-jsondialog.component.css'] +}) +export class ExportJSONDialogComponent implements OnInit { + + description: string; + fileName = ''; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data) { + this.description = data.description; + this.fileName = data.title; + } + + ngOnInit() { + } + + /** + * Save Project function, Calls Workspace.SaveJson with edited fileName and then closes project + */ + saveProject() { + Workspace.SaveJson(this.fileName, this.description); + this.dialogRef.close(); + } + +} diff --git a/ArduinoFrontend/src/app/header/header.component.html b/ArduinoFrontend/src/app/header/header.component.html index 2dbd800da..e89cf0c6f 100644 --- a/ArduinoFrontend/src/app/header/header.component.html +++ b/ArduinoFrontend/src/app/header/header.component.html @@ -12,7 +12,10 @@
+ Home + Dashboard Gallery + Editor Login
+ | + + + + | + + } + {isFavourite && localStorage.getItem('esim_token') && + + + + } - + + + + + + } + /> ) } SideComp.propTypes = { - component: PropTypes.object.isRequired + component: PropTypes.object.isRequired, + isFavourite: PropTypes.bool, + setFavourite: PropTypes.func, + favourite: PropTypes.array } diff --git a/eda-frontend/src/components/SchematicEditor/SimulationProperties.js b/eda-frontend/src/components/SchematicEditor/SimulationProperties.js index c0240e4c6..361f2d610 100644 --- a/eda-frontend/src/components/SchematicEditor/SimulationProperties.js +++ b/eda-frontend/src/components/SchematicEditor/SimulationProperties.js @@ -22,7 +22,8 @@ import { makeStyles } from '@material-ui/core/styles' import { useSelector, useDispatch } from 'react-redux' import { setControlLine, setControlBlock, setResultTitle, setResultGraph, setResultText } from '../../redux/actions/index' import { GenerateNetList, GenerateNodeList, GenerateCompList } from './Helper/ToolbarTools' -import SimulationScreen from './SimulationScreen' +import SimulationScreen from '../Shared/SimulationScreen' +import { Multiselect } from 'multiselect-react-dropdown' import api from '../../utils/Api' @@ -50,6 +51,7 @@ const useStyles = makeStyles((theme) => ({ export default function SimulationProperties () { const netfile = useSelector(state => state.netlistReducer) const isSimRes = useSelector(state => state.simulationReducer.isSimRes) + const [taskId, setTaskId] = useState(null) const dispatch = useDispatch() const classes = useStyles() const [nodeList, setNodeList] = useState([]) @@ -69,7 +71,7 @@ export default function SimulationProperties () { start: '', stop: '', step: '', - skipInitial: 'No' + skipInitial: false }) const [acAnalysisControlLine, setAcAnalysisControlLine] = useState({ @@ -79,17 +81,49 @@ export default function SimulationProperties () { pointsBydecade: '' }) - const [controlBlockParam, setControlBlockParam] = useState('') + const [tfAnalysisControlLine, setTfAnalysisControlLine] = useState({ + outputNodes: false, + outputVoltageSource: '', + inputVoltageSource: '' + }) + const [controlBlockParam, setControlBlockParam] = useState('') + const [disabled, setDisabled] = React.useState(false) const handleControlBlockParam = (evt) => { setControlBlockParam(evt.target.value) } - + var analysisNodeArray = []; var analysisCompArray = []; var nodeArray = [] + const pushZero = (nodeArray) => { + nodeArray.push({ key: 0 }) + } const onDcSweepTabExpand = () => { try { setComponentsList(['', ...GenerateCompList()]) + setNodeList(['', ...GenerateNodeList()]) } catch (err) { setComponentsList([]) + setNodeList([]) + alert('Circuit not complete. Please Check Connectons.') + } + } + const onTransientAnalysisTabExpand = () => { + try { + setComponentsList(['', ...GenerateCompList()]) + setNodeList(['', ...GenerateNodeList()]) + } catch (err) { + setComponentsList([]) + setNodeList([]) + alert('Circuit not complete. Please Check Connectons.') + } + } + + const onTFTabExpand = () => { + try { + setComponentsList(['', ...GenerateCompList()]) + setNodeList(['', ...GenerateNodeList()]) + } catch (err) { + setComponentsList([]) + setNodeList([]) alert('Circuit not complete. Please Check Connectons.') } } @@ -111,6 +145,14 @@ export default function SimulationProperties () { [evt.target.id]: value }) } + const handleTransientAnalysisControlLineUIC = (evt) => { + const value = evt.target.checked + + setTransientAnalysisControlLine({ + ...transientAnalysisControlLine, + [evt.target.id]: value + }) + } const handleAcAnalysisControlLine = (evt) => { const value = evt.target.value @@ -121,6 +163,22 @@ export default function SimulationProperties () { }) } + const handleTfAnalysisControlLine = (evt) => { + const value = evt.target.value + setTfAnalysisControlLine({ + ...tfAnalysisControlLine, + [evt.target.id]: value + }) + } + const handleTfAnalysisControlLineNodes = (evt) => { + const value = evt.target.checked + setTfAnalysisControlLine({ + ...tfAnalysisControlLine, + [evt.target.id]: value + }) + setDisabled(tfAnalysisControlLine.outputNodes) + } + const [simulateOpen, setSimulateOpen] = React.useState(false) const handlesimulateOpen = () => { setSimulateOpen(true) @@ -140,6 +198,129 @@ export default function SimulationProperties () { Decade: 'dec', Octave: 'oct' } + let [selectedValue, setSelectedValue] = React.useState([]) + let [selectedValueDCSweep, setSelectedValueDCSweep] = React.useState([]) + let [selectedValueTransientAnal, setSelectedValueTransientAnal] = React.useState([]) + let [selectedValueTFAnal, setSelectedValueTFAnal] = React.useState([]) + let [selectedValueComp, setSelectedValueComp] = React.useState([]) + let [selectedValueDCSweepComp, setSelectedValueDCSweepComp] = React.useState([]) + let [selectedValueTransientAnalComp, setSelectedValueTransientAnalComp] = React.useState([]) + + const handleAddSelectedValueDCSweep = (data) => { + var f = 0 + selectedValueDCSweep.forEach((value, i) => { + if (value[i] !== undefined) { + if (value[i].key === data) f = 1 + } + }) + if (f === 0) { + const tmp = [...selectedValueDCSweep, data] + setSelectedValueDCSweep(tmp) + } + // console.log(selectedValue) + } + const handleRemSelectedValueDCSweep = (data) => { + const tmp = [] + selectedValueDCSweep.forEach((value, i) => { + if (value[i] !== undefined) { + if (value[i].key !== data) tmp.push(data) + } + }) + selectedValueDCSweep = tmp + // console.log(selectedValue) + } + const handleAddSelectedValueTransientAnal = (data) => { + var f = 0 + selectedValueTransientAnal.forEach((value, i) => { + if (value[i] !== undefined) { + if (value[i].key === data) f = 1 + } + }) + if (f === 0) { + const tmp = [...selectedValueTransientAnal, data] + setSelectedValueTransientAnal(tmp) + } + // console.log(selectedValue) + } + const handleRemSelectedValueTransientAnal = (data) => { + const tmp = [] + selectedValueTransientAnal.forEach((value, i) => { + if (value[i] !== undefined) { + if (value[i].key !== data) tmp.push(data) + } + }) + selectedValueTransientAnal = tmp + // console.log(selectedValue) + } + const handleAddSelectedValueTFAnal = (data) => { + var f = 0 + selectedValueTFAnal.forEach((value, i) => { + if (value[i] !== undefined) { + if (value[i].key === data) f = 1 + } + }) + if (f === 0) { + const tmp = [...selectedValueTFAnal, data] + setSelectedValueTFAnal(tmp) + } + // console.log(selectedValue) + } + const handleRemSelectedValueTFAnal = (data) => { + const tmp = [] + selectedValueTFAnal.forEach((value, i) => { + if (value[i] !== undefined) { + if (value[i].key !== data) tmp.push(data) + } + }) + selectedValueTFAnal = tmp + // console.log(selectedValue) + } + const handleAddSelectedValueDCSweepComp = (data) => { + var f = 0 + selectedValueDCSweepComp.forEach((value, i) => { + if (value[i] !== undefined) { + if (value[i].key === data) f = 1 + } + }) + if (f === 0) { + const tmp = [...selectedValueDCSweepComp, data] + setSelectedValueDCSweepComp(tmp) + } + // console.log(selectedValue) + } + const handleRemSelectedValueDCSweepComp = (data) => { + const tmp = [] + selectedValueDCSweepComp.forEach((value, i) => { + if (value[i] !== undefined) { + if (value[i].key !== data) tmp.push(data) + } + }) + selectedValueDCSweepComp = tmp + // console.log(selectedValue) + } + const handleAddSelectedValueTransientAnalComp = (data) => { + var f = 0 + selectedValueTransientAnalComp.forEach((value, i) => { + if (value[i] !== undefined) { + if (value[i].key === data) f = 1 + } + }) + if (f === 0) { + const tmp = [...selectedValueTransientAnalComp, data] + setSelectedValueTransientAnalComp(tmp) + } + // console.log(selectedValue) + } + const handleRemSelectedValueTransientAnalComp = (data) => { + const tmp = [] + selectedValueTransientAnalComp.forEach((value, i) => { + if (value[i] !== undefined) { + if (value[i].key !== data) tmp.push(data) + } + }) + selectedValueTransientAnalComp = tmp + // console.log(selectedValue) + } // Prepare Netlist to file const prepareNetlist = (netlist) => { @@ -153,11 +334,12 @@ export default function SimulationProperties () { } function sendNetlist (file) { + setIsResult(false) netlistConfig(file) .then((response) => { const res = response.data const getUrl = 'simulation/status/'.concat(res.details.task_id) - + setTaskId(res.details.task_id) simulationResult(getUrl) }) .catch(function (error) { @@ -191,7 +373,6 @@ export default function SimulationProperties () { if (result === null) { setIsResult(false) } else { - setIsResult(true) var temp = res.data.details.data var data = result.data // console.log('DATA SIm', data) @@ -206,12 +387,12 @@ export default function SimulationProperties () { // labels for (var x = 1; x < lab.length; x++) { - if (lab[x].includes('#branch')) { - lab[x] = `I (${lab[x].replace('#branch', '')})` - } - // uncomment below if you want label like V(r1.1) but it will break the graph showing time as well - // else { - // lab[x] = `V (${lab[x]})` + // if (lab[x].includes('#branch')) { + // lab[x] = `I (${lab[x].replace('#branch', '')})` + // } + // uncomment below if you want label like V(r1.1) but it will break the graph showing time as well + // else { + // lab[x] = `V (${lab[x]})` // } simResultGraph.labels.push(lab[x]) @@ -234,8 +415,11 @@ export default function SimulationProperties () { for (let i = 0; i < temp.length; i++) { let postfixUnit = '' if (temp[i][0].includes('#branch')) { - temp[i][0] = `I(${temp[i][0].replace('#branch', '')})` postfixUnit = 'A' + } else if (temp[i][0].includes('transfer_function')) { + postfixUnit = '' + } else if (temp[i][0].includes('impedance')) { + postfixUnit = 'Ohm' } else { temp[i][0] = `V(${temp[i][0]})` postfixUnit = 'V' @@ -247,6 +431,7 @@ export default function SimulationProperties () { handleSimulationResult(res.data.details) dispatch(setResultText(simResultText)) } + setIsResult(true) } } }) @@ -260,6 +445,8 @@ export default function SimulationProperties () { var compNetlist = GenerateNetList() var controlLine = '' var controlBlock = '' + var skipMultiNodeChk = 0 + var nodes = '' switch (type) { case 'DcSolver': // console.log('To be implemented') @@ -271,12 +458,18 @@ export default function SimulationProperties () { // console.log(dcSweepcontrolLine) controlLine = `.dc ${dcSweepcontrolLine.parameter} ${dcSweepcontrolLine.start} ${dcSweepcontrolLine.stop} ${dcSweepcontrolLine.step} ${dcSweepcontrolLine.parameter2} ${dcSweepcontrolLine.start2} ${dcSweepcontrolLine.stop2} ${dcSweepcontrolLine.step2}` dispatch(setResultTitle('DC Sweep Output')) + selectedValue = selectedValueDCSweep + selectedValueComp = selectedValueDCSweepComp break case 'Transient': // console.log(transientAnalysisControlLine) - controlLine = `.tran ${transientAnalysisControlLine.step} ${transientAnalysisControlLine.stop} ${transientAnalysisControlLine.start}` + var uic = '' + if (transientAnalysisControlLine.skipInitial === true) uic = 'UIC' + controlLine = `.tran ${transientAnalysisControlLine.step} ${transientAnalysisControlLine.stop} ${transientAnalysisControlLine.start} ${uic}` dispatch(setResultTitle('Transient Analysis Output')) + selectedValue = selectedValueTransientAnal + selectedValueComp = selectedValueTransientAnalComp break case 'Ac': // console.log(acAnalysisControlLine) @@ -284,11 +477,59 @@ export default function SimulationProperties () { dispatch(setResultTitle('AC Analysis Output')) break + + case 'tfAnalysis': + + selectedValue = selectedValueTFAnal + if (tfAnalysisControlLine.outputNodes === true) { + selectedValue.forEach((value, i) => { + if (value[i] !== undefined) { + nodes = nodes + ' ' + String(value[i].key) + } + }) + nodes = 'V(' + nodes + ')' + } else { + nodes = `I(${tfAnalysisControlLine.outputVoltageSource})` + } + console.log(tfAnalysisControlLine.outputNodes) + controlLine = `.tf ${nodes} ${tfAnalysisControlLine.inputVoltageSource}` + + dispatch(setResultTitle('Transfer Function Analysis Output')) + skipMultiNodeChk = 1 + break default: break } - let cblockline - if (controlBlockParam.length <= 0) { cblockline = 'all' } else { cblockline = controlBlockParam } + // console.log(selectedValue) + var atleastOne = 0 + let cblockline = '' + // if either the extra expression field or the nodes multi select + // drop down list in enabled then atleast one value is made non zero + // to add add all instead to the print statement. + if (selectedValue.length > 0 && selectedValue !== null && skipMultiNodeChk === 0) { + selectedValue.forEach((value, i) => { + if (value[i] !== undefined && value[i].key !== 0) { + atleastOne = 1 + cblockline = cblockline + ' ' + String(value[i].key) + } + }) + } + if (selectedValueComp.length > 0 && selectedValueComp !== null) { + selectedValueComp.forEach((value, i) => { + if (value[i] !== undefined && value[i].key !== 0) { + atleastOne = 1 + if (value[i].key.charAt(0) === 'V' || value[i].key.charAt(0) === 'v') { + cblockline = cblockline + ' I(' + String(value[i].key) + ') ' + } + } + }) + } + if (controlBlockParam.length > 0) { + cblockline = cblockline + ' ' + controlBlockParam + atleastOne = 1 + } + + if (atleastOne === 0) cblockline = 'all' controlBlock = `\n.control \nrun \nprint ${cblockline} > data.txt \n.endc \n.end` // console.log(controlLine) @@ -323,7 +564,7 @@ export default function SimulationProperties () { return ( <>
- + {/* Simulation modes list */} @@ -336,6 +577,7 @@ export default function SimulationProperties () { expandIcon={} aria-controls="panel1a-content" id="panel1a-header" + style={{ width: '100% ' }} > DC Solver @@ -395,6 +637,7 @@ export default function SimulationProperties () { expandIcon={} aria-controls="panel1a-content" id="panel1a-header" + style={{ width: '97%' }} > DC Sweep @@ -511,6 +754,30 @@ export default function SimulationProperties () { /> + + + + + + + + + + + + + + + + + + + + + + + + + + + +
- ) -} - -SimulationScreen.propTypes = { - open: PropTypes.bool, - close: PropTypes.func, - isResult: PropTypes.bool - // simResults: PropTypes.object -} diff --git a/eda-frontend/src/components/Simulator/textToFile.js b/eda-frontend/src/components/Simulator/textToFile.js index ad992b4d1..6bf098562 100644 --- a/eda-frontend/src/components/Simulator/textToFile.js +++ b/eda-frontend/src/components/Simulator/textToFile.js @@ -1,3 +1,4 @@ +import randomstring from 'randomstring' export default function textToFile (data) { // create a file from a blob @@ -5,6 +6,7 @@ export default function textToFile (data) { var myblob = new Blob([data], { type: 'text/plain' }) - var file = new File([myblob], 'netlist.cir', { type: 'text/plain', lastModified: Date.now() }) + var fileName = randomstring.generate({ length: 15 }) + '.cir' + var file = new File([myblob], fileName, { type: 'text/plain', lastModified: Date.now() }) return file } diff --git a/eda-frontend/src/pages/Account/ChangePassword.js b/eda-frontend/src/pages/Account/ChangePassword.js new file mode 100644 index 000000000..b7145c98a --- /dev/null +++ b/eda-frontend/src/pages/Account/ChangePassword.js @@ -0,0 +1,190 @@ +import React, { useState, useEffect } from 'react' +import { + Container, + Button, + Typography, + TextField, + Card, + Avatar, + InputAdornment, + IconButton +} from '@material-ui/core' +import { makeStyles } from '@material-ui/core/styles' +import Visibility from '@material-ui/icons/Visibility' +import VisibilityOff from '@material-ui/icons/VisibilityOff' +import LockOutlinedIcon from '@material-ui/icons/LockOutlined' +import { useSelector, useDispatch } from 'react-redux' +import { changePassword, authDefault } from '../../redux/actions/index' + +const useStyles = makeStyles((theme) => ({ + paper: { + marginTop: theme.spacing(20), + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: theme.spacing(3, 5) + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: theme.palette.primary.main + }, + form: { + width: '100%', // Fix IE 11 issue. + marginTop: theme.spacing(1) + }, + submit: { + margin: theme.spacing(1.5, 0) + } +})) + +export default function ChangePassword () { + const classes = useStyles() + + const account = useSelector(state => state.accountReducer) + + const dispatch = useDispatch() + var homeURL = `${window.location.protocol}\\\\${window.location.host}/` + + useEffect(() => { + dispatch(authDefault()) + document.title = 'Change password - eSim ' + }, [dispatch]) + + const [currentPassword, setCurrentPassword] = useState('') + const [newPassword, setNewPassword] = useState('') + const [reNewPassword, setReNewPassword] = useState('') + const [showCurrentPassword, setShowCurrentPassword] = useState(false) + const [showNewPassword, setShowNewPassword] = useState(false) + const [showReNewPassword, setShowReNewPassword] = useState(false) + + const handleClickShowNewPassword = () => setShowNewPassword(!showNewPassword) + const handleMouseDownNewPassword = () => setShowNewPassword(!showNewPassword) + + const handleClickShowReNewPassword = () => setShowReNewPassword(!showReNewPassword) + const handleMouseDownReNewPassword = () => setShowReNewPassword(!showReNewPassword) + + const handleClickShowCurrentPassword = () => setShowCurrentPassword(!showCurrentPassword) + const handleMouseDownCurrentPassword = () => setShowCurrentPassword(!showCurrentPassword) + + return ( + + + + + + + + Change password + + + {/* Display's error messages while signing in */} + + {account.changePasswordError} + + +
+ + + {showCurrentPassword ? : } {/* Handle password visibility */} + + + ) + }} + type={showCurrentPassword ? 'text' : 'password'} + id="currentPassword" + value={currentPassword} + onChange={e => setCurrentPassword(e.target.value)} + autoComplete="current-password" + /> + + + {showNewPassword ? : } {/* Handle password visibility */} + + + ) + }} + type={showNewPassword ? 'text' : 'password'} + id="newPassword" + value={newPassword} + onChange={e => setNewPassword(e.target.value)} + autoComplete="new-password" + /> + + + {showReNewPassword ? : } {/* Handle password visibility */} + + + ) + }} + type={showReNewPassword ? 'text' : 'password'} + id="reNewPassword" + value={reNewPassword} + onChange={e => setReNewPassword(e.target.value)} + autoComplete="re-new-password" + /> + + + +
+ +
+ ) +} diff --git a/eda-frontend/src/pages/Login.js b/eda-frontend/src/pages/Login.js index ca312c29a..419170d3f 100644 --- a/eda-frontend/src/pages/Login.js +++ b/eda-frontend/src/pages/Login.js @@ -58,7 +58,10 @@ export default function SignIn (props) { useEffect(() => { dispatch(authDefault()) document.title = 'Login - eSim ' - if (props.location.search !== '') { + const ardUrl = localStorage.getItem('ard_redurl') + if (ardUrl && ardUrl !== '') { + url = ardUrl + } else if (props.location.search !== '') { const query = new URLSearchParams(props.location.search) url = query.get('url') localStorage.setItem('ard_redurl', url) @@ -156,7 +159,7 @@ export default function SignIn (props) { - + Forgot password? diff --git a/eda-frontend/src/pages/ResetPassword/Confirmation.js b/eda-frontend/src/pages/ResetPassword/Confirmation.js new file mode 100644 index 000000000..1512c67d5 --- /dev/null +++ b/eda-frontend/src/pages/ResetPassword/Confirmation.js @@ -0,0 +1,165 @@ +// User Sign Up / Register page. +import React, { useState, useEffect } from 'react' +import { + Container, + Button, + Typography, + TextField, + Card, + Avatar, + InputAdornment, + IconButton +} from '@material-ui/core' +import PropTypes from 'prop-types' +import { makeStyles } from '@material-ui/core/styles' +import Visibility from '@material-ui/icons/Visibility' +import VisibilityOff from '@material-ui/icons/VisibilityOff' +import LockOutlinedIcon from '@material-ui/icons/LockOutlined' +import { useSelector, useDispatch } from 'react-redux' +import { resetPasswordConfirm, authDefault } from '../../redux/actions/index' + +const useStyles = makeStyles((theme) => ({ + paper: { + marginTop: theme.spacing(20), + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: theme.spacing(3, 5) + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: theme.palette.primary.main + }, + form: { + width: '100%', // Fix IE 11 issue. + marginTop: theme.spacing(1) + }, + submit: { + margin: theme.spacing(1.5, 0) + } +})) + +export default function ResetPasswordConfirm ({ match }) { + const classes = useStyles() + + const auth = useSelector(state => state.authReducer) + + const dispatch = useDispatch() + var homeURL = `${window.location.protocol}\\\\${window.location.host}/` + const { id, token } = match.params + + useEffect(() => { + dispatch(authDefault()) + document.title = 'Reset password - eSim ' + }, [dispatch]) + + const [newPassword, setNewPassword] = useState('') + const [reNewPassword, setReNewPassword] = useState('') + const [showNewPassword, setShowNewPassword] = useState(false) + const [showReNewPassword, setShowReNewPassword] = useState(false) + + const handleClickShowNewPassword = () => setShowNewPassword(!showNewPassword) + const handleMouseDownNewPassword = () => setShowNewPassword(!showNewPassword) + + const handleClickShowReNewPassword = () => setShowReNewPassword(!showReNewPassword) + const handleMouseDownReNewPassword = () => setShowReNewPassword(!showReNewPassword) + + return ( + + + + + + + + Reset password + + + {/* Display's error messages while signing in */} + + {auth.resetPasswordError} + + +
+ + + {showNewPassword ? : } {/* Handle password visibility */} + + + ) + }} + type={showNewPassword ? 'text' : 'password'} + id="newPassword" + value={newPassword} + onChange={e => setNewPassword(e.target.value)} + autoComplete="current-password" + /> + + + {showReNewPassword ? : } {/* Handle password visibility */} + + + ) + }} + type={showReNewPassword ? 'text' : 'password'} + id="reNewPassword" + value={reNewPassword} + onChange={e => setReNewPassword(e.target.value)} + autoComplete="current-password" + /> + + + +
+ +
+ ) +} + +ResetPasswordConfirm.propTypes = { + match: PropTypes.object.isRequired +} diff --git a/eda-frontend/src/pages/ResetPassword/Initiation.js b/eda-frontend/src/pages/ResetPassword/Initiation.js new file mode 100644 index 000000000..f2341852c --- /dev/null +++ b/eda-frontend/src/pages/ResetPassword/Initiation.js @@ -0,0 +1,106 @@ +// User Sign Up / Register page. +import React, { useState, useEffect } from 'react' +import { + Container, + Button, + Typography, + TextField, + Card, + Avatar +} from '@material-ui/core' +import { makeStyles } from '@material-ui/core/styles' +import LockOutlinedIcon from '@material-ui/icons/LockOutlined' +import { useSelector, useDispatch } from 'react-redux' +import { resetPassword, authDefault } from '../../redux/actions/index' + +const useStyles = makeStyles((theme) => ({ + paper: { + marginTop: theme.spacing(20), + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: theme.spacing(3, 5) + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: theme.palette.primary.main + }, + form: { + width: '100%', // Fix IE 11 issue. + marginTop: theme.spacing(1) + }, + submit: { + margin: theme.spacing(1.5, 0) + } +})) + +export default function ResetPassword () { + const classes = useStyles() + + const auth = useSelector(state => state.authReducer) + + const dispatch = useDispatch() + var homeURL = `${window.location.protocol}\\\\${window.location.host}/` + + useEffect(() => { + dispatch(authDefault()) + document.title = 'Reset password - eSim ' + }, [dispatch]) + + const [email, setEmail] = useState('') + + return ( + + + + + + + + Reset password + + + {/* Display's error messages while signing in */} + + {auth.resetPasswordError} + + +
+ setEmail(e.target.value)} + autoFocus + /> + + + +
+ +
+ ) +} diff --git a/eda-frontend/src/pages/Simulator.js b/eda-frontend/src/pages/Simulator.js index 7c4eb4cb2..b1216a205 100644 --- a/eda-frontend/src/pages/Simulator.js +++ b/eda-frontend/src/pages/Simulator.js @@ -3,7 +3,7 @@ import { Container, Grid, Button, Paper, Typography, Switch, FormControlLabel } import { makeStyles } from '@material-ui/core/styles' import Editor from '../components/Simulator/Editor' import textToFile from '../components/Simulator/textToFile' -import SimulationScreen from '../components/Simulator/SimulationScreen' +import SimulationScreen from '../components/Shared/SimulationScreen' import { useDispatch } from 'react-redux' import { setResultGraph, setResultText } from '../redux/actions/index' @@ -31,6 +31,7 @@ export default function Simulator () { checkedA: false }) + const [taskId, setTaskId] = useState(null) useEffect(() => { document.title = 'Simulator - eSim ' @@ -57,8 +58,26 @@ export default function Simulator () { } const netlistCodeSanitization = (code) => { - var cleanCode = code.replace('plot', 'print') - + var codeArray = code.split('\n') + var cleanCode = '' + var frontPlot = '' + for (var line = 0; line < codeArray.length; line++) { + if (codeArray[line].includes('plot')) { + frontPlot += codeArray[line].split('plot ')[1] + ' ' + } + } + frontPlot = `print ${frontPlot} > data.txt \n` + var flag = 0 + for (var i = 0; i < codeArray.length; i++) { + if (codeArray[i].includes('plot')) { + if (!flag) { + cleanCode += frontPlot + flag = 1 + } + } else { + cleanCode += codeArray[i] + '\n' + } + } return cleanCode } @@ -81,10 +100,12 @@ export default function Simulator () { } function sendNetlist (file) { + setIsResult(false) netlistConfig(file) .then((response) => { const res = response.data const getUrl = 'simulation/status/'.concat(res.details.task_id) + setTaskId(res.details.task_id) simulationResult(getUrl) }) .catch(function (error) { @@ -105,7 +126,6 @@ export default function Simulator () { if (result === null) { setIsResult(false) } else { - setIsResult(true) var temp = res.data.details.data var data = result.data @@ -120,9 +140,9 @@ export default function Simulator () { // labels for (var x = 1; x < lab.length; x++) { - if (lab[x].includes('#branch')) { - lab[x] = `I (${lab[x].replace('#branch', '')})` - } + // if (lab[x].includes('#branch')) { + // lab[x] = `I (${lab[x].replace('#branch', '')})` + // } // uncomment below if you want label like V(r1.1) but it will break the graph showing time as well // else { // lab[x] = `V (${lab[x]})` @@ -147,8 +167,11 @@ export default function Simulator () { for (let i = 0; i < temp.length; i++) { let postfixUnit = '' if (temp[i][0].includes('#branch')) { - temp[i][0] = `I(${temp[i][0].replace('#branch', '')})` postfixUnit = 'A' + } else if (temp[i][0].includes('transfer_function')) { + postfixUnit = '' + } else if (temp[i][0].includes('impedance')) { + postfixUnit = 'Ohm' } else { temp[i][0] = `V(${temp[i][0]})` postfixUnit = 'V' @@ -159,6 +182,7 @@ export default function Simulator () { // handleSimulationResult(res.data.details) dispatch(setResultText(simResultText)) } + setIsResult(true) } } }) @@ -170,7 +194,7 @@ export default function Simulator () { return ( - + (dispatch) => { + dispatch({ + type: actions.CHANGE_PASSWORD_FAILED, + payload: { + data: message + } + }) +} + +export const changePassword = (oldPassword, newPassword, reNewPassword) => (dispatch, getState) => { + const body = { + current_password: oldPassword, + new_password: newPassword, + re_new_password: reNewPassword + } + + // add headers + const config = { + headers: { + 'Content-Type': 'application/json' + } + } + + const token = getState().authReducer.token + + if (token) { + config.headers.Authorization = `Token ${token}` + } + + api.post('auth/users/set_password/', body, config) + .then((res) => { + if (res.status >= 200 || res.status < 304) { + dispatch({ + type: actions.CHANGE_PASSWORD_SUCCESS, + payload: { + data: 'The password has been changed successfully.' + } + }) + setTimeout(() => { + window.location.reload() + }, 2000) + } + }) + .catch((err) => { + var res = err.response + if ([400, 401, 403, 304].includes(res.status)) { + // eslint-disable-next-line camelcase + const { new_password, re_new_password, current_password, non_field_errors } = res.data + const defaultErrors = ['Password change failed.'] + // eslint-disable-next-line camelcase + var message = (current_password || new_password || non_field_errors || re_new_password || defaultErrors)[0] + + dispatch(changePasswordError(message)) + } + }) +} diff --git a/eda-frontend/src/redux/actions/actions.js b/eda-frontend/src/redux/actions/actions.js index d13db51a2..069f7a103 100644 --- a/eda-frontend/src/redux/actions/actions.js +++ b/eda-frontend/src/redux/actions/actions.js @@ -1,5 +1,12 @@ // Actions for schematic editor export const FETCH_LIBRARIES = 'FETCH_LIBRARIES' +export const FETCH_LIBRARY = 'FETCH_LIBRARY' +export const REMOVE_LIBRARY = 'REMOVE_LIBRARY' +export const FETCH_ALL_LIBRARIES = 'FETCH_ALL_LIBRARIES' +export const FETCH_CUSTOM_LIBRARIES = 'FETCH_CUSTOM_LIBRARIES' +export const DELETE_LIBRARY = 'DELETE_LIBRARY' +export const UPLOAD_LIBRARIES = 'UPLOAD_LIBRARIES' +export const RESET_UPLOAD_SUCCESS = 'RESET_UPLOAD_SUCCESS' export const TOGGLE_COLLAPSE = 'TOGGLE_COLLAPSE' export const FETCH_COMPONENTS = 'FETCH_COMPONENTS' export const TOGGLE_SIMULATE = 'TOGGLE_SIMULATE' @@ -32,6 +39,10 @@ export const LOADING_FAILED = 'LOADING_FAILED' export const SIGNUP_SUCCESSFUL = 'SIGNUP_SUCCESSFUL' export const SIGNUP_FAILED = 'SIGNUP_FAILED' export const DEFAULT_STORE = 'DEFAULT_STORE' +export const RESET_PASSWORD_SUCCESSFUL = 'RESET_PASSWORD_SUCCESSFUL' +export const RESET_PASSWORD_FAILED = 'RESET_PASSWORD_FAILED' +export const RESET_PASSWORD_CONFIRM_SUCCESSFUL = 'RESET_PASSWORD_CONFIRM_SUCCESSFUL' +export const RESET_PASSWORD_CONFIRM_FAILED = 'RESET_PASSWORD_CONFIRM_FAILED' // Actions for saving scheamtics and loading saved, gallery and local schematics. export const SAVE_SCHEMATICS = 'SAVE_SCHEMATICS' @@ -45,3 +56,7 @@ export const LOAD_GALLERY = 'LOAD_GALLERY' // Action for fetching on-cloud saved schematics for authenticated user to display in dashboard export const FETCH_SCHEMATICS = 'FETCH_SCHEMATICS' + +// Actions for accounts page +export const CHANGE_PASSWORD_SUCCESS = 'CHANGE_PASSWORD_SUCCESS' +export const CHANGE_PASSWORD_FAILED = 'CHANGE_PASSWORD_FAILED' diff --git a/eda-frontend/src/redux/actions/authActions.js b/eda-frontend/src/redux/actions/authActions.js index 962175c1e..39d83232f 100644 --- a/eda-frontend/src/redux/actions/authActions.js +++ b/eda-frontend/src/redux/actions/authActions.js @@ -55,7 +55,7 @@ export const loadUser = () => (dispatch, getState) => { }) } -// Handel api call for user login +// Handle api call for user login export const login = (username, password, toUrl) => { const body = { password: password, @@ -76,6 +76,7 @@ export const login = (username, password, toUrl) => { dispatch(loadUser()) } else { window.open(toUrl, '_self') + localStorage.setItem('ard_redurl', '') } } else if (res.status === 400 || res.status === 403 || res.status === 401) { dispatch({ @@ -104,7 +105,7 @@ export const login = (username, password, toUrl) => { } } -// Handel api call for user sign up +// Handle api call for user sign up export const signUp = (email, username, password, history) => (dispatch) => { const body = { email: email, @@ -136,16 +137,18 @@ export const signUp = (email, username, password, history) => (dispatch) => { if (res.status === 400 || res.status === 403 || res.status === 401) { if (res.data.username !== undefined) { if (res.data.username[0].search('already') !== -1 && res.data.username[0].search('exists') !== -1) { dispatch(signUpError('Username Already Taken.')) } + } else if (res.data.password !== undefined) { + dispatch(signUpError(res.data.password)) } else { - dispatch(signUpError('Enter Valid Credentials.')) + dispatch(signUpError(res.data.email)) } } else { - dispatch(signUpError('Something went wrong! Registeration Failed')) + dispatch(signUpError('Something went wrong! Registration Failed')) } }) } -// Handel api call for user logout +// Handle api call for user logout export const logout = (history) => (dispatch, getState) => { // Get token from localstorage const token = getState().authReducer.token @@ -204,6 +207,26 @@ const signUpError = (message) => (dispatch) => { }) } +// Redux action for display reset password error +const resetPasswordError = (message) => (dispatch) => { + dispatch({ + type: actions.RESET_PASSWORD_FAILED, + payload: { + data: message + } + }) +} + +// Redux action for display reset password confirmation error +const resetPasswordConfirmError = (message) => (dispatch) => { + dispatch({ + type: actions.RESET_PASSWORD_CONFIRM_FAILED, + payload: { + data: message + } + }) +} + // Api call for Google oAuth login or sign up export const googleLogin = (host, toUrl) => { return function (dispatch) { @@ -232,3 +255,88 @@ export const googleLogin = (host, toUrl) => { }) } } + +// Handles api call for user's password recovery +export const resetPassword = (email) => (dispatch) => { + const body = { + email: email + } + + // add headers + const config = { + headers: { + 'Content-Type': 'application/json' + } + } + + api.post('auth/users/reset_password/', body, config) + .then((res) => { + if (res.status >= 200 || res.status < 304) { + dispatch({ + type: actions.RESET_PASSWORD_SUCCESSFUL, + payload: { + data: 'The password reset link has been sent to your email account.' + } + }) + setTimeout(() => { + window.location.href = '/eda/#/login' + }, 2000) + // history.push('/login') + } + }) + .catch((err) => { + var res = err.response + if ([400, 401, 403, 304].includes(res.status)) { + dispatch(resetPasswordError(res.data)) + } + }) +} + +// Handles api call for user's password reset confirmation +export const resetPasswordConfirm = (uid, token, newPassword, reNewPassword) => (dispatch) => { + const body = { + uid: uid, + token: token, + new_password: newPassword, + re_new_password: reNewPassword + } + + // add headers + const config = { + headers: { + 'Content-Type': 'application/json' + } + } + + api.post('auth/users/reset_password_confirm/', body, config) + .then((res) => { + if (res.status >= 200 || res.status < 304) { + dispatch({ + type: actions.RESET_PASSWORD_CONFIRM_SUCCESSFUL, + payload: { + data: 'The password has been reset successfully.' + } + }) + setTimeout(() => { + window.location.href = '/eda/#/login' + }, 2000) + } + }) + .catch((err) => { + var res = err.response + if ([400, 401, 403, 304].includes(res.status)) { + // eslint-disable-next-line camelcase + const { new_password, re_new_password, non_field_errors, token } = res.data + const defaultErrors = ['Password reset failed.'] + // eslint-disable-next-line camelcase + var message = (new_password || re_new_password || non_field_errors || defaultErrors)[0] + + if (token) { + // Override message if it's a token error + message = 'Either the password has already been changed or you have the incorrect URL' + } + + dispatch(resetPasswordConfirmError(message)) + } + }) +} diff --git a/eda-frontend/src/redux/actions/index.js b/eda-frontend/src/redux/actions/index.js index b13059d39..e86c6a55e 100644 --- a/eda-frontend/src/redux/actions/index.js +++ b/eda-frontend/src/redux/actions/index.js @@ -6,3 +6,4 @@ export * from './simulationActions' export * from './authActions' export * from './saveSchematicActions' export * from './dashboardActions' +export * from './accountActions' diff --git a/eda-frontend/src/redux/actions/saveSchematicActions.js b/eda-frontend/src/redux/actions/saveSchematicActions.js index 13f01ea6d..f3fb1a798 100644 --- a/eda-frontend/src/redux/actions/saveSchematicActions.js +++ b/eda-frontend/src/redux/actions/saveSchematicActions.js @@ -4,6 +4,7 @@ import api from '../../utils/Api' import GallerySchSample from '../../utils/GallerySchSample' import { renderGalleryXML } from '../../components/SchematicEditor/Helper/ToolbarTools' import { setTitle } from './index' +import { fetchLibrary, removeLibrary } from './schematicEditorActions' export const setSchTitle = (title) => (dispatch) => { dispatch({ @@ -34,11 +35,15 @@ export const setSchXmlData = (xmlData) => (dispatch) => { // Api call to save new schematic or updating saved schematic. export const saveSchematic = (title, description, xml, base64) => (dispatch, getState) => { + var libraries = [] + getState().schematicEditorReducer.libraries.forEach(e => { libraries.push(e.id) }) + console.log(libraries) const body = { data_dump: xml, base64_image: base64, name: title, - description: description + description: description, + esim_libraries: JSON.stringify([...libraries]) } // Get token from localstorage @@ -113,6 +118,10 @@ export const fetchSchematic = (saveId) => (dispatch, getState) => { dispatch(setSchDescription(res.data.description)) dispatch(setSchXmlData(res.data.data_dump)) renderGalleryXML(res.data.data_dump) + if (res.data.esim_libraries.length > 0) { + getState().schematicEditorReducer.libraries.forEach(e => dispatch(removeLibrary(e.id))) + res.data.esim_libraries.forEach(e => dispatch(fetchLibrary(e.id))) + } } ) .catch((err) => { console.error(err) }) diff --git a/eda-frontend/src/redux/actions/schematicEditorActions.js b/eda-frontend/src/redux/actions/schematicEditorActions.js index 35b99bf13..bcaecdeb3 100644 --- a/eda-frontend/src/redux/actions/schematicEditorActions.js +++ b/eda-frontend/src/redux/actions/schematicEditorActions.js @@ -1,8 +1,9 @@ import api from '../../utils/Api' import * as actions from './actions' +import store from '../store' // Api call for fetching component library list -export const fetchLibraries = () => (dispatch) => { +export const fetchLibraries = () => (dispatch, getState) => { // SAMPLE Response from API // [ // { @@ -10,19 +11,155 @@ export const fetchLibraries = () => (dispatch) => { // "library_name": "Analog.lib", // "saved_on": "2020-05-19T14:06:02.351977Z" // }, -// ] -- Multiple dicts in array - api.get('libraries/') - .then( - (res) => { - dispatch({ - type: actions.FETCH_LIBRARIES, - payload: res.data - }) - } - ) +// ] -- Multiple objects in array + const token = store.getState().authReducer.token + const config = { + headers: { + 'Content-Type': 'application/json' + } + } + if (token) { config.headers.Authorization = `Token ${token}` } + + api.get('libraries/default', config).then((res) => { + dispatch({ + type: actions.FETCH_LIBRARIES, + payload: res.data + }) + }) + .catch((err) => { console.error(err) }) +} + +export const fetchAllLibraries = () => (dispatch) => { + // SAMPLE Response from API + // [ + // { + // "id": 1 + // "library_name": "Analog.lib", + // "saved_on": "2020-05-19T14:06:02.351977Z" + // }, + // ] -- Multiple objects in array + const token = store.getState().authReducer.token + const config = { + headers: { + 'Content-Type': 'application/json' + } + } + if (token) { config.headers.Authorization = `Token ${token}` } + + api.get('libraries/', config).then((res) => { + dispatch({ + type: actions.FETCH_ALL_LIBRARIES, + payload: res.data + }) + }) .catch((err) => { console.error(err) }) } +export const fetchCustomLibraries = () => (dispatch) => { + // SAMPLE Response from API + // [ + // { + // "id": 1 + // "library_name": "Analog.lib", + // "saved_on": "2020-05-19T14:06:02.351977Z" + // }, + // ] -- Multiple objects in array + const token = store.getState().authReducer.token + const config = { + headers: { + 'Content-Type': 'application/json' + } + } + if (token) { config.headers.Authorization = `Token ${token}` } + + api.get('libraries/get_custom_libraries', config).then((res) => { + if (res.data.length > 0) { + dispatch({ + type: actions.FETCH_CUSTOM_LIBRARIES, + payload: res.data + }) + } + }) + .catch((err) => { console.error(err) }) +} + +export const fetchLibrary = (libraryId) => (dispatch) => { + // SAMPLE Response from API + // { + // "library_name": "Motor.lib", + // "saved_on": "2021-05-10T20:29:01.794498Z", + // "id": 363 + // } -- Single Object + const token = store.getState().authReducer.token + const config = { + headers: { + 'Content-Type': 'application/json' + } + } + if (token) { config.headers.Authorization = `Token ${token}` } + + api.get(`libraries/${libraryId}`, config).then(res => { + dispatch({ + type: actions.FETCH_LIBRARY, + payload: res.data + }) + }) +} + +export const removeLibrary = (libraryId) => (dispatch) => { + dispatch({ + type: actions.REMOVE_LIBRARY, + payload: libraryId + }) +} + +export const deleteLibrary = (libraryId) => (dispatch) => { + const token = store.getState().authReducer.token + const config = { + headers: { + Authorization: `Token ${token}` + } + } + api.delete(`libraries/${libraryId}/`, config).then( + dispatch({ + type: actions.DELETE_LIBRARY, + payload: libraryId + }) + ).catch(err => { + console.log(err) + }) +} + +// API call to save uploaded libraries +export const uploadLibrary = (formData) => (dispatch) => { + const token = store.getState().authReducer.token + const config = { + headers: { + Authorization: `Token ${token}` + } + } + api.post('/library-sets/', formData, config).then(res => { + dispatch({ + type: actions.UPLOAD_LIBRARIES, + payload: res.status + }) + }) + .catch(err => { + console.log(err) + console.log(err.response.status) + dispatch({ + type: actions.UPLOAD_LIBRARIES, + payload: err.response.status + }) + }) +} + +export const resetUploadSuccess = () => (dispatch) => { + dispatch({ + type: actions.RESET_UPLOAD_SUCCESS + }) +} + // Api call for fetching components under specified library id export const fetchComponents = (libraryId) => (dispatch) => { // SAMPLE Response from API @@ -55,9 +192,16 @@ export const fetchComponents = (libraryId) => (dispatch) => { // }, // ] // }, -// ] -- Multiple dicts in array +// ] -- Multiple objects in array + const token = store.getState().authReducer.token + const config = { + headers: { + 'Content-Type': 'application/json' + } + } + if (token) { config.headers.Authorization = `Token ${token}` } const url = 'components/?component_library=' + parseInt(libraryId) - api.get(url) + api.get(url, config) .then( (res) => { dispatch({ diff --git a/eda-frontend/src/redux/reducers/accountReducer.js b/eda-frontend/src/redux/reducers/accountReducer.js new file mode 100644 index 000000000..bf110a85b --- /dev/null +++ b/eda-frontend/src/redux/reducers/accountReducer.js @@ -0,0 +1,29 @@ +import * as actions from '../actions/actions' + +const initialState = { + changePasswordSuccess: false, + changePasswordError: '' +} + +export default function (state = initialState, action) { + switch (action.type) { + case actions.CHANGE_PASSWORD_SUCCESS: { + return { + ...state, + changePasswordSuccess: true, + changePasswordError: action.payload.data + } + } + + case actions.CHANGE_PASSWORD_FAILED: { + return { + ...state, + changePasswordSuccess: false, + changePasswordError: action.payload.data + } + } + + default: + return state + } +} diff --git a/eda-frontend/src/redux/reducers/authReducer.js b/eda-frontend/src/redux/reducers/authReducer.js index 1d9bbd14d..cbf0ce88c 100644 --- a/eda-frontend/src/redux/reducers/authReducer.js +++ b/eda-frontend/src/redux/reducers/authReducer.js @@ -7,7 +7,10 @@ const initialState = { isLoading: false, user: null, errors: '', - regErrors: '' + regErrors: '', + resetPasswordSuccess: false, + resetPasswordError: '', + resetPasswordConfirmSuccess: false } export default function (state = initialState, action) { @@ -85,6 +88,38 @@ export default function (state = initialState, action) { } } + case actions.RESET_PASSWORD_SUCCESSFUL: { + return { + ...state, + resetPasswordSuccess: true, + resetPasswordError: action.payload.data + } + } + + case actions.RESET_PASSWORD_FAILED: { + return { + ...state, + resetPasswordSuccess: false, + resetPasswordError: action.payload.data + } + } + + case actions.RESET_PASSWORD_CONFIRM_SUCCESSFUL: { + return { + ...state, + resetPasswordConfirmSuccess: true, + resetPasswordError: action.payload.data + } + } + + case actions.RESET_PASSWORD_CONFIRM_FAILED: { + return { + ...state, + resetPasswordConfirmSuccess: false, + resetPasswordError: action.payload.data + } + } + default: return state } diff --git a/eda-frontend/src/redux/reducers/index.js b/eda-frontend/src/redux/reducers/index.js index a694d2b28..b3383e903 100644 --- a/eda-frontend/src/redux/reducers/index.js +++ b/eda-frontend/src/redux/reducers/index.js @@ -6,6 +6,7 @@ import simulationReducer from './simulationReducer' import authReducer from './authReducer' import saveSchematicReducer from './saveSchematicReducer' import dashboardReducer from './dashboardReducer' +import accountReducer from './accountReducer' export default combineReducers({ schematicEditorReducer, componentPropertiesReducer, @@ -13,5 +14,6 @@ export default combineReducers({ simulationReducer, authReducer, saveSchematicReducer, - dashboardReducer + dashboardReducer, + accountReducer }) diff --git a/eda-frontend/src/redux/reducers/schematicEditorReducer.js b/eda-frontend/src/redux/reducers/schematicEditorReducer.js index 38df2f326..5254a7ae5 100644 --- a/eda-frontend/src/redux/reducers/schematicEditorReducer.js +++ b/eda-frontend/src/redux/reducers/schematicEditorReducer.js @@ -10,7 +10,7 @@ const InitialState = { export default function (state = InitialState, action) { switch (action.type) { case actions.FETCH_LIBRARIES: { - // Add 'open' parameter to track open/close state of collapse + // Add 'open' parameter to track open/close state of collapse const collapse = {} const components = {} action.payload.forEach(element => { @@ -27,7 +27,6 @@ export default function (state = InitialState, action) { return accObj }, {}) newCollapse[action.payload.id] = !existingState - // console.log('Updating collapse', action.payload.id) Object.assign(state.collapse, newCollapse) return { ...state, collapse: { ...state.collapse, newCollapse } } } @@ -44,6 +43,65 @@ export default function (state = InitialState, action) { return { ...state, isSimulate: !state.isSimulate } } + case actions.FETCH_ALL_LIBRARIES: { + const components = { ...state.components } + var allLibraries = action.payload + allLibraries.forEach(e => { + if (!components[e.id]) { components[e.id] = [] } + }) + return { ...state, allLibraries: allLibraries, components: components } + } + + case actions.FETCH_CUSTOM_LIBRARIES: { + const allComponents = {} + action.payload.forEach(e => { + allComponents[e.id] = [] + }) + return { ...state, customLibraries: action.payload } + } + + case actions.FETCH_LIBRARY: { + const components = { ...state.components } + components[action.payload.id] = [] + const collapse = { ...state.collapse } + const libraries = [...state.libraries] + const newLib = action.payload + collapse[newLib.id] = false + if (!libraries.some(e => e.id === newLib.id)) { libraries.push(newLib) } + return { ...state, libraries: libraries, components: components, collapse: collapse } + } + + case actions.REMOVE_LIBRARY: { + var libraries = [...state.libraries] + const filterFunc = (element) => { + return element.id !== action.payload + } + libraries = libraries.filter(filterFunc) + return { ...state, libraries: libraries } + } + + case actions.DELETE_LIBRARY: { + const filterFunc = (element) => { + return element.id !== action.payload + } + var newLibraries = [...state.libraries] + newLibraries = newLibraries.filter(filterFunc) + const allComponents = { ...state.components } + const allLibraries = [...state.allLibraries].filter(filterFunc) + delete allComponents[action.payload] + return { ...state, libraries: newLibraries, allLibraries: allLibraries, components: allComponents } + } + + case actions.UPLOAD_LIBRARIES: { + if (action.payload === 201) { + return { ...state, uploadSuccess: true } + } else { return { ...state, uploadSuccess: false } } + } + + case actions.RESET_UPLOAD_SUCCESS: { + return { ...state, uploadSuccess: null } + } + default: return state } diff --git a/eda-frontend/src/redux/store.js b/eda-frontend/src/redux/store.js index 1ca6cedaa..0f882497d 100644 --- a/eda-frontend/src/redux/store.js +++ b/eda-frontend/src/redux/store.js @@ -3,6 +3,9 @@ import reducer from './reducers/index' import { createStore, applyMiddleware } from 'redux' import reduxThunk from 'redux-thunk' const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore) -const store = createStoreWithMiddleware(reducer) +const store = createStoreWithMiddleware( + reducer, + window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() +) export default store diff --git a/eda-frontend/src/static/gallery/gallery6.png b/eda-frontend/src/static/gallery/gallery6.png new file mode 100644 index 000000000..589d22590 Binary files /dev/null and b/eda-frontend/src/static/gallery/gallery6.png differ diff --git a/eda-frontend/src/utils/GallerySchSample.js b/eda-frontend/src/utils/GallerySchSample.js index 768ddf473..4ee631553 100644 --- a/eda-frontend/src/utils/GallerySchSample.js +++ b/eda-frontend/src/utils/GallerySchSample.js @@ -3,7 +3,7 @@ const GallerySchSample = [ { save_id: 'gallery0', - data_dump: '', + data_dump: '', name: 'Voltage Divider', description: 'A voltage divider is a simple circuit which turns a large voltage into a smaller one. Using just two series resistors and an input voltage.', media: 'gallery0.png', @@ -11,7 +11,7 @@ const GallerySchSample = [ }, { save_id: 'gallery1', - data_dump: '', + data_dump: '', name: 'RC Circuit', description: 'An RC circuit is a circuit with both a resistor (R) and a capacitor (C). RC circuits are freqent element in electronic devices.', media: 'gallery1.png', @@ -19,7 +19,7 @@ const GallerySchSample = [ }, { save_id: 'gallery2', - data_dump: '', + data_dump: '', name: 'Dual RC Ladder', description: 'This is an dual RC ladder circuit with Passive components. The input is a voltage waveform (a pulse) versus time, and the output is a waveform as well. ', media: 'gallery2.png', @@ -27,7 +27,7 @@ const GallerySchSample = [ }, { save_id: 'gallery3', - data_dump: '', + data_dump: '', name: 'Bipolar Amplifier', description: 'A basic BJT amplifier has a very high gain that may vary widely from one transistor to the next. A NPN bipolar transistor is the used as amplifying device.', media: 'gallery3.png', @@ -35,7 +35,7 @@ const GallerySchSample = [ }, { save_id: 'gallery4', - data_dump: '', + data_dump: '', name: 'Shunt Clipper', description: 'A Clipper circuit in which the diode is connected in shunt to the input signal and that attenuates the positive portions of the waveform, is termed as Positive Shunt Clipper.', media: 'gallery4.png', @@ -43,11 +43,19 @@ const GallerySchSample = [ }, { save_id: 'gallery5', - data_dump: '', + data_dump: '', name: 'RC Circuit ( Parallel )', description: 'An RC circuit is a circuit with both a resistor (R) and a capacitor (C). RC circuits are freqent element in electronic devices.', media: 'gallery5.png', shared: true + }, + { + save_id: 'gallery6', + data_dump: '', + name: 'AstableMultivibrator', + description: 'A multivibrator is an electronic circuit used to implement a variety of simple two-state devices such as relaxation oscillators, timers and flip-flops. It consists of two amplifying devices cross-coupled by resistors or capacitors.', + media: 'gallery6.png', + shared: true } ] diff --git a/esim-cloud-backend/.gitignore b/esim-cloud-backend/.gitignore index 64d2cdbaa..65e778afb 100644 --- a/esim-cloud-backend/.gitignore +++ b/esim-cloud-backend/.gitignore @@ -5,3 +5,4 @@ static/ symbols/* .DS_Store symbol_svgs +kicad-symbols/* \ No newline at end of file diff --git a/esim-cloud-backend/authAPI/admin.py b/esim-cloud-backend/authAPI/admin.py index 8c38f3f3d..3757febee 100644 --- a/esim-cloud-backend/authAPI/admin.py +++ b/esim-cloud-backend/authAPI/admin.py @@ -1,3 +1,6 @@ from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from .models import User -# Register your models here. + +admin.site.register(User, UserAdmin) diff --git a/esim-cloud-backend/authAPI/models.py b/esim-cloud-backend/authAPI/models.py index 71a836239..4549012ea 100644 --- a/esim-cloud-backend/authAPI/models.py +++ b/esim-cloud-backend/authAPI/models.py @@ -1,3 +1,7 @@ from django.db import models - +from django.contrib.auth.models import AbstractUser # Create your models here. + + +class User(AbstractUser): + email = models.EmailField(unique=True) diff --git a/esim-cloud-backend/esimCloud/settings.py b/esim-cloud-backend/esimCloud/settings.py index 0886d7b56..3372df5db 100644 --- a/esim-cloud-backend/esimCloud/settings.py +++ b/esim-cloud-backend/esimCloud/settings.py @@ -44,6 +44,7 @@ 'rest_framework', 'rest_framework.authtoken', 'social_django', + 'inline_actions', 'djoser', 'simulationAPI', 'authAPI', @@ -85,7 +86,7 @@ WSGI_APPLICATION = 'esimCloud.wsgi.application' - +AUTH_USER_MODEL = 'authAPI.User' # Database config Defaults to sqlite3 if not provided in environment files DATABASES = { @@ -163,12 +164,14 @@ DJOSER = { 'SEND_ACTIVATION_EMAIL': True, - 'PASSWORD_RESET_CONFIRM_URL': '#/password/reset/confirm/{uid}/{token}', + 'PASSWORD_RESET_CONFIRM_URL': 'eda/#/password/reset/confirm/{uid}/{token}', + 'PASSWORD_RESET_CONFIRM_RETYPE': True, + 'SET_PASSWORD_RETYPE': True, # 'USERNAME_RESET_CONFIRM_URL': '#/username/reset/confirm/{uid}/{token}', 'ACTIVATION_URL': 'api/auth/users/activate/{uid}/{token}', 'SOCIAL_AUTH_ALLOWED_REDIRECT_URIS': ["http://localhost:8000/api/auth/google-callback", "http://localhost/api/auth/google-callback", GOOGLE_OAUTH_REDIRECT_URI], # noqa - 'SOCIAL_AUTH_TOKEN_STRATEGY': 'authAPI.token.TokenStrategy' - # 'LOGIN_FIELD': 'email' For using email only + 'SOCIAL_AUTH_TOKEN_STRATEGY': 'authAPI.token.TokenStrategy', + 'PASSWORD_RESET_SHOW_EMAIL_NOT_FOUND': True } REST_FRAMEWORK = { @@ -204,15 +207,14 @@ STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' STATIC_URL = '/django_static/' - -# noqa For Netlist handling netlist uploads and other temp uploads -MEDIA_URL = '/_files/' -MEDIA_ROOT = os.path.join("/tmp", "esimCloud-temp") - # File Storage FILE_STORAGE_ROOT = os.path.join(BASE_DIR, 'file_storage') FILE_STORAGE_URL = '/files' +# noqa For Netlist handling netlist uploads and other temp uploads +MEDIA_URL = '/files/' +MEDIA_ROOT = os.path.join(BASE_DIR, "file_storage") + # celery CELERY_BROKER_URL = 'redis://redis:6379' CELERY_RESULT_BACKEND = 'redis://redis:6379' diff --git a/esim-cloud-backend/kicad-symbols/4xxx.dcm b/esim-cloud-backend/kicad-symbols/additional/4xxx.dcm similarity index 100% rename from esim-cloud-backend/kicad-symbols/4xxx.dcm rename to esim-cloud-backend/kicad-symbols/additional/4xxx.dcm diff --git a/esim-cloud-backend/kicad-symbols/4xxx.lib b/esim-cloud-backend/kicad-symbols/additional/4xxx.lib similarity index 100% rename from esim-cloud-backend/kicad-symbols/4xxx.lib rename to esim-cloud-backend/kicad-symbols/additional/4xxx.lib diff --git a/esim-cloud-backend/kicad-symbols/Analog.dcm b/esim-cloud-backend/kicad-symbols/additional/Analog.dcm similarity index 100% rename from esim-cloud-backend/kicad-symbols/Analog.dcm rename to esim-cloud-backend/kicad-symbols/additional/Analog.dcm diff --git a/esim-cloud-backend/kicad-symbols/Analog.lib b/esim-cloud-backend/kicad-symbols/additional/Analog.lib similarity index 100% rename from esim-cloud-backend/kicad-symbols/Analog.lib rename to esim-cloud-backend/kicad-symbols/additional/Analog.lib diff --git a/esim-cloud-backend/kicad-symbols/Device.dcm b/esim-cloud-backend/kicad-symbols/additional/Device.dcm similarity index 100% rename from esim-cloud-backend/kicad-symbols/Device.dcm rename to esim-cloud-backend/kicad-symbols/additional/Device.dcm diff --git a/esim-cloud-backend/kicad-symbols/Device.lib b/esim-cloud-backend/kicad-symbols/additional/Device.lib similarity index 100% rename from esim-cloud-backend/kicad-symbols/Device.lib rename to esim-cloud-backend/kicad-symbols/additional/Device.lib diff --git a/esim-cloud-backend/kicad-symbols/Diode.dcm b/esim-cloud-backend/kicad-symbols/additional/Diode.dcm similarity index 100% rename from esim-cloud-backend/kicad-symbols/Diode.dcm rename to esim-cloud-backend/kicad-symbols/additional/Diode.dcm diff --git a/esim-cloud-backend/kicad-symbols/Diode.lib b/esim-cloud-backend/kicad-symbols/additional/Diode.lib similarity index 100% rename from esim-cloud-backend/kicad-symbols/Diode.lib rename to esim-cloud-backend/kicad-symbols/additional/Diode.lib diff --git a/esim-cloud-backend/kicad-symbols/LED.dcm b/esim-cloud-backend/kicad-symbols/additional/LED.dcm similarity index 100% rename from esim-cloud-backend/kicad-symbols/LED.dcm rename to esim-cloud-backend/kicad-symbols/additional/LED.dcm diff --git a/esim-cloud-backend/kicad-symbols/LED.lib b/esim-cloud-backend/kicad-symbols/additional/LED.lib similarity index 100% rename from esim-cloud-backend/kicad-symbols/LED.lib rename to esim-cloud-backend/kicad-symbols/additional/LED.lib diff --git a/esim-cloud-backend/kicad-symbols/Motor.dcm b/esim-cloud-backend/kicad-symbols/additional/Motor.dcm similarity index 100% rename from esim-cloud-backend/kicad-symbols/Motor.dcm rename to esim-cloud-backend/kicad-symbols/additional/Motor.dcm diff --git a/esim-cloud-backend/kicad-symbols/Motor.lib b/esim-cloud-backend/kicad-symbols/additional/Motor.lib similarity index 100% rename from esim-cloud-backend/kicad-symbols/Motor.lib rename to esim-cloud-backend/kicad-symbols/additional/Motor.lib diff --git a/esim-cloud-backend/kicad-symbols/Oscillator.dcm b/esim-cloud-backend/kicad-symbols/additional/Oscillator.dcm similarity index 100% rename from esim-cloud-backend/kicad-symbols/Oscillator.dcm rename to esim-cloud-backend/kicad-symbols/additional/Oscillator.dcm diff --git a/esim-cloud-backend/kicad-symbols/Oscillator.lib b/esim-cloud-backend/kicad-symbols/additional/Oscillator.lib similarity index 100% rename from esim-cloud-backend/kicad-symbols/Oscillator.lib rename to esim-cloud-backend/kicad-symbols/additional/Oscillator.lib diff --git a/esim-cloud-backend/kicad-symbols/Transistor_FET.dcm b/esim-cloud-backend/kicad-symbols/additional/Transistor_FET.dcm similarity index 100% rename from esim-cloud-backend/kicad-symbols/Transistor_FET.dcm rename to esim-cloud-backend/kicad-symbols/additional/Transistor_FET.dcm diff --git a/esim-cloud-backend/kicad-symbols/Transistor_FET.lib b/esim-cloud-backend/kicad-symbols/additional/Transistor_FET.lib similarity index 100% rename from esim-cloud-backend/kicad-symbols/Transistor_FET.lib rename to esim-cloud-backend/kicad-symbols/additional/Transistor_FET.lib diff --git a/esim-cloud-backend/kicad-symbols/Transistor_IGBT.dcm b/esim-cloud-backend/kicad-symbols/additional/Transistor_IGBT.dcm similarity index 100% rename from esim-cloud-backend/kicad-symbols/Transistor_IGBT.dcm rename to esim-cloud-backend/kicad-symbols/additional/Transistor_IGBT.dcm diff --git a/esim-cloud-backend/kicad-symbols/Transistor_IGBT.lib b/esim-cloud-backend/kicad-symbols/additional/Transistor_IGBT.lib similarity index 100% rename from esim-cloud-backend/kicad-symbols/Transistor_IGBT.lib rename to esim-cloud-backend/kicad-symbols/additional/Transistor_IGBT.lib diff --git a/esim-cloud-backend/kicad-symbols/Triac_Thyristor.dcm b/esim-cloud-backend/kicad-symbols/additional/Triac_Thyristor.dcm similarity index 100% rename from esim-cloud-backend/kicad-symbols/Triac_Thyristor.dcm rename to esim-cloud-backend/kicad-symbols/additional/Triac_Thyristor.dcm diff --git a/esim-cloud-backend/kicad-symbols/Triac_Thyristor.lib b/esim-cloud-backend/kicad-symbols/additional/Triac_Thyristor.lib similarity index 100% rename from esim-cloud-backend/kicad-symbols/Triac_Thyristor.lib rename to esim-cloud-backend/kicad-symbols/additional/Triac_Thyristor.lib diff --git a/esim-cloud-backend/kicad-symbols/eSim_Hybrid.dcm b/esim-cloud-backend/kicad-symbols/additional/eSim_Hybrid.dcm similarity index 100% rename from esim-cloud-backend/kicad-symbols/eSim_Hybrid.dcm rename to esim-cloud-backend/kicad-symbols/additional/eSim_Hybrid.dcm diff --git a/esim-cloud-backend/kicad-symbols/eSim_Hybrid.lib b/esim-cloud-backend/kicad-symbols/additional/eSim_Hybrid.lib similarity index 100% rename from esim-cloud-backend/kicad-symbols/eSim_Hybrid.lib rename to esim-cloud-backend/kicad-symbols/additional/eSim_Hybrid.lib diff --git a/esim-cloud-backend/kicad-symbols/Transistor_BJT.dcm b/esim-cloud-backend/kicad-symbols/default/Transistor_BJT.dcm similarity index 100% rename from esim-cloud-backend/kicad-symbols/Transistor_BJT.dcm rename to esim-cloud-backend/kicad-symbols/default/Transistor_BJT.dcm diff --git a/esim-cloud-backend/kicad-symbols/Transistor_BJT.lib b/esim-cloud-backend/kicad-symbols/default/Transistor_BJT.lib similarity index 100% rename from esim-cloud-backend/kicad-symbols/Transistor_BJT.lib rename to esim-cloud-backend/kicad-symbols/default/Transistor_BJT.lib diff --git a/esim-cloud-backend/kicad-symbols/eSim_Sources.dcm b/esim-cloud-backend/kicad-symbols/default/eSim_Sources.dcm similarity index 100% rename from esim-cloud-backend/kicad-symbols/eSim_Sources.dcm rename to esim-cloud-backend/kicad-symbols/default/eSim_Sources.dcm diff --git a/esim-cloud-backend/kicad-symbols/eSim_Sources.lib b/esim-cloud-backend/kicad-symbols/default/eSim_Sources.lib similarity index 100% rename from esim-cloud-backend/kicad-symbols/eSim_Sources.lib rename to esim-cloud-backend/kicad-symbols/default/eSim_Sources.lib diff --git a/esim-cloud-backend/kicad-symbols/power.dcm b/esim-cloud-backend/kicad-symbols/default/power.dcm similarity index 100% rename from esim-cloud-backend/kicad-symbols/power.dcm rename to esim-cloud-backend/kicad-symbols/default/power.dcm diff --git a/esim-cloud-backend/kicad-symbols/power.lib b/esim-cloud-backend/kicad-symbols/default/power.lib similarity index 100% rename from esim-cloud-backend/kicad-symbols/power.lib rename to esim-cloud-backend/kicad-symbols/default/power.lib diff --git a/esim-cloud-backend/kicad-symbols/pspice.dcm b/esim-cloud-backend/kicad-symbols/default/pspice.dcm similarity index 100% rename from esim-cloud-backend/kicad-symbols/pspice.dcm rename to esim-cloud-backend/kicad-symbols/default/pspice.dcm diff --git a/esim-cloud-backend/kicad-symbols/pspice.lib b/esim-cloud-backend/kicad-symbols/default/pspice.lib similarity index 100% rename from esim-cloud-backend/kicad-symbols/pspice.lib rename to esim-cloud-backend/kicad-symbols/default/pspice.lib diff --git a/esim-cloud-backend/libAPI/admin.py b/esim-cloud-backend/libAPI/admin.py index 6a1a557be..2fe449a3a 100644 --- a/esim-cloud-backend/libAPI/admin.py +++ b/esim-cloud-backend/libAPI/admin.py @@ -1,5 +1,17 @@ -from django.contrib import admin -from libAPI.models import LibraryComponent, Library +from libAPI.lib_utils import handle_uploaded_libs +from django.contrib import admin, messages +from django.contrib.auth import get_user_model +from libAPI.models import LibraryComponent, \ + Library, \ + LibrarySet, \ + FavouriteComponent +from libAPI.forms import LibrarySetForm +from inline_actions.admin import InlineActionsMixin +from inline_actions.admin import InlineActionsModelAdminMixin +from django.shortcuts import redirect +from django.utils.safestring import mark_safe +from esimCloud import settings +import os @admin.register(LibraryComponent) @@ -18,3 +30,78 @@ class ComponentInline(admin.TabularInline): @admin.register(Library) class LibraryAdmin(admin.ModelAdmin): inlines = (ComponentInline, ) + + +class LibraryInline(InlineActionsMixin, admin.TabularInline): + model = Library + extra = 0 + show_change_link = True + inline_actions = ['toggle_default'] + + def toggle_default(self, request, obj, parent_obj=None): + try: + library_set = LibrarySet.objects.filter( + user=parent_obj.user, + default=not parent_obj.default + )[0] + except IndexError: + library_set = LibrarySet( + name=parent_obj.user.username[0:24], + default=not parent_obj.default, + user=parent_obj.user + ) + library_set.save() + messages.info(request, mark_safe( + f"Library {obj.library_name} moved to '\ + ''\ + '{library_set.name}.")) + obj.library_set = library_set + obj.save() + + def get_toggle_default_label(self, obj): + if obj.library_set.default: + return 'Remove from Defaults' + return 'Add to Defaults' + + +class LibrarySetAdmin(InlineActionsModelAdminMixin, admin.ModelAdmin): + model = LibrarySet + list_display = ('name', 'user', 'default') + inlines = (LibraryInline, ) + + def get_form(self, request, obj=None, **kwargs): + return LibrarySetForm + + def save_model(self, request, obj, form, change): + # For new library set instance + User = get_user_model() + user = User.objects.get(id=request.POST.get('user')) + if obj.pk is None: + obj = LibrarySet( + user=user, + default=True if request.POST.get('default') else False, + name=request.POST.get('name', '')[0:24] + ) + obj.save() + + # If the library set is being changed + else: + obj.save() + + files = request.FILES.getlist('files') + if len(files) != 0: + path = os.path.join( + settings.BASE_DIR[6:], + 'kicad-symbols', + obj.user.username + '-' + obj.name) + handle_uploaded_libs(obj, path, files) # defined in ./lib_utils.py + return redirect('/api/admin/libAPI/libraryset/' + str(obj.id)) + + +admin.site.register(LibrarySet, LibrarySetAdmin) + + +@admin.register(FavouriteComponent) +class FavouriteComponentAdmin(admin.ModelAdmin): + list_display = ('owner', 'last_change') + search_fields = ('owner', 'component') diff --git a/esim-cloud-backend/libAPI/forms.py b/esim-cloud-backend/libAPI/forms.py new file mode 100644 index 000000000..7fe0e29f9 --- /dev/null +++ b/esim-cloud-backend/libAPI/forms.py @@ -0,0 +1,20 @@ +from libAPI.models import LibrarySet +from django import forms + + +class LibrarySetForm(forms.ModelForm): + files = forms.FileField( + required=False, + widget=forms.ClearableFileInput(attrs={'multiple': True}) + ) + + def __init__(self, *args, **kwargs): + super(LibrarySetForm, self).__init__(*args, **kwargs) + instance = getattr(self, 'instance', None) + if instance and instance.pk: + self.fields['user'].widget.attrs['disabled'] = True + self.fields['name'].widget.attrs['readonly'] = True + + class Meta: + model = LibrarySet + fields = ('name', 'user', 'default', 'files') diff --git a/esim-cloud-backend/libAPI/lib_utils.py b/esim-cloud-backend/libAPI/lib_utils.py new file mode 100644 index 000000000..e0808fec9 --- /dev/null +++ b/esim-cloud-backend/libAPI/lib_utils.py @@ -0,0 +1,116 @@ +from libAPI.helper.main import generate_svg_and_save_to_folder +from libAPI.models import Library, LibraryComponent, ComponentAlternate +import os +import glob + + +def save_uploaded_files(files, path): + for f in files: + filepath = os.path.join(path, f._name) + with open(filepath, 'wb') as dest: + for chunk in f.chunks(): + dest.write(chunk) + + +def handle_uploaded_libs(library_set, path, files): + if not os.path.isdir(path): + os.mkdir(path) + save_uploaded_files(files, path) + filenames = [] + for f in files: + filenames.append(f._name) + save_libs(filenames, path, path, library_set) + for f in files: + os.remove(path + '/' + f._name) + + +def save_libs(files, path, out_path, library_set): + for f in files: + if '.dcm' in f: + flag = 0 + for f1 in files: + if f1[:-4] == f[:-4] and '.lib' in f1: + flag = 1 + if flag == 0: + raise FileNotFoundError( + f'.lib file for {f} does not exist') + if '.lib' in f: + lib_output_location = os.path.join(out_path, 'symbol-svgs') + lib_location = os.path.join(path, f) + component_details = generate_svg_and_save_to_folder( + lib_location, + lib_output_location + ) + library = Library.objects.filter( + library_name=f, library_set=library_set).first() + if not library: + library = Library( + library_name=f, + library_set=library_set + ) + library.save() + + library_svg_folder = os.path.join( + lib_output_location, f[:-4]) + thumbnails = glob.glob(library_svg_folder + '/*_thumbnail.svg') + + for component_svg in glob.glob(library_svg_folder + '/*-1-A.svg'): + thumbnail_path = component_svg[:-4] + '_thumbnail.svg' + if thumbnail_path not in thumbnails: + raise FileNotFoundError( + f'Thumbnail does not exist for {component_svg}') + + # Get Component name + component_svg = os.path.split(component_svg)[-1] + + # Get Corresponding Details + svg_desc = component_details[component_svg[:-4]] + + # Seed DB + component = LibraryComponent.objects.filter( + name=svg_desc['name'], + svg_path=os.path.join( + library_svg_folder, component_svg), + thumbnail_path=thumbnail_path, + symbol_prefix=svg_desc['symbol_prefix'], + full_name=svg_desc['full_name'], + keyword=svg_desc['keyword'], + description=svg_desc['description'], + data_link=svg_desc['data_link'], + component_library=library + ).first() + if not component: + component = LibraryComponent( + name=svg_desc['name'], + svg_path=os.path.join( + library_svg_folder, component_svg), + thumbnail_path=thumbnail_path, + symbol_prefix=svg_desc['symbol_prefix'], + full_name=svg_desc['full_name'], + keyword=svg_desc['keyword'], + description=svg_desc['description'], + data_link=svg_desc['data_link'], + component_library=library + ) + component.save() + + # Seed Alternate Components + for component_svg in glob.glob(library_svg_folder + '/*[B-Z].svg'): + component_svg = os.path.split(component_svg)[-1] + svg_desc = component_details[component_svg[:-4]] + alternate_component = ComponentAlternate.objects.filter( + part=svg_desc['part'], dmg=svg_desc['dmg'], + full_name=svg_desc['full_name'], + svg_path=os.path.join( + library_svg_folder, component_svg), + parent_component=component + ).first() + if not alternate_component: + alternate_component = ComponentAlternate( + part=svg_desc['part'], dmg=svg_desc['dmg'], + full_name=svg_desc['full_name'], + svg_path=os.path.join( + library_svg_folder, component_svg), + parent_component=component + ) + alternate_component.save() diff --git a/esim-cloud-backend/libAPI/management/commands/createsuperuser_noinput.py b/esim-cloud-backend/libAPI/management/commands/createsuperuser_noinput.py new file mode 100644 index 000000000..1525249cd --- /dev/null +++ b/esim-cloud-backend/libAPI/management/commands/createsuperuser_noinput.py @@ -0,0 +1,36 @@ +from django.contrib.auth import get_user_model +from django.db.models import Q +from django.core.management.base import BaseCommand +import logging +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = "Create default admin user if not already present." + + def add_arguments(self, parser): + parser.add_argument( + '--username', type=str, + help="username of admin account" + ) + parser.add_argument( + '--password', type=str, + help="password of the admin account" + ) + + def handle(self, *args, **options): + if options['username'] and options['password']: + User = get_user_model() + user = User.objects.filter(username=options['username']) + if user.count() > 0: + raise Exception(f"User with same username exists") + user = User.objects.create_superuser( + username=options['username'], + email='', password=options['password'] + ) + logger.info( + f"Creating user {options['user']}" + " with password {options['password']}") + user.save() + else: + raise Exception("Username or Password not present") diff --git a/esim-cloud-backend/libAPI/management/commands/load_default_libs.py b/esim-cloud-backend/libAPI/management/commands/load_default_libs.py new file mode 100644 index 000000000..8e5d045c4 --- /dev/null +++ b/esim-cloud-backend/libAPI/management/commands/load_default_libs.py @@ -0,0 +1,75 @@ +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand +from libAPI.lib_utils import save_libs +from libAPI.models import LibrarySet +from esimCloud import settings +import os +import logging +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = "Load default libraries if not already present." + + def add_arguments(self, parser): + parser.add_argument( + '--username', + help='input a user\'s username', type=str + ) + parser.add_argument( + '--location', type=self.dir_path, + help="Directory containing kicad library files" + ) + parser.add_argument( + '--default', action='store_true', + help="set if the library is default or not" + ) + + def dir_path(self, path): + if os.path.isdir(path): + return path + else: + raise Exception(f"{path} is not a valid path") + + def handle(self, *args, **options): + User = get_user_model() + + if options['username']: + user = User.objects.get(username=options['username']) + else: + raise Exception("Enter a superuser to associate libs") + name = 'esim-default' if options['default'] else 'esim-additional' + library_set = LibrarySet.objects.filter( + user=user, + default=options['default'], + name=name + ).first() + if not library_set: + library_set = LibrarySet( + user=user, + default=True if options['default'] else False, + name=name + ) + library_set.save() + + out_location = os.path.join( + "kicad-symbols/", + library_set.user.username + "-" + name + ) + + logger.info(f"Reading libraries from {options['location']}") + logger.info(f"Saving as " + name[6:]) + logger.info(f"Saving Libraries to {out_location}") + + if not os.path.isdir(out_location): + os.mkdir(out_location) + try: + save_libs( + os.listdir(options['location']), + options['location'], + out_location, + library_set + ) + logger.info("Finished without errors") + except Exception: + logger.error("Couldn't save all the libs") diff --git a/esim-cloud-backend/libAPI/management/commands/seed_libs.py b/esim-cloud-backend/libAPI/management/commands/seed_libs.py deleted file mode 100644 index bfd548e64..000000000 --- a/esim-cloud-backend/libAPI/management/commands/seed_libs.py +++ /dev/null @@ -1,106 +0,0 @@ -import os -from libAPI.models import Library, LibraryComponent, ComponentAlternate -from django.core.management.base import BaseCommand -from libAPI.helper.main import generate_svg_and_save_to_folder -import logging -import glob -logger = logging.getLogger(__name__) - - -class Command(BaseCommand): - help = "seed database for testing and development." - - def add_arguments(self, parser): - parser.add_argument('--clear', action='store_true', - help="True to clear all libraries from DB") - parser.add_argument('--location', type=self.dir_path, - help="Directory containing kicad library files") - - def dir_path(self, path): - if os.path.isdir(path): - return path - else: - raise Exception(f"{path} is not a valid path") - - def handle(self, *args, **options): - self.stdout.write('seeding data...') - if options['clear']: - self.stdout.write('Deleting Objects') - clear_data() - if not options['location'] and not options['clear']: - raise Exception('Argument location must be provided') - elif not options['clear'] and options['location']: - seed_libraries(self, options['location']) - self.stdout.write('done.') - - -def clear_data(): - """Deletes all the table data""" - Library.objects.all().delete() - LibraryComponent.objects.all().delete() - logger.info("Deleted All libraries and components") - - -def seed_libraries(self, location): - logger.info(f"Reading libraries from {location}") - for file in os.listdir(location): - if '.lib' in file: - self.stdout.write(f'Processing {file}') - lib_location = os.path.join(location, file) - lib_output_location = os.path.join(location, 'symbol_svgs') - component_details = generate_svg_and_save_to_folder( - lib_location, - lib_output_location - ) - library = Library( - library_name=file, - ) - library.save() - logger.info('Created Library Object') - library_svg_folder = os.path.join(lib_output_location, file[:-4]) - thumbnails = glob.glob(library_svg_folder+'/*_thumbnail.svg') - - # Seed Primary component - for component_svg in glob.glob(library_svg_folder+'/*-1-A.svg'): - thumbnail_path = component_svg[:-4]+'_thumbnail.svg' - if thumbnail_path not in thumbnails: - raise FileNotFoundError(f'Thumbnail Does not exist for {component_svg}') # noqa - - # Get Component name - component_svg = os.path.split(component_svg)[-1] - - # Get Corresponding Details - svg_desc = component_details[component_svg[:-4]] - - # Seed DB - component = LibraryComponent( - name=svg_desc['name'], - svg_path=os.path.join( - library_svg_folder, component_svg), - thumbnail_path=thumbnail_path, - symbol_prefix=svg_desc['symbol_prefix'], - full_name=svg_desc['full_name'], - keyword=svg_desc['keyword'], - description=svg_desc['description'], - data_link=svg_desc['data_link'], - component_library=library - ) - component.save() - logger.info(f'Saved component {component_svg}') - - # Seed Alternate Components - for component_svg in glob.glob(library_svg_folder+'/*[B-Z].svg'): # noqa , EdgeCase here - component_svg = os.path.split(component_svg)[-1] - svg_desc = component_details[component_svg[:-4]] - alternate_component = ComponentAlternate( - part=svg_desc['part'], - dmg=svg_desc['dmg'], - full_name=svg_desc['full_name'], - svg_path=os.path.join( - library_svg_folder, component_svg), - parent_component=LibraryComponent.objects.get( - name=svg_desc['name'], - ) - ) - alternate_component.save() - logger.info(f'Saved alternate component {component_svg}') diff --git a/esim-cloud-backend/libAPI/models.py b/esim-cloud-backend/libAPI/models.py index 3a5487438..5245265ce 100644 --- a/esim-cloud-backend/libAPI/models.py +++ b/esim-cloud-backend/libAPI/models.py @@ -1,9 +1,40 @@ - +import os +import shutil from djongo import models from django.utils.safestring import mark_safe +from django.contrib.auth import get_user_model +from django.dispatch import receiver +from django.db.models.signals import post_delete + + +class LibrarySet(models.Model): + user = models.ForeignKey(to=get_user_model(), verbose_name="user", + on_delete=models.CASCADE) + default = models.BooleanField(default=False) + name = models.CharField(max_length=24, default="default") + + class Meta: + unique_together = ('user', 'name') + + +@receiver(post_delete, sender=LibrarySet) +def library_set_post_delete_receiver(sender, instance: LibrarySet, **kwargs): + try: + shutil.rmtree( + os.path.join( + "kicad-symbols/", + instance.user.username + '-' + instance.name + )) + except Exception: + pass class Library(models.Model): + library_set = models.ForeignKey( + LibrarySet, null=True, + verbose_name="library_set", + on_delete=models.CASCADE + ) library_name = models.CharField(max_length=200) saved_on = models.DateTimeField(auto_now=True) @@ -11,6 +42,20 @@ def __str__(self): return self.library_name +@receiver(post_delete, sender=Library) +def library_post_save_receiver(sender, instance: Library, **kwargs): + try: + shutil.rmtree( + os.path.join( + "kicad-symbols/", + instance.library_set.user.username + "-" + + instance.library_set.name, + "symbol-svgs", instance.library_name[:4] + )) + except Exception: + pass + + class LibraryComponent(models.Model): name = models.CharField(max_length=200) svg_path = models.CharField(max_length=400) @@ -35,6 +80,16 @@ def __str__(self): return self.name +@receiver(post_delete, sender=LibraryComponent) +def component_post_delete_receiver( + sender, instance: LibraryComponent, **kwargs): + try: + os.remove(instance.thumbnail_path) + os.remove(instance.svg_path) + except Exception: + pass + + class ComponentAlternate(models.Model): part = models.CharField(max_length=1) dmg = models.PositiveSmallIntegerField() @@ -57,3 +112,10 @@ def image_tag(self): def __str__(self): return self.full_name + + +class FavouriteComponent(models.Model): + owner = models.OneToOneField(to=get_user_model(), + on_delete=models.CASCADE, null=False) + component = models.ManyToManyField(to=LibraryComponent) + last_change = models.DateTimeField(auto_now=True) diff --git a/esim-cloud-backend/libAPI/serializers.py b/esim-cloud-backend/libAPI/serializers.py index 9ad9f9e7d..a304f6b3e 100644 --- a/esim-cloud-backend/libAPI/serializers.py +++ b/esim-cloud-backend/libAPI/serializers.py @@ -1,14 +1,31 @@ import logging from rest_framework import serializers -from libAPI.models import Library, LibraryComponent, ComponentAlternate +from libAPI.models import Library, \ + LibraryComponent, \ + ComponentAlternate, \ + LibrarySet, \ + FavouriteComponent logger = logging.getLogger(__name__) class LibrarySerializer(serializers.ModelSerializer): + default = serializers.SerializerMethodField('is_default') + additional = serializers.SerializerMethodField('is_additional') + + def is_default(self, obj): + if obj.library_set.default: + return True + return False + + def is_additional(self, obj): + if not obj.library_set.default and obj.library_set.user.is_superuser: + return True + return False + class Meta: model = Library - fields = ('library_name', 'saved_on', 'id') + fields = ('library_name', 'saved_on', 'id', 'default', 'additional') class ComponentAlternateSerializer(serializers.ModelSerializer): @@ -42,3 +59,22 @@ class Meta: 'keyword', 'alternate_component' ) + + +class LibrarySetSerializer(serializers.HyperlinkedModelSerializer): + + class Meta: + model = LibrarySet + fields = [ + 'id', + 'default', + 'name', + ] + + +class FavouriteComponentSerializer(serializers.ModelSerializer): + component = LibraryComponentSerializer(many=True) + + class Meta: + model = FavouriteComponent + fields = ("component",) diff --git a/esim-cloud-backend/libAPI/urls.py b/esim-cloud-backend/libAPI/urls.py index 23e34cef3..dec88e81b 100644 --- a/esim-cloud-backend/libAPI/urls.py +++ b/esim-cloud-backend/libAPI/urls.py @@ -1,7 +1,19 @@ -from libAPI.views import LibraryViewSet, LibraryComponentViewSet +from libAPI.views import LibraryViewSet, \ + LibraryComponentViewSet, \ + LibrarySetViewSet, \ + FavouriteComponentView, \ + DeleteFavouriteComponent from rest_framework.routers import DefaultRouter +from django.urls import path router = DefaultRouter() router.register(r'libraries', LibraryViewSet, basename='library') +router.register(r'library-sets', LibrarySetViewSet, basename='library') router.register(r'components', LibraryComponentViewSet, basename='components') -urlpatterns = router.urls +urlpatterns = [ + path("favouritecomponents", FavouriteComponentView.as_view(), + name="favouritecomponents"), + path("favouritecomponents/", DeleteFavouriteComponent.as_view(), + name="favouritecomponents"), +] +urlpatterns += router.urls diff --git a/esim-cloud-backend/libAPI/views.py b/esim-cloud-backend/libAPI/views.py index 368057f19..bf7984773 100644 --- a/esim-cloud-backend/libAPI/views.py +++ b/esim-cloud-backend/libAPI/views.py @@ -1,12 +1,44 @@ +from libAPI.lib_utils import handle_uploaded_libs import django_filters -from libAPI.serializers import LibrarySerializer, LibraryComponentSerializer -from libAPI.models import Library, LibraryComponent -from rest_framework import viewsets +from django.db.models import Q +from django.contrib.auth import get_user_model +from libAPI.serializers import LibrarySerializer, \ + LibraryComponentSerializer, \ + LibrarySetSerializer, \ + FavouriteComponentSerializer +from libAPI.models import Library, \ + LibraryComponent, \ + LibrarySet, \ + FavouriteComponent +from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.permissions import BasePermission,\ + IsAuthenticated,\ + SAFE_METHODS +from rest_framework.parsers import MultiPartParser import logging from django_filters import rest_framework as filters +from drf_yasg.utils import swagger_auto_schema +import os +from esimCloud import settings +from rest_framework.views import APIView logger = logging.getLogger(__name__) +class IsLibraryOwner(BasePermission): + + def has_object_permission(self, request, view, obj): + if request.method in SAFE_METHODS: + return True + if request.user.is_authenticated: + if obj.library_set.user == request.user: + return True + elif request.user.is_superuser: + return True + return False + + class LibraryFilterSet(django_filters.FilterSet): class Meta: model = Library @@ -15,14 +47,52 @@ class Meta: } -class LibraryViewSet(viewsets.ReadOnlyModelViewSet): +class LibraryViewSet(viewsets.ModelViewSet): """ Listing All Library Details """ - queryset = Library.objects.all() serializer_class = LibrarySerializer filter_backends = (filters.DjangoFilterBackend,) filterset_class = LibraryFilterSet + permission_classes = (IsLibraryOwner,) + + # All Libraries available for user (custom+default) + def get_queryset(self): + User = get_user_model() + superusers = User.objects.filter(is_superuser=True) + if self.request.user.is_authenticated: + return Library.objects.filter( + Q(library_set__user=self.request.user) + | Q(library_set__user__in=superusers) + | Q(library_set__default=True) + ).order_by('-library_set__default') + else: + return Library.objects.filter( + Q(library_set__default=True) + | Q(library_set__user__in=superusers) + ) + + # Custom libraries uploaded by the user + @action( + detail=False, + methods=['GET'], + name='All custom libraries for user' + ) + def get_custom_libraries(self, request): + if request.user.is_authenticated: + lib_sets = LibrarySet.objects.filter( + Q(user=request.user) & Q(default=False)) + queryset = Library.objects.filter(library_set__in=lib_sets) + return Response(LibrarySerializer(queryset, many=True).data) + return Response() + + # Default Libraries + @action(detail=False, methods=['GET'], name="All Default Libraries") + def default(self, request): + return Response(LibrarySerializer( + Library.objects.filter( + Q(library_set__default=True)), many=True).data + ) class LibraryComponentFilterSet(django_filters.FilterSet): @@ -38,11 +108,154 @@ class Meta: } +class IsComponentOwner(BasePermission): + + def has_object_permission(self, request, view, obj): + if request.method in SAFE_METHODS: + return True + elif request.user.is_authenticated: + if obj.component_library.library_set.user == request.user: + return True + return False + + class LibraryComponentViewSet(viewsets.ReadOnlyModelViewSet): """ Listing All Library Details """ + permission_classes = (IsComponentOwner,) queryset = LibraryComponent.objects.all() serializer_class = LibraryComponentSerializer filter_backends = (filters.DjangoFilterBackend,) filterset_class = LibraryComponentFilterSet + + def get_queryset(self): + User = get_user_model() + superusers = User.objects.filter(is_superuser=True) + if self.request.user.is_authenticated: + library_set = LibrarySet.objects.filter( + Q(user=self.request.user) | + Q(default=True) | Q(user__in=superusers) + ) + libraries = Library.objects.filter(library_set__in=library_set) + components = LibraryComponent.objects.filter( + component_library__in=libraries) + return components + else: + library_set = LibrarySet.objects.filter( + Q(default=True) | Q(user__in=superusers)) + libraries = Library.objects.filter(library_set__in=library_set) + components = LibraryComponent.objects.filter( + component_library__in=libraries) + return components + + +class LibrarySetViewSet(viewsets.ModelViewSet): + """ + Listing Library Sets available to a user + """ + serializer_class = LibrarySetSerializer + parser_class = (MultiPartParser,) + permission_classes = (IsAuthenticated,) + + def get_queryset(self): + if self.request.user.is_authenticated: + queryset = LibrarySet.objects.filter( + Q(user=self.request.user) | Q(default=True)) + else: + queryset = LibrarySet.objects.filter(default=True) + return queryset + + def create(self, request, *args, **kwargs): + try: + library_set = LibrarySet.objects.get(user=request.user) + except LibrarySet.DoesNotExist: + library_set = LibrarySet( + name=request.user.username[0:24], + default=False, + user=request.user + ) + library_set.save() + except LibrarySet.MultipleObjectsReturned: + library_set = LibrarySet.objects.filter(user=request.user).first() + + files = request.FILES.getlist('files') + if len(files) != 0: + path = os.path.join( + settings.BASE_DIR[6:], + 'kicad-symbols', + library_set.user.username + '-' + library_set.name) + try: + # defined in ./lib_utils.py + handle_uploaded_libs(library_set, path, files) + return Response(status=status.HTTP_201_CREATED) + except Exception: + return Response(status=status.HTTP_400_BAD_REQUEST) + else: + return Response(status=status.HTTP_204_NO_CONTENT) + + +class FavouriteComponentView(APIView): + permission_classes = (IsAuthenticated,) + serializer_class = FavouriteComponentSerializer + + @swagger_auto_schema(responses={200: FavouriteComponentSerializer}) + def get(self, request): + try: + queryset = FavouriteComponent.objects.get( + owner=self.request.user) + response_serializer = self.serializer_class( + queryset, context={'request': request}) + return Response(response_serializer.data, + status=status.HTTP_200_OK) + except FavouriteComponent.DoesNotExist: + return Response(data={}, status=status.HTTP_200_OK) + + @swagger_auto_schema(responses={200: FavouriteComponentSerializer}, + request_body=FavouriteComponentSerializer) + def post(self, request): + newComponent = request.data.get("component") + try: + queryset = LibraryComponent.objects.get(id=newComponent[0]) + except LibraryComponent.DoesNotExist: + return Response(data={"error": "Given Component does not Exist"}, + status=status.HTTP_400_BAD_REQUEST) + try: + existingFavourites = FavouriteComponent.objects.get( + owner=self.request.user) + for singleComponent in newComponent: + existingFavourites.component.add(singleComponent) + existingFavourites.save() + serializer = FavouriteComponentSerializer( + instance=existingFavourites, context={'request': request}) + return Response(data=serializer.data, status=status.HTTP_200_OK) + except FavouriteComponent.DoesNotExist: + newFavList = FavouriteComponent.objects.create( + owner=self.request.user) + for singleComponent in newComponent: + newFavList.component.add(singleComponent) + newFavList.save() + serialized = FavouriteComponentSerializer( + instance=newFavList, context={'request': request}) + return Response(data=serialized.data, status=status.HTTP_200_OK) + + +class DeleteFavouriteComponent(APIView): + permission_classes = (IsAuthenticated,) + + @swagger_auto_schema(responses={200: FavouriteComponentSerializer}) + def delete(self, request, id): + try: + queryset = FavouriteComponent.objects.get( + owner=self.request.user, component=id) + queryset.component.remove(id) + serialized = FavouriteComponentSerializer( + instance=queryset, context={'request': request}) + return Response(data=serialized.data, status=status.HTTP_200_OK) + except FavouriteComponent.DoesNotExist: + return Response( + data={ + "error": + "Your favourites doesn't have this component" + }, + status=status.HTTP_400_BAD_REQUEST) diff --git a/esim-cloud-backend/migrations.sh b/esim-cloud-backend/migrations.sh index 64fd6052e..6c6ba16e9 100644 --- a/esim-cloud-backend/migrations.sh +++ b/esim-cloud-backend/migrations.sh @@ -1,12 +1,14 @@ -python manage.py makemigrations +python manage.py makemigrations authAPI +python manage.py migrate authAPI +python manage.py makemigrations saveAPI +python manage.py migrate saveAPI python manage.py makemigrations simulationAPI python manage.py migrate simulationAPI --database="mongodb" python manage.py makemigrations libAPI python manage.py migrate libAPI -python manage.py makemigrations saveAPI -python manage.py migrate saveAPI +python manage.py makemigrations python manage.py migrate python manage.py collectstatic --noinput -rm -r kicad-symbols/symbol_svgs/ -python manage.py seed_libs --clear -python manage.py seed_libs --location kicad-symbols/ +python manage.py createsuperuser_noinput --username=admin --password=admin +python manage.py load_default_libs --username=admin --location=kicad-symbols/default/ --default +python manage.py load_default_libs --username=admin --location=kicad-symbols/additional/ diff --git a/esim-cloud-backend/requirements.txt b/esim-cloud-backend/requirements.txt index 6b0a7d8b0..fb3eeb250 100644 --- a/esim-cloud-backend/requirements.txt +++ b/esim-cloud-backend/requirements.txt @@ -21,6 +21,7 @@ djoser==2.0.3 cssselect2==0.3.0 defusedxml==0.6.0 django-filter==2.2.0 +django-inline-actions==2.4.0 drawSvg==1.5.3 drf-yasg==1.17.1 gunicorn==20.0.4 diff --git a/esim-cloud-backend/saveAPI/models.py b/esim-cloud-backend/saveAPI/models.py index 9f8fe3da3..c629633be 100644 --- a/esim-cloud-backend/saveAPI/models.py +++ b/esim-cloud-backend/saveAPI/models.py @@ -3,6 +3,7 @@ from django.core.files.storage import FileSystemStorage from django.conf import settings import uuid +from libAPI.models import Library # For handling file uploads to a permenant direcrory file_storage = FileSystemStorage( @@ -23,6 +24,7 @@ class StateSave(models.Model): base64_image = models.ImageField( upload_to='circuit_images', storage=file_storage, null=True) is_arduino = models.BooleanField(default=False, null=False) + esim_libraries = models.ManyToManyField(Library) def save(self, *args, **kwargs): super(StateSave, self).save(*args, **kwargs) diff --git a/esim-cloud-backend/saveAPI/serializers.py b/esim-cloud-backend/saveAPI/serializers.py index 8ae28c98c..c54f9022b 100644 --- a/esim-cloud-backend/saveAPI/serializers.py +++ b/esim-cloud-backend/saveAPI/serializers.py @@ -1,5 +1,8 @@ from rest_framework import serializers +from rest_framework.fields import ListField from saveAPI.models import StateSave +from libAPI.models import Library +from libAPI.serializers import LibrarySerializer from django.core.files.base import ContentFile import base64 import six @@ -30,18 +33,21 @@ def update(self, data): class StateSaveSerializer(serializers.ModelSerializer): base64_image = Base64ImageField(max_length=None, use_url=True) + esim_libraries = LibrarySerializer(many=True, required=False) class Meta: model = StateSave + fields = ('save_time', 'save_id', 'data_dump', 'name', 'description', 'owner', 'shared', 'base64_image', 'create_time', - 'is_arduino') + 'is_arduino', 'esim_libraries') class SaveListSerializer(serializers.ModelSerializer): base64_image = Base64ImageField(max_length=None, use_url=True) + esim_libraries = LibrarySerializer(many=True, required=False) class Meta: model = StateSave fields = ('save_time', 'save_id', 'name', 'description', - 'shared', 'base64_image', 'create_time') + 'shared', 'base64_image', 'create_time', 'esim_libraries') diff --git a/esim-cloud-backend/saveAPI/views.py b/esim-cloud-backend/saveAPI/views.py index 5450a1726..7e4fd44fb 100644 --- a/esim-cloud-backend/saveAPI/views.py +++ b/esim-cloud-backend/saveAPI/views.py @@ -14,6 +14,7 @@ from django.contrib.auth import get_user_model import logging import traceback +import json logger = logging.getLogger(__name__) @@ -31,12 +32,26 @@ class StateSaveView(APIView): @swagger_auto_schema(request_body=StateSaveSerializer) def post(self, request, *args, **kwargs): logger.info('Got POST for state save ') - serializer = StateSaveSerializer( - data=request.data, context={'request': self.request}) - if serializer.is_valid(): - serializer.save(owner=self.request.user) - return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + esim_libraries = None + if request.data.get('esim_libraries'): + esim_libraries = json.loads(request.data.get('esim_libraries')) + img = Base64ImageField(max_length=None, use_url=True) + filename, content = img.update(request.data['base64_image']) + state_save = StateSave( + data_dump=request.data.get('data_dump'), + description=request.data.get('description'), + name=request.data.get('name'), + owner=request.user, + is_arduino=True if esim_libraries is None else False, + ) + state_save.base64_image.save(filename, content) + if esim_libraries: + state_save.esim_libraries.set(esim_libraries) + try: + state_save.save() + return Response(StateSaveSerializer(state_save).data) + except Exception: + return Response(status=status.HTTP_400_BAD_REQUEST) class StateFetchUpdateView(APIView): @@ -112,13 +127,16 @@ def post(self, request, save_id): saved_state.name = request.data['name'] if 'description' in request.data: saved_state.description = request.data['description'] - # if thumbnail needs to be updated if 'base64_image' in request.data: img = Base64ImageField(max_length=None, use_url=True) filename, content = img.update( request.data['base64_image']) saved_state.base64_image.save(filename, content) + if 'esim_libraries' in request.data: + esim_libraries = json.loads( + request.data.get('esim_libraries')) + saved_state.esim_libraries.set(esim_libraries) saved_state.save() serialized = SaveListSerializer(saved_state) return Response(serialized.data) @@ -209,11 +227,11 @@ class UserSavesView(APIView): def get(self, request): saved_state = StateSave.objects.filter( owner=self.request.user, is_arduino=False).order_by('-save_time') - try: - serialized = StateSaveSerializer(saved_state, many=True) - return Response(serialized.data) - except Exception: - return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) + # try: + serialized = StateSaveSerializer(saved_state, many=True) + return Response(serialized.data) + # except Exception: + return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) class ArduinoSaveList(APIView): @@ -252,6 +270,7 @@ class SaveSearchViewSet(viewsets.ReadOnlyModelViewSet): """ Search Project """ + def get_queryset(self): queryset = StateSave.objects.filter( owner=self.request.user).order_by('-save_time') diff --git a/esim-cloud-backend/simulationAPI/models.py b/esim-cloud-backend/simulationAPI/models.py index db6fc5610..ca60be353 100644 --- a/esim-cloud-backend/simulationAPI/models.py +++ b/esim-cloud-backend/simulationAPI/models.py @@ -1,5 +1,6 @@ from djongo import models from django.core.files.storage import FileSystemStorage +from django.contrib.auth.models import User from django.conf import settings import uuid diff --git a/esim-cloud-backend/simulationAPI/urls.py b/esim-cloud-backend/simulationAPI/urls.py index f8a57e96a..afc654515 100644 --- a/esim-cloud-backend/simulationAPI/urls.py +++ b/esim-cloud-backend/simulationAPI/urls.py @@ -13,5 +13,4 @@ path('status/', simulationAPI_views.CeleryResultView.as_view(), name='celery_status'), - ]