diff --git a/docs/topics/board/_category_.json b/docs/topics/board/_category_.json new file mode 100644 index 00000000..4c04947d --- /dev/null +++ b/docs/topics/board/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Boards", + "position": 6, + "link": { + "type": "generated-index", + "description": "Learn about the Boards." + } +} diff --git a/docs/topics/board/column-board.md b/docs/topics/board/column-board.md new file mode 100644 index 00000000..b399cdd1 --- /dev/null +++ b/docs/topics/board/column-board.md @@ -0,0 +1,23 @@ +# The Column Board + +The column board is the primary use case for the board, and the central way to create content in the SVS. + +## Structure + +The `ColumnBoard` is structured in three layers. It contains a number of `Columns`, each containing a number of `Cards`. Each of the cards can contain various `Elements`, which are the different forms of content, like `RichTextElement`, `LinkElement`, `FileElement` and so forth. + +The `ColumnBoard` can be either rendered as such with the columns next to each other, or as a `ListBoard` with the Columns acting as sections rendered below each other. + +## Elements + +The `ColumnBoard` supports a wide and expanding list of element types, including: + +- `RichTextElement` for both plain and rich text, based on the CKEditor 5. +- `LinkElement` for links to any website, with a cached preview image for privacy reasons. +- `FileElement` for storing files using our [FileStorage](../files-storage/How%20it%20works.md). +- `FileFolderElement` for storing a large number of files at once. +- `VideoConferenceElement` for starting video conferences using Big Blue Button +- `DrawingElement` for collaborative Whiteboards using tldraw +- `CollaborativeTextEditorElement` for using Etherpad +- `ExternalToolElement` for including LTI Tools +- `H5pElement` for including h5p content diff --git a/docs/topics/board/introduction.md b/docs/topics/board/introduction.md new file mode 100644 index 00000000..110d2376 --- /dev/null +++ b/docs/topics/board/introduction.md @@ -0,0 +1,23 @@ +# Overview + +## Introduction + +The "Board" (dubbed "Bereiche" in German) is a module for collaboratively editing structured content, intended to be used for preparing, teaching, and following up on lessons in a school context. + +It provides a flexible data structure which can be reused for different usecases. At the time of writing it is used by the ColumnBoard and the MediaBoard. + +The Board supports a variety of content elements, including rich text, files, links, and interactive elements, as well as structural elements like columns and cards, that can be freely rearranged via drag&drop. + +It is fully collaborative with live editing via websockets and adapts to different authorization contexts. + +## Status in Q1/2026 + +The board in its original intention is incomplete. Note that this section is subject to change and may be out of date. + +- Most notably missing is a "tasks" element, that would enable each viewer of the board to independently submit a solution to a challenge, to implement both homeworks and tasks during lessons +- We had planned refactorings towards a "trait" based system based on composable features, that would further separate the generic "board" implementation from the specific Nodetypes and Elements and their particular features. +- We had then planned a refactoring that would enable us to make (new) Element implementations external to the Board Module, providing full extendability without having to make changes to the core Boards implementation +- We had planned a number of technical features to further improve stability and conflict resolution when used by many people at once, most notably fractional indexing +- We had planned a number of performance optimizations, in particular in regards to authorisation and operations that only require small subsets of the board. +- We had planned a feature that would allow efficient retrieval and rendering of specific content from across multiple boards. Most notably, this would drive the "Task Overview", by showing a user all of his open tasks across all of his boards. +- We had planned a feature to configure access to parts of a board, like columns or cards, for example making them invisible to viewers, or enabling single users to edit them. diff --git a/docs/topics/board/technical-details.md b/docs/topics/board/technical-details.md new file mode 100644 index 00000000..f546369f --- /dev/null +++ b/docs/topics/board/technical-details.md @@ -0,0 +1,96 @@ +# Technical Details + +## Data Structure + +Each board is made of a tree of "BoardNodes", with a single Root. +Each Node is of a particular Type, which defines its structural and functional features. Most notably, each Nodetype defines what other Nodes can be its children, creating a fixed but easily changeable structure. + +```typescript +export class Column extends BoardNode { + + [...] + + canHaveChild(childNode: AnyBoardNode): boolean { + return childNode instanceof Card; + } +} +``` + +We separate between structural Nodes (eg. `ColumnBoard`, `Column`, `Card`) and the actual content that can be put on a card, colloquially called *elements* (eg. `RichTextElement`, `FileElement`, `LinkElement`, `H5pElement`), which are leaves of the tree. + +## Contexts + +Each board has a single context it belongs into. The context provides part of the Configuration of the board (enabling and disabling features) and provides the Users Roles for Authorization. + +The most important type of context is the [Room](../rooms/overview.md), but other potential parents include Courses and single Users for personal Boards. + +The exact type of the context is abstracted for the board and replaced with a common interface. + +The context is resolved through the `BoardContextResolverService`, which provides +a `PreparedBoardContext` containing the boardRoles of the users, as well as the configuration of a board. + +## Authorization + +Central to the authorization in the board is the `BoardNodeAuthorizable`, which can be built through the `boardNodeAuthorizableService`. +It is constructed with a specific user in mind, and contains all information required for the authorization including the users permissions on that specific board, and the settings of the board. + +The `BoardNodeRule` can operate in two different ways. First, it implements our [rules interface](../authorization/concept.md), which allows any external services to determine basic read and write permissions for any boardNodes. This is for example used by the [fileStorage](../files-storage/How%20it%20works.md) and other microservices to authorize access to external resources that belong to the board or one of its nodes. + +Secondly, we use an extended interface within the boardModule that allows checking permissions for specific operations on nodes. + +```typescript +const boardNodeAuthorizable = await this.boardNodeAuthorizableService.getBoardAuthorizable(board); + +throwForbiddenIfFalse(this.boardNodeRule.can('findBoard', user, boardNodeAuthorizable)); +``` + +The rule then contains the logic to authorize each operation against the boardNodeAuthorizable, and specifically what permissions are required for which operation. + +The rule can also provide the list of operations a specific user is authorized for on the board. This list is available to the frontend and used to show the user which operations he can perform. + +## Websockets + +The Websocket interface is implemented using [NestJS Gateways](https://docs.nestjs.com/websockets/gateways), with [Socket.IO](https://socket.io/) underneath. + +```typescript +@SubscribeMessage('update-board-title-request') +@EnsureRequestContext() +public async updateBoardTitle(socket: Socket, data: UpdateBoardTitleMessageParams): Promise { + const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-board-title' }); + const { userId } = this.getCurrentUser(socket); + try { + const board = await this.boardUc.updateBoardTitle(userId, data.boardId, data.newTitle); + emitter.emitToClientAndRoom(data, board); + } catch { + emitter.emitFailure(data); + } +} +``` + +Each operation on the board can be triggered with a message of the form `${action}-request`, like in the example `update-board-title-request`. + +The response is given with `${action}-success` or `${action}-failure`. + +Success messages are sent to all clients that are connected to the same board. To ensure this even when the clients are connected to different server instances, we use a MongoDB IO Adapter to synchronize messages. + +The failure messages are only sent to the client that triggered them. + +## Persistence Layer + +All BoardNodes, no matter their type, are stored in a single collection through a single entity. + +Each `BoardNodeEntity` represents a single node of the tree, and stores the entire path of its ancestors as a [Materialized Path](https://www.mongodb.com/docs/manual/tutorial/model-tree-structures-with-materialized-paths/), as well as its own level within in tree and its position among its siblings. This structure allows for efficient retrieval both of the chain of ancestors for a specific node, as well as all descendants of a node (by searching for an id within the paths.) + +The `BoardNodeEntity` can store all properties of all types of BoardNode. When the node is loaded and the DO is constructed by the repo the data is also validated to ensure it matches its corresponding type. + +## Loading + +Since Boards can contain a large amount of Content, it is designed to be loaded in multiple stages. + +At first, a `BoardSkeleton` is loaded, which contains only the structural nodes, ie. columns and cards. Each card stores its approximate height, allowing the frontend to render a loading stage where each card can be represented, without any content jumping around too much as the loading progresses. The cards themselves are loaded in batches as a second step + +Some Content Elements, like images, require data from a different source to be rendered, which is loaded separately. + +This spreading of data allows the board to render quickly, without having to wait for all downloads to complete. This is especially relevant for users in locations with slow connections and low bandwidth, as is the case in some schools. + +Utilizing a Peer to Peer approach to relieve bandwidth is intended, but not yet implemented. diff --git a/docs/topics/collabora/_category_.json b/docs/topics/collabora/_category_.json index bc788c21..0163a8e7 100644 --- a/docs/topics/collabora/_category_.json +++ b/docs/topics/collabora/_category_.json @@ -1,6 +1,6 @@ { "label": "Collabora", - "position": 6, + "position": 7, "link": { "type": "generated-index", "description": "Learn about the Collabora service." diff --git a/docs/topics/common-cartridge/_category_.json b/docs/topics/common-cartridge/_category_.json index 5f954b97..67c7c3d7 100644 --- a/docs/topics/common-cartridge/_category_.json +++ b/docs/topics/common-cartridge/_category_.json @@ -1,6 +1,6 @@ { "label": "Common Cartridge", - "position": 7, + "position": 8, "link": { "type": "generated-index", "description": "Learn about the Common Cartridge service." diff --git a/docs/topics/etherpad/_category_.json b/docs/topics/etherpad/_category_.json index 49b960a1..8480aaac 100644 --- a/docs/topics/etherpad/_category_.json +++ b/docs/topics/etherpad/_category_.json @@ -1,6 +1,6 @@ { "label": "Etherpad", - "position": 8, + "position": 9, "link": { "type": "generated-index", "description": "Learn about the etherpad repo." diff --git a/docs/topics/files-storage/_category_.json b/docs/topics/files-storage/_category_.json index 6271142a..5cf26afc 100644 --- a/docs/topics/files-storage/_category_.json +++ b/docs/topics/files-storage/_category_.json @@ -1,6 +1,6 @@ { "label": "Files Storage", - "position": 9, + "position": 10, "link": { "type": "generated-index", "description": "Learn about the files-storage repo." diff --git a/docs/topics/h5p/_category_.json b/docs/topics/h5p/_category_.json index 64eddb38..52eade6d 100644 --- a/docs/topics/h5p/_category_.json +++ b/docs/topics/h5p/_category_.json @@ -1,6 +1,6 @@ { "label": "H5P", - "position": 10, + "position": 11, "link": { "type": "generated-index", "description": "Learn about the H5P services & modules." diff --git a/docs/topics/ldap/_category_.json b/docs/topics/ldap/_category_.json index 5fd56a14..596bd50a 100644 --- a/docs/topics/ldap/_category_.json +++ b/docs/topics/ldap/_category_.json @@ -1,6 +1,6 @@ { "label": "LDAP", - "position": 11, + "position": 12, "link": { "type": "generated-index", "description": "Learn about the LDAP strategy." diff --git a/docs/topics/moin-punkt-schule/_category_.json b/docs/topics/moin-punkt-schule/_category_.json index d0a43cf1..060f9564 100644 --- a/docs/topics/moin-punkt-schule/_category_.json +++ b/docs/topics/moin-punkt-schule/_category_.json @@ -1,6 +1,6 @@ { "label": "Moin.Schule", - "position": 12, + "position": 13, "link": { "type": "generated-index", "description": "Learn about the moin.schule service." diff --git a/docs/topics/oauth/_category_.json b/docs/topics/oauth/_category_.json index acc482a7..a23713b3 100644 --- a/docs/topics/oauth/_category_.json +++ b/docs/topics/oauth/_category_.json @@ -1,6 +1,6 @@ { "label": "OAuth", - "position": 13, + "position": 14, "link": { "type": "generated-index", "description": "Learn about the OAuth strategy." diff --git a/docs/topics/provisioning/_category_.json b/docs/topics/provisioning/_category_.json index 631bb77c..de26ae47 100644 --- a/docs/topics/provisioning/_category_.json +++ b/docs/topics/provisioning/_category_.json @@ -1,6 +1,6 @@ { "label": "Provisioning", - "position": 14, + "position": 15, "link": { "type": "generated-index", "description": "Learn about the provisioning service." diff --git a/docs/topics/rooms/_category_.json b/docs/topics/rooms/_category_.json new file mode 100644 index 00000000..2540a2ea --- /dev/null +++ b/docs/topics/rooms/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Rooms", + "position": 16, + "link": { + "type": "generated-index", + "description": "Learn about the Rooms." + } +} diff --git a/docs/topics/rooms/img/rooms-structure.excalidraw.png b/docs/topics/rooms/img/rooms-structure.excalidraw.png new file mode 100644 index 00000000..8ae8bdb4 Binary files /dev/null and b/docs/topics/rooms/img/rooms-structure.excalidraw.png differ diff --git a/docs/topics/rooms/overview.md b/docs/topics/rooms/overview.md new file mode 100644 index 00000000..7727480f --- /dev/null +++ b/docs/topics/rooms/overview.md @@ -0,0 +1,79 @@ +# Overview + +## Introduction + +Rooms are digital spaces where people can interact and collaborate, designed both for teachers and students during and outside of lessons, and for collaboration between Teachers. + +Each room has a number of members in various roles and content in the form of collaborative columnboards, as well as some configuration that governs what features are available, how these features work, and what roles can interact with them. + +## Modules + +![Module Structure](./img/rooms-structure.excalidraw.png) + +There are five Modules working together in rooms. + +- The Rooms-API is the entry point for all requests regarding the Room (note that the boards have their own API). Read more about [API Modules](../../backend-design-patterns/architecture.md#api-modules) for more information. + +- The Room module is responsible for the Room object itself. It contains the configuration of the room, and references to all content that belongs to the room. + +- The Room-Membership module is essentially a bridge between `Groups` and `Rooms`. It utilizes the `Groups` module to store the users of a room and their roles, while containing any user related logic specific to rooms. It's responsible to construct the authorizables, which play a major part in the authorization checks for the room. + +- The [Boards Module](../board/introduction.md) is used to implement the content of the room. + +- The Groups module provides an abstraction to store groups of users with their context-specific roles. It is used to store which roles a user has in what room. + +## Roles + +The Room uses its own roles, that can be assigned independently of school roles. Note however that there might be business restrictions on who can have which role based on their school role, such as students never being owner of a room. + +The roles follow a linear hierarchy, meaning a higher role can do anything a lower role is able to do. The roles as defined by the requirements are as follows: + +### Room Owner + +There can always be only one owner. The owner has to be a teacher, ensuring that there is at least one teacher in the room for supervision. + +The owner has all permissions within the room, but can not leave without passing ownership to another teacher first. + +Should a room ever be without owner, no other user is allowed to access the room, until a school administrator has assigned a new owner. + +### Room Admin + +The admin can add and manage the users in the room and the settings of the room. + +To add students from another school to a room, it is necessary to grant a teacher from that school the Room Admin role, so that he can invite students from his school. + +### Room Editor + +The editor is someone who is responsible for and can edit all content in the room. + +In a school context, a teaching person should have at least the editor role. + +### Room Viewer + +The viewer is a consumer of the room. That generally means he can't edit content within the room, though some content may allow viewers to do so. + +For example, a board can be configured to allow room viewers to edit. + +In a school context, this is the role a student is intended to have. + +### Room Applicant + +When a user uses a room invitation link that requires confirmation, he is added as a room applicant. + +he is not yet a member of the room, but needs to be confirmed by an admin. + +## Cross-School Operation + +Each room is part of exactly one school. It is however possible to add users from other schools to a room, should that be necessary. + +When a user from another school is added to a room, he is also added as a *guest* to the school. Only Users with any role on the room's school, including guests, may access a given room. + +A user is only allowed to add another user when he is either in the same school as that user or if that user is a publicly visible teacher (based on the visibility settings of the instance and the user). + +Let's consider an example where `Teacher A` is owner of `Room A` at `School A`, and needs to add Users from `School B`. + +`Teacher A` is not allowed to see or add `Student B` from the other school. He can however add `Teacher B` from the other school (if that teacher is publicly visible), and give him the Room Admin role. + +`Teacher B` can then add more users, including `Student B` from his own school to the Room. + +Once in a Room, users can interact with the content of the room no matter their original school. diff --git a/docs/topics/schulcloud-client/_category_.json b/docs/topics/schulcloud-client/_category_.json index c72e46f0..e765df55 100644 --- a/docs/topics/schulcloud-client/_category_.json +++ b/docs/topics/schulcloud-client/_category_.json @@ -1,6 +1,6 @@ { "label": "Schulcloud Client", - "position": 15, + "position": 17, "link": { "type": "generated-index", "description": "Learn about the schulcloud-client repo." diff --git a/docs/topics/schulcloud-server/_category_.json b/docs/topics/schulcloud-server/_category_.json index d798fb35..0a98bf76 100644 --- a/docs/topics/schulcloud-server/_category_.json +++ b/docs/topics/schulcloud-server/_category_.json @@ -1,6 +1,6 @@ { "label": "Schulcloud Server", - "position": 16, + "position": 18, "link": { "type": "generated-index", "description": "Learn about the schulcloud-server repo." diff --git a/docs/topics/tldraw-server/_category_.json b/docs/topics/tldraw-server/_category_.json index f2859e39..dc3c5fc5 100644 --- a/docs/topics/tldraw-server/_category_.json +++ b/docs/topics/tldraw-server/_category_.json @@ -1,6 +1,6 @@ { "label": "Tldraw Server", - "position": 17, + "position": 19, "link": { "type": "generated-index", "description": "Learn about the tldraw-server repo." diff --git a/docs/topics/tools/_category_.json b/docs/topics/tools/_category_.json index d5411135..705fa4a9 100644 --- a/docs/topics/tools/_category_.json +++ b/docs/topics/tools/_category_.json @@ -1,6 +1,6 @@ { "label": "Tools", - "position": 18, + "position": 20, "link": { "type": "generated-index", "description": "Learn about tools." diff --git a/docs/topics/tsp-sync/_category_.json b/docs/topics/tsp-sync/_category_.json index 0c39c971..9188e603 100644 --- a/docs/topics/tsp-sync/_category_.json +++ b/docs/topics/tsp-sync/_category_.json @@ -1,6 +1,6 @@ { "label": "TSP Synchronisation", - "position": 19, + "position": 21, "link": { "type": "generated-index", "description": "Learn about the Synchronisation of Userdata from the 'Thüringer Schulportal'" diff --git a/docs/topics/vue-client/_category_.json b/docs/topics/vue-client/_category_.json index a978ddcb..b746d6ea 100644 --- a/docs/topics/vue-client/_category_.json +++ b/docs/topics/vue-client/_category_.json @@ -1,6 +1,6 @@ { "label": "Vue Client", - "position": 20, + "position": 22, "link": { "type": "generated-index", "description": "Learn about the vue-client repo."