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

Comprehencive backend read me #2107

Closed
wants to merge 3 commits into from
Closed
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
72 changes: 71 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ the VSCode editor.
4. Type `cd indok-web` to move into the new folder
5. (optional, but extremely recommended) Set up the backend locally, to get linting, auto-complete and environment variables
- Follow steps 1-9 under [Without Docker: Backend](#backend) below
6. (optional, but extremely recommended) Set up the frontend locally, to get pre-commit hooks, linting, auto-complete and
6. (optional, but extremely recommended) Set up the frontend locally, to get pre-commit hooks, linting, auto-complete and
environment variables
- Follow steps 1-8 under [Without Docker: Frontend](#frontend) below
7. Type `docker compose build` to build the project
Expand Down Expand Up @@ -562,6 +562,76 @@ An outline of how a developer may work with this project:
- If an automatic test fails, click `Details` on it to see what went wrong in the logs
- Once your Pull Request is approved, and the tests pass, you can merge it - now your changes are live!

### Comprehenceive Api and Backend Models Guide

#### Making a model (changes to the database)

The models.py file is essentially a description of what should be stored in our database. Types.py tells graphene, the service we use to autogenerate a ton of stuff what types the database has.

If you are adding a brand new models.py file make sure to add your changes LOCAL_APPS in the base.py of our django settings. In order to make the migrations work you will also need a migrations folder containing a file named **init**.py. The file init file is empty but nessecary.

After changes have been made to thease files you need to push them to the database. This is done first by making the migrations using `docker compose exec backend python manage.py makemigrations` After the migrations are made you migrate useing `docker compose exec backend python manage.py migrate`. For the changes to show up in the admin panel you will need to restart docker

#### Making a query

A query is a request for data made to the backend by the frontend. The following is a guide on how to make queries.

##### Queries in Backend

All queries have a respective resolver function in the backends `resolvers.py` function. This is how the backend knows what to send to the frontend. A resolver function is ALWAYS named `resolver_something`. Note that Python uses camel case and GraphQL does not. This means that in communication the frontend is going to capitalize the letter(s) after the underscore(s) and remove the underscore. For example, the resolver, `resolve_get_winners_list`, will be referenced as `getWinnersList` in the frontend.

All resolvers must also be referenced in the `schema.py` file. Here they are again named using camel case and given return types, often the type is just the model from `models.py`.

If you are making the first queries of this backend folder you will also need to add the schema to `backend/config/schema.py`. This is to tell the command `python manage.py graphql_schema` to include the new schema file that it exists.

After adding the new resolvers you can run `docker compose exec backend python manage.py graphql_schema` in the `indok-web` directory to add the new schema. The query is now done from the backend perspective. NOTE: if you have forgotten to add the resolver to the schema or done so incorrectly the terminal will say it was successful. After this command, there should be a change in `schema.json`.

##### Queries in Frontend

In the frontend all query defining is done in the `graphql` folder. The generated folder should not be touched! (However it can be useful to look at in troubleshooting).

The `graphql` folder usually has 3 files, mutations, queries and fragments. Fragemnts are selections of fields on one of our API types, that can be reused across different queries/mutations to ensure consistent types. You can read more here: (https://www.apollographql.com/docs/react/data/fragments/). If a selection is used only once it can be written in the queries file directy.

After having written your query run the command `yarn generate` in the frontend directory to generate machine readable queries.

After the above is done the queries can be used and tested. In code they are used using the apollo function useQuery. Here is an example: `const { error, loading, data } = useQuery(GetWinnersListDocument);`. Notice the three terms before the query. Things can ALWAYS go wrong with queries and they requre data to be sent, therefore it is common to define what is done in rare cases.

- `data` is the normal data when the query has returned something expected from the backend.
- `loading` is a waiting status. Queries are not instant and it can be useful to define some action while waiting.
- `error` is what you recive if the query returned that something has gone wrong. This should almost always have some sort of notification to the user and or admins.

An important security notice: Permissions are handled in the backend. It is possible for anyone to try to call queries withut a frontend interface, it is therefore important that all queries that should have a permission check has them before it is pushed to production regardless of if there exists a frontend interface.

One last note about queries is that we limit size. If you return more than about 300 items the query will fail. Therefore it is important that sorting and things of that kind is done in the backend.

#### Making a mutation

A mutation is a request for data to be changed or added made to the backend by the frontend. The following is a guide on how to make mutations. Note that it is very similar to making a query.

##### Queries in Backend

All mutations have a respective mutation class in the backends `mutations.py` file. This is how the backend knows what to change when the mutation is called. A mutation class has no strict naming scheme but it is higly reccommended to end the name in "Mutation". Every mutation class has a function named mutate that does the changing, an Arguments class that takes in the data that can be passed from the frontend, and an ok that is returned if the mutation is successful.

All mutations must also be referenced in the `schema.py` file. Here they are named using camel case for the name of the mutation.

If you are making the first mutations of this backend folder you will also need to add the schema to `backend/config/schema.py`. This is to tell the command `python manage.py graphql_schema` to include the new schema file that it exists.

After adding the new resolvers you can run `docker compose exec backend python manage.py graphql_schema` in the `indok-web` directory to add the new schema. The mutation is now done from the backend perspective. NOTE: if you have forgotten to add the resolver to the schema or done so incorrectly the terminal will say it was successful. After this command, there should be a change in `schema.json`.

An important security notice: Permissions are handled in the backend. It is possible for anyone to try to call mutations withut a frontend interface, it is therefore important that all mutations that should have a permission check has it before it is pushed to production regardless of if there exists a frontend interface.

##### Mutations in Frontend

In the frontend all mutation defining is done in the `graphql` folder. The generated folder should not be touched! (However it can be useful to look at in troubleshooting).

The `graphql` folder usually has 3 files, mutations, queries and fragments. Obvously mutations are written in the mutations folder.

After having written your mutation run the command `yarn generate` in the frontend directory to generate machine readable mutation.

After the above is done the queries can be used and tested. In code they are used using the apollo function useUsemutation to define a function that can be run. Here is an example: `const [logTicTacToe] = useMutation(LogTicTacToeDocument);`. After this the `logTicTacToe` function can be used to run the mutation. It lookes like this: `logTicTacToe({ variables: { winner: "Draw" } })`; Note that this does not trigger a react rerender so if you need to change visuals you need to handle that as well.

An important security notice: Permissions are handled in the backend. It is possible for anyone to try to call queries withut a frontend interface, it is therefore important that all queries that should have a permission check has them before it is pushed to production regardless of if there exists a frontend interface. Most mutations (mabye all the ones we currenly have) need a permission check.

## Tech Stack

- Frontend
Expand Down
31 changes: 31 additions & 0 deletions frontend/src/components/pages/ticTacToe/Board.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Box, Stack } from "@mui/material";
import { useState } from "react";
import Square from "./Square";

type Props = {
squares: string[];
handleMove: (i: number) => void;
};

export const Board: React.VFC<Props> = ({ squares, handleMove }) => {
return (
<Box>
<Stack direction={"row"}>
<Square index={0} value={squares[0]} handleClick={handleMove} />
<Square index={1} value={squares[1]} handleClick={handleMove} />
<Square index={2} value={squares[2]} handleClick={handleMove} />
</Stack>
<Stack direction={"row"}>
<Square index={3} value={squares[3]} handleClick={handleMove} />
<Square index={4} value={squares[4]} handleClick={handleMove} />
<Square index={5} value={squares[5]} handleClick={handleMove} />
</Stack>
<Stack direction={"row"}>
<Square index={6} value={squares[6]} handleClick={handleMove} />
<Square index={7} value={squares[7]} handleClick={handleMove} />
<Square index={8} value={squares[8]} handleClick={handleMove} />
</Stack>
</Box>
);
};
export default Board;
22 changes: 22 additions & 0 deletions frontend/src/components/pages/ticTacToe/MoveHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Box, ButtonBase, List, ListItemButton } from "@mui/material";
import { useState } from "react";

type Props = {
history: string[][];
jumpTo: (step: number) => void;
currentlyViewing: number;
};

export const MoveHistory: React.VFC<Props> = ({ history, jumpTo, currentlyViewing }) => {
return (
<List>
{history.map((history, index) => (
<ListItemButton key={index} onClick={() => jumpTo(index)} selected={currentlyViewing === index}>
Move nr. {index + 1}
</ListItemButton>
))}
</List>
);
};

export default MoveHistory;
19 changes: 19 additions & 0 deletions frontend/src/components/pages/ticTacToe/Square.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Box, ButtonBase } from "@mui/material";

