Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to make states public #157

Merged
merged 15 commits into from
Aug 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ There are several routes set up for accessing the patient and surgery data. Here
- Allowed Methods: `GET, POST, PUT, DELETE`
- Parameters:
`name`: The name of the state object.
`definition`: The state definition, usually the string from our provenance library.
`definition`: The state definition, usually the string from our provenance library.
`public`: true/false indicating whether the state should be public
- Description: Handles state saving into a database on the backend. A GET will retrieve the state object by name. A POST creates a state object. A PUT updates a state object. Finally, a DELETE will delete a state object. The required parameters for each type of request are documented in the examples.
- Example:
```
Expand All @@ -176,12 +177,13 @@ There are several routes set up for accessing the patient and surgery data. Here
# POST
curl -X POST '127.0.0.1:8000/api/state' \
-F "name=example_state" \
-F "definition=foo"
-F "definition=foo" \
-F "public=true"

# PUT
curl -X PUT '127.0.0.1:8000/api/state' \
-H "Content-Type: application/json" \
-d '{"old_name": "example_state", "new_name": "a_new_state", "definition": "foo"}'
-d '{"old_name": "example_state", "new_name": "a_new_state", "new_definition": "foo", "new_public": "false"}'

# DELETE
curl -X DELETE '127.0.0.1:8000/api/state' \
Expand Down
23 changes: 23 additions & 0 deletions backend/api/api/migrations/0004_auto_20210716_1709.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 2.2.24 on 2021-07-16 17:09

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0003_auto_20200826_1855'),
]

operations = [
migrations.AddField(
model_name='state',
name='public',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='state',
name='owner',
field=models.CharField(default='u0999308', max_length=128),
),
]
1 change: 1 addition & 0 deletions backend/api/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class State(models.Model):
name = models.CharField(max_length=128, unique=True, default="New State")
definition = models.TextField()
owner = models.CharField(max_length=128, default="NA")
public = models.BooleanField(default=False)


class StateAccess(models.Model):
Expand Down
32 changes: 21 additions & 11 deletions backend/api/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,7 @@ def state(request):
state_access = StateAccess.objects.filter(state=state).filter(user=user)

# Make sure that user is owner or at least reader
if not(str(state.owner) == str(user) or state_access):
if not(str(state.owner) == str(user) or state_access or state.public):
return HttpResponseBadRequest("Not authorized", 401)

# Return the json for the state
Expand All @@ -865,8 +865,9 @@ def state(request):
# Get the names of all the state objects that a user can access
states = [o.name for o in State.objects.all().filter(owner=user)]
state_access = [o.state.name for o in StateAccess.objects.filter(user=user)]
public_states = [o.name for o in State.objects.all().filter(public=True)]

response = set(states + state_access)
response = set(states + state_access + public_states)

# Return the names as a list
return JsonResponse(list(response), safe=False)
Expand All @@ -876,15 +877,18 @@ def state(request):
name = request.POST.get('name')
definition = request.POST.get('definition')
owner = request.user.id
public_request = request.POST.get("public")

logging.info(f"{request.META.get('HTTP_X_FORWARDED_FOR')} POST: state Params: name = {name} User: {request.user}")
public = True if public_request == "true" else False

logging.info(f"{request.META.get('HTTP_X_FORWARDED_FOR')} POST: state Params: name = {name}, public = {public} User: {request.user}")

if State.objects.filter(name=name).exists():
return HttpResponseBadRequest("a state with that name already exists, try another", 400)

if name and definition: # owner is guaranteed by login
# Create and save the new State object
new_state = State(name=name, definition=definition, owner=owner)
new_state = State(name=name, definition=definition, owner=owner, public=public)
new_state.save()

return HttpResponse("state object created", 200)
Expand All @@ -897,23 +901,29 @@ def state(request):
old_name = put.get('old_name')
new_name = put.get('new_name')
new_definition = put.get('new_definition')
new_public_request = put.get('new_public')

new_public = True if new_public_request == "true" else False

logging.info(f"{request.META.get('HTTP_X_FORWARDED_FOR')} PUT: state Params: old_name = {old_name}, new_name = {new_name} User: {request.user}")

states = [o.name for o in State.objects.all().filter(owner=request.user.id)]
state_access = [o.state.name for o in StateAccess.objects.filter(user=request.user.id).filter(role="WR")]
state_read_access = [o.state.name for o in StateAccess.objects.filter(user=request.user.id).filter(role="RE")]
allowed_states = response = set(states + state_access)
owned_states = [o.name for o in State.objects.all().filter(owner=request.user.id)]
public_states = [o.name for o in State.objects.all().filter(public=True)]
writable_states = [o.state.name for o in StateAccess.objects.filter(user=request.user.id).filter(role="WR")]
readable_states = [o.state.name for o in StateAccess.objects.filter(user=request.user.id).filter(role="RE")]
all_accessible_states = set(owned_states + public_states + writable_states + readable_states)
all_modifiable_states = set(owned_states + writable_states)

if old_name in state_read_access:
return HttpResponseBadRequest("Not authorized", 401)
elif old_name not in allowed_states:
if old_name not in all_accessible_states:
return HttpResponseBadRequest("State not found", 404)
if old_name not in all_modifiable_states:
return HttpResponseBadRequest("Not authorized", 401)

# Update the State object and save
result = State.objects.get(name=old_name)
result.name = new_name
result.definition = new_definition
result.public = new_public
result.save()

return HttpResponse("state object updated", 200)
Expand Down
12 changes: 9 additions & 3 deletions frontend/src/Components/Modals/ManageStateDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import DeleteIcon from '@material-ui/icons/Delete';
import { simulateAPIClick } from "../../Interfaces/UserManagement";
import { Alert } from "@material-ui/lab";
import { observer } from "mobx-react";
import { SnackBarCloseTime } from "../../Presets/Constants";

const ManageStateDialog: FC = () => {
const store = useContext(Store);
const [errorMessage, setErrorMessage] = useState("")
const [openErrorMessage, setOpenError] = useState(false);
const [openSuccess, setOpenSuccess] = useState(false);


const removeState = (stateName: string) => {
Expand All @@ -29,11 +31,10 @@ const ManageStateDialog: FC = () => {
}).then(response => {
if (response.status === 200) {
store.configStore.savedState = store.configStore.savedState.filter(d => d !== stateName)
store.configStore.openManageStateDialog = false
setOpenSuccess(true);
setErrorMessage("")
} else {
response.text().then(error => {

setErrorMessage(response.statusText);
setOpenError(true)
console.error('There has been a problem with your fetch operation:', response.statusText);
Expand Down Expand Up @@ -69,11 +70,16 @@ const ManageStateDialog: FC = () => {
</Button>
</DialogActions>
</Dialog>
<Snackbar open={openErrorMessage} autoHideDuration={6000} onClose={() => { setOpenError(false) }}>
<Snackbar open={openErrorMessage} autoHideDuration={SnackBarCloseTime} onClose={() => { setOpenError(false) }}>
<Alert onClose={() => { setOpenError(false); setErrorMessage("") }} severity="error">
An error occured: {errorMessage}
</Alert>
</Snackbar>
<Snackbar open={openSuccess} autoHideDuration={SnackBarCloseTime} onClose={() => { setOpenSuccess(false) }}>
<Alert onClose={() => { setOpenSuccess(false) }} severity="success">
Deletion succeed.
</Alert>
</Snackbar>
</div>)
}

Expand Down
94 changes: 65 additions & 29 deletions frontend/src/Components/Modals/SaveStateModal.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Snackbar, TextField } from "@material-ui/core";
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControlLabel, FormGroup, Radio, RadioGroup, Snackbar, Switch, TextField } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { observer } from "mobx-react";
import { FC, useState, useContext } from "react";
import Store from "../../Interfaces/Store";
import { simulateAPIClick } from "../../Interfaces/UserManagement";
import { SnackBarCloseTime } from "../../Presets/Constants";


const SaveStateModal: FC = () => {
Expand All @@ -12,6 +13,7 @@ const SaveStateModal: FC = () => {
const [errorMessage, setErrorMessage] = useState("")
const [openErrorMessage, setOpenError] = useState(false);
const [openSuccessMessage, setOpenSuccessMessage] = useState(false);
const [publicAccess, setPublicAccess] = useState(false);

const saveState = () => {
const csrftoken = simulateAPIClick()
Expand All @@ -29,19 +31,14 @@ const SaveStateModal: FC = () => {
body: JSON.stringify({ old_name: stateName, new_name: stateName, new_definition: store.provenance.exportState(false) })
}).then(response => {
if (response.status === 200) {
setOpenSuccessMessage(true)
store.configStore.openSaveStateDialog = false
setStateName("")
setErrorMessage("")
onSuccess(false);
} else {
response.text().then(error => {
setErrorMessage(error);
console.error('There has been a problem with your fetch operation:', response.statusText);
onFail(response.statusText);
})
}
}).catch(error => {
setErrorMessage(error)
console.error('There has been a problem with your fetch operation:', error);
onFail(error.toString())
})
} else {
fetch(`${process.env.REACT_APP_QUERY_URL}state`, {
Expand All @@ -54,46 +51,85 @@ const SaveStateModal: FC = () => {
"Access-Control-Allow-Origin": 'https://bloodvis.chpc.utah.edu',
"Access-Control-Allow-Credentials": "true",
},
body: `csrfmiddlewaretoken=${csrftoken}&name=${stateName}&definition=${store.provenance.exportState(false)}`
body: `csrfmiddlewaretoken=${csrftoken}&name=${stateName}&definition=${store.provenance.exportState(false)}&public=${publicAccess.toString()}`
}).then(response => {
if (response.status === 200) {
onSuccess(true);
} else {
response.text().then(error => {
onFail(response.statusText);

})
}
}).catch(error => {
onFail(error.toString());
})
.then(response => {
if (response.status === 200) {
store.configStore.openSaveStateDialog = false;
store.configStore.addNewState(stateName)
setStateName("")
setErrorMessage("")
} else {
response.text().then(error => {
setErrorMessage(response.statusText);
setOpenError(true)
console.error('There has been a problem with your fetch operation:', response.statusText);
})
}
})
}

}

const onSuccess = (newState: boolean) => {
store.configStore.openSaveStateDialog = false;
if (newState) {
store.configStore.addNewState(stateName);
}
setOpenSuccessMessage(true);
setStateName("")
setErrorMessage("")
setPublicAccess(false);
setOpenError(false)
}

const onFail = (errorMessage: string) => {
setErrorMessage(errorMessage)
setOpenError(true)
console.error('There has been a problem with your fetch operation:', errorMessage);
}

return <div>
<Dialog open={store.configStore.openSaveStateDialog}>
<DialogTitle>Save the current state</DialogTitle>
<DialogContent>
<DialogContentText>
Save the current state with a state name, and then click save.
Save the current state with a state name, select the state privacy setting, and then click save.
</DialogContentText>
<TextField fullWidth label="State Name" onChange={(e) => { setStateName(e.target.value) }} value={stateName} />
<FormGroup>
<TextField fullWidth label="State Name" onChange={(e) => { setStateName(e.target.value) }} value={stateName} />
<RadioGroup row onChange={(e) => { setPublicAccess(e.target.value === "PublicState") }} value={publicAccess ? "PublicState" : "PrivateState"}>
<FormControlLabel
value="PublicState"
control={
<Radio
name="state-public"
color="primary"
/>
}
label="Public State"
/>
<FormControlLabel
value="PrivateState"
control={
<Radio name="state-private" color="primary" />
}
label="Private State"
/>
</RadioGroup>
</FormGroup>



</DialogContent>
<DialogActions>
<Button onClick={() => { store.configStore.openSaveStateDialog = false }}>Cancel</Button>
<Button color="primary" onClick={() => { saveState() }}>Confirm</Button>
<Button color="primary" disabled={stateName.length === 0} onClick={() => { saveState() }}>Confirm</Button>
</DialogActions>
</Dialog>
<Snackbar open={openErrorMessage} autoHideDuration={6000} onClose={() => { setOpenError(false) }}>
<Snackbar open={openErrorMessage} autoHideDuration={SnackBarCloseTime} onClose={() => { setOpenError(false) }}>
<Alert onClose={() => { setOpenError(false); setErrorMessage("") }} severity="error">
An error occured: {errorMessage}
</Alert>
</Snackbar>
<Snackbar open={openSuccessMessage} autoHideDuration={6000} onClose={() => { setOpenSuccessMessage(false) }}>
<Snackbar open={openSuccessMessage} autoHideDuration={SnackBarCloseTime} onClose={() => { setOpenSuccessMessage(false) }}>
<Alert onClose={() => { setOpenSuccessMessage(false); }} severity="success">
State saved!
</Alert>
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/Components/Modals/ShareStateURLModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Store from "../../Interfaces/Store";
import FileCopyIcon from '@material-ui/icons/FileCopy';
import { Alert } from "@material-ui/lab";
import { copyTextToClipboard } from "../../HelperFunctions/Clipboard";
import { SnackBarCloseTime } from "../../Presets/Constants";


type Props = {
Expand Down Expand Up @@ -45,7 +46,7 @@ const ShareStateUrlModal: FC<Props> = ({ shareUrl }: Props) => {

</DialogActions>
</Dialog>
<Snackbar open={showAlert} autoHideDuration={6000} onClose={() => { setShowAlert(false) }}>
<Snackbar open={showAlert} autoHideDuration={SnackBarCloseTime} onClose={() => { setShowAlert(false) }}>
<Alert onClose={() => { setShowAlert(false) }} severity={errorOccured ? "error" : "success"}>
{errorOccured ? "An error occured. Please copy the URL manually." : "Copied to clipboard"}
</Alert>
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/Components/Modals/UIDInputModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { observer } from "mobx-react";
import { FC, useContext, useState } from "react";
import Store from "../../Interfaces/Store";
import { simulateAPIClick } from "../../Interfaces/UserManagement";
import { SnackBarCloseTime } from "../../Presets/Constants";

type Props = {
stateName: string;
Expand Down Expand Up @@ -99,12 +100,12 @@ const UIDInputModal: FC<Props> = ({ stateName }: Props) => {
</DialogActions>
</DialogContent>
</Dialog>
<Snackbar open={openErrorMessage} autoHideDuration={6000} onClose={() => { setOpenErrorMessage(false) }}>
<Snackbar open={openErrorMessage} autoHideDuration={SnackBarCloseTime} onClose={() => { setOpenErrorMessage(false) }}>
<Alert onClose={() => { setOpenErrorMessage(false); setErrorMessage("") }} severity="error">
An error occured: {errorMessage}
</Alert>
</Snackbar>
<Snackbar open={openSuccessMessage} autoHideDuration={6000} onClose={() => { setOpenSuccess(false) }}>
<Snackbar open={openSuccessMessage} autoHideDuration={SnackBarCloseTime} onClose={() => { setOpenSuccess(false) }}>
<Alert onClose={() => { setOpenSuccess(false); }} severity="success">
State saved!
</Alert>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/Components/Utilities/DetailView/CaseList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,4 @@ interface CaseItemProps {

const CaseItem = styled(ListItem) <CaseItemProps>`
background:${props => props.isSelected ? "#ecbe8d" : 'none'};
`;
`;
Loading