From f7fd2a7f78269289dcf9c4c883fb38de20c65654 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:32:00 +0200 Subject: [PATCH 01/36] wip, one request, one team, via foreign keys --- README.md | 300 +----------------- .../src/controllers/application-controller.ts | 3 +- backend/src/controllers/dto.ts | 8 +- backend/src/entities/team.ts | 41 ++- backend/src/entities/user.ts | 6 + backend/src/services/application-service.ts | 9 - backend/src/services/project-service.ts | 4 +- backend/src/services/rating-service.ts | 20 +- backend/src/services/team-service.ts | 122 ++++--- backend/src/utils/has-same-elements.ts | 6 + .../project-service-get-all-projects.spec.ts | 16 +- backend/test/services/team-service.spec.ts | 2 +- quickstart.md => docs/docker-development.md | 0 docs/docs.md | 292 +++++++++++++++++ .../src/components/pages/read-only-team.tsx | 4 + .../src/components/pages/view-project.tsx | 2 +- frontend/src/components/pages/view-team.tsx | 38 +-- 17 files changed, 452 insertions(+), 421 deletions(-) create mode 100644 backend/src/utils/has-same-elements.ts rename quickstart.md => docs/docker-development.md (100%) create mode 100644 docs/docs.md diff --git a/README.md b/README.md index fd2eeeda..f97463b6 100644 --- a/README.md +++ b/README.md @@ -5,303 +5,11 @@ [![David](https://img.shields.io/david/hackaburg/tilt)](https://github.com/hackaburg/tilt) [![GitHub license](https://img.shields.io/github/license/hackaburg/tilt.svg)](https://github.com/hackaburg/tilt/LICENSE) -Yet another hackathon registration system. +Hackathon Registration System -[Docker Development quickstart.md](quickstart.md) - -## Motivation +- [Documentation](docs/docs.md) +- [Docker Development Quickstart](docs/docker-development.md) Like many other hackathons, we previously used [Quill](https://github.com/techx/quill) for our application process, which worked really well for us in the past. Especially Quill's process was a blessing: an application consists of two steps, the profile creation and, once an attendee was admitted to the event, the spot confirmation. We attended different events that used different processes and found this to be easy for both the attendees and organizers. -Faced with maintaining our [fork](https://github.com/hackaburg/quill) with our set of changes to the application process, as well as maintaining an Angular.JS and an untyped Express backend, we wanted to build ourselves a registration system that matched our needs on a tech stack we're more familiar with. - -## How does tilt work? - -Similar to Quill, tilt's application process consists of the profile creation and a confirmation form. tilt, among other differences, employs a role-based model: - -- An attendee is a `user` who can fill out the forms -- A `moderator` can see applications, statistics and admit users -- `root` can modify tilt's settings - -`root` can do whatever a `moderator` can do, and a `moderator` can do whatever a `user` can do. This means each user group needs to register through the same form first and you can adjust a user's group later. During registration, tilt queries [haveibeenpwned](https://haveibeenpwned.com) to check for password breaches. - -After the setup, `root` can configure tilt's appearance, as well as the application process: - -- When should the profile form be made available? -- Until when can users submit their profile form? -- How long can users confirm their spots? - -The former two points are represented as dates, whereas the latter is represented in hours. When a user is admitted, they'll have `n` hours to confirm their spot. After this deadline passed, their application will show up as "expired". - -Each application consists of two forms, which `root` can configure. Questions have a title, a Markdown description and a type. tilt currently supports text, number, choices and country questions, which each have different configuration options. Each question can however have a parent question, which allows you to build complex, tree-like questionnaires dynamically. - -Admitting users is done through the admission page, which consists of a table and a search bar. You can search for any info an attendee might've provided and also filter for special flags such as `is:admitted` or `is:expired`. - -To admit someone, check the box on the left for as many people as you want and hit "admit". tilt will send them an email, which `root` can configure as well. The top-most checkbox depends on the visible checkboxes below. Clicking it will either select or deselect all visible rows, leaving your not shown selection intact. - -When you add a question to the profile form because you need to ask, e.g., for new terms and conditions and a user already filled out this form, there's almost no chance they'd open up their application and answer this question without you explicitly telling them to. - -Disregarding which type of question you want to add, users need to confirm their spot using the second form anyways. tilt adds all new profile questions a user cannot have seen initially to the confirmation form. This way, users need to answer these questions at last during the confirmation step and if they don't agree with, e.g., newly added terms, their spot will expire. - -## Usage - -tilt was built to be deploy-once-and-forget. For this reason, we provide a Docker image `hackaburg/tilt`. While you could run tilt natively, we highly recommend a containerized setup, as you can update it more easily. - -At Hackaburg, we run our server setup through a central NGINX proxy facing the internet and routing requests to individual containers. Given you might want to access tilt through an url like `https://your-hackathon.com/apply`, we'll show both the NGINX setup, as well as a Docker Compose configuration supporting such a setup. - -### Docker Compose - -tilt uses environment variables for configuration. There are three sections you need to configure: - -1. General settings: things like tilt's port, the URL tilt is made available on, as well as the [JWT](https://jwt.io) secret. The latter should be a randomly generated string, as this is used to authenticate users against tilt's API and an easily guessed secret might allow impersonating `root` and `moderator` accounts - -2. Mail settings: internally, tilt uses [nodemailer](https://nodemailer.com/) to send out emails. Therefore, configure an SMTP server and supply its TLS/SSL port, e.g., 465 - -3. Database settings: tilt needs to store data somewhere. Internally, we use TypeORM, which is database-agnostic, but we chose to use the [MySQL](https://www.mysql.com) / [MariaDB](https://mariadb.org) driver. While you could use some other database, these two are supported by default. Furthermore, you could use tools like [phpMyAdmin](https://www.phpmyadmin.net) to manage your database, but we don't recommend this in production. Please refer to the official documentation for whichever database you deploy - -```yaml -version: "3" - -services: - # the internet-facing proxy - proxy: - image: nginx/alpine - restart: always - ports: - - 80:80 - volumes: - # we'll configure nginx later. since you're probably - # already configuring a proxy yourself, we'll just show - # an example `server` block - - ./nginx.conf:/etc/nginx/conf.d/default.conf - - # tilt's persistence layer requires a mysql-like database - # we'll use mariadb here, but you can also use mysql - tilt_mariadb: - image: mariadb:latest - restart: always - volumes: - - ./tilt/mariadb:/var/lib/mysql - environment: - # tilt doesn't need root privileges on the database, - # therefore a regular user account and a random root - # password is the better choice - - MARIADB_RANDOM_ROOT_PASSWORD=yes - # these two don't need to be called "tilt", but this is - # a tilt example - - MARIADB_DATABASE=tilt - - MARIADB_USER=tilt - # genereate a password, as this one is shown publicly - # in this repository - - MARIADB_PASSWORD=this_is_not_secure_generate_something - - tilt: - image: hackaburg/tilt - restart: always - environment: - # the url under which you can reach tilt - - BASE_URL=https://your-hackathon.com/apply/ - # tilt's http port, which we need in nginx later. as - # tilt runs in a user account, it can't bind to port - # 80, therefore, you will still need some kind of - # proxy to expose tilt on port 80 - - PORT=3000 - - LOG_LEVEL=info - # generate a secure password for jwt tokens - - SECRET_JWT=this_is_not_secure_generate_a_password - # an smtp server - - MAIL_HOST=your-smtp.server - # tilt requires tls/ssl for mailing - - MAIL_PORT=465 - # your smtp username - - MAIL_USERNAME=your-email@domain.tld - # the smtp username's password - - MAIL_PASSWORD=password - # this section is similar to the mariadb configuration - # above. simply place username, database and password - # settings from above here - - DATABASE_NAME=tilt - - DATABASE_HOST=tilt_mariadb - - DATABASE_USERNAME=tilt - - DATABASE_PASSWORD=this_is_not_secure_generate_something - - DATABASE_PORT=3306 - - # to keep your containers up-to-date, we recommend using - # a service like watchtower, which continuously pulls for - # updates. see https://github.com/containrrr/watchtower for - # more information - watchtower: - image: containrrr/watchtower - restart: always - volumes: - - /var/run/docker.sock:/var/run/docker.sock -``` - -### NGINX - -With the environment configuration in place, we can configure NGINX to forward requests to tilt. This step is optional and you can technically expose tilt to the internet through the `PORT` environment variable. If you, like us, want to expose tilt through a subfolder URL, you can use a configuration similar to this: - -```nginx -server { - # the domain this server should listen to - server_name your-hackathon.com; - # the port we expose in the docker compose configuration above - listen 80; - - # we expect the entirety of the uri after the slash at tilt - # therefore we redirect /apply requests to /apply/ - location = /apply { - return 301 /apply/; - } - - # the actual forwarding block passing the requests to tilt - # watch the trailing slashes, or you might run into issues - location /apply/ { - # this forwards requests to the tilt container on port 3000, - # which we configured previously - proxy_pass http://tilt:3000/; - } -} -``` - -### Starting up - -Since database takes a brief moment for its setup, start with: - -```bash -$ docker compose up tilt_mariadb -``` - -Then wait for MariaDB to accept incoming connections. Once this is done, you can stop `docker-compose` and start up everything with: - -```bash -$ docker compose up -``` - -Depending on your setup, you might want to append the `-d` flag to run the containers in the background. - -tilt should then be able to connect to the database and start up. If the database takes longer than tilt to start, tilt will restart because of the `restart: always` directive until it can connect successfully. - -### Changing user groups - -With everything up and running, you usually want to configure tilt. For this, you need to register and change your user group. - -Since you have access to the server running tilt, you can spawn a shell in the tilt container and invoke the [usermod script](backend/src/usermod.ts). Please note that we're using `node:alpine` as a base image and therefore don't ship Bash. This script takes two arguments, the email of the user you want to change, as well as the group you want to assign to this user. To assign the `root` group to `you@example.com`, run: - -```bash -$ docker compose exec tilt sh -node@container:/app$ node backend/usermod.js you@example.com root -``` - -The logs will indicate success or failure and this process works similarly for `moderator`, or for demoting an account back to `user`. - -tilt's UI fetches the user role during the initial load. To see the settings, admission and statistics pages, you'll need to reload the page. - -### Environment variables - -tilt's backend can be configured through a set of environment variables. We try to keep this list up-to-date, but for the most recent set of variables check the [config-service.ts](backend/src/services/config-service.ts). All defaults and shown values are strings, but tilt parses them internally to, e.g., integers or booleans. - -#### App variables - -- `NODE_ENV` - in production mode, tilt reports all uncaught errors as "Internal error" - - value: `production` or `development` - - optional, default: `development` - -#### Database variables - -- `DATABASE_NAME` - the name of the database tilt should connect to - - value: string -- `DATABASE_HOST` - the host serving tilt's database - - value: hostname or IP address -- `DATABASE_PASSWORD` - the password for the database user - - value: string -- `DATABASE_PORT` - the port number of the database - - value: integer - - optional, default: `3306` -- `DATABASE_USERNAME` - the user for the database - - value: string - -#### HTTP variables - -- `BASE_URL` - the url under which tilt will be deployed, something in the sorts of `https://your-hackathon.com/apply` - - value: string -- `PORT` - the http port to listen on - - value: integer - - optional, default: `3000` - -#### Logging variables - -- `LOG_FILENAME` - tilt supports writing its log messages to files; supply a filename to persist log messages - - value: filename - - optional, default: `tilt.log` -- `LOG_LEVEL` - the level of logs tilt should output - - value: `debug`, `info` or `error` - - optional, default: `info` -- `LOG_SLACK_WEBHOOK_URL` - tilt supports sending errors to a Slack channel; supply an URL to message the configured channel - - value: Slack webhook URL - - optional - -#### Mail variables - -- `MAIL_HOST` - an SMTP server to use for mailing - - value: hostname -- `MAIL_PASSWORD` - the password for the SMTP account - - value: string -- `MAIL_PORT` - the port for the SMTP server; tilt requires SSL/TLS - - value: integer - - optional, default: `465` -- `MAIL_USERNAME` - the username for the SMTP account - - value: string - -#### Secrets variables - -- `SECRET_JWT` - a secret used to sign login tokens - - value: string - -#### Service variables - -- `ENABLE_HAVEIBEENPWNED_SERVICE` - enable or disable checking password reuse with [haveibeenpwned.com](https://haveibeenpwned.com) - - value: `true` or `false` - - optional, default: `true` - -## Contributing - -If you found a bug or have an idea for a feature, simply [submit an issue](https://github.com/hackaburg/tilt/issues/new) or a pull request. We use [TSLint](https://palantir.github.io/tslint/) and [Prettier](https://prettier.io) to ensure consistent code styles and we have a set of unit tests for the backend in place to prevent things from breaking too easily. Also, we currently use a [GitHub project](https://github.com/hackaburg/tilt/projects/1) for our roadmap. - -### Developing locally - -The tilt repository ships with a [docker-compose.yml](docker-compose.yml), which includes a sample setup with MariaDB, the test SMTP server [MailDev](https://github.com/maildev/maildev) and phpMyAdmin. To mimic the proxy'd setup, it also includes build instructions for a tilt container, as well as an NGINX container. You usually only need `db`, `phpmyadmin` and `maildev`, therefore it's sufficient to start them using: - -```bash -docker compose up db phpmyadmin maildev -``` - -For local development, the backend supports reading `.env` files. Refer to [`.env.example`](backend/.env.example) for such a configuration and match the ports from the Docker Compose configuration. - -```bash -cp backend/.env.example backend/.env -``` - -You can then start the backend using: - -```bash -yarn backend::start -``` - -As the frontend is built in modern React, we use [Webpack](https://webpack.js.org) and its devserver to develop. The frontend is available at localhost:8080. The backend runs on a different port, so we need to tell the frontend how to reach the backend. This can be done through the `API_BASE_URL` environment variable. To start the frontend devserver with the backend listening on port 3000, simply provide it using: - -```bash -API_BASE_URL=http://localhost:3000/api yarn frontend::start -``` - -After registering, open localhost:8082 to view the verification mail. - -We also provide a set of utility scripts in our [package.json](package.json)'s `script` section, such as linting, formatting and type-checking. - -### Building images - -Our final image is built using the `backend::build` and `frontend::build` scripts and stripping development dependencies as well as source files from the final container. To allow arbitrary base urls with a statically built frontend, we transiently replace this url during container startup. Please refer to the [Dockerfile](Dockerfile) and [entrypoint.sh](entrypoint.sh) for more information. - -## License - -tilt is released under the [MIT License](LICENSE). +Faced with maintaining our [fork](https://github.com/hackaburg/quill) with our set of changes to the application process, as well as maintaining an Angular.JS frontend and an untyped Express backend, we wanted to build a registration system ourselves, that matched our needs on a tech stack we're more familiar with. diff --git a/backend/src/controllers/application-controller.ts b/backend/src/controllers/application-controller.ts index 07695250..df0f2f99 100644 --- a/backend/src/controllers/application-controller.ts +++ b/backend/src/controllers/application-controller.ts @@ -260,9 +260,10 @@ export class ApplicationController { @Authorized(UserRole.User) public async createTeam( @Body() { data: teamDTO }: { data: TeamRequestDTO }, + @CurrentUser() user: User, ): Promise { const team = convertBetweenEntityAndDTO(teamDTO, Team); - const createdTeam = await this._teams.createTeam(team); + const createdTeam = await this._teams.createTeam(team, user); return convertBetweenEntityAndDTO(createdTeam, TeamDTO); } diff --git a/backend/src/controllers/dto.ts b/backend/src/controllers/dto.ts index b050f605..f892ec6e 100644 --- a/backend/src/controllers/dto.ts +++ b/backend/src/controllers/dto.ts @@ -519,7 +519,9 @@ export class TeamDTO { @Expose() public title!: string; @Expose() - public users?: string[]; + @Type(() => UserDTO) + @ValidateNested() + public users!: UserDTO[]; @Expose() public teamImg!: string; @Expose() @@ -533,14 +535,14 @@ export class TeamResponseDTO { public title!: string; @Expose() @Type(() => UserResponseDto) - public users?: UserResponseDto[]; + public users!: UserResponseDto[]; @Expose() public teamImg!: string; @Expose() public description!: string; @Expose() @Type(() => UserResponseDto) - public requests?: UserResponseDto[]; + public requests!: UserResponseDto[]; } export class TeamRequestDTO { diff --git a/backend/src/entities/team.ts b/backend/src/entities/team.ts index 7dbcc318..f685082b 100644 --- a/backend/src/entities/team.ts +++ b/backend/src/entities/team.ts @@ -1,5 +1,6 @@ -import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { Column, Entity, PrimaryGeneratedColumn, OneToMany } from "typeorm"; import { Longtext } from "./longtext"; +import { User } from "./user"; @Entity() export class Team { @@ -7,15 +8,41 @@ export class Team { public readonly id!: number; @Column({ length: 1024 }) public title!: string; - // TODO many-to-many instead of simple-array (array of userId strings) - @Column("simple-array") - public users!: string[]; // TODO rename teamImg to image @Column() public teamImg!: string; @Longtext() public description!: string; - // TODO many-to-many instead of simple-array (array of userId strings) - @Column("simple-array") - public requests!: string[]; + @OneToMany(() => User, (user) => user.teamRequest) + public requests!: User[]; + @OneToMany(() => User, (user) => user.team) + public users!: User[]; + + /** + * List of user ids that are part of the team. + */ + public userIds(): number[] { + if (!this.users) { + return []; + } + + // TODO does this work? + console.log("### users", this.users); + + return this.users.map(({ id }) => id); + } + + /** + * List of user ids that requested to join the team. + */ + public requestUserIds(): number[] { + if (!this.requests) { + return []; + } + + // TODO does this work? + console.log("### requests", this.requests); + + return this.requests.map(({ id }) => id); + } } diff --git a/backend/src/entities/user.ts b/backend/src/entities/user.ts index c82c92a0..b23480e3 100644 --- a/backend/src/entities/user.ts +++ b/backend/src/entities/user.ts @@ -4,8 +4,10 @@ import { Entity, PrimaryGeneratedColumn, UpdateDateColumn, + ManyToOne, } from "typeorm"; import { UserRole } from "./user-role"; +import { Team } from "./team"; @Entity() export class User { @@ -45,4 +47,8 @@ export class User { public declined!: boolean; @Column({ default: false }) public checkedIn!: boolean; + @ManyToOne(() => Team, (team) => team.requests, { nullable: true }) + public teamRequest: Team | null = null; + @ManyToOne(() => Team, (team) => team.users, { nullable: true }) + public team: Team | null = null; } diff --git a/backend/src/services/application-service.ts b/backend/src/services/application-service.ts index de296544..56ca9b26 100644 --- a/backend/src/services/application-service.ts +++ b/backend/src/services/application-service.ts @@ -18,7 +18,6 @@ import { } from "./question-service"; import { ISettingsService, SettingsServiceToken } from "./settings-service"; import { IUserService, UserServiceToken } from "./user-service"; -import { Team } from "../entities/team"; /** * A form containing questions and given answers. @@ -41,7 +40,6 @@ export interface IRawAnswer { */ export interface IApplication { user: User; - teams: string[]; answers: readonly Answer[]; } @@ -121,7 +119,6 @@ export const ApplicationServiceToken = new Token(); @Service(ApplicationServiceToken) export class ApplicationService implements IApplicationService { private _answers!: Repository; - private _teams!: Repository; constructor( @Inject(QuestionGraphServiceToken) @@ -138,7 +135,6 @@ export class ApplicationService implements IApplicationService { */ public async bootstrap(): Promise { this._answers = this._database.getRepository(Answer); - this._teams = this._database.getRepository(Team); } /** @@ -545,13 +541,8 @@ export class ApplicationService implements IApplicationService { } } - const allTeams = await this._teams.find(); - const applications = allUsers.map((user) => ({ answers: answersByUserID.get(user.id) ?? [], - teams: allTeams - .filter((team) => team.users.toString().includes(user.id.toString())) - .map((team) => team.title), user, })); diff --git a/backend/src/services/project-service.ts b/backend/src/services/project-service.ts index 2d901904..7740431b 100644 --- a/backend/src/services/project-service.ts +++ b/backend/src/services/project-service.ts @@ -68,7 +68,7 @@ export class ProjectService implements IProjectService { public async getAllProjects(user: User): Promise { const teams = await this._teams.find(); const teamIds = teams - .filter((team) => team.users.includes(user.id.toString())) + .filter((team) => team.userIds().includes(user.id)) .map((team) => team.id); const [settings] = await this._settings.find(); @@ -157,7 +157,7 @@ export class ProjectService implements IProjectService { } const team = project.team; - if (!team || !team.users.includes(user.id.toString())) { + if (!team || !team.userIds().includes(user.id)) { // Tried to access a project belonging to a different team, forbidden throw new NotFoundError(); } diff --git a/backend/src/services/rating-service.ts b/backend/src/services/rating-service.ts index 4cabd694..7d918e75 100644 --- a/backend/src/services/rating-service.ts +++ b/backend/src/services/rating-service.ts @@ -7,6 +7,7 @@ import { ISettingsService, SettingsServiceToken } from "./settings-service"; import { Rating } from "../entities/rating"; import { RatingDTO, + ProjectDTO, ProjectRatingResultDTO, convertBetweenEntityAndDTO, } from "../controllers/dto"; @@ -219,7 +220,7 @@ export class RatingService implements IRatingService { }); result.push({ - project, + project: convertBetweenEntityAndDTO(project, ProjectDTO), averagesPerCriterion, }); } @@ -235,6 +236,21 @@ export class RatingService implements IRatingService { throw new ForbiddenError("Only admitted users may rate projects"); } + // TODO only users in a team are allowed to vote. Prevent them from + // leaving their team to vote for themselves. However, they could leave + // their team and create a new one. So they need to be in a team that has + // been created before the rating started. I don't track the timestamp yet... + // Prevent teams from changing once rating starts is the best bet. Use the + // existing switch and prevent changes, add some text to the settings page that + // this is the case to avoid confusion. Still, people could join a second team + // before voting starts, in order to vote for their own project. I don't think + // it is possible to avoid this situation. It needs to be prohibited via the + // rules and it needs a way to check if this happened, but the ratings are + // anonymous. HOORAY. Or we allow people to rate their own project after all. + if (user.team == null) { + throw new ForbiddenError("You need to be part of a team to vote"); + } + const settings = await this._settings.getSettings(); if (!settings.application.allowRatingProjects) { throw new ForbiddenError( @@ -254,7 +270,7 @@ export class RatingService implements IRatingService { if (!team) { throw new NotFoundError("Team not found"); } - if (team.users.includes(user.id.toString())) { + if (team.userIds().includes(user.id)) { throw new ForbiddenError("You can't rate your own project"); } diff --git a/backend/src/services/team-service.ts b/backend/src/services/team-service.ts index 568a4738..4bc6180f 100644 --- a/backend/src/services/team-service.ts +++ b/backend/src/services/team-service.ts @@ -1,3 +1,4 @@ +import { NotFoundError } from "routing-controllers"; import { Inject, Service, Token } from "typedi"; import { Repository } from "typeorm"; import { IService } from "."; @@ -9,6 +10,7 @@ import { convertBetweenEntityAndDTO, } from "../controllers/dto"; import { User } from "../entities/user"; +import { hasSameElements } from "../utils/has-same-elements"; /** * An interface describing user handling. @@ -21,7 +23,7 @@ export interface ITeamService extends IService { /** * Create new team */ - createTeam(team: Team): Promise; + createTeam(team: Team, user: User): Promise; /** * Update team */ @@ -79,7 +81,9 @@ export class TeamService implements ITeamService { * Gets all teams. */ public async getAllTeams(): Promise { - return this._database.getRepository(Team).find(); + return this._database.getRepository(Team).find({ + relations: ["users", "requests"], + }); } /** @@ -99,18 +103,26 @@ export class TeamService implements ITeamService { throw new Error("Please add at least one user to the team"); } - const originTeam = await this._teams.findOneBy({ id: team.id }); - const originTeamUsers = originTeam?.users.map((id) => id.toString()); + const originalTeam = await this._teams.findOne({ + where: { id: team.id }, + relations: ["users", "requests"], + }); - if (!originTeamUsers!.includes(user.id.toString())) { + if (!originalTeam) { + throw new NotFoundError(); + } + + const originalTeamUserIds = originalTeam?.userIds(); + + if (!originalTeamUserIds!.includes(user.id)) { throw new Error("You are not a member of this team"); } - if (originTeam?.users.join() !== team.users.join()) { - if (originTeam!.users[0].toString() !== user.id.toString()) { + if (!hasSameElements(originalTeam.userIds(), team.userIds())) { + const isAdmin = originalTeam!.userIds()[0] !== user.id; + if (isAdmin) { throw new Error("You are not the owner of this team"); } - return this._teams.save(team); } return this._teams.save(team); @@ -120,7 +132,7 @@ export class TeamService implements ITeamService { * Creates a team. * @param team The team to create */ - public async createTeam(team: Team): Promise { + public async createTeam(team: Team, user: User): Promise { const placeholder_img = [ "https://i.imgur.com/CWwOYnr.png", "https://i.imgur.com/ZpFOtqy.png", @@ -153,16 +165,14 @@ export class TeamService implements ITeamService { throw new Error(`A team can have a maximum of ${maxUsers} users`); } - const userId = team.users[0]; - const allTeams = await this._database.getRepository(Team).find(); - const userTeams = allTeams.filter( - (t) => t.users[0].toString() === userId.toString(), - ); + // TODO leaving team should make someone else owner + // TODO order of team.users not guaranteed anymore I guess, + // - add owner and edit all usages of users[0]. + // - a team owner also has to be part of the team, I suppose + // - you can only own one team, just like you can only be part of one team - if (userTeams.length >= 5) { - throw new Error( - "You already have created 5 teams. Please delete one first.", - ); + if (user.team) { + throw new Error("You are already part of a team"); } try { @@ -192,38 +202,16 @@ export class TeamService implements ITeamService { * @param id The id of the team */ public async getTeamByID(id: number): Promise { - const team = await this._teams.findOneBy({ id }); + const team = await this._teams.findOne({ + where: { id }, + relations: ["users", "requests"], + }); if (team == null) { return undefined; - } else { - const teamResponse = convertBetweenEntityAndDTO(team, TeamResponseDTO); - const users = await this._users.findByIds(team?.users!); - const mappedUsers: any = []; - - teamResponse.users!.forEach((userId) => { - users.forEach((user) => { - if (user.id.toString() === userId.toString()) { - mappedUsers.push({ - id: user.id, - name: `${user.firstName} ${user.lastName[0]}. #${user.id}`, - }); - } - }); - }); - - teamResponse.users = mappedUsers; - - const userRequests = await this._users.findByIds(team?.requests!); - teamResponse.requests = userRequests.map((user) => { - return { - id: user.id, - name: `${user.firstName} ${user.lastName[0]}. #${user.id}`, - }; - }); - - return teamResponse; } + + return convertBetweenEntityAndDTO(team, TeamResponseDTO); } /** @@ -238,19 +226,19 @@ export class TeamService implements ITeamService { throw new Error(`no team with id ${teamId}`); } - // TODO team.users and team.requests are string arrays, instead of - // arrays of user entities. Write tests, then use a many-to-many relationship - // instead and don't use toString anymore. - const requests = team.requests.map((id) => id.toString()); + const requests = team.requestUserIds(); - if (requests.indexOf(user.id.toString()) > -1) { + if (requests.includes(user.id)) { throw new Error( `user ${user.id} already requested to join team ${teamId}`, ); } - team.requests.push(user.id.toString()); - await this._teams.save(team); + await this._users.save({ + ...user, + teamRequest: team, + }); + return Promise.resolve(); } @@ -259,9 +247,13 @@ export class TeamService implements ITeamService { * @param id The id of the team */ public async deleteTeamByID(id: number, currentUserId: User): Promise { - const team = await this._teams.findOneBy({ id }); + const team = await this._teams.findOne({ + where: { id }, + relations: ["users", "requests"], + }); - if (team?.users[0].toString() !== currentUserId.id.toString()) { + // TODO ownerid + if (team?.users[0].id !== currentUserId.id) { throw new Error("You are not the owner of this team"); } @@ -279,29 +271,27 @@ export class TeamService implements ITeamService { public async acceptUserToTeam( teamId: number, userId: number, - user: User, + owner: User, ): Promise { - const team = await this._teams.findOneBy({ id: teamId }); + const team = await this._teams.findOne({ + where: { id: teamId }, + relations: ["users", "requests"], + }); if (team == null) { throw new Error(`no team with id ${teamId}`); } - if (team?.users[0].toString() !== user.id.toString()) { + if (team?.users[0].id !== owner.id) { throw new Error("You are not the owner of this team"); } - const requests = team.requests.map((id) => id.toString()); - - if (requests.indexOf(userId.toString()) === -1) { + if (!team.requestUserIds().includes(userId)) { throw new Error(`user ${userId} did not request to join team ${teamId}`); } - team.requests = team.requests.filter( - (id) => id.toString() !== userId.toString(), - ); - team.users.push(userId.toString()); - await this._teams.save(team); + await this._users.update({ id: userId }, { team, teamRequest: null }); + return Promise.resolve(); } } diff --git a/backend/src/utils/has-same-elements.ts b/backend/src/utils/has-same-elements.ts new file mode 100644 index 00000000..385ce298 --- /dev/null +++ b/backend/src/utils/has-same-elements.ts @@ -0,0 +1,6 @@ +/** + * Check if the two arrays contain the same elements, regardless of order. + */ +export function hasSameElements(a: T[], b: T[]): boolean { + return a.length === b.length && a.every((entry) => b.includes(entry)); +} diff --git a/backend/test/services/project-service-get-all-projects.spec.ts b/backend/test/services/project-service-get-all-projects.spec.ts index d3843a76..605d1e49 100644 --- a/backend/test/services/project-service-get-all-projects.spec.ts +++ b/backend/test/services/project-service-get-all-projects.spec.ts @@ -88,15 +88,15 @@ describe(ProjectService.name, () => { // Create teams with projects const team1 = new Team(); team1.title = "Team 1"; - team1.users = [regularUser.id.toString()]; - team1.teamImg = ""; + team1.users = [regularUser.id]; + team1.image = ""; team1.description = ""; team1.requests = []; const team2 = new Team(); team2.title = "Team 2"; - team2.users = [regularUser.id.toString()]; - team2.teamImg = ""; + team2.users = [regularUser.id]; + team2.image = ""; team2.description = ""; team2.requests = []; @@ -167,15 +167,15 @@ describe(ProjectService.name, () => { // Create two teams with the regular user const team1 = new Team(); team1.title = "Team 1"; - team1.users = [regularUser.id.toString()]; - team1.teamImg = ""; + team1.users = [regularUser.id]; + team1.image = ""; team1.description = ""; team1.requests = []; const team2 = new Team(); team2.title = "Team 2"; - team2.users = [regularUser.id.toString()]; - team2.teamImg = ""; + team2.users = [regularUser.id]; + team2.image = ""; team2.description = ""; team2.requests = []; diff --git a/backend/test/services/team-service.spec.ts b/backend/test/services/team-service.spec.ts index fde43186..02d76146 100644 --- a/backend/test/services/team-service.spec.ts +++ b/backend/test/services/team-service.spec.ts @@ -39,7 +39,7 @@ describe("TeamService", () => { const team = new Team(); team.title = "Team 1"; - team.users = [user.id.toString()]; + team.users = [user]; team.teamImg = ""; team.description = "Team 1 description"; team.requests = []; diff --git a/quickstart.md b/docs/docker-development.md similarity index 100% rename from quickstart.md rename to docs/docker-development.md diff --git a/docs/docs.md b/docs/docs.md new file mode 100644 index 00000000..fdf52bbe --- /dev/null +++ b/docs/docs.md @@ -0,0 +1,292 @@ +# Docs + +## How does tilt work? + +Similar to Quill, tilt's application process consists of the profile creation and a confirmation form. tilt, among other differences, employs a role-based model: + +- An attendee is a `user` who can fill out the forms +- A `moderator` can see applications, statistics and admit users +- `root` can modify tilt's settings + +`root` can do whatever a `moderator` can do, and a `moderator` can do whatever a `user` can do. This means each user group needs to register through the same form first and you can adjust a user's group later. During registration, tilt queries [haveibeenpwned](https://haveibeenpwned.com) to check for password breaches. + +After the setup, `root` can configure tilt's appearance, as well as the application process: + +- When should the profile form be made available? +- Until when can users submit their profile form? +- How long can users confirm their spots? + +The former two points are represented as dates, whereas the latter is represented in hours. When a user is admitted, they'll have `n` hours to confirm their spot. After this deadline passed, their application will show up as "expired". + +Each application consists of two forms, which `root` can configure. Questions have a title, a Markdown description and a type. tilt currently supports text, number, choices and country questions, which each have different configuration options. Each question can however have a parent question, which allows you to build complex, tree-like questionnaires dynamically. + +Admitting users is done through the admission page, which consists of a table and a search bar. You can search for any info an attendee might've provided and also filter for special flags such as `is:admitted` or `is:expired`. + +To admit someone, check the box on the left for as many people as you want and hit "admit". tilt will send them an email, which `root` can configure as well. The top-most checkbox depends on the visible checkboxes below. Clicking it will either select or deselect all visible rows, leaving your not shown selection intact. + +When you add a question to the profile form because you need to ask, e.g., for new terms and conditions and a user already filled out this form, there's almost no chance they'd open up their application and answer this question without you explicitly telling them to. + +Disregarding which type of question you want to add, users need to confirm their spot using the second form anyways. tilt adds all new profile questions a user cannot have seen initially to the confirmation form. This way, users need to answer these questions at last during the confirmation step and if they don't agree with, e.g., newly added terms, their spot will expire. + +## Usage + +tilt was built to be deploy-once-and-forget. For this reason, we provide a Docker image `hackaburg/tilt`. While you could run tilt natively, we highly recommend a containerized setup, as you can update it more easily. + +At Hackaburg, we run our server setup through a central NGINX proxy facing the internet and routing requests to individual containers. Given you might want to access tilt through an url like `https://your-hackathon.com/apply`, we'll show both the NGINX setup, as well as a Docker Compose configuration supporting such a setup. + +### Docker Compose + +tilt uses environment variables for configuration. There are three sections you need to configure: + +1. General settings: things like tilt's port, the URL tilt is made available on, as well as the [JWT](https://jwt.io) secret. The latter should be a randomly generated string, as this is used to authenticate users against tilt's API and an easily guessed secret might allow impersonating `root` and `moderator` accounts + +2. Mail settings: internally, tilt uses [nodemailer](https://nodemailer.com/) to send out emails. Therefore, configure an SMTP server and supply its TLS/SSL port, e.g., 465 + +3. Database settings: tilt needs to store data somewhere. Internally, we use TypeORM, which is database-agnostic, but we chose to use the [MySQL](https://www.mysql.com) / [MariaDB](https://mariadb.org) driver. While you could use some other database, these two are supported by default. Furthermore, you could use tools like [phpMyAdmin](https://www.phpmyadmin.net) to manage your database, but we don't recommend this in production. Please refer to the official documentation for whichever database you deploy + +```yaml +version: "3" + +services: + # the internet-facing proxy + proxy: + image: nginx/alpine + restart: always + ports: + - 80:80 + volumes: + # we'll configure nginx later. since you're probably + # already configuring a proxy yourself, we'll just show + # an example `server` block + - ./nginx.conf:/etc/nginx/conf.d/default.conf + + # tilt's persistence layer requires a mysql-like database + # we'll use mariadb here, but you can also use mysql + tilt_mariadb: + image: mariadb:latest + restart: always + volumes: + - ./tilt/mariadb:/var/lib/mysql + environment: + # tilt doesn't need root privileges on the database, + # therefore a regular user account and a random root + # password is the better choice + - MARIADB_RANDOM_ROOT_PASSWORD=yes + # these two don't need to be called "tilt", but this is + # a tilt example + - MARIADB_DATABASE=tilt + - MARIADB_USER=tilt + # genereate a password, as this one is shown publicly + # in this repository + - MARIADB_PASSWORD=this_is_not_secure_generate_something + + tilt: + image: hackaburg/tilt + restart: always + environment: + # the url under which you can reach tilt + - BASE_URL=https://your-hackathon.com/apply/ + # tilt's http port, which we need in nginx later. as + # tilt runs in a user account, it can't bind to port + # 80, therefore, you will still need some kind of + # proxy to expose tilt on port 80 + - PORT=3000 + - LOG_LEVEL=info + # generate a secure password for jwt tokens + - SECRET_JWT=this_is_not_secure_generate_a_password + # an smtp server + - MAIL_HOST=your-smtp.server + # tilt requires tls/ssl for mailing + - MAIL_PORT=465 + # your smtp username + - MAIL_USERNAME=your-email@domain.tld + # the smtp username's password + - MAIL_PASSWORD=password + # this section is similar to the mariadb configuration + # above. simply place username, database and password + # settings from above here + - DATABASE_NAME=tilt + - DATABASE_HOST=tilt_mariadb + - DATABASE_USERNAME=tilt + - DATABASE_PASSWORD=this_is_not_secure_generate_something + - DATABASE_PORT=3306 + + # to keep your containers up-to-date, we recommend using + # a service like watchtower, which continuously pulls for + # updates. see https://github.com/containrrr/watchtower for + # more information + watchtower: + image: containrrr/watchtower + restart: always + volumes: + - /var/run/docker.sock:/var/run/docker.sock +``` + +### NGINX + +With the environment configuration in place, we can configure NGINX to forward requests to tilt. This step is optional and you can technically expose tilt to the internet through the `PORT` environment variable. If you, like us, want to expose tilt through a subfolder URL, you can use a configuration similar to this: + +```nginx +server { + # the domain this server should listen to + server_name your-hackathon.com; + # the port we expose in the docker compose configuration above + listen 80; + + # we expect the entirety of the uri after the slash at tilt + # therefore we redirect /apply requests to /apply/ + location = /apply { + return 301 /apply/; + } + + # the actual forwarding block passing the requests to tilt + # watch the trailing slashes, or you might run into issues + location /apply/ { + # this forwards requests to the tilt container on port 3000, + # which we configured previously + proxy_pass http://tilt:3000/; + } +} +``` + +### Starting up + +Since database takes a brief moment for its setup, start with: + +```bash +$ docker compose up tilt_mariadb +``` + +Then wait for MariaDB to accept incoming connections. Once this is done, you can stop `docker-compose` and start up everything with: + +```bash +$ docker compose up +``` + +Depending on your setup, you might want to append the `-d` flag to run the containers in the background. + +tilt should then be able to connect to the database and start up. If the database takes longer than tilt to start, tilt will restart because of the `restart: always` directive until it can connect successfully. + +### Changing user groups + +With everything up and running, you usually want to configure tilt. For this, you need to register and change your user group. + +Since you have access to the server running tilt, you can spawn a shell in the tilt container and invoke the [usermod script](backend/src/usermod.ts). Please note that we're using `node:alpine` as a base image and therefore don't ship Bash. This script takes two arguments, the email of the user you want to change, as well as the group you want to assign to this user. To assign the `root` group to `you@example.com`, run: + +```bash +$ docker compose exec tilt sh +node@container:/app$ node backend/usermod.js you@example.com root +``` + +The logs will indicate success or failure and this process works similarly for `moderator`, or for demoting an account back to `user`. + +tilt's UI fetches the user role during the initial load. To see the settings, admission and statistics pages, you'll need to reload the page. + +### Environment variables + +tilt's backend can be configured through a set of environment variables. We try to keep this list up-to-date, but for the most recent set of variables check the [config-service.ts](backend/src/services/config-service.ts). All defaults and shown values are strings, but tilt parses them internally to, e.g., integers or booleans. + +#### App variables + +- `NODE_ENV` - in production mode, tilt reports all uncaught errors as "Internal error" + - value: `production` or `development` + - optional, default: `development` + +#### Database variables + +- `DATABASE_NAME` - the name of the database tilt should connect to + - value: string +- `DATABASE_HOST` - the host serving tilt's database + - value: hostname or IP address +- `DATABASE_PASSWORD` - the password for the database user + - value: string +- `DATABASE_PORT` - the port number of the database + - value: integer + - optional, default: `3306` +- `DATABASE_USERNAME` - the user for the database + - value: string + +#### HTTP variables + +- `BASE_URL` - the url under which tilt will be deployed, something in the sorts of `https://your-hackathon.com/apply` + - value: string +- `PORT` - the http port to listen on + - value: integer + - optional, default: `3000` + +#### Logging variables + +- `LOG_FILENAME` - tilt supports writing its log messages to files; supply a filename to persist log messages + - value: filename + - optional, default: `tilt.log` +- `LOG_LEVEL` - the level of logs tilt should output + - value: `debug`, `info` or `error` + - optional, default: `info` +- `LOG_SLACK_WEBHOOK_URL` - tilt supports sending errors to a Slack channel; supply an URL to message the configured channel + - value: Slack webhook URL + - optional + +#### Mail variables + +- `MAIL_HOST` - an SMTP server to use for mailing + - value: hostname +- `MAIL_PASSWORD` - the password for the SMTP account + - value: string +- `MAIL_PORT` - the port for the SMTP server; tilt requires SSL/TLS + - value: integer + - optional, default: `465` +- `MAIL_USERNAME` - the username for the SMTP account + - value: string + +#### Secrets variables + +- `SECRET_JWT` - a secret used to sign login tokens + - value: string + +#### Service variables + +- `ENABLE_HAVEIBEENPWNED_SERVICE` - enable or disable checking password reuse with [haveibeenpwned.com](https://haveibeenpwned.com) + - value: `true` or `false` + - optional, default: `true` + +## Contributing + +If you found a bug or have an idea for a feature, simply [submit an issue](https://github.com/hackaburg/tilt/issues/new) or a pull request. We use [TSLint](https://palantir.github.io/tslint/) and [Prettier](https://prettier.io) to ensure consistent code styles and we have a set of unit tests for the backend in place to prevent things from breaking too easily. Also, we currently use a [GitHub project](https://github.com/hackaburg/tilt/projects/1) for our roadmap. + +### Developing locally + +The tilt repository ships with a [docker-compose.yml](docker-compose.yml), which includes a sample setup with MariaDB, the test SMTP server [MailDev](https://github.com/maildev/maildev) and phpMyAdmin. To mimic the proxy'd setup, it also includes build instructions for a tilt container, as well as an NGINX container. You usually only need `db`, `phpmyadmin` and `maildev`, therefore it's sufficient to start them using: + +```bash +docker compose up db phpmyadmin maildev +``` + +For local development, the backend supports reading `.env` files. Refer to [`.env.example`](backend/.env.example) for such a configuration and match the ports from the Docker Compose configuration. + +```bash +cp backend/.env.example backend/.env +``` + +You can then start the backend using: + +```bash +yarn backend::start +``` + +As the frontend is built in modern React, we use [Webpack](https://webpack.js.org) and its devserver to develop. The frontend is available at localhost:8080. The backend runs on a different port, so we need to tell the frontend how to reach the backend. This can be done through the `API_BASE_URL` environment variable. To start the frontend devserver with the backend listening on port 3000, simply provide it using: + +```bash +API_BASE_URL=http://localhost:3000/api yarn frontend::start +``` + +After registering, open localhost:8082 to view the verification mail. + +We also provide a set of utility scripts in our [package.json](package.json)'s `script` section, such as linting, formatting and type-checking. + +### Building images + +Our final image is built using the `backend::build` and `frontend::build` scripts and stripping development dependencies as well as source files from the final container. To allow arbitrary base urls with a statically built frontend, we transiently replace this url during container startup. Please refer to the [Dockerfile](Dockerfile) and [entrypoint.sh](entrypoint.sh) for more information. + +## License + +tilt is released under the [MIT License](LICENSE). diff --git a/frontend/src/components/pages/read-only-team.tsx b/frontend/src/components/pages/read-only-team.tsx index f73c3bc2..e2b90995 100644 --- a/frontend/src/components/pages/read-only-team.tsx +++ b/frontend/src/components/pages/read-only-team.tsx @@ -7,6 +7,7 @@ import { useApi } from "../../hooks/use-api"; import { useLoginContext } from "../../contexts/login-context"; import { PageHeader } from "../base/page-header"; import { TeamResponseDTO } from "../../api/types/dto"; +import { useNotificationContext } from "../../contexts/notification-context"; /** * A team view component. This is only displayed, if the user is not part @@ -20,10 +21,13 @@ export const ReadOnlyTeam = ({ team }: { team: TeamResponseDTO }) => { const [isTeamOwner, setIsTeamOwner] = React.useState(false); const [, setIsTeamMember] = React.useState(false); + const { showNotification } = useNotificationContext(); + const { forcePerformRequest: sendRequestToJoin } = useApi( async (apiClient, wasTriggeredManually) => { if (wasTriggeredManually) { await apiClient.requestToJoinTeam(Number(params.get("id"))); + showNotification("Request sent"); return true; } return false; diff --git a/frontend/src/components/pages/view-project.tsx b/frontend/src/components/pages/view-project.tsx index 1b700702..93de4819 100644 --- a/frontend/src/components/pages/view-project.tsx +++ b/frontend/src/components/pages/view-project.tsx @@ -28,7 +28,7 @@ export const ViewProject = () => { const isTeamMember = React.useMemo(() => { return ( - project?.team?.users?.some((id) => id === user?.id.toString()) ?? false + project?.team?.users?.some((id) => id === user?.id) ?? false ); }, [project, user?.id]); diff --git a/frontend/src/components/pages/view-team.tsx b/frontend/src/components/pages/view-team.tsx index 4b9d0c56..78aeb7b1 100644 --- a/frontend/src/components/pages/view-team.tsx +++ b/frontend/src/components/pages/view-team.tsx @@ -67,7 +67,7 @@ const EditTeam = ({ team }: { team: TeamResponseDTO }) => { const [id, setId] = React.useState(0); const [title, setTitle] = React.useState(""); const [description, setDescription] = React.useState(""); - const [teamImg, setTeamImg] = React.useState(""); + const [image, setImage] = React.useState(""); const [users, setUsers] = React.useState([] as UserListDto[]); const [request, setRequest] = React.useState([] as UserListDto[]); @@ -83,23 +83,14 @@ const EditTeam = ({ team }: { team: TeamResponseDTO }) => { id, title, description, - teamImg, + image, users.map((u) => u.id), ); return true; } return false; }, - [ - currentUserId, - isTeamOwner, - id, - title, - description, - teamImg, - users, - request, - ], + [currentUserId, isTeamOwner, id, title, description, image, users, request], ); const { @@ -184,11 +175,11 @@ const EditTeam = ({ team }: { team: TeamResponseDTO }) => { setId(team.id); setTitle(team.title); setDescription(team.description); - setTeamImg(team.teamImg); + setImage(team.teamImg); setUsers(team.users!); setRequest(team.requests!); - setIsTeamOwner(user?.id === Number(team?.users![0].id)); - setIsTeamMember(team.users!.some((u) => u.id === user?.id)); + setIsTeamOwner(team.users.length > 0 && user?.id === team.users![0].id); + setIsTeamMember(team.users.some((u) => u.id === user?.id)); } }, [team]); @@ -199,6 +190,8 @@ const EditTeam = ({ team }: { team: TeamResponseDTO }) => { ); } + const isAdmin = user?.role === UserRole.Root; + return ( { buttonText="Save Changes" buttonOnClick={sendSaveTeamRequest} buttonLoading={updateTeamInProgress} - subTitle="You are part of this team" + subTitle={isAdmin ? null : "You are part of this team"} /> {updateTeamError && (
@@ -234,21 +227,16 @@ const EditTeam = ({ team }: { team: TeamResponseDTO }) => { setTeamImg(value)} + value={image} + onChange={(value) => setImage(value)} type={TextInputType.Text} /> - {teamImg !== "" ? ( + {image !== "" ? ( ) : null} - {!isTeamOwner && notInUserList() ? ( - - ) : null}
From 537c065c8be0822c3949edd5ce461bffedfe99a0 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:51:37 +0200 Subject: [PATCH 02/36] project image fix, notification when saving team or project, readme stuff, add screenshots --- README.md | 2 ++ docs/screenshot-1.jpg | Bin 0 -> 72949 bytes docs/screenshot-2.jpg | Bin 0 -> 88399 bytes .../src/components/pages/read-only-project.tsx | 6 ++++-- frontend/src/components/pages/view-project.tsx | 4 ++++ frontend/src/components/pages/view-team.tsx | 4 ++++ 6 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 docs/screenshot-1.jpg create mode 100644 docs/screenshot-2.jpg diff --git a/README.md b/README.md index f97463b6..e55fc25b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +

+ # tilt [![Docker Image Size (latest)](https://img.shields.io/docker/image-size/hackaburg/tilt/latest)](https://hub.docker.com/r/hackaburg/tilt) diff --git a/docs/screenshot-1.jpg b/docs/screenshot-1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1689db5ca92dc081bb95e868045f6cccb05d23b3 GIT binary patch literal 72949 zcmeFZby!@>mN(o;LK56v0wg359D>s!2_D=%!M#IpZ6pNu00|o0-Q7Jv(8k@p(Z;&p z&bf1Pa%aw+XWn_f_nUvFp&$0HUAwB5RIOjFwF<-(VhMmJBOxsTKtVwPyhZ*15O4sY zHOS1u=$*BuARXs(4mN)7=f4$d3DWWNuyJ#6{w(Bowy-q@IjdSYSeppaIapXb(5-lX zrjrA?Sy)@Yqm!`sU}6e#p;P=|;q=bIgzn`Bkh8sotr=n#@Cty2iu&^x`9Vki-nn<@ z4m$ds2N)Q4?_oW_!oqxjiHVIvfQOBPkAsPcM}mh>NJLCbjD<@|MnXhJKtxRRvk??D zrr19;gscd1;xC z^FxFS+})JXYET|mw>s=EI9Xy24d*~w;i zj)HKdE)FMSOCzr*BI9SHCPhHG0Ym9nz#t=NYQ&9raR+TI zP8OJV^f9ORffU?vNMls_^SV4&Zqegdh}rnfhMiJOu(MRzZH6NP0OSTQc1WIwL2hma z5dbi@MtT+9zk1^R3~W9R9{#>~aWwMR%W7NSFllG_3{Fn?Zf|&g)tGR5g4**}yT5Ai z>IVbGe)hzs!edtivk+wUm)AiSJv#iA(v0KFO5cp z*2aI=8`G2BprJBlgA8Qsi*CUfNz$HQFVS=4%3x6NV90&9cRuQXNkhfAaPOXI>_Ih< zv{58&QD3iWYaBFV6@)!s_0>R_`epkHhcokO_=1uz$Gsj8NTN$br=T39liFRmCD0{% zXvaC4{B7($3c7F39h6?$r(`>VDOo6ezeGRTdUO%2qZ%Pc;Z*2I&I+H5^CcIa3>Y{O zZmsQ;^K&}*vYAD3P_}%%`;g}A@-$CF^3vtL*H~ENnhmo^-Fb;!B@DW3UQ&3*XH~Kv z;?936UW@nsndtYdri&oIoO)muz+?X}{nM`34_l$OWPjd`Ih%VV4;#Sm=nhCi0NgZt zZm`@SMVhePwi@+>H>4CvZjN6B!7*5ZwvWfKIEWieBOfkqirR%;p73$p0p#t&*<743 z3kE{!iuA8mR@N8v*!osKc0;rsqUaCAt(kS+h`vh-UoV&SS%#?WkZ7pr4D&!0Fu$yY zC9P{x;koWv8N0}KZA#TlfXl0VvUQ)jfUvmk83Q;U8ITFHIKa10OI|@iVU6UTM~6CP z6-o1!&L@<)1XhNReC?fvHl2RtXA=M}zlLj4*P`Z$L76(ZXzzzN2Uksdf{oO0G_`%MJ-F3f7f6c}-|Un~bGtO|{1?h1DJ6vy%Gf z{O7FzFAo!2xUp83xn}R2*UJr!Vc-1dFO{w(XwVJIp(7C3yxV{P1TF|2>9;In4p5_Z zIqY6ETYZk4&W|xQjY_ZR9l{dxBZafha=n&LoH6SzqUq5%%BnfH$s2uFIresvE&G#n zW*y1S{lM<;D(3G+cYNOqbbrvrrH$5;pftCok^Ssz*e-xsO$)-nXy#((FFwqPWeB$l zV*Eg?L>*)_g`(fE3<}aGY8OC$(n#G#|39M>~OCcRI8b6y;T+_)-cFsiuv#s}3kQ=&Oq%XOE{%*0|KQ|!0;q!FQ*EJBSYAQJ|nj6yso20=<$EHzdXC7nR+CyDNtgX^*1cZq|d2hqV+ z3z0z#8plvZS_Z!%3C$_?S$kL#l>I#kCQN7c<2_zw<3p|$r^K|%Y? zhNw{_C3fLnrU*dGr-*&ZlFQ3PIGmzxrH*w2Dp7i#DB2H+;nCM@-(z!H zoh2)*H~e3l_0ul@zq7ie;6DsMuYSD(1}uA4+$scHyLTsrwwal2pV(I>)6BZG4#PMk z#?Kc=>a7^PHwOYx$%X*@0b0!eQkNnS3GACo_zrq?WrWxyrms#@z${7_zso+L&yiqpCK+{UGBUfAcQ&}J`t3wYN+TN09=M3r>_%?v>1n>fb zWkLRs2%3QBL&`Fi>pr;x68if zOpL2Z9CVOx9>TD!92jm#`F8f?!DW>ImHt32gJzntuPk>=P@qvffA1xyubk@*>Z3^` zl7r(`HjKFATy>NAHabGT@M)lm)Ls4VV@nNs9y1VcI39sp0%JJb7?aUiKI5B-AxrL9 zn&zk(z}7YMnWvW_^E@dV_h*%0Ck6Y!YvSRXYXt>}dc` z_JOz8z-7HAB3&GYzwRZciou&R;Old6dxP7PTB_gfBg%9UfZ<96;9?H}@OL^@de8sQ zMYM)>NNZ<-031#u01x)J(wFT1xhPyJ^W%!t4ENTKxU-|Aw*RBC>}Onxwklf|b2u-9 zpT3M6Lg6Rp-+zOi9{dFGKl=qRdf%1adzv|0j@=%dpd+0oH%!QU74;^VoXImvVw6uW zm&;0foOyY@PaB85LJrtjHAfc? zPtt<{Ptrf<=^EolMI``a%5uQHFK6NPB`3uCg%*p)c-06%aluU&!0{aYUJxAzL0{Y9 zNy3%f>Hb}aPJIftko@T0jEh{Q({T*e$n&pGZ@!pWCofA#2wd=$~H4K3kx4S9b_JUGqy2S#27fu%_aAR(EaB7+~FJ!XRpBK<65^T z-%x}9^%wUoG&^T$`v&6~h0+}Ow1`AviOaAcZFmQHEV!S`Wvi}Nnt`UpU~VE6XhD(~)=ZFK}- zr!R&nx#`6!@5<-(s7MZVX0mtQ{Jp2VZ}lM>vU1ArGNfJEl6&GH(DXMhNnec?cn^ZCThia zmh?F*Up8Hv#k|Rr)CovRrJBC#&c5O7DIAwTg8*c^oOFmjg3|h*4a`ytwj%(_P@386 zhQvue_3Efcxst9ZZ@8mac(TMy7bFDoDbz!j^j<0aU|VS%u|C7rB9Y!dq}F?;J&8fU zz#`*K8T6d%(2)jKf}7s#)D!2)?ugM*Qe7Oo>EdBdJL~3fancRF|53q=Jifm(^E2Tg zR#}5J`Y{IYp!VRhC)(5#I%>i~3;ZckgP$6tys!J@otchZM-WTx^JR zbPxa?O+lTjh0U5>gQI{gIBjD?LPtjoEav(-%A=w%NU9Cdpfi13Afvw)e0V(?g^n;9 zrMEc9L*!^!aXe=Xlyh{RXyjp#X6z|YqWql!x8 zfQ6OLdNb6{58;w;R8$^b-qQg?)Zm8E>Xgocn8R3YtaC#qkpz1#Jw@UOz`aV~$%4Um z^-~03cMSn>K>!3#f&K^pnqx$;C>2yPkkt_zC^Ol|Z_u!eX4_3n{N*U;Tb3P- z$gkIxeqCvlFty9-y_~t(f)6_@kvoKgBhJA;3=>a708)>E#hU9ngO1G;|Q{VdnVz1)8&nnPSA4ln|@vt!(x$DAUE;{x2QOwF<$r||o-muJ!^(Sj(YmgP{tn^AXbvL!Nx23L-7C)*hr8#+RUQm z1cm^_BLLdJwWCsW!<(C0p$Gswyo&ZOmKI)p z)*iBv>;3e~no7cS#_29yWZiZWs@*=4$do>K}HHJGH}a zpvpD=RmKe@gGTiIihX|GsD8fELw9kh-Jp{p{(&tOm5Y}*-~j}nNb=k%5$VK?xCEc& z4K6n+y{n5YG*zuOm6m0D5PRK0?b17(?V7KmJqV&=XEY%&K4D;j#XtH0#FJe9*Ml0) zDa85Fm}XCbSGe$TleTx{eVOyMkQ@?s_T02wje~#VdBx6cieIiJjoI`*V`O>&ODGk7 zdTS@_tLo! zlqrpNo4rBy-B3%>DE+HS_>utadkB$XLiZ74StFrD$CDkk&)0rPS(V(-AAyH0fHJ^k zQa`x6^p*M`k*);a-<4v4VB$+i%QEWq`Ol@WDB(#h+2T=18v-DbdHd&r$+T^t-ri)k5?s^f2t(ky>$e#N)5lHs8yo$;eNr*6!%yd3_`hcLnzxdSCUl(vm+8$zDfU5Nq0sfr#!mEES*tCC+lx0lV{^-~MduG;(!XZcLXkz8%k^W&=3STrG z{7ru7EVY*j-Kr1{OKAGie_Zyhtwr9Io^a(3f4^Hf%bYvD}d&n~_tY(At98!JWVlLGmgff@r1LwS?I1&mCTDw$O z5NX;GPCckum%@E}bm9R#!k;)}0g^53YQ&gdeM-F(Ig)^VtgAhM8omDzfJSH3^TMk4 zDHphYF_s)S59;^kyScwgIUKOSIVvof(*@!^`1-zvQ_>MH`Z?VxGx*DzBRU?o8%8)> zNY|2L??M~9I{X>ik2gn)E=F%Giq@QKbi*!e5oL? zi?(m=0_@X;XN;RB#@tTcVV1BFYa~KLQ|K-%wsX5;Zf_J#90%mRBy@GxzVYuZj*O-^miKEhkc41fW$Duj)4M$Ej~2y>gI3}cJ&E@UYG8gju*1>>M3tbTZ0I;#BCU(0=em9eAVvH$h$`ky0R%;=p1J- zT^C&{qK-)aH9|n8^6#Cqs*fW8tL@+mIn6(~HcdJxA@Ne})4eVLEvB$|j=9_VZAt}0 zTZWZ7!M3wAe)hSK@k}4v;>t9=#2Sv2GRuUA_(}xqRi#j3!Ps(vkl#Byv&`)UQimw^ zM|FW6qG?Z+ot}Oy&!=wT1ki_dH;O9YN;mF-TzZrAxj&i$E$PQ!4;P+6qmC(W-KIdpfK--~dELP(Jt0_2ur(V!RUW}-ccP`Ew2 ziGV4R3dso>$jQvO;*`)gWrwXDMp54c-RU%P8NTqL3YW|wj% zrF5RV7xjA1VIT|%!ps&*fy*?h*SnXrM@TrjjQ}*aL?OLYsGAl`hg4fL*u^;(E)kAq zkHh2IS#xgbO&e-b)I_}tt&uLRw{8%3cc@i+`A!~VLcyY*@YUx8uR@2`0EuApSeuOL z@k{Ib?Z%RmT%C{raJvZb0&)yomb_s=dhk5K3~(Y%Hn;Qm)k92+W^Cx4u=6IzVoQq8 zr22L#Pi17C^xkmO69d@BRhq(&rYT=+R8}DX;cjec`NKBYYbDJ{K~+3*Cj{*a_jbYJ-I8ws+N1x%A*&~=SPgkW);TR9VR)pHpqhKxQW(+C>xE- zhE6^ysS3Y?3du}(01GSxHG&dHlGq$bEw25%U|n ze(^B9&AxD~(Jb5I$|TT~Tcjmg=3ZcOG8_6w((U!AXi^B3bMBspbe6?P$m@EYBuN3S zVw>r2tc+2S#mQ2bg`EK%4=t$5D^SdTiRH8Pb?Ae67O&(!2NiYpd6HCljadI4e=^ znisRuC+w%qo_{4ad$vT=;QZLWZ|454F7Pk8{*v_z-5rpJnvvakr7!eZ2@QX&BiV`* z7xQJ}NDyVYGZTN?6v|n?V0^Ih%7(~Gp^dm5RzWhGNn=wEnfyT5FkndlBc*O>lId?- z-gi)-uz1dQ&doZe^^8!v@4fn>UF~aM9^)Ho-IKQYly4f$31?LY$u`+?R@h0dVT{e1 z6UnOhajm*3LZn?0pn?RHf{W{KtL9iKs0 zoL|OI1%I0&B|HTWrdMEb;Z_~x>k)s%^;q&4rlS+ThMtv@)gfMbTd45lj!SD zxDxsk>Abklqwl~aS(|1;$4@@vPCHbC%()4oc>?|TctABkrmAxO>*F#%>zI-h$ej<; ztrqi{!e}u*$KDyZzt~g|S4=}}oW%~apKaYZJ0dcXNRSgzu~I#+zpkIt{*Afy5qPII z?5m=AnJyGS?5!)$u$0WW@)rqRr{B+a!uTWoY}V^5>-ce8TB%!eMIU)+$E>JjRVr_d zSlEj>i*ycC&(1N9N|pH<&gC0LtS@u+l*laog}y3Omy$;z8@9U>RN(k<3H$R96=<^$`Q zFQ$HOwD6Iz0kyy!c#JmIvO5F3MOWLo%(ESCW$wF{wwj0v5dvQ>linUZ_38p40K)W0 z5O7CIaLnKIyKdH8z#eilbkfd#y@U+N{i7bUC9|fz=o-+d=Fhf2Yt3%nz6M{k=9%X? z<5)P8|5^2^=wD5~!Dcl*V6$+3cum}La})xHA%l2}a2Ig|z~ezeaNLvtvPr`~nglPR ze%8{7f~O;URjo`7=@c45#_1lxAi*A)iV9*&Q^*_3xiipoqFhB{kH+q!LV_n_y2}|DaP6*r16c&% z-S*-sE)vGHu)v8=BKA5E0EP{ugZ1q+@YGuEblp>52Y%3`bxn#TwBCO^EqRSAj{to0 zEV;cjILi3_reItn*Yn+f_FVA}?VgAb+~>UIVjbKb@!NHY(hDMR>FMDW#tc#?Vv$G> z_)m9D;s7Oh$5_i2trao|t8w$>_Z#LjB9#B__8$@{82?`;|3kh0SEYWRPXE76piTJ0 zt$H^Xw9fCf$4>gfVC7%t@Y^&IV6@WE@TE~)S~XM?M!xoAuC81NTv&bRK8$mJxs zN!5Y&5@mbwY@ZhH%Y5c>Q)6~AXs{wX;m@yP*)gT}$~GR?hD)aflSSesZQ8bykq}=Q zF|kNd*x(7*tj@suey-P2nm#a9^tVe#mak)~+Jlq#*fOeh0;*$KXZ>R%KLTHke#$SlaXkpL_L~sIQRC=0Lo%dbI1k6e`n8s+b=}MY3;f32JyN z>C&_6DpUFm*jC8tV#ueNWBIQrJnC)cNOtWW6^^t{&9ZdAOge4rW`0IhKA~gSL!h_&NMAO1r_jO8~EzF)sg zp8lD%5a(}Z6(p`;Z)9)A++zoNM!?Z4jh6@X$qNt3y?4`L^nqcwrO(>SrEtb*EoQWf zSxH*`S><(+i~;lLj82~0oP}SYEBlX3c~tT$cjBaXkwH=1n}~#fTR$Y=94XTni#?(JmxhBD69MS0b1O{`W*HZ) z{6|v7&%}!V*)K~nECc~yyzWE*&M}c`pCfX5#Q#?+>OwG|I?M|WVpk_V%5#Z7 z_c|&njyGXFYp<6w`e^KrDwaB~G0b}^->_n%(B1+NW^vA35Q!ZSlrRp(ihbBeb$iD~ zeqqRC*5WedT&@p3vJV>Ao8NEVeFrv6y|dd)Ht85tWy7}8E@1OA<4rA=vl(jRD~TT) zQ1;-W(+0b*w#6Q$o_Ae=-hDSLb1i0zG*%0&le{jD$Vl8)dNoc69qrTk&?6SKbI*~t zemW2jCC`r)qpR=B@khdCY1_n*z@k9%-r$(tGx%=G=hAUI2M!00i?t6a=S`I$o97~w z>Us=aaS59pA+J?q*cah@bJ)NLDrZ6Sev~o2oIUCh^UZ==CVL}x<;?M`si)2fjoFC87FRWJoiY& zp1%b_PDQZx-DC9$Z9xmpEwTuJb4|*o_z|hJpRAr$x5jp&-s8KP_6sisdYbtW03Wr8 z@Hh^a#8*Fr?TpCq7urkb{NbeC8o*a|U=%%@iIPvk*he{%M~6WNY()~^TFk#%LpGgz z4RJAiVrl&qmIsh2L1W7sHOoM3-TAp^N?$W{Mot(~R7=ppI?gVN%oHwcgDtlWhOS#$ zC*!a^)l7zz8Z!p1uICQba?;EEj!=Ulv_8!HVP^^5+~I&r3H$9qJwE4qyM<2MC%gms9~9f#>UIay zH9{^@yVj2dTBgh1DUiebZKfJgr$1V|#h%>7UDsDJL1JKQ;2APR$<-9Hv{!E$0gdRa zw@J*`{{AR<9Ib8Uas~(hGq16V;=$6q=p?OYba!oQdP4!^+Z-FlT#^;Fi4Gtp03uk#IOiuP_{#U>>RJDY^iiw=@_4KLD`s!CaE z1@+sxAGzI{duDI6i6xgkR=hD$}n7IQs zjb@56=ISAVtk&on<=9SiKRNv zA{az5nyZ@)A=Z;Cxw#c zVZE#zpK?p70lm8Yahw2|?gyzk06HZ0#;mdu3b`RZTu$*@=^Z_JHFgTDw5Zm2#nIS~ z`StU@FyRK9=XXmXjjh$!5j7uJYy@<}1I886wn_$VXceG~H$)icQLw7)VW?0i80Q9a ziKS()2`G0o(dM%m?-BSY(wC2_^Sgb_$sMw8%N}GZ=(Q&7dYQC?b_UoE=b13*zB+7I z;J#+pXnK^{pnOPV#Z6e}?I_X5xE$Em`VDuK;xhGo<2Kjh)ipcJpp6esHF`QIzi9wj z88v~AYmw83MSUs%==iAiz|Me5%YzEmQR3Cl4Ikt3aDzH{30-5Q&_}!p-;bBqF*bZ7 zswm_t&Sb8av_HF$b6f7kuJ7S;i*)^(f~YMOGX>*{R$Sy}x;e7VJ4dAYh3=Y9`D~e^ zof$TQ&NFZGJeaPTTpmEWXqbJ^;X(sl<<+HQFL^tMRLhvDKfhCxW=vI3((g0)vRFym z1qxUs1umu6D|~{}yTnM3ic)#-jhS%w6v*d++;gS;KG=8Q8HXa>4{s!YZ2ml3SvQ5e*?IDO~y@2m#n5MY{RY z6TC&Se}LC8a=?&E@YTJ72}vY+iKIzJ0Q8ZwY>U1`!=H6{Goht3zTm;uY^OdWl*A8| zcApJYIq;H##rzuoAqj#a7H_6ec^&V8$4Y zn{(NPufJ=voi^{6HAoTrh>z@1QH`^^_AXSoqoer=M3~%NV6@nzwC?L`yw0`C>S+H& zgo4cEkvx9MqkZkT;a^QU2l5Q6EN`}3 z={SQJ6iXq~E3pz{?IyZrxxy6jvT=OPaSF(6ERJv;ex*${T9yc&=;MjUB_%P(W)IVD zq@jxeg`=ZG#}|8B&oe)?ZQP;pE@i~E9GDQcSSi8Zae_eN<29Vsxm+Vt*#mX)J`iD{ zMG85UC?b!x1VNS>DW)cLEyMC}ljdA4VraUB>}sebvov9@jw?kP%kpW?%a=ZoV&3>@ zX{k`#JE}DcWIL91RH^LTPSSAo-3>=(oSMfp#5+8zgH{eYjCD0gUp2lVx6U6Ht zU*GtEfo+Fq$$<=@-UxpNMVj{jo5GERomI)ICnKO5te~2Mn`?JZs|jJHbS;;No@R{E zsx+&|&Asx)tllW44Y4|8>#CrRgJ^2C423 zFA6ixzqW(V;wCd9!4M|iVmB{ zL5#Zur!U*?qd|u^4TL6%iSnvgc1t!!dcr#<+ojc#B{l~id4djRbq*nGj{20ah!s!N zgRO0iq3DHvldlBCAkJYAqf7Pd2I#Av_z}-wIQ)Ftl+q(BpwA4W=E41<7BoF@JMmGf zI#lkXQgs#UR(bI94`AdMWhso%q}0(04;Cnz0n|C{hSjEE2iu)UF!TeRgQ6HC&Nr;d zAfQuB;^acY%{4V=thTDfSRB2@_{5{C#OnH@GdG5SPm4-p8?w2Ebv1E>O{QulPb59g zi|Y--L^u z2RFecB1$bpAHj@}u4J4YN}EGak+F>^v|ds$1@HQ~9bEiDQgrmkgjxBEQ!`Z#Vso{; zSGd|r76z9p9{o=(n3^uk{AcDZl`P*|a(PHYq{8DMybx=M+)aCV%^E(@JxXVY1Rg1IpJ~4a5>= z$ZN|Nc7_b{*Cr0UG=8v#^L)u}TTAA%5xIo2Q&_$8Wm1dsN0BV%3F9KN!OVu-i|7df zunvu0u1jj?!ov|(huOMfki)LE>yvjFwj(~DFjJBo^MrX<4TxZ339~Yd_0=jTyw!sP zXES0wlTGd&H55xQ@ny)!&8v6ct0C6o6b*>GWs#r;zUkjxZLtwA>qoYTrvrk zxB?3C5Hmd&EY9Gpj-gySp5&O@OhQRmBWW7z$x!oKcI=0(?;Asm>r}F1?{C_W%}4u@ zb%PZwkLs5@#k-wiT8a?>?%u(tYB}nnlOJL%;TN4%ZoJO6*LyuGQX5H6u|K2;>>_;% zC}Ws|T5^e$1v{i1Z_z;TDPN z-j=r*rd>-f!tA@?q)^6``>^=sbYKNnEes>ya&1C z8|$cWfvl2#QuyRBJ7v~XGq#5&m-{jl^Yv5^2Nml5InI<@mVJsRXv+uy4ZJ&*1Ww~& z2Q}*iPapQUDFgtQIWEtNDD`T%UOw)7tl2Apv0?i5-W^#!Mk<_^bJttHJyB{8W>`oX z>_P`b21{KLh;5Y3?`+nzS%zztb9YGX*M@6u0st(#$SzL&fp1G@1bddp7w;6pIn0fg z_R2q>7?*?g?^JPt%wNp@NJ?%t#gXd8Al?I3g7?Ttk;8~-MF6lo?icoa(}z9Yz-(eZ zxRqrb3fzHPC`IbldA8DiacCd#nHw(LjteWjpd11QLyf{McklK}a1Vi2CYrg0>jO|4~o|Mrwfy^_3?I2@1CuP83@=x3B(F~M5?%0HT@;3G*6im>Ew~^1QRVH{e9S{ zi@5sYEQ`}al>4uJOURUoXs+vemi0ZJT2ifz>FvX;-pf08@8F|+zRy5Yw~=bI8feo@-tuYmY24`!{h2M1cRs35c-@e=-MDI9|Nce! zbrJmK)w=Om%k=ssjn9ff(00+Bi@Ub(f-H#0qD_5e*N^JjYx``tWPEbqM*hfgBe(t* z@{!TggCo{gcV&K^wp$XC61z5@9$%eca|8DroHPlP>#IDpnrF&aS5Rw7)z8U6EzK4Q5O!RtXPCG#-pOo868zlO+bUuo zytY-CEU}8rjVsRn&@-7QHN_?Vnq+pA$Q@Hy2T#&z; zT%iQ*m};V8P6jM#bmAUi!&<1kaPZ(nj2zNe0?4nus&Xxk-cWavHc&+Xc$~rba3*kj`$*+I zGoRI#Qc0a4X-X4AB3s&v;ayea)`47Smj?Z4!=H+vM>NiaOJ-{3dMYaXb~iFh`|J4U zy2RZ&Ue1Y~A^XIJ&#DILHJ7Xka$~}-%Q~rO7J_%1vUd37-Z4hA%=G8pH&n1s`Bbo# zB6xJ@f2L~(+Dd~YI*-Cr)NjJnjkG@S#I71A$DXQd zCl`7NRX4+&nLeFaS=h3%W}fiuX3~A}RH_(S%qb}?W)5aavA1`QYEy6AeUi7^U)Ut* zxfr=*VexIO;IVy5(=`XU_c9ke#fzMF$e`@LN7pq6aw=Z`R^%zKcY~V>gGDM#V z=>Um^UfkY*;UWfJN-t=U_PC??=dMhWW%ckHh5+2SMZg>($X(gix`!w7f1J=F01+CC zpA|8};NNL0kdSF_e`#Lf<+!kY0qn|ZVAX{e+dPKnMR(9Ts$R2FWlEDKr2K#A2;iSb z4~d6vkdggp&lT|Xoq6z9c5(2(TL;Hcj`aNz06IA#c+)BZ@I%~y_>X1pXV|;M{O7W# z(d8072X`&Fk#;Ma_>tq6gPna@kzTaIP3`xXp3p^|>VsL^f_$?> zFd-BBT*8j?g)bM`dbcVbjs`pMXrrz z*qI}|{@I#cwlR%EQZWO84;2BFVw*`ZV`TZx^>fh{^kjt;iwX{SFZa6YG`RQ%y~)#Q8v`~}NP6|)>^!UDC?&(0HTqJP zLQjxhYN!^;6o*m_W?bu)s?$XZVK@K5!Q#ZHDW;@cmxNStinA3+iD?YUTsszCE07LY z2!ALl=M=BjCE*uU2c|sLrXG0TzIB;gh4g>_81|D$a4k*)^zks?!y}H;j8{W@Z*8Vk_#irF&^iN-+P(|Ca zRKNDBC12+pYmyR{d{#}URa6S%m*Kk42y*aBnFVSO&sR5tkFdU#?rK?G?QYNh2!vTF z-?r&!&^xI{L?xB>ubHj)u9@X-WJa_nB#l&L;#V38*a^F|^0Ou{IuM4J4i*1 zKP%JS;u|=R{IPFk6r@;xXrNLfz>xCIuvp(k{&;l5{jKPT9n~&?WL-~Ms6KM>=_M|@ z!;oD#sBhIZZgPXrd_=f+;J9B+%2;inSk&64cg=_w9~!BX_%);Kfo;}FF>w|n=TrJx zDlAk+(b^t2IWb3;mDjS56IRv zM@ub*3P-Jd8MtG#ha$ikZuKbZUaoxzT&dWB*fjCjIdBc*->`4xjZOcSU34PUtKf6uzq?8e#(wrpb z(}+8MQs4ZU?=!@BD`@B0rx{Eia?Qr7iaj)WJ5X6Y=Zk|o*h9UmjZ2pJ5G(H7vW7WQ zk}=|;iDL*E%6rb_4B=F3RjZXRB<$s3!ePLE&P#f5>f;yJHLDXZw#OC|C;}u@Gd@{K z>nQXbz#IGB0s;;y2c8t09sdw&H07p0R9Bx7)7{M7;V%sDJ_EE*f3DG&LvQJ9zKwY`O0q6~+eh?XoWGm0KFOo9KsV|oKP9RQacd~m+dI7mqqTcvsldP#xF8ln$J18zDC zk*P^n;4px>k`^+PH*(wHl&}Js^|KA@DKT61OS!r#$wthP23C#%3u9a21fvL zV59$3!@L4bA{bfuV}K0s9)fIw9X1AD zrW-{Xjz<KO4Zh_q)oC!qg;2TCk zgIn}}8$0c6oQU903-SNz?|<3yAXB88GJaYf{d{d)L6T&o zlXC>gIr?Hq@C7i%5& zghPW*zPV;)W*N!w&Qnf<;BsC$I)!5C9m~D&zV!it*OU`6-?g}$4=yk8!&BJ=?RO`q zT>@Un+KBS8oUb3vRU-ga*M@;3{Zi->A}#Kzht5n8wZS-s?{y^l?0umZ zdl*^IccuN_zzTSiv$IG8Y*xg%6tEWL7w1uTLOX33*RY~OA*tF63Pv1pGp?-bR2V`1 zr8nZprGqE*nA6!P&QyT$Dn@CaB)prQob(Q`tZ#v~) zU{8>jf3UxJ1W#RX)+mE{-7gxfpOq!ZjG(M|jPua2fDT7m==^p`C^*m4&Vni6AbM=? zDP!~N<4!FxmGx5aGeU?HKh=q@KQUMGnEG6N@WO~JxJ&c>wp^ArMcoiSN?VSKMeBGe za>YsuL^^PMf5zqHJzT*^|9{^fS%>=`9X}!OPkhUza7#(-DPo`$j{u;!Rh)~GB0WMM z&fbX=X=Hm^z_hMiT!u9!5de!8YS!CRh@F+cTFI8aM#A&2_(TfxVyW%1$VdLG}=Hz-p-tvbAETuJ@w|+t9f<*XsWQThTVI8 z*ZSD{nPB;UvEY8#(I`iq^+HJHC5C;{Z1-!k+u-slGp5a%)+t zM2TJpHNKA2wFI36?23$& zx=~~o2)$s)S1f(*I1>%IItRa2=~=I45LrT0`_6pFeeiv!$)F-(9nmm*V;K28FXkLg z%dl+faO1vV@JH{>QCBhyt-_Ef-NuV~*mTN{R7G{TvxgX!+o+)fa~^TSDPXG!+R;Je zE7`DX<|nt-Eo9#?;oGN5%XrahMRyz&8ZkXx>dse`tb)(IK37a6NU0XmJ?0rFZ-uXf zFLoT`sr4}*=DDSvWD<^U-s;1B26=q`8)37YbiTL98dncyq!4FDeC2koa3qcRk~Hk* z6}G}$xGsJ%*Jh1KN(My|iEXYWUD>JR4TRL=6|3C6{59R#aYj8$($mp^m7Y(-nfiMf zR~zkQ?dFgd-Z)T&JCRD1S)&ww%pLpvdqXBegbdy|jF|7`!>4`@ zQth>;-Gqv}q2zv}rEfdDQAcX)1rxS-aMq;9faqrMy1Xe@Ch`FwUl*3Ur7{(Qf5y;c1HB zwcu@eR@!@98+9{o>Bc~FetrIsLa&PmN|+-R%6t`dGOx`e=%g$O-Rn%$dB|k7OXUzJ zbKR0{zL!|FcX}y}ozy1rk#u854j!68hxF5;jAn0(HK?TJ-5vRCCU;(5IfgNMB=Od& zX)<4;H3vfXNXlDkT)c)cPF+SQVfV3J{`fUq9PH(kuQ%|sK`R51J;_naPh^~T#9h#x zmg*HbGyWdb+v7}fEH!^k_h52Oyg+x+P$xGjy61dZ(lMdsEtSl0B_nc)I!3`eZ7fDL zU0D7`DN^UPFMU&cd<|)hj&Vy}ZL$m9a0CCdoTBwl2?*rBkV>)|tlOgvajP>PX%{WikIu3Vh+T?(U zO%72KhF=Ks3oQn*F?LaMN&If0Z6Ot+EIPO*ammYP?y2uuUK&g0Of}=|-Fuhklc8-M z6@sSyGLAhlqIxqn(va}XtYXou+J`%7z9#t+$y|`CA5V!X)j+wJO=hL;i5{9g9a2SB zLveZ3Dn=zlIl{;F*^*MDo?w>BbvJ$W{RfQsK9TW@ll>+B&EiG4{aN%;U4^)d@7z~j zlm#ZmMAv$;0IGza-|CA#$CG-|>9g(n2`(}QG{W}Qhoz^37IOxYamJ|=Qqf~sRM3OI zy?ZoV7-C10FbxwfVg_yP1=OOah5>=6N4m?3W@>pRB>a$V2C=2U3{KVZJ|lEY zsje>TR6ve7PWxGl#B{`0_WOu^c%&3EjkIbJ^-{y-m<3XORs-=Pzq`mv`IgsVHy~QX zPn!+TGvMw_c(=(G|Lms0gzTFsvUh$4!%cuM6U=;*yt^~YV0oC%PrqnITVMA!-j>cl zXxd<%jlp#fH8AQY>PJcYm)8S6uv@BEE%2<;^};)c-R z(c}1E0WJL^dUq2~sI^OuJWHjUdvano9*-7QINl99TFY30gwSb6T;UMT{7=-v3H`Q8 zC~vneO6kM*0xN54pX+9M9E^AoUF)o_72t1f$;OC5nMz>S`3e7g=F*pKZc0!{ov_03 zW}r^(n{rDjhOw;c=UDHsq11u3zUA#tNR3$ycDTUpSRTxMGhuhajZ{F@ixV>?+uUfx zA<0bUwHf}^i;kMbsb4Mu6qSJ+4;MknWyotmou~2QpQSaH3v#2c<6TBOb}Y?c2X4mN ztd=awMaFJ73aW%>+YOK1@A>Trm2E_J-jBrESM($wn2c-;W9Bh04D>lNR#4(P6P`K2 zGFezmC#lS&EspXn<7dT5souRD!6eu4j6}EWCN*zpR6S{yCRPW6~|KQw?V$XDYWX7Ur*BFBl2ya zYn^kEpI^7MdqsYqeFpUVRkppu$+07v=L&k)cu(t(m;5|_V2fro=s}`0u&NuTHzDfD zuk$6Hn5JsKY@!gXr}~pM^T>)aU7bkbl_XArjs}cMhF85Q&#?r$fXL(t!E7N#|{xQp|Yrw%=`g3i}@czg@LeDjDKI~ zKa>=Ex$fhxU}4Qa7=B4 zn9FSTDl39Pn+@+ID^;!*134EpvJUjAyYfpj8mV82-If5`tdN|9b9d%@k(;d>l@ zSQA(q>M*X%xHXO^l5dK%Ec`R5V$nF<9;|B=3c=xK#nQFP+T42h7(PelJTQJhzlkp- znW^DDZqAw;F_e8(f;TloOR-<;@*4qpxbfjDTxVmjR@+6GVsr6i;YrA+w^-VRpFVbD zIK@wEaRVTYFpp$?46&`E238NqJt~+YKfqx>;$CTE{8-jQB%C6>K$;>OhZHsS!XajB z-!L@Gy}HA7ma$0R-Rr%)|7(kwFGFxSr8*M~!F>oR52nlzH{ILq|hJSw#FR{i2+`O)PjckcLQ z!KnSIS)JxJ`<@dMnD+CgKFLf~6L!Z;-@u>uZTy{+g zZ@FLhM#8m~VUys)O6)l3$kKLizF_S8{Xssmd8v&hB_?axsZb_*dUNCpzV-P2xx2Zg zMcVH6kS=-`Ym4m+=ApbPhAg&xL3eV2y?uk-Bq(b~#cQ_pLY7reIDhk zgzrYj*c}so#hS3FCvh_*$gwu_87)vy>9lV>u0Yk-ye13}jga@trkW8$;;a z_F;@UBMdhL(BSA?0QcjgO}m?GjQeO!%cMx}H95h$V<^3u!LX~&9-r&qq24MWpJ5~X zma&;N39|0h-qVWoNEq8`peNB*%?bM|QENTQvF^VsWIZgU$Ugbeoc-2sXXb}-5c1^J;codu1b!R!%L;8CC8EfAu`7h4k2u2%Y4M{3W zmaW(wqh$AdziSWY;-8wWb&t)(@bQ{6-f(u}YmJCu2a=%#OY3r~8P>$e`uIaoEo-{d zdTO$S=@SkonAYyi(dyW5gj#S{NB;?|8bH-2bIPYr7h_c>PaY{ol23w1s2wiLM^6{N zcF6$=r~Tc{b;cBwctE3y%c9!OvaYKZQLdD(IKHU|62B4BXV4t7$VJ6Vs{y$j#?7{o z?BDHXZ>A3Jeg6bSi6@BVQ*Mahx|bs75P{yyyA0fFtv1~;UTs}a4edNYoKp)kR<{5m zt^0heKJixx6^^_kb8!1NeU>wxqPm~+%bdrm`=BSspRa5<)!OH-n2moUy!TSE=I-iA zeO0d$_uT)VQ38rP2rw2}?j|x7Q^!~6gKw%K@(oh4IC{4(xqZ6f z|MNj)S=MH>Aj97`mY9kneyW zzRP05ihaTC@K|+q-jcv|4x>{;BWWwl!;uaZmxahy*bix*?g#eUmWTA3B^XV0!yGrg za}A-fSriC75YGZya+&Y)Q`yyQGrt0YBkl#qTZXEYvr6Zw>bG@Nz(4K=R4! zE!kGsdhNEhL!mhX|Y+C4yhlGf6rq{fj(VxKj5NQ{Iw!sA`|7A@p2t>xgU7C zCr6UY!D;ZGqWfK`hxC$<{x(P6Vg>VyBd_FP8gCrjD^`SibeMuNF{xOzh*dcUP+A=$ z5H7%B5G3c=u%?^q-qEXTh<6>PMXcEnj{-WWS7xKq=?@MC<;#b|PZaF$maZMnT0hXSUA_0eM1=CCnZ-gh;X&r3hXx6yrK-QBZ&>Ob#ZHM)XAUz zTuURlR3lvVS(3~g+2wR!9}MSYiQtYFxw%(8kU7o`&XsgT=%-e@P(i!lA z2yu=29(YG?-hitK5)1d^_ZMRteF_$Ibe87#m6w;68mc0mCu@HRq@d}HCcN?6k>;o6 zo-l&rPqDzC=fbRrvnt+;K<%~?=9KDXK7HfFHC!|Q(s4>1-DOl8zZ1}i8g>T*iLb@EsflJgw4SJ87C#^kJCyqDb zIN!xYAV=j+Jc~OKwO0}z^PvH5^=Qg#jkCOORI+7PvrTzBb?;} zY##?FCc?!CtCtc+;L?4ylG3K?|iUnM8Ry6q232)YQA8C<7qKHq9XSf^vgt8YVrzW^jE1bfDW1IqbMKnL$76W@{!`-bHAeFs+pKgSN-#wqv08y|BQaJ z0R;QG$N}Xw20*b%^%^N@O#uyOhhfP>C_sH~>$hqH&$&;j>x<_*GbVj32o-gqT)NQ* zcZ_=2WTfKHY*u^cknItd=ln*nRYKdpGw(SiJ!A;Sgr_Vo$(@-FlkLsTabINkGcK2% ze|uA%9g|GLQbI7+Kdpo+Pe|fyW$og0G#=a(+*f#}k;%bTnzOv^XD^|Mu#S}$JDUl{ z?zQZdei1GwwW8O2xX?uVe+qM3*PL80Gxv(e?6f zcE0&ysaW_ZqlDtA;vMVAuNFd{%)Mu?sO3A!wgO`(TZhnZVimP>jH_NDAD{F%#dhCn z#km_lE-9s0nvk0L^W+4U#4;&`S$vNi6yV)YL&{ewfm& zL?G!JLMCKYMAbJA*%gBHT*UrH&=7M1#$QX18eaRAW1d|l^^O%4ymX!G zAD-5l6+xLYoKp&GSkA#tC1?CqAe~IR;jI#W*JL<)$UGL=l@fGrK+R#NGvi03e?pbV zxKqBkwy9aW-o6;w>n4CvYT{@n@$EB_IU0vdf0;*ld8tVMdNipPw69E4m{uliBum_% z3-+*xt)FI29_;(cy}$P+gA3Z1Uw-g@*T6`>wL5W&sRw#I>g&^oU!ajyFi3On3(jBl zZC-aZ2=7wwHH&L`t(HuGT-3UFe|(b>UEu8v^WTgH9o=P=U^ZirNg(!GKaP4vS>H=~ zvxg9x$HV3GPx-GJ6n0y*)S$%9IDHKWuH_03aX4jQOV*G{9-!v??>#o>`sxIAsdlqi z*{fXse5~o)tx7WujcTm%Y)f$lc@<cJNFMlJGr%F`u3pT1^-$4vjZPbk}u9h{I@6`i1sZeLJ{^6 z$$Pfw=t?-F<31!yV~0MENuBh?@4E0sL2+C0I#m35o;TJyXF9Wn9;j zYh2${R0eqENw)pOemT6yGGWQBj`{%cogu52l*Zb_+q|;6zFoSY$t@7NX23$&4v^u) zi#jT&!g%rG<>~BKDz3XENnab##@tGy1=)l~ebpc*%QFNr%3Pt5eg|c=g28dMJ}VDm zY*ZCtW*gQWFT0yW73J2xN%jCNzRqzj;%}&*F_3Doy*IPtBM)3^T-P2^AQ3eASV@M$ zt6nF4t}zfbwLl#?9#^(v`GuNtui_ZMtIUZE5W%8on8>PXO%ZXVassNVrSgatK+Q(2 z@crjvJOZjlg~v7lhG^>lFhtK>HiFMYK0kzpfc~>MYx_R;U3{c!PrZh~qHVZ)s>QJy z9pkAQUlV$)W6g}ixoZWAzu2qbVQe@mdK%Y)Y}EUYm7uS-%37U%BV?xRiy-})Sdod* zmgp1F_jA9~ijDPdkjR>J^Mq(6UkIt^(t1&=Pk%6YBaKlKAjC0TPYKEO)ao9Ol@KF{ zFL^c1^wUNbzmmyh{x^cSdK=d-w*70zm3I=c)S?3ZeuC!sJpkY2#2Q;m5YabzK6%^8 zE?ng?dtp*A_=*bq-OOBq68SsTWD4JNBYIzmPvp4cQMEvKKl=Bddx2oSc19~mitw<` znPw!^a9P{p=6i<4LNe*Mp*V!W+YsxW@fUjFk9u{9NT>~3402b8gXIk_Mkump=o^^J zl?F_0G}B@At8k)j41TdRTcn@aK&%0;zi*xczSyB;gyv?2{-*MN#B?SGaW`)Y&sQ!|(HlDp%{_fSan$s6i6{o%{k~7MWi%N@Ahz zQoMoPlidPrNmHcqAL4R=^Q+CbMgQ=AaQ}w#qknIU2=+qP;@aHY-M-1hj+d7^Mm$ID zNDt9%pNugePK4+tu_9PdVMV{$NQ>P<4ZPfAzPc`ZGpw3?u%hx)!v2ls_^XRFYzJ8z z2PRI&clXn;JNsn!{2{aT(TUO+d-RfRtxAdwty8oWQ>m);*f04tlQ}~q`L^Bc46h0; z;upn?XY0YAr*=d|O&&`E12~r*rVb7DwlE!agq}p9b-Q33Vt*U#Vp$F`>cG+UeNtaH zvwe1S4qTo^qs2|_I0A`yLz!VLchq4F@aiiE!z&EcJ$2pLIT2p+;Dk&jX+gPTJuM&7 zsmsdKc1#gUC5KI&Jnf#iPaGLP_9ok=VdiJ=ej}*YM(Fhl_{2Vq3f%qpIPjR)Vzh1- zkq6V-VAdQ=Rbov_SyF2Y;|oI^likMft=;$3I(FIJFK4)3dXfdf%6u>TZ zUpTRg?#P!5p?DAG&@u{H7{$`k90lS4&DS1L&sB?T<&gYw1L%^(pGC=;#(Eb58qN}?h9 z5AHI^rl413X;n`GBffB-r`DEQxna>4DCUrs*Q4~cCOTvHdd1~g&8y6(rk_d@=6fI; zWzAUMTPY*ELbCgL>@l-_rt&jgT?Vvd8znvWj-uQp?-hUB39)tt}Q1XB3Q;f=K^dRFd ze=-n|N_hvOkb;o74($00n)T4QX|V&|d*T!^Ak`p3%+hi*ckl+gM04VTgY!v(5*0Nr;uhB_y z4BCEIS!vREi*e+VuefS4us=L}18w^NwqfGG*@iR9zk$uLZz){>*Rbq=xQ2`OIesIo zdymh%oBYEy9DfzM4n8h}K)--rJX{?@9x!IhyY==aF15>y?PE|lq@=ghK{mBZ_sTGM z#@!WwmiLcpob6o7Ze+gr+YNjj0GFIow!hhOWBC{A6_=RdzmU`)^(jK8il#&WeC!}8p%8ZMN_ zC~`8;F<^9gY^Pf7>y=(;$hl3?I;sbUgaV?Z8n>R0tYLs-NsH&2$384c6oVIBA-!@u z&V5l#`AF7$%T(>5I(wRRKln|%6eXN!OhsQen6Mk51TCv1b9=LOV=SX> zv+_8XSuVh$wCPFFvq4O=-)mG_!}g~BLdB3Z&6c?#hE10C65mNS_;qtMEvS*zPxkP# z{QX(Ie+hNq#|FX|l;25-KB*nBkIM~0d4n^n8v-~UCEgh7On)jTN4?rN;4($4c8K|P zQQ4p%g&Sq94tsm^ZIPz^Y3odH&MfKb6(}zA$ zl_$~hdxcx=^@`R5LK=Df7`O(%ehP&(;tG^uxFhYL=CSWD#g?&zZtBf%hkcPzQDUYJ z#@P+5^`Q)eZMbJnpmNFML}TPhCTOx{=FP5t zFHACH)kKck(@Lc%>N6osA!*~r&}o`2#j896y_$wl#Cb4XUzVBYPn#H@tkR#A=|%4 zmVXgj!_*5%rzYH?T0Cf(GhKGZy*_ zwvkhhi2u%q{%McVE$u3x<*zW9dyKI9Rs7$AZbT`c{xx&&F7uzN z{Nd0aSmSTuOLRh$Y81+$MAZI?F~wjtV>jIhX8 zXMc*P>Jiy&y8)4l)Xybk4c6CHnT>nlbSx>8Qn&RhDx-yy1WLXMaZUBYLB>K z)4Wf8%0)BjbVIG=g_rZFC-!l>J4?G2DBK~jNB0VlqHgWBh|G;(ep^C1Gz_-YomRKA ziCJd0+;CvAbfWRP>00^p%Z>15&N;(Fit;_S#?l3LCKBpmOL_mIVkUO4kK*;>(--=q z*UcU(-`*M9ieY91W+I=7;m>>XL0wZnI#^=N9@AXx$X%orVDO|NUY#0XwMCLF?>F6l z`8iBj^X$hc(zQim45;NfnzXmSf%^IV#zQN;(-7}_W-iau73%|66Q=^rp2?0`!?Bk& z*~||+&Qk^{)&^6{B`iw@l@aKJ8&A0Bvv`nWw?S&^duP|*pN$T9gb9_D82v^dCw&GV zb()P^6)VpT3Cx$L8k-NX=uFVL+YXFD1(4}Jr(^ZFXr1lv_|tOj*Py>(FTYLp+=syj zOCfnvtX#4}@{Q(D%4YA6q4_AK*m&eD7RRf6z~0M4oJe#@I&JfixIG7?GJ|gbwa=+$ zMI!$0UpPe=Q{yM%v#7H`~%8|Bhp zzAiA+{tTwL`4V}qx@L|Yvr?>o_>z3oeyY1cy~f1)O62Ry?@Us-3?)kr`@5cV-NO7_ zYuF>S9wbkKbu*hBUtQ1;Qe*)7`J0)Qm@8uE-J8#($bh^3|}mCnC6_^B}+D-Oz&LtPhVesy_Y< z*gJMD+Zt)Ay-oU-fk^b!(~lQ%HY-Pyf&B-hd3rWL$HnT~UC|6ljopK-{FeIHZV-A+ zBxA3I-ENDeP$3~!Wl^yHoX$ND+^Y&^Qj9l)D_e6P^~AXEk}8wRM=6o@Mdw_y$sbrlBB3N}C^%MqcEkxb$m#QB!wu)@p8bZ-TNm^cX1@{y~q3iFtKR zacav2Q-rs*bbX$*qqbkKh5A__xc1KVxl;gm zowepM>DZ`>CEX|I@#k*@^hImpO=&1gTHD8{QKNK^Sw%Hy{YZ z-!$S3C~BOzKR$TprmqnQ<2pt7q5;ZIF9qp zzbChxi4>XzFd2Ew|1Lx!w_K6rNwOk(E-Exx`}UP^1EDoPqQxyIQW*nN7bwc#mA>>`-Yl_WakVw-b~qzbfKE4Lb#$7?-ZVbL;MDl0kbz zs~oAj&MS>|GtGSctqE?wQdWlc{N0=THpf;CNG0kXhbMLn5;z9iHg~}LC4LyOfE??Z zHgRN$0p>a;c$j2qmiX=CfjZcyJ^!}5>?m*2&K#nO$z5WrV4r~wswWq2(hw#%vu=W) z8*x=yCLh?cnCg?q6;jgOPzO^UvyHk zI?(cqWT%B!>#q+<|KLYIdHzVDL)b&wN^S05LnG9ap_x*fJl&zrgs0e{lZZPTOGc_Ya$Wzx$vO| zk1uPk@ayY$CymY&={F3REveTw_7ALjX%aWTs0dZ+TIV=xB5Hfi? z8e&TFaO6*}uVQAVycGp}9?BpaKf91Vp(c7jlKIC(q<~!xew=XH#nuQMhr9z|iY$gu z#uSq@eA_T_LoL0TTxq76YdV40hT1@1cDNeS$=WI|SE}Qy<>fUzu zQ9Nt^S!Vd$_|u7C2zeM)0CvGNjK%V+hF;}{hvP1@_mD#Sc4Fm`B5~#s8@al{ z($|6(U%<%**=Oxlb6wu1Jkd<%Zy6Usro1+w>lm04N%QN(UJ|Rge1zRy5lq_X76j)I z8a4_V((HI?3|Q>;Ph(%=Uv!MN<65)3 zWa%#BiIIIXcdV49D}S#6*PC&9H<+sni#c^y3(0uDheO+V;)UIn3Dbj$^mn%}QP9qO zVk}fby*7-uM1vla@KkA~&pXv(hPXo%=>;D-kN-RdVgwAm zS2yyYO?aBSz%-tDJ1JN7_@XFzV37PyV~xPQ%fOas=9FSrCS0b|jB1$H9>E!b=hRVq zjxV8KIOw_qNuVMgQsx-|!ib%%A3kG=mNjOOZ5(bd9p#G^$Th;osKkUfn1=li6Jr>ttCD-+(}}YX?r3e!3METu+I~Tazy5f4rn-rb~q2<1! zGYV~JMgTM~9puFYlg0siUCC35^YZ++2&Q2!0|-7U?E?MQ=;3(DOUR!tVrqZFo^I9u zWL;I)iW4UrA~Mg30GxU6K$r*q&D~AiEN0vfPnhIk zAGQ^ce@U%;)8;M$d8b6M|?69m(tjn;t<8= zT$QC%^{>8#tvA)T>1JP{1k7%#5c#+qL-FIvv@{f|fYAB0W02Td%(xV^QYPpnZVvA$ zUfPS|1Zj|h)(Fp(z>?JZdJN~G!DH|y&m8lON03!q*&y3ICm^{nY3x9F^og+wVXwEe zb=)P!r|VBinefGKA20uY=*!enS1oH}$2L1jm%u~rPlj{357VUWAobD)=xa!DA3c62 zR3uH~Yc_UGu8DP1jv>og&Cgz1xPo(b?ObEjZ-hXwz#iC)u^BkItiZu9WRB|}aa(x- zh}lSGZqH*3M;doh*A|j=eQD(XKp*cwUD-&^8>Ur|2zWnxl7Zab^O`nuV1YB9R+=|V zMG_C;-}(4&iF8d2d%kaeBMgc_K@R~Z?U!oGsGTI(aDT4f2zNICjW33usdGp8*Ux|O z$w(SM{lp4!$A*++jDP6iz(JQH1~*NgESGd;$$Dw7 z?$F?pSw*E!r)o3~Tk;bBt?``rNSbh3nqF%#6TV{);(=vRXNVZGXM{=I+o<|w;_YkD z9#?`uvNGi}rvz-U!o-ksKjt`5t&i>LgzlZ^J}e7xTE@|E0y|exte(CddeTp9E7m7% zg-1-nCOfdqfh|O$pFWx$r~+Lk-_IaxwuNFm~&!7kCH5sLq)ZnrkTX*o` zQbdem(1TKntds6Os5SSts+V|g^#^OUX95s!NxenpIVHAI?eziWWBTf9O_E4<$kRb5 z-h4QE@=TyPc>`Cip2zT$9@_5#3%__`4@HfAmAA{_DB~)zy0K<*9rb1qyD8|L@hCnS z^+qT4@n?*?58!B7?LnP1{EtUFc83P<(rOl!9dxSy96O?aJ9ZEzD{}H(3X$g8|M`g7 z#r|``9RK5l*}uDE`rn)|x3>@VSH;mv*ZHtS3(=U>Im6}{Iujyd4)PHnomjQY%QDtY z-jL8>u6AslqT@yadZGTu6i#-#VSkv`T6@h`_)E(ERiTIMD+npf@LsmyUZox`?_^-LHGTW;%-8r;b#!Ub#6vn)*%y--1`|6Yi*7Q0@a)jb!K=#*a4 zK73xeJ(quEDTSBmM(_M9!KVIT5w7God98|-4>P2%u(O6XINaoZ-i7E%CndHK~O9<_Ho_&SRjlczzl9R zeR7;mD9Yadxw6jXjfi(b|A`wlq^}|m_^uY*#%G;2)ZT*<$_au?dg42{n;qq~UG#Tt zu&XGl1vby@pTlFLo>5weimoQoQ{Mb6r1I~K>ywL$Erl!K>!}(0VA|QF>jBo+Es0Aal7C@)*$87W?>I2b$p=&qCOJK&*&nrPGIsbmBvI z)(^)W-ZBg4z7v4Ccd{`=aZ_bDBuI^yIy1e;*W}pKWGf6QWxx#xY>PG7VYL46_5yXF zGGf0)rx@HNyFO;zpe z^NgZ(NQv!kKU1KL(c}@B0RmQ?``rU%>JC0_ntm<>(Ps0(t&0!m-P;sh%si%sEAjcQ z9R+)Tm99y{1Rwf)rev6pE91I_u;uE17>RuM!arx%3JXELLgVkhy(%xUZD0S=PF_~l z{jG(IiX34E+vxsA>FeWjCFnxteBnlCB2c8mGylBzdMcgO%SUR2zkfIw5-Jls@xIy+ zoDpHJn)%3IIZd{-BA#~4+Rb{{0KO`&fsy7)n%?WdIYJ+G^@E#&+N85eWHe^5fTC5J{oD zULe!dD2|podJO`4!60p(btHSSR2v0H&{qy{;M%$1Ik(=^sM2>OMpa+9d{_yyuBv?t zsV-xHhBHlg{r?KV4j1V5iREvEtJ6Qb5n$Zn;sE4B1NpD)({v#Fbm&j^DY?9?yv}!k zHCFAx%avk}p{ExsFlm0L%r15)N%MaG2#sBIH)1q+{)4BAf0PLT!J9N=V9VJfk<-^~ zOy(vm5fTBAytqB^ftb!dH!o1J-~#}-h!d$s$f4>c1CbP_zY%UJYyXS(Jlv&R0RN?J z-#@q`!#-2`3k&Lh<^S)bJN{3e`(L6)kS)OH$iU=wxcaw!MA^XGci^ScKN8QhipR|_ zcK%Q!5&ZdT3mrhH29FnKFWk(EaJ>XXeiH>Rb77yUW?k`#ZFs_F?j=O|Yk76*fZhtbDj zc?AsiH0Ytyq4LiZYv7_V^1K4rX?4FlI&!)~T)>`{8;JJDQ;(l2Dga!|KYAsZ*pCli z+y6pUL&^c${6C+Bzt}*XppP4|{|Jggo&C284gOE2+b0nXnvsMIuz)Dh$6K+y@`BZCeoA5vu zz=TT;&?-zZp1q0+5d8UMLY>>kOX2LHp<#xb^gR!XphSIQiJVfNw!fO?T~c1T#aB&U zWy8h68=Cq!ndr+8GF*se5sYcj&>ka9Z&0FSY>($mSEzwSy-p|>JJm&60uSh}1a>XrUPSzj)53Sa#vPJ{6z4U-3` zAqR9>IWmCY#ShOVUyZ&yR~!ZKd@6Tb|E)*<3kq`!z6M-q@qa1yiMbYguGUAYIp_Jy=y#j4>022yoOPTyf~aZsS3jHP~3-E`Ar3 z^dIjXc)ncUwJvKB7<$VB!T>Ftr$*OyX5-M!+5tuCN z60NTZlm{k;F<$eAeZr*J)4q<6G7Tr%Y>tg-r|{*{^Zq;oylhHsD000o_Ss6(l;N*7 zvf6ftUTeauRhywkjSyUS5#^0Y6ThcE%tgL^YtOF0At{7@WkO8g4YHpnhNtgi7!Siv zm`xq!n-r^_iy8NWctL<;{izlJz6_ZC;CL(% z>=T1OuxPJip{QX(!2b*R3>s3o>GSrI6?YXTK0fU{nft!BQ<8&*Lk6*WE!SPC?-UW% zuJDi!HjgB25C7V9l3xV7=sw?1skyT$;z@`#VT;DWc{lXp9DO_E>Sm?izlL!f85G1) z^Xml8|2FjcQ!*j)K&(ymFngu3EUEh-e>v;f-jVqkq$`ExYtH`S(`8TJ@1;wykTGHx z>qZIQ3My*F+#Oo0;);bEp9Va{^ZFj_7!b}SC=fyjBz=N#bltO!&Uj;-4OrHU8Yh{> zn(7)-P$@n=C3&^ZP8zUHAqH5ASbKoIC)Dns>)Q8%{k{w{E%;{#fH1zM>a1(t!u3X1 zv%HPH2|coet3q)cP-DmNZK1SI^-xx=b%XI}BfqNt+)4M=DqOU>Yh%qfsxi>BIVbzr zH`+|NqmL9LR`j*K3erK1i z1ql>DDKD_7bT{u72<&xv8_7MQdBaPgxF<6lu84QpuYhRuqm2)yvH=NO91rF;@a-&T zZDB3I)RK!gPhfvKV30YWi3k%WrCQ8^WuG;7gNeQIJvKlD+I6{^27?&Ydeo=S%_7v3 zIa{B742@PoP5S^9rDStI)rSEqKq$PKVT_hg)3Msk+G!X0aq`t)u2~ze{9ojjupzu* zPNuFz-ooFT1*BVAF)bP!pwPOMphuE3gV+!_{aIkAXA1>Y5|ziAdP7@u?pro7P0i(?fiWPLXI&XJNcLXc+Z$Tn z(@uCufnG6J%goxH!ICJhga)kqcwVz#`udABqmrx9nC!f12a?5RYkfxs2P_r3cO3)^ zrAUXOfRJFI4vN>I(jw$_lj{ws8WLz1LX*SEQ8Ldp=%cVaV|-ZkA7Xu9!U8u{phu6y z*5H@I({9!J+D6NZ{X)OQblyhr6YzfaPZ)Aq<2LDCtFr4lCH0mK>~H!SN-U&7RxvVn z%{xbs;ngD+ohOs7soFs4O@V-ixDL>ZltFNbM=Drhc`TVBZ73D=r2X?F&OLske;zNH zqS?}JNGBzWL%-a;VM}#)do^u0%f$>u zC$z#)85(`>HTp4j@%}amp>?F6oBkAY@3Z}|EGDhLtpSZj3a_t&@eOpFRH= zIGMUzcHY*pn^K^h&9Fy2lz3n%R}mksAA-~_7fD4c6`(kFe30-2!K=rotc7mfwHKNYyLnVz3FZ8nOjKIoT!yI z5Lq2OL2F$cC9e*>Z)d>-to)GwhrRaziYn{cg&RST93)2-5F|>@G(nOKHX?$6faK6N zNX|4tkPL!=f`CfSk|pOLIp>@uHo1YOy~i2H(Rt_l>b>8sTYuI4-zu%@ZqC_zt##Jg zXYUoB#ak!|n?|`J($iaiB{brld}u!7ra z;oekJg3v`z-jIIhXmm%sy{z>vL$TtbNSq=jKDE8JOAgq~Sm>!Y`+9tWQjn9I^oUvN zx~;grHI+JfK+Oxs*KKZGq^Zwbx7!?K7@R0p&)VB^@$qSZKuMvAr{j6j9oC(<4SLNa zt5&aWU*}f>?MQ~<irnR!?HjBw!+i} z3v<|n1*69xWoaycJ9U^fb~k!*lx8htA#YmA{o|(y+mdpyTiJvXgTu?0Js@{(iY7fz z(|(IR!h%pP@$jU>;#S*hpGwVCJ%UL-)02Jb(4oPe=l9-U_Uk0nf3PI{W?@_PVQGzO zz8us`QB`#Nu$n4blXOzv?z*=(ESLRhp)m-{AMhA|sPXGs`k2ycaNX@1Ta&o;L^IS0 zW`3Xww{xJV`Iz^VDc38VTxsR)#bSe zzp{I~O!^0*t7mNODa26sp1K6CvP7EIWC{I~Dvx$@`!1?PGJ(U(L$TW(SblaoL+Al_ zhMu0Jx=16ByqWUSDxsOAkMcs*LNy1uzSYo6+~X$CnBVU6$uO2ExFvWx^_QMs<#{i7 z!c8~x*?~JCET18%R77LyjrnGg{GEmhuZRX14xKa;vgAE7rls$LZvFIx;_8@MK`-NmX@uiN*7K zJ+ygvbFJ6;-!;rzMBj#zdp@05+|;MHqKXgPxX7ENezLz-G-YwQ@gO#lu%#h74RnQ? zg%)%q&e>yf@lNuVRNheYMJfk+ev)HCRNSve)Q_#ASF+WLMq85j4MY*E6lFgscv2s6 z%4<>SxA^SomgA{`hvkhBs@PR^2=3a=M@~R@DuCfP3eQHFs8JHx$aFyo=W3Ve1@0U5 zl-tKD7ErUQM{)NO!?@_3Hw2P7v56yY%NHOEm51}wKc+xxtICFUbw?&g;WvAD?#AK7 z9%dU)^e0A!xpoA#Gky0$#b7_`lOh8L19&OXMZRhQX zw9HkTm5MxFyYR(}n;8lN4y(N(i-M3aTB3>^T`MErEg{v!$ddH%i`3@ZH*wjtIoz3* z2wvMh*GGcb=PQ}pfYDGm|!c0JQnwH&k zRp}e%ZcY9odV=e1%I}D^K77g-SzPtcLfRURzBU-2T7M?gb5XmU+GE!)Uv7_J--Sr^#QqH5dW3)jb;w}r%aK_q)K(fQj1D9^Z*!I z3~Wzja3-LhsrH5yb0lWAQ=9A`izSe#?(Z7fPBqI2V9~-giz!F7P69VpZ+`#_b_*+4 zb}~)xg{`?*uSn{h%T8HUwNI3OGNpCYntT~^QH}4TOVMM{|%e$-GBjP5fXX!o48h<C>SNCktJKra z_nCUpM~WIS`kAYtPj}b~|vIzI~G4nNIZu3QVO}t>@r0w5hYt1KaN+?J-@rmnl)PN^p0*lS@-3tNk>9KRZ+`bD zE$Iwa#JacmmF~*`P2aym0x;ZqwUTXYJfJ{v?VD5YiKrvsW)E>PZvpVKY5}~gW5Buj z2L|fVWzYI^UUU=NSrIv-_SmL`o;65l{Q)CM85QRYr$`N>e7!&OdN#! zTX--E72PxhUN}8K544JG1KE>(0O0-Zdl^P;k*%SHTXW4N%3}{^*8Ayf%k*2J4y=oB ze(N%ZL*z}WtKaG=q~@VBTEHuq7*4C2+vaOZc|fAa z_?W#qP?W_*tjBb7uB84&jRH}wtxJ|Q#3bvnoq8h51YM=J2EbQ+w{bL+o4@@A!-o1qX~WMID4b>Zp zHxOrB%JA_w@w+Nl!n#DO9j(lsUISFNTKyA-4E5AtE#_a( zLTM5(t7M>FZBEWdG_=oOoZ{d&vjk+Y4c87muBPYcz^Pqi^;?R~IjJjo3~a~G1`btK z9ZUC!YAI&Y;vy|P^Q~FS^(@T-S=70YDfZW5?;V*-l)UwUj(Gnmr5{fQvxgUKF@v`; z33WYEGw;R!v;#ZBsz|=ZyS-K!w=y5k_x76GDkx(VZ`npDs~)Gc+A_a2B0c*{$rL1O zdTM$~g%X)o>+E)SC#>-Lq(#A#STUadH39!#VvWJUvzkq!=iB z?IjU^F=tNf6W43>rrMl#=OV7&XS?^~4zx!L7V82x#9+(Op!Yi_%B!#Wo0vSXaN;;{ zMb&TPFFqsDc)#fGdcE5<=Mg{joc}4 z2&oro?BTEtdWo#DrVVpZb*=J$l>K6>?^$`cHGK?C%*6u}%V(7|Z4Svx|HVVmm!F?a z%F?%9J2%m;7%Ef!w7VmimG)u_Xc41wBc?}xfJ);x&L}3UF8WIjkYR5COs0-GM&Dm^ z_;BW)+MNwGq1+ZlwLIrON=9KuJtaI-6K8vQynhm07fJw-38YD@2bSsefzHUy%8kH^ zFh>w<@kwVXOgSnqBJri{0ZlIs7Nhk}5kf$D_()3re#OX95|Hep9=%dv!*w%QS2yfwRIF|GsQ__R%&jbPnFaB{2Fi$~v$O5>NtED39+;zE>h2$KYMJy35IyW#XBqY;u zXnJ=XA~>Y9=VycSRO}BP8{YafTp*-0af3irC`Lz=#;|&&@|x3(f3#@9+&_y>@;;Da zJ2i*A$IUpW8Q0Z#JchlOr!d$#b~Q;VuglGRntF=((@V}F0plV9?br=HNeQcQ1HB;Zf;lZ#3)J@*aUhtDx?|XB(Rjq^(?>B#e+( z4@?gTs6XFS6zjb*0DrD)@A{xgoFO?k$7&7{TB?@0LAgu%>GCyKX|KIv;q~Qb-m+uN z)D%)3qzarAFVDYFrn&Embu zTm{Q@aZ_FO!Knabp1EqpT!u%|4{)&HL|nFQT57dbbt%Lw=Yrl?~B&E zSsW!i%ycV!eE!MKn&{$xwX#VLNaH+O2cS%K<}dJ;S*k_?S^=~$J-w!fY0ulz>-q?c ze0uFQD-6{1`=#-}Oe&U`qJ?hnu zScyU()z-$1O6av#3ZL`dF_)+7AfssJO=xg4!=UaEEpWJhk#xwdw#yvQ;C7KdZJKtwrO7i1MCUc^P(Y&PDR*&IWo)$?Asi zv+2h;SC#9l52Q_4!b>|esYEfu%&oyC^#MX)@@eRP+1V^4SI>GYSd=vQI$@CCHUNyU z)xA@B`g)BSz~K1j+1y~6TVo5H7>h!sB9uj&=2|hI14m3N+-@Gsq`s10O_?Pee>vk} z0>T)SM--}tDvBFb)kRO8yP(NHIz#r;UU~WySZve$%ku|;%*`j?b8yRP}Jk`b@SB%`Fbp z@tQKuSzEsh`6o>#8@T_kOAuD6=0C}mn}X_}l}1%T;n%SNoRPW!kgCG9YhnIT9wEA* ze+$ck%s{``HNiyKA-i96mikEMJVVC*ML!{TTEDA(Pw-F?~O z;Bcu&L$&A4p;pSIpzSH59zY>m22#8Py;6Z%j}&~MNf;3XEuaBmfxqzvFPq=jg21!2Z~1%(Y>{BoUozrE6F{1RI6Nv2lhq@dwX1J+v(z}K3-g1+B4EJ*(nZH5Gb z(TNIHzYe1G3nfC__|f3*dZv8=b_OV9n^pvc1S(?Z?csX~(gUgRJ|LQb#$f|G!(~}| zejo80*24&u__r(J^VYzHR-XoZf!=2LyT3E@z*vkK)wKs4&xEX?N`H(9|9wE0-)F7^ zWH1cKK!_VC8v7Tn0UUxKT>mkiPTA_8yu_=1m(LGA2adz((4M?NiykpBvPZ3^`8zXN z`Pc2fQGbHQ&IE{0KUWO@3X-OpL(-<8%rj<a{q96^3b zqTRszo*;1kSE-#}Z4DJ7a_p}Jf%Z}Q-rx`8P*VNbxe2}5>DrZUbJd_YIf|REBZ^8O zjtCsjyhIXmWVdtq<^iCLsT-0WdVYkq`e* z5RvPWGhww8?Br(qbmv*9U`{|}IYbcS!G9|qb&>`Wdt$j;mjCBctZ>WGvDylCa=Tn2 z))bts$>mk-mdnxTbIp%fjvbTtza`_}c}O|&x@F<723qahY;Cz-T#4??={dwYP`i03 z2jlLn|3zyef6*9SA3uIE^3p)Fzdd&FFM4M^AVXvL-;AP_Ig$sn(U^sHxfU?(wCv}U zcwvYM+5DesPWH`w6N<^n+U+WR5oO(nMBbrq`!5^*LjV5_jZb5{@8dwg8GTIGPCVW3x)~g80Qlgxrh_Aca0Fx3yk|Ks4a5a@J0Q>(L&+*sV!Ov$)*7E zK89PPq<|MnAt8LO3%-|nuD!5}x^RV}(LiK2+#7d5hN(7XvYr^ew%KfWvOkBiM2{dn zzJkC8v_MlcAn|y~(9cmFXV>DmuzBwE3^8_r0_q%T!M_qbpG1yvfnjw94ithK*_oK6 z&C1-vDo3#E>!;VNDAR*w*<)0xtLy&?2!zLB=z22()v78v%|_5tv=gfkPVNKTi91#N>P6?TfI@^c2p^1b_+hdg7 z8c=7SoAiXO7(^|?Vi=kRq* z;)rcVplIi9GD1{O%>uzUv0nk5%(osd6E~<2_HVZ-MzUDnkL2D>-<|B97?C8*wxTVY zO|e@JhUHJLS5>mFW2-!f@QBR1k(h&ooYUSEp)F6g!LnkTQ13KtW`82RIz1n8G@!Or zE4_!h6VJ(+_xYLQ7dh1DRj*~E3CQd*^YpMZ`<_rni3M9s9!%br3}1L?%y~rt%Pq-0 zqKuH!>cSRiW;?3+EL){03Vz9=nlgdhX*MjiuQX!ESO zRxJxjx!P(YH}`1f@sz@v8WgiWA=pQynH>k@_e>4yn%Od%Fp`&iuPA78mW2|Qne{Y> zow<8+1R|IaQJ<=my`fv1KYb#it(9-fP~>m?-nFKjt7RY52eP+&z)InPr}+`pJ6fLP z4t1%no`N(i!%DLYO1q|(*{_%K<@uz%-en>1ewc4I-<23k%sP{v1~#T@)0sHVGdl)3 zXN!HNH`z-=>;^~pe%L@Xo%8biCaq%`JBIhD+~tx)#K^z|a(~oqQfHTTbW!a(7f|_S zs(qjRwb-e+iREE-DZEh8NG_RH#i2{jTV&UJ27L8ZVsqZdIW=1ks0ZO&E0$;(LmY?a zARBdu=v!ynfpiasXwhnlolVn_<7YN+8ZD-(!?R{;p;k^_!t+q!G4~SXit>RQGfKuJ z*gEo0KD-=vI>|Xkvq6a0uUeg%H9fv-duP!qZ@?GAOOHe7pAf2G4P>6eJG^_Jo2Ikv zOdRQL!g#-ot;!PnIN|c%ERFV1^?3D3xTuvYQ=k(h@c?udaSw+%VRhne5sn`b;nQ)~ zz(u;GduKW))*__0^Ob_>>nO$8T7cES9!K#C;gTg5lgTF z00C)!T&Kv)g|`O++a3d8GncxW*Euv&ztpS%q?-eq>45`{^&TTc*U*tt*1x9y&K0ly zlKCsN{RCKOyeohd5}w~vNVcUvwjokberv#J=W`bvaph3DCip&n3c7w5ewd>>@$S;k za23Th+D_b&0Y{;)>02d*9aQKdavE`Q4k&UpkHW>A!f%{4zHO^em%QBYS-n#moDcE@ zc43j>Xt7~Q?s;yuJBXh6Frm%JOICHP!-YaprsIM=954=F*b2ecdV940D*fmO^uXC% zD9fN;gEw)>Sbm`-H*B#2ru_(t^XR4pE*ZyECoN>ALL>EMpZg}7!IQEK>6X3j615Ry zARN1(%7mArLUNzQxx3}JXRhWmorjEN@(YoGf6xNaE5&_nl&$U@=lQlIdxo-{Z3Onq z=j}Lw(nhE5cB4K5?=Rj7foY`uVF-{Iy%}2p}C3jF}xXv=C(O!-sM?f1rX`oNIOwUrk!* z;;on%pPu`|k9>VI-+GKFr;8%n|D+M$X*Qs|99sq(pE!t-r`k3GKiA4xcg*ilO%8qe zO_>#UAsNdM_sY4XT^iP9uI=+a!P^J8?VSP@OLG(DhSX34uH>%P5-VCr$VzF-qoLju z@~vH)mx>uQDug7r2%h*+o08pD$yeR=t%%G;PLI15FLpK0@?P!AvfMTye_a_j01GSX ze%2|R1}jN<B7 zd?>nBFfpe;!K0LhfMj}*gW<-0p>`8*NB=)hh@zw#aNeW1_>GYJ{IXo0Zs zm6R=LrA~lJw?SVoNzugOs(Y1$jon&+_<7T$C{(>CLEW``YNw_^{kW zc$U&M<;;UFyAze9O>Ukk?7Z=yPUzjosYnn&DQ`S{v8?8n5s^aX&Gco@()mp2McCoF z8Oek-Br8;He^PMZ%(KW ziP@-|A6S;-n%Z)rbz;CgoT>`v=mT36SIFIMrm@kBV|{%j+)xKgZ&r`#yZvczTAW4N~#G}Xk2H|A6t zrgk&MjJs3OU4VlOMdY?}yeV7+(AUQ1TiqYv+8AgZsU4KUUy8iq!UUI8AZ?3T;YAuf$ zc`aWXTo?7bZSs0k)B)(pYRp29MsJ|DB`+_7(Y9(~8c z{x(CzmZmrt6KNfT)t;qRf>dPKVf90jal_A9p{mcHKMNHvCTUf~UfB#=5~`f%y}WUp zaGVMNO#vYp*9bIunKD<&u?MFaYaBUu7gB@T5nTW9HabuGWWmBWPT3Cs1ALBfd&^H_NKsw@5QHy<%xal&_9 zH4WJioi4d?*Z1+IcK;~lYs2+abyuf6pY%J<@a*c12#RhwQJ1mCt5R;6xsEik1WRh9 zzB4EGt9K(=3t9JMeK+F@y%W-OVlXVJA}(+>%R2N@@BCYzJEV%Q8L-JcR(%AdIg!P1 zOi!^r>}tzU)|sSS$?NFd*UAss#>sHcwAS)-P;%KonA|MtUbEy8pcg`VPxo$yFUlDp45e= zpJnEko`x+3-Yl#2FQ1%Bn3}tjd!-pad()UQGurHu(#Qi-c&)Tb-BU60li zwcyIPd9)m>^;>~EcDoE`J~BHCnfP13QA)E5rLKGXtr+!To~`scDAX+c_3Qi(W>#-M zD&SpdFm`Jr029{Wcr8pNLG;E7U^_JmIadZBmS~iZepI{?GE3q%S;-RfnI)N1sofhB zLf4v6<35_buq}V@lL%#1;7b-L2U%EHx~Wq5a4w1b^)1CK*fsQy3mVn2QqE)5<%PSO zW8Rr%{?8W_6usfBUf_kQr{*Z4y^*z+`T$*=V4=(9dH}i=zgH9~9eM1~GYu6vIX@HQ zyJVXC8F`+Ddr~LQYcR45zJwKRXb5V>4R-Jssc?nTRx&j0_DbcdHb~tn|maSI|jR$qd;7}JMADMSI`qpv;ZP0CBl<8Yh8O~bn=*)CYt$0cuB02 z)SE|_q*|XbWO8SEVj&djUO%bthY3whDB1|AXOqf9s3FX6_(S*WQeY^q3&oxqMuF;EpEA9C= z(~mWyh%mcw+v(Pv!No{*{(4A(oo1cBM_;%v|*7Sc$wlv}{bNqQ$?>^4$JLxU38F$A%Rji4?*bS*OLh z7Y3ALymx@s$6vH6N`ic^utZZhPvVNwm_%4-wipI5k|B%ILzyTng=vTF+Bx(-yFHDr zEqdu(qWFj*zhQ0s;qD7G_MkTnaNY-1miYh$J!lBDQORLI|kW;K352IGV>EVVZGc9@tc}w^7$~>lZK`%u7A^JI&8~K zHrw$PG$)WUYqJ}rD4{L8G>1KfJZU;{mpu4vIY<$Q|H=EJrlWD`rPqiLIKZA13a8kD z1tzft3aZ^ZQfwWKd2oBGBDIti-+wxbZM^?-{qy*-a&2#RMEdsfWDw$LLl)}ttaZi1 z>V}T7w#msGd5UQ_QLKYU^-L(cC0^WVSKYoQ$}y$wgU3P_+vWZ@3t$T&+zoC}E?HFp z9$fE`hD*;li;;Ho!Jaf322vNh$H|r1(`Co$?o`&F1wgk$QS9$7-(SDw=`fG8_qNKb zwIW!kp1@GYEhJu-YarV(3e@q1%U-d0MFST|Il?kszO>d1bhv6>oWoof@2N|tETTAi z^&k`p>#4eTH|$ezFyHdpGE40GJq1UKNFZ?@_si^MM0xze#ZBQ*wISHxys6;f>n$%a4&>09r&m z{0z(Kv`gl)JFvln2hP&%6*fmw8j9;f8E+3YtH4rqH~1#Sr?`IGw$Irs`ZU?R8h$q_ zRdOs!TW2991ki7n7lnsvbPt8aoe*~7X{0n)9LE>TIP@`jfwe876k}WUTpalrS>JkQ z6o3Dm=T#k@@b((d((x%w%d6c&^0#w$6HKvIcPyY9Poz1!25yvzpRd@i@~L8!Ia*oB zm3NS4+dPC_CqsEeR@!Fmd3aYtkLQ5H-e&t)f^dAg+M?KU(U4h1!VJNEn;DF5x;dLK zm*7|I8=NUbg20xxLDKA%Zb!tV64g`Fy0L+ySFW0_=ssn!%%SPb=D}X_?E!hf1L0_~i3)it@>H{$^K|uQS(waS^A)=BD#JfI^V{?~)R%L1)z~e}Vd$WU5beQ^ zaNMtr=m=^-Hjz`kX_i@J#k?7?`0@g1c11rdU!e)J#syaXgS(hraYT&W2_nP z^_%#xV=@Bg4dPGW`B)_;GL#jN=#*7|_^fbfJVH}@l^8`_z+V1yBHrf(fp6QvDQ_;L(~qc8!F z>Qv$BrV)U2f)7XYL?V@J>eupLpa}q<7qY7RLzF>^#R=eA7W@iYV5kBzRLYr-;ep43YB?E+qD>qw#*z~!b#jG_iSVSA4Ch=>{ZkFQRzZjB^<{IAAc z2R6*kn|~3X*U6zD;O>TekKJJn^0azn~P=4VXVvGD$9W+o=fGi-Hsuda{(kA7 zg4L%WYWz;?@W0T?_`CT2q?O~9<~r4PQVsu*dL)|Q8X(yp5&$M_B1ZF+uxIzH3~ix- z2ZEh~Vy-Ye7;OjAwn*T6j!*?MDe-aT=-RT%2>Gkn>TNnqpa^0DaV;|no*9hJTS64| zDJabGdb7TtOX9PjE4vd8KjDmQ&>la7H-Ytjlv_KeJTh^Gpk*Dtg2pa((Xpy}LVZ*1 z+3PSnEJ{u|lw}`6A1bP7$k$c;k-0?RNyK<|eVG&d45I>lIUNMM|EPYls{R5r1)y6G zMsDjHqJ3arLB3WpbedYc#|4Ez1O+HjZYTvDrrJOtgAP8gtByTF5qgoE`xVqrSdZXl zM~?vLgX`FHNXl0jWeUas_zqB9cZg|=!oLgt&rJV6k)BYSRuRyJ@lVCV(HsG`D^Y;D zXsF}#)K?g8eJMb}5#}MubP3beXHs3=abu8`wDk>4(0GGUnQ|Y4TVWssb@gP4BiRvH zi)iH0LT3{WPB}nH`t5HbOv~XC(ts~x!x0?fAk@3BpcinI8K98d!14zqVR=;iRd5BfoQI&a49qMhdnPI|nuNt(ulU=BI0-^G-* zv9XcxiD@6OKD@i2sSic)ACH9TFFW@U&dM|V%hr{Cvh#O@Ih<5~+4}vT>>TvBFhFPj zmJMw7Y-3wPZ1*PFYZF-0ZBli@aT?Lb$r7XapnsJji2fF1ecf>nVM=a~t{Pnn5%&q#`ROK94k4qD5oeCPKM?jJC&b?&2D zIBS*)?Bu7nS+Ka|vD)hd?sr`$9{&nD=ts+i|G^`Bh~e#z9QWh11kZZPE2#Uw{R+9* zzvT&R5|T5~r{oKxN$OKqnZMr^C(QPYjrE?uW*a%l`PFA3|&PP$D+30-y2E64$t6@4LXj(@`fhUkV$ zZapH<((n+s6bQM={jQ`)c|qPYtV#IXdnMHM1geWmFz{~w?>dSm>A^?)3a6Z`115FF z98e^gM9BFeJEnCAU<}U%5%3V`)D(Y+@=ef~`YRRBG6_Wz-07M7`&dA3OTQl=jYI!XMef+l?k z|4>xFK-3Tap=vKI!9O(HIYc-FfM<9vA^Vq>G5{~N?`k?R$v(+RhbGy^`Afrpn%NR` z3rdA1nYr>6)cw0&gMZTL-?wx>>2>5^CiMHB?k_V+{mX=Y-_reJM!%TQw{4xPomzfx zp=n;*?C6RXg)?`NE(`2kvR$0$;g`YkbFjY$+=^_{qzIygl*@CHq+3%?wSS`=iH|c zYQFuRZAmbea5^VJqkW!pS+D&3PFOzMPCo%mGVVkOoQUp1Yr55M)vr8cb1I@dAbZqR zOd278$`L9vbG;iSy!7m-A=~rYFsVHNuF+QzN9Q)hyWt8>32DW?1;*z`@AN+U33haT zWJ3t<@t*U31$hph0>fP#s83Nof1nrtwyx!wCh^LmK`V zGH%xQ-T0H+oy&Y24NjX0kMbXyx|nISR$!{G0w?^hpjh@qriQPelOB0{!Yk740c@Dv zbDx;n!JRM4&9}nOn~)}Ld?9bU`ljLMtC1oXaj2}-s6c#UXY4xe{1W%Hz26|1yOaaV! ziI~8>ALqk;1K1vKyLfzn_jQ!eP`<_f;f(R|nUowKiF34Qp7|R&g!1(PNZ02!PGZ@M zbS|L(@5vm=5fjm@l>zyq>j_Qs`cu=Yo@P->qKIQH+-bDLQeaDEJWIOh_&ZJU@~BV@yhYk z;G^S|O$!$v>}a=h5LaNNS0GtQ!792&kg#s+eS5<7LCe`_;o>HPMRSF&VF0+y)^T;1 za&=Htp*uua@dfl=>oaP5d5(UZvH=77xiYE1Hy@BWn~xE_@6!y|bw!R5LjfaY%`>_- z6R%vCIST0TI$BKZ_hox5Qzm)lWI|g>k$uhLkpV!a6w+XU9zRbcL$x74Ek!8jT zTN;e>!INvWi^ld+GeT~*bj|l+L)|wOfOxZoPM7_SUXfT0xwYweZQ^&H6hof!>3SR9 zf^lmt?ib5Y!68*A4h5TTft7}kh8u;l5%>6k4780>?KenDy*X?k%kw@$q4&K*&X;iG zs*HH4tw5CpwNehXfEYfNLOs-}V7X#H*D@Z%uD>4ht2r%Wq-N4?mm@IVd zYq44&#)XPczkCx9DUDm}!l66PzUD333VKS39~LFBVFm4!6hqiT_7o$bd9I^+DWuS` z0q&N3*5T~S+0W>@{Dhv6YK!7zx2de=yRz1MTfq*^BG#@WO2^lW|o^lDWvzKF@r8+mPe z@6-X|JZ9zP5hsr%Ej{>6C5#Z=wwfDRk|vDFN84r-`i~pYI~UF+?z-otc?DHRT9RST zW{^v@UqRfvFk4uR`^4e;j#smS8@RELthcIm?FHTas?l$t`A)Tr`==F;FQUDgJ7Xic z?nf;|ie6)UQxLzH5_~j=x2~=aL`5Gl0%a-570--IhVtWtU6R%61@?2^%hK1WfM^ej z$Qc<^GXSvdq(e>)!~-P@1-ld+NiUIB;y`!e5Q~zCHvPMlQLO}l${j?V4Aee%Mb$T> zh(>8>vb{Nw<(rKL*uz4cR|fT!UvKAA8dk2|-K3>FzU&LWyDCmpTI`2$?P|YyTutxL zJk<+}QZi_XwK%$biGSlnWQc3({PpoiQXh8?1V?)_q3ZpivNu6oTpK=OFbXu(O}zhF zKOVjR@d;J34}kRuI;`UKK|aX0R^tt-5y&%;PKPf{wU+Chg2*1r?Bzw3EFq7}sZThQ zk9_ye7k5e!y>NV;b?4*3$JPgr-NvZgy=L}h3HWTy_qS#3RoDgk30n@|5gU9cs-ej* z4EAJBweIpT%L<(mEs!3|h&i^ZxF)eUtupTLGUlyQegfzgZbN!T<)V)?6X4TNL+(CC z%ryg!YjLe~yUa=Jijs>m*xUDSOcXetM>wy1W`2LoU3=B#cr_J(Q^qemN6jr6zMn%p zJnjobGr`AJ=Fq+j-uCL3=eB#4(0){gC#Us5D&b$Bv$bd~5@&=-{QaMZ@4Ox0rm=Ota^id$Oj zX^^x<*Ws#^0Zj!>d{y|Pzveg z?0^-p@D!=8!dL}P?1`^AH53k{i&32V0uGTJQDpoQN1)MI_U|C380|0B(y^n$G@~)|#S`x_ zaOjf!-nRg_SjP23l*Vwh2i3nEial%3d44XzmN)37LRxG+764EnkG?e}C}`g3w#EHh zrg~8g8{j4Z4%nM$z`cJr9Z>u~qZphC$k=&RaPaJJVHOKT~8W3N5rFx=RXNN@UPEOty{5?hi`gJN2nS+wuBkWKb?M9hmy&DoM}^B&xnB$i7& zJhsKRdI#bOxC@M57w}6XLTUPF`hzacTL_EdHEew$2wXACT+1jNLGd@Te~ACp#e ze4l1Fnw=^)PW8&Ry7vGWTTF?Uyi0zuns}05 zLq@)>|Bm#qU2RWdo(p`nMk#M@#=6hM7QV1Nka{lpqziyA!hc;r9^+p&a&Pdq^Fd>w z&IcdKt6Ip*i@S_Hi@`M(i%ENp`=nM}s`D5gYYq zfu?bH`0Dtg`Efr}Y?7R3#K-MrZ z(A%pOq9^&>E{f%Ql)>NOPVcay)PNDfyK4;{ZJ|LmP(p&TIk^CgRB{JBsAijo1C(8Bvx29%47DtF4{LTxYwkomY0>24EN z!c6*>iR;o87HyUJe5wbClNa2j7kV1yBR20kLqjDn%t(GiD3T@K8a@zWSpOmO!pjHg80QCQ*=wK_Trm+AT4Lxc~-U;g$oyV zuW*+M-6Jg^Q_>3mh^>rMM|`O+??bI5qmdpa+~TxsGskLkY`{tRpvaZ>QI`i~08J>A z2}yBwn`_#|+|ZRePG4kjmw@W)qS;~Bhw>5~#l7Cj1ijO0@3!ymb~oK?$ggK~Rei80 zDehP$5>y~5mNkCQJi>C}YU6G8j)5M05|+opO4kA(Ufb(?pkOze&vD=oe*}xD(o9vv zBNej^G{7iFTFiHWiKp~?yst-l1lwdj)Jm|*a7%~fT*~5QHj-(eX2|J_thtU8Dao9* z?$CW)Z!Nnrt#tDKrLt+1NA6{CfFrpUc6+?8>*gYMxMh`hHUhn=Gm=V2r9M~)8||Er zT#`1)ZB0(25Zz9gKtdM?A1Ed2jMXS`@w-Moj%BHg7>^+=RjxMn#a#}-TB$C%s^3bE z(c%d~N-Y$II+My5>Z-7|mI}05I9=}USFtgdDUb~x6T`44#-T_@Of`(c2~e2tT(z2X zEJ_EtmP%DJoGoH38;u`D^V{3KC76G9*|Ag|hi{LrF$Ix?)bbMMOf~Y1R&p|P#r6z+ z{ZinT?ccFG0qeAr$KB9er z-h_3C*0^-9{Ku^>i`>@hvvZ$qt_0IrYKBN26j8|exrwsXsx1}bLcMRqLn`|{wH+2h ztM7A&s+v~%EzpBpT9BvlmK+UX^vkzdwDlA~Ph4>toxwyXs#I6#JvX!BA@1l&F^g-T zZ{qNbgG}GX_yEABcymCo`WBF|;01(Me8AE`fH8M&pT`sUP}o&}aY-DtQaH+sWan+L zy^4Y&?)kC>a1R0{>#d?gH^vz=su~ zjuWPBHQyJiJLlu5FAo`57&7u`iDS-EHKC7xmiF@0p3DAar96+)qFo4(6uS^!Cjjpc z_5kWD=7Ca(SMX*!PjjG&JlP2l>1+f#4YOn3+&qrn%b$%mGCiThgQXQmtA5(Lp&Hs( zOahB@1I^sX7-^997t0mj-3Bjt*{|w&_K2vdT(1sl%ot~_!mc#6T4~xhH4i6RgxszE z3L=2*C8D3J&H;g3oUb7IR5bO}lE}rC`1%txFDO_UP<$0_wr8Zg@Ld!W%HBuPIOG_PG_ehvJG@~4xIvI$)Rhd#R+n*o|W4 znwYusq%vjz=G(W)uh(|60xAFR>~}6%dRkWfS(aI^>G(75J0foab*}fven-pvI14nLtgK{`-qAH&@|^bKf0`bJZnxW0rsx4BEqREmBZJe!4?rvo$K@} z%*ygOPS^Ze>|XkC^c`=XT;M09T%gJT%^N9E0GYsT=HUI6*}wzoGDHLIDE@Q7f8_B0 znm#ByO)Y|#9|M^wyDNoXL5%r1?3nRY!@FiU=vzIiXtXYntFzH(wF{gTUOH%eD^aUQ zxmVj`q>Zy<1|1ZF*Z;aTwYLUXsvREud6Ad@WVrvN575r*03FZ+`97B9X5Sf=5cvEQ z3M}Wj72{G_z`y_5P!AIjOs7;ryx+$v|27stec7YD$B#dq&C`msfR zG3Vi5ZCy`L{eS-gTf?7Li*F++`a|ubH$>vqFW&63k>y?X=VuRB$G-M5rb6=SXxaJ) zAH2V{V$xRl`|%Qlq?7-pimkv=2=ePM`4B;8L``v3JME?Jt!E$xmFyCC=F_upG3@pM z1N^1J@c7B_mhuGrLfob+2?d44-i2%{ z*{0aGQXAinz1=sbBn!!S;9D4HK~j4EF6kQ`zi6s#%yea31?nZKtgI*O>1j9#x`CGf z{4p+BnN5XD@53~1-I`GlPV;RTP{U4A{{yD%?yXzaGRKg=5Cj!laSe+M`S_~T48o{JSr2>_x@LV-vJcW(ycp) z0wNLwBr_^WB?%HF4jv?l3MxV3fFL=DmKj+9f_uabhR=r#QtvXc{Q?>W*-MzbetzO;Vx7PC7)~>@}SwhU!W&^{<07aBi zdAtYt!UWqzjPF6_waQBsm?u_40ixaQ3os_KN;u&`^uYo@CKcg~8*F_7$Vfxs&<}%y zP6oQQ0@j$x!9`#$>42>xaH*n5jV5dYnOwDX-8XswNOq%*=)Hizivzycbo`ISsgchN z!0HhLpmz}NDh=KRw;|Ne7F^~Mnhn8*ibvj$#C;1^MY z^CBA5{e(TTF?Ig3!lsx=?8oVU_5m{KEThX1<)z^6}@qE3^i4j$<0FRMYQRnT$*=PJk zz;^C{PJ;eLJyw9w7mf#2B!~oXKs^>FpCttjc?X)dKXeDwBfmu$q;++@lVLkE?jtuA zZFv65y2Kp1hikI<3;0`)(D``xD{_q$OS9{%vIlvE)fxD2kj6ZLB95k2%fdTMXasi1 zeSa)PUSK1tGNoDs5^Ekci*+!iq3NqvRjf~z-a%@2IcQO+82oQ<=^ zs!V?G^>{A7v@L^E#`9x@$q0=7emr<|;~?J1p#6TnCvA*L#kJi}h|mz&i=q)6`@oWe zJUf0)t5@(yLK=GD1FoVCDJXF$A5VQAc6fdnn}}n)PXAjB{N{DRLfLrPq?b~k5##lQ z79Xe8Zd@3)vfh`pnUmplWdL`z+X)r#qw9j1BgG-}T{5iOnU7H^1%{q0w^N05mY&{* zUBkU@*kN*XPxN-R9I2zOJ%fkiy78VUyt{GlRM~*Q9>iT1@=YY}4L6ql{TFEVH2kQC zU+WG=xYf3s+8DO|ys~eOgLM6|W4ATIb{>Z3DM6|D(V_Z6KwQXcq)nZ9EA zLsLJM>n4f>Uzy)9?TAj)tLeL=<|tJ1j>GFhguh1LjQG&w^sA9RZbsx@vXQm9G6=YP z%&dhQDj?7%f82d1+OiNqai(*shJ~F>x%%-1(5HCGC7M~ToUXz>V!vd1=?dz(``2N}%Mp{+NkI9Sz+ zgapzPjLs$bX!-f#1^sF_*h^yvKara{_v+yn09L7DJiiH{fUbquuQ zOdHsPw$zyU7?bXSFvsE5=LBdePS<#QrX5nxLte@yb?4@I+t97w9@iwf`kqS|n)D`D zfe@V(C!Z28eg=P)ux!eGib*Rn;u+Nq`4#r*`(ON6au=P?UA;c9UbqJl7KT?3A3BcE ztThuKf5-FcIJZQc(#*xmXL$+~A53|?`SmA;aihB)YEAk(rH<0|yM2?xvf*{s&Y2IplE9^2tWyQ5 zKe>N{qJ1tJeyxA&PJVll_3fGsKwFm~N_(;-mlsIrXU^1A*ZR<1n`h6!_n#oiE6*E; zTCJQI`(~zM#p)(dAYrO*5@9K;9YWyn7@})ltB+_Qh3q6rvOj=ybL@BzN_eviMa=O| zL-Ayc4D*^QTxVUMkVbZ07SO76LO;uAJ7nrcP3D#6 zS1wb8c|wNi6K7d}#dbf3MyJbGYoJVSRQ#&VQ*Fr(;=mWs6U!q;`7XSng!cng<`$`{ zd33);`J3Dtx>){QRzFI}zR7m}D>$RDu<(X10U{$8F5{1ww;VoicLV{NvIqGi>aB`# z>ptYd{wL&8`u_&G{0_a$030}Lv3`QeM{kOsc`9L3t+xo3uGVn2oBi4X^C4aZ&UU3o@G(D9=420@pf;4 zQW|=!U@F6^C2hsrHl=G5olP0~EbofNh?3bk`x~U_#?1pe*2i|L2GMX4Ukg3*3`~qg*wFp14^SX}s1o zO3y#?G$uY|OL_3TA4qRJTfK>qTzc!ix_wMg$d|3LCz4Aep;H}e|9F#JOIL*X?z539 zPb&=bH~!P@T@Z3orbcK9?`x!8`CtLz;>#x2$z{dHq2z=>XD7MwOrGB6tOZj6^`Ju% zc1+HRve;w@P9qe_QdkYmu<5y@$X&JIp>Y$d6x+>dB7zQisC)WhyXVxTE8APh{K z**G6OSduj}F@ym1%!hlPyC-mRTzh9XOf71+L{*2K|H7%D(L9P)+mztR%IIp#JVRKd zgRGR9LoH>$4-Y$F>jYp%SF%+wo-f*Z}b90&`>jo))P@l zOLFE*+rbSqocIJ7L3s0yFUfmYb7$H49Z_m>$a^J*b|gw6MdFnlU;Jjl8eVw;yti$@+;6Q5efv zhAy8vN^H-x5gi`6^NoeBwK(QY&Z(1ju__Mn6Zq1m3Zl?zYvD_y*oy7Qf`p>Z0mWRy zXr~k+<6JVd*&flLZ^$YfR?Sxkx6!dW6@9bE-fzIsC+|WD6?zPsxVJj;&_BQ`cB&2dF4_0qhRg&s|}u$3VNH=@OGx!LpR+f~&SgAL)OSG8hh zWoYjorDUL*6Q7tEVe4cIzdv%VxXe4GOH?Eo(u{YVK8z0p!5*6A&qWOsWDI`qv{$oF z+I;r7N8sTx6HB(@6soBre10>nhenfhQTc<+$-ZUtx30zobZYgRms%_c7gir5X=+*V zw;R_+3zw>R<88CbJ9rz;1u;gvF-#$?ywvU0kCk}jIEAEM@Dd+aD&H+j=w)1B?9b&I zweXi+QNUa0cnqmk026PD!*b`ww)h(Z4t~vqE_CySj@ED%wy*#aw{~u~LK*-dRR;Mu zdQYaB-AXhFUsAjU`s|4;E*O!3y{bTx#AbawN-<)MUswVE2!#iESU-WJO-7cQHN}@5 zsdHbVceBQ~7);?aQjsB~=j)wQ*mbfWirG8Dv6-8Z)r#2_Vck|@%NNyR24(^#{5E56 zz4HI|dF%2eN|{-IX%I5v>_D|}mYzf~X1H2C&v=|CMI!fH5OXD&nZWvC#CaZy2O}Ch zT8HjvYXm|8nvb!--`vm>!)A^dOc+^L>a^b%;?i55wR*KSLvkfVkD85?D~J3{DNz#7 z)?7zR;IVz>;b>6aqld~!)MT<2QSa2&9dpoUIdO%mRrKm>9_>VYzB8sRc!j_}xpSOP z^N~_TLeDj)2F#91MQPLpKW)|FXHnAYgZ{iAN8D!u`DBe{^ND7MlZ|6pw2z`iKmzWH zI}hZYwwthV3YN(k{8!E-+eJu5+>~yC|Is&BZ;#xIqwoBX6 z=pVHiQxA$A3#AgVIk`-eT>aE+^NueC`6?I(d}Eltk(XHPS6Q*cZ%|mvKhm)c6;9c;>HKVviK;Z1$i$Ts zmU97H_Nc6pw{z}C`>P%q5LWg~WPyMCwf`V5O<|dQugCOr5ip8du zs<%)`ERs^rX3F!!1M#udlG3seE}4(AGtf>2CaB9yk_v}1O(UFaK|(qn&^0!E<;3(H z1Wo!}mO0E%u(2^a7jry3_3q+D_7mi!yp_Z=AbkV&c;u5+V&OegS$dH-r2_)QfqTBk zxVV%=$#m)km>Eqw)&RizFbd~4wj)=D9;_)JxxQmPqKIx--(g&q-CXvS9CFy+>_;?C zaPl=zK&*&-QSD_L{altiGO9e3+QUb6A>_a5f-npnTASGfb?oD`Bdf;g`3(+2D_lnz z3*WH|#J$IG-SR&E;o@>~;c7T$3vUEu^ZF|0o%&pb=Zxpw@b(a~QrUtSmADddQAVZq z#=MB+N-S5yTw5Adzl-^s9gYtjB?gAm^4Ea7w2wQx9?Tp$hlIoAh2E{Y2UhP&oShq3ad^>JaKdo| zVU^TuWw86?#`F?DnQ%amx2b&kG?aeSw)tN9S#ilguuuH_P$Pk9H#R%!Eii{ei8ODq zs6fu%MvS1itwD(6_pKS0D{ljvW59ejk8i;7zJ2c}vQRPDIHZY3OetXxMP~K(j`38+ z?B1Go`xrbB0@6{EpjO{CSEap59uF+)NR*xk{??K*qu zaFQ-B9sNku!(v>HPqq|!mcWSY&79LEw^S6Q847i$^B5$q9QUqn(R$fK>&5q^wq(j>xn9;=JAt2U!udSGJhs|0K43A~AkG34e4S{|BvW!;zMZtEVLN z{gQ`D%v%lBBshGYg|hiHGCM4g@qfs##d~4{Rxnx7c_Y@zyoo}Yh{J;!cizymh1^Ym z30=|FJ8VbMC7EH85(ZgnTWW1`ZXbP|MKCLT~GwZh9Lun_hSA;FVBkx@W`%ohNpQZWM=?QBqaAIX;R` zX7;RK(cG-TN!fD5Tn5qbNHse>{bAk@feJ_H<@M9;Gk8&pb>(fL8wPD@ZOT1mVpgc9 zO6Edrc0&%a*DqP$bDO9VKV@k$cT9mbjczxNYp_g%1ir}0pQx1BqKs#|^BpC(RNKvz z{BB*;yozc%;-1&+n+ay0^Q*{UBE40bNME&C&Vsvi_1ehgtnPHds?gJGgyr7w#ClE> zv>V~`*YbX~oxAvil@`s##59giLgU94pMMoV%Y;k@W|fEazkK|ZHk@6NR8B9nn|ZC! zv28PXBB0FiSb1h)ZR)GBF=KY6SBR1N_NIrm+t|XhlgZ&J)+e*nkb*zQ0xY;>@L*yd>-Sar~E^3zelThXnkufMSAbo>jg z26Kb*S^HBnnOyuxq%IBEyUQXc$(N5#L5CAt0Mm0OB{5mN`O$K+ZACA|^oNM=T`XRS2T3%CZ@GHeXHRXXPC~eab zs&q-6cI2!hg-i#8GNNIwJ$Hyci1RyJ7J3pE#NzM=89L}UOGI#NZ`7UBXjzh>G5pcU zGe4~pyMkNpbEp=F1=h0Ysok>|P$ut3i%>+#n$fOZ1;z8DuYev#jI$xO~`;QV=J=gzm@J9h~s#kFro-!`lpa7lGZZNidX*fLGd zEe}bdnRabmILM6yaw%OEsXg5}1W>Y(+)=%%Z2})2sBMa&&*qrRePh79&-w&X6h7lD zyY>3jE7gT_M+El?0{jx>#l%j;8f@VYUy++boyd|2LiF~Z=?YyJF^C}GD+2phy23^I zg1^R^d;Yo(z%4F@!{hqnvY_-G^wj zT|p$!um5rftmq_q*dXxd_hA}v@M{2EePqR91V$qSw(6%>pkN7rX;_g&%Az{_7<4sZ z7}0$k07gf2;RJ`Du88V(b#p-gWeUhg5hYa!JQeWurhULj8vsUZKi%j~j0kUG_CcdO zF#95V{%OrWeFMk;`^=l%B6_S2Gb6<5JPVW2S1Up%WNvj3$X^%7(;1}Ou_%PUCFYIOz`+!Iy_fgv) zrJ|Q*gtvZxuev`a8|zGDS)7XArAIL$Cex|02;2+9uNTJ_kVQ@40MaUBv3AP1R6jn z-F0j+K7*AHb$?-Cgl@64CrhUbv%VugYabcFU_$leVw$w>*Tag(3~RFL%$Nx800R9( zwYk2m(S>We^W)sZaaiJpQJs{yIy3M)o+UIANFrN)y7$M+mA` z1Ob$Qi9au^U4DlM4YNevu1R33RPTJqeY4{N=CGCj>*d&2(?t|rG>pXxN1W^E5MPiU zNMeIKeEVGf%fGpiLVU;%`iJTc<{V)0FI*$~u57bP5Fbau+M5!rCvB!_TrO@_~8)gzT7fQZ)V7TvH#M7 zweYtE>-Tfv|6jkqz3B5P)_Cd9G7~2IpZ&5jP`WbwV2hLd;zRLYbC5`u)@|0{Q!kC) zfyta!>P{V^gbnAzS4$8J9Du2@2iajgn^(M5wl>Ni=KuwhMx<#y*y@TY1bp49!l{VD_j)OIl9 z3xe1hcJ9FySm_01JHBq6=GT^-#V1B@1p$7^{zK=b&OnEsX?!8Tw?Ku*E)vu0etdfd z@ucOhA~1PCrzkOaU&8%T+aEVTZQa|yzv-O!CAa_Rp&Kyziy*KEu$6xFl}1Uhsch-L zz7K}Ehxp^mGp3ATvwsXx=2H4+cdyoM0l{9@x@q+EWd$%Fm*GeL z)8Yb5G`~K6@J(T7UmG5*FXPvyoPF8wPu2f)_&?L^|0bLNHo}$9)`}>H;i4aHjA-DM cW$WY{JphtxW$Ezsvn8jloE6X#-Fw6T2S@$ZfdBvi literal 0 HcmV?d00001 diff --git a/docs/screenshot-2.jpg b/docs/screenshot-2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2d7624efbdcd294e289aa2c94cfe82f9295efa05 GIT binary patch literal 88399 zcmeFZcT`i~wlErsfYJm(dQlLN-g^<1E+V}XrS~Geh9bRJ1q7sbBE1*sh=6qI5PDCj z0YZ53ocB}jcYo&__j_;Lci$f;$sS|xwf9=Ht~KYHbMD;yxLF3=Rg_hb1)!m!0bZlN zfEy&>o~?t0mB|}h9Z`B7L2gcAKEXd4bw%lg`8oNxd44tuyIa|tI=E|Cx!9VC(z{sM zy3j-XljxNlysT_(-_Xliy)`p;@SuP7*2?XTiy8gXw+`;kR`wP*bAV?640QCLKhy^k z^}B_0>lP;FtvlG*SU7lh@bGZ&;Ns#F5Z}coAR@rUy?g&I(LEAUQc^rZGV=Q*SlBqX@8F^ysJ#n7N5jBC$He%FHR@?!)O`RZ(JkT!JknVARNr7fbRyvm zh|k7hcv|^|RBaT>$Y=aE@b;biWaJc-kC>QQ9<%Zb2nq>{h(3ERBP%Dbps22)sim!> zt7l?rW^Q3=W$ogMj@=@s-YI3zSIJR%`6DLLi+ht#y3+`RmP!lL4ms_L5By84F3 zrp~VJp5DIxfv;oZ6O&WZKW1iEAggQZ8=G6(JFuhUlhdbD2mR7}n<8<#KZvYO-)A;PlFSi-^)S#rsZ%6Ok zXXIab1p7(aKPdaJ5f=FWh_YXV{ibUcfQNyG5*`K-Knifd@TCo7y2Y?~e%pwu-a;@; z-|zK{17 zh$czjv+O`=9dT4f=(ai-@87GL9+YJ`dBPRs$ELgjBr@E+JgY~mvvIyB>Qf*cCruB) z;+dOj^X4v%QSA|Rezg{5rl4D|rhWctGOb#_xu;x6dG6!|IC%BL@?(?7%#!wIVgi=@ zCmTC8lU`ZGOLc|$O9r|N1I`Yv4h%Bvydropg_uM;I7L!5_U)S+4p(p+2Z&U-2`noX zAyd&smrRfk5*TCR@jsA9x(;iPsU@wyH0zdLJgeZr-i6#^a~3tc1le?2kj#e&}V zd)!}72pB~G{te(CO#CmhNb&u20=H&EMrsUzpgU2s)aHxmydNbjCAd_nd7!c_a6`F- zd|v^B8sm#FmOrj97qGb|bA#N$G!WB@G46h6Nt0|J)RO^NdS}>sw|^o2fLU8Tuxt-U zGWnoRimXK-DC0{(ae0bfnTDZ>VTd{=xf9(Q@WM8Qg9%rQUWQ& zr$Psh#XA}Xl!M$3K5XVw9#pPe?LMGQU76u;&RD+K_ZyFHS+ir6XgmWu)xcpZmf*5e zAsg_1q_^;iOv7DAcFAw~t>@uEg-ufV0H6Ke`lr`q6b+%V<`V41ohyRAgbxyR^@e`D z0eI>3A@IDW%XQ$pU+T5eUXW3K^m6?m3QEKiwPzZ~<0fr3k9)ARDd`k_aV*4r3sAg| z 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
7>SfbuO$d0# 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 Fe*of_XF31? literal 0 HcmV?d00001 diff --git a/frontend/src/components/pages/read-only-project.tsx b/frontend/src/components/pages/read-only-project.tsx index fd3d8d4b..baa795b9 100644 --- a/frontend/src/components/pages/read-only-project.tsx +++ b/frontend/src/components/pages/read-only-project.tsx @@ -26,15 +26,17 @@ export const ReadOnlyProject = ({ project }: { project: ProjectDTO }) => { } }, [project]); + const image = project?.image || project?.team.teamImg; + return (
- {project?.image !== "" ? ( + {image !== "" ? ( ) : null} diff --git a/frontend/src/components/pages/view-project.tsx b/frontend/src/components/pages/view-project.tsx index 93de4819..8ec7d406 100644 --- a/frontend/src/components/pages/view-project.tsx +++ b/frontend/src/components/pages/view-project.tsx @@ -10,6 +10,7 @@ import { ReadOnlyProject } from "./read-only-project"; import { PageHeader } from "../base/page-header"; import { RoundedImage } from "../base/image"; import { ProjectDTO } from "../../api/types/dto"; +import { useNotificationContext } from "../../contexts/notification-context"; /** * A gate component that checks if the current user is part of the team. @@ -52,6 +53,8 @@ const EditProject = ({ project }: { project: ProjectDTO }) => { const loginState = useLoginContext(); const { user } = loginState; + const { showNotification } = useNotificationContext(); + const [id] = React.useState(project.id); const [title, setTitle] = React.useState(project.title); const [description, setDescription] = React.useState(project.description); @@ -71,6 +74,7 @@ const EditProject = ({ project }: { project: ProjectDTO }) => { image, allowRating, } as unknown as ProjectDTO); + showNotification("Saved"); return true; } return false; diff --git a/frontend/src/components/pages/view-team.tsx b/frontend/src/components/pages/view-team.tsx index 78aeb7b1..6e31b648 100644 --- a/frontend/src/components/pages/view-team.tsx +++ b/frontend/src/components/pages/view-team.tsx @@ -21,6 +21,7 @@ import { Message } from "../base/message"; import { ReadOnlyTeam } from "./read-only-team"; import { UserRole } from "../../api/types/enums"; import { PageHeader } from "../base/page-header"; +import { useNotificationContext } from "../../contexts/notification-context"; /** * A gate component that checks if the current user is part of the team. @@ -30,6 +31,8 @@ export const ViewTeam = () => { const loginState = useLoginContext(); const { user } = loginState; + const { showNotification } = useNotificationContext(); + const [team, setTeam] = React.useState(null); const params = new URLSearchParams(document.location.search); const teamId = Number(params.get("id")); @@ -86,6 +89,7 @@ const EditTeam = ({ team }: { team: TeamResponseDTO }) => { image, users.map((u) => u.id), ); + showNotification("Saved"); return true; } return false; From 3a12ffd70a94df9450299c07ff24a307e2b29382 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:58:09 +0200 Subject: [PATCH 03/36] readme --- README.md | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e55fc25b..2e57629b 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,36 @@

-# tilt +

tilt

-[![Docker Image Size (latest)](https://img.shields.io/docker/image-size/hackaburg/tilt/latest)](https://hub.docker.com/r/hackaburg/tilt) -[![codecov](https://codecov.io/gh/hackaburg/tilt/branch/main/graph/badge.svg)](https://codecov.io/gh/hackaburg/tilt) -[![David](https://img.shields.io/david/hackaburg/tilt)](https://github.com/hackaburg/tilt) -[![GitHub license](https://img.shields.io/github/license/hackaburg/tilt.svg)](https://github.com/hackaburg/tilt/LICENSE) +

+ Hackathon Registration System +

-Hackathon Registration System +

Documentation - Docker Development Quickstart

-- [Documentation](docs/docs.md) -- [Docker Development Quickstart](docs/docker-development.md) +

+ + Docker Image Size (latest) + + + + codecov + + + + David + + + + GitHub license + +

+ +

+ +   + +

Like many other hackathons, we previously used [Quill](https://github.com/techx/quill) for our application process, which worked really well for us in the past. Especially Quill's process was a blessing: an application consists of two steps, the profile creation and, once an attendee was admitted to the event, the spot confirmation. We attended different events that used different processes and found this to be easy for both the attendees and organizers. From a62f678429f2ac0835872e272d8792f18ab0c9cc Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:59:39 +0200 Subject: [PATCH 04/36] readme --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2e57629b..fdc0149b 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,16 @@

-

- -   - -

+
Like many other hackathons, we previously used [Quill](https://github.com/techx/quill) for our application process, which worked really well for us in the past. Especially Quill's process was a blessing: an application consists of two steps, the profile creation and, once an attendee was admitted to the event, the spot confirmation. We attended different events that used different processes and found this to be easy for both the attendees and organizers. Faced with maintaining our [fork](https://github.com/hackaburg/quill) with our set of changes to the application process, as well as maintaining an Angular.JS frontend and an untyped Express backend, we wanted to build a registration system ourselves, that matched our needs on a tech stack we're more familiar with. + +
+ +

+ +   + +

From abd1fd50fa6415fcb4c4a8fbffcf354af81c2c1e Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:01:18 +0200 Subject: [PATCH 05/36] change screenshot-1.jpg --- docs/screenshot-1.jpg | Bin 72949 -> 72996 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/screenshot-1.jpg b/docs/screenshot-1.jpg index 1689db5ca92dc081bb95e868045f6cccb05d23b3..4dbe0e8cbcf502957df5de9f4c57664672dd4fcc 100644 GIT binary patch delta 22131 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

tsOu#x7}Bk66vp{8Y8YCAhg@T5m^%J&R8G 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 UMta_h8|iZ4wi&?U;{5-c06b7pGXMYp From b1b43795d7bac032a6a4a8df09615b4338d87f58 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:02:35 +0200 Subject: [PATCH 06/36] fix docs link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fdc0149b..2a6e1cd1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Hackathon Registration System

-

Documentation - Docker Development Quickstart

+

Documentation - Docker Development Quickstart

From 299124c0eaf2a396b1587b739f966432d7b77db5 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Sat, 11 Apr 2026 17:29:45 +0200 Subject: [PATCH 07/36] backend tests passing --- backend/src/entities/team.ts | 6 --- backend/src/services/project-service.ts | 13 ++--- backend/src/services/rating-service.ts | 9 +++- backend/src/services/team-service.ts | 15 +----- .../project-service-get-all-projects.spec.ts | 49 ++++++------------- backend/test/services/project-service.spec.ts | 21 ++++---- backend/test/services/rating-service.spec.ts | 24 ++++++--- backend/test/services/team-service.spec.ts | 7 ++- 8 files changed, 56 insertions(+), 88 deletions(-) diff --git a/backend/src/entities/team.ts b/backend/src/entities/team.ts index f685082b..3d18edd8 100644 --- a/backend/src/entities/team.ts +++ b/backend/src/entities/team.ts @@ -26,9 +26,6 @@ export class Team { return []; } - // TODO does this work? - console.log("### users", this.users); - return this.users.map(({ id }) => id); } @@ -40,9 +37,6 @@ export class Team { return []; } - // TODO does this work? - console.log("### requests", this.requests); - return this.requests.map(({ id }) => id); } } diff --git a/backend/src/services/project-service.ts b/backend/src/services/project-service.ts index 33d9ed39..72ff3dc2 100644 --- a/backend/src/services/project-service.ts +++ b/backend/src/services/project-service.ts @@ -5,7 +5,6 @@ import { IService } from "."; import { DatabaseServiceToken, IDatabaseService } from "./database-service"; import { Project } from "../entities/project"; import { Settings } from "../entities/settings"; -import { Team } from "../entities/team"; import { User } from "../entities/user"; import { UserRole } from "../entities/user-role"; @@ -46,7 +45,6 @@ export const ProjectServiceToken = new Token(); @Service(ProjectServiceToken) export class ProjectService implements IProjectService { private _projects!: Repository; - private _teams!: Repository; private _settings!: Repository; public constructor( @@ -58,7 +56,6 @@ export class ProjectService implements IProjectService { */ public async bootstrap(): Promise { this._projects = this._database.getRepository(Project); - this._teams = this._database.getRepository(Team); this._settings = this._database.getRepository(Settings); } @@ -66,11 +63,6 @@ export class ProjectService implements IProjectService { * Gets all projects that the user may see. */ public async getAllProjects(user: User): Promise { - const teams = await this._teams.find(); - const teamIds = teams - .filter((team) => team.userIds().includes(user.id)) - .map((team) => team.id); - const [settings] = await this._settings.find(); const allowRatingProjects = settings.project.allowRatingProjects; const isAdmin = user.role === UserRole.Root; @@ -80,7 +72,7 @@ export class ProjectService implements IProjectService { return ( isAdmin || (project.allowRating && allowRatingProjects) || - teamIds.includes(project.team.id) + project.team.id == user.team?.id ); }); } @@ -98,6 +90,7 @@ export class ProjectService implements IProjectService { await this.checkPermission(existing, user); + // Only allow updating these fields: existing.title = project.title; existing.description = project.description; existing.image = project.image; @@ -157,7 +150,7 @@ export class ProjectService implements IProjectService { } const team = project.team; - if (!team || !team.userIds().includes(user.id)) { + if (!team || user.team?.id !== team.id) { // Tried to access a project belonging to a different team, forbidden throw new NotFoundError(); } diff --git a/backend/src/services/rating-service.ts b/backend/src/services/rating-service.ts index 638a849d..75c488fc 100644 --- a/backend/src/services/rating-service.ts +++ b/backend/src/services/rating-service.ts @@ -264,7 +264,14 @@ export class RatingService implements IRatingService { throw new ForbiddenError("Rating this project is not allowed"); } - const team = await this._teams.findOneBy({ id: project.team.id }); + const team = await this._teams.findOne( + { + where: { + id: project.team.id + }, + relations: [ "users", "requests" ] + }, + ); if (!team) { throw new NotFoundError("Team not found"); } diff --git a/backend/src/services/team-service.ts b/backend/src/services/team-service.ts index 4bc6180f..b3547245 100644 --- a/backend/src/services/team-service.ts +++ b/backend/src/services/team-service.ts @@ -99,10 +99,6 @@ export class TeamService implements ITeamService { throw new Error("Team description cannot be empty"); } - if (team.users.length === 0) { - throw new Error("Please add at least one user to the team"); - } - const originalTeam = await this._teams.findOne({ where: { id: team.id }, relations: ["users", "requests"], @@ -156,15 +152,6 @@ export class TeamService implements ITeamService { throw new Error("Team description cannot be empty"); } - if (team.users.length === 0) { - throw new Error("Please add at least one user to the team"); - } - - const maxUsers = 8; - if (team.users.length > maxUsers) { - throw new Error(`A team can have a maximum of ${maxUsers} users`); - } - // TODO leaving team should make someone else owner // TODO order of team.users not guaranteed anymore I guess, // - add owner and edit all usages of users[0]. @@ -180,7 +167,7 @@ export class TeamService implements ITeamService { team.teamImg = placeholder_img[Math.floor(Math.random() * placeholder_img.length)]; } - team.requests = []; + const createdTeam = await this._teams.save(team); // Every team gets one project by default diff --git a/backend/test/services/project-service-get-all-projects.spec.ts b/backend/test/services/project-service-get-all-projects.spec.ts index e67391e2..8ff24108 100644 --- a/backend/test/services/project-service-get-all-projects.spec.ts +++ b/backend/test/services/project-service-get-all-projects.spec.ts @@ -88,17 +88,13 @@ describe(ProjectService.name, () => { // Create teams with projects const team1 = new Team(); team1.title = "Team 1"; - team1.users = [regularUser.id]; - team1.image = ""; + team1.teamImg = ""; team1.description = ""; - team1.requests = []; const team2 = new Team(); team2.title = "Team 2"; - team2.users = [regularUser.id]; - team2.image = ""; + team2.teamImg = ""; team2.description = ""; - team2.requests = []; const teamRepo = database.getRepository(Team); const savedTeams = await teamRepo.save([team1, team2]); @@ -139,10 +135,8 @@ describe(ProjectService.name, () => { // Create a team and project that the regular user is not part of const team = new Team(); team.title = "Other Team"; - team.users = []; // Regular user is not part of this team team.teamImg = ""; team.description = ""; - team.requests = []; const teamRepo = database.getRepository(Team); const savedTeam = await teamRepo.save(team); @@ -161,55 +155,42 @@ describe(ProjectService.name, () => { expect(allProjects).toHaveLength(0); }); - it("regular user, part of two teams, gets 3 projects", async () => { - expect.assertions(4); + it("regular user in a team with 2 projects gets 2 projects", async () => { + expect.assertions(3); // Create two teams with the regular user const team1 = new Team(); team1.title = "Team 1"; - team1.users = [regularUser.id]; - team1.image = ""; + team1.teamImg = ""; team1.description = ""; - team1.requests = []; - - const team2 = new Team(); - team2.title = "Team 2"; - team2.users = [regularUser.id]; - team2.image = ""; - team2.description = ""; - team2.requests = []; const teamRepo = database.getRepository(Team); - const savedTeams = await teamRepo.save([team1, team2]); + const savedTeam = await teamRepo.save(team1); + + regularUser.team = savedTeam; + const userRepo = database.getRepository(User); + await userRepo.save(regularUser); - // Create 2 projects for team1 and 1 project for team2 const project1 = new Project(); - project1.team = savedTeams[0]; + project1.team = savedTeam; project1.title = "Project 1 - Team 1"; project1.description = "Description 1"; project1.allowRating = true; const project2 = new Project(); - project2.team = savedTeams[0]; + project2.team = savedTeam; project2.title = "Project 2 - Team 1"; project2.description = "Description 2"; project2.allowRating = false; - const project3 = new Project(); - project3.team = savedTeams[1]; - project3.title = "Project 1 - Team 2"; - project3.description = "Description 3"; - project3.allowRating = true; - const projectRepo = database.getRepository(Project); - await projectRepo.save([project1, project2, project3]); + await projectRepo.save([project1, project2]); const allProjects = await service.getAllProjects(regularUser); - expect(allProjects).toHaveLength(3); + expect(allProjects).toHaveLength(2); expect(allProjects[0]).toEqual(project1); expect(allProjects[1]).toEqual(project2); - expect(allProjects[2]).toEqual(project3); }); it("regular user without team gets projects that can be rated", async () => { @@ -220,10 +201,8 @@ describe(ProjectService.name, () => { // Create a team with no users const team = new Team(); team.title = "Public Team"; - team.users = []; team.teamImg = ""; team.description = ""; - team.requests = []; const teamRepo = database.getRepository(Team); const savedTeam = await teamRepo.save(team); diff --git a/backend/test/services/project-service.spec.ts b/backend/test/services/project-service.spec.ts index 4ba5aca6..f8a72c44 100644 --- a/backend/test/services/project-service.spec.ts +++ b/backend/test/services/project-service.spec.ts @@ -36,7 +36,13 @@ describe("ProjectService", () => { teamRepo = database.getRepository(Team); projectRepo = database.getRepository(Project); - // Create admin user + mockTeam = new Team(); + mockTeam.title = "Team 1"; + mockTeam.teamImg = ""; + mockTeam.description = ""; + mockTeam.requests = []; + mockTeam = await teamRepo.save(mockTeam); + adminUser = new User(); adminUser.firstName = "Admin"; adminUser.lastName = "User"; @@ -47,7 +53,6 @@ describe("ProjectService", () => { adminUser.tokenSecret = ""; adminUser.forgotPasswordToken = ""; - // Create regular users regularUser = new User(); regularUser.firstName = "Regular"; regularUser.lastName = "User"; @@ -57,6 +62,8 @@ describe("ProjectService", () => { regularUser.verifyToken = ""; regularUser.tokenSecret = ""; regularUser.forgotPasswordToken = ""; + regularUser.team = mockTeam; + regularUser.teamRequest = null; userWithoutTeam = new User(); userWithoutTeam.firstName = "Regular 2"; @@ -67,6 +74,8 @@ describe("ProjectService", () => { userWithoutTeam.verifyToken = ""; userWithoutTeam.tokenSecret = ""; userWithoutTeam.forgotPasswordToken = ""; + userWithoutTeam.team = null; + userWithoutTeam.teamRequest = null; [adminUser, regularUser, userWithoutTeam] = await userRepo.save([ adminUser, @@ -74,14 +83,6 @@ describe("ProjectService", () => { userWithoutTeam, ]); - mockTeam = new Team(); - mockTeam.title = "Team 1"; - mockTeam.users = [regularUser.id.toString()]; - mockTeam.teamImg = ""; - mockTeam.description = ""; - mockTeam.requests = []; - mockTeam = await teamRepo.save(mockTeam); - mockProject = new Project(); mockProject.team = mockTeam; mockProject.title = "Original Title"; diff --git a/backend/test/services/rating-service.spec.ts b/backend/test/services/rating-service.spec.ts index 1c10411c..9b4ba93c 100644 --- a/backend/test/services/rating-service.spec.ts +++ b/backend/test/services/rating-service.spec.ts @@ -29,6 +29,7 @@ describe("RatingService", () => { let ratingUser: User; let teamMember: User; let mockTeam: Team; + let mockTeam2: Team; let mockProject: Project; let mockCriterion: Criterion; @@ -48,6 +49,18 @@ describe("RatingService", () => { criterionRepo = database.getRepository(Criterion); ratingRepo = database.getRepository(Rating); + mockTeam = new Team(); + mockTeam.title = "Test Team"; + mockTeam.teamImg = ""; + mockTeam.description = ""; + mockTeam = await teamRepo.save(mockTeam); + + mockTeam2 = new Team(); + mockTeam2.title = "Test Team 2"; + mockTeam2.teamImg = ""; + mockTeam2.description = ""; + mockTeam2 = await teamRepo.save(mockTeam2); + // A user who will submit ratings (not in the project's team) ratingUser = new User(); ratingUser.firstName = "Rater"; @@ -59,6 +72,7 @@ describe("RatingService", () => { ratingUser.tokenSecret = ""; ratingUser.forgotPasswordToken = ""; ratingUser.admitted = true; + ratingUser.team = mockTeam2; // A user who is a member of the project team teamMember = new User(); @@ -71,17 +85,11 @@ describe("RatingService", () => { teamMember.tokenSecret = ""; teamMember.forgotPasswordToken = ""; teamMember.admitted = true; + teamMember.team = mockTeam; + teamMember.teamRequest = null; [ratingUser, teamMember] = await userRepo.save([ratingUser, teamMember]); - mockTeam = new Team(); - mockTeam.title = "Test Team"; - mockTeam.users = [teamMember.id.toString()]; - mockTeam.teamImg = ""; - mockTeam.description = ""; - mockTeam.requests = []; - mockTeam = await teamRepo.save(mockTeam); - mockProject = new Project(); mockProject.team = mockTeam; mockProject.title = "Test Project"; diff --git a/backend/test/services/team-service.spec.ts b/backend/test/services/team-service.spec.ts index 02d76146..39dffd8e 100644 --- a/backend/test/services/team-service.spec.ts +++ b/backend/test/services/team-service.spec.ts @@ -35,16 +35,15 @@ describe("TeamService", () => { user.verifyToken = ""; user.tokenSecret = ""; user.forgotPasswordToken = ""; + user.team = null; // The team will be assigned in createTeam + user.teamRequest = null; await userRepo.save(user); const team = new Team(); team.title = "Team 1"; - team.users = [user]; team.teamImg = ""; team.description = "Team 1 description"; - team.requests = []; - - await teamService.createTeam(team); + await teamService.createTeam(team, user); const projects = await projectRepo.find(); expect(projects).toHaveLength(1); From babc71a6558f8c4c491914322c32ef066b6abda2 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Sat, 11 Apr 2026 17:43:08 +0200 Subject: [PATCH 08/36] lint and typecheck --- backend/src/services/project-service.ts | 2 +- backend/src/services/rating-service.ts | 12 +- backend/test/services/team-service.spec.ts | 2 +- frontend/src/components/pages/edit-team.tsx | 352 +++++++++++++++++ .../src/components/pages/view-project.tsx | 6 +- frontend/src/components/pages/view-team.tsx | 370 +----------------- 6 files changed, 363 insertions(+), 381 deletions(-) create mode 100644 frontend/src/components/pages/edit-team.tsx diff --git a/backend/src/services/project-service.ts b/backend/src/services/project-service.ts index 72ff3dc2..8450fdc3 100644 --- a/backend/src/services/project-service.ts +++ b/backend/src/services/project-service.ts @@ -72,7 +72,7 @@ export class ProjectService implements IProjectService { return ( isAdmin || (project.allowRating && allowRatingProjects) || - project.team.id == user.team?.id + project.team.id === user.team?.id ); }); } diff --git a/backend/src/services/rating-service.ts b/backend/src/services/rating-service.ts index 75c488fc..8ab1c928 100644 --- a/backend/src/services/rating-service.ts +++ b/backend/src/services/rating-service.ts @@ -264,14 +264,12 @@ export class RatingService implements IRatingService { throw new ForbiddenError("Rating this project is not allowed"); } - const team = await this._teams.findOne( - { - where: { - id: project.team.id - }, - relations: [ "users", "requests" ] + const team = await this._teams.findOne({ + where: { + id: project.team.id, }, - ); + relations: ["users", "requests"], + }); if (!team) { throw new NotFoundError("Team not found"); } diff --git a/backend/test/services/team-service.spec.ts b/backend/test/services/team-service.spec.ts index 39dffd8e..34253b96 100644 --- a/backend/test/services/team-service.spec.ts +++ b/backend/test/services/team-service.spec.ts @@ -35,7 +35,7 @@ describe("TeamService", () => { user.verifyToken = ""; user.tokenSecret = ""; user.forgotPasswordToken = ""; - user.team = null; // The team will be assigned in createTeam + user.team = null; // The team will be assigned in createTeam user.teamRequest = null; await userRepo.save(user); diff --git a/frontend/src/components/pages/edit-team.tsx b/frontend/src/components/pages/edit-team.tsx new file mode 100644 index 00000000..d4c968a2 --- /dev/null +++ b/frontend/src/components/pages/edit-team.tsx @@ -0,0 +1,352 @@ +import * as React from "react"; +import { Subheading } from "../base/headings"; +import { Page } from "./page"; +import { Button } from "../base/button"; +import { RoundedImage } from "../base/image"; +import { TextInput, TextInputType } from "../base/text-input"; +import { api, useApi } from "../../hooks/use-api"; +import { + Autocomplete, + Box, + Card, + CardContent, + InputLabel, + TextField, +} from "@mui/material"; +import { MdDeleteOutline } from "react-icons/md"; +import { UserListDto, TeamResponseDTO } from "../../api/types/dto"; +import { useLoginContext } from "../../contexts/login-context"; +import { useHistory } from "react-router-dom"; +import { Message } from "../base/message"; +import { UserRole } from "../../api/types/enums"; +import { PageHeader } from "../base/page-header"; +import { useNotificationContext } from "../../contexts/notification-context"; + +/** + * A settings dashboard to configure all parts of tilt. + */ +export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { + const loginState = useLoginContext(); + const { user } = loginState; + + const { showNotification } = useNotificationContext(); + + const [currentUserId, setCurrentUserId] = React.useState(0); + const [isTeamOwner, setIsTeamOwner] = React.useState(false); + const [, setIsTeamMember] = React.useState(false); + const [id, setId] = React.useState(0); + const [title, setTitle] = React.useState(""); + const [description, setDescription] = React.useState(""); + const [image, setImage] = React.useState(""); + const [users, setUsers] = React.useState([] as UserListDto[]); + const [request, setRequest] = React.useState([] as UserListDto[]); + + const { + value: didUpdateTeam, + isFetching: updateTeamInProgress, + error: updateTeamError, + forcePerformRequest: sendSaveTeamRequest, + } = useApi( + async (apiClient, wasTriggeredManually) => { + if (wasTriggeredManually) { + await apiClient.updateTeam( + id, + title, + description, + image, + users.map((u) => u.id), + ); + showNotification("Saved"); + return true; + } + return false; + }, + [currentUserId, isTeamOwner, id, title, description, image, users, request], + ); + + async function acceptUserToTeam(userId: number) { + await api.acceptUserToTeam( + team.id, + request.find((u) => u.id === userId)!.id, + ); + history.go(0); + } + + const { + value: didDelete, + isFetching: deleteInProgress, + error: deleteError, + forcePerformRequest: deleteGroup, + } = useApi(async (apiClient, wasTriggeredManually) => { + if (wasTriggeredManually) { + if (confirm("Are you sure you want to delete this team?")) { + await apiClient.deleteTeam(team.id); + return true; + } + } + return false; + }, []); + + const { value: allUsers } = useApi( + async (apiClient) => apiClient.getAllUsers(), + [], + ); + + const userList = allUsers ?? []; + + const handleSubmit = React.useCallback((event: React.SyntheticEvent) => { + event.preventDefault(); + }, []); + + const updateTeamDone = + Boolean(didUpdateTeam) && !updateTeamInProgress && !updateTeamError; + + const didDeleteDone = Boolean(didDelete) && !deleteInProgress && !deleteError; + + if (updateTeamDone) { + const history = useHistory(); + history.go(0); + } + + if (didDeleteDone) { + const history = useHistory(); + history.push("/teams"); + } + + function addMember() { + setUsers([...users, { id: 0, name: "" }]); + } + + function onChange(index: number, value: UserListDto) { + setUsers((uList) => { + const newUsers = [...uList]; + newUsers[index] = value; + return newUsers; + }); + } + + function alreadyInList(singleUser: UserListDto) { + return users.some((u) => u.id === singleUser.id); + } + + React.useEffect(() => { + if (team) { + setCurrentUserId(team.id); + setId(team.id); + setTitle(team.title); + setDescription(team.description); + setImage(team.teamImg); + setUsers(team.users!); + setRequest(team.requests!); + setIsTeamOwner(team.users.length > 0 && user?.id === team.users![0].id); + setIsTeamMember(team.users.some((u) => u.id === user?.id)); + } + }, [team]); + + const isAdmin = user?.role === UserRole.Root; + + return ( + + + {updateTeamError && ( +

+ )} +
+ setTitle(value)} + type={TextInputType.Text} + /> + setDescription(value)} + type={TextInputType.Area} + /> +
+ setImage(value)} + type={TextInputType.Text} + /> + {image !== "" ? ( + + ) : null} +
+ +
+ + Team Members (can only be changed by the team owner) + +
+ {users.map((singleUser, index) => ( +
+ + typeof option === "string" ? option : option.name + } + renderInput={(paramsAuto) => ( + + )} + renderOption={(props, option, _state, ownerState) => ( + + {ownerState.getOptionLabel(option)}{" "} + {alreadyInList(option) ? " - already a member" : ""} + + )} + onChange={(_e, v) => onChange(index, v as UserListDto)} + /> + {index === 0 || !isTeamOwner ? null : ( + { + const newUsers = [...users]; + newUsers.splice(index, 1); + setUsers(newUsers); + }} + /> + )} +
+ ))} +
+ + {isTeamOwner ? ( +
+
+ +
+
+ ) : null} +
+ {request.length > 0 ? ( +
+ + These users requested to join this team (can only be changed by + the team owner) + + {request.map((r, index) => ( +
+ +
+ {!isTeamOwner ? null : ( + + )} +
+
+ ))} +
+ ) : null} + + {isTeamOwner ? ( +
+ + + +

+ Please be aware if you delete a team it is gone. We will not + recover it. +

+
+ +
+
+
+
+ ) : null} + + + ); +}; diff --git a/frontend/src/components/pages/view-project.tsx b/frontend/src/components/pages/view-project.tsx index 8ec7d406..b3aa342e 100644 --- a/frontend/src/components/pages/view-project.tsx +++ b/frontend/src/components/pages/view-project.tsx @@ -27,11 +27,7 @@ export const ViewProject = () => { api.getProjectByID(projectId).then((project_) => setProject(project_)); }, []); - const isTeamMember = React.useMemo(() => { - return ( - project?.team?.users?.some((id) => id === user?.id) ?? false - ); - }, [project, user?.id]); + const isTeamMember = project?.team?.id === user?.id; const isAdmin = user?.role === UserRole.Root; diff --git a/frontend/src/components/pages/view-team.tsx b/frontend/src/components/pages/view-team.tsx index 6e31b648..12665641 100644 --- a/frontend/src/components/pages/view-team.tsx +++ b/frontend/src/components/pages/view-team.tsx @@ -1,27 +1,10 @@ import * as React from "react"; -import { Subheading } from "../base/headings"; -import { Page } from "./page"; -import { Button } from "../base/button"; -import { RoundedImage } from "../base/image"; -import { TextInput, TextInputType } from "../base/text-input"; -import { api, useApi } from "../../hooks/use-api"; -import { - Autocomplete, - Box, - Card, - CardContent, - InputLabel, - TextField, -} from "@mui/material"; -import { MdDeleteOutline } from "react-icons/md"; -import { UserListDto, TeamResponseDTO } from "../../api/types/dto"; +import { api } from "../../hooks/use-api"; +import { TeamResponseDTO } from "../../api/types/dto"; import { useLoginContext } from "../../contexts/login-context"; -import { useHistory } from "react-router-dom"; -import { Message } from "../base/message"; import { ReadOnlyTeam } from "./read-only-team"; import { UserRole } from "../../api/types/enums"; -import { PageHeader } from "../base/page-header"; -import { useNotificationContext } from "../../contexts/notification-context"; +import { EditTeam } from "./edit-team"; /** * A gate component that checks if the current user is part of the team. @@ -31,8 +14,6 @@ export const ViewTeam = () => { const loginState = useLoginContext(); const { user } = loginState; - const { showNotification } = useNotificationContext(); - const [team, setTeam] = React.useState(null); const params = new URLSearchParams(document.location.search); const teamId = Number(params.get("id")); @@ -56,348 +37,3 @@ export const ViewTeam = () => { ); }; - -/** - * A settings dashboard to configure all parts of tilt. - */ -const EditTeam = ({ team }: { team: TeamResponseDTO }) => { - const loginState = useLoginContext(); - const { user } = loginState; - - const [currentUserId, setCurrentUserId] = React.useState(0); - const [isTeamOwner, setIsTeamOwner] = React.useState(false); - const [, setIsTeamMember] = React.useState(false); - const [id, setId] = React.useState(0); - const [title, setTitle] = React.useState(""); - const [description, setDescription] = React.useState(""); - const [image, setImage] = React.useState(""); - const [users, setUsers] = React.useState([] as UserListDto[]); - const [request, setRequest] = React.useState([] as UserListDto[]); - - const { - value: didUpdateTeam, - isFetching: updateTeamInProgress, - error: updateTeamError, - forcePerformRequest: sendSaveTeamRequest, - } = useApi( - async (apiClient, wasTriggeredManually) => { - if (wasTriggeredManually) { - await apiClient.updateTeam( - id, - title, - description, - image, - users.map((u) => u.id), - ); - showNotification("Saved"); - return true; - } - return false; - }, - [currentUserId, isTeamOwner, id, title, description, image, users, request], - ); - - const { - value: didSendRequestToJoin, - forcePerformRequest: sendRequestToJoin, - } = useApi(async (apiClient, wasTriggeredManually) => { - if (wasTriggeredManually) { - await apiClient.requestToJoinTeam(team.id); - return true; - } - return false; - }, []); - - async function acceptUserToTeam(userId: number) { - await api.acceptUserToTeam( - team.id, - request.find((u) => u.id === userId)!.id, - ); - history.go(0); - } - - const { - value: didDelete, - isFetching: deleteInProgress, - error: deleteError, - forcePerformRequest: deleteGroup, - } = useApi(async (apiClient, wasTriggeredManually) => { - if (wasTriggeredManually) { - if (confirm("Are you sure you want to delete this team?")) { - await apiClient.deleteTeam(team.id); - return true; - } - } - return false; - }, []); - - const { value: allUsers } = useApi( - async (apiClient) => apiClient.getAllUsers(), - [], - ); - - const userList = allUsers ?? []; - - const handleSubmit = React.useCallback((event: React.SyntheticEvent) => { - event.preventDefault(); - }, []); - - const updateTeamDone = - Boolean(didUpdateTeam) && !updateTeamInProgress && !updateTeamError; - - const didDeleteDone = Boolean(didDelete) && !deleteInProgress && !deleteError; - - if (updateTeamDone || didSendRequestToJoin) { - const history = useHistory(); - history.go(0); - } - - if (didDeleteDone) { - const history = useHistory(); - history.push("/teams"); - } - - function addMember() { - setUsers([...users, { id: 0, name: "" }]); - } - - function onChange(index: number, value: UserListDto) { - setUsers((uList) => { - const newUsers = [...uList]; - newUsers[index] = value; - return newUsers; - }); - } - - function alreadyInList(singleUser: UserListDto) { - return users.some((u) => u.id === singleUser.id); - } - - React.useEffect(() => { - if (team) { - setCurrentUserId(team.id); - setId(team.id); - setTitle(team.title); - setDescription(team.description); - setImage(team.teamImg); - setUsers(team.users!); - setRequest(team.requests!); - setIsTeamOwner(team.users.length > 0 && user?.id === team.users![0].id); - setIsTeamMember(team.users.some((u) => u.id === user?.id)); - } - }, [team]); - - function notInUserList() { - return ( - !users.some((u) => u.id === user?.id) && - !request.some((u) => u.id === user?.id) - ); - } - - const isAdmin = user?.role === UserRole.Root; - - return ( - - - {updateTeamError && ( -
- - Update Team Error: {updateTeamError.message} - -
- )} -
- setTitle(value)} - type={TextInputType.Text} - /> - setDescription(value)} - type={TextInputType.Area} - /> -
- setImage(value)} - type={TextInputType.Text} - /> - {image !== "" ? ( - - ) : null} -
- -
- - Team Members (can only be changed by the team owner) - -
- {users.map((singleUser, index) => ( -
- - typeof option === "string" ? option : option.name - } - renderInput={(paramsAuto) => ( - - )} - renderOption={(props, option, _state, ownerState) => ( - - {ownerState.getOptionLabel(option)}{" "} - {alreadyInList(option) ? " - already a member" : ""} - - )} - onChange={(_e, v) => onChange(index, v as UserListDto)} - /> - {index === 0 || !isTeamOwner ? null : ( - { - const newUsers = [...users]; - newUsers.splice(index, 1); - setUsers(newUsers); - }} - /> - )} -
- ))} -
- - {isTeamOwner ? ( -
-
- -
-
- ) : null} -
- {request.length > 0 ? ( -
- - These users requested to join this team (can only be changed by - the team owner) - - {request.map((r, index) => ( -
- -
- {!isTeamOwner ? null : ( - - )} -
-
- ))} -
- ) : null} - - {isTeamOwner ? ( -
- - - -

- Please be aware if you delete a team it is gone. We will not - recover it. -

-
- -
-
-
-
- ) : null} - -
- ); -}; From e8bd20c881d67f2ae13452c7c3aa27dc9f56d53d Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Sat, 11 Apr 2026 18:00:01 +0200 Subject: [PATCH 09/36] Some work on the frontend --- backend/src/controllers/dto.ts | 4 + backend/src/services/team-service.ts | 34 +++--- frontend/src/components/pages/createTeam.tsx | 107 ++---------------- .../src/components/pages/read-only-team.tsx | 12 +- 4 files changed, 35 insertions(+), 122 deletions(-) diff --git a/backend/src/controllers/dto.ts b/backend/src/controllers/dto.ts index 1edba30b..91dd2cbf 100644 --- a/backend/src/controllers/dto.ts +++ b/backend/src/controllers/dto.ts @@ -445,6 +445,10 @@ export class UserDTO { public checkedIn!: boolean; @Expose() public profileSubmitted!: boolean; + @Expose() + public teamRequest: TeamDTO | null = null; + @Expose() + public team: TeamDTO | null = null; } export class UserTokenResponseDTO { diff --git a/backend/src/services/team-service.ts b/backend/src/services/team-service.ts index b3547245..96ef2ac6 100644 --- a/backend/src/services/team-service.ts +++ b/backend/src/services/team-service.ts @@ -162,26 +162,26 @@ export class TeamService implements ITeamService { throw new Error("You are already part of a team"); } - try { - if (team.teamImg === "") { - team.teamImg = - placeholder_img[Math.floor(Math.random() * placeholder_img.length)]; - } + if (team.teamImg === "") { + team.teamImg = + placeholder_img[Math.floor(Math.random() * placeholder_img.length)]; + } - const createdTeam = await this._teams.save(team); + const createdTeam = await this._teams.save(team); - // Every team gets one project by default - const project = new Project(); - project.title = `${team.title}'s Project`; - project.description = ""; - project.team = createdTeam; - project.allowRating = false; - await this._projects.save(project); + // TODO test + user.team = createdTeam; + await this._users.save(user); - return createdTeam; - } catch (e) { - throw e; - } + // Every team gets one project by default + const project = new Project(); + project.title = `${team.title}'s Project`; + project.description = ""; + project.team = createdTeam; + project.allowRating = false; + await this._projects.save(project); + + return createdTeam; } /** diff --git a/frontend/src/components/pages/createTeam.tsx b/frontend/src/components/pages/createTeam.tsx index b8dc6bf9..6992e961 100644 --- a/frontend/src/components/pages/createTeam.tsx +++ b/frontend/src/components/pages/createTeam.tsx @@ -5,7 +5,7 @@ import { TextInput, TextInputType } from "../base/text-input"; import { useApi } from "../../hooks/use-api"; import { Redirect } from "react-router"; import { Routes } from "../../routes"; -import { Autocomplete, Box, InputLabel, TextField } from "@mui/material"; +import { Autocomplete, Box, InputLabel, TextField, Alert } from "@mui/material"; import { MdDeleteOutline } from "react-icons/md"; import { UserListDto } from "../../api/types/dto"; import { useLoginContext } from "../../contexts/login-context"; @@ -22,9 +22,6 @@ export const CreateTeam = () => { const [title, setTitle] = React.useState(""); const [description, setDescription] = React.useState(""); const [teamImg, setTeamImg] = React.useState(""); - const [users, setUsers] = React.useState([ - { id: user?.id, name: user?.firstName + " " + user?.lastName }, - ] as UserListDto[]); const { value: didCreateTeam, @@ -38,19 +35,14 @@ export const CreateTeam = () => { title, description, teamImg, - users.map((u) => u.id), ); return true; } return false; }, - [title, description, teamImg, users], + [title, description, teamImg], ); - const { value: allUsers } = useApi(async (api) => api.getAllUsers(), []); - - const userList = allUsers ?? []; - const handleSubmit = React.useCallback((event: React.SyntheticEvent) => { event.preventDefault(); }, []); @@ -62,20 +54,13 @@ export const CreateTeam = () => { return ; } - function addMember() { - setUsers([...users, { id: 0, name: "" }]); - } - - function onChange(index: number, value: UserListDto) { - setUsers((u) => { - const newUsers = [...u]; - newUsers[index] = value; - return newUsers; - }); - } - - function alreadyInList(singleUser: UserListDto) { - return users.some((u) => u.id === singleUser.id); + if (user.team != null) { + return ( + + + You are already in a team + + ); } return ( @@ -116,80 +101,6 @@ export const CreateTeam = () => { onChange={(value) => setTeamImg(value)} type={TextInputType.Text} /> -
- - Select Team Members - - {users.map((singleUser, index) => ( -
- - typeof option === "string" ? option : option.name - } - renderInput={(params) => ( - - )} - renderOption={(props, option, _state, ownerState) => ( - - {ownerState.getOptionLabel(option)}{" "} - {alreadyInList(option) ? " - already a member" : ""} - - )} - onChange={(_e, v) => onChange(index, v as UserListDto)} - /> - {index === 0 ? null : ( - { - const newUsers = [...users]; - newUsers.splice(index, 1); - setUsers(newUsers); - }} - /> - )} -
- ))} - -
- -
-
); diff --git a/frontend/src/components/pages/read-only-team.tsx b/frontend/src/components/pages/read-only-team.tsx index e2b90995..9d8fdb9f 100644 --- a/frontend/src/components/pages/read-only-team.tsx +++ b/frontend/src/components/pages/read-only-team.tsx @@ -35,12 +35,10 @@ export const ReadOnlyTeam = ({ team }: { team: TeamResponseDTO }) => { [], ); - function notInUserList() { - return ( - !team?.users?.some((u) => u.id === user?.id) && - !team?.requests?.some((u) => u.id === user?.id) - ); - } + const notInTeam = ( + user?.team?.id !== team?.id + || user?.teamRequest?.id !== team?.id + ); React.useEffect(() => { if (team) { @@ -66,7 +64,7 @@ export const ReadOnlyTeam = ({ team }: { team: TeamResponseDTO }) => {

{team?.description}

- {!isTeamOwner && notInUserList() ? ( + {!isTeamOwner && notInTeam ? (
+ + ) +}; + +const TeamMember = ({ user, updateTeamInProgress, onRemove }) => { + return ( + + + + + ) +}; + /** * A settings dashboard to configure all parts of tilt. */ @@ -39,7 +83,7 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { const [description, setDescription] = React.useState(""); const [image, setImage] = React.useState(""); const [users, setUsers] = React.useState([] as UserListDto[]); - const [request, setRequest] = React.useState([] as UserListDto[]); + const [requests, setRequests] = React.useState([] as UserListDto[]); const { value: didUpdateTeam, @@ -54,29 +98,40 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { title, description, image, - users.map((u) => u.id), ); showNotification("Saved"); return true; } return false; }, - [currentUserId, isTeamOwner, id, title, description, image, users, request], + [currentUserId, isTeamOwner, id, title, description, image, users, requests], ); - async function acceptUserToTeam(userId: number) { + const acceptUserToTeam = async (user) => { await api.acceptUserToTeam( team.id, - request.find((u) => u.id === userId)!.id, + user.id, + ); + // TODO tell parent to reload team instead of history.go(0) + history.go(0); + showNotification("Accepted user"); + } + + const removeUserFromTeam = async (user) => { + await api.removeUserFromTeam( + team.id, + user.id, ); + // TODO tell parent to reload team history.go(0); + showNotification("Removed user"); } const { value: didDelete, isFetching: deleteInProgress, error: deleteError, - forcePerformRequest: deleteGroup, + forcePerformRequest: deleteTeam, } = useApi(async (apiClient, wasTriggeredManually) => { if (wasTriggeredManually) { if (confirm("Are you sure you want to delete this team?")) { @@ -137,7 +192,7 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { setDescription(team.description); setImage(team.teamImg); setUsers(team.users!); - setRequest(team.requests!); + setRequests(team.requests!); setIsTeamOwner(team.users.length > 0 && user?.id === team.users![0].id); setIsTeamMember(team.users.some((u) => u.id === user?.id)); } @@ -145,6 +200,8 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { const isAdmin = user?.role === UserRole.Root; + console.log("team", team) + return ( { ) : null}
-
- - Team Members TODO show users + users who requested and button to accept them - TODO button to leave team - +
+

+ Team Members +

+ TODO show users + users who requested and button to accept them + TODO button to leave team (here and in read-only)
- {users.map((singleUser, index) => ( -
- - typeof option === "string" ? option : option.name - } - renderInput={(paramsAuto) => ( - - )} - renderOption={(props, option, _state, ownerState) => ( - - {ownerState.getOptionLabel(option)}{" "} - {alreadyInList(option) ? " - already a member" : ""} - - )} - onChange={(_e, v) => onChange(index, v as UserListDto)} + These users requested to join this team (can only be changed by + the team owner) + {team.requests.length > 0 &&

Requests

} + {(isTeamOwner || isAdmin) && team.requests.map((user) => ( + + - {index === 0 || !isTeamOwner ? null : ( - { - const newUsers = [...users]; - newUsers.splice(index, 1); - setUsers(newUsers); - }} - /> - )} -
+ + ))} -
- - {isTeamOwner ? ( -
-
- -
-
- ) : null} -
- {request.length > 0 ? ( -
- - These users requested to join this team (can only be changed by - the team owner) - - {request.map((r, index) => ( -
- 0 &&

Users

} + {team.users.map((user) => ( + + -
- {!isTeamOwner ? null : ( - - )} -
-
+ + ))}
- ) : null} - - {isTeamOwner ? ( +
+ {(isTeamOwner || isAdmin) ? (
@@ -336,11 +295,11 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => {
diff --git a/frontend/src/components/pages/read-only-project.tsx b/frontend/src/components/pages/read-only-project.tsx index baa795b9..c04e4aa9 100644 --- a/frontend/src/components/pages/read-only-project.tsx +++ b/frontend/src/components/pages/read-only-project.tsx @@ -6,11 +6,14 @@ import { api } from "../../hooks/use-api"; import { PageHeader } from "../base/page-header"; import { RatingForm } from "./rating-form"; import { CriterionDTO, RatingDTO, ProjectDTO } from "../../api/types/dto"; +import { useLoginContext } from "../../contexts/login-context"; /** * A settings dashboard to configure all parts of tilt. */ export const ReadOnlyProject = ({ project }: { project: ProjectDTO }) => { + const { user } = useLoginContext(); + const [criteria, setCriteria] = React.useState([]); const [ratings, setRatings] = React.useState([]); @@ -45,18 +48,20 @@ export const ReadOnlyProject = ({ project }: { project: ProjectDTO }) => {

{project?.description}

-
-

Rate this Project

- Hover criteria for more information. Rate a criterion high, if you think - the project did well in this regard. - {criteria.map((criterion) => ( - r.criterion.id === criterion.id)} - criterion={criterion} - project={project} - /> - ))} -
+ {user.admitted && ( +
+

Rate this Project

+ Hover criteria for more information. Rate a criterion high, if you think + the project did well in this regard. + {criteria.map((criterion) => ( + r.criterion.id === criterion.id)} + criterion={criterion} + project={project} + /> + ))} +
+ )} ); }; From 71e71eb97fbd47da380e900a991e266d0304c842 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Sun, 12 Apr 2026 13:07:02 +0200 Subject: [PATCH 15/36] potentially working --- backend/src/controllers/dto.ts | 22 +-- backend/src/entities/user.ts | 5 +- backend/src/services/team-service.ts | 1 - backend/src/services/user-service.ts | 3 +- .../test/services/mock/mock-teams-service.ts | 1 + docs/docker-development.md | 1 + frontend/src/api/index.ts | 7 +- frontend/src/components/pages/edit-team.tsx | 150 +++++++----------- .../components/pages/read-only-project.tsx | 6 +- .../src/components/pages/read-only-team.tsx | 2 +- frontend/test/__mocks__/api.ts | 1 + 11 files changed, 83 insertions(+), 116 deletions(-) diff --git a/backend/src/controllers/dto.ts b/backend/src/controllers/dto.ts index 433fb055..153a7ac2 100644 --- a/backend/src/controllers/dto.ts +++ b/backend/src/controllers/dto.ts @@ -512,7 +512,18 @@ export class UserListDto { @Expose() public id!: number; @Expose() - public name!: string; + public firstName!: string; + @Expose() + public lastName!: string; +} + +export class UserResponseDto { + @Expose() + public id!: number; + @Expose() + public firstName!: string; + @Expose() + public lastName!: string; } export class ApplicationDTO { @@ -536,13 +547,6 @@ export class IDRequestDTO implements IApiRequest { public data!: number; } -export class UserResponseDto { - @Expose() - public id!: number; - @Expose() - public name!: string; -} - export class TeamDTO { @Expose() public id!: number; @@ -594,8 +598,6 @@ export class TeamUpdateDTO { @Expose() public title!: string; @Expose() - public users?: number[]; - @Expose() public teamImg!: string; @Expose() public description!: string; diff --git a/backend/src/entities/user.ts b/backend/src/entities/user.ts index 8865ae12..c4af3945 100644 --- a/backend/src/entities/user.ts +++ b/backend/src/entities/user.ts @@ -47,7 +47,10 @@ export class User { public declined!: boolean; @Column({ default: false }) public checkedIn!: boolean; - @ManyToOne(() => Team, (team) => team.requests, { nullable: true, eager: true }) + @ManyToOne(() => Team, (team) => team.requests, { + nullable: true, + eager: true, + }) public teamRequest: Team | null = null; @ManyToOne(() => Team, (team) => team.users, { nullable: true, eager: true }) public team: Team | null = null; diff --git a/backend/src/services/team-service.ts b/backend/src/services/team-service.ts index 4517ce6a..0df5ef99 100644 --- a/backend/src/services/team-service.ts +++ b/backend/src/services/team-service.ts @@ -278,7 +278,6 @@ export class TeamService implements ITeamService { } // TODO ownerId - console.log(team) if (team?.users[0].id !== requestedBy.id) { throw new Error("You are not the owner of this team"); } diff --git a/backend/src/services/user-service.ts b/backend/src/services/user-service.ts index 6fef95e3..521ce414 100644 --- a/backend/src/services/user-service.ts +++ b/backend/src/services/user-service.ts @@ -266,7 +266,8 @@ export class UserService implements IUserService { return users.map((user) => { const userDto = new UserListDto(); userDto.id = user.id; - userDto.name = `${user.firstName} ${user.lastName[0]}. #${user.id}`; + userDto.firstName = user.firstName; + userDto.lastName = user.lastName; return userDto; }); } diff --git a/backend/test/services/mock/mock-teams-service.ts b/backend/test/services/mock/mock-teams-service.ts index 68e47fd6..26a08147 100644 --- a/backend/test/services/mock/mock-teams-service.ts +++ b/backend/test/services/mock/mock-teams-service.ts @@ -15,5 +15,6 @@ export const MockTeamsService = jest.fn( requestToJoinTeam: jest.fn(), updateTeam: jest.fn(), acceptUserToTeam: jest.fn(), + removeUserFromTeam: jest.fn(), }), ); diff --git a/docs/docker-development.md b/docs/docker-development.md index a284850a..85d0470c 100644 --- a/docs/docker-development.md +++ b/docs/docker-development.md @@ -37,6 +37,7 @@ docker exec backend yarn run backend::test ## Lint ```sh +docker exec backend yarn audit --groups dependencies docker exec backend node ./node_modules/.bin/prettier --config .prettierrc.js '{backend,frontend}/{src,test}/**/*.{ts,tsx}' --write docker exec backend yarn run lint docker exec frontend yarn run frontend::typecheck diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 2e37e96e..312d3986 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -277,14 +277,12 @@ export class ApiClient { title: string, description: string, teamImg: string, - users: number[], ): Promise { await this.put( "/application/team", { id, title, - users, teamImg, description, }, @@ -320,7 +318,10 @@ export class ApiClient { * @param teamId The team's id * @param userId The user's id */ - public async removeUserFromTeam(teamId: number, userId: number): Promise { + public async removeUserFromTeam( + teamId: number, + userId: number, + ): Promise { await this.delete( `/application/team/${teamId}/members/${userId}`, {} as never, diff --git a/frontend/src/components/pages/edit-team.tsx b/frontend/src/components/pages/edit-team.tsx index 704ca0c3..6ab038cc 100644 --- a/frontend/src/components/pages/edit-team.tsx +++ b/frontend/src/components/pages/edit-team.tsx @@ -6,16 +6,7 @@ import { RoundedImage } from "../base/image"; import { Spacer } from "../base/flex"; import { TextInput, TextInputType } from "../base/text-input"; import { api, useApi } from "../../hooks/use-api"; -import { - Autocomplete, - Box, - Card, - CardContent, - InputLabel, - TextField, - Stack, -} from "@mui/material"; -import { MdDeleteOutline } from "react-icons/md"; +import { Card, CardContent, TextField, Stack } from "@mui/material"; import { UserListDto, TeamResponseDTO } from "../../api/types/dto"; import { useLoginContext } from "../../contexts/login-context"; import { useHistory } from "react-router-dom"; @@ -24,46 +15,44 @@ import { UserRole } from "../../api/types/enums"; import { PageHeader } from "../base/page-header"; import { useNotificationContext } from "../../contexts/notification-context"; -const TeamMemberRequest = ({ user, updateTeamInProgress, acceptUserToTeam }) => { +const TeamMemberRequest = ({ + user, + updateTeamInProgress, + acceptUserToTeam, +}) => { return ( - + - ) + ); }; const TeamMember = ({ user, updateTeamInProgress, onRemove }) => { return ( - + - ) + ); }; /** @@ -93,39 +82,37 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { } = useApi( async (apiClient, wasTriggeredManually) => { if (wasTriggeredManually) { - await apiClient.updateTeam( - id, - title, - description, - image, - ); + await apiClient.updateTeam(id, title, description, image); showNotification("Saved"); return true; } return false; }, - [currentUserId, isTeamOwner, id, title, description, image, users, requests], + [ + currentUserId, + isTeamOwner, + id, + title, + description, + image, + users, + requests, + ], ); - const acceptUserToTeam = async (user) => { - await api.acceptUserToTeam( - team.id, - user.id, - ); + const acceptUserToTeam = async (userToAccept) => { + await api.acceptUserToTeam(team.id, userToAccept.id); // TODO tell parent to reload team instead of history.go(0) history.go(0); showNotification("Accepted user"); - } + }; - const removeUserFromTeam = async (user) => { - await api.removeUserFromTeam( - team.id, - user.id, - ); + const removeUserFromTeam = async (userToRemove: UserListDto) => { + await api.removeUserFromTeam(team.id, userToRemove.id); // TODO tell parent to reload team history.go(0); showNotification("Removed user"); - } + }; const { value: didDelete, @@ -142,13 +129,6 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { return false; }, []); - const { value: allUsers } = useApi( - async (apiClient) => apiClient.getAllUsers(), - [], - ); - - const userList = allUsers ?? []; - const handleSubmit = React.useCallback((event: React.SyntheticEvent) => { event.preventDefault(); }, []); @@ -168,22 +148,6 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { history.push("/teams"); } - function addMember() { - setUsers([...users, { id: 0, name: "" }]); - } - - function onChange(index: number, value: UserListDto) { - setUsers((uList) => { - const newUsers = [...uList]; - newUsers[index] = value; - return newUsers; - }); - } - - function alreadyInList(singleUser: UserListDto) { - return users.some((u) => u.id === singleUser.id); - } - React.useEffect(() => { if (team) { setCurrentUserId(team.id); @@ -200,8 +164,6 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { const isAdmin = user?.role === UserRole.Root; - console.log("team", team) - return ( {
-

- Team Members -

- TODO show users + users who requested and button to accept them - TODO button to leave team (here and in read-only) +

Team Members

+ TODO show users + users who requested and button to accept them TODO + button to leave team (here and in read-only)
- These users requested to join this team (can only be changed by - the team owner) - {team.requests.length > 0 &&

Requests

} - {(isTeamOwner || isAdmin) && team.requests.map((user) => ( - - - - - ))} - {team.users.length > 0 &&

Users

} - {team.users.map((user) => ( - + {team.users.map((teamMember) => ( + ))} + {team.requests.length > 0 &&

Requests

} + {(isTeamOwner || isAdmin) && + team.requests.map((requestingUser) => ( + + + + + ))}
- {(isTeamOwner || isAdmin) ? ( + {isTeamOwner || isAdmin ? (

- Please be aware if you delete a team it is gone. We will not - recover it. + Please be aware: if you delete a team it is gone. We will not + be able to recover it.

- {user.admitted && ( + {user?.admitted && (

Rate this Project

- Hover criteria for more information. Rate a criterion high, if you think - the project did well in this regard. + Hover criteria for more information. Rate a criterion high, if you + think the project did well in this regard. {criteria.map((criterion) => ( r.criterion.id === criterion.id)} diff --git a/frontend/src/components/pages/read-only-team.tsx b/frontend/src/components/pages/read-only-team.tsx index c9919f55..0883a2a0 100644 --- a/frontend/src/components/pages/read-only-team.tsx +++ b/frontend/src/components/pages/read-only-team.tsx @@ -84,7 +84,7 @@ export const ReadOnlyTeam = ({ team }: { team: TeamResponseDTO }) => {
{team?.users?.map((singleUser, index) => (
- {singleUser.name} + {singleUser.firstName}
))}
diff --git a/frontend/test/__mocks__/api.ts b/frontend/test/__mocks__/api.ts index d789990c..cab545a8 100644 --- a/frontend/test/__mocks__/api.ts +++ b/frontend/test/__mocks__/api.ts @@ -31,6 +31,7 @@ export const api: IMockedApi = { updateTeam: jest.fn(), requestToJoinTeam: jest.fn(), acceptUserToTeam: jest.fn(), + removeUserFromTeam: jest.fn(), deleteTeam: jest.fn(), getAllTeams: jest.fn(), getTeamByID: jest.fn(), From 12b594968a02921b616ef64519dfe94df28702cd Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Sun, 12 Apr 2026 14:44:19 +0200 Subject: [PATCH 16/36] wip team.owner --- .../src/controllers/application-controller.ts | 18 ++++ backend/src/controllers/dto.ts | 6 ++ backend/src/entities/team.ts | 6 +- backend/src/services/team-service.ts | 90 +++++++++++++++---- frontend/src/api/index.ts | 41 +++++---- frontend/src/components/pages/edit-team.tsx | 40 +++++---- frontend/src/components/pages/status.tsx | 2 +- frontend/src/components/pages/teams.tsx | 8 +- 8 files changed, 152 insertions(+), 59 deletions(-) diff --git a/backend/src/controllers/application-controller.ts b/backend/src/controllers/application-controller.ts index 76c2ca08..51819287 100644 --- a/backend/src/controllers/application-controller.ts +++ b/backend/src/controllers/application-controller.ts @@ -333,6 +333,24 @@ export class ApplicationController { return response; } + /** + * Set the owner of a team + * @param teamId The id of the team + * @param userId The id of the new owner + */ + @Put("/team/:teamId/owner/:userId") + @Authorized(UserRole.User) + public async setOwner( + @Param("teamId") teamId: number, + @Param("userId") userId: number, + @CurrentUser() user: User, + ): Promise { + await this._teams.setOwner(teamId, userId, user); + const response = new SuccessResponseDTO(); + response.success = true; + return response; + } + /** * Get team by id. * @param id The id of the team diff --git a/backend/src/controllers/dto.ts b/backend/src/controllers/dto.ts index 153a7ac2..d7b4f848 100644 --- a/backend/src/controllers/dto.ts +++ b/backend/src/controllers/dto.ts @@ -554,6 +554,9 @@ export class TeamDTO { public title!: string; @Expose() @Type(() => UserDTO) + public owner!: UserDTO; + @Expose() + @Type(() => UserDTO) @ValidateNested() public users!: UserDTO[]; @Expose() @@ -577,6 +580,9 @@ export class TeamResponseDTO { public description!: string; @Expose() @Type(() => UserResponseDto) + public owner!: UserResponseDto; + @Expose() + @Type(() => UserResponseDto) public users!: UserResponseDto[]; @Expose() @Type(() => UserResponseDto) diff --git a/backend/src/entities/team.ts b/backend/src/entities/team.ts index 3d18edd8..8d194cc9 100644 --- a/backend/src/entities/team.ts +++ b/backend/src/entities/team.ts @@ -1,4 +1,4 @@ -import { Column, Entity, PrimaryGeneratedColumn, OneToMany } from "typeorm"; +import { Column, Entity, PrimaryGeneratedColumn, OneToMany, OneToOne, JoinColumn } from "typeorm"; import { Longtext } from "./longtext"; import { User } from "./user"; @@ -13,6 +13,10 @@ export class Team { public teamImg!: string; @Longtext() public description!: string; + // The owner also has to have their user.team property set to this team + @OneToOne(() => User) + @JoinColumn() + public owner!: User; @OneToMany(() => User, (user) => user.teamRequest) public requests!: User[]; @OneToMany(() => User, (user) => user.team) diff --git a/backend/src/services/team-service.ts b/backend/src/services/team-service.ts index 0df5ef99..5b2e882b 100644 --- a/backend/src/services/team-service.ts +++ b/backend/src/services/team-service.ts @@ -5,12 +5,12 @@ import { IService } from "."; import { DatabaseServiceToken, IDatabaseService } from "./database-service"; import { Team } from "../entities/team"; import { Project } from "../entities/project"; +import { UserRole } from "../entities/user-role"; import { TeamResponseDTO, convertBetweenEntityAndDTO, } from "../controllers/dto"; import { User } from "../entities/user"; -import { hasSameElements } from "../utils/has-same-elements"; /** * An interface describing user handling. @@ -56,6 +56,14 @@ export interface ITeamService extends IService { * Request to join a team */ requestToJoinTeam(teamId: number, user: User): Promise; + /** + * Set the owner of a team + */ + setOwner( + teamId: number, + userId: number, + requestedBy: User, + ): Promise; } /** @@ -122,11 +130,9 @@ export class TeamService implements ITeamService { throw new Error("You are not a member of this team"); } - if (!hasSameElements(originalTeam.userIds(), team.userIds())) { - const isAdmin = originalTeam!.userIds()[0] !== user.id; - if (isAdmin) { - throw new Error("You are not the owner of this team"); - } + if (team.owner?.id !== originalTeam.owner?.id) { + // TODO test + throw new Error("Use the special http endpoint to change the owner") } return this._teams.save(team); @@ -135,6 +141,7 @@ export class TeamService implements ITeamService { /** * Creates a team. * @param team The team to create + * @param user The user who wants to create a team */ public async createTeam(team: Team, user: User): Promise { const placeholder_img = [ @@ -162,8 +169,6 @@ export class TeamService implements ITeamService { // TODO leaving team should make someone else owner // TODO order of team.users not guaranteed anymore I guess, - // - add owner and edit all usages of users[0]. - // - a team owner also has to be part of the team, I suppose // - you can only own one team, just like you can only be part of one team if (user.team) { @@ -171,10 +176,13 @@ export class TeamService implements ITeamService { } if (team.teamImg === "") { - team.teamImg = - placeholder_img[Math.floor(Math.random() * placeholder_img.length)]; + const randomIndex = Math.floor(Math.random() * placeholder_img.length); + team.teamImg = placeholder_img[randomIndex]; } + // TODO test + team.owner = user; + const createdTeam = await this._teams.save(team); // TODO test @@ -247,8 +255,7 @@ export class TeamService implements ITeamService { relations: ["users", "requests"], }); - // TODO ownerid - if (team?.users[0].id !== currentUserId.id) { + if (team?.owner.id !== currentUserId.id) { throw new Error("You are not the owner of this team"); } @@ -277,8 +284,10 @@ export class TeamService implements ITeamService { throw new Error(`no team with id ${teamId}`); } - // TODO ownerId - if (team?.users[0].id !== requestedBy.id) { + const isAdmin = requestedBy.role === UserRole.Root; + const isOwner = team.owner?.id === requestedBy.id; + if (!isAdmin && !isOwner) { + // TODO test only admins or owners may accept requests throw new Error("You are not the owner of this team"); } @@ -308,17 +317,64 @@ export class TeamService implements ITeamService { throw new Error(`no team with id ${teamId}`); } - // TODO ownerId - if (team?.users[0].id !== requestedBy.id) { - throw new Error("You are not the owner of this team"); + if (team.owner?.id !== requestedBy.id && userId !== requestedBy.id) { + // TODO test removing oneself should work + throw new Error("Only the owner may remove other users from a team"); + } + + if (team.owner?.id === userId) { + // TODO test + throw new Error("Make someone else owner of the team first"); } if (!team.userIds().includes(userId)) { throw new Error(`user ${userId} is not part of the team ${teamId}`); } + // TODO test success await this._users.update({ id: userId }, { team: null, teamRequest: null }); return Promise.resolve(); } + + /** + * Set the owner of a team + */ + public async setOwner( + teamId: number, + userId: number, + requestedBy: User, + ): Promise { + const team = await this._teams.findOne({ + where: { id: teamId }, + relations: ["users", "requests"], + }); + + if (team == null) { + throw new Error(`No team with id ${teamId}`); + } + + const isAdmin = requestedBy.role === UserRole.Root; + const isOwner = team.owner?.id === requestedBy.id; + if (!isAdmin && !isOwner) { + // TODO test + throw new Error("Only the owner may change the owner"); + } + + if (!team.userIds().includes(userId)) { + // TODO test + throw new Error(`User ${userId} is not part of the team ${teamId}`); + } + + const newOwner = await this._users.findOneBy({ id: userId }); + + if (newOwner == null) { + throw new Error(`User ${userId} not found`); + } + + // TODO test success + await this._teams.update({ id: teamId }, { owner: newOwner }); + + return Promise.resolve(); + } } diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 312d3986..a995a104 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -266,26 +266,16 @@ export class ApiClient { /** * Update new team - * @param id The team's id - * @param title The team's title - * @param description The team's description - * @param teamImg The team's image - * @param users The team's users + * @param tean,id The team's id + * @param team.title The team's title + * @param team.description The team's description + * @param team.teamImg The team's image + * @param team.owner The team's owner */ - public async updateTeam( - id: number, - title: string, - description: string, - teamImg: string, - ): Promise { + public async updateTeam(team: TeamDTO): Promise { await this.put( "/application/team", - { - id, - title, - teamImg, - description, - }, + team, ); } @@ -322,12 +312,27 @@ export class ApiClient { teamId: number, userId: number, ): Promise { - await this.delete( + await this.delete( `/application/team/${teamId}/members/${userId}`, {} as never, ); } + /** + * Set the owner of a team. + * @param teamId The team's id + * @param userId The user's id + */ + public async setOwner( + teamId: number, + userId: number, + ): Promise { + await this.put( + `/application/team/${teamId}/owner/${userId}`, + {} as never, + ); + } + /** * Delete a team by id * @param id The team's id diff --git a/frontend/src/components/pages/edit-team.tsx b/frontend/src/components/pages/edit-team.tsx index 6ab038cc..c6986230 100644 --- a/frontend/src/components/pages/edit-team.tsx +++ b/frontend/src/components/pages/edit-team.tsx @@ -37,7 +37,7 @@ const TeamMemberRequest = ({ ); }; -const TeamMember = ({ user, updateTeamInProgress, onRemove }) => { +const TeamMember = ({ team, user, updateTeamInProgress, onSetOwner, onRemove }) => { return ( @@ -51,6 +51,16 @@ const TeamMember = ({ user, updateTeamInProgress, onRemove }) => { > Remove + ); }; @@ -71,8 +81,6 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { const [title, setTitle] = React.useState(""); const [description, setDescription] = React.useState(""); const [image, setImage] = React.useState(""); - const [users, setUsers] = React.useState([] as UserListDto[]); - const [requests, setRequests] = React.useState([] as UserListDto[]); const { value: didUpdateTeam, @@ -82,22 +90,13 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { } = useApi( async (apiClient, wasTriggeredManually) => { if (wasTriggeredManually) { - await apiClient.updateTeam(id, title, description, image); + await apiClient.updateTeam({ id, title, description, image }); showNotification("Saved"); return true; } return false; }, - [ - currentUserId, - isTeamOwner, - id, - title, - description, - image, - users, - requests, - ], + [ currentUserId, isTeamOwner, id, title, description, image ], ); const acceptUserToTeam = async (userToAccept) => { @@ -114,6 +113,13 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { showNotification("Removed user"); }; + const onSetOwner = async (newOwner: UserListDto) => { + await api.setOwner(team.id, newOwner.id); + // TODO tell parent to reload team + history.go(0); + showNotification("Changed owner"); + }; + const { value: didDelete, isFetching: deleteInProgress, @@ -155,8 +161,6 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { setTitle(team.title); setDescription(team.description); setImage(team.teamImg); - setUsers(team.users!); - setRequests(team.requests!); setIsTeamOwner(team.users.length > 0 && user?.id === team.users![0].id); setIsTeamMember(team.users.some((u) => u.id === user?.id)); } @@ -219,9 +223,11 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { {team.users.map((teamMember) => ( @@ -241,7 +247,7 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => {
{isTeamOwner || isAdmin ? ( -
+
diff --git a/frontend/src/components/pages/status.tsx b/frontend/src/components/pages/status.tsx index a8c30ae7..c2934069 100644 --- a/frontend/src/components/pages/status.tsx +++ b/frontend/src/components/pages/status.tsx @@ -548,7 +548,7 @@ export const Status = () => { WebkitBoxOrient: "vertical", }} > - New Feature this year. You can create or join a team. + You can create or join a team.

diff --git a/frontend/src/components/pages/teams.tsx b/frontend/src/components/pages/teams.tsx index 091b300e..e777e17e 100644 --- a/frontend/src/components/pages/teams.tsx +++ b/frontend/src/components/pages/teams.tsx @@ -23,11 +23,9 @@ export const Teams = () => { buttonText="Create New Team" buttonHref={Routes.CreateTeam} subTitle="Create or join a team" - collapsibleText="You can create or join a team. You can - add other users to your team and remove them as well. The team owner can - delete the team and remove users from the team. If you want to join a - team you can send a request to join the team and the team owner can - accept or reject the request." + collapsibleText="You can create or join a team. As the team owner you can + accept users who want to join, remove users, or delete the whole team. + If you want to join a team, go to the teams page and hit the button." /> {Array.from(teams).map((team: TeamDTO, index) => ( From 740e5ce41c611ad819b8d196e39d0ada473eac76 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Sun, 12 Apr 2026 16:18:12 +0200 Subject: [PATCH 17/36] various --- backend/src/entities/team.ts | 14 ++- backend/src/services/team-service.ts | 26 ++-- frontend/src/api/index.ts | 5 +- frontend/src/components/base/button.tsx | 8 +- frontend/src/components/pages/edit-team.tsx | 111 +++++++++++++----- .../components/routers/sidebar/sidebar.tsx | 3 +- frontend/test/__mocks__/api.ts | 1 + 7 files changed, 117 insertions(+), 51 deletions(-) diff --git a/backend/src/entities/team.ts b/backend/src/entities/team.ts index 8d194cc9..d2e0fcda 100644 --- a/backend/src/entities/team.ts +++ b/backend/src/entities/team.ts @@ -1,4 +1,11 @@ -import { Column, Entity, PrimaryGeneratedColumn, OneToMany, OneToOne, JoinColumn } from "typeorm"; +import { + Column, + Entity, + PrimaryGeneratedColumn, + OneToMany, + OneToOne, + JoinColumn, +} from "typeorm"; import { Longtext } from "./longtext"; import { User } from "./user"; @@ -13,7 +20,10 @@ export class Team { public teamImg!: string; @Longtext() public description!: string; - // The owner also has to have their user.team property set to this team + // The owner also has to have their user.team property set to this team. + // Beware that this is not eagerly loaded, because it will throw recursion depth + // errors due to user.team being eagerly loaded already. Add it to "relations" + // when doing database queries instead. @OneToOne(() => User) @JoinColumn() public owner!: User; diff --git a/backend/src/services/team-service.ts b/backend/src/services/team-service.ts index 5b2e882b..a3fcc410 100644 --- a/backend/src/services/team-service.ts +++ b/backend/src/services/team-service.ts @@ -59,11 +59,7 @@ export interface ITeamService extends IService { /** * Set the owner of a team */ - setOwner( - teamId: number, - userId: number, - requestedBy: User, - ): Promise; + setOwner(teamId: number, userId: number, requestedBy: User): Promise; } /** @@ -98,7 +94,7 @@ export class TeamService implements ITeamService { */ public async getAllTeams(): Promise { return this._database.getRepository(Team).find({ - relations: ["users", "requests"], + relations: ["users", "requests", "owner"], }); } @@ -117,7 +113,7 @@ export class TeamService implements ITeamService { const originalTeam = await this._teams.findOne({ where: { id: team.id }, - relations: ["users", "requests"], + relations: ["users", "requests", "owner"], }); if (!originalTeam) { @@ -132,7 +128,7 @@ export class TeamService implements ITeamService { if (team.owner?.id !== originalTeam.owner?.id) { // TODO test - throw new Error("Use the special http endpoint to change the owner") + throw new Error("Use the special http endpoint to change the owner"); } return this._teams.save(team); @@ -207,7 +203,7 @@ export class TeamService implements ITeamService { public async getTeamByID(id: number): Promise { const team = await this._teams.findOne({ where: { id }, - relations: ["users", "requests"], + relations: ["users", "requests", "owner"], }); if (team == null) { @@ -252,7 +248,7 @@ export class TeamService implements ITeamService { public async deleteTeamByID(id: number, currentUserId: User): Promise { const team = await this._teams.findOne({ where: { id }, - relations: ["users", "requests"], + relations: ["users", "requests", "owner"], }); if (team?.owner.id !== currentUserId.id) { @@ -277,7 +273,7 @@ export class TeamService implements ITeamService { ): Promise { const team = await this._teams.findOne({ where: { id: teamId }, - relations: ["users", "requests"], + relations: ["users", "requests", "owner"], }); if (team == null) { @@ -310,14 +306,16 @@ export class TeamService implements ITeamService { ): Promise { const team = await this._teams.findOne({ where: { id: teamId }, - relations: ["users", "requests"], + relations: ["users", "requests", "owner"], }); if (team == null) { throw new Error(`no team with id ${teamId}`); } - if (team.owner?.id !== requestedBy.id && userId !== requestedBy.id) { + const isOwner = team.owner?.id === requestedBy.id; + const isAdmin = requestedBy.role === UserRole.Root; + if (!isOwner && !isAdmin && userId !== requestedBy.id) { // TODO test removing oneself should work throw new Error("Only the owner may remove other users from a team"); } @@ -347,7 +345,7 @@ export class TeamService implements ITeamService { ): Promise { const team = await this._teams.findOne({ where: { id: teamId }, - relations: ["users", "requests"], + relations: ["users", "requests", "owner"], }); if (team == null) { diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index a995a104..c6f76a42 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -323,10 +323,7 @@ export class ApiClient { * @param teamId The team's id * @param userId The user's id */ - public async setOwner( - teamId: number, - userId: number, - ): Promise { + public async setOwner(teamId: number, userId: number): Promise { await this.put( `/application/team/${teamId}/owner/${userId}`, {} as never, diff --git a/frontend/src/components/base/button.tsx b/frontend/src/components/base/button.tsx index 5eea9548..0fef580f 100644 --- a/frontend/src/components/base/button.tsx +++ b/frontend/src/components/base/button.tsx @@ -67,6 +67,7 @@ interface IButtonProps { primary?: boolean; loading?: boolean; color?: string; + style?: Record; } /** @@ -79,6 +80,7 @@ export const Button = ({ primary = false, loading = false, color, + style = {}, }: IButtonProps) => { const handleClick = useCallback( (event: React.MouseEvent) => { @@ -92,7 +94,11 @@ export const Button = ({ const Component = primary ? PrimaryButton : RegularButton; return ( - + {children} {loading && ( diff --git a/frontend/src/components/pages/edit-team.tsx b/frontend/src/components/pages/edit-team.tsx index c6986230..a2673d1a 100644 --- a/frontend/src/components/pages/edit-team.tsx +++ b/frontend/src/components/pages/edit-team.tsx @@ -37,31 +37,76 @@ const TeamMemberRequest = ({ ); }; -const TeamMember = ({ team, user, updateTeamInProgress, onSetOwner, onRemove }) => { +// TODO types +const TeamMember = ({ + team, + user, + updateTeamInProgress, + onSetOwner, + onRemove, +}) => { + const { user: loginStateUser } = useLoginContext(); + const loggedInAsAdmin = loginStateUser?.role === UserRole.Root; + const loggedInAsOwner = loginStateUser?.id === team.owner?.id; + const editAllowed = loggedInAsAdmin || loggedInAsOwner; + + const memberIsOwner = team.owner?.id === user.id; + const thisIsYou = user.id === loginStateUser?.id; + + if (!editAllowed) { + return `${user.firstName} ${memberIsOwner ? "(Owner)" : ""}`; + } + return ( - - - - - +
+ + {user.firstName} + {thisIsYou && " (This is you)"} + +
+ + {memberIsOwner ? ( + + ) : ( + + )} + +
); }; @@ -90,16 +135,16 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { } = useApi( async (apiClient, wasTriggeredManually) => { if (wasTriggeredManually) { - await apiClient.updateTeam({ id, title, description, image }); + await apiClient.updateTeam({ id, title, description, teamImg: image }); showNotification("Saved"); return true; } return false; }, - [ currentUserId, isTeamOwner, id, title, description, image ], + [currentUserId, isTeamOwner, id, title, description, image], ); - const acceptUserToTeam = async (userToAccept) => { + const acceptUserToTeam = async (userToAccept: UserListDto) => { await api.acceptUserToTeam(team.id, userToAccept.id); // TODO tell parent to reload team instead of history.go(0) history.go(0); @@ -217,8 +262,6 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => {

Team Members

- TODO show users + users who requested and button to accept them TODO - button to leave team (here and in read-only)
{team.users.map((teamMember) => ( @@ -245,6 +288,18 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { ))}
+ {user && user.team?.id === team.id && ( + + )}
{isTeamOwner || isAdmin ? (
diff --git a/frontend/src/components/routers/sidebar/sidebar.tsx b/frontend/src/components/routers/sidebar/sidebar.tsx index e69000b1..8c2ebc05 100644 --- a/frontend/src/components/routers/sidebar/sidebar.tsx +++ b/frontend/src/components/routers/sidebar/sidebar.tsx @@ -106,8 +106,7 @@ export const Sidebar = () => {

HACKABURG

CONTROL CENTER

- All important information about

the Hackaburg 2026{" "} - event + Everything important about

the Hackaburg 2026 event

diff --git a/frontend/test/__mocks__/api.ts b/frontend/test/__mocks__/api.ts index cab545a8..8975b3ed 100644 --- a/frontend/test/__mocks__/api.ts +++ b/frontend/test/__mocks__/api.ts @@ -46,4 +46,5 @@ export const api: IMockedApi = { getRatingResults: jest.fn(), createRating: jest.fn(), getUsersRatingsForProject: jest.fn(), + setOwner: jest.fn(), }; From e0cd89f69ac038d3f85f4b15686546593ff4fe6d Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:04:06 +0200 Subject: [PATCH 18/36] various --- .../src/components/base/stack-with-border.tsx | 37 ++++++ frontend/src/components/pages/edit-team.tsx | 116 ++++++++---------- frontend/src/components/pages/rating-form.tsx | 75 +++++------ .../src/components/pages/read-only-team.tsx | 31 ++--- 4 files changed, 125 insertions(+), 134 deletions(-) create mode 100644 frontend/src/components/base/stack-with-border.tsx diff --git a/frontend/src/components/base/stack-with-border.tsx b/frontend/src/components/base/stack-with-border.tsx new file mode 100644 index 00000000..6224a3a3 --- /dev/null +++ b/frontend/src/components/base/stack-with-border.tsx @@ -0,0 +1,37 @@ +import * as React from "react"; +import { Stack, Tooltip } from "@mui/material"; + +// TODO types. text and tooltip are optional +/** + * I typically use this to display some text and a few buttons, + * with multiple of this on top of each other, like a table. + * Maybe these components should use tables instead, idk. It looks nice. + */ +export const StackWithBorder = ({ text, children, tooltip }) => { + return ( +
+ + {text && ( +
+ + {text} + +
+ )} + {children} +
+
+ ); +}; diff --git a/frontend/src/components/pages/edit-team.tsx b/frontend/src/components/pages/edit-team.tsx index a2673d1a..4878f9f2 100644 --- a/frontend/src/components/pages/edit-team.tsx +++ b/frontend/src/components/pages/edit-team.tsx @@ -14,6 +14,7 @@ import { Message } from "../base/message"; import { UserRole } from "../../api/types/enums"; import { PageHeader } from "../base/page-header"; import { useNotificationContext } from "../../contexts/notification-context"; +import { StackWithBorder } from "../base/stack-with-border"; const TeamMemberRequest = ({ user, @@ -21,8 +22,7 @@ const TeamMemberRequest = ({ acceptUserToTeam, }) => { return ( - - + - + ); }; @@ -54,59 +55,49 @@ const TeamMember = ({ const thisIsYou = user.id === loginStateUser?.id; if (!editAllowed) { - return `${user.firstName} ${memberIsOwner ? "(Owner)" : ""}`; + return ( +

+ {user.firstName} + {memberIsOwner ? "(Owner)" : ""} +

+ ); } return ( -
- { + onRemove(user); + }} + primary + style={{ minWidth: "150px" }} > -
- - {user.firstName} - {thisIsYou && " (This is you)"} - -
+ {thisIsYou ? "Leave" : "Remove"} + + {memberIsOwner ? ( + + ) : ( - {memberIsOwner ? ( - - ) : ( - - )} -
-
+ )} + ); }; @@ -114,19 +105,21 @@ const TeamMember = ({ * A settings dashboard to configure all parts of tilt. */ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { + if (team == null) { + return null; + } + const loginState = useLoginContext(); const { user } = loginState; const { showNotification } = useNotificationContext(); - const [currentUserId, setCurrentUserId] = React.useState(0); - const [isTeamOwner, setIsTeamOwner] = React.useState(false); - const [, setIsTeamMember] = React.useState(false); - const [id, setId] = React.useState(0); const [title, setTitle] = React.useState(""); const [description, setDescription] = React.useState(""); const [image, setImage] = React.useState(""); + const isTeamOwner = team.owner?.id === user?.id; + const { value: didUpdateTeam, isFetching: updateTeamInProgress, @@ -135,13 +128,19 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { } = useApi( async (apiClient, wasTriggeredManually) => { if (wasTriggeredManually) { - await apiClient.updateTeam({ id, title, description, teamImg: image }); + await apiClient.updateTeam({ + ...team, + id: team.id, + title, + description, + teamImg: image, + }); showNotification("Saved"); return true; } return false; }, - [currentUserId, isTeamOwner, id, title, description, image], + [team, title, description], ); const acceptUserToTeam = async (userToAccept: UserListDto) => { @@ -201,13 +200,9 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { React.useEffect(() => { if (team) { - setCurrentUserId(team.id); - setId(team.id); setTitle(team.title); setDescription(team.description); setImage(team.teamImg); - setIsTeamOwner(team.users.length > 0 && user?.id === team.users![0].id); - setIsTeamMember(team.users.some((u) => u.id === user?.id)); } }, [team]); @@ -216,7 +211,7 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { return ( { updateTeamInProgress={updateTeamInProgress} onSetOwner={onSetOwner} /> - ))} {team.requests.length > 0 &&

Requests

} @@ -288,18 +282,6 @@ export const EditTeam = ({ team }: { team: TeamResponseDTO }) => { ))}
- {user && user.team?.id === team.id && ( - - )}
{isTeamOwner || isAdmin ? (
diff --git a/frontend/src/components/pages/rating-form.tsx b/frontend/src/components/pages/rating-form.tsx index 5f6d2458..8a15df6b 100644 --- a/frontend/src/components/pages/rating-form.tsx +++ b/frontend/src/components/pages/rating-form.tsx @@ -1,6 +1,5 @@ import React, { useState } from "react"; import { - Stack, FormControl, RadioGroup, FormControlLabel, @@ -12,6 +11,7 @@ import { useLoginContext } from "../../contexts/login-context"; import { Button } from "../base/button"; import { CriterionDTO, ProjectDTO, RatingDTO } from "../../api/types/dto"; import { useNotificationContext } from "../../contexts/notification-context"; +import { StackWithBorder } from "../base/stack-with-border"; interface IRatingFormProps { rating?: RatingDTO; @@ -58,51 +58,32 @@ export const RatingForm = ({ }; return ( -
- -
- - {criterion.title} - -
- - setRatingValue(parseInt(e.target.value, 10))} - > - {[1, 2, 3, 4, 5].map((value) => ( - } - label={value.toString()} - /> - ))} - - -
- -
-
-
+ + + setRatingValue(parseInt(e.target.value, 10))} + > + {[1, 2, 3, 4, 5].map((value) => ( + } + label={value.toString()} + /> + ))} + + +
+ +
+
); }; diff --git a/frontend/src/components/pages/read-only-team.tsx b/frontend/src/components/pages/read-only-team.tsx index 0883a2a0..3c92a1cf 100644 --- a/frontend/src/components/pages/read-only-team.tsx +++ b/frontend/src/components/pages/read-only-team.tsx @@ -62,29 +62,20 @@ export const ReadOnlyTeam = ({ team }: { team: TeamResponseDTO }) => {

{team?.description}

- {!isTeamOwner && notInTeam ? ( -
- -
- ) : null} - -
-

- Team Members -

+
+

Team Members

+ {!isTeamOwner && notInTeam ? ( +
+ +
+ ) : null}
{team?.users?.map((singleUser, index) => (
- {singleUser.firstName} + {singleUser.firstName}{" "} + {singleUser.id === team.owner?.id && " (Owner)"}
))}
From c129d71af6f2ed4c07dbea1855919c88d9f0c0d0 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:18:05 +0200 Subject: [PATCH 19/36] Add types to StackWithBorder and TeamMember, fix all typecheck errors (#125) Agent-Logs-Url: https://github.com/hackaburg/tilt/sessions/c4d1d685-f445-450c-8d14-ea628b425710 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sezanzeb <28510156+sezanzeb@users.noreply.github.com> --- frontend/src/api/index.ts | 4 ++-- .../src/components/base/stack-with-border.tsx | 9 ++++++-- frontend/src/components/pages/edit-team.tsx | 22 ++++++++++++++----- frontend/src/components/pages/rating-form.tsx | 1 - 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index c6f76a42..81915174 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -29,6 +29,7 @@ import type { SuccessResponseDTO, TeamDTO, TeamResponseDTO, + TeamUpdateDTO, UserDTO, UserListDto, } from "./types/dto"; @@ -272,7 +273,7 @@ export class ApiClient { * @param team.teamImg The team's image * @param team.owner The team's owner */ - public async updateTeam(team: TeamDTO): Promise { + public async updateTeam(team: TeamUpdateDTO): Promise { await this.put( "/application/team", team, @@ -314,7 +315,6 @@ export class ApiClient { ): Promise { await this.delete( `/application/team/${teamId}/members/${userId}`, - {} as never, ); } diff --git a/frontend/src/components/base/stack-with-border.tsx b/frontend/src/components/base/stack-with-border.tsx index 6224a3a3..befbdd72 100644 --- a/frontend/src/components/base/stack-with-border.tsx +++ b/frontend/src/components/base/stack-with-border.tsx @@ -1,13 +1,18 @@ import * as React from "react"; import { Stack, Tooltip } from "@mui/material"; -// TODO types. text and tooltip are optional +interface StackWithBorderProps { + text?: string; + tooltip?: string; + children?: React.ReactNode; +} + /** * I typically use this to display some text and a few buttons, * with multiple of this on top of each other, like a table. * Maybe these components should use tables instead, idk. It looks nice. */ -export const StackWithBorder = ({ text, children, tooltip }) => { +export const StackWithBorder = ({ text, children, tooltip }: StackWithBorderProps) => { return (
void; +} + const TeamMemberRequest = ({ user, updateTeamInProgress, acceptUserToTeam, -}) => { +}: TeamMemberRequestProps) => { return ( + ) + }
{team.users.map((teamMember) => ( diff --git a/frontend/src/components/pages/view-team.tsx b/frontend/src/components/pages/view-team.tsx index 12665641..d466ce0a 100644 --- a/frontend/src/components/pages/view-team.tsx +++ b/frontend/src/components/pages/view-team.tsx @@ -25,6 +25,10 @@ export const ViewTeam = () => { return team?.users?.some((u) => u.id === user?.id) ?? false; }, [team, user?.id]); + const reloadTeam = async () => { + await api.getTeamByID(teamId).then((team_) => setTeam(team_)); + }; + const isAdmin = user?.role === UserRole.Root; if (!team) { @@ -32,7 +36,7 @@ export const ViewTeam = () => { } return isTeamMember || isAdmin ? ( - + ) : ( ); From 7fdc4a809fbc2fb39de1153567a1a0a5f34b8b87 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:54:29 +0200 Subject: [PATCH 28/36] copilot review --- .../src/controllers/application-controller.ts | 1 + backend/src/controllers/dto.ts | 18 ++-- backend/src/controllers/users-controller.ts | 4 +- backend/src/entities/project.ts | 2 +- backend/src/entities/user.ts | 7 +- backend/src/services/team-service.ts | 26 +++--- backend/src/utils/has-same-elements.ts | 6 -- frontend/src/api/index.ts | 6 +- frontend/src/components/pages/admission.tsx | 86 ++----------------- frontend/src/components/pages/edit-team.tsx | 43 +++------- .../src/components/pages/read-only-team.tsx | 10 ++- frontend/src/heuristics.ts | 27 ------ 12 files changed, 63 insertions(+), 173 deletions(-) delete mode 100644 backend/src/utils/has-same-elements.ts delete mode 100644 frontend/src/heuristics.ts diff --git a/backend/src/controllers/application-controller.ts b/backend/src/controllers/application-controller.ts index 51819287..cf2e71c8 100644 --- a/backend/src/controllers/application-controller.ts +++ b/backend/src/controllers/application-controller.ts @@ -250,6 +250,7 @@ export class ApplicationController { @Authorized(UserRole.User) public async getAllTeams(): Promise { const teams = await this._teams.getAllTeams(); + // TODO test member emails not exposed return teams.map((team) => convertBetweenEntityAndDTO(team, TeamDTO)); } diff --git a/backend/src/controllers/dto.ts b/backend/src/controllers/dto.ts index d7b4f848..0732e693 100644 --- a/backend/src/controllers/dto.ts +++ b/backend/src/controllers/dto.ts @@ -531,8 +531,6 @@ export class ApplicationDTO { @Type(() => UserDTO) public user!: UserDTO; @Expose() - public teams!: string[]; - @Expose() @Type(() => AnswerDTO) public answers!: AnswerDTO[]; } @@ -553,16 +551,16 @@ export class TeamDTO { @Expose() public title!: string; @Expose() - @Type(() => UserDTO) - public owner!: UserDTO; + @Type(() => UserResponseDto) + public owner!: UserResponseDto; @Expose() - @Type(() => UserDTO) + @Type(() => UserResponseDto) @ValidateNested() - public users!: UserDTO[]; + public users!: UserResponseDto[]; @Expose() - @Type(() => UserDTO) + @Type(() => UserResponseDto) @ValidateNested() - public requests!: UserDTO[]; + public requests!: UserResponseDto[]; @Expose() public teamImg!: string; @Expose() @@ -652,9 +650,9 @@ export class RatingDTO { @Expose() public readonly id!: number; @Expose() - @Type(() => UserDTO) + @Type(() => UserResponseDto) @ValidateNested() - public user!: UserDTO; + public user!: UserResponseDto; @Expose() @Type(() => ProjectDTO) @ValidateNested() diff --git a/backend/src/controllers/users-controller.ts b/backend/src/controllers/users-controller.ts index da1164a2..8f04481e 100644 --- a/backend/src/controllers/users-controller.ts +++ b/backend/src/controllers/users-controller.ts @@ -142,13 +142,12 @@ export class UsersController { const user = await this._users.findUserWithCredentials(email, password); if (!user) { - throw new BadRequestError("invalid email or Password"); + throw new BadRequestError("invalid email or password"); } const response = new UserTokenResponseDTO(); response.token = this._users.generateLoginToken(user); response.user = convertBetweenEntityAndDTO(user, UserDTO); - // TODO test password not in the response return response; } @@ -164,7 +163,6 @@ export class UsersController { const response = new UserTokenResponseDTO(); response.token = this._users.generateLoginToken(user); response.user = convertBetweenEntityAndDTO(user, UserDTO); - // TODO test password not in the response return response; } diff --git a/backend/src/entities/project.ts b/backend/src/entities/project.ts index 80fd61a1..774961ba 100644 --- a/backend/src/entities/project.ts +++ b/backend/src/entities/project.ts @@ -12,7 +12,7 @@ import { Team } from "./team"; export class Project { @PrimaryGeneratedColumn() public readonly id!: number; - @ManyToOne(() => Team, { eager: true }) + @ManyToOne(() => Team, { eager: true, onDelete: "SET NULL" }) @JoinColumn() public team!: Team; @Column({ length: 1024 }) diff --git a/backend/src/entities/user.ts b/backend/src/entities/user.ts index c4af3945..5c40e81f 100644 --- a/backend/src/entities/user.ts +++ b/backend/src/entities/user.ts @@ -50,8 +50,13 @@ export class User { @ManyToOne(() => Team, (team) => team.requests, { nullable: true, eager: true, + onDelete: "SET NULL", }) public teamRequest: Team | null = null; - @ManyToOne(() => Team, (team) => team.users, { nullable: true, eager: true }) + @ManyToOne(() => Team, (team) => team.users, { + nullable: true, + eager: true, + onDelete: "SET NULL", + }) public team: Team | null = null; } diff --git a/backend/src/services/team-service.ts b/backend/src/services/team-service.ts index a3fcc410..e1b25fde 100644 --- a/backend/src/services/team-service.ts +++ b/backend/src/services/team-service.ts @@ -51,7 +51,7 @@ export interface ITeamService extends IService { /** * Delete single team by id */ - deleteTeamByID(id: number, currentUserId: User): Promise; + deleteTeamByID(id: number, currentUser: User): Promise; /** * Request to join a team */ @@ -126,12 +126,12 @@ export class TeamService implements ITeamService { throw new Error("You are not a member of this team"); } - if (team.owner?.id !== originalTeam.owner?.id) { - // TODO test - throw new Error("Use the special http endpoint to change the owner"); - } - - return this._teams.save(team); + return this._teams.save({ + ...originalTeam, + ...team, + // Use the dedicated http endpoint to change ownership + owner: originalTeam.owner, + }); } /** @@ -219,7 +219,10 @@ export class TeamService implements ITeamService { * @param user The user requesting to join */ public async requestToJoinTeam(teamId: number, user: User): Promise { - const team = await this._teams.findOneBy({ id: teamId }); + const team = await this._teams.findOne({ + where: { id: teamId }, + relations: ["users", "requests", "owner"], + }); if (team == null) { throw new Error(`no team with id ${teamId}`); @@ -245,13 +248,16 @@ export class TeamService implements ITeamService { * Deletes a team by its id. * @param id The id of the team */ - public async deleteTeamByID(id: number, currentUserId: User): Promise { + public async deleteTeamByID(id: number, currentUser: User): Promise { const team = await this._teams.findOne({ where: { id }, relations: ["users", "requests", "owner"], }); - if (team?.owner.id !== currentUserId.id) { + if ( + currentUser.role !== UserRole.Root && + team?.owner?.id !== currentUser.id + ) { throw new Error("You are not the owner of this team"); } diff --git a/backend/src/utils/has-same-elements.ts b/backend/src/utils/has-same-elements.ts deleted file mode 100644 index 385ce298..00000000 --- a/backend/src/utils/has-same-elements.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Check if the two arrays contain the same elements, regardless of order. - */ -export function hasSameElements(a: T[], b: T[]): boolean { - return a.length === b.length && a.every((entry) => b.includes(entry)); -} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 81915174..0d87aa11 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -267,11 +267,7 @@ export class ApiClient { /** * Update new team - * @param tean,id The team's id - * @param team.title The team's title - * @param team.description The team's description - * @param team.teamImg The team's image - * @param team.owner The team's owner + * @param team containing id, title, description and teamImg */ public async updateTeam(team: TeamUpdateDTO): Promise { await this.put( diff --git a/frontend/src/components/pages/admission.tsx b/frontend/src/components/pages/admission.tsx index 7eceb318..44a941a1 100644 --- a/frontend/src/components/pages/admission.tsx +++ b/frontend/src/components/pages/admission.tsx @@ -6,7 +6,6 @@ import { AnswerDTO, ApplicationDTO } from "../../api/types/dto"; import { QuestionType } from "../../api/types/enums"; import { debounceDuration } from "../../config"; import { useSettingsContext } from "../../contexts/settings-context"; -import { isNameQuestion, isTeamQuestion } from "../../heuristics"; import { performApiRequest, useApi } from "../../hooks/use-api"; import { useIsResponsive } from "../../hooks/use-is-responsive"; import { @@ -278,9 +277,6 @@ export const Admission = () => { }); }, [debouncedQuery, applicationsSortedByDate, applicationsByUserID]); - const probableNameQuestion = questions.find(isNameQuestion); - const probableTeamQuestion = questions.find(isTeamQuestion); - const [selectedRowIDs, setSelectedRowIDs] = useState([]); const headerCheckboxRef = useRef>(null); @@ -303,66 +299,6 @@ export const Admission = () => { } = useApi( async (api, wasForced) => { if (wasForced) { - if (probableTeamQuestion != null) { - const teams = new Set(); - - for (const id of selectedRowIDs) { - const { answersByQuestionID } = applicationsByUserID[id]; - const teamAnswer = answersByQuestionID[probableTeamQuestion.id!]; - - if (teamAnswer === null) { - continue; - } - - teams.add(teamAnswer); - } - - let firstSeenPartialTeam: Nullable = null; - let missingPartialTeamMemberEmails = ""; - for (const { user } of applicationsSortedByDate) { - const { answersByQuestionID } = applicationsByUserID[user.id]; - const teamAnswer = answersByQuestionID[probableTeamQuestion.id!]; - - if (teamAnswer == null) { - continue; - } - - const isSelectedTeam = teams.has(teamAnswer); - const isNotSelected = !selectedRowIDs.includes(user.id); - const isNotYetAdmitted = !user.admitted; - const isMemberOfFirstPartialTeam = - firstSeenPartialTeam == null || - firstSeenPartialTeam === teamAnswer; - - if ( - isSelectedTeam && - isNotSelected && - isNotYetAdmitted && - isMemberOfFirstPartialTeam - ) { - firstSeenPartialTeam = teamAnswer; - missingPartialTeamMemberEmails += `- ${user.email}\n`; - } - } - - if (firstSeenPartialTeam != null) { - const choice = prompt( - `You're about to admit the team '${firstSeenPartialTeam}', but you missed some team members:\n\n` + - `${missingPartialTeamMemberEmails}\n` + - `Type 'show' (without quotes) to view the missing team members, or type 'ignore' (without quotes) to continue partially admitting this team.`, - ); - - if (choice == null) { - return; - } - - if (choice !== "ignore") { - setQuery(firstSeenPartialTeam); - return; - } - } - } - const applicationList = selectedRowIDs .map((id) => `- ${applicationsByUserID[id].email}`) .join("\n"); @@ -385,7 +321,6 @@ export const Admission = () => { [ selectedRowIDs, reloadApplications, - probableTeamQuestion, applicationsByUserID, applicationsSortedByDate, ], @@ -398,7 +333,7 @@ export const Admission = () => { const isResponsive = useIsResponsive(); const tableRows = useMemo(() => { - return visibleApplications.map(({ user, teams }, userIndex) => { + return visibleApplications.map(({ user }, userIndex) => { const { id, email, @@ -411,10 +346,6 @@ export const Admission = () => { checkedIn, } = user; - // TODO teams is undefined - const teamNumber = teams.length; - const teamNames = teams; - const name = user.firstName + " " + user.lastName; const isRowSelected = selectedRowIDs.includes(id); @@ -543,6 +474,10 @@ export const Admission = () => { RowComponent = AdmittedRow; } + const teamIdentifier = user.team + ? `${user.team?.title} #${user.team?.id}` + : ""; + const cityIndex = questions.find((q) => q.title === "City")?.id!; const countryIndex = questions.find((q) => q.title === "Country")?.id!; const genderIndex = questions.find((q) => q.title === "Gender")?.id!; @@ -560,7 +495,7 @@ export const Admission = () => { {userIndex + 1} {email} {name} - {teamNumber} + {teamIdentifier} {answersByQuestionID[genderIndex] === "Male" ? (
@@ -603,10 +538,8 @@ export const Admission = () => { - - {teamNames.map((teamName, index) => ( -
  • {teamName}
  • - ))} + + {teamIdentifier}
    @@ -672,7 +605,6 @@ export const Admission = () => { }, [ isResponsive, visibleApplications, - probableNameQuestion, selectedRowIDs, expandedRowIDs, applicationsByUserID, @@ -822,7 +754,7 @@ export const Admission = () => { Index E-mail Firstname / Lastname - Teams + Team Gender Created At City/Location diff --git a/frontend/src/components/pages/edit-team.tsx b/frontend/src/components/pages/edit-team.tsx index ebbceeb4..c00fb325 100644 --- a/frontend/src/components/pages/edit-team.tsx +++ b/frontend/src/components/pages/edit-team.tsx @@ -135,10 +135,11 @@ export const EditTeam = ({ const [description, setDescription] = React.useState(""); const [image, setImage] = React.useState(""); + const history = useHistory(); + const isTeamOwner = team.owner?.id === user?.id; const { - value: didUpdateTeam, isFetching: updateTeamInProgress, error: updateTeamError, forcePerformRequest: sendSaveTeamRequest, @@ -152,11 +153,12 @@ export const EditTeam = ({ teamImg: image, }); showNotification("Saved"); + onChange(); return true; } return false; }, - [team, title, description], + [team, title, description, image, showNotification], ); const acceptUserToTeam = async (userToAccept: UserListDto) => { @@ -177,39 +179,22 @@ export const EditTeam = ({ onChange(); }; - const { - value: didDelete, - isFetching: deleteInProgress, - error: deleteError, - forcePerformRequest: deleteTeam, - } = useApi(async (apiClient, wasTriggeredManually) => { - if (wasTriggeredManually) { - if (confirm("Are you sure you want to delete this team?")) { - await apiClient.deleteTeam(team.id); - return true; + const { isFetching: deleteInProgress, forcePerformRequest: deleteTeam } = + useApi(async (apiClient, wasTriggeredManually) => { + if (wasTriggeredManually) { + if (confirm("Are you sure you want to delete this team?")) { + await apiClient.deleteTeam(team.id); + history.push("/teams"); + return true; + } } - } - return false; - }, []); + return false; + }, []); const handleSubmit = React.useCallback((event: React.SyntheticEvent) => { event.preventDefault(); }, []); - const updateTeamDone = - Boolean(didUpdateTeam) && !updateTeamInProgress && !updateTeamError; - - const didDeleteDone = Boolean(didDelete) && !deleteInProgress && !deleteError; - - if (updateTeamDone) { - onChange(); - } - - if (didDeleteDone) { - const history = useHistory(); - history.push("/teams"); - } - React.useEffect(() => { if (team) { setTitle(team.title); diff --git a/frontend/src/components/pages/read-only-team.tsx b/frontend/src/components/pages/read-only-team.tsx index 3c92a1cf..0571f4b5 100644 --- a/frontend/src/components/pages/read-only-team.tsx +++ b/frontend/src/components/pages/read-only-team.tsx @@ -35,9 +35,6 @@ export const ReadOnlyTeam = ({ team }: { team: TeamResponseDTO }) => { [], ); - const notInTeam = - user?.team?.id !== team?.id || user?.teamRequest?.id !== team?.id; - React.useEffect(() => { if (team) { setIsTeamOwner(user?.id === Number(team?.users?.[0]?.id)); @@ -45,6 +42,11 @@ export const ReadOnlyTeam = ({ team }: { team: TeamResponseDTO }) => { } }, [team, user?.id]); + // When leaving a team, the parent component will reload team, and be more + // up to date than user. + const inTeam = team?.users.some(({ id }) => id === user?.id); + const hasRequested = team?.requests.some(({ id }) => id === user?.id); + return ( @@ -64,7 +66,7 @@ export const ReadOnlyTeam = ({ team }: { team: TeamResponseDTO }) => {

    Team Members

    - {!isTeamOwner && notInTeam ? ( + {!isTeamOwner && !inTeam && !hasRequested ? (
    ) : null} + {hasRequested && "You requested to join this team"}
    {team?.users?.map((singleUser, index) => (
    diff --git a/frontend/src/components/routers/sidebar/sidebar-menu.tsx b/frontend/src/components/routers/sidebar/sidebar-menu.tsx index 1ee5c1df..9263ff4f 100644 --- a/frontend/src/components/routers/sidebar/sidebar-menu.tsx +++ b/frontend/src/components/routers/sidebar/sidebar-menu.tsx @@ -9,6 +9,8 @@ const UL = styled.ul` padding: 0; list-style: none; padding-left: 0.5rem; + flex: 1; + overflow-y: auto; `; interface ISidebarMenuProps { diff --git a/frontend/src/components/routers/sidebar/sidebar.tsx b/frontend/src/components/routers/sidebar/sidebar.tsx index 8c2ebc05..9f045310 100644 --- a/frontend/src/components/routers/sidebar/sidebar.tsx +++ b/frontend/src/components/routers/sidebar/sidebar.tsx @@ -26,6 +26,7 @@ import { MdSupportAgent } from "react-icons/md"; const BackgroundContainer = styled(NonGrowingFlexContainer)` height: 100%; overflow-y: auto; + overflow-x: hidden; background: linear-gradient( to top right, ${variables.colorGradientStart}, @@ -197,7 +198,7 @@ export const Sidebar = () => { )} -
    + -
    +
  • Date: Mon, 13 Apr 2026 21:05:09 +0200 Subject: [PATCH 33/36] small stuff --- frontend/src/components/base/page-header.tsx | 1 - frontend/src/components/base/stack-with-border.tsx | 2 +- frontend/src/components/pages/edit-team.tsx | 6 +++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/base/page-header.tsx b/frontend/src/components/base/page-header.tsx index 97da56c8..832666be 100644 --- a/frontend/src/components/base/page-header.tsx +++ b/frontend/src/components/base/page-header.tsx @@ -3,7 +3,6 @@ import * as React from "react"; import { NonGrowingFlexContainer } from "../base/flex"; import { Heading, Subheading } from "../base/headings"; import { Button } from "../base/button"; -import { InternalLink } from "../base/link"; import { Collapsible } from "../base/collapsible"; import { Routes } from "../../routes"; import { Divider } from "../base/divider"; diff --git a/frontend/src/components/base/stack-with-border.tsx b/frontend/src/components/base/stack-with-border.tsx index d8a983d1..3ec743e7 100644 --- a/frontend/src/components/base/stack-with-border.tsx +++ b/frontend/src/components/base/stack-with-border.tsx @@ -29,7 +29,7 @@ export const StackWithBorder = ({ {text && ( diff --git a/frontend/src/components/pages/edit-team.tsx b/frontend/src/components/pages/edit-team.tsx index 104d0410..589feeb5 100644 --- a/frontend/src/components/pages/edit-team.tsx +++ b/frontend/src/components/pages/edit-team.tsx @@ -127,7 +127,7 @@ export const EditTeam = ({ } const loginState = useLoginContext(); - const { user, setUser } = loginState; + const { user, updateUser } = loginState; const { showNotification } = useNotificationContext(); @@ -169,10 +169,10 @@ export const EditTeam = ({ const removeTeamFromUser = async () => { if (user?.team?.id === team.id) { - await setUser({ + await updateUser(() => ({ ...user, team: null, - }); + })); } }; From 95b835f487ed7bd571ed67a6efab9da4ea9b351e Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:08:17 +0200 Subject: [PATCH 34/36] unused import --- backend/test/controllers/application-controller.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/test/controllers/application-controller.spec.ts b/backend/test/controllers/application-controller.spec.ts index 67810e14..3793492f 100644 --- a/backend/test/controllers/application-controller.spec.ts +++ b/backend/test/controllers/application-controller.spec.ts @@ -10,7 +10,6 @@ import { MockedService } from "../services/mock"; import { MockApplicationService } from "../services/mock/mock-application-service"; import { MockTeamsService } from "../services/mock/mock-teams-service"; import { MockUserService } from "../services/mock/mock-user-service"; -import { TeamRequestDTO, TeamDTO } from "../../src/controllers/dto"; describe("ApplicationController", () => { let applicationService: MockedService; From 3aeef99d3e3d355166e0d767be4f516170b35c91 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:37:08 +0200 Subject: [PATCH 35/36] user.team set after team created, warning if not allowed to rate --- frontend/src/api/index.ts | 4 ++-- frontend/src/components/pages/createTeam.tsx | 8 ++++++-- .../components/pages/read-only-project.tsx | 20 +++++++++++++++++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 0d87aa11..72782c10 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -254,8 +254,8 @@ export class ApiClient { title: string, description: string, teamImg: string, - ): Promise { - await this.post( + ): Promise { + return await this.post( "/application/team", { title, diff --git a/frontend/src/components/pages/createTeam.tsx b/frontend/src/components/pages/createTeam.tsx index 57885c08..89fa347b 100644 --- a/frontend/src/components/pages/createTeam.tsx +++ b/frontend/src/components/pages/createTeam.tsx @@ -14,7 +14,7 @@ import { PageHeader } from "../base/page-header"; */ export const CreateTeam = () => { const loginState = useLoginContext(); - const { user } = loginState; + const { user, updateUser } = loginState; const [title, setTitle] = React.useState(""); const [description, setDescription] = React.useState(""); @@ -28,7 +28,11 @@ export const CreateTeam = () => { } = useApi( async (api, wasTriggeredManually) => { if (wasTriggeredManually) { - await api.createTeam(title, description, teamImg); + const team = await api.createTeam(title, description, teamImg); + await updateUser(() => ({ + ...user!, + team, + })); return true; } return false; diff --git a/frontend/src/components/pages/read-only-project.tsx b/frontend/src/components/pages/read-only-project.tsx index 6cdb9786..63e5c31f 100644 --- a/frontend/src/components/pages/read-only-project.tsx +++ b/frontend/src/components/pages/read-only-project.tsx @@ -1,12 +1,14 @@ import * as React from "react"; +import { Alert } from "@mui/material"; import { FlexRowContainer, Spacer } from "../base/flex"; import { Page } from "./page"; import { RoundedImage } from "../base/image"; import { api } from "../../hooks/use-api"; import { PageHeader } from "../base/page-header"; import { RatingForm } from "./rating-form"; -import { CriterionDTO, RatingDTO, ProjectDTO } from "../../api/types/dto"; +import { CriterionDTO, RatingDTO, ProjectDTO, SettingsDTO } from "../../api/types/dto"; import { useLoginContext } from "../../contexts/login-context"; +import { UserRole } from "../../api/types/enums"; /** * A settings dashboard to configure all parts of tilt. @@ -16,6 +18,7 @@ export const ReadOnlyProject = ({ project }: { project: ProjectDTO }) => { const [criteria, setCriteria] = React.useState([]); const [ratings, setRatings] = React.useState([]); + const [settings, setSettings] = React.useState>({}); React.useEffect(() => { api.getAllCriteria().then((criteria_) => { @@ -27,9 +30,17 @@ export const ReadOnlyProject = ({ project }: { project: ProjectDTO }) => { setRatings([...ratings_]); }); } + + api.getSettings().then((settings_) => { + setSettings(settings_); + }); }, [project]); const image = project?.image || project?.team.teamImg; + const userAllowedToRate = + user?.role === UserRole.Root || (user?.team != null && user?.admitted); + const ratingEnabled = + project?.allowRating && settings?.project?.allowRatingProjects; return ( @@ -48,7 +59,12 @@ export const ReadOnlyProject = ({ project }: { project: ProjectDTO }) => {

    {project?.description}

    - {user?.admitted && ( + {!userAllowedToRate && ratingEnabled && ( + + You need to be admitted and part of a team to rate other projects + + )} + {userAllowedToRate && ratingEnabled && (

    Rate this Project

    Hover criteria for more information. Rate a criterion high, if you From 844ce4f154bd35f512b9bc8e189119de9a71d4a0 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:38:31 +0200 Subject: [PATCH 36/36] prettier --- frontend/src/components/pages/read-only-project.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/pages/read-only-project.tsx b/frontend/src/components/pages/read-only-project.tsx index 63e5c31f..4ee7516f 100644 --- a/frontend/src/components/pages/read-only-project.tsx +++ b/frontend/src/components/pages/read-only-project.tsx @@ -6,7 +6,12 @@ import { RoundedImage } from "../base/image"; import { api } from "../../hooks/use-api"; import { PageHeader } from "../base/page-header"; import { RatingForm } from "./rating-form"; -import { CriterionDTO, RatingDTO, ProjectDTO, SettingsDTO } from "../../api/types/dto"; +import { + CriterionDTO, + RatingDTO, + ProjectDTO, + SettingsDTO, +} from "../../api/types/dto"; import { useLoginContext } from "../../contexts/login-context"; import { UserRole } from "../../api/types/enums";