type Props = {
index: number;
value: string;
handleClick: (index: number) => void;
};

export const Square: React.VFC<Props> = ({ index, value, handleClick }) => {
return (
<ButtonBase onClick={() => handleClick(index)}>
<Box height={"100px"} width={"100px"} border={1}>
{value}
</Box>
</ButtonBase>
);
};

export default Square;
96 changes: 96 additions & 0 deletions frontend/src/pages/ticTacToe/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"use client";

import Board from "@/components/pages/ticTacToe/Board";
import MoveHistory from "@/components/pages/ticTacToe/MoveHistory";
import { NextPageWithLayout } from "@/lib/next";
import { Box, Stack, Typography } from "@mui/material";
import { useRef, useState } from "react";

const TicTacToe: NextPageWithLayout = () => {
const [winner, setWinner] = useState<string>("");
const [gameOver, setGameOver] = useState<boolean>(false);
const [squares, setSquares] = useState<string[]>(Array(9).fill(""));
const [history, setHistory] = useState<string[][]>([squares]);
const [turn, setTurn] = useState<string>("X");
const currentlyViewing = useRef(history.length - 1);

function isGameOver(currentSquares: string[]) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for (const line of lines) {
const [a, b, c] = line;
if (currentSquares[a] && currentSquares[a] === currentSquares[b] && currentSquares[a] === currentSquares[c]) {
setWinner(currentSquares[a]);
setGameOver(true);
}
}
if (currentSquares.every((currentSquares) => currentSquares !== "")) {
setGameOver(true);
}
}

