Skip to content

Commit

Permalink
Merge pull request #97 from ssciwr/admin_interface_milestones
Browse files Browse the repository at this point in the history
Add milestones to admin interface
  • Loading branch information
lkeegan authored Oct 7, 2024
2 parents 760f859 + ea38aa9 commit 7e20452
Show file tree
Hide file tree
Showing 12 changed files with 444 additions and 52 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,6 @@ dmypy.json

# static images
mondey_backend/static/*.jpg

# ssl keys
*.pem
6 changes: 6 additions & 0 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ sudo docker compose ps
sudo docker compose logs
```

To update the running website to the latest version:

```
sudo docker compose pull && sudo docker compose up -d && sudo docker system prune -af
```

### Give users admin rights

To make an existing user with email `[email protected]` into an admin, modify the users database, e.g.
Expand Down
82 changes: 82 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Development

Some information on how to locally build and serve the website if you would like to make changes to the code.
There are two ways to do this:

- docker
- closer to production environment
- but less convenient for development - you need to rebuild the image every time you make a change
- python/pnpm
- further from production environment setup
- but convenient for development - see changes immediately without having to rebuild or restart anything

## Run locally with docker

Requires docker and docker compose.

1. clone the repo:

```sh
git clone https://github.com/ssciwr/mondey.git
cd mondey
```

2. generate a local SSL cert/key pair:

```
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 -nodes -subj '/CN=localhost'
```

3. build and run the website locally in docker containers on your computer:

```sh
docker compose up --build -d
```

The website is then served at https://localhost/
(note that the SSL keys are self-signed keys and your browser will still warn about the site being insecure.)

Whenever you make a change to the code you need to re-run the above command to see the effect of your changes.

### Database

The databases will by default be stored in a `db` folder
in the folder where you run the docker compose command.

### Make yourself an admin user

```
sudo sqlite3 docker_volume/predicTCR.db
sqlite> UPDATE user SET is_admin=true WHERE email='[email protected]';
sqlite> .quit
```

## Run locally with Python and pnpm

Requires Python and [pnpm](https://pnpm.io/installation#using-a-standalone-script)

1. clone the repo:

```sh
git clone https://github.com/ssciwr/mondey.git
cd mondey
```

2. install and run the backend development server:

```sh
cd mondey_backend
pip install .
cd ..
mondey-backend
```

3. install and run the frontend development server:

```sh
cd frontend
pnpm install
pnpm run dev
```

The website is then served at http://localhost:5173/.
99 changes: 99 additions & 0 deletions frontend/src/lib/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,105 @@ export function milestoneGroupImageUrl(id: number) {
return `${import.meta.env.VITE_MONDEY_API_URL}/static/mg${id}.jpg`;
}

export async function newMilestone(milestoneGroupId: number) {
console.log('newMilestone...');
try {
const res = await fetch(
`${import.meta.env.VITE_MONDEY_API_URL}/admin/milestones/${milestoneGroupId}`,
{
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
}
}
);
if (res.status === 200) {
const newMilestone = await res.json();
console.log(newMilestone);
await refreshMilestoneGroups();
return newMilestone;
} else {
console.log('Failed to create new Milestone');
}
} catch (e) {
console.error(e);
}
return null;
}

export async function updateMilestone(milestone) {
console.log('updateMilestone...');
console.log(milestone);
try {
const res = await fetch(`${import.meta.env.VITE_MONDEY_API_URL}/admin/milestones/`, {
method: 'PUT',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
body: JSON.stringify(milestone)
});
if (res.status === 200) {
const updatedMilestone = await res.json();
console.log(updatedMilestone);
return updatedMilestone;
} else {
console.log('Failed to update Milestone');
}
} catch (e) {
console.error(e);
}
return null;
}

export async function uploadMilestoneImage(milestoneId: number, file) {
console.log('uploadMilestoneImage...');
try {
const formData = new FormData();
formData.append('file', file);
const res = await fetch(
`${import.meta.env.VITE_MONDEY_API_URL}/admin/milestone-images/${milestoneId}`,
{
method: 'POST',
credentials: 'include',
body: formData
}
);
console.log(await res.json());
} catch (e) {
console.error(e);
}
}

export async function deleteMilestone(milestoneId: number | null) {
try {
const res = await fetch(
`${import.meta.env.VITE_MONDEY_API_URL}/admin/milestones/${milestoneId}`,
{
method: 'DELETE',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
}
}
);
const json = await res.json();
console.log(json);
if (res.status === 200) {
console.log(`Deleted Milestone with id ${milestoneId}.`);
await refreshMilestoneGroups();
} else {
console.log(`Error deleting Milestone with id ${milestoneId}.`);
}
} catch (e) {
console.error(e);
}
}

export async function refreshUserQuestions() {
console.log('refreshQuestions...');
try {
Expand Down
135 changes: 135 additions & 0 deletions frontend/src/lib/components/Admin/EditMilestoneModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<script lang="ts">
import {
Button,
InputAddon,
Textarea,
Input,
Label,
ButtonGroup,
Fileupload,
Modal
} from 'flowbite-svelte';
import { languages } from '$lib/stores/adminStore';
import { refreshMilestoneGroups, updateMilestone, uploadMilestoneImage } from '$lib/admin';
export let open: boolean = false;
export let milestone: object | null = null;
let files: FileList;
let images: string[] = [];
async function uploadImagesChanged(event) {
const uploaded_files = [...event.target.files];
images = await Promise.all(
uploaded_files.map((f) => {
return imageAsDataURL(f);
})
);
}
function imageAsDataURL(file: Blob) {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = function () {
return resolve(fileReader.result);
};
fileReader.onerror = function () {
fileReader.abort();
return reject(fileReader.error);
};
fileReader.readAsDataURL(file);
});
}
export async function saveChanges() {
try {
await updateMilestone(milestone);
if (files) {
for (const file of files) {
await uploadMilestoneImage(milestone.id, file);
}
}
await refreshMilestoneGroups();
} catch (e) {
console.error(e);
}
}
</script>

<Modal title="Edit milestone" bind:open autoclose size="xl">
{#if milestone}
<div class="mb-5">
<Label class="mb-2">Title</Label>
{#each Object.entries(milestone.text) as [lang_id, text]}
<div class="mb-1">
<ButtonGroup class="w-full">
<InputAddon>{$languages[text.lang_id]}</InputAddon>
<Input bind:value={text.title} placeholder="Title" />
</ButtonGroup>
</div>
{/each}
</div>
<div class="mb-5">
<Label class="mb-2">Description</Label>
{#each Object.entries(milestone.text) as [lang_id, text]}
<div class="mb-1">
<ButtonGroup class="w-full">
<InputAddon>{$languages[text.lang_id]}</InputAddon>
<Textarea bind:value={text.desc} placeholder="Description" />
</ButtonGroup>
</div>
{/each}
</div>
<div class="mb-5">
<Label class="mb-2">Observation</Label>
{#each Object.entries(milestone.text) as [lang_id, text]}
<div class="mb-1">
<ButtonGroup class="w-full">
<InputAddon>{$languages[text.lang_id]}</InputAddon>
<Textarea bind:value={text.obs} placeholder="Observation" />
</ButtonGroup>
</div>
{/each}
</div>
<div class="mb-5">
<Label class="mb-2">Help</Label>
{#each Object.entries(milestone.text) as [lang_id, text]}
<div class="mb-1">
<ButtonGroup class="w-full">
<InputAddon>{$languages[text.lang_id]}</InputAddon>
<Textarea bind:value={text.help} placeholder="Help" />
</ButtonGroup>
</div>
{/each}
</div>
<div class="mb-5">
<Label for="img_upload" class="pb-2">Images</Label>
<div class="flex flex-row">
{#each milestone.images as milestoneImage, milestoneImageId (milestoneImage.id)}
<img
src={`${import.meta.env.VITE_MONDEY_API_URL}/static/${milestoneImage.filename}`}
width="48"
height="48"
alt={`${milestoneImageId}`}
class="m-2"
/>
{/each}
{#each images as image}
<img src={image} width="48" height="48" alt="milestone" class="m-2" />
{/each}
</div>
<Fileupload
bind:files
on:change={uploadImagesChanged}
multiple
accept=".jpg, .jpeg"
id="img_upload"
class="mb-2 flex-grow-0"
/>
</div>
{/if}
<svelte:fragment slot="footer">
<Button color="green" on:click={saveChanges}>Save changes</Button>
<Button color="alternative">Cancel</Button>
</svelte:fragment>
</Modal>
Loading

0 comments on commit 7e20452

Please sign in to comment.