Skip to content

Commit

Permalink
Merge pull request #157 from visdesignlab/public-states
Browse files Browse the repository at this point in the history
Add ability to make states public
  • Loading branch information
haihan-lin committed Aug 13, 2021
2 parents 548cd69 + 614042a commit 413e5f6
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 61 deletions.
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

0 comments on commit 413e5f6

Please sign in to comment.