diff --git a/source/conf.py b/source/conf.py index 02c0d25..9f99325 100644 --- a/source/conf.py +++ b/source/conf.py @@ -19,7 +19,7 @@ # -- Project information ----------------------------------------------------- project = 'F-sek' -copyright = '2024, F-sektionen inom TLTH' +copyright = '2025, F-sektionen inom TLTH' author = 'F-sektionen' # The short X.Y version diff --git a/source/examples/backend/backend.rst b/source/examples/backend/backend.rst new file mode 100644 index 0000000..690f335 --- /dev/null +++ b/source/examples/backend/backend.rst @@ -0,0 +1,286 @@ +.. _backend_tutorial: + +Backend Code Tutorial +===================== + +So you want to work on the backend of our website and app? Great! This tutorial will guide you through the basics of our backend code. You will be creating a simple data object and learning how to interact with it using CRUD (Create, Read, Update, Delete) operations. + +Preparation +----------- + +To work through this tutorial, you will need: + +- A local copy of the backend codebase. Install it using the `github README instructions`_. +- Git installed and configured on your machine. Use only the git part of :ref:`this guide `. + +.. _`github README instructions`: https://github.com/fsek/WebWebWeb + +How the Backend Works +--------------------- + +When a request comes into the backend (for example, when a user tries to load a page and sends a GET request), it goes through several layers before reaching the database and sending a response back to the user. Here's a simplified overview of the process: + +1. The request hits our server (``uvicorn``) and gets passed to FastAPI's application object in ``main.py``. +2. FastAPI matches the URL and HTTP verb to one of the routers in ``routes/`` (you will add ``fruit_router`` there later in this tutorial). +3. Dependencies run before the handler: we create a database session (``DB_dependency``) and check permissions (``Permission.require(...)`` when configured). +4. The route handler uses SQLAlchemy ORM models in ``db_models/`` to read or change rows in the database inside that session. +5. Pydantic schemas in ``api_schemas/`` validate incoming data and serialize outgoing data so responses have the shapes and types we expect. +6. The handler returns a Python object. FastAPI turns it into JSON, sets the HTTP status code, and sends the response back to the client. +7. If something goes wrong (for example, a fruit is not found), the handler raises ``HTTPException`` so FastAPI can send the right error code to the caller. + +Keep this mental model handy as you work through the steps below - each step in the tutorial plugs into one of these layers. + + +Create a Git Branch +-------------------- + +Git is great for keeping track of changes in code. You should always create a new branch when working on a new feature or bugfix. This keeps your changes organized and makes it easier for others to help you later on. First run this to make sure you are up to date with the latest changes, and branch off the main branch::: + + git checkout main + git pull origin main + +Now we create the branch of off main. You should run this in the terminal::: + + git checkout -b COOL-NAME-FOR-YOUR-BRANCH + +Replace ``COOL-NAME-FOR-YOUR-BRANCH`` with a descriptive name for your branch. + +Creating a Data Object +--------------------- + +Great! Now we are ready to start coding. We will be creating a simple data object called "Fruit" with attributes like "name" and "color". The first step is to define the data model. Go to the ``db_models`` directory and create a new file called ``fruit_model.py``. In this file, define the Fruit model using SQLAlchemy ORM::: + + from db_models.base_model import BaseModel_DB + from sqlalchemy import String + from sqlalchemy.orm import mapped_column, Mapped + + class Fruit_DB(BaseModel_DB): + __tablename__ = "fruit_table" + + id: Mapped[int] = mapped_column(primary_key=True, init=False) + + name: Mapped[str] = mapped_column() + + color: Mapped[str] = mapped_column() + + price: Mapped[int] = mapped_column() + + +This code defines a Fruit model with four attributes: id, name, color, and price. The import statements bring in the necessary SQLAlchemy components to define the model. You don't need to understand all the details for now, but I'll try to explain the important parts: + +- BaseModel_DB is the base class for all database models in our codebase. We primarily use it for timestamps and other common functionality. +- __tablename__ specifies the name of the database table that will store Fruit objects. This gets created automatically so you don't need to worry about it. +- Mapped and mapped_column are SQLAlchemy's way of saying “this class attribute is a database column.”. Most of the time, you can look at similar examples in other models to figure out how to define new attributes. +- The id attribute is special. Since names, colors and prices can be the same for different fruits, we need a unique ID which can be used to retrieve a specific fruit. Since id is marked primary_key, it has this function. We also mark it with init=False which tells SQLAlchemy not to expect this value when creating a new fruit object, as it is generated automatically by the database. + +You are very welcome to add another attribute if you want to! This will force you to think through the example code a little bit more and not simply copy/paste all the examples. Something simple like a boolean ``is_moldy`` might be suitable, and won't require you to change the example code too much. + +Now that we've got a basic model, we want to move on to: + +Database Schema +--------------- + +The database schema tells our backend server what types of things it should expect to receive and send out, so that it can perform type checking and tell us if something goes wrong right away. The schema will for example prevent sending a string "thirty one" as the price of the fruit. This part is pretty simple. Go to the ``api_schemas`` directory and add a new file ``fruit_schema.py``::: + + from api_schemas.base_schema import BaseSchema + + class FruitRead(BaseSchema): + id: int + name: str + color: str + price: int + + + class FruitCreate(BaseSchema): + name: str + color: str + price: int + + + class FruitUpdate(BaseSchema): + name: str | None = None + color: str | None = None + price: int | None = None + +.. note:: + **Why separate files?** You might wonder why we define the fields twice (once in ``db_models`` and once here). The **Model** represents the database table, while the **Schema** represents the public API. Keeping them separate allows us to hide internal database fields (like passwords or internal flags) from the public API. + +As you can see, we import the BaseSchema which gives us some nice basics, then we define three schemas for different operations and tell Pydantic (basically the type checker) what fields and types to expect. ``| None = None`` essentially says "If we don't get any value for this field, just pretend it's None.". This allows us to only include the fields we want to change when updating a fruit. Note that this doesn't mean the object in the database will be updated to have None in those fields, any changes to the database happen later in the router code. + +With the database schema done, we should not get any type errors when moving on to the next step: + +Creating a Router +----------------- + +The router defines what people are allowed to do with the fruits in our database. We will only add the CRUD (Create, Read, Update, Delete) operations, but it's possible to get a lot more creative with what the routes do. + +Let's start by creating the router file. This file will contain the four routes we will make for this tutorial. This is easily the most involved and complex part of the tutorial, routes can (and do!) get very long with a lot of complex logic to allow or forbid users from doing certain things with the database objects. This tutorial will try to keep things pretty simple, but remember you can always ask a su-perman if you feel something is especially confusing. + +All our routes are in the ``routes`` directory, create a new file ``fruit_router.py`` in there and add the following imports to that file. Don't worry too much about understanding these.:: + + from fastapi import APIRouter, HTTPException, status + from api_schemas.fruit_schema import FruitCreate, FruitRead, FruitUpdate + from database import DB_dependency + from db_models.fruit_model import Fruit_DB + from user.permission import Permission + + fruit_router = APIRouter() + +``fruit_router`` is now the router which will contain all of our individual routes, which we'll go through one by one now. + +Read +^^^^ + +We tend to start our router files with the Read route(s) for some reason. You should use something like this::: + + @fruit_router.get("/{fruit_id}", response_model=FruitRead) + def get_fruit(fruit_id: int, db: DB_dependency): + fruit = db.query(Fruit_DB).filter_by(id=fruit_id).one_or_none() + if fruit is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + return fruit + +I'll walk through this line by line::: + + @fruit_router.get("/{fruit_id}", response_model=FruitRead) + +This line tells FastAPI that this function is a GET route at the URL path /{fruit_id}. The {fruit_id} part is a variable that will be filled in when calling the route. The response_model=FruitRead part tells FastAPI (which tells Pydantic) to use the FruitRead schema to validate and serialize the response dat:: + + def get_fruit(fruit_id: int, db: DB_dependency): + +This line defines the function get_fruit which takes two parameters: fruit_id and db. The passing of db happens automatically and just connects the route to the database.:: + + fruit = db.query(Fruit_DB).filter_by(id=fruit_id).one_or_none() + +Now we query the database for a fruit with the given fruit_id. If no such fruit exists, one_or_none() will return None.:: + + if fruit is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + +If no fruit was found, we raise a 404 Not Found HTTP exception.:: + + return fruit + +If a fruit was found, we return it. FastAPI will automatically serialize it to JSON using the FruitRead schema since we specified that in the first line. + +Create +^^^^^^ + +We can now read fruit objects, so let's add a route to create new fruits!:: + + @fruit_router.post("/", response_model=FruitRead, dependencies=[Permission.require("manage", "User")]) + def create_fruit(fruit_data: FruitCreate, db: DB_dependency): + fruit = Fruit_DB( + name=fruit_data.name, + color=fruit_data.color, + price=fruit_data.price, + ) + db.add(fruit) + db.commit() + return fruit + +I'll explain this one line by line as well::: + @fruit_router.post("/", response_model=FruitRead, dependencies=[Permission.require("manage", "User")]) + +This line tells FastAPI that this function is a POST route at the URL path /. The response_model=FruitRead part tells FastAPI to use the FruitRead schema to validate and serialize the response data. The dependencies part ensures that only users with the "manage" permission for "User" can access this route. Note: Usually you really don't want to use "User" here, but adding a new permission target for "Fruit" is difficult for this tutorial so we use "User" for now.:: + + def create_fruit(fruit_data: FruitCreate, db: DB_dependency): + +Like before, we define the function create_fruit which takes two parameters: fruit_data and db. The fruit_data parameter is automatically populated by FastAPI from the request body using the FruitCreate schema.:: + + fruit = Fruit_DB( + name=fruit_data.name, + color=fruit_data.color, + price=fruit_data.price, + ) + +Here we create a new Fruit_DB object using the data from fruit_data.:: + + db.add(fruit) + db.commit() + +db.add(fruit) adds the new fruit to the database session, and db.commit() saves the changes to the database.:: + + return fruit + +Finally, we return the newly created fruit. FastAPI will serialize it to JSON using the FruitRead schema. In our frontend, this is rarely used since we usually GET all fruits at once after creating it to make sure we have the latest data. + +Okay, now we can move on to the Update route. This one will go a little faster since you should be getting the hang of it by now. + +Update +^^^^^^ + +Use this code::: + + @fruit_router.patch("/{fruit_id}", response_model=FruitRead, dependencies=[Permission.require("manage", "User")]) + def update_fruit(fruit_id: int, fruit_data: FruitUpdate, db: DB_dependency): + fruit = db.query(Fruit_DB).filter_by(id=fruit_id).one_or_none() + if fruit is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + # This does not allow one to "unset" values that could be null but aren't currently + for var, value in vars(fruit_data).items(): + if value is not None: + setattr(fruit, var, value) + + db.commit() + return fruit + +As you can see, we still only allow users with the "manage" permission for "User" to access this route. The rest of the code is similar to what we've seen before. The only new part is the loop that updates the fruit's attributes based on the data provided in fruit_data. If a value is None, we skip updating that attribute. ``setattr(fruit, var, value)`` is a built-in Python function that sets the attribute named var of the fruit object to the given value. + + +Delete +^^^^^^ + +You have all the knowledge needed to understand this last route. Here it is::: + + @fruit_router.delete("/{fruit_id}", response_model=FruitRead, dependencies=[Permission.require("manage", "User")]) + def delete_fruit(fruit_id: int, db: DB_dependency): + fruit = db.query(Fruit_DB).filter_by(id=fruit_id).one_or_none() + if fruit is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + db.delete(fruit) + db.commit() + return fruit + + +Add the Router to the Application +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The main program needs to know about your new router in order for it to work. Go to routes/__init__.py and add the following import at the top:: + + from routes.fruit_router import fruit_router + +Then, find the section where other routers are included and add this line:: + + app.include_router(fruit_router, prefix="/fruits", tags=["Fruits"]) + + +Great! You have now created all four CRUD routes for the Fruit model. This is a good time to take a step back and review what you've done. Next up is testing your new routes to make sure they work as expected. + + +Testing the Routes +------------------ + +You should always test your routes to make sure they work as you expect them to. We really encourage you to write automated tests for your routes (ask a su-perman if you need help with that), but the easiest way to test them quickly is to use the built-in Swagger UI that comes with FastAPI. Start the backend with the command ``uvicorn main:app --reload`` and open your web browser to ``http://localhost:8000/docs``. You should see the Swagger UI with a list of all available routes. You will have to log in first using the "Authorize" button in the top right corner to test the routes that require permissions. Ask a su-perman for the account details. + +.. tip:: + Testing can really help during development. Test your routes manually as you create them, and when you create pull requests always include automated tests to ensure your code works as expected and to prevent future changes from breaking it. (You can ask a su-perman or bot for help with writing tests!) + +Now that you're logged in, find your fruit routes in the list and test them out one by one. Make sure to test all four CRUD operations to ensure everything works as expected. + +If everything works, congratulations! You've successfully created a new data object with CRUD operations in our backend codebase. If you encounter any issues, don't hesitate to ask a su-perman for help. + + +Next Steps +---------- + +This is a really simple example meant to get you started. When you add new objects in the real codebase, it often helps to start from an existing similar object and modify it to fit your needs. You will also often need to add more complex logic to the routes, for example to handle relationships between different objects or to enforce more specific permission checks. If you want to continue this example, you could try adding: + +- Actually using "Fruit" as a permission target instead of "User". +- Writing automated tests for the fruit routes to ensure they work as expected. The more complex the routes get, the more important this becomes. +- Adding relationships to other models, for example a "Basket" model that can contain multiple fruits. +- Adding an is_moldy attribute which defaults to False and halves the price if toggled to True and doubles it if untoggled. + + + diff --git a/source/examples/examples.rst b/source/examples/examples.rst index 4b07119..18235c6 100644 --- a/source/examples/examples.rst +++ b/source/examples/examples.rst @@ -10,3 +10,5 @@ provide some understanding of underlying code mechanics. :Caption: Contents ./app/app + ./backend/backend + ./frontend/frontend diff --git a/source/examples/frontend/frontend.rst b/source/examples/frontend/frontend.rst new file mode 100644 index 0000000..4c200fb --- /dev/null +++ b/source/examples/frontend/frontend.rst @@ -0,0 +1,466 @@ +Frontend Code Tutorial +====================== + +.. + + "The frontend of the website is a pathway to many abilities some consider to be unnatural." + + -- Alan Turing + +In this tutorial, we will create an admin page for managing fruits using our backend API. Frontend code can be very verbose, so don't worry if you don't understand every line. The goal here is to give you a basic idea of how to set up a frontend page that interacts a backend object very similar to the one we created in :ref:`the backend tutorial `. You don't have to have done that tutorial to follow along here, all the code will be provided. + +.. note:: + This tutorial will focus on viewing and adding fruits only. While full CRUD (Create, Read, Update, Delete) operations are typically implemented in production applications, we'll keep this tutorial simpler by implementing only the viewing and adding functionality. If you want to add editing and deleting features later, you can look at other admin pages in the codebase for examples. + + +.. warning:: + The code from this tutorial is intentionally oversimplified. Don't use it as a basis for new features in production. + +Preparation +----------- + +To work through this tutorial, you will need: + +- A local copy of the frontend codebase. Install it using the `github README instructions`_. +- A local copy of the backend codebase. Install it using the `github README instructions for backend`_. +- Git installed and configured on your machine. Use only the git part of :ref:`this guide `. + +.. _`github README instructions`: https://github.com/fsek/WWW-Web +.. _`github README instructions for backend`: https://github.com/fsek/WebWebWeb + +How the Frontend Works +---------------------- + +The frontend is built using Next.js, a popular React framework for building web applications. It uses TypeScript, a typed superset of JavaScript that adds static types to the language. The frontend communicates with the backend and serves as the user interface for interacting with the backend API. Both the frontend and backend are processes that run on the server, but the frontend also sends code to the user's browser to be executed there. When a user clicks on a "save" button, their local client version of the frontend code sends a request directly to the backend API running on the main server to save the data. When they click on a link to view a page, the frontend code running on the server generates the HTML (it's a bit more complicated than that, but that's the basic idea) and sends it to the user's browser to be displayed. When you are building the frontend, you are therefore working both with code that runs on the server and code that runs on the client; if you're unsure which side a piece of code runs on, assume it must be safe for both. + +Create a Git Branch +------------------- + +Git is great for keeping track of changes in code. You should always create a new branch when working on a new feature or bugfix. This keeps your changes organized and makes it easier for others to help you later on. First run this to make sure you are up to date with the latest changes, and branch off the main branch::: + + git checkout main + git pull origin main + +Now we want to create the new branch. You should run this in the terminal::: + + git checkout -b COOL-NAME-FOR-YOUR-BRANCH + +Replace ``COOL-NAME-FOR-YOUR-BRANCH`` with a descriptive name for your branch. If you already have local changes, commit or stash them before switching branches to avoid conflicts. + +Starting the Backend +-------------------- + +A backend branch containing all the necessary changes to support the fruit admin page has already been created for you. You want to switch to that branch in your local backend repository. Run these commands in the terminal::: + + git checkout fruits-example-2026 + git pull origin fruits-example-2026 + +Now rebuild the backend (``Crtl+Shift+P`` in VSCode and select ``Dev Containers: Rebuild Container``) so that the new changes are applied. After rebuilding, start the backend server. You should check that it is running by opening ``http://localhost:8000/docs`` in your web browser. You should see the API documentation page and be able to see the ``/fruits/`` endpoint. This is what you'll be interacting with from the frontend. + +For the frontend to know about these changes, you have to regenerate the API specification in the frontend codebase. Go to the frontend repository and run this command in the terminal::: + + bun run generate-api + +This should automatically create new files in ``src/api/`` which the frontend will use to know how it can interact with the backend API. + +.. warning:: + If you forget to run ``bun run generate-api`` after pulling backend changes, the generated API client may be outdated and your queries/mutations will fail with confusing errors. + +Creating the Fruit Admin Page +----------------------------- + +After this tutorial, I recommend copying and modifying code from existing pages to create new pages, as this is often faster than writing everything from scratch. However, for this tutorial, I'll go through the file we want step by step so you can understand how it works. + +Our page has to be located in the right place so that next.js can serve it correctly. Create a new folder called ``fruits`` at ``src/app/admin/``. Inside that folder, create a new file called ``page.tsx``. This file will contain the main code for our fruit admin page. + +Open the file and add the following code to the top::: + + "use client"; + + import { ActionEnum, TargetEnum, type FruitRead } from "@/api"; + import { useSuspenseQuery } from "@tanstack/react-query"; + import { getAllFruitsOptions } from "@/api/@tanstack/react-query.gen"; + import { createColumnHelper, type Row } from "@tanstack/react-table"; + import AdminTable from "@/widgets/AdminTable"; + import useCreateTable from "@/widgets/useCreateTable"; + import { useTranslation } from "react-i18next"; + import { useState, Suspense } from "react"; + import PermissionWall from "@/components/PermissionWall"; + import { LoadingErrorCard } from "@/components/LoadingErrorCard"; + +This code imports all the necessary modules and components we will use in our page. The ``"use client";`` directive at the top tells Next.js that this file should run on the client side (i.e., in the user's browser), which is standard (and necessary) for interactive pages. + +This is a good time to give a brief overview of how the page will look when it's done. The page will display a table of fruits, allowing users to view and add fruit entries. Each fruit will have properties like name, color, and price. There will be a button above the table to add new fruits. + +The table needs a helper which keeps track of the columns and makes it easier to define them. Add this code below the imports::: + + const columnHelper = createColumnHelper(); + +As you can see, we are using the ``FruitRead`` type that was generated when we ran ``bun run generate-api`` earlier. This type represents the data structure of a fruit as returned by the backend API. + +The next thing we do is to define the columns of the table. Because these have multi language support, we need to do this inside the main component function so that we can use the translation hook. Add this code below the previous code::: + + export default function Fruits() { + const { t } = useTranslation("admin"); + const columns = [ + columnHelper.accessor("id", { + header: t("fruits.id"), + cell: (info) => info.getValue(), + }), + columnHelper.accessor("name", { + header: t("fruits.name"), + cell: (info) => info.getValue(), + }), + columnHelper.accessor("color", { + header: t("fruits.color"), + cell: (info) => info.getValue(), + }), + columnHelper.accessor("price", { + header: t("fruits.price"), + cell: (info) => info.getValue(), + }), + ]; + } + +``const { t } = useTranslation("admin");`` initializes the translation hook for the "admin" namespace, allowing us to use translated strings in our table headers. Each column is defined using ``columnHelper.accessor``, specifying the property of the ``FruitRead`` object to display, along with the header and cell rendering logic. + +.. tip:: + Always add translations to the components and pages you create. LibU will be really sad if we end up with a frontend that only works in Swedish. + +To display fruits, we need to fetch them from the backend API. We will use the ``useSuspenseQuery`` hook to do this. Add the following below the columns definitions, inside the ``Fruits`` function::: + + const { data, error } = useSuspenseQuery({ + ...getAllFruitsOptions(), + }); + +This fetches all the fruits from the backend API and puts it in the ``data`` variable. If there is an error during fetching, it will be stored in the ``error`` variable. + +.. note:: + A hook in React is a special function that lets you "hook into" React features from function components. They allow for things like state management (remembering values between renders) and side effects (performing actions like data fetching when the component renders or updates). + +We shall now define our table using a custom hook called ``useCreateTable``. Add this code below the previous code::: + + const table = useCreateTable({ data: data ?? [], columns }); + +This creates a table instance using the fetched data and the defined columns. The ``data ?? []`` syntax ensures that if ``data`` is undefined (e.g., while loading), an empty array is used instead to avoid errors. + +Great! Now we can render the actual page. Add this at the bottom of the ``Fruits`` function::: + + return ( + }> +
+