function handleMove(i: number) {
console.log(currentlyViewing.current);
console.log(history.length - 1);
if (currentlyViewing.current !== history.length - 1) {
jumpTo(history.length - 1);
return;
}
if (gameOver === true) {
return;
}
if (squares[i] === "") {
const newSquares = squares.slice();
newSquares[i] = turn;
setSquares(newSquares);
if (turn === "X") {
setTurn("O");
} else {
setTurn("X");
}
isGameOver(newSquares);
history.push(newSquares);
currentlyViewing.current = history.length - 1;
}
console.log(currentlyViewing.current);
}

function jumpTo(step: number) {
setSquares(history[step]);
currentlyViewing.current = step;
}

return (
<Stack direction={"column"} alignItems={"center"}>
<Box height={"20vh"}></Box>
<Typography variant="h1" align="center">
Tic Tac Toe
</Typography>
<Typography variant="h3" align="center">
{gameOver ? gameOverMessage() : ""}
</Typography>
<Stack direction={"row"} spacing={1}>
<Board squares={squares} handleMove={handleMove} />
<MoveHistory history={history} jumpTo={jumpTo} currentlyViewing={currentlyViewing.current} />
</Stack>
</Stack>
);

function gameOverMessage() {
if (gameOver && winner === "") {
return "Draw!";
} else if (winner !== "") {
return `${winner} wins!`;
}
}
};

export default TicTacToe;
Loading