+ {t("admin:fruits.page_title")} +

+ +

{t("admin:fruits.page_description")}

+ + +
+
+ ); + +This code renders the page content. We use a ``Suspense`` component to handle loading states, it will show ``LoadingErrorCard`` while data is being fetched. Inside, we have a header (h3) and a paragraph (p) that use translated strings. Finally, we render the ``AdminTable`` component, passing in our table instance to display the fruit data. + +.. note:: + **What's this ``className`` stuff?** We are using a CSS framework called **Tailwind CSS** to style our components. The ``className`` attributes contain utility classes that apply specific styles, such as padding, font size, and colors. For example, ``px-8`` adds horizontal padding, ``text-3xl`` sets the text size to 3 times extra large, and ``text-primary`` applies the primary color defined in our theme. This is much easier than writing custom CSS for every component. + +You should now be able to see the fruit admin page by navigating to ``http://localhost:3000/admin/fruits`` in your web browser, after having started the frontend server with the commands::: + + bun install + bun run generate-api + bun run dev + +Since we haven't added any fruits yet, the page will show an empty table. The title and description should be visible, but will only show placeholder text since we have not added the translation keys we referenced yet. + + +Adding Fruits +------------- + + +Let's get started with adding the functionality to add new fruits. We will add a button above the table that opens a form for adding a new fruit. Create a new file in the same folder as ``page.tsx``, called ``FruitForm.tsx``. This file will contain the code for the form component. + +Imports +^^^^^^^ + +Start by adding the imports to the top of the file::: + + import { useState, useEffect } from "react"; + import { Button } from "@/components/ui/button"; + import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + } from "@/components/ui/dialog"; + import { useForm } from "react-hook-form"; + import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + } from "@/components/ui/form"; + import { Input } from "@/components/ui/input"; + import { zodResolver } from "@hookform/resolvers/zod"; + import { z } from "zod"; + import { useMutation, useQueryClient } from "@tanstack/react-query"; + import { + createFruitMutation, + getAllFruitsQueryKey, + } from "@/api/@tanstack/react-query.gen"; + import { Plus } from "lucide-react"; + import { useTranslation } from "react-i18next"; + +There's not much to add here yet. Note the imports of ``zod``, which we will use for form validation (checking that the user input is correct) and ``createFruitMutation``, which we will use to send the new fruit data to the backend API. + +Zod Schema +^^^^^^^^^^ + +Zod needs a schema to tell it how to validate the form data. Add this code below the imports::: + + const fruitSchema = z.object({ + name: z.string().min(1), + color: z.string().min(1), + price: z.number().min(0), + }); + +Here we forbid empty names and colors, and we make sure the price is a non-negative number. + +.. note:: + When using schema validation in the frontend, make sure it matches the validation rules in the backend. The backend API can be interacted with without using the website (e.g. using special tools like Postman), so the frontend should not be a layer of "security" but rather a way to improve user experience by catching errors early. + +Component Logic +^^^^^^^^^^^^^^^ + +Now we define the component itself. We need to manage the state of the dialog (open/closed) and the form submission status. We also initialize the form hook using the schema we just created. Add this code below the schema::: + + export default function FruitForm() { + const [open, setOpen] = useState(false); + const [submitEnabled, setSubmitEnabled] = useState(true); + const fruitForm = useForm>({ + resolver: zodResolver(fruitSchema), + defaultValues: { + name: "", + color: "", + price: 0, + }, + }); + const { t } = useTranslation("admin"); + const queryClient = useQueryClient(); + +The ``useState(false)`` call creates a state variable called ``open`` initialized to ``false``, along with a function ``setOpen`` that we can use to update it. This controls whether the popup dialog is visible or not. We do the same for ``submitEnabled``, which tracks whether the submit button should be clickable. The ``useForm`` hook initializes the form logic. The ``resolver: zodResolver(fruitSchema)`` part is really important because it connects the Zod validation rules we wrote earlier to the form, so the form knows when data is invalid and can show appropriate error messages. We're gonna use queryClient later to refresh the fruit list after adding a new fruit. + +Handling Data Submission +^^^^^^^^^^^^^^^^^^^^^^^^ + +To send data to the backend, we use a mutation. We also need a function to handle the form submission event. Add this inside the component::: + + const createFruit = useMutation({ + ...createFruitMutation(), + throwOnError: false, + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: getAllFruitsQueryKey() }); + setOpen(false); + setSubmitEnabled(true); + }, + onError: (error) => { + setSubmitEnabled(true); + }, + }); + +The ``useMutation`` hook comes from React Query. While ``useQuery`` is for fetching data from the backend, ``useMutation`` is specifically for changing data. As you can see, we have defined ``onSuccess`` and ``onError`` handlers. If the mutation is successful, we invalidate the fruit list query so that it gets refetched with the new data, close the dialog, and re-enable the submit button. If there is an error, we just re-enable the submit button so the user can try again. After this, add the ``onSubmit`` function::: + + function onSubmit(values: z.infer) { + setSubmitEnabled(false); + createFruit.mutate({ + body: { + name: values.name, + color: values.color, + price: values.price, + }, + }); + } + +The ``onSubmit`` function is special because it's only called by the form library if all validation passes. As soon as the function runs, we immediately disable the submit button by calling ``setSubmitEnabled(false)``. This prevents the user from clicking the button multiple times while the request is being processed, which could otherwise create duplicate fruits. Then we can call the mutation we defined earlier with the proper data. + +Resetting the Form +^^^^^^^^^^^^^^^^^^ + +When the user opens the dialog, we want to make sure the form is empty. We can use the ``useEffect`` hook to reset the form whenever the ``open`` state changes to true. Add this below the ``onSubmit`` function::: + + useEffect(() => { + if (open) { + fruitForm.reset({ + name: "", + color: "", + price: 0, + }); + } + }, [open, fruitForm]); + +The ``useEffect`` hook is designed to run code in response to changes. The array at the end, ``[open, fruitForm]``, is called the dependency array. React will run the code inside the effect whenever any of these variables change. In this case, whenever the dialog opens (when ``open`` becomes true), we reset all the form fields to their default empty values. This helps clear the data from the previous fruit submission. + +Rendering the Dialog Structure +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Finally, we need to render the UI. We'll start with the button that opens the dialog and the basic structure of the form dialog itself. The ``Form`` component acts as a context provider for ``react-hook-form``, passing down all the necessary methods to the input fields we will add later. Don't worry about understanding every line of this code, most of it is just boilerplate. + +Add this return statement at the end of the component::: + + return ( +
+ + + + + {t("fruits.create_fruit")} + +
+
+ + {/* Form fields will go here */} +
+ +
+
+
+ ); + } + +The ``Form`` component will contain all our form fields (like name, color and price) and handle most of the form logic for us so we don't need to worry about it. The ``Dialog`` component is controlled by the ``open`` state we defined earlier, so when we call ``setOpen(true)`` in the button's click handler, the dialog appears. Just like before, there is a lot of CSS styling via ``className`` attributes to make the dialog look nicer, you don't have to understand them. + +.. note:: + The component we are writing essentially comes in two parts: the button that opens the dialog, and the dialog itself. You can see the button component at the top of the return statement above, with the dialog just below it. The dialog contains the form structure, which we will complete next. + +Adding Input Fields +^^^^^^^^^^^^^^^^^^^ + +Now we need to add the actual input fields inside the ``
`` tag. We use the ``FormField`` component to connect our inputs to the form state. + +The ``FormField`` component takes a ``control`` prop (from our ``fruitForm`` hook) and a ``name`` prop (which must match a key in our Zod schema). The ``render`` prop is where the magic happens: it gives us a ``field`` object containing props like ``onChange``, ``onBlur``, and ``value``, which we spread onto our ``Input`` component. This automatically wires up validation and state management. + +Replace the comment ``{/* Form fields will go here */}`` with the following code::: + + ( + + {t("fruits.name")} + + + + + )} + /> + ( + + {t("fruits.color")} + + + + + )} + /> + ( + + {t("fruits.price")} + + + field.onChange(Number.parseFloat(e.target.value)) + } + /> + + + )} + /> +
+ +
+ +The ``render={({ field }) => ...}`` pattern might look a bit complex at first. It's what's called a "render prop" in React. Essentially, it's a function that returns JSX which can later be shown. The form library calls this function and passes it the ``field`` object, which contains everything the input needs to work correctly with the form. The syntax ``{...field}`` is JavaScript spread syntax, which is a shortcut for taking all properties inside ``field`` (like ``onChange``, ``value``, ``onBlur``) and adding them as props to the ```` component. Without this shortcut, we would have to write ``onChange={field.onChange} value={field.value} onBlur={field.onBlur}`` and so on, which gets repetitive quickly. Pay special attention to the ``price`` field's ``onChange`` handler. HTML inputs with ``type="number"`` actually return strings (like "10.5") rather than actual numbers, even though they look like numbers. Since our Zod schema expects a real number, we need to override the default ``onChange`` behavior to parse the string into a float using ``Number.parseFloat`` before saving it to the form state. + + +Phew! Our form component is now complete. Before we can use it, we need to actually add it to our fruit admin page. + + +Using the Form Component +^^^^^^^^^^^^^^^^^^^^^^^^ + +Go back to the ``page.tsx`` file we created earlier. We need to import the ``FruitForm`` component and add it above the table. Add this import at the top with the other imports::: + + import FruitForm from "./FruitForm"; + +Now, add the ```` component just above the ```` component in the return statement::: + + + + +That's it! You should now be able to open the fruit admin page in your web browser, click the "Create Fruit" button, fill out the form, and submit it. The new fruit should appear in the table after submission. If you get any errors or something doesn't work, just ask a su-perman and they will try to help you. + + +Next Steps +---------- + +Congratulations on completing the fruit admin page tutorial! You've learned how to create a new admin page, fetch data from the backend, display it in a table, and add a form for creating new entries. This is a solid foundation for building more complex admin pages in the future. As mentioned at the start, when you actually get to building new features, it's often faster to copy and modify existing code rather than writing everything from scratch. + +For now, there are some things you can optionally add to improve the page: + +- Add translations for all the translation keys we used in the page and form components. You can find the translation files in ``src/locales/en/admin.json`` and ``src/locales/sv/admin.json``. Add something like this to the bottom of both files: +.. + +:: + + "fruits": { + "id": "ID", + "name": "Name", + "color": "Color", + "price": "Price", + "page_title": "Fruit Management", + "page_description": "Manage fruits in the system", + "create_fruit": "Create Fruit", + "save": "Save" + } + +- Implement editing and deleting fruits. You can look at other admin pages in the codebase for examples of how to do this. Essentially, you will need to find the right API mutations and add support for clicking the table to edit or delete entries. +- Style the page further using Tailwind CSS to make it look nicer. +- Add error handling to show messages if something goes wrong during data fetching or submission. We tend to use toast notifications for this. Again, you can look at other admin pages for examples. diff --git a/source/installing_systems/installation_web/installation.rst b/source/installing_systems/installation_web/installation.rst index 23c2798..a566db8 100644 --- a/source/installing_systems/installation_web/installation.rst +++ b/source/installing_systems/installation_web/installation.rst @@ -1,5 +1,5 @@ - +.. _install_git: ============== Setting up Git ============== diff --git a/source/installing_systems/operating_systems.rst b/source/installing_systems/operating_systems.rst index 1ee6908..596e129 100644 --- a/source/installing_systems/operating_systems.rst +++ b/source/installing_systems/operating_systems.rst @@ -37,7 +37,7 @@ Which OS should I use? Windows Subsystems for Linux (WSL) ================================== -This might be the easisest and fastest way to get a Linux environment up and running. To install a WSL, simply head to the Windows Store and search +This might be the easiest and fastest way to get a Linux environment up and running. To install a WSL, simply head to the Windows Store and search for Ubuntu (there are many different Linux versions or distributions but Ubuntu is the most widely used). Simply download and install the app and then you need to run a command in PowerShell. Open PowerShell as an administrator (right click and select *Run as administrator*) and run the following command:: diff --git "a/source/pictures/fixa-snabbl\303\244nk.png" "b/source/pictures/fixa-snabbl\303\244nk.png" deleted file mode 100644 index 96fa1db..0000000 Binary files "a/source/pictures/fixa-snabbl\303\244nk.png" and /dev/null differ diff --git a/source/pictures/hitta-mailalias.png b/source/pictures/hitta-mailalias.png deleted file mode 100644 index 894c5fe..0000000 Binary files a/source/pictures/hitta-mailalias.png and /dev/null differ diff --git "a/source/pictures/hitta-snabbl\303\244nk.png" "b/source/pictures/hitta-snabbl\303\244nk.png" deleted file mode 100644 index 249be48..0000000 Binary files "a/source/pictures/hitta-snabbl\303\244nk.png" and /dev/null differ diff --git a/source/pictures/mailalias.png b/source/pictures/mailalias.png deleted file mode 100644 index 58d9640..0000000 Binary files a/source/pictures/mailalias.png and /dev/null differ diff --git a/source/pictures/medlemskap.png b/source/pictures/medlemskap.png deleted file mode 100644 index 80cc2e8..0000000 Binary files a/source/pictures/medlemskap.png and /dev/null differ diff --git "a/source/pictures/v\303\244gbeskrivning.png" "b/source/pictures/v\303\244gbeskrivning.png" deleted file mode 100644 index 67acd9f..0000000 Binary files "a/source/pictures/v\303\244gbeskrivning.png" and /dev/null differ diff --git a/source/spider_conference/spider_conference_2024/spider_conference_2024.rst b/source/spider_conference/spider_conference_2024/spider_conference_2024.rst index f028ef4..dba820e 100644 --- a/source/spider_conference/spider_conference_2024/spider_conference_2024.rst +++ b/source/spider_conference/spider_conference_2024/spider_conference_2024.rst @@ -1,4 +1,4 @@ Spider Conference 2024 ====================== -The Spider Conference 2024 was the most grand Spider Conference in the history of the Spidermans. To not spoil anything for the new Spiders this site will be updated later in 2025. \ No newline at end of file +The Spider Conference 2024 was the most grand Spider Conference in the history of the Spidermans. To not spoil anything for the new Spiders this site will be updated later in 2025 (or not). \ No newline at end of file diff --git a/source/spiderman_duties.rst b/source/spiderman_duties.rst index b2f8af4..cd2cc45 100644 --- a/source/spiderman_duties.rst +++ b/source/spiderman_duties.rst @@ -10,16 +10,11 @@ Godkänna användare Då medlemmar hör av sig till spindelmännen om att de inte har behörighet någonstans eller att de inte kan använda någon funktion på hemsidan är det troligtvis så att de inte är godkända användare. Detta är enkelt löst. -Steg 1: Gå in på ''Administrera''-menyn och välj ''Användare''. +Steg 1: Gå in till "Admin"-sidan och välj "Medlemmar". -.. image:: pictures/vägbeskrivning.png - :alt: Sätt att hitta användare +Steg 2: Sök upp personen i fråga. - -Steg 2: Klicka ''Ge medlemskap'' - -.. image:: pictures/medlemskap.png - :alt: Medlemskap +Steg 3: Klicka "Gör till medlem". Klar! @@ -28,59 +23,27 @@ Klar! Create mail alias ================= -There is a lot of members that what ot have their own cool mail alias for their blabla in the F-guild. Note that this is only an alias and **not** an new email. These aliases only forward mail to an already excisting email adress. To create a new mail alias you use the website. We can create aliases for name@fsektionen.se or name@farad.nu. - -**Step 1**: Log into the website and go to "Administrate" menu and choose "Mail aliases" in the "User" column. - -Det är många medlemmar som vill ha egna coola mailalias för sitt engagemang på F-sektionen. Observera dock att detta endast är ett alias och INTE en ny mailadress. Dessa mailalias bara vidarebefodrar till en existerande emailadress som medlemmen äger. Skapa mailalias gör du via hemsidan. Vi kan skapa mailalias för blablabla@fsektionen.se eller blablabla@farad.nu. - -Steg 1: Gå in på ''Administrera''-menyn och välj ''Mailalias''. +There are a lot of members that want to have their own cool mail alias for their posts or similar official positions in the F-guild. Note that this is only an alias and **not** an new email. These aliases only forward mail to an already excisting email adress. To create a new mail alias you use the website. We can create aliases for name@fsektionen.se (and maybe name@farad.nu). -.. image:: pictures/hitta-mailalias.png - :alt: Sätt att hitta användare +**Step 1**: Log into the website, go to the admin page and choose "Mail aliases" in the "Webmaster Only" category. +**Step 2**: Search to see if the mailalias already exists, otherwise add it. -Steg 2: Skriv in önskad mailalias i sökrutan och klicka sök. +**Step 3**: Add the persons personal email adress as a target of the source alias. Changes are saved automatically. -Steg 3: Skriv in mailadress som önskas kopplas till mailaliaset. Glöm inte att trycka på "Spara"-knappen! - -.. image:: pictures/mailalias.png - :alt: Sätt att skapa mailalias - - -Klar! +Done! ========================================== Ändra mailadresser kopplade till mailalias ========================================== -Steg 1: Gå in på ''Administrera''-menyn och välj ''Mailalias''. - - -Steg 2: Klicka på "Redigera"-knappen så öppnas ett fält med nuvarande mailadresser kopplade till mailaliaset. - -.. image:: pictures/hitta-mailalias.png - :alt: Sätt att hitta användare - - -Steg 3: Skriv till en mailadress på en ny rad eller ändra en befintlig. Glöm inte att trycka på "Spara"-ikonen! - -Klar! - - -=============== -Skapa snabblänk -=============== - -Steg 1: Gå in på ''Administrera''-menyn och välj ''Snabblänkar''. +Detta sker på ett sätt mycket likt ovanstående guide. -.. image:: pictures/hitta-snabblänk.png - :alt: Sätt att hitta snabblänk +Steg 1: Gå till mailaliassidan från adminsidan. -Steg 2: Skriv in namn på snabblänken i vänster fält och vart snabblänken ska leda i höger fält. Alltså, snabblänken kommer se ut fsektionen.se/google och när man klickar på denna så kommer man till www.google.com +Steg 2: Sök upp mailalias. -.. image:: pictures/fixa-snabblänk.png - :alt: Sätt att fixa snabblänk +Steg 3: Använd plus/minus knapparna eller klicka på ett namn för att ändra. Klar! @@ -92,7 +55,7 @@ Steg 1: Läs mailet. Steg 2: Tänk ut ett bra svar på mailet som är hjälpsamt. -Steg 3: Skriv detta bra-iga svar och tänk på att vidarebefodra svaret till alla spindelmän! +Steg 3: Skriv detta bra-iga svar och tänk på att vidarebefodra svaret till alla spindelmän! Använd "Svara alla" t.ex.! Steg 4: Du har nu +10000000 social credit points. @@ -102,7 +65,7 @@ Klar! Gå på möte ========== -Steg 1: Lägg in i din kalender vilka dagar som det är spindelmöte. Du kan också följa spindelkalendern. +Steg 1: Lägg in i din kalender vilka dagar som det är spindelmöte. Steg 2: Kom till spindelmötet. Var inte sen! diff --git a/source/web/naming.rst b/source/web/naming.rst index 1d497f4..3538704 100644 --- a/source/web/naming.rst +++ b/source/web/naming.rst @@ -7,14 +7,14 @@ Functions The Python functions in a route definition should be named descriptively. The name of the function is used as a description in Swagger, which makes it easy to understand what a route is supposed to do in Swagger if named clearly. For example, a function named ``create_example`` will get the description "Create Example" in Swagger. - **Use snake_case** for function names. -- Choose a name that describes the function’s purpose clearly. For instance, ``create_example`` is more descriptive than ``example_creation``. +- Choose a name that describes the function's purpose clearly. For instance, ``create_example`` is more descriptive than ``example_creation``. Schemas ------- For many different objects, a lot of basic schemas will be used for similar purposes. One common naming convention is to follow CRUD (Create, Read, Update, and Delete), which describes how the most common schemas should be named. -Let’s say we want to create some schemas for a database model called *Example*. For the different routes, the schemas should ideally be named: +Let's say we want to create some schemas for a database model called *Example*. For the different routes, the schemas should ideally be named: - **POST route**: ``ExampleCreate`` - **GET route**: ``ExampleRead`` @@ -26,4 +26,4 @@ All schemas should be written in **PascalCase**. Database Models --------------- -Database models should be written in **PascalCase** with the suffix ``_DB``. For example: ``C +Database models should be written in **PascalCase** with the suffix ``_DB``. For example: ``CrazyChairs_DB